diff --git a/resources/celerity/map.php b/resources/celerity/map.php index 12b63a615d..778c419638 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => 'a11c3643', + 'core.pkg.css' => 'c65b251d', 'core.pkg.js' => '47dc9ebb', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', @@ -67,7 +67,7 @@ return array( 'rsrc/css/application/differential/table-of-contents.css' => 'ae4b7a55', 'rsrc/css/application/diffusion/diffusion-icons.css' => '2941baf1', 'rsrc/css/application/diffusion/diffusion-readme.css' => '2106ea08', - 'rsrc/css/application/diffusion/diffusion-source.css' => '66fdf661', + 'rsrc/css/application/diffusion/diffusion-source.css' => '075ba788', 'rsrc/css/application/feed/feed.css' => 'ecd4ec57', 'rsrc/css/application/files/global-drag-and-drop.css' => '697324ad', 'rsrc/css/application/flag/flag.css' => '5337623f', @@ -147,7 +147,7 @@ return array( 'rsrc/css/phui/phui-status.css' => '888cedb8', 'rsrc/css/phui/phui-tag-view.css' => '402691cc', 'rsrc/css/phui/phui-text.css' => 'cf019f54', - 'rsrc/css/phui/phui-timeline-view.css' => 'f1bccf73', + 'rsrc/css/phui/phui-timeline-view.css' => '2efceff8', 'rsrc/css/phui/phui-two-column-view.css' => '39ecafb1', 'rsrc/css/phui/phui-workboard-view.css' => '6704d68d', 'rsrc/css/phui/phui-workpanel-view.css' => 'adec7699', @@ -521,7 +521,7 @@ return array( 'differential-table-of-contents-css' => 'ae4b7a55', 'diffusion-icons-css' => '2941baf1', 'diffusion-readme-css' => '2106ea08', - 'diffusion-source-css' => '66fdf661', + 'diffusion-source-css' => '075ba788', 'diviner-shared-css' => '5a337049', 'font-fontawesome' => 'd2fc4e8d', 'font-lato' => '5ab1a46a', @@ -797,7 +797,7 @@ return array( 'phui-tag-view-css' => '402691cc', 'phui-text-css' => 'cf019f54', 'phui-theme-css' => '6b451f24', - 'phui-timeline-view-css' => 'f1bccf73', + 'phui-timeline-view-css' => '2efceff8', 'phui-two-column-view-css' => '39ecafb1', 'phui-workboard-view-css' => '6704d68d', 'phui-workpanel-view-css' => 'adec7699', diff --git a/resources/sql/autopatches/20141107.phriction.policy.2.php b/resources/sql/autopatches/20141107.phriction.policy.2.php index a7cc6ca3e4..5e00bd7a40 100644 --- a/resources/sql/autopatches/20141107.phriction.policy.2.php +++ b/resources/sql/autopatches/20141107.phriction.policy.2.php @@ -24,12 +24,14 @@ foreach (new LiskMigrationIterator($table) as $doc) { $prefix = 'projects/'; if (($slug != $prefix) && (strncmp($slug, $prefix, strlen($prefix)) === 0)) { $parts = explode('/', $slug); - $project_slug = $parts[1].'/'; + + $project_slug = $parts[1]; + $project_slug = PhabricatorSlug::normalizeProjectSlug($project_slug); $project_slugs = array($project_slug); $project = id(new PhabricatorProjectQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withPhrictionSlugs($project_slugs) + ->withSlugs($project_slugs) ->executeOne(); if ($project) { diff --git a/resources/sql/autopatches/20151009.drydock.auth.1.sql b/resources/sql/autopatches/20151009.drydock.auth.1.sql new file mode 100644 index 0000000000..8e68977492 --- /dev/null +++ b/resources/sql/autopatches/20151009.drydock.auth.1.sql @@ -0,0 +1,14 @@ +CREATE TABLE {$NAMESPACE}_drydock.drydock_authorization ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + blueprintPHID VARBINARY(64) NOT NULL, + blueprintAuthorizationState VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}, + objectPHID VARBINARY(64) NOT NULL, + objectAuthorizationState VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (phid), + UNIQUE KEY `key_unique` (objectPHID, blueprintPHID), + KEY `key_blueprint` (blueprintPHID, blueprintAuthorizationState), + KEY `key_object` (objectPHID, objectAuthorizationState) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/autopatches/20151010.drydock.auth.2.sql b/resources/sql/autopatches/20151010.drydock.auth.2.sql new file mode 100644 index 0000000000..14c98b8b61 --- /dev/null +++ b/resources/sql/autopatches/20151010.drydock.auth.2.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_drydock.drydock_lease + ADD authorizingPHID VARBINARY(64) NOT NULL; diff --git a/resources/sql/autopatches/20151013.drydock.op.1.sql b/resources/sql/autopatches/20151013.drydock.op.1.sql new file mode 100644 index 0000000000..e5fed58afd --- /dev/null +++ b/resources/sql/autopatches/20151013.drydock.op.1.sql @@ -0,0 +1,16 @@ +CREATE TABLE {$NAMESPACE}_drydock.drydock_repositoryoperation ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + phid VARBINARY(64) NOT NULL, + authorPHID VARBINARY(64) NOT NULL, + objectPHID VARBINARY(64) NOT NULL, + repositoryPHID VARBINARY(64) NOT NULL, + repositoryTarget LONGBLOB NOT NULL, + operationType VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}, + operationState VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}, + properties LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + UNIQUE KEY `key_phid` (phid), + KEY `key_object` (objectPHID), + KEY `key_repository` (repositoryPHID, operationState) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/resources/sql/patches/090.forceuniqueprojectnames.php b/resources/sql/patches/090.forceuniqueprojectnames.php index a12de8ecfd..a3e029d50a 100644 --- a/resources/sql/patches/090.forceuniqueprojectnames.php +++ b/resources/sql/patches/090.forceuniqueprojectnames.php @@ -10,13 +10,14 @@ $projects = $table->loadAll(); $slug_map = array(); foreach ($projects as $project) { - $project->setPhrictionSlug($project->getName()); - $slug = $project->getPhrictionSlug(); - if ($slug == '/') { + $slug = PhabricatorSlug::normalizeProjectSlug($project->getName()); + + if (!strlen($slug)) { $project_id = $project->getID(); echo pht("Project #%d doesn't have a meaningful name...", $project_id)."\n"; $project->setName(trim(pht('Unnamed Project %s', $project->getName()))); } + $slug_map[$slug][] = $project->getID(); } @@ -47,8 +48,8 @@ while ($update) { foreach ($update as $key => $project) { $id = $project->getID(); $name = $project->getName(); - $project->setPhrictionSlug($name); - $slug = $project->getPhrictionSlug(); + + $slug = PhabricatorSlug::normalizeProjectSlug($name).'/'; echo pht("Updating project #%d '%s' (%s)... ", $id, $name, $slug); try { @@ -87,8 +88,8 @@ function rename_project($project, $projects) { $suffix = 2; while (true) { $new_name = $project->getName().' ('.$suffix.')'; - $project->setPhrictionSlug($new_name); - $new_slug = $project->getPhrictionSlug(); + + $new_slug = PhabricatorSlug::normalizeProjectSlug($new_name).'/'; $okay = true; foreach ($projects as $other) { diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 184a484929..89f65a8db0 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -467,6 +467,7 @@ phutil_register_library_map(array( 'DifferentialRevisionListController' => 'applications/differential/controller/DifferentialRevisionListController.php', 'DifferentialRevisionListView' => 'applications/differential/view/DifferentialRevisionListView.php', 'DifferentialRevisionMailReceiver' => 'applications/differential/mail/DifferentialRevisionMailReceiver.php', + 'DifferentialRevisionOperationController' => 'applications/differential/controller/DifferentialRevisionOperationController.php', 'DifferentialRevisionPHIDType' => 'applications/differential/phid/DifferentialRevisionPHIDType.php', 'DifferentialRevisionPackageHeraldField' => 'applications/differential/herald/DifferentialRevisionPackageHeraldField.php', 'DifferentialRevisionPackageOwnerHeraldField' => 'applications/differential/herald/DifferentialRevisionPackageOwnerHeraldField.php', @@ -607,6 +608,7 @@ phutil_register_library_map(array( 'DiffusionLowLevelGitRefQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelGitRefQuery.php', 'DiffusionLowLevelMercurialBranchesQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelMercurialBranchesQuery.php', 'DiffusionLowLevelMercurialPathsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelMercurialPathsQuery.php', + 'DiffusionLowLevelMercurialPathsQueryTests' => 'applications/diffusion/query/lowlevel/__tests__/DiffusionLowLevelMercurialPathsQueryTests.php', 'DiffusionLowLevelParentsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelParentsQuery.php', 'DiffusionLowLevelQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelQuery.php', 'DiffusionLowLevelResolveRefsQuery' => 'applications/diffusion/query/lowlevel/DiffusionLowLevelResolveRefsQuery.php', @@ -618,6 +620,7 @@ phutil_register_library_map(array( 'DiffusionMercurialServeSSHWorkflow' => 'applications/diffusion/ssh/DiffusionMercurialServeSSHWorkflow.php', 'DiffusionMercurialWireClientSSHProtocolChannel' => 'applications/diffusion/ssh/DiffusionMercurialWireClientSSHProtocolChannel.php', 'DiffusionMercurialWireProtocol' => 'applications/diffusion/protocol/DiffusionMercurialWireProtocol.php', + 'DiffusionMercurialWireProtocolTests' => 'applications/diffusion/protocol/__tests__/DiffusionMercurialWireProtocolTests.php', 'DiffusionMercurialWireSSHTestCase' => 'applications/diffusion/ssh/__tests__/DiffusionMercurialWireSSHTestCase.php', 'DiffusionMergedCommitsQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionMergedCommitsQueryConduitAPIMethod.php', 'DiffusionMirrorDeleteController' => 'applications/diffusion/controller/DiffusionMirrorDeleteController.php', @@ -686,6 +689,7 @@ phutil_register_library_map(array( 'DiffusionRepositoryDefaultController' => 'applications/diffusion/controller/DiffusionRepositoryDefaultController.php', 'DiffusionRepositoryEditActionsController' => 'applications/diffusion/controller/DiffusionRepositoryEditActionsController.php', 'DiffusionRepositoryEditActivateController' => 'applications/diffusion/controller/DiffusionRepositoryEditActivateController.php', + 'DiffusionRepositoryEditAutomationController' => 'applications/diffusion/controller/DiffusionRepositoryEditAutomationController.php', 'DiffusionRepositoryEditBasicController' => 'applications/diffusion/controller/DiffusionRepositoryEditBasicController.php', 'DiffusionRepositoryEditBranchesController' => 'applications/diffusion/controller/DiffusionRepositoryEditBranchesController.php', 'DiffusionRepositoryEditController' => 'applications/diffusion/controller/DiffusionRepositoryEditController.php', @@ -798,6 +802,14 @@ phutil_register_library_map(array( 'DoorkeeperTagsController' => 'applications/doorkeeper/controller/DoorkeeperTagsController.php', 'DrydockAlmanacServiceHostBlueprintImplementation' => 'applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php', 'DrydockApacheWebrootInterface' => 'applications/drydock/interface/webroot/DrydockApacheWebrootInterface.php', + 'DrydockAuthorization' => 'applications/drydock/storage/DrydockAuthorization.php', + 'DrydockAuthorizationAuthorizeController' => 'applications/drydock/controller/DrydockAuthorizationAuthorizeController.php', + 'DrydockAuthorizationListController' => 'applications/drydock/controller/DrydockAuthorizationListController.php', + 'DrydockAuthorizationListView' => 'applications/drydock/view/DrydockAuthorizationListView.php', + 'DrydockAuthorizationPHIDType' => 'applications/drydock/phid/DrydockAuthorizationPHIDType.php', + 'DrydockAuthorizationQuery' => 'applications/drydock/query/DrydockAuthorizationQuery.php', + 'DrydockAuthorizationSearchEngine' => 'applications/drydock/query/DrydockAuthorizationSearchEngine.php', + 'DrydockAuthorizationViewController' => 'applications/drydock/controller/DrydockAuthorizationViewController.php', 'DrydockBlueprint' => 'applications/drydock/storage/DrydockBlueprint.php', 'DrydockBlueprintController' => 'applications/drydock/controller/DrydockBlueprintController.php', 'DrydockBlueprintCoreCustomField' => 'applications/drydock/customfield/DrydockBlueprintCoreCustomField.php', @@ -828,6 +840,7 @@ phutil_register_library_map(array( 'DrydockDefaultViewCapability' => 'applications/drydock/capability/DrydockDefaultViewCapability.php', 'DrydockFilesystemInterface' => 'applications/drydock/interface/filesystem/DrydockFilesystemInterface.php', 'DrydockInterface' => 'applications/drydock/interface/DrydockInterface.php', + 'DrydockLandRepositoryOperation' => 'applications/drydock/operation/DrydockLandRepositoryOperation.php', 'DrydockLease' => 'applications/drydock/storage/DrydockLease.php', 'DrydockLeaseAcquiredLogType' => 'applications/drydock/logtype/DrydockLeaseAcquiredLogType.php', 'DrydockLeaseActivatedLogType' => 'applications/drydock/logtype/DrydockLeaseActivatedLogType.php', @@ -838,6 +851,8 @@ phutil_register_library_map(array( 'DrydockLeaseDestroyedLogType' => 'applications/drydock/logtype/DrydockLeaseDestroyedLogType.php', 'DrydockLeaseListController' => 'applications/drydock/controller/DrydockLeaseListController.php', 'DrydockLeaseListView' => 'applications/drydock/view/DrydockLeaseListView.php', + 'DrydockLeaseNoAuthorizationsLogType' => 'applications/drydock/logtype/DrydockLeaseNoAuthorizationsLogType.php', + 'DrydockLeaseNoBlueprintsLogType' => 'applications/drydock/logtype/DrydockLeaseNoBlueprintsLogType.php', 'DrydockLeasePHIDType' => 'applications/drydock/phid/DrydockLeasePHIDType.php', 'DrydockLeaseQuery' => 'applications/drydock/query/DrydockLeaseQuery.php', 'DrydockLeaseQueuedLogType' => 'applications/drydock/logtype/DrydockLeaseQueuedLogType.php', @@ -863,7 +878,16 @@ phutil_register_library_map(array( 'DrydockManagementUpdateLeaseWorkflow' => 'applications/drydock/management/DrydockManagementUpdateLeaseWorkflow.php', 'DrydockManagementUpdateResourceWorkflow' => 'applications/drydock/management/DrydockManagementUpdateResourceWorkflow.php', 'DrydockManagementWorkflow' => 'applications/drydock/management/DrydockManagementWorkflow.php', + 'DrydockObjectAuthorizationView' => 'applications/drydock/view/DrydockObjectAuthorizationView.php', 'DrydockQuery' => 'applications/drydock/query/DrydockQuery.php', + 'DrydockRepositoryOperation' => 'applications/drydock/storage/DrydockRepositoryOperation.php', + 'DrydockRepositoryOperationListController' => 'applications/drydock/controller/DrydockRepositoryOperationListController.php', + 'DrydockRepositoryOperationPHIDType' => 'applications/drydock/phid/DrydockRepositoryOperationPHIDType.php', + 'DrydockRepositoryOperationQuery' => 'applications/drydock/query/DrydockRepositoryOperationQuery.php', + 'DrydockRepositoryOperationSearchEngine' => 'applications/drydock/query/DrydockRepositoryOperationSearchEngine.php', + 'DrydockRepositoryOperationType' => 'applications/drydock/operation/DrydockRepositoryOperationType.php', + 'DrydockRepositoryOperationUpdateWorker' => 'applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php', + 'DrydockRepositoryOperationViewController' => 'applications/drydock/controller/DrydockRepositoryOperationViewController.php', 'DrydockResource' => 'applications/drydock/storage/DrydockResource.php', 'DrydockResourceActivationFailureLogType' => 'applications/drydock/logtype/DrydockResourceActivationFailureLogType.php', 'DrydockResourceActivationYieldLogType' => 'applications/drydock/logtype/DrydockResourceActivationYieldLogType.php', @@ -1437,6 +1461,7 @@ phutil_register_library_map(array( 'PHUIPropertyListExample' => 'applications/uiexample/examples/PHUIPropertyListExample.php', 'PHUIPropertyListView' => 'view/phui/PHUIPropertyListView.php', 'PHUIRemarkupPreviewPanel' => 'view/phui/PHUIRemarkupPreviewPanel.php', + 'PHUIRemarkupView' => 'infrastructure/markup/view/PHUIRemarkupView.php', 'PHUISpacesNamespaceContextView' => 'applications/spaces/view/PHUISpacesNamespaceContextView.php', 'PHUIStatusItemView' => 'view/phui/PHUIStatusItemView.php', 'PHUIStatusListView' => 'view/phui/PHUIStatusListView.php', @@ -2945,6 +2970,7 @@ phutil_register_library_map(array( 'PhabricatorSpacesTestCase' => 'applications/spaces/__tests__/PhabricatorSpacesTestCase.php', 'PhabricatorSpacesViewController' => 'applications/spaces/controller/PhabricatorSpacesViewController.php', 'PhabricatorStandardCustomField' => 'infrastructure/customfield/standard/PhabricatorStandardCustomField.php', + 'PhabricatorStandardCustomFieldBlueprints' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldBlueprints.php', 'PhabricatorStandardCustomFieldBool' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php', 'PhabricatorStandardCustomFieldCredential' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldCredential.php', 'PhabricatorStandardCustomFieldDatasource' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldDatasource.php', @@ -4190,6 +4216,7 @@ phutil_register_library_map(array( 'DifferentialRevisionListController' => 'DifferentialController', 'DifferentialRevisionListView' => 'AphrontView', 'DifferentialRevisionMailReceiver' => 'PhabricatorObjectMailReceiver', + 'DifferentialRevisionOperationController' => 'DifferentialController', 'DifferentialRevisionPHIDType' => 'PhabricatorPHIDType', 'DifferentialRevisionPackageHeraldField' => 'DifferentialRevisionHeraldField', 'DifferentialRevisionPackageOwnerHeraldField' => 'DifferentialRevisionHeraldField', @@ -4330,6 +4357,7 @@ phutil_register_library_map(array( 'DiffusionLowLevelGitRefQuery' => 'DiffusionLowLevelQuery', 'DiffusionLowLevelMercurialBranchesQuery' => 'DiffusionLowLevelQuery', 'DiffusionLowLevelMercurialPathsQuery' => 'DiffusionLowLevelQuery', + 'DiffusionLowLevelMercurialPathsQueryTests' => 'PhabricatorTestCase', 'DiffusionLowLevelParentsQuery' => 'DiffusionLowLevelQuery', 'DiffusionLowLevelQuery' => 'Phobject', 'DiffusionLowLevelResolveRefsQuery' => 'DiffusionLowLevelQuery', @@ -4341,6 +4369,7 @@ phutil_register_library_map(array( 'DiffusionMercurialServeSSHWorkflow' => 'DiffusionMercurialSSHWorkflow', 'DiffusionMercurialWireClientSSHProtocolChannel' => 'PhutilProtocolChannel', 'DiffusionMercurialWireProtocol' => 'Phobject', + 'DiffusionMercurialWireProtocolTests' => 'PhabricatorTestCase', 'DiffusionMercurialWireSSHTestCase' => 'PhabricatorTestCase', 'DiffusionMergedCommitsQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionMirrorDeleteController' => 'DiffusionController', @@ -4409,6 +4438,7 @@ phutil_register_library_map(array( 'DiffusionRepositoryDefaultController' => 'DiffusionController', 'DiffusionRepositoryEditActionsController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditActivateController' => 'DiffusionRepositoryEditController', + 'DiffusionRepositoryEditAutomationController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditBasicController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditBranchesController' => 'DiffusionRepositoryEditController', 'DiffusionRepositoryEditController' => 'DiffusionController', @@ -4535,6 +4565,17 @@ phutil_register_library_map(array( 'DoorkeeperTagsController' => 'PhabricatorController', 'DrydockAlmanacServiceHostBlueprintImplementation' => 'DrydockBlueprintImplementation', 'DrydockApacheWebrootInterface' => 'DrydockWebrootInterface', + 'DrydockAuthorization' => array( + 'DrydockDAO', + 'PhabricatorPolicyInterface', + ), + 'DrydockAuthorizationAuthorizeController' => 'DrydockController', + 'DrydockAuthorizationListController' => 'DrydockController', + 'DrydockAuthorizationListView' => 'AphrontView', + 'DrydockAuthorizationPHIDType' => 'PhabricatorPHIDType', + 'DrydockAuthorizationQuery' => 'DrydockQuery', + 'DrydockAuthorizationSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'DrydockAuthorizationViewController' => 'DrydockController', 'DrydockBlueprint' => array( 'DrydockDAO', 'PhabricatorApplicationTransactionInterface', @@ -4576,6 +4617,7 @@ phutil_register_library_map(array( 'DrydockDefaultViewCapability' => 'PhabricatorPolicyCapability', 'DrydockFilesystemInterface' => 'DrydockInterface', 'DrydockInterface' => 'Phobject', + 'DrydockLandRepositoryOperation' => 'DrydockRepositoryOperationType', 'DrydockLease' => array( 'DrydockDAO', 'PhabricatorPolicyInterface', @@ -4589,6 +4631,8 @@ phutil_register_library_map(array( 'DrydockLeaseDestroyedLogType' => 'DrydockLogType', 'DrydockLeaseListController' => 'DrydockLeaseController', 'DrydockLeaseListView' => 'AphrontView', + 'DrydockLeaseNoAuthorizationsLogType' => 'DrydockLogType', + 'DrydockLeaseNoBlueprintsLogType' => 'DrydockLogType', 'DrydockLeasePHIDType' => 'PhabricatorPHIDType', 'DrydockLeaseQuery' => 'DrydockQuery', 'DrydockLeaseQueuedLogType' => 'DrydockLogType', @@ -4617,7 +4661,19 @@ phutil_register_library_map(array( 'DrydockManagementUpdateLeaseWorkflow' => 'DrydockManagementWorkflow', 'DrydockManagementUpdateResourceWorkflow' => 'DrydockManagementWorkflow', 'DrydockManagementWorkflow' => 'PhabricatorManagementWorkflow', + 'DrydockObjectAuthorizationView' => 'AphrontView', 'DrydockQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'DrydockRepositoryOperation' => array( + 'DrydockDAO', + 'PhabricatorPolicyInterface', + ), + 'DrydockRepositoryOperationListController' => 'DrydockController', + 'DrydockRepositoryOperationPHIDType' => 'PhabricatorPHIDType', + 'DrydockRepositoryOperationQuery' => 'DrydockQuery', + 'DrydockRepositoryOperationSearchEngine' => 'PhabricatorApplicationSearchEngine', + 'DrydockRepositoryOperationType' => 'Phobject', + 'DrydockRepositoryOperationUpdateWorker' => 'DrydockWorker', + 'DrydockRepositoryOperationViewController' => 'DrydockController', 'DrydockResource' => array( 'DrydockDAO', 'PhabricatorPolicyInterface', @@ -5301,6 +5357,7 @@ phutil_register_library_map(array( 'PHUIPropertyListExample' => 'PhabricatorUIExample', 'PHUIPropertyListView' => 'AphrontView', 'PHUIRemarkupPreviewPanel' => 'AphrontTagView', + 'PHUIRemarkupView' => 'AphrontView', 'PHUISpacesNamespaceContextView' => 'AphrontView', 'PHUIStatusItemView' => 'AphrontTagView', 'PHUIStatusListView' => 'AphrontTagView', @@ -7089,6 +7146,7 @@ phutil_register_library_map(array( 'PhabricatorSpacesTestCase' => 'PhabricatorTestCase', 'PhabricatorSpacesViewController' => 'PhabricatorSpacesController', 'PhabricatorStandardCustomField' => 'PhabricatorCustomField', + 'PhabricatorStandardCustomFieldBlueprints' => 'PhabricatorStandardCustomFieldTokenizer', 'PhabricatorStandardCustomFieldBool' => 'PhabricatorStandardCustomField', 'PhabricatorStandardCustomFieldCredential' => 'PhabricatorStandardCustomField', 'PhabricatorStandardCustomFieldDatasource' => 'PhabricatorStandardCustomFieldTokenizer', diff --git a/src/aphront/configuration/AphrontApplicationConfiguration.php b/src/aphront/configuration/AphrontApplicationConfiguration.php index 50b8c123fd..b48ea3be13 100644 --- a/src/aphront/configuration/AphrontApplicationConfiguration.php +++ b/src/aphront/configuration/AphrontApplicationConfiguration.php @@ -372,6 +372,13 @@ abstract class AphrontApplicationConfiguration extends Phobject { $result = $this->routePath($maps, $path.'/'); if ($result) { $slash_uri = $request->getRequestURI()->setPath($path.'/'); + + // We need to restore URI encoding because the webserver has + // interpreted it. For example, this allows us to redirect a path + // like `/tag/aa%20bb` to `/tag/aa%20bb/`, which may eventually be + // resolved meaningfully by an application. + $slash_uri = phutil_escape_uri($slash_uri); + $external = strlen($request->getRequestURI()->getDomain()); return $this->buildRedirectController($slash_uri, $external); } diff --git a/src/applications/badges/controller/PhabricatorBadgesViewController.php b/src/applications/badges/controller/PhabricatorBadgesViewController.php index abaafe6d0f..411970f0bd 100644 --- a/src/applications/badges/controller/PhabricatorBadgesViewController.php +++ b/src/applications/badges/controller/PhabricatorBadgesViewController.php @@ -104,14 +104,10 @@ final class PhabricatorBadgesViewController $description = $badge->getDescription(); if (strlen($description)) { - $description = PhabricatorMarkupEngine::renderOneObject( - id(new PhabricatorMarkupOneOff())->setContent($description), - 'default', - $viewer); - $view->addSectionHeader( pht('Description'), PHUIPropertyListView::ICON_SUMMARY); - $view->addTextContent($description); + $view->addTextContent( + new PHUIRemarkupView($viewer, $description)); } $badge = id(new PHUIBadgeView()) diff --git a/src/applications/conduit/controller/PhabricatorConduitConsoleController.php b/src/applications/conduit/controller/PhabricatorConduitConsoleController.php index 79312b5480..e570861f29 100644 --- a/src/applications/conduit/controller/PhabricatorConduitConsoleController.php +++ b/src/applications/conduit/controller/PhabricatorConduitConsoleController.php @@ -195,15 +195,10 @@ final class PhabricatorConduitConsoleController pht('Errors'), $error_description); - - $description = $method->getMethodDescription(); - $description = PhabricatorMarkupEngine::renderOneObject( - id(new PhabricatorMarkupOneOff())->setContent($description), - 'default', - $viewer); $view->addSectionHeader( pht('Description'), PHUIPropertyListView::ICON_SUMMARY); - $view->addTextContent($description); + $view->addTextContent( + new PHUIRemarkupView($viewer, $method->getMethodDescription())); return $view; } diff --git a/src/applications/config/check/PhabricatorPygmentSetupCheck.php b/src/applications/config/check/PhabricatorPygmentSetupCheck.php index 228eb7a33b..0ffb4ff7ad 100644 --- a/src/applications/config/check/PhabricatorPygmentSetupCheck.php +++ b/src/applications/config/check/PhabricatorPygmentSetupCheck.php @@ -45,8 +45,8 @@ final class PhabricatorPygmentSetupCheck extends PhabricatorSetupCheck { 'Phabricator has %s available in %s, but the binary '. 'exited with an error code when run as %s. Check that it is '. 'installed correctly.', - phutil_tag('tt', array(), '$PATH'), phutil_tag('tt', array(), 'pygmentize'), + phutil_tag('tt', array(), '$PATH'), phutil_tag('tt', array(), 'pygmentize -h')); $this diff --git a/src/applications/differential/application/PhabricatorDifferentialApplication.php b/src/applications/differential/application/PhabricatorDifferentialApplication.php index 2cc611027f..9203d0c522 100644 --- a/src/applications/differential/application/PhabricatorDifferentialApplication.php +++ b/src/applications/differential/application/PhabricatorDifferentialApplication.php @@ -75,6 +75,8 @@ final class PhabricatorDifferentialApplication extends PhabricatorApplication { => 'DifferentialRevisionCloseDetailsController', 'update/(?P[1-9]\d*)/' => 'DifferentialDiffCreateController', + 'operation/(?P[1-9]\d*)/' + => 'DifferentialRevisionOperationController', ), 'comment/' => array( 'preview/(?P[1-9]\d*)/' => 'DifferentialCommentPreviewController', diff --git a/src/applications/differential/controller/DifferentialCommentPreviewController.php b/src/applications/differential/controller/DifferentialCommentPreviewController.php index 66d1cc9338..706c2dcaf2 100644 --- a/src/applications/differential/controller/DifferentialCommentPreviewController.php +++ b/src/applications/differential/controller/DifferentialCommentPreviewController.php @@ -3,19 +3,13 @@ final class DifferentialCommentPreviewController extends DifferentialController { - private $id; - - public function willProcessRequest(array $data) { - $this->id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); + $id = $request->getURIData('id'); $revision = id(new DifferentialRevisionQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->executeOne(); if (!$revision) { return new Aphront404Response(); @@ -119,7 +113,7 @@ final class DifferentialCommentPreviewController $metadata['action'] = $action; } - $draft_key = 'differential-comment-'.$this->id; + $draft_key = 'differential-comment-'.$id; $draft = id(new PhabricatorDraft()) ->setAuthorPHID($viewer->getPHID()) ->setDraftKey($draft_key) diff --git a/src/applications/differential/controller/DifferentialCommentSaveController.php b/src/applications/differential/controller/DifferentialCommentSaveController.php index 7d918a792c..831d7c90c8 100644 --- a/src/applications/differential/controller/DifferentialCommentSaveController.php +++ b/src/applications/differential/controller/DifferentialCommentSaveController.php @@ -3,15 +3,9 @@ final class DifferentialCommentSaveController extends DifferentialController { - private $id; - - public function willProcessRequest(array $data) { - $this->id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); + $id = $request->getURIData('id'); if (!$request->isFormPost()) { return new Aphront400Response(); @@ -19,7 +13,7 @@ final class DifferentialCommentSaveController $revision = id(new DifferentialRevisionQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->needReviewerStatus(true) ->needReviewerAuthority(true) ->executeOne(); diff --git a/src/applications/differential/controller/DifferentialDiffViewController.php b/src/applications/differential/controller/DifferentialDiffViewController.php index 6ffd57396c..716a183b5b 100644 --- a/src/applications/differential/controller/DifferentialDiffViewController.php +++ b/src/applications/differential/controller/DifferentialDiffViewController.php @@ -2,23 +2,17 @@ final class DifferentialDiffViewController extends DifferentialController { - private $id; - public function shouldAllowPublic() { return true; } - public function willProcessRequest(array $data) { - $this->id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); + $id = $request->getURIData('id'); $diff = id(new DifferentialDiffQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->executeOne(); if (!$diff) { return new Aphront404Response(); diff --git a/src/applications/differential/controller/DifferentialRevisionCloseDetailsController.php b/src/applications/differential/controller/DifferentialRevisionCloseDetailsController.php index 25051bcbb5..d5a7d897a8 100644 --- a/src/applications/differential/controller/DifferentialRevisionCloseDetailsController.php +++ b/src/applications/differential/controller/DifferentialRevisionCloseDetailsController.php @@ -3,20 +3,11 @@ final class DifferentialRevisionCloseDetailsController extends DifferentialController { - private $phid; - - public function willProcessRequest(array $data) { - $this->phid = idx($data, 'phid'); - } - - public function processRequest() { - $request = $this->getRequest(); - - $viewer = $request->getUser(); - $xaction_phid = $this->phid; + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); $xaction = id(new PhabricatorObjectQuery()) - ->withPHIDs(array($xaction_phid)) + ->withPHIDs(array($request->getURIData('phid'))) ->setViewer($viewer) ->executeOne(); if (!$xaction) { diff --git a/src/applications/differential/controller/DifferentialRevisionEditController.php b/src/applications/differential/controller/DifferentialRevisionEditController.php index 1c664f07d2..a52538ca55 100644 --- a/src/applications/differential/controller/DifferentialRevisionEditController.php +++ b/src/applications/differential/controller/DifferentialRevisionEditController.php @@ -3,24 +3,18 @@ final class DifferentialRevisionEditController extends DifferentialController { - private $id; + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); + $id = $request->getURIData('id'); - public function willProcessRequest(array $data) { - $this->id = idx($data, 'id'); - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); - - if (!$this->id) { - $this->id = $request->getInt('revisionID'); + if (!$id) { + $id = $request->getInt('revisionID'); } - if ($this->id) { + if ($id) { $revision = id(new DifferentialRevisionQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->needRelationships(true) ->needReviewerStatus(true) ->needActiveDiffs(true) diff --git a/src/applications/differential/controller/DifferentialRevisionLandController.php b/src/applications/differential/controller/DifferentialRevisionLandController.php index e19762b44d..4f8787956f 100644 --- a/src/applications/differential/controller/DifferentialRevisionLandController.php +++ b/src/applications/differential/controller/DifferentialRevisionLandController.php @@ -11,9 +11,8 @@ final class DifferentialRevisionLandController extends DifferentialController { $this->strategyClass = $data['strategy']; } - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); $revision_id = $this->revisionID; diff --git a/src/applications/differential/controller/DifferentialRevisionListController.php b/src/applications/differential/controller/DifferentialRevisionListController.php index e48b088e6b..29567a53bf 100644 --- a/src/applications/differential/controller/DifferentialRevisionListController.php +++ b/src/applications/differential/controller/DifferentialRevisionListController.php @@ -2,19 +2,13 @@ final class DifferentialRevisionListController extends DifferentialController { - private $queryKey; - public function shouldAllowPublic() { return true; } - public function willProcessRequest(array $data) { - $this->queryKey = idx($data, 'queryKey'); - } - - public function processRequest() { + public function handleRequest(AphrontRequest $request) { $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($this->queryKey) + ->setQueryKey($request->getURIData('queryKey')) ->setSearchEngine(new DifferentialRevisionSearchEngine()) ->setNavigation($this->buildSideNavView()); diff --git a/src/applications/differential/controller/DifferentialRevisionOperationController.php b/src/applications/differential/controller/DifferentialRevisionOperationController.php new file mode 100644 index 0000000000..6ec5b8edec --- /dev/null +++ b/src/applications/differential/controller/DifferentialRevisionOperationController.php @@ -0,0 +1,113 @@ +getViewer(); + $id = $request->getURIData('id'); + + $revision = id(new DifferentialRevisionQuery()) + ->withIDs(array($id)) + ->setViewer($viewer) + ->needActiveDiffs(true) + ->executeOne(); + if (!$revision) { + return new Aphront404Response(); + } + + $detail_uri = "/D{$id}"; + + $repository = $revision->getRepository(); + if (!$repository) { + return $this->rejectOperation( + $revision, + pht('No Repository'), + pht( + 'This revision is not associated with a known repository. Only '. + 'revisions associated with a tracked repository can be landed '. + 'automatically.')); + } + + if (!$repository->canPerformAutomation()) { + return $this->rejectOperation( + $revision, + pht('No Repository Automation'), + pht( + 'The repository this revision is associated with ("%s") is not '. + 'configured to support automation. Configure automation for the '. + 'repository to enable revisions to be landed automatically.', + $repository->getMonogram())); + } + + // TODO: At some point we should allow installs to give "land reviewed + // code" permission to more users than "push any commit", because it is + // a much less powerful operation. For now, just require push so this + // doesn't do anything users can't do on their own. + $can_push = PhabricatorPolicyFilter::hasCapability( + $viewer, + $repository, + DiffusionPushCapability::CAPABILITY); + if (!$can_push) { + return $this->rejectOperation( + $revision, + pht('Unable to Push'), + pht( + 'You do not have permission to push to the repository this '. + 'revision is associated with ("%s"), so you can not land it.', + $repository->getMonogram())); + } + + if ($request->isFormPost()) { + // NOTE: The operation is locked to the current active diff, so if the + // revision is updated before the operation applies nothing sneaky + // occurs. + + $diff = $revision->getActiveDiff(); + + $op = new DrydockLandRepositoryOperation(); + + $operation = DrydockRepositoryOperation::initializeNewOperation($op) + ->setAuthorPHID($viewer->getPHID()) + ->setObjectPHID($revision->getPHID()) + ->setRepositoryPHID($repository->getPHID()) + ->setRepositoryTarget('branch:master') + ->setProperty('differential.diffPHID', $diff->getPHID()); + + $operation->save(); + $operation->scheduleUpdate(); + + return id(new AphrontRedirectResponse()) + ->setURI($detail_uri); + } + + return $this->newDialog() + ->setTitle(pht('Land Revision')) + ->appendParagraph( + pht( + 'In theory, this will do approximately what `arc land` would do. '. + 'In practice, that is almost certainly not what it will actually '. + 'do.')) + ->appendParagraph( + pht( + 'THIS FEATURE IS EXPERIMENTAL AND DANGEROUS! USE IT AT YOUR '. + 'OWN RISK!')) + ->addCancelButton($detail_uri) + ->addSubmitButton(pht('Mutate Repository Unpredictably')); + } + + private function rejectOperation( + DifferentialRevision $revision, + $title, + $body) { + + $id = $revision->getID(); + $detail_uri = "/D{$id}"; + + return $this->newDialog() + ->setTitle($title) + ->appendParagraph($body) + ->addCancelButton($detail_uri); + } + +} diff --git a/src/applications/differential/controller/DifferentialRevisionViewController.php b/src/applications/differential/controller/DifferentialRevisionViewController.php index 4af487252c..2e1cb4c931 100644 --- a/src/applications/differential/controller/DifferentialRevisionViewController.php +++ b/src/applications/differential/controller/DifferentialRevisionViewController.php @@ -8,15 +8,11 @@ final class DifferentialRevisionViewController extends DifferentialController { return true; } - public function willProcessRequest(array $data) { - $this->revisionID = $data['id']; - } + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); + $this->revisionID = $request->getURIData('id'); - public function processRequest() { - - $request = $this->getRequest(); - $user = $request->getUser(); - $viewer_is_anonymous = !$user->isLoggedIn(); + $viewer_is_anonymous = !$viewer->isLoggedIn(); $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($this->revisionID)) @@ -68,7 +64,7 @@ final class DifferentialRevisionViewController extends DifferentialController { $repository = $revision->getRepository(); } else { $repository = id(new PhabricatorRepositoryQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withPHIDs(array($repository_phid)) ->executeOne(); } @@ -117,7 +113,7 @@ final class DifferentialRevisionViewController extends DifferentialController { $revision->loadCommitPHIDs(), array( $revision->getAuthorPHID(), - $user->getPHID(), + $viewer->getPHID(), )); foreach ($revision->getAttached() as $type => $phids) { @@ -130,7 +126,7 @@ final class DifferentialRevisionViewController extends DifferentialController { $revision, PhabricatorCustomField::ROLE_VIEW); - $field_list->setViewer($user); + $field_list->setViewer($viewer); $field_list->readFieldsFromStorage($revision); $warning_handle_map = array(); @@ -174,7 +170,7 @@ final class DifferentialRevisionViewController extends DifferentialController { $new = array_select_keys($changesets, $new_ids); $query = id(new DifferentialInlineCommentQuery()) - ->setViewer($user) + ->setViewer($viewer) ->needHidden(true) ->withRevisionPHIDs(array($revision->getPHID())); $inlines = $query->execute(); @@ -205,7 +201,7 @@ final class DifferentialRevisionViewController extends DifferentialController { $commit_hashes = array_unique(array_filter($commit_hashes)); if ($commit_hashes) { $commits_for_links = id(new DiffusionCommitQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withIdentifiers($commit_hashes) ->execute(); $commits_for_links = mpull( @@ -217,7 +213,7 @@ final class DifferentialRevisionViewController extends DifferentialController { } $revision_detail = id(new DifferentialRevisionDetailView()) - ->setUser($user) + ->setUser($viewer) ->setRevision($revision) ->setDiff(end($diffs)) ->setCustomFields($field_list) @@ -239,7 +235,7 @@ final class DifferentialRevisionViewController extends DifferentialController { } $revision_detail->setActions($actions); - $revision_detail->setUser($user); + $revision_detail->setUser($viewer); $revision_detail_box = $revision_detail->render(); @@ -261,7 +257,7 @@ final class DifferentialRevisionViewController extends DifferentialController { $detail_diffs = mpull($detail_diffs, null, 'getPHID'); $buildables = id(new HarbormasterBuildableQuery()) - ->setViewer($user) + ->setViewer($viewer) ->withBuildablePHIDs(array_keys($detail_diffs)) ->withManualBuildables(false) ->needBuilds(true) @@ -311,7 +307,7 @@ final class DifferentialRevisionViewController extends DifferentialController { '/differential/changeset/?view=old', '/differential/changeset/?view=new'); - $changeset_view->setUser($user); + $changeset_view->setUser($viewer); $changeset_view->setDiff($target); $changeset_view->setRenderingReferences($rendering_references); $changeset_view->setVsMap($vs_map); @@ -323,7 +319,7 @@ final class DifferentialRevisionViewController extends DifferentialController { $changeset_view->setTitle(pht('Diff %s', $target->getID())); $diff_history = id(new DifferentialRevisionUpdateHistoryView()) - ->setUser($user) + ->setUser($viewer) ->setDiffs($diffs) ->setSelectedVersusDiffID($diff_vs) ->setSelectedDiffID($target->getID()) @@ -331,7 +327,7 @@ final class DifferentialRevisionViewController extends DifferentialController { ->setCommitsForLinks($commits_for_links); $local_view = id(new DifferentialLocalCommitsView()) - ->setUser($user) + ->setUser($viewer) ->setLocalCommits(idx($props, 'local:commits')) ->setCommitsForLinks($commits_for_links); @@ -352,13 +348,13 @@ final class DifferentialRevisionViewController extends DifferentialController { $toc_view = $this->buildTableOfContents( $changesets, $visible_changesets, - $target->loadCoverageMap($user)); + $target->loadCoverageMap($viewer)); $comment_form = null; if (!$viewer_is_anonymous) { $draft = id(new PhabricatorDraft())->loadOneWhere( 'authorPHID = %s AND draftKey = %s', - $user->getPHID(), + $viewer->getPHID(), 'differential-comment-'.$revision->getID()); $reviewers = array(); @@ -394,7 +390,7 @@ final class DifferentialRevisionViewController extends DifferentialController { 'comment/save/'.$revision->getID().'/'); $comment_form->setActionURI($action_uri); - $comment_form->setUser($user); + $comment_form->setUser($viewer); $comment_form->setDraft($draft); $comment_form->setReviewers(mpull($reviewers, 'getFullName', 'getPHID')); $comment_form->setCCs(mpull($ccs, 'getFullName', 'getPHID')); @@ -461,13 +457,16 @@ final class DifferentialRevisionViewController extends DifferentialController { // TODO: For now, just use this to get "Login to Comment". $page_pane->appendChild( id(new PhabricatorApplicationTransactionCommentView()) - ->setUser($user) + ->setUser($viewer) ->setRequestURI($request->getRequestURI())); } $object_id = 'D'.$revision->getID(); + $operations_box = $this->buildOperationsBox($revision); + $content = array( + $operations_box, $revision_detail_box, $diff_detail_box, $page_pane, @@ -476,7 +475,7 @@ final class DifferentialRevisionViewController extends DifferentialController { $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($object_id, '/'.$object_id); - $prefs = $user->loadPreferences(); + $prefs = $viewer->loadPreferences(); $pref_filetree = PhabricatorUserPreferences::PREFERENCE_DIFF_FILETREE; if ($prefs->getPreference($pref_filetree)) { @@ -1036,4 +1035,55 @@ final class DifferentialRevisionViewController extends DifferentialController { return $view; } + private function buildOperationsBox(DifferentialRevision $revision) { + $viewer = $this->getViewer(); + + // Save a query if we can't possibly have pending operations. + $repository = $revision->getRepository(); + if (!$repository || !$repository->canPerformAutomation()) { + return null; + } + + $operations = id(new DrydockRepositoryOperationQuery()) + ->setViewer($viewer) + ->withObjectPHIDs(array($revision->getPHID())) + ->withOperationStates( + array( + DrydockRepositoryOperation::STATE_WAIT, + DrydockRepositoryOperation::STATE_WORK, + DrydockRepositoryOperation::STATE_FAIL, + )) + ->execute(); + if (!$operations) { + return null; + } + + $operation = head(msort($operations, 'getID')); + + // TODO: This is completely made up for now, give it useful information and + // a sweet progress bar. + + switch ($operation->getOperationState()) { + case DrydockRepositoryOperation::STATE_WAIT: + case DrydockRepositoryOperation::STATE_WORK: + $severity = PHUIInfoView::SEVERITY_NOTICE; + $text = pht( + 'Some sort of repository operation is currently running.'); + break; + default: + $severity = PHUIInfoView::SEVERITY_ERROR; + $text = pht( + 'Some sort of repository operation failed.'); + break; + } + + $info_view = id(new PHUIInfoView()) + ->setSeverity($severity) + ->appendChild($text); + + return id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Active Operations (EXPERIMENTAL!)')) + ->setInfoView($info_view); + } + } diff --git a/src/applications/differential/landing/DifferentialLandingActionMenuEventListener.php b/src/applications/differential/landing/DifferentialLandingActionMenuEventListener.php index ad713db943..7dcf3f462d 100644 --- a/src/applications/differential/landing/DifferentialLandingActionMenuEventListener.php +++ b/src/applications/differential/landing/DifferentialLandingActionMenuEventListener.php @@ -37,6 +37,18 @@ final class DifferentialLandingActionMenuEventListener return null; } + if ($repository->canPerformAutomation()) { + $revision_id = $revision->getID(); + + $action = id(new PhabricatorActionView()) + ->setWorkflow(true) + ->setName(pht('Land Revision')) + ->setIcon('fa-fighter-jet') + ->setHref("/differential/revision/operation/{$revision_id}/"); + + $this->addActionMenuItems($event, $action); + } + $strategies = id(new PhutilClassMapQuery()) ->setAncestorClass('DifferentialLandingStrategy') ->execute(); diff --git a/src/applications/differential/storage/DifferentialDiff.php b/src/applications/differential/storage/DifferentialDiff.php index 7b3a31635a..42bee674ba 100644 --- a/src/applications/differential/storage/DifferentialDiff.php +++ b/src/applications/differential/storage/DifferentialDiff.php @@ -251,6 +251,12 @@ final class DifferentialDiff $dict['changes'] = $this->buildChangesList(); + return $dict + $this->getDiffAuthorshipDict(); + } + + public function getDiffAuthorshipDict() { + $dict = array(); + $properties = id(new DifferentialDiffProperty())->loadAllWhere( 'diffID = %d', $this->getID()); @@ -447,12 +453,8 @@ final class DifferentialDiff $results['repository.vcs'] = $repo->getVersionControlSystem(); $results['repository.uri'] = $repo->getPublicCloneURI(); - // TODO: We're just hoping to get lucky. Instead, `arc` should store - // where it sent changes and we should only provide staging details - // if we reasonably believe they are accurate. - $staging_ref = 'refs/tags/phabricator/diff/'.$this->getID(); $results['repository.staging.uri'] = $repo->getStagingURI(); - $results['repository.staging.ref'] = $staging_ref; + $results['repository.staging.ref'] = $this->getStagingRef(); } } @@ -480,6 +482,13 @@ final class DifferentialDiff ); } + public function getStagingRef() { + // TODO: We're just hoping to get lucky. Instead, `arc` should store + // where it sent changes and we should only provide staging details + // if we reasonably believe they are accurate. + return 'refs/tags/phabricator/diff/'.$this->getID(); + } + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php index 0314318643..09a5b35f19 100644 --- a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php +++ b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php @@ -102,6 +102,7 @@ final class PhabricatorDiffusionApplication extends PhabricatorApplication { 'update/' => 'DiffusionRepositoryEditUpdateController', 'symbol/' => 'DiffusionRepositorySymbolsController', 'staging/' => 'DiffusionRepositoryEditStagingController', + 'automation/' => 'DiffusionRepositoryEditAutomationController', ), 'pathtree/(?P.*)' => 'DiffusionPathTreeController', 'mirror/' => array( diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditAutomationController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditAutomationController.php new file mode 100644 index 0000000000..1c94e8bbc5 --- /dev/null +++ b/src/applications/diffusion/controller/DiffusionRepositoryEditAutomationController.php @@ -0,0 +1,94 @@ +getUser(); + $drequest = $this->diffusionRequest; + $repository = $drequest->getRepository(); + + $repository = id(new PhabricatorRepositoryQuery()) + ->setViewer($viewer) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->withIDs(array($repository->getID())) + ->executeOne(); + if (!$repository) { + return new Aphront404Response(); + } + + if (!$repository->supportsAutomation()) { + return new Aphront404Response(); + } + + $edit_uri = $this->getRepositoryControllerURI($repository, 'edit/'); + + $v_blueprints = $repository->getHumanReadableDetail( + 'automation.blueprintPHIDs'); + + if ($request->isFormPost()) { + $v_blueprints = $request->getArr('blueprintPHIDs'); + + $xactions = array(); + $template = id(new PhabricatorRepositoryTransaction()); + + $type_blueprints = + PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS; + + $xactions[] = id(clone $template) + ->setTransactionType($type_blueprints) + ->setNewValue($v_blueprints); + + id(new PhabricatorRepositoryEditor()) + ->setContinueOnNoEffect(true) + ->setContentSourceFromRequest($request) + ->setActor($viewer) + ->applyTransactions($repository, $xactions); + + return id(new AphrontRedirectResponse())->setURI($edit_uri); + } + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb(pht('Edit Automation')); + + $title = pht('Edit %s', $repository->getName()); + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->appendRemarkupInstructions( + pht( + "Configure **Repository Automation** to allow Phabricator to ". + "write to this repository.". + "\n\n". + "IMPORTANT: This feature is new, experimental, and not supported. ". + "Use it at your own risk.")) + ->appendControl( + id(new AphrontFormTokenizerControl()) + ->setLabel(pht('Use Blueprints')) + ->setName('blueprintPHIDs') + ->setValue($v_blueprints) + ->setDatasource(new DrydockBlueprintDatasource())) + ->appendChild( + id(new AphrontFormSubmitControl()) + ->setValue(pht('Save')) + ->addCancelButton($edit_uri)); + + $object_box = id(new PHUIObjectBoxView()) + ->setHeaderText($title) + ->setForm($form); + + return $this->buildApplicationPage( + array( + $crumbs, + $object_box, + ), + array( + 'title' => $title, + )); + } + +} diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditMainController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditMainController.php index 6519a4380e..5def1c754d 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryEditMainController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryEditMainController.php @@ -31,6 +31,7 @@ final class DiffusionRepositoryEditMainController $has_branches = ($is_git || $is_hg); $has_local = $repository->usesLocalWorkingCopy(); $supports_staging = $repository->supportsStaging(); + $supports_automation = $repository->supportsAutomation(); $crumbs = $this->buildApplicationCrumbs($is_main = true); @@ -100,6 +101,13 @@ final class DiffusionRepositoryEditMainController $this->buildStagingActions($repository)); } + $automation_properties = null; + if ($supports_automation) { + $automation_properties = $this->buildAutomationProperties( + $repository, + $this->buildAutomationActions($repository)); + } + $actions_properties = $this->buildActionsProperties( $repository, $this->buildActionsActions($repository)); @@ -171,6 +179,12 @@ final class DiffusionRepositoryEditMainController ->addPropertyList($staging_properties); } + if ($automation_properties) { + $boxes[] = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Automation')) + ->addPropertyList($automation_properties); + } + $boxes[] = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Text Encoding')) ->addPropertyList($encoding_properties); @@ -622,7 +636,6 @@ final class DiffusionRepositoryEditMainController return $view; } - private function buildStagingActions(PhabricatorRepository $repository) { $viewer = $this->getViewer(); @@ -661,6 +674,47 @@ final class DiffusionRepositoryEditMainController return $view; } + private function buildAutomationActions(PhabricatorRepository $repository) { + $viewer = $this->getViewer(); + + $view = id(new PhabricatorActionListView()) + ->setObjectURI($this->getRequest()->getRequestURI()) + ->setUser($viewer); + + $edit = id(new PhabricatorActionView()) + ->setIcon('fa-pencil') + ->setName(pht('Edit Automation')) + ->setHref( + $this->getRepositoryControllerURI($repository, 'edit/automation/')); + $view->addAction($edit); + + return $view; + } + + private function buildAutomationProperties( + PhabricatorRepository $repository, + PhabricatorActionListView $actions) { + $viewer = $this->getViewer(); + + $view = id(new PHUIPropertyListView()) + ->setUser($viewer) + ->setActionList($actions); + + $blueprint_phids = $repository->getAutomationBlueprintPHIDs(); + if (!$blueprint_phids) { + $blueprint_view = phutil_tag('em', array(), pht('Not Configured')); + } else { + $blueprint_view = id(new DrydockObjectAuthorizationView()) + ->setUser($viewer) + ->setObjectPHID($repository->getPHID()) + ->setBlueprintPHIDs($blueprint_phids); + } + + $view->addProperty(pht('Automation'), $blueprint_view); + + return $view; + } + private function buildHostingActions(PhabricatorRepository $repository) { $user = $this->getRequest()->getUser(); diff --git a/src/applications/diffusion/controller/DiffusionServeController.php b/src/applications/diffusion/controller/DiffusionServeController.php index a76acd704c..271ab36e91 100644 --- a/src/applications/diffusion/controller/DiffusionServeController.php +++ b/src/applications/diffusion/controller/DiffusionServeController.php @@ -536,12 +536,14 @@ final class DiffusionServeController extends DiffusionController { $body = strlen($stderr)."\n".$stderr; } else { list($length, $body) = explode("\n", $stdout, 2); + if ($cmd == 'capabilities') { + $body = DiffusionMercurialWireProtocol::filterBundle2Capability($body); + } } return id(new DiffusionMercurialResponse())->setContent($body); } - private function getMercurialArguments() { // Mercurial sends arguments in HTTP headers. "Why?", you might wonder, // "Why would you do this?". diff --git a/src/applications/diffusion/protocol/DiffusionMercurialWireProtocol.php b/src/applications/diffusion/protocol/DiffusionMercurialWireProtocol.php index 4f7fa6e29c..11e864e74e 100644 --- a/src/applications/diffusion/protocol/DiffusionMercurialWireProtocol.php +++ b/src/applications/diffusion/protocol/DiffusionMercurialWireProtocol.php @@ -99,4 +99,34 @@ final class DiffusionMercurialWireProtocol extends Phobject { return true; } + /** If the server version is running 3.4+ it will respond + * with 'bundle2' capability in the format of "bundle2=(url-encoding)". + * Until we maange to properly package up bundles to send back we + * disallow the client from knowing we speak bundle2 by removing it + * from the capabilities listing. + * + * The format of the capabilties string is: "a space separated list + * of strings representing what commands the server supports" + * @link https://www.mercurial-scm.org/wiki/CommandServer#Protocol + * + * @param string $capabilities - The string of capabilities to + * strip the bundle2 capability from. This is expected to be + * the space-separated list of strings resulting from the + * querying the 'capabilties' command. + * + * @return string The resulting space-separated list of capabilities + * which no longer contains the 'bundle2' capability. This is meant + * to replace the original $body to send back to client. + */ + public static function filterBundle2Capability($capabilities) { + $parts = explode(' ', $capabilities); + foreach ($parts as $key => $part) { + if (preg_match('/^bundle2=/', $part)) { + unset($parts[$key]); + break; + } + } + return implode(' ', $parts); + } + } diff --git a/src/applications/diffusion/protocol/__tests__/DiffusionMercurialWireProtocolTests.php b/src/applications/diffusion/protocol/__tests__/DiffusionMercurialWireProtocolTests.php new file mode 100644 index 0000000000..5878f5a9e2 --- /dev/null +++ b/src/applications/diffusion/protocol/__tests__/DiffusionMercurialWireProtocolTests.php @@ -0,0 +1,48 @@ + pht('Filter bundle2 from Mercurial 3.5.1'), + 'input' => $capabilities_with_bundle2_hg_351, + 'expect' => $capabilities_without_bundle2_hg_351, + ), + + array( + 'name' => pht('Filter bundle does not affect Mercurial 2.6.2'), + 'input' => $capabilities_hg_262, + 'expect' => $capabilities_hg_262, + ), + ); + + foreach ($cases as $case) { + $actual = DiffusionMercurialWireProtocol::filterBundle2Capability( + $case['input']); + $this->assertEqual($case['expect'], $actual, $case['name']); + } + } + +} diff --git a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelMercurialPathsQuery.php b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelMercurialPathsQuery.php index aca82df79f..12b9e661d7 100644 --- a/src/applications/diffusion/query/lowlevel/DiffusionLowLevelMercurialPathsQuery.php +++ b/src/applications/diffusion/query/lowlevel/DiffusionLowLevelMercurialPathsQuery.php @@ -24,10 +24,17 @@ final class DiffusionLowLevelMercurialPathsQuery $path = $this->path; $commit = $this->commit; + $hg_paths_command = 'locate --print0 --rev %s -I %s'; + $hg_version = PhabricatorRepositoryVersion::getMercurialVersion(); + if (PhabricatorRepositoryVersion::isMercurialFilesCommandAvailable( + $hg_version)) { + $hg_paths_command = 'files --print0 --rev %s -I %s'; + } + $match_against = trim($path, '/'); $prefix = trim('./'.$match_against, '/'); list($entire_manifest) = $repository->execxLocalCommand( - 'locate --print0 --rev %s -I %s', + $hg_paths_command, hgsprintf('%s', $commit), $prefix); return explode("\0", $entire_manifest); diff --git a/src/applications/diffusion/query/lowlevel/__tests__/DiffusionLowLevelMercurialPathsQueryTests.php b/src/applications/diffusion/query/lowlevel/__tests__/DiffusionLowLevelMercurialPathsQueryTests.php new file mode 100644 index 0000000000..075ca2f1a5 --- /dev/null +++ b/src/applications/diffusion/query/lowlevel/__tests__/DiffusionLowLevelMercurialPathsQueryTests.php @@ -0,0 +1,31 @@ + pht('Versions which should not use `files`'), + 'versions' => array('2.6.2', '2.9', '3.1'), + 'match' => false, + ), + + array( + 'name' => pht('Versions which should use `files`'), + 'versions' => array('3.2', '3.3', '3.5.2'), + 'match' => true, + ), + ); + + foreach ($cases as $case) { + foreach ($case['versions'] as $version) { + $actual = PhabricatorRepositoryVersion + ::isMercurialFilesCommandAvailable($version); + $expect = $case['match']; + $this->assertEqual($expect, $actual, $case['name']); + } + } + } + +} diff --git a/src/applications/diffusion/ssh/DiffusionMercurialServeSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionMercurialServeSSHWorkflow.php index 51ffdeb3bd..3f66110abc 100644 --- a/src/applications/diffusion/ssh/DiffusionMercurialServeSSHWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionMercurialServeSSHWorkflow.php @@ -107,8 +107,14 @@ final class DiffusionMercurialServeSSHWorkflow $this->didSeeWrite = true; } + $raw_message = $message['raw']; + if ($command == 'capabilities') { + $raw_message = DiffusionMercurialWireProtocol::filterBundle2Capability( + $raw_message); + } + // If we're good, return the raw message data. - return $message['raw']; + return $raw_message; } } diff --git a/src/applications/drydock/application/PhabricatorDrydockApplication.php b/src/applications/drydock/application/PhabricatorDrydockApplication.php index e662fea9e6..237e4afd9a 100644 --- a/src/applications/drydock/application/PhabricatorDrydockApplication.php +++ b/src/applications/drydock/application/PhabricatorDrydockApplication.php @@ -57,6 +57,8 @@ final class PhabricatorDrydockApplication extends PhabricatorApplication { 'DrydockResourceListController', 'logs/(?:query/(?P[^/]+)/)?' => 'DrydockLogListController', + 'authorizations/(?:query/(?P[^/]+)/)?' => + 'DrydockAuthorizationListController', ), 'create/' => 'DrydockBlueprintCreateController', 'edit/(?:(?P[1-9]\d*)/)?' => 'DrydockBlueprintEditController', @@ -81,6 +83,20 @@ final class PhabricatorDrydockApplication extends PhabricatorApplication { 'DrydockLogListController', ), ), + '(?Pauthorization)/' => array( + '(?P[1-9]\d*)/' => array( + '' => 'DrydockAuthorizationViewController', + '(?Pauthorize|decline)/' => + 'DrydockAuthorizationAuthorizeController', + ), + ), + '(?Poperation)/' => array( + '(?:query/(?P[^/]+)/)?' + => 'DrydockRepositoryOperationListController', + '(?P[1-9]\d*)/' => array( + '' => 'DrydockRepositoryOperationViewController', + ), + ), ), ); } diff --git a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php index 0f2e0aad44..0c53b19fcf 100644 --- a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php +++ b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php @@ -292,7 +292,8 @@ abstract class DrydockBlueprintImplementation extends Phobject { } protected function newLease(DrydockBlueprint $blueprint) { - return DrydockLease::initializeNewLease(); + return DrydockLease::initializeNewLease() + ->setAuthorizingPHID($blueprint->getPHID()); } protected function requireActiveLease(DrydockLease $lease) { @@ -342,9 +343,12 @@ abstract class DrydockBlueprintImplementation extends Phobject { $counts = queryfx_all( $conn_r, - 'SELECT status, COUNT(*) N FROM %T WHERE blueprintPHID = %s', + 'SELECT status, COUNT(*) N FROM %T + WHERE blueprintPHID = %s AND status != %s + GROUP BY status', $resource->getTableName(), - $blueprint->getPHID()); + $blueprint->getPHID(), + DrydockResourceStatus::STATUS_DESTROYED); $counts = ipull($counts, 'N', 'status'); $n_alloc = idx($counts, DrydockResourceStatus::STATUS_PENDING, 0); diff --git a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php index 30dd6fe4c5..1d7776af14 100644 --- a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php +++ b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php @@ -118,10 +118,13 @@ final class DrydockWorkingCopyBlueprintImplementation $resource_phid = $resource->getPHID(); + $blueprint_phids = $blueprint->getFieldValue('blueprintPHIDs'); + $host_lease = $this->newLease($blueprint) ->setResourceType('host') ->setOwnerPHID($resource_phid) - ->setAttribute('workingcopy.resourcePHID', $resource_phid); + ->setAttribute('workingcopy.resourcePHID', $resource_phid) + ->setAllowedBlueprintPHIDs($blueprint_phids); $resource ->setAttribute('host.leasePHID', $host_lease->getPHID()) @@ -390,5 +393,15 @@ final class DrydockWorkingCopyBlueprintImplementation return $lease; } + public function getFieldSpecifications() { + return array( + 'blueprintPHIDs' => array( + 'name' => pht('Use Blueprints'), + 'type' => 'blueprints', + 'required' => true, + ), + ) + parent::getFieldSpecifications(); + } + } diff --git a/src/applications/drydock/controller/DrydockAuthorizationAuthorizeController.php b/src/applications/drydock/controller/DrydockAuthorizationAuthorizeController.php new file mode 100644 index 0000000000..6422512df5 --- /dev/null +++ b/src/applications/drydock/controller/DrydockAuthorizationAuthorizeController.php @@ -0,0 +1,85 @@ +getViewer(); + $id = $request->getURIData('id'); + + $authorization = id(new DrydockAuthorizationQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$authorization) { + return new Aphront404Response(); + } + + $authorization_uri = $this->getApplicationURI("authorization/{$id}/"); + $is_authorize = ($request->getURIData('action') == 'authorize'); + + $state_authorized = DrydockAuthorization::BLUEPRINTAUTH_AUTHORIZED; + $state_declined = DrydockAuthorization::BLUEPRINTAUTH_DECLINED; + + $state = $authorization->getBlueprintAuthorizationState(); + $can_authorize = ($state != $state_authorized); + $can_decline = ($state != $state_declined); + + if ($is_authorize && !$can_authorize) { + return $this->newDialog() + ->setTitle(pht('Already Authorized')) + ->appendParagraph( + pht( + 'This authorization has already been approved.')) + ->addCancelButton($authorization_uri); + } + + if (!$is_authorize && !$can_decline) { + return $this->newDialog() + ->setTitle(pht('Already Declined')) + ->appendParagraph( + pht('This authorization has already been declined.')) + ->addCancelButton($authorization_uri); + } + + if ($request->isFormPost()) { + if ($is_authorize) { + $new_state = $state_authorized; + } else { + $new_state = $state_declined; + } + + $authorization + ->setBlueprintAuthorizationState($new_state) + ->save(); + + return id(new AphrontRedirectResponse())->setURI($authorization_uri); + } + + if ($is_authorize) { + $title = pht('Approve Authorization'); + $body = pht( + 'Approve this authorization? The object will be able to lease and '. + 'allocate resources created by this blueprint.'); + $button = pht('Approve Authorization'); + } else { + $title = pht('Decline Authorization'); + $body = pht( + 'Decline this authorization? The object will not be able to lease '. + 'or allocate resources created by this blueprint.'); + $button = pht('Decline Authorization'); + } + + return $this->newDialog() + ->setTitle($title) + ->appendParagraph($body) + ->addSubmitButton($button) + ->addCancelButton($authorization_uri); + } + +} diff --git a/src/applications/drydock/controller/DrydockAuthorizationListController.php b/src/applications/drydock/controller/DrydockAuthorizationListController.php new file mode 100644 index 0000000000..164ca8e5cc --- /dev/null +++ b/src/applications/drydock/controller/DrydockAuthorizationListController.php @@ -0,0 +1,87 @@ +blueprint = $blueprint; + return $this; + } + + public function getBlueprint() { + return $this->blueprint; + } + + public function shouldAllowPublic() { + return true; + } + + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); + + $engine = new DrydockAuthorizationSearchEngine(); + + $id = $request->getURIData('id'); + + $blueprint = id(new DrydockBlueprintQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$blueprint) { + return new Aphront404Response(); + } + + $this->setBlueprint($blueprint); + $engine->setBlueprint($blueprint); + + $querykey = $request->getURIData('queryKey'); + + $controller = id(new PhabricatorApplicationSearchController()) + ->setQueryKey($querykey) + ->setSearchEngine($engine) + ->setNavigation($this->buildSideNavView()); + + return $this->delegateToController($controller); + } + + public function buildSideNavView() { + $nav = new AphrontSideNavFilterView(); + $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); + + $engine = id(new DrydockAuthorizationSearchEngine()) + ->setViewer($this->getViewer()); + + $engine->setBlueprint($this->getBlueprint()); + $engine->addNavigationItems($nav->getMenu()); + + $nav->selectFilter(null); + + return $nav; + } + + protected function buildApplicationCrumbs() { + $crumbs = parent::buildApplicationCrumbs(); + + $blueprint = $this->getBlueprint(); + if ($blueprint) { + $id = $blueprint->getID(); + + $crumbs->addTextCrumb( + pht('Blueprints'), + $this->getApplicationURI('blueprint/')); + + $crumbs->addTextCrumb( + $blueprint->getBlueprintName(), + $this->getApplicationURI("blueprint/{$id}/")); + + $crumbs->addTextCrumb( + pht('Authorizations'), + $this->getApplicationURI("blueprint/{$id}/authorizations/")); + } + + return $crumbs; + } + +} diff --git a/src/applications/drydock/controller/DrydockAuthorizationViewController.php b/src/applications/drydock/controller/DrydockAuthorizationViewController.php new file mode 100644 index 0000000000..3609b95f9f --- /dev/null +++ b/src/applications/drydock/controller/DrydockAuthorizationViewController.php @@ -0,0 +1,131 @@ +getViewer(); + $id = $request->getURIData('id'); + + $authorization = id(new DrydockAuthorizationQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$authorization) { + return new Aphront404Response(); + } + + $id = $authorization->getID(); + $title = pht('Authorization %d', $id); + + $blueprint = $authorization->getBlueprint(); + $blueprint_id = $blueprint->getID(); + + $header = id(new PHUIHeaderView()) + ->setHeader($title) + ->setUser($viewer) + ->setPolicyObject($authorization); + + + $state = $authorization->getBlueprintAuthorizationState(); + $icon = DrydockAuthorization::getBlueprintStateIcon($state); + $name = DrydockAuthorization::getBlueprintStateName($state); + + $header->setStatus($icon, null, $name); + + $actions = $this->buildActionListView($authorization); + $properties = $this->buildPropertyListView($authorization); + $properties->setActionList($actions); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb( + pht('Blueprints'), + $this->getApplicationURI('blueprint/')); + $crumbs->addTextCrumb( + $blueprint->getBlueprintName(), + $this->getApplicationURI("blueprint/{$blueprint_id}/")); + $crumbs->addTextCrumb($title); + + $object_box = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->addPropertyList($properties); + + return $this->buildApplicationPage( + array( + $crumbs, + $object_box, + ), + array( + 'title' => $title, + )); + + } + + private function buildActionListView(DrydockAuthorization $authorization) { + $viewer = $this->getViewer(); + $id = $authorization->getID(); + + $view = id(new PhabricatorActionListView()) + ->setUser($viewer) + ->setObjectURI($this->getRequest()->getRequestURI()) + ->setObject($authorization); + + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $authorization, + PhabricatorPolicyCapability::CAN_EDIT); + + $authorize_uri = $this->getApplicationURI("authorization/{$id}/authorize/"); + $decline_uri = $this->getApplicationURI("authorization/{$id}/decline/"); + + $state_authorized = DrydockAuthorization::BLUEPRINTAUTH_AUTHORIZED; + $state_declined = DrydockAuthorization::BLUEPRINTAUTH_DECLINED; + + $state = $authorization->getBlueprintAuthorizationState(); + $can_authorize = $can_edit && ($state != $state_authorized); + $can_decline = $can_edit && ($state != $state_declined); + + $view->addAction( + id(new PhabricatorActionView()) + ->setHref($authorize_uri) + ->setName(pht('Approve Authorization')) + ->setIcon('fa-check') + ->setWorkflow(true) + ->setDisabled(!$can_authorize)); + + $view->addAction( + id(new PhabricatorActionView()) + ->setHref($decline_uri) + ->setName(pht('Decline Authorization')) + ->setIcon('fa-times') + ->setWorkflow(true) + ->setDisabled(!$can_decline)); + + return $view; + } + + private function buildPropertyListView(DrydockAuthorization $authorization) { + $viewer = $this->getViewer(); + + $object_phid = $authorization->getObjectPHID(); + $handles = $viewer->loadHandles(array($object_phid)); + $handle = $handles[$object_phid]; + + $view = new PHUIPropertyListView(); + + $view->addProperty( + pht('Authorized Object'), + $handle->renderLink($handle->getFullName())); + + $view->addProperty(pht('Object Type'), $handle->getTypeName()); + + $object_state = $authorization->getObjectAuthorizationState(); + + $view->addProperty( + pht('Authorization State'), + DrydockAuthorization::getObjectStateName($object_state)); + + return $view; + } + +} diff --git a/src/applications/drydock/controller/DrydockBlueprintViewController.php b/src/applications/drydock/controller/DrydockBlueprintViewController.php index 7102962600..f90dcb9d82 100644 --- a/src/applications/drydock/controller/DrydockBlueprintViewController.php +++ b/src/applications/drydock/controller/DrydockBlueprintViewController.php @@ -51,6 +51,8 @@ final class DrydockBlueprintViewController extends DrydockBlueprintController { $resource_box = $this->buildResourceBox($blueprint); + $authorizations_box = $this->buildAuthorizationsBox($blueprint); + $timeline = $this->buildTransactionTimeline( $blueprint, new DrydockBlueprintTransactionQuery()); @@ -68,6 +70,7 @@ final class DrydockBlueprintViewController extends DrydockBlueprintController { $crumbs, $object_box, $resource_box, + $authorizations_box, $log_box, $timeline, ), @@ -167,12 +170,78 @@ final class DrydockBlueprintViewController extends DrydockBlueprintController { ->setTag('a') ->setHref($resources_uri) ->setIconFont('fa-search') - ->setText(pht('View All Resources'))); + ->setText(pht('View All'))); return id(new PHUIObjectBoxView()) ->setHeader($resource_header) ->setObjectList($resource_list); } + private function buildAuthorizationsBox(DrydockBlueprint $blueprint) { + $viewer = $this->getViewer(); + + $limit = 25; + + // If there are pending authorizations against this blueprint, make sure + // we show them first. + + $pending_authorizations = id(new DrydockAuthorizationQuery()) + ->setViewer($viewer) + ->withBlueprintPHIDs(array($blueprint->getPHID())) + ->withObjectStates( + array( + DrydockAuthorization::OBJECTAUTH_ACTIVE, + )) + ->withBlueprintStates( + array( + DrydockAuthorization::BLUEPRINTAUTH_REQUESTED, + )) + ->setLimit($limit) + ->execute(); + + $all_authorizations = id(new DrydockAuthorizationQuery()) + ->setViewer($viewer) + ->withBlueprintPHIDs(array($blueprint->getPHID())) + ->withObjectStates( + array( + DrydockAuthorization::OBJECTAUTH_ACTIVE, + )) + ->withBlueprintStates( + array( + DrydockAuthorization::BLUEPRINTAUTH_REQUESTED, + DrydockAuthorization::BLUEPRINTAUTH_AUTHORIZED, + )) + ->setLimit($limit) + ->execute(); + + $authorizations = + mpull($pending_authorizations, null, 'getPHID') + + mpull($all_authorizations, null, 'getPHID'); + + $authorization_list = id(new DrydockAuthorizationListView()) + ->setUser($viewer) + ->setAuthorizations($authorizations) + ->setNoDataString( + pht('No objects have active authorizations to use this blueprint.')); + + $id = $blueprint->getID(); + $authorizations_uri = "blueprint/{$id}/authorizations/query/all/"; + $authorizations_uri = $this->getApplicationURI($authorizations_uri); + + $authorizations_header = id(new PHUIHeaderView()) + ->setHeader(pht('Active Authorizations')) + ->addActionLink( + id(new PHUIButtonView()) + ->setTag('a') + ->setHref($authorizations_uri) + ->setIconFont('fa-search') + ->setText(pht('View All'))); + + return id(new PHUIObjectBoxView()) + ->setHeader($authorizations_header) + ->setObjectList($authorization_list); + + } + } diff --git a/src/applications/drydock/controller/DrydockConsoleController.php b/src/applications/drydock/controller/DrydockConsoleController.php index 1964f508d0..77346cea94 100644 --- a/src/applications/drydock/controller/DrydockConsoleController.php +++ b/src/applications/drydock/controller/DrydockConsoleController.php @@ -15,6 +15,7 @@ final class DrydockConsoleController extends DrydockController { $nav->addFilter('blueprint', pht('Blueprints')); $nav->addFilter('resource', pht('Resources')); $nav->addFilter('lease', pht('Leases')); + $nav->addFilter('operation', pht('Repository Operations')); $nav->selectFilter(null); @@ -52,6 +53,13 @@ final class DrydockConsoleController extends DrydockController { ->setHref($this->getApplicationURI('lease/')) ->addAttribute(pht('Manage leases on resources.'))); + $menu->addItem( + id(new PHUIObjectItemView()) + ->setHeader(pht('Repository Operations')) + ->setFontIcon('fa-fighter-jet') + ->setHref($this->getApplicationURI('operation/')) + ->addAttribute(pht('Review the repository operation queue.'))); + $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Console')); diff --git a/src/applications/drydock/controller/DrydockController.php b/src/applications/drydock/controller/DrydockController.php index 760334cbdf..ddb6788fb6 100644 --- a/src/applications/drydock/controller/DrydockController.php +++ b/src/applications/drydock/controller/DrydockController.php @@ -2,12 +2,6 @@ abstract class DrydockController extends PhabricatorController { - abstract public function buildSideNavView(); - - public function buildApplicationMenu() { - return $this->buildSideNavView()->getMenu(); - } - protected function buildLocksTab($owner_phid) { $locks = DrydockSlotLock::loadLocks($owner_phid); @@ -105,7 +99,7 @@ abstract class DrydockController extends PhabricatorController { ->setTag('a') ->setHref($all_uri) ->setIconFont('fa-search') - ->setText(pht('View All Logs'))); + ->setText(pht('View All'))); return id(new PHUIObjectBoxView()) ->setHeader($log_header) diff --git a/src/applications/drydock/controller/DrydockLeaseViewController.php b/src/applications/drydock/controller/DrydockLeaseViewController.php index b9cf592313..0b5eae3e12 100644 --- a/src/applications/drydock/controller/DrydockLeaseViewController.php +++ b/src/applications/drydock/controller/DrydockLeaseViewController.php @@ -116,6 +116,14 @@ final class DrydockLeaseViewController extends DrydockLeaseController { } $view->addProperty(pht('Owner'), $owner_display); + $authorizing_phid = $lease->getAuthorizingPHID(); + if ($authorizing_phid) { + $authorizing_display = $viewer->renderHandle($authorizing_phid); + } else { + $authorizing_display = phutil_tag('em', array(), pht('None')); + } + $view->addProperty(pht('Authorized By'), $authorizing_display); + $resource_phid = $lease->getResourcePHID(); if ($resource_phid) { $resource_display = $viewer->renderHandle($resource_phid); diff --git a/src/applications/drydock/controller/DrydockRepositoryOperationListController.php b/src/applications/drydock/controller/DrydockRepositoryOperationListController.php new file mode 100644 index 0000000000..581302817b --- /dev/null +++ b/src/applications/drydock/controller/DrydockRepositoryOperationListController.php @@ -0,0 +1,37 @@ +getURIData('queryKey'); + + $engine = new DrydockRepositoryOperationSearchEngine(); + + $controller = id(new PhabricatorApplicationSearchController()) + ->setQueryKey($query_key) + ->setSearchEngine($engine) + ->setNavigation($this->buildSideNavView()); + + return $this->delegateToController($controller); + } + + public function buildSideNavView() { + $nav = new AphrontSideNavFilterView(); + $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); + + $engine = id(new DrydockRepositoryOperationSearchEngine()) + ->setViewer($this->getViewer()); + + $engine->addNavigationItems($nav->getMenu()); + + $nav->selectFilter(null); + + return $nav; + } + +} diff --git a/src/applications/drydock/controller/DrydockRepositoryOperationViewController.php b/src/applications/drydock/controller/DrydockRepositoryOperationViewController.php new file mode 100644 index 0000000000..e740073dd9 --- /dev/null +++ b/src/applications/drydock/controller/DrydockRepositoryOperationViewController.php @@ -0,0 +1,89 @@ +getViewer(); + $id = $request->getURIData('id'); + + $operation = id(new DrydockRepositoryOperationQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$operation) { + return new Aphront404Response(); + } + + $id = $operation->getID(); + $title = pht('Repository Operation %d', $id); + + $header = id(new PHUIHeaderView()) + ->setHeader($title) + ->setUser($viewer) + ->setPolicyObject($operation); + + $state = $operation->getOperationState(); + $icon = DrydockRepositoryOperation::getOperationStateIcon($state); + $name = DrydockRepositoryOperation::getOperationStateName($state); + $header->setStatus($icon, null, $name); + + $actions = $this->buildActionListView($operation); + $properties = $this->buildPropertyListView($operation); + $properties->setActionList($actions); + + $crumbs = $this->buildApplicationCrumbs(); + $crumbs->addTextCrumb( + pht('Operations'), + $this->getApplicationURI('operation/')); + $crumbs->addTextCrumb($title); + + $object_box = id(new PHUIObjectBoxView()) + ->setHeader($header) + ->addPropertyList($properties); + + return $this->buildApplicationPage( + array( + $crumbs, + $object_box, + ), + array( + 'title' => $title, + )); + + } + + private function buildActionListView(DrydockRepositoryOperation $operation) { + $viewer = $this->getViewer(); + $id = $operation->getID(); + + $view = id(new PhabricatorActionListView()) + ->setUser($viewer) + ->setObjectURI($this->getRequest()->getRequestURI()) + ->setObject($operation); + + return $view; + } + + private function buildPropertyListView( + DrydockRepositoryOperation $operation) { + + $viewer = $this->getViewer(); + + $view = new PHUIPropertyListView(); + $view->addProperty( + pht('Repository'), + $viewer->renderHandle($operation->getRepositoryPHID())); + + $view->addProperty( + pht('Object'), + $viewer->renderHandle($operation->getObjectPHID())); + + return $view; + } + +} diff --git a/src/applications/drydock/controller/DrydockResourceViewController.php b/src/applications/drydock/controller/DrydockResourceViewController.php index 4809cf970c..71e4a09db1 100644 --- a/src/applications/drydock/controller/DrydockResourceViewController.php +++ b/src/applications/drydock/controller/DrydockResourceViewController.php @@ -170,7 +170,7 @@ final class DrydockResourceViewController extends DrydockResourceController { ->setTag('a') ->setHref($leases_uri) ->setIconFont('fa-search') - ->setText(pht('View All Leases'))); + ->setText(pht('View All'))); $lease_list = id(new DrydockLeaseListView()) ->setUser($viewer) diff --git a/src/applications/drydock/customfield/DrydockBlueprintCoreCustomField.php b/src/applications/drydock/customfield/DrydockBlueprintCoreCustomField.php index f4e6ba3a27..9af418b4d1 100644 --- a/src/applications/drydock/customfield/DrydockBlueprintCoreCustomField.php +++ b/src/applications/drydock/customfield/DrydockBlueprintCoreCustomField.php @@ -41,11 +41,6 @@ final class DrydockBlueprintCoreCustomField $object->setDetail($key, $value); } - public function applyApplicationTransactionExternalEffects( - PhabricatorApplicationTransaction $xaction) { - return; - } - public function getBlueprintFieldValue() { return $this->getProxy()->getFieldValue(); } diff --git a/src/applications/drydock/logtype/DrydockLeaseNoAuthorizationsLogType.php b/src/applications/drydock/logtype/DrydockLeaseNoAuthorizationsLogType.php new file mode 100644 index 0000000000..b5a7ca1b13 --- /dev/null +++ b/src/applications/drydock/logtype/DrydockLeaseNoAuthorizationsLogType.php @@ -0,0 +1,26 @@ +getViewer(); + $authorizing_phid = idx($data, 'authorizingPHID'); + + return pht( + 'The object which authorized this lease (%s) is not authorized to use '. + 'any of the blueprints the lease lists. Approve the authorizations '. + 'before using the lease.', + $viewer->renderHandle($authorizing_phid)->render()); + } + +} diff --git a/src/applications/drydock/logtype/DrydockLeaseNoBlueprintsLogType.php b/src/applications/drydock/logtype/DrydockLeaseNoBlueprintsLogType.php new file mode 100644 index 0000000000..b835ba32ef --- /dev/null +++ b/src/applications/drydock/logtype/DrydockLeaseNoBlueprintsLogType.php @@ -0,0 +1,19 @@ +getViewer(); $resource_type = $args->getArg('type'); if (!$resource_type) { @@ -59,6 +59,23 @@ final class DrydockManagementLeaseWorkflow $lease = id(new DrydockLease()) ->setResourceType($resource_type); + $drydock_phid = id(new PhabricatorDrydockApplication())->getPHID(); + $lease->setAuthorizingPHID($drydock_phid); + + // TODO: This is not hugely scalable, although this is a debugging workflow + // so maybe it's fine. Do we even need `bin/drydock lease` in the long run? + $all_blueprints = id(new DrydockBlueprintQuery()) + ->setViewer($viewer) + ->execute(); + $allowed_phids = mpull($all_blueprints, 'getPHID'); + if (!$allowed_phids) { + throw new Exception( + pht( + 'No blueprints exist which can plausibly allocate resources to '. + 'satisfy the requested lease.')); + } + $lease->setAllowedBlueprintPHIDs($allowed_phids); + if ($attributes) { $lease->setAttributes($attributes); } diff --git a/src/applications/drydock/operation/DrydockLandRepositoryOperation.php b/src/applications/drydock/operation/DrydockLandRepositoryOperation.php new file mode 100644 index 0000000000..050aaeeb36 --- /dev/null +++ b/src/applications/drydock/operation/DrydockLandRepositoryOperation.php @@ -0,0 +1,151 @@ +getViewer(); + $repository = $operation->getRepository(); + + $cmd = array(); + $arg = array(); + + $object = $operation->getObject(); + if ($object instanceof DifferentialRevision) { + $revision = $object; + + $diff_phid = $operation->getProperty('differential.diffPHID'); + + $diff = id(new DifferentialDiffQuery()) + ->setViewer($viewer) + ->withPHIDs(array($diff_phid)) + ->executeOne(); + if (!$diff) { + throw new Exception( + pht( + 'Unable to load diff "%s".', + $diff_phid)); + } + + $diff_revid = $diff->getRevisionID(); + $revision_id = $revision->getID(); + if ($diff_revid != $revision_id) { + throw new Exception( + pht( + 'Diff ("%s") has wrong revision ID ("%s", expected "%s").', + $diff_phid, + $diff_revid, + $revision_id)); + } + + $cmd[] = 'git fetch --no-tags -- %s +%s:%s'; + $arg[] = $repository->getStagingURI(); + $arg[] = $diff->getStagingRef(); + $arg[] = $diff->getStagingRef(); + + $merge_src = $diff->getStagingRef(); + + $dict = $diff->getDiffAuthorshipDict(); + $author_name = idx($dict, 'authorName'); + $author_email = idx($dict, 'authorEmail'); + + $api_method = 'differential.getcommitmessage'; + $api_params = array( + 'revision_id' => $revision->getID(), + ); + + $commit_message = id(new ConduitCall($api_method, $api_params)) + ->setUser($viewer) + ->execute(); + } else { + throw new Exception( + pht( + 'Invalid or unknown object ("%s") for land operation, expected '. + 'Differential Revision.', + $operation->getObjectPHID())); + } + + $target = $operation->getRepositoryTarget(); + list($type, $name) = explode(':', $target, 2); + switch ($type) { + case 'branch': + $push_dst = 'refs/heads/'.$name; + $merge_dst = 'refs/remotes/origin/'.$name; + break; + default: + throw new Exception( + pht( + 'Unknown repository operation target type "%s" (in target "%s").', + $type, + $target)); + } + + $committer_info = $this->getCommitterInfo($operation); + + $cmd[] = 'git checkout %s'; + $arg[] = $merge_dst; + + $cmd[] = 'git merge --no-stat --squash --ff-only -- %s'; + $arg[] = $merge_src; + + $cmd[] = 'git -c user.name=%s -c user.email=%s commit --author %s -m %s'; + + $arg[] = $committer_info['name']; + $arg[] = $committer_info['email']; + + $arg[] = "{$author_name} <{$author_email}>"; + $arg[] = $commit_message; + + $cmd[] = 'git push origin -- %s:%s'; + $arg[] = 'HEAD'; + $arg[] = $push_dst; + + $cmd = implode(' && ', $cmd); + $argv = array_merge(array($cmd), $arg); + + $result = call_user_func_array( + array($interface, 'execx'), + $argv); + } + + private function getCommitterInfo(DrydockRepositoryOperation $operation) { + $viewer = $this->getViewer(); + + $committer_name = null; + + $author_phid = $operation->getAuthorPHID(); + $object = id(new PhabricatorObjectQuery()) + ->setViewer($viewer) + ->withPHIDs(array($author_phid)) + ->executeOne(); + + if ($object) { + if ($object instanceof PhabricatorUser) { + $committer_name = $object->getUsername(); + } + } + + if (!strlen($committer_name)) { + $committer_name = pht('autocommitter'); + } + + // TODO: Probably let users choose a VCS email address in settings. For + // now just make something up so we don't leak anyone's stuff. + + return array( + 'name' => $committer_name, + 'email' => 'autocommitter@example.com', + ); + } + +} diff --git a/src/applications/drydock/operation/DrydockRepositoryOperationType.php b/src/applications/drydock/operation/DrydockRepositoryOperationType.php new file mode 100644 index 0000000000..a78ed3173e --- /dev/null +++ b/src/applications/drydock/operation/DrydockRepositoryOperationType.php @@ -0,0 +1,35 @@ +viewer = $viewer; + return $this; + } + + final public function getViewer() { + return $this->viewer; + } + + final public function getOperationConstant() { + return $this->getPhobjectClassConstant('OPCONST', 32); + } + + final public static function getAllOperationTypes() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getOperationConstant') + ->execute(); + } + +} diff --git a/src/applications/drydock/phid/DrydockAuthorizationPHIDType.php b/src/applications/drydock/phid/DrydockAuthorizationPHIDType.php new file mode 100644 index 0000000000..e518149945 --- /dev/null +++ b/src/applications/drydock/phid/DrydockAuthorizationPHIDType.php @@ -0,0 +1,37 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $authorization = $objects[$phid]; + $id = $authorization->getID(); + + $handle->setName(pht('Drydock Authorization %d', $id)); + $handle->setURI("/drydock/authorization/{$id}/"); + } + } + +} diff --git a/src/applications/drydock/phid/DrydockBlueprintPHIDType.php b/src/applications/drydock/phid/DrydockBlueprintPHIDType.php index 86eeb7f3c5..e63f1294a7 100644 --- a/src/applications/drydock/phid/DrydockBlueprintPHIDType.php +++ b/src/applications/drydock/phid/DrydockBlueprintPHIDType.php @@ -8,6 +8,14 @@ final class DrydockBlueprintPHIDType extends PhabricatorPHIDType { return pht('Blueprint'); } + public function getPHIDTypeApplicationClass() { + return 'PhabricatorDrydockApplication'; + } + + public function getTypeIcon() { + return 'fa-map-o'; + } + public function newObject() { return new DrydockBlueprint(); } @@ -28,9 +36,12 @@ final class DrydockBlueprintPHIDType extends PhabricatorPHIDType { foreach ($handles as $phid => $handle) { $blueprint = $objects[$phid]; $id = $blueprint->getID(); + $name = $blueprint->getBlueprintName(); - $handle->setName($blueprint->getBlueprintName()); - $handle->setURI("/drydock/blueprint/{$id}/"); + $handle + ->setName($name) + ->setFullName(pht('Blueprint %d: %s', $id, $name)) + ->setURI("/drydock/blueprint/{$id}/"); } } diff --git a/src/applications/drydock/phid/DrydockLeasePHIDType.php b/src/applications/drydock/phid/DrydockLeasePHIDType.php index c3006164e5..fc921cee3a 100644 --- a/src/applications/drydock/phid/DrydockLeasePHIDType.php +++ b/src/applications/drydock/phid/DrydockLeasePHIDType.php @@ -8,6 +8,14 @@ final class DrydockLeasePHIDType extends PhabricatorPHIDType { return pht('Drydock Lease'); } + public function getPHIDTypeApplicationClass() { + return 'PhabricatorDrydockApplication'; + } + + public function getTypeIcon() { + return 'fa-link'; + } + public function newObject() { return new DrydockLease(); } diff --git a/src/applications/drydock/phid/DrydockRepositoryOperationPHIDType.php b/src/applications/drydock/phid/DrydockRepositoryOperationPHIDType.php new file mode 100644 index 0000000000..d21efd8a86 --- /dev/null +++ b/src/applications/drydock/phid/DrydockRepositoryOperationPHIDType.php @@ -0,0 +1,37 @@ +withPHIDs($phids); + } + + public function loadHandles( + PhabricatorHandleQuery $query, + array $handles, + array $objects) { + + foreach ($handles as $phid => $handle) { + $operation = $objects[$phid]; + $id = $operation->getID(); + + $handle->setName(pht('Drydock Repository Operation %d', $id)); + $handle->setURI("/drydock/operation/{$id}/"); + } + } + +} diff --git a/src/applications/drydock/phid/DrydockResourcePHIDType.php b/src/applications/drydock/phid/DrydockResourcePHIDType.php index 9eb85e7561..966cf35abe 100644 --- a/src/applications/drydock/phid/DrydockResourcePHIDType.php +++ b/src/applications/drydock/phid/DrydockResourcePHIDType.php @@ -8,6 +8,14 @@ final class DrydockResourcePHIDType extends PhabricatorPHIDType { return pht('Drydock Resource'); } + public function getPHIDTypeApplicationClass() { + return 'PhabricatorDrydockApplication'; + } + + public function getTypeIcon() { + return 'fa-map'; + } + public function newObject() { return new DrydockResource(); } diff --git a/src/applications/drydock/query/DrydockAuthorizationQuery.php b/src/applications/drydock/query/DrydockAuthorizationQuery.php new file mode 100644 index 0000000000..6d2cddcf8a --- /dev/null +++ b/src/applications/drydock/query/DrydockAuthorizationQuery.php @@ -0,0 +1,146 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withBlueprintPHIDs(array $phids) { + $this->blueprintPHIDs = $phids; + return $this; + } + + public function withObjectPHIDs(array $phids) { + $this->objectPHIDs = $phids; + return $this; + } + + public function withBlueprintStates(array $states) { + $this->blueprintStates = $states; + return $this; + } + + public function withObjectStates(array $states) { + $this->objectStates = $states; + return $this; + } + + public function newResultObject() { + return new DrydockAuthorization(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function willFilterPage(array $authorizations) { + $blueprint_phids = mpull($authorizations, 'getBlueprintPHID'); + if ($blueprint_phids) { + $blueprints = id(new DrydockBlueprintQuery()) + ->setViewer($this->getViewer()) + ->setParentQuery($this) + ->withPHIDs($blueprint_phids) + ->execute(); + $blueprints = mpull($blueprints, null, 'getPHID'); + } else { + $blueprints = array(); + } + + foreach ($authorizations as $key => $authorization) { + $blueprint = idx($blueprints, $authorization->getBlueprintPHID()); + if (!$blueprint) { + $this->didRejectResult($authorization); + unset($authorizations[$key]); + continue; + } + $authorization->attachBlueprint($blueprint); + } + + $object_phids = mpull($authorizations, 'getObjectPHID'); + 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 ($authorizations as $key => $authorization) { + $object = idx($objects, $authorization->getObjectPHID()); + if (!$object) { + $this->didRejectResult($authorization); + unset($authorizations[$key]); + continue; + } + $authorization->attachObject($object); + } + + return $authorizations; + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->blueprintPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'blueprintPHID IN (%Ls)', + $this->blueprintPHIDs); + } + + if ($this->objectPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'objectPHID IN (%Ls)', + $this->objectPHIDs); + } + + if ($this->blueprintStates !== null) { + $where[] = qsprintf( + $conn, + 'blueprintAuthorizationState IN (%Ls)', + $this->blueprintStates); + } + + if ($this->objectStates !== null) { + $where[] = qsprintf( + $conn, + 'objectAuthorizationState IN (%Ls)', + $this->objectStates); + } + + return $where; + } + +} diff --git a/src/applications/drydock/query/DrydockAuthorizationSearchEngine.php b/src/applications/drydock/query/DrydockAuthorizationSearchEngine.php new file mode 100644 index 0000000000..7aaef65650 --- /dev/null +++ b/src/applications/drydock/query/DrydockAuthorizationSearchEngine.php @@ -0,0 +1,87 @@ +blueprint = $blueprint; + return $this; + } + + public function getBlueprint() { + return $this->blueprint; + } + + public function getResultTypeDescription() { + return pht('Drydock Authorizations'); + } + + public function getApplicationClassName() { + return 'PhabricatorDrydockApplication'; + } + + public function canUseInPanelContext() { + return false; + } + + public function newQuery() { + $query = new DrydockAuthorizationQuery(); + + $blueprint = $this->getBlueprint(); + $query->withBlueprintPHIDs(array($blueprint->getPHID())); + + return $query; + } + + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); + + return $query; + } + + protected function buildCustomSearchFields() { + return array(); + } + + protected function getURI($path) { + $blueprint = $this->getBlueprint(); + $id = $blueprint->getID(); + return "/drydock/blueprint/{$id}/authorizations/".$path; + } + + protected function getBuiltinQueryNames() { + return array( + 'all' => pht('All Authorizations'), + ); + } + + 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 $authorizations, + PhabricatorSavedQuery $query, + array $handles) { + + $list = id(new DrydockAuthorizationListView()) + ->setUser($this->requireViewer()) + ->setAuthorizations($authorizations); + + $result = new PhabricatorApplicationSearchResultView(); + $result->setTable($list); + + return $result; + } + +} diff --git a/src/applications/drydock/query/DrydockBlueprintQuery.php b/src/applications/drydock/query/DrydockBlueprintQuery.php index 7ce5dcbe5b..169e47b4f7 100644 --- a/src/applications/drydock/query/DrydockBlueprintQuery.php +++ b/src/applications/drydock/query/DrydockBlueprintQuery.php @@ -7,6 +7,7 @@ final class DrydockBlueprintQuery extends DrydockQuery { private $blueprintClasses; private $datasourceQuery; private $disabled; + private $authorizedPHIDs; public function withIDs(array $ids) { $this->ids = $ids; @@ -33,10 +34,19 @@ final class DrydockBlueprintQuery extends DrydockQuery { return $this; } + public function withAuthorizedPHIDs(array $phids) { + $this->authorizedPHIDs = $phids; + return $this; + } + public function newResultObject() { return new DrydockBlueprint(); } + protected function getPrimaryTableAlias() { + return 'blueprint'; + } + protected function loadPage() { return $this->loadStandardPage($this->newResultObject()); } @@ -63,39 +73,66 @@ final class DrydockBlueprintQuery extends DrydockQuery { if ($this->ids !== null) { $where[] = qsprintf( $conn, - 'id IN (%Ld)', + 'blueprint.id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( $conn, - 'phid IN (%Ls)', + 'blueprint.phid IN (%Ls)', $this->phids); } if ($this->datasourceQuery !== null) { $where[] = qsprintf( $conn, - 'blueprintName LIKE %>', + 'blueprint.blueprintName LIKE %>', $this->datasourceQuery); } if ($this->blueprintClasses !== null) { $where[] = qsprintf( $conn, - 'className IN (%Ls)', + 'blueprint.className IN (%Ls)', $this->blueprintClasses); } if ($this->disabled !== null) { $where[] = qsprintf( $conn, - 'isDisabled = %d', + 'blueprint.isDisabled = %d', (int)$this->disabled); } return $where; } + protected function shouldGroupQueryResultRows() { + if ($this->authorizedPHIDs !== null) { + return true; + } + return parent::shouldGroupQueryResultRows(); + } + + protected function buildJoinClauseParts(AphrontDatabaseConnection $conn) { + $joins = parent::buildJoinClauseParts($conn); + + if ($this->authorizedPHIDs !== null) { + $joins[] = qsprintf( + $conn, + 'JOIN %T authorization + ON authorization.blueprintPHID = blueprint.phid + AND authorization.objectPHID IN (%Ls) + AND authorization.objectAuthorizationState = %s + AND authorization.blueprintAuthorizationState = %s', + id(new DrydockAuthorization())->getTableName(), + $this->authorizedPHIDs, + DrydockAuthorization::OBJECTAUTH_ACTIVE, + DrydockAuthorization::BLUEPRINTAUTH_AUTHORIZED); + } + + return $joins; + } + } diff --git a/src/applications/drydock/query/DrydockRepositoryOperationQuery.php b/src/applications/drydock/query/DrydockRepositoryOperationQuery.php new file mode 100644 index 0000000000..b72e654603 --- /dev/null +++ b/src/applications/drydock/query/DrydockRepositoryOperationQuery.php @@ -0,0 +1,145 @@ +ids = $ids; + return $this; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function withObjectPHIDs(array $object_phids) { + $this->objectPHIDs = $object_phids; + return $this; + } + + public function withRepositoryPHIDs(array $repository_phids) { + $this->repositoryPHIDs = $repository_phids; + return $this; + } + + public function withOperationStates(array $states) { + $this->operationStates = $states; + return $this; + } + + public function newResultObject() { + return new DrydockRepositoryOperation(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function willFilterPage(array $operations) { + $implementations = DrydockRepositoryOperationType::getAllOperationTypes(); + + foreach ($operations as $key => $operation) { + $impl = idx($implementations, $operation->getOperationType()); + if (!$impl) { + $this->didRejectResult($operation); + unset($operations[$key]); + continue; + } + $impl = clone $impl; + $operation->attachImplementation($impl); + } + + $repository_phids = mpull($operations, 'getRepositoryPHID'); + if ($repository_phids) { + $repositories = id(new PhabricatorRepositoryQuery()) + ->setViewer($this->getViewer()) + ->setParentQuery($this) + ->withPHIDs($repository_phids) + ->execute(); + $repositories = mpull($repositories, null, 'getPHID'); + } else { + $repositories = array(); + } + + foreach ($operations as $key => $operation) { + $repository = idx($repositories, $operation->getRepositoryPHID()); + if (!$repository) { + $this->didRejectResult($operation); + unset($operations[$key]); + continue; + } + $operation->attachRepository($repository); + } + + return $operations; + } + + protected function didFilterPage(array $operations) { + $object_phids = mpull($operations, 'getObjectPHID'); + 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 ($operations as $key => $operation) { + $object = idx($objects, $operation->getObjectPHID()); + $operation->attachObject($object); + } + + return $operations; + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->phids !== null) { + $where[] = qsprintf( + $conn, + 'phid IN (%Ls)', + $this->phids); + } + + if ($this->objectPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'objectPHID IN (%Ls)', + $this->objectPHIDs); + } + + if ($this->repositoryPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'repositoryPHID IN (%Ls)', + $this->repositoryPHIDs); + } + + if ($this->operationStates !== null) { + $where[] = qsprintf( + $conn, + 'operationState IN (%Ls)', + $this->operationStates); + } + + return $where; + } + +} diff --git a/src/applications/drydock/query/DrydockRepositoryOperationSearchEngine.php b/src/applications/drydock/query/DrydockRepositoryOperationSearchEngine.php new file mode 100644 index 0000000000..c4befe8201 --- /dev/null +++ b/src/applications/drydock/query/DrydockRepositoryOperationSearchEngine.php @@ -0,0 +1,99 @@ +newQuery(); + + return $query; + } + + protected function buildCustomSearchFields() { + return array( + ); + } + + protected function getURI($path) { + return '/drydock/operation/'.$path; + } + + protected function getBuiltinQueryNames() { + return array( + 'all' => pht('All Operations'), + ); + } + + 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 $operations, + PhabricatorSavedQuery $query, + array $handles) { + assert_instances_of($operations, 'DrydockRepositoryOperation'); + + $viewer = $this->requireViewer(); + + $view = new PHUIObjectItemListView(); + foreach ($operations as $operation) { + $id = $operation->getID(); + + $item = id(new PHUIObjectItemView()) + ->setHeader($operation->getOperationDescription($viewer)) + ->setHref($this->getApplicationURI("operation/{$id}/")) + ->setObjectName(pht('Repository Operation %d', $id)); + + $state = $operation->getOperationState(); + + $icon = DrydockRepositoryOperation::getOperationStateIcon($state); + $name = DrydockRepositoryOperation::getOperationStateName($state); + + $item->addIcon($icon, $name); + $item->addByline( + array( + pht('Via:'), + ' ', + $viewer->renderHandle($operation->getAuthorPHID()), + )); + + $item->addAttribute( + $viewer->renderHandle( + $operation->getObjectPHID())); + + $item->addAttribute( + $viewer->renderHandle( + $operation->getRepositoryPHID())); + + $view->addItem($item); + } + + $result = id(new PhabricatorApplicationSearchResultView()) + ->setObjectList($view) + ->setNoDataString(pht('No matching operations.')); + + return $result; + } + +} diff --git a/src/applications/drydock/storage/DrydockAuthorization.php b/src/applications/drydock/storage/DrydockAuthorization.php new file mode 100644 index 0000000000..8872ef7f6f --- /dev/null +++ b/src/applications/drydock/storage/DrydockAuthorization.php @@ -0,0 +1,202 @@ + true, + self::CONFIG_COLUMN_SCHEMA => array( + 'blueprintAuthorizationState' => 'text32', + 'objectAuthorizationState' => 'text32', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_unique' => array( + 'columns' => array('objectPHID', 'blueprintPHID'), + 'unique' => true, + ), + 'key_blueprint' => array( + 'columns' => array('blueprintPHID', 'blueprintAuthorizationState'), + ), + 'key_object' => array( + 'columns' => array('objectPHID', 'objectAuthorizationState'), + ), + ), + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + DrydockAuthorizationPHIDType::TYPECONST); + } + + public function attachBlueprint(DrydockBlueprint $blueprint) { + $this->blueprint = $blueprint; + return $this; + } + + public function getBlueprint() { + return $this->assertAttached($this->blueprint); + } + + public function attachObject($object) { + $this->object = $object; + return $this; + } + + public function getObject() { + return $this->assertAttached($this->object); + } + + public static function getBlueprintStateIcon($state) { + $map = array( + self::BLUEPRINTAUTH_REQUESTED => 'fa-exclamation-circle indigo', + self::BLUEPRINTAUTH_AUTHORIZED => 'fa-check-circle green', + self::BLUEPRINTAUTH_DECLINED => 'fa-times red', + ); + + return idx($map, $state, null); + } + + public static function getBlueprintStateName($state) { + $map = array( + self::BLUEPRINTAUTH_REQUESTED => pht('Requested'), + self::BLUEPRINTAUTH_AUTHORIZED => pht('Authorized'), + self::BLUEPRINTAUTH_DECLINED => pht('Declined'), + ); + + return idx($map, $state, pht('', $state)); + } + + public static function getObjectStateName($state) { + $map = array( + self::OBJECTAUTH_ACTIVE => pht('Active'), + self::OBJECTAUTH_INACTIVE => pht('Inactive'), + ); + + return idx($map, $state, pht('', $state)); + } + + /** + * Apply external authorization effects after a user chagnes the value of a + * blueprint selector control an object. + * + * @param PhabricatorUser User applying the change. + * @param phid Object PHID change is being applied to. + * @param list Old blueprint PHIDs. + * @param list New blueprint PHIDs. + * @return void + */ + public static function applyAuthorizationChanges( + PhabricatorUser $viewer, + $object_phid, + array $old, + array $new) { + + $old_phids = array_fuse($old); + $new_phids = array_fuse($new); + + $rem_phids = array_diff_key($old_phids, $new_phids); + $add_phids = array_diff_key($new_phids, $old_phids); + + $altered_phids = $rem_phids + $add_phids; + + if (!$altered_phids) { + return; + } + + $authorizations = id(new DrydockAuthorizationQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withObjectPHIDs(array($object_phid)) + ->withBlueprintPHIDs($altered_phids) + ->execute(); + $authorizations = mpull($authorizations, null, 'getBlueprintPHID'); + + $state_active = self::OBJECTAUTH_ACTIVE; + $state_inactive = self::OBJECTAUTH_INACTIVE; + + $state_requested = self::BLUEPRINTAUTH_REQUESTED; + + // Disable the object side of the authorization for any existing + // authorizations. + foreach ($rem_phids as $rem_phid) { + $authorization = idx($authorizations, $rem_phid); + if (!$authorization) { + continue; + } + + $authorization + ->setObjectAuthorizationState($state_inactive) + ->save(); + } + + // For new authorizations, either add them or reactivate them depending + // on the current state. + foreach ($add_phids as $add_phid) { + $needs_update = false; + + $authorization = idx($authorizations, $add_phid); + if (!$authorization) { + $authorization = id(new DrydockAuthorization()) + ->setObjectPHID($object_phid) + ->setObjectAuthorizationState($state_active) + ->setBlueprintPHID($add_phid) + ->setBlueprintAuthorizationState($state_requested); + + $needs_update = true; + } else { + $current_state = $authorization->getObjectAuthorizationState(); + if ($current_state != $state_active) { + $authorization->setObjectAuthorizationState($state_active); + $needs_update = true; + } + } + + if ($needs_update) { + $authorization->save(); + } + } + } + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + return $this->getBlueprint()->getPolicy($capability); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return $this->getBlueprint()->hasAutomaticCapability($capability, $viewer); + } + + public function describeAutomaticCapability($capability) { + return pht( + 'An authorization inherits the policies of the blueprint it '. + 'authorizes access to.'); + } + + +} diff --git a/src/applications/drydock/storage/DrydockLease.php b/src/applications/drydock/storage/DrydockLease.php index 01ccb0a5c4..af475c5d61 100644 --- a/src/applications/drydock/storage/DrydockLease.php +++ b/src/applications/drydock/storage/DrydockLease.php @@ -7,6 +7,7 @@ final class DrydockLease extends DrydockDAO protected $resourceType; protected $until; protected $ownerPHID; + protected $authorizingPHID; protected $attributes = array(); protected $status = DrydockLeaseStatus::STATUS_PENDING; @@ -141,6 +142,25 @@ final class DrydockLease extends DrydockDAO pht('Only new leases may be queued for activation!')); } + if (!$this->getAuthorizingPHID()) { + throw new Exception( + pht( + 'Trying to queue a lease for activation without an authorizing '. + 'object. Use "%s" to specify the PHID of the authorizing object. '. + 'The authorizing object must be approved to use the allowed '. + 'blueprints.', + 'setAuthorizingPHID()')); + } + + if (!$this->getAllowedBlueprintPHIDs()) { + throw new Exception( + pht( + 'Trying to queue a lease for activation without any allowed '. + 'Blueprints. Use "%s" to specify allowed blueprints. The '. + 'authorizing object must be approved to use the allowed blueprints.', + 'setAllowedBlueprintPHIDs()')); + } + $this ->setStatus(DrydockLeaseStatus::STATUS_PENDING) ->save(); @@ -376,6 +396,15 @@ final class DrydockLease extends DrydockDAO return $this; } + public function setAllowedBlueprintPHIDs(array $phids) { + $this->setAttribute('internal.blueprintPHIDs', $phids); + return $this; + } + + public function getAllowedBlueprintPHIDs() { + return $this->getAttribute('internal.blueprintPHIDs', array()); + } + private function didActivate() { $viewer = PhabricatorUser::getOmnipotentUser(); $need_update = false; diff --git a/src/applications/drydock/storage/DrydockRepositoryOperation.php b/src/applications/drydock/storage/DrydockRepositoryOperation.php new file mode 100644 index 0000000000..7a8e35ea68 --- /dev/null +++ b/src/applications/drydock/storage/DrydockRepositoryOperation.php @@ -0,0 +1,170 @@ +setOperationState(self::STATE_WAIT) + ->setOperationType($op->getOperationConstant()); + } + + protected function getConfiguration() { + return array( + self::CONFIG_AUX_PHID => true, + self::CONFIG_SERIALIZATION => array( + 'properties' => self::SERIALIZATION_JSON, + ), + self::CONFIG_COLUMN_SCHEMA => array( + 'repositoryTarget' => 'bytes', + 'operationType' => 'text32', + 'operationState' => 'text32', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_object' => array( + 'columns' => array('objectPHID'), + ), + 'key_repository' => array( + 'columns' => array('repositoryPHID', 'operationState'), + ), + ), + ) + parent::getConfiguration(); + } + + public function generatePHID() { + return PhabricatorPHID::generateNewPHID( + DrydockRepositoryOperationPHIDType::TYPECONST); + } + + public function attachRepository(PhabricatorRepository $repository) { + $this->repository = $repository; + return $this; + } + + public function getRepository() { + return $this->assertAttached($this->repository); + } + + public function attachObject($object) { + $this->object = $object; + return $this; + } + + public function getObject() { + return $this->assertAttached($this->object); + } + + public function attachImplementation(DrydockRepositoryOperationType $impl) { + $this->implementation = $impl; + return $this; + } + + public function getImplementation() { + return $this->implementation; + } + + public function getProperty($key, $default = null) { + return idx($this->properties, $key, $default); + } + + public function setProperty($key, $value) { + $this->properties[$key] = $value; + return $this; + } + + public static function getOperationStateIcon($state) { + $map = array( + self::STATE_WAIT => 'fa-clock-o', + self::STATE_WORK => 'fa-refresh blue', + self::STATE_DONE => 'fa-check green', + self::STATE_FAIL => 'fa-times red', + ); + + return idx($map, $state, null); + } + + public static function getOperationStateName($state) { + $map = array( + self::STATE_WAIT => pht('Waiting'), + self::STATE_WORK => pht('Working'), + self::STATE_DONE => pht('Done'), + self::STATE_FAIL => pht('Failed'), + ); + + return idx($map, $state, pht('', $state)); + } + + public function scheduleUpdate() { + PhabricatorWorker::scheduleTask( + 'DrydockRepositoryOperationUpdateWorker', + array( + 'operationPHID' => $this->getPHID(), + ), + array( + 'objectPHID' => $this->getPHID(), + 'priority' => PhabricatorWorker::PRIORITY_ALERTS, + )); + } + + public function applyOperation(DrydockInterface $interface) { + return $this->getImplementation()->applyOperation( + $this, + $interface); + } + + public function getOperationDescription(PhabricatorUser $viewer) { + return $this->getImplementation()->getOperationDescription( + $this, + $viewer); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + ); + } + + public function getPolicy($capability) { + return $this->getRepository()->getPolicy($capability); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return $this->getRepository()->hasAutomaticCapability($capability, $viewer); + } + + public function describeAutomaticCapability($capability) { + return pht( + 'A repository operation inherits the policies of the repository it '. + 'affects.'); + } + +} diff --git a/src/applications/drydock/view/DrydockAuthorizationListView.php b/src/applications/drydock/view/DrydockAuthorizationListView.php new file mode 100644 index 0000000000..28296b6a3a --- /dev/null +++ b/src/applications/drydock/view/DrydockAuthorizationListView.php @@ -0,0 +1,65 @@ +authorizations = $authorizations; + return $this; + } + + public function setNoDataString($string) { + $this->noDataString = $string; + return $this; + } + + public function getNoDataString() { + return $this->noDataString; + } + + public function render() { + $viewer = $this->getUser(); + + $authorizations = $this->authorizations; + + $view = new PHUIObjectItemListView(); + + $nodata = $this->getNoDataString(); + if ($nodata) { + $view->setNoDataString($nodata); + } + + $handles = $viewer->loadHandles(mpull($authorizations, 'getObjectPHID')); + + foreach ($authorizations as $authorization) { + $id = $authorization->getID(); + $object_phid = $authorization->getObjectPHID(); + $handle = $handles[$object_phid]; + + $item = id(new PHUIObjectItemView()) + ->setHref("/drydock/authorization/{$id}/") + ->setObjectName(pht('Authorization %d', $id)) + ->setHeader($handle->getFullName()); + + $item->addAttribute($handle->getTypeName()); + + $object_state = $authorization->getObjectAuthorizationState(); + $item->addAttribute( + DrydockAuthorization::getObjectStateName($object_state)); + + $state = $authorization->getBlueprintAuthorizationState(); + $icon = DrydockAuthorization::getBlueprintStateIcon($state); + $name = DrydockAuthorization::getBlueprintStateName($state); + + $item->setStatusIcon($icon, $name); + + $view->addItem($item); + } + + return $view; + } + +} diff --git a/src/applications/drydock/view/DrydockObjectAuthorizationView.php b/src/applications/drydock/view/DrydockObjectAuthorizationView.php new file mode 100644 index 0000000000..261829bfe5 --- /dev/null +++ b/src/applications/drydock/view/DrydockObjectAuthorizationView.php @@ -0,0 +1,79 @@ +objectPHID = $object_phid; + return $this; + } + + public function getObjectPHID() { + return $this->objectPHID; + } + + public function setBlueprintPHIDs(array $blueprint_phids) { + $this->blueprintPHIDs = $blueprint_phids; + return $this; + } + + public function getBlueprintPHIDs() { + return $this->blueprintPHIDs; + } + + public function render() { + $viewer = $this->getUser(); + $blueprint_phids = $this->getBlueprintPHIDs(); + $object_phid = $this->getObjectPHID(); + + // NOTE: We're intentionally letting you see the authorization state on + // blueprints you can't see because this has a tremendous potential to + // be extremely confusing otherwise. You still can't see the blueprints + // themselves, but you can know if the object is authorized on something. + + if ($blueprint_phids) { + $handles = $viewer->loadHandles($blueprint_phids); + + $authorizations = id(new DrydockAuthorizationQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withObjectPHIDs(array($object_phid)) + ->withBlueprintPHIDs($blueprint_phids) + ->execute(); + $authorizations = mpull($authorizations, null, 'getBlueprintPHID'); + } else { + $handles = array(); + $authorizations = array(); + } + + $items = array(); + foreach ($blueprint_phids as $phid) { + $authorization = idx($authorizations, $phid); + if (!$authorization) { + continue; + } + + $handle = $handles[$phid]; + + $item = id(new PHUIStatusItemView()) + ->setTarget($handle->renderLink()); + + $state = $authorization->getBlueprintAuthorizationState(); + $item->setIcon( + DrydockAuthorization::getBlueprintStateIcon($state), + null, + DrydockAuthorization::getBlueprintStateName($state)); + + $items[] = $item; + } + + $status = new PHUIStatusListView(); + foreach ($items as $item) { + $status->addItem($item); + } + + return $status; + } + +} diff --git a/src/applications/drydock/worker/DrydockLeaseUpdateWorker.php b/src/applications/drydock/worker/DrydockLeaseUpdateWorker.php index a93997259d..7d8cc98219 100644 --- a/src/applications/drydock/worker/DrydockLeaseUpdateWorker.php +++ b/src/applications/drydock/worker/DrydockLeaseUpdateWorker.php @@ -211,10 +211,19 @@ final class DrydockLeaseUpdateWorker extends DrydockWorker { $exceptions); } + $resources = $this->removeUnacquirableResources($resources, $lease); + if (!$resources) { + // If we make it here, we just built a resource but aren't allowed + // to acquire it. We expect this during routine operation if the + // resource prevents acquisition until it activates. Yield and wait + // for activation. + throw new PhabricatorWorkerYieldException(15); + } + // NOTE: We have not acquired the lease yet, so it is possible that the // resource we just built will be snatched up by some other lease before - // we can. This is not problematic: we'll retry a little later and should - // suceed eventually. + // we can acquire it. This is not problematic: we'll retry a little later + // and should suceed eventually. } $resources = $this->rankResources($resources, $lease); @@ -300,11 +309,46 @@ final class DrydockLeaseUpdateWorker extends DrydockWorker { return array(); } - $blueprints = id(new DrydockBlueprintQuery()) + $query = id(new DrydockBlueprintQuery()) ->setViewer($viewer) ->withBlueprintClasses(array_keys($impls)) - ->withDisabled(false) - ->execute(); + ->withDisabled(false); + + $blueprint_phids = $lease->getAllowedBlueprintPHIDs(); + if (!$blueprint_phids) { + $lease->logEvent(DrydockLeaseNoBlueprintsLogType::LOGCONST); + return array(); + } + + // The Drydock application itself is allowed to authorize anything. This + // is primarily used for leases generated by CLI administrative tools. + $drydock_phid = id(new PhabricatorDrydockApplication())->getPHID(); + + $authorizing_phid = $lease->getAuthorizingPHID(); + if ($authorizing_phid != $drydock_phid) { + $blueprints = id(clone $query) + ->withAuthorizedPHIDs(array($authorizing_phid)) + ->execute(); + if (!$blueprints) { + // If we didn't hit any blueprints, check if this is an authorization + // problem: re-execute the query without the authorization constraint. + // If the second query hits blueprints, the overall configuration is + // fine but this is an authorization problem. If the second query also + // comes up blank, this is some other kind of configuration issue so + // we fall through to the default pathway. + $all_blueprints = $query->execute(); + if ($all_blueprints) { + $lease->logEvent( + DrydockLeaseNoAuthorizationsLogType::LOGCONST, + array( + 'authorizingPHID' => $authorizing_phid, + )); + return array(); + } + } + } else { + $blueprints = $query->execute(); + } $keep = array(); foreach ($blueprints as $key => $blueprint) { @@ -347,6 +391,22 @@ final class DrydockLeaseUpdateWorker extends DrydockWorker { )) ->execute(); + return $this->removeUnacquirableResources($resources, $lease); + } + + + /** + * Remove resources which can not be acquired by a given lease from a list. + * + * @param list Candidate resources. + * @param DrydockLease Acquiring lease. + * @return list Resources which the lease may be able to + * acquire. + * @task allocator + */ + private function removeUnacquirableResources( + array $resources, + DrydockLease $lease) { $keep = array(); foreach ($resources as $key => $resource) { $blueprint = $resource->getBlueprint(); diff --git a/src/applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php b/src/applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php new file mode 100644 index 0000000000..556119f6b3 --- /dev/null +++ b/src/applications/drydock/worker/DrydockRepositoryOperationUpdateWorker.php @@ -0,0 +1,177 @@ +getTaskDataValue('operationPHID'); + + $hash = PhabricatorHash::digestForIndex($operation_phid); + $lock_key = 'drydock.operation:'.$hash; + + $lock = PhabricatorGlobalLock::newLock($lock_key) + ->lock(1); + + try { + $operation = $this->loadOperation($operation_phid); + $this->handleUpdate($operation); + } catch (Exception $ex) { + $lock->unlock(); + throw $ex; + } + + $lock->unlock(); + } + + + private function handleUpdate(DrydockRepositoryOperation $operation) { + $viewer = $this->getViewer(); + + $operation_state = $operation->getOperationState(); + + switch ($operation_state) { + case DrydockRepositoryOperation::STATE_WAIT: + $operation + ->setOperationState(DrydockRepositoryOperation::STATE_WORK) + ->save(); + break; + case DrydockRepositoryOperation::STATE_WORK: + break; + case DrydockRepositoryOperation::STATE_DONE: + case DrydockRepositoryOperation::STATE_FAIL: + // No more processing for these requests. + return; + } + + // TODO: We should probably check for other running operations with lower + // IDs and the same repository target and yield to them here? That is, + // enforce sequential evaluation of operations against the same target so + // that if you land "A" and then land "B", we always finish "A" first. + // For now, just let stuff happen in any order. We can't lease until + // we know we're good to move forward because we might deadlock if we do: + // we're waiting for another operation to complete, and that operation is + // waiting for a lease we're holding. + + try { + $lease = $this->loadWorkingCopyLease($operation); + + $interface = $lease->getInterface( + DrydockCommandInterface::INTERFACE_TYPE); + + // No matter what happens here, destroy the lease away once we're done. + $lease->releaseOnDestruction(true); + + $operation->getImplementation() + ->setViewer($viewer); + + $operation->applyOperation($interface); + + } catch (PhabricatorWorkerYieldException $ex) { + throw $ex; + } catch (Exception $ex) { + $operation + ->setOperationState(DrydockRepositoryOperation::STATE_FAIL) + ->save(); + throw $ex; + } + + $operation + ->setOperationState(DrydockRepositoryOperation::STATE_DONE) + ->save(); + + // TODO: Once we have sequencing, we could awaken the next operation + // against this target after finishing or failing. + } + + private function loadWorkingCopyLease( + DrydockRepositoryOperation $operation) { + $viewer = $this->getViewer(); + + // TODO: This is very similar to leasing in Harbormaster, maybe we can + // share some of the logic? + + $lease_phid = $operation->getProperty('exec.leasePHID'); + if ($lease_phid) { + $lease = id(new DrydockLeaseQuery()) + ->setViewer($viewer) + ->withPHIDs(array($lease_phid)) + ->executeOne(); + if (!$lease) { + throw new PhabricatorWorkerPermanentFailureException( + pht( + 'Lease "%s" could not be loaded.', + $lease_phid)); + } + } else { + $working_copy_type = id(new DrydockWorkingCopyBlueprintImplementation()) + ->getType(); + + $repository = $operation->getRepository(); + + $allowed_phids = $repository->getAutomationBlueprintPHIDs(); + $authorizing_phid = $repository->getPHID(); + + $lease = DrydockLease::initializeNewLease() + ->setResourceType($working_copy_type) + ->setOwnerPHID($operation->getPHID()) + ->setAuthorizingPHID($authorizing_phid) + ->setAllowedBlueprintPHIDs($allowed_phids); + + $map = $this->buildRepositoryMap($operation); + + $lease->setAttribute('repositories.map', $map); + + $task_id = $this->getCurrentWorkerTaskID(); + if ($task_id) { + $lease->setAwakenTaskIDs(array($task_id)); + } + + $operation + ->setProperty('exec.leasePHID', $lease->getPHID()) + ->save(); + + $lease->queueForActivation(); + } + + if ($lease->isActivating()) { + throw new PhabricatorWorkerYieldException(15); + } + + if (!$lease->isActive()) { + throw new PhabricatorWorkerPermanentFailureException( + pht( + 'Lease "%s" never activated.', + $lease->getPHID())); + } + + return $lease; + } + + private function buildRepositoryMap(DrydockRepositoryOperation $operation) { + $repository = $operation->getRepository(); + + $target = $operation->getRepositoryTarget(); + list($type, $name) = explode(':', $target, 2); + switch ($type) { + case 'branch': + $spec = array( + 'branch' => $name, + ); + break; + default: + throw new Exception( + pht( + 'Unknown repository operation target type "%s" (in target "%s").', + $type, + $target)); + } + + $map = array(); + $map[$repository->getCloneName()] = array( + 'phid' => $repository->getPHID(), + 'default' => true, + ) + $spec; + + return $map; + } +} diff --git a/src/applications/drydock/worker/DrydockWorker.php b/src/applications/drydock/worker/DrydockWorker.php index d2dc1ca399..029cbf1c84 100644 --- a/src/applications/drydock/worker/DrydockWorker.php +++ b/src/applications/drydock/worker/DrydockWorker.php @@ -36,6 +36,21 @@ abstract class DrydockWorker extends PhabricatorWorker { return $resource; } + protected function loadOperation($operation_phid) { + $viewer = $this->getViewer(); + + $operation = id(new DrydockRepositoryOperationQuery()) + ->setViewer($viewer) + ->withPHIDs(array($operation_phid)) + ->executeOne(); + if (!$operation) { + throw new PhabricatorWorkerPermanentFailureException( + pht('No such operation "%s"!', $operation_phid)); + } + + return $operation; + } + protected function loadCommands($target_phid) { $viewer = $this->getViewer(); diff --git a/src/applications/harbormaster/controller/HarbormasterBuildActionController.php b/src/applications/harbormaster/controller/HarbormasterBuildActionController.php index 932a498a37..d729d868b0 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildActionController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildActionController.php @@ -3,24 +3,15 @@ final class HarbormasterBuildActionController extends HarbormasterController { - private $id; - private $action; - private $via; - - public function willProcessRequest(array $data) { - $this->id = $data['id']; - $this->action = $data['action']; - $this->via = idx($data, 'via'); - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); - $command = $this->action; + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); + $id = $request->getURIData('id'); + $action = $request->getURIData('action'); + $via = $request->getURIData('via'); $build = id(new HarbormasterBuildQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, @@ -31,7 +22,7 @@ final class HarbormasterBuildActionController return new Aphront404Response(); } - switch ($command) { + switch ($action) { case HarbormasterBuildCommand::COMMAND_RESTART: $can_issue = $build->canRestartBuild(); break; @@ -48,7 +39,7 @@ final class HarbormasterBuildActionController return new Aphront400Response(); } - switch ($this->via) { + switch ($via) { case 'buildable': $return_uri = '/'.$build->getBuildable()->getMonogram(); break; @@ -66,14 +57,14 @@ final class HarbormasterBuildActionController $xaction = id(new HarbormasterBuildTransaction()) ->setTransactionType(HarbormasterBuildTransaction::TYPE_COMMAND) - ->setNewValue($command); + ->setNewValue($action); $editor->applyTransactions($build, array($xaction)); return id(new AphrontRedirectResponse())->setURI($return_uri); } - switch ($command) { + switch ($action) { case HarbormasterBuildCommand::COMMAND_RESTART: if ($can_issue) { $title = pht('Really restart build?'); diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php b/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php index 2716befd00..9fa7d6f006 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php @@ -3,22 +3,14 @@ final class HarbormasterBuildableActionController extends HarbormasterController { - private $id; - private $action; - - public function willProcessRequest(array $data) { - $this->id = $data['id']; - $this->action = $data['action']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); - $command = $this->action; + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); + $id = $request->getURIData('id'); + $action = $request->getURIData('action'); $buildable = id(new HarbormasterBuildableQuery()) ->setViewer($viewer) - ->withIDs(array($this->id)) + ->withIDs(array($id)) ->needBuilds(true) ->requireCapabilities( array( @@ -33,7 +25,7 @@ final class HarbormasterBuildableActionController $issuable = array(); foreach ($buildable->getBuilds() as $build) { - switch ($command) { + switch ($action) { case HarbormasterBuildCommand::COMMAND_RESTART: if ($build->canRestartBuild()) { $issuable[] = $build; @@ -69,7 +61,7 @@ final class HarbormasterBuildableActionController $xaction = id(new HarbormasterBuildableTransaction()) ->setTransactionType(HarbormasterBuildableTransaction::TYPE_COMMAND) - ->setNewValue($command); + ->setNewValue($action); $editor->applyTransactions($buildable, array($xaction)); @@ -82,14 +74,14 @@ final class HarbormasterBuildableActionController foreach ($issuable as $build) { $xaction = id(new HarbormasterBuildTransaction()) ->setTransactionType(HarbormasterBuildTransaction::TYPE_COMMAND) - ->setNewValue($command); + ->setNewValue($action); $build_editor->applyTransactions($build, array($xaction)); } return id(new AphrontRedirectResponse())->setURI($return_uri); } - switch ($command) { + switch ($action) { case HarbormasterBuildCommand::COMMAND_RESTART: if ($issuable) { $title = pht('Really restart all builds?'); diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableListController.php b/src/applications/harbormaster/controller/HarbormasterBuildableListController.php index aac41dc57b..b7e6dcd978 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildableListController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildableListController.php @@ -2,19 +2,13 @@ final class HarbormasterBuildableListController extends HarbormasterController { - private $queryKey; - public function shouldAllowPublic() { return true; } - public function willProcessRequest(array $data) { - $this->queryKey = idx($data, 'queryKey'); - } - - public function processRequest() { + public function handleRequest(AphrontRequest $request) { $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($this->queryKey) + ->setQueryKey($request->getURIData('queryKey')) ->setSearchEngine(new HarbormasterBuildableSearchEngine()) ->setNavigation($this->buildSideNavView()); diff --git a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php index 95db4c52c0..680594277d 100644 --- a/src/applications/harbormaster/controller/HarbormasterPlanViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterPlanViewController.php @@ -4,7 +4,6 @@ final class HarbormasterPlanViewController extends HarbormasterPlanController { public function handleRequest(AphrontRequest $request) { $viewer = $this->getviewer(); - $id = $request->getURIData('id'); $plan = id(new HarbormasterBuildPlanQuery()) diff --git a/src/applications/harbormaster/controller/HarbormasterStepEditController.php b/src/applications/harbormaster/controller/HarbormasterStepEditController.php index 089a801220..9e99cc6c7f 100644 --- a/src/applications/harbormaster/controller/HarbormasterStepEditController.php +++ b/src/applications/harbormaster/controller/HarbormasterStepEditController.php @@ -4,11 +4,11 @@ final class HarbormasterStepEditController extends HarbormasterController { public function handleRequest(AphrontRequest $request) { $viewer = $this->getViewer(); + $id = $request->getURIData('id'); $this->requireApplicationCapability( HarbormasterManagePlansCapability::CAPABILITY); - $id = $request->getURIData('id'); if ($id) { $step = id(new HarbormasterBuildStepQuery()) ->setViewer($viewer) diff --git a/src/applications/harbormaster/customfield/HarbormasterBuildStepCoreCustomField.php b/src/applications/harbormaster/customfield/HarbormasterBuildStepCoreCustomField.php index 0ad8f960cf..296669d2b5 100644 --- a/src/applications/harbormaster/customfield/HarbormasterBuildStepCoreCustomField.php +++ b/src/applications/harbormaster/customfield/HarbormasterBuildStepCoreCustomField.php @@ -67,11 +67,6 @@ final class HarbormasterBuildStepCoreCustomField $object->setDetail($key, $value); } - public function applyApplicationTransactionExternalEffects( - PhabricatorApplicationTransaction $xaction) { - return; - } - public function getBuildTargetFieldValue() { return $this->getProxy()->getFieldValue(); } diff --git a/src/applications/harbormaster/phid/HarbormasterBuildStepPHIDType.php b/src/applications/harbormaster/phid/HarbormasterBuildStepPHIDType.php index b728f0e3a7..92e1980d8c 100644 --- a/src/applications/harbormaster/phid/HarbormasterBuildStepPHIDType.php +++ b/src/applications/harbormaster/phid/HarbormasterBuildStepPHIDType.php @@ -28,9 +28,13 @@ final class HarbormasterBuildStepPHIDType extends PhabricatorPHIDType { foreach ($handles as $phid => $handle) { $build_step = $objects[$phid]; + $id = $build_step->getID(); $name = $build_step->getName(); - $handle->setName($name); + $handle + ->setName($name) + ->setFullName(pht('Build Step %d: %s', $id, $name)) + ->setURI("/harbormaster/step/{$id}/edit/"); } } diff --git a/src/applications/harbormaster/step/HarbormasterLeaseWorkingCopyBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterLeaseWorkingCopyBuildStepImplementation.php index 73ea19bd7a..1bf5b33a0c 100644 --- a/src/applications/harbormaster/step/HarbormasterLeaseWorkingCopyBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterLeaseWorkingCopyBuildStepImplementation.php @@ -41,9 +41,14 @@ final class HarbormasterLeaseWorkingCopyBuildStepImplementation $working_copy_type = id(new DrydockWorkingCopyBlueprintImplementation()) ->getType(); + $allowed_phids = $build_target->getFieldValue('repositoryPHIDs'); + $authorizing_phid = $build_target->getBuildStep()->getPHID(); + $lease = DrydockLease::initializeNewLease() ->setResourceType($working_copy_type) - ->setOwnerPHID($build_target->getPHID()); + ->setOwnerPHID($build_target->getPHID()) + ->setAuthorizingPHID($authorizing_phid) + ->setAllowedBlueprintPHIDs($allowed_phids); $map = $this->buildRepositoryMap($build_target); @@ -104,6 +109,11 @@ final class HarbormasterLeaseWorkingCopyBuildStepImplementation 'type' => 'text', 'required' => true, ), + 'blueprintPHIDs' => array( + 'name' => pht('Use Blueprints'), + 'type' => 'blueprints', + 'required' => true, + ), 'repositoryPHIDs' => array( 'name' => pht('Also Clone'), 'type' => 'datasource', diff --git a/src/applications/home/controller/PhabricatorHomeQuickCreateController.php b/src/applications/home/controller/PhabricatorHomeQuickCreateController.php index deb54072a0..40f9afeb8c 100644 --- a/src/applications/home/controller/PhabricatorHomeQuickCreateController.php +++ b/src/applications/home/controller/PhabricatorHomeQuickCreateController.php @@ -3,8 +3,8 @@ final class PhabricatorHomeQuickCreateController extends PhabricatorHomeController { - public function processRequest() { - $viewer = $this->getRequest()->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); $items = $this->getCurrentApplication()->loadAllQuickCreateItems($viewer); diff --git a/src/applications/maniphest/command/ManiphestAssignEmailCommand.php b/src/applications/maniphest/command/ManiphestAssignEmailCommand.php index 9d2a51b107..98e8a913a8 100644 --- a/src/applications/maniphest/command/ManiphestAssignEmailCommand.php +++ b/src/applications/maniphest/command/ManiphestAssignEmailCommand.php @@ -34,6 +34,7 @@ final class ManiphestAssignEmailCommand array $argv) { $xactions = array(); + $assign_phid = null; $assign_to = head($argv); if ($assign_to) { diff --git a/src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php b/src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php index 0c353bf543..af5ca34712 100644 --- a/src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php +++ b/src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php @@ -18,7 +18,7 @@ final class PhabricatorMetaMTAMailgunReceiveController return phutil_hashes_are_identical($sig, $hash); } - public function processRequest() { + public function handleRequest(AphrontRequest $request) { // No CSRF for Mailgun. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); @@ -28,7 +28,6 @@ final class PhabricatorMetaMTAMailgunReceiveController pht('Mail signature is not valid. Check your Mailgun API key.')); } - $request = $this->getRequest(); $user = $request->getUser(); $raw_headers = $request->getStr('headers'); diff --git a/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php b/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php index 646d6ef2ae..0a5e28fcee 100644 --- a/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php +++ b/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php @@ -7,12 +7,10 @@ final class PhabricatorMetaMTASendGridReceiveController return false; } - public function processRequest() { + public function handleRequest(AphrontRequest $request) { // No CSRF for SendGrid. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); - - $request = $this->getRequest(); $user = $request->getUser(); $raw_headers = $request->getStr('headers'); diff --git a/src/applications/multimeter/data/MultimeterControl.php b/src/applications/multimeter/data/MultimeterControl.php index 1b01ce2e35..6658319469 100644 --- a/src/applications/multimeter/data/MultimeterControl.php +++ b/src/applications/multimeter/data/MultimeterControl.php @@ -265,6 +265,7 @@ final class MultimeterControl extends Phobject { 'init' => true, 'diff' => true, 'cat' => true, + 'files' => true, ), 'svnadmin' => array( 'create' => true, diff --git a/src/applications/passphrase/controller/PassphraseCredentialEditController.php b/src/applications/passphrase/controller/PassphraseCredentialEditController.php index 89fe783fac..a749e82af3 100644 --- a/src/applications/passphrase/controller/PassphraseCredentialEditController.php +++ b/src/applications/passphrase/controller/PassphraseCredentialEditController.php @@ -249,6 +249,7 @@ final class PassphraseCredentialEditController extends PassphraseController { id(new AphrontFormPolicyControl()) ->setName('viewPolicy') ->setPolicyObject($credential) + ->setSpacePHID($v_space) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW) ->setPolicies($policies)) ->appendControl( diff --git a/src/applications/passphrase/controller/PassphraseCredentialViewController.php b/src/applications/passphrase/controller/PassphraseCredentialViewController.php index 46fd0c4c7a..46ae06cdc7 100644 --- a/src/applications/passphrase/controller/PassphraseCredentialViewController.php +++ b/src/applications/passphrase/controller/PassphraseCredentialViewController.php @@ -201,11 +201,7 @@ final class PassphraseCredentialViewController extends PassphraseController { pht('Description'), PHUIPropertyListView::ICON_SUMMARY); $properties->addTextContent( - PhabricatorMarkupEngine::renderOneObject( - id(new PhabricatorMarkupOneOff()) - ->setContent($description), - 'default', - $viewer)); + new PHUIRemarkupView($viewer, $description)); } return $properties; diff --git a/src/applications/phid/query/PhabricatorObjectQuery.php b/src/applications/phid/query/PhabricatorObjectQuery.php index d4bcc9edf3..65e9938311 100644 --- a/src/applications/phid/query/PhabricatorObjectQuery.php +++ b/src/applications/phid/query/PhabricatorObjectQuery.php @@ -178,4 +178,40 @@ final class PhabricatorObjectQuery return null; } + + /** + * Select invalid or restricted PHIDs from a list. + * + * PHIDs are invalid if their objects do not exist or can not be seen by the + * viewer. This method is generally used to validate that PHIDs affected by + * a transaction are valid. + * + * @param PhabricatorUser Viewer. + * @param list List of ostensibly valid PHIDs. + * @return list List of invalid or restricted PHIDs. + */ + public static function loadInvalidPHIDsForViewer( + PhabricatorUser $viewer, + array $phids) { + + if (!$phids) { + return array(); + } + + $objects = id(new PhabricatorObjectQuery()) + ->setViewer($viewer) + ->withPHIDs($phids) + ->execute(); + $objects = mpull($objects, null, 'getPHID'); + + $invalid = array(); + foreach ($phids as $phid) { + if (empty($objects[$phid])) { + $invalid[] = $phid; + } + } + + return $invalid; + } + } diff --git a/src/applications/ponder/controller/PonderAnswerSaveController.php b/src/applications/ponder/controller/PonderAnswerSaveController.php index 3953fed23f..cac17acbed 100644 --- a/src/applications/ponder/controller/PonderAnswerSaveController.php +++ b/src/applications/ponder/controller/PonderAnswerSaveController.php @@ -32,7 +32,7 @@ final class PonderAnswerSaveController extends PonderController { return id(new AphrontDialogResponse())->setDialog($dialog); } - $answer = PonderAnswer::initializeNewAnswer($viewer); + $answer = PonderAnswer::initializeNewAnswer($viewer, $question); // Question Editor diff --git a/src/applications/ponder/storage/PonderAnswer.php b/src/applications/ponder/storage/PonderAnswer.php index 2c8aff23da..722fee8eec 100644 --- a/src/applications/ponder/storage/PonderAnswer.php +++ b/src/applications/ponder/storage/PonderAnswer.php @@ -26,15 +26,18 @@ final class PonderAnswer extends PonderDAO private $userVotes = array(); - public static function initializeNewAnswer(PhabricatorUser $actor) { + public static function initializeNewAnswer( + PhabricatorUser $actor, + PonderQuestion $question) { $app = id(new PhabricatorApplicationQuery()) ->setViewer($actor) ->withClasses(array('PhabricatorPonderApplication')) ->executeOne(); return id(new PonderAnswer()) - ->setQuestionID(0) + ->setQuestionID($question->getID()) ->setContent('') + ->attachQuestion($question) ->setAuthorPHID($actor->getPHID()) ->setVoteCount(0) ->setStatus(PonderAnswerStatus::ANSWER_STATUS_VISIBLE); diff --git a/src/applications/project/conduit/ProjectQueryConduitAPIMethod.php b/src/applications/project/conduit/ProjectQueryConduitAPIMethod.php index 3d059d064d..2512a9a019 100644 --- a/src/applications/project/conduit/ProjectQueryConduitAPIMethod.php +++ b/src/applications/project/conduit/ProjectQueryConduitAPIMethod.php @@ -112,7 +112,7 @@ final class ProjectQueryConduitAPIMethod extends ProjectConduitAPIMethod { $slug_map = array(); if ($slugs) { foreach ($slugs as $slug) { - $normal = rtrim(PhabricatorSlug::normalize($slug), '/'); + $normal = PhabricatorSlug::normalizeProjectSlug($slug); foreach ($projects as $project) { if (in_array($normal, $project['slugs'])) { $slug_map[$slug] = $project['phid']; diff --git a/src/applications/project/controller/PhabricatorProjectViewController.php b/src/applications/project/controller/PhabricatorProjectViewController.php index 45329285df..bfc9827ab5 100644 --- a/src/applications/project/controller/PhabricatorProjectViewController.php +++ b/src/applications/project/controller/PhabricatorProjectViewController.php @@ -26,10 +26,17 @@ final class PhabricatorProjectViewController } $project = $query->executeOne(); if (!$project) { + + // If this request corresponds to a project but just doesn't have the + // slug quite right, redirect to the proper URI. + $uri = $this->getNormalizedURI($slug); + if ($uri !== null) { + return id(new AphrontRedirectResponse())->setURI($uri); + } + return new Aphront404Response(); } - $columns = id(new PhabricatorProjectColumnQuery()) ->setViewer($viewer) ->withProjectPHIDs(array($project->getPHID())) @@ -53,4 +60,31 @@ final class PhabricatorProjectViewController return $this->delegateToController($controller_object); } + private function getNormalizedURI($slug) { + if (!strlen($slug)) { + return null; + } + + $normal = PhabricatorSlug::normalizeProjectSlug($slug); + if ($normal === $slug) { + return null; + } + + $viewer = $this->getViewer(); + + // Do execute() instead of executeOne() here so we canonicalize before + // raising a policy exception. This is a little more polished than letting + // the user hit the error on any variant of the slug. + + $projects = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withSlugs(array($normal)) + ->execute(); + if (!$projects) { + return null; + } + + return "/tag/{$normal}/"; + } + } diff --git a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php index c5253da956..7813195b02 100644 --- a/src/applications/project/editor/PhabricatorProjectTransactionEditor.php +++ b/src/applications/project/editor/PhabricatorProjectTransactionEditor.php @@ -81,9 +81,9 @@ final class PhabricatorProjectTransactionEditor switch ($xaction->getTransactionType()) { case PhabricatorProjectTransaction::TYPE_NAME: - $object->setName($xaction->getNewValue()); - // TODO - this is really "setPrimarySlug" - $object->setPhrictionSlug($xaction->getNewValue()); + $name = $xaction->getNewValue(); + $object->setName($name); + $object->setPrimarySlug(PhabricatorSlug::normalizeProjectSlug($name)); return; case PhabricatorProjectTransaction::TYPE_SLUGS: return; @@ -265,9 +265,8 @@ final class PhabricatorProjectTransactionEditor $errors[] = $error; } - $slug_builder = clone $object; - $slug_builder->setPhrictionSlug($name); - $slug = $slug_builder->getPrimarySlug(); + $slug = PhabricatorSlug::normalizeProjectSlug($name); + $slug_used_already = id(new PhabricatorProjectSlug()) ->loadOneWhere('slug = %s', $slug); if ($slug_used_already && @@ -498,9 +497,7 @@ final class PhabricatorProjectTransactionEditor PhabricatorLiskDAO $object, $name) { - $object = (clone $object); - $object->setPhrictionSlug($name); - $slug = $object->getPrimarySlug(); + $slug = PhabricatorSlug::normalizeProjectSlug($name); $slug_object = id(new PhabricatorProjectSlug())->loadOneWhere( 'slug = %s', diff --git a/src/applications/project/query/PhabricatorProjectQuery.php b/src/applications/project/query/PhabricatorProjectQuery.php index e607ff2dc5..e534e1c4bf 100644 --- a/src/applications/project/query/PhabricatorProjectQuery.php +++ b/src/applications/project/query/PhabricatorProjectQuery.php @@ -7,7 +7,6 @@ final class PhabricatorProjectQuery private $phids; private $memberPHIDs; private $slugs; - private $phrictionSlugs; private $names; private $nameTokens; private $icons; @@ -50,11 +49,6 @@ final class PhabricatorProjectQuery return $this; } - public function withPhrictionSlugs(array $slugs) { - $this->phrictionSlugs = $slugs; - return $this; - } - public function withNames(array $names) { $this->names = $names; return $this; @@ -308,13 +302,6 @@ final class PhabricatorProjectQuery $this->slugs); } - if ($this->phrictionSlugs !== null) { - $where[] = qsprintf( - $conn, - 'phrictionSlug IN (%Ls)', - $this->phrictionSlugs); - } - if ($this->names !== null) { $where[] = qsprintf( $conn, diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php index e5e6e19554..8bbc6e14ad 100644 --- a/src/applications/project/storage/PhabricatorProject.php +++ b/src/applications/project/storage/PhabricatorProject.php @@ -189,24 +189,11 @@ final class PhabricatorProject extends PhabricatorProjectDAO return $this->assertAttached($this->memberPHIDs); } - public function setPhrictionSlug($slug) { - - // NOTE: We're doing a little magic here and stripping out '/' so that - // project pages always appear at top level under projects/ even if the - // display name is "Hack / Slash" or similar (it will become - // 'hack_slash' instead of 'hack/slash'). - - $slug = str_replace('/', ' ', $slug); - $slug = PhabricatorSlug::normalize($slug); - $this->phrictionSlug = $slug; + public function setPrimarySlug($slug) { + $this->phrictionSlug = $slug.'/'; return $this; } - public function getFullPhrictionSlug() { - $slug = $this->getPhrictionSlug(); - return 'projects/'.$slug; - } - // TODO - once we sever project => phriction automagicalness, // migrate getPhrictionSlug to have no trailing slash and be called // getPrimarySlug diff --git a/src/applications/repository/constants/PhabricatorRepositoryVersion.php b/src/applications/repository/constants/PhabricatorRepositoryVersion.php index a68e07d56a..5f722fa40a 100644 --- a/src/applications/repository/constants/PhabricatorRepositoryVersion.php +++ b/src/applications/repository/constants/PhabricatorRepositoryVersion.php @@ -19,4 +19,22 @@ final class PhabricatorRepositoryVersion extends Phobject { return null; } + /** + * The `locate` command is deprecated as of Mercurial 3.2, to be + * replaced with `files` command, which supports most of the same + * arguments. This determines whether the new `files` command should + * be used instead of the `locate` command. + * + * @param string $mercurial_version - The current version of mercurial + * which can be retrieved by calling: + * PhabricatorRepositoryVersion::getMercurialVersion() + * + * @return boolean True if the version of Mercurial is new enough to support + * the `files` command, or false if otherwise. + */ + public static function isMercurialFilesCommandAvailable($mercurial_version) { + $min_version_for_files = '3.2'; + return version_compare($mercurial_version, $min_version_for_files, '>='); + } + } diff --git a/src/applications/repository/editor/PhabricatorRepositoryEditor.php b/src/applications/repository/editor/PhabricatorRepositoryEditor.php index 2e769975ab..af4226a4f6 100644 --- a/src/applications/repository/editor/PhabricatorRepositoryEditor.php +++ b/src/applications/repository/editor/PhabricatorRepositoryEditor.php @@ -44,6 +44,7 @@ final class PhabricatorRepositoryEditor $types[] = PhabricatorRepositoryTransaction::TYPE_SYMBOLS_LANGUAGE; $types[] = PhabricatorRepositoryTransaction::TYPE_SYMBOLS_SOURCES; $types[] = PhabricatorRepositoryTransaction::TYPE_STAGING_URI; + $types[] = PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS; $types[] = PhabricatorTransactions::TYPE_EDGE; $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY; @@ -107,6 +108,8 @@ final class PhabricatorRepositoryEditor return $object->getSymbolSources(); case PhabricatorRepositoryTransaction::TYPE_STAGING_URI: return $object->getDetail('staging-uri'); + case PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS: + return $object->getDetail('automation.blueprintPHIDs', array()); } } @@ -143,6 +146,7 @@ final class PhabricatorRepositoryEditor case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_LANGUAGE: case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_SOURCES: case PhabricatorRepositoryTransaction::TYPE_STAGING_URI: + case PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS: return $xaction->getNewValue(); case PhabricatorRepositoryTransaction::TYPE_NOTIFY: case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE: @@ -226,6 +230,11 @@ final class PhabricatorRepositoryEditor case PhabricatorRepositoryTransaction::TYPE_STAGING_URI: $object->setDetail('staging-uri', $xaction->getNewValue()); return; + case PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS: + $object->setDetail( + 'automation.blueprintPHIDs', + $xaction->getNewValue()); + return; case PhabricatorRepositoryTransaction::TYPE_ENCODING: // Make sure the encoding is valid by converting to UTF-8. This tests // that the user has mbstring installed, and also that they didn't type @@ -276,33 +285,17 @@ final class PhabricatorRepositoryEditor $editor->save(); break; + case PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS: + DrydockAuthorization::applyAuthorizationChanges( + $this->getActor(), + $object->getPHID(), + $xaction->getOldValue(), + $xaction->getNewValue()); + break; } } - protected function mergeTransactions( - PhabricatorApplicationTransaction $u, - PhabricatorApplicationTransaction $v) { - - $type = $u->getTransactionType(); - switch ($type) {} - - return parent::mergeTransactions($u, $v); - } - - protected function transactionHasEffect( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - - $old = $xaction->getOldValue(); - $new = $xaction->getNewValue(); - - $type = $xaction->getTransactionType(); - switch ($type) {} - - return parent::transactionHasEffect($object, $xaction); - } - protected function requireCapabilities( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { @@ -338,6 +331,7 @@ final class PhabricatorRepositoryEditor case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_SOURCES: case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_LANGUAGE: case PhabricatorRepositoryTransaction::TYPE_STAGING_URI: + case PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS: PhabricatorPolicyFilter::requireCapability( $this->requireActor(), $object, @@ -431,6 +425,29 @@ final class PhabricatorRepositoryEditor } } break; + + case PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS: + foreach ($xactions as $xaction) { + $old = nonempty($xaction->getOldValue(), array()); + $new = nonempty($xaction->getNewValue(), array()); + + $add = array_diff($new, $old); + + $invalid = PhabricatorObjectQuery::loadInvalidPHIDsForViewer( + $this->getActor(), + $add); + if ($invalid) { + $errors[] = new PhabricatorApplicationTransactionValidationError( + $type, + pht('Invalid'), + pht( + 'Some of the selected automation blueprints are invalid '. + 'or restricted: %s.', + implode(', ', $invalid)), + $xaction); + } + } + break; } return $errors; diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index 084f531a10..c7ef71e9b3 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -1799,7 +1799,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO } -/* -( Staging )-------------------------------------------------------------*/ +/* -( Staging )------------------------------------------------------------ */ public function supportsStaging() { @@ -1815,6 +1815,33 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO } +/* -( Automation )--------------------------------------------------------- */ + + + public function supportsAutomation() { + return $this->isGit(); + } + + public function canPerformAutomation() { + if (!$this->supportsAutomation()) { + return false; + } + + if (!$this->getAutomationBlueprintPHIDs()) { + return false; + } + + return true; + } + + public function getAutomationBlueprintPHIDs() { + if (!$this->supportsAutomation()) { + return array(); + } + return $this->getDetail('automation.blueprintPHIDs', array()); + } + + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ diff --git a/src/applications/repository/storage/PhabricatorRepositoryTransaction.php b/src/applications/repository/storage/PhabricatorRepositoryTransaction.php index de1310f320..c9077e0236 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryTransaction.php +++ b/src/applications/repository/storage/PhabricatorRepositoryTransaction.php @@ -28,6 +28,7 @@ final class PhabricatorRepositoryTransaction const TYPE_SYMBOLS_SOURCES = 'repo:symbol-source'; const TYPE_SYMBOLS_LANGUAGE = 'repo:symbol-language'; const TYPE_STAGING_URI = 'repo:staging-uri'; + const TYPE_AUTOMATION_BLUEPRINTS = 'repo:automation-blueprints'; // TODO: Clean up these legacy transaction types. const TYPE_SSH_LOGIN = 'repo:ssh-login'; @@ -65,6 +66,7 @@ final class PhabricatorRepositoryTransaction } break; case self::TYPE_SYMBOLS_SOURCES: + case self::TYPE_AUTOMATION_BLUEPRINTS: if ($old) { $phids = array_merge($phids, $old); } @@ -436,6 +438,34 @@ final class PhabricatorRepositoryTransaction $old, $new); } + + case self::TYPE_AUTOMATION_BLUEPRINTS: + $add = array_diff($new, $old); + $rem = array_diff($old, $new); + + if ($add && $rem) { + return pht( + '%s changed %s automation blueprint(s), '. + 'added %s: %s; removed %s: %s.', + $this->renderHandleLink($author_phid), + new PhutilNumber(count($add) + count($rem)), + new PhutilNumber(count($add)), + $this->renderHandleList($add), + new PhutilNumber(count($rem)), + $this->renderHandleList($rem)); + } else if ($add) { + return pht( + '%s added %s automation blueprint(s): %s.', + $this->renderHandleLink($author_phid), + new PhutilNumber(count($add)), + $this->renderHandleList($add)); + } else { + return pht( + '%s removed %s automation blueprint(s): %s.', + $this->renderHandleLink($author_phid), + new PhutilNumber(count($rem)), + $this->renderHandleList($rem)); + } } return parent::getTitle(); diff --git a/src/docs/user/userguide/drydock.diviner b/src/docs/user/userguide/drydock.diviner index a03346f1cb..49b1f1df9e 100644 --- a/src/docs/user/userguide/drydock.diviner +++ b/src/docs/user/userguide/drydock.diviner @@ -58,3 +58,12 @@ a corresponding resource by either finding a suitable unused resource or creating a new resource. When work completes, the resource is returned to the resource pool or destroyed. +Next Steps +========== + +Continue by: + + - understanding Drydock security concerns with + @{article:Drydock User Guide: Security}; or + - allowing Phabricator to write to repositories with + @{article:Drydock User Guide: Repository Automation}. diff --git a/src/docs/user/userguide/drydock_repository_automation.diviner b/src/docs/user/userguide/drydock_repository_automation.diviner new file mode 100644 index 0000000000..a16e8d33e9 --- /dev/null +++ b/src/docs/user/userguide/drydock_repository_automation.diviner @@ -0,0 +1,39 @@ +@title Drydock User Guide: Repository Automation +@group userguide + +Configuring repository automation so Phabricator can push commits. + + +Overview +======== + +IMPORTANT: This feature is very new and most of the capabilities described +in this document are not yet available. This feature as a whole is a prototype. + +By configuring Drydock and Diffusion appropriately, you can enable **Repository +Automation** for a repository. Once automation is set up, Phabricator will be +able to make changes to the repository. + + +Security +======== + +Configuring repository automation amounts to telling Phabricator where it +should perform working copy operations (like merges, cherry-picks and pushes) +when doing writes. + +Depending on how stringent you are about change control, you may want to +make sure these processes are isolated and can not be tampered with. If you +run tests and automation on the same hardware, tests may be able to interfere +with automation. You can read more about this in +@{article:Drydock User Guide: Security}. + + +Next Steps +========== + +Continue by: + + - understanding Drydock security concerns with + @{article:Drydock User Guide: Security}; or + - returning to the @{article:Drydock User Guide}. diff --git a/src/docs/user/userguide/drydock_security.diviner b/src/docs/user/userguide/drydock_security.diviner new file mode 100644 index 0000000000..f12586ab35 --- /dev/null +++ b/src/docs/user/userguide/drydock_security.diviner @@ -0,0 +1,209 @@ +@title Drydock User Guide: Security +@group userguide + +Understanding security concerns in Drydock. + +Overview +======== + +Different applications use Drydock for different things, and some of the things +they do with Drydock require different levels of trust and access. It is +important to configure Drydock properly so that less trusted code can't do +anything you aren't comfortable with. + +For example, running unit tests on Drydock normally involves running relatively +untrusted code (it often has a single author and has not yet been reviewed) +that needs very few capabilities (generally, it only needs to be able to report +results back to Phabricator). In contrast, automating merge requests on Drydock +involves running trusted code that needs more access (it must be able to write +to repositories). + +Drydock allows resources to be shared and reused, so it's possible to configure +Drydock in a way that gives untrusted code a lot of access by accident. + +One way Drydock makes allocations faster is by sharing, reusing, and recycling +resources. When an application asks Drydock for a working copy, it will try to +satisfy the request by cleaning up a suitable existing working copy if it can, +instead of building a new one. This is faster, but it means that tasks have +some ability to interact or interfere with each other. + +Similarly, Drydock may allocate multiple leases on the same host at the same +time, running as the same user. This is generally simpler to configure and less +wasteful than fully isolating leases, but means that they can interact. + +Depending on your organization, environment and use cases, you might not want +this, and it may be important that different use cases are unable to interfere +with each other. For example, you might want to prevent unit tests from writing +to repositories. + +**Drydock does not guarantee that resources are isolated by default**. When +resources are more isolated, they are usually also harder to configure and +slower to allocate. Because most installs will want to find a balance between +isolation and complexity/performance, Drydock does not make assumptions about +either isolation or performance having absolute priority. + +You'll usually want to isolate things just enough that nothing bad can happen. +Fortunately, this is straightforward. This document describes how to make sure +you have enough isolation so that nothing you're uncomfortable with can occur. + + +Choosing an Isolation Policy +============================ + +This section provides some reasonable examples of ways you might approach +configuring Drydock. + +| Isolation | Suitable For | Description +|-----------|-----|------- +| Zero | Development | Everything on one host. +| Low | Small Installs | Use a dedicated Drydock host. +| High | Most Installs | **Recommended**. Use low-trust and high-trust pools. +| Custom | Special Requirements | Use multiple pools. +| Absolute | Special Requirements | Completely isolate all resources. + +**Zero Isolation**: Run Drydock operations on the same host that Phabricator +runs on. This is only suitable for developing or testing Phabricator. Any +Drydock operation can potentially compromise Phabricator. It is intentionally +difficult to configure Drydock to operate in this mode. Running Drydock +operations on the Phabricator host is strongly discouraged. + +**Low Isolation**: Designate a separate Drydock host and run Drydock +operations on it. This is suitable for small installs and provides a reasonable +level of isolation. However, it will allow unit tests (which often run +lower-trust code) to interfere with repository automation operations. + +**High Isolation**: Designate two Drydock host pools and run low-trust +operations (like builds) on one pool and high-trust operations (like repository +automation) on a separate pool. This provides a good balance between isolation +and performance, although tests can still potentially interfere with the +execution of unrelated tests. + +**Custom Isolation**: You can continue adding pools to refine the resource +isolation model. For example, you may have higher-trust and lower-trust +repositories or do builds on a mid-trust tier which runs only reviewed code. + +**Absolute Isolation**: Configure blueprints to completely initialize and +destroy hosts or containers on every request, and limit all resources to one +simultaneous lease. This will completely isolate every operation, but come at +a high performance and complexity cost. + +NOTE: It is not currently possible to configure Drydock in an absolute +isolation mode. + +It is usually reasonable to choose one of these approaches as a starting point +and then adjust it to fit your requirements. You can also evolve your use of +Drydock over time as your needs change. + + +Threat Scenarios +================ + +This section will help you understand the threats to a Drydock environment. +Not all threats will be concerning to all installs, and you can choose an +approach which defuses only the threats you care about. + +Attackers have three primary targets: + + - capturing hosts; + - compromising Phabricator; and + - compromising the integrity of other Drydock processes. + +**Attacks against hosts** are the least sophisticated. In this scenario, an +attacker wants to run a program like a Bitcoin miner or botnet client on +hardware that they aren't paying for or which can't be traced to them. They +write a "unit test" or which launches this software, then send a revision +containing this "unit test" for review. If Phabricator is configured to +automatically run tests on new revisions, it may execute automatically and give +the attacker access to computing resources they did not previously control and +which can not easily be traced back to them. + +This is usually only a meaningful threat for open source installs, because +there is a high probability of eventual detection and the value of these +resources is small, so employees will generally not have an incentive to +attempt this sort of attack. The easiest way to prevent this attack is to +prevent untrusted, anonymous contributors from running tests. For example, +create a "Trusted Contributors" project and only run tests if a revision author +is a member of the project. + +**Attacks against Phabricator** are more sophisticated. In this scenario, an +attacker tries to compromise Phabricator itself (for example, to make themselves +an administrator or gain access to an administrator account). + +This is made possible if Drydock is running on the same host as Phabricator or +runs on a privileged subnet with access to resources like Phabricator database +hosts. Most installs should be concerned about this attack. + +The best way to defuse this attack is to run Drydock processes on a separate +host which is not on a privileged subnet. For example, use a +`build.mycompany.com` host or pool for Drydock processes, separate from your +`phabricator.mycompany.com` host or pool. + +Even if the host is not privileged, many Drydock processes have some level of +privilege (enabling them to clone repositories, or report test results back to +Phabricator). Be aware that tests can hijack credentials they are run with, +and potentialy hijack credentials given to other processes on the same hosts. +You should use credentials with a minimum set of privileges and assume all +processes on a host have the highest level of access that any process on the +host has. + +**Attacks against Drydock** are the most sophisticated. In this scenario, an +attacker uses one Drydock process to compromise a different process: for +example, a unit test which tampers with a merge or injects code into a build. +This might allow an attacker to make changes to a repository or binary without +going through review or triggering other rules which would normally detect the +change. + +These attackers could also make failing tests appear to pass, or break tests or +builds, but these attacks are generally less interesting than tampering with +a repository or binary. + +This is a complex attack which you may not have to worry about unless you have +a high degree of process and control in your change pipeline. If users can push +changes directly to repositories, this often represents a faster and easier way +to achieve the same tampering. + +The best way to defuse this attack is to prevent high-trust (repository +automation) processes from running on the same hosts as low-trust (unit test) +processes. For example, use an `automation.mycompany.com` host or pool for +repository automation, and a `build.mycompany.com` host or pool for tests. + + +Applying an Isolation Policy +============================ + +Designing a security and isolation policy for Drydock can take some thought, +but applying it is straightforward. Applications which want to use Drydock must +explicitly list which blueprints they are allowed to use, and they must be +approved to use them in Drydock. By default, nothing can do anything, which is +very safe and secure. + +To get builds or automation running on a host, specify the host blueprint as a +usable blueprint in the build step or repository configuration. This creates a +new authorization request in Drydock which must be approved before things can +move forward. + +Until the authorization is approved, the process can not use the blueprint to +create any resources, nor can it use resources previously created by the +blueprint. + +You can review and approve requests from the blueprint detail view in Drydock: +find the request and click {nav Approve Authorization}. You can also revoke +approval at any time from this screen which will prevent the object from +continuing to use the blueprint (but note that this does not release any +existing leases). + +Once the authorization request is approved, the build or automation process +should be able to run if everything else is configured properly. + +Note that authorizations are transitive: if a build step is authorized to use +blueprint A, and blueprint A is authorized to use blueprint B, the build step +may indirectly operate on resources created by blueprint B. This should +normally be consistent with expectations. + + +Next Steps +========== + +Continue by: + + - returning to the @{article:Drydock User Guide}. diff --git a/src/infrastructure/customfield/field/PhabricatorCustomField.php b/src/infrastructure/customfield/field/PhabricatorCustomField.php index f9f5979029..64639ed34f 100644 --- a/src/infrastructure/customfield/field/PhabricatorCustomField.php +++ b/src/infrastructure/customfield/field/PhabricatorCustomField.php @@ -540,9 +540,7 @@ abstract class PhabricatorCustomField extends Phobject { * @task storage */ public function newStorageObject() { - if ($this->proxy) { - return $this->proxy->newStorageObject(); - } + // NOTE: This intentionally isn't proxied, to avoid call cycles. throw new PhabricatorCustomFieldImplementationIncompleteException($this); } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php index 76a1de9989..ac3e163f6a 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php @@ -186,7 +186,12 @@ abstract class PhabricatorStandardCustomField } public function shouldUseStorage() { - return true; + try { + $object = $this->newStorageObject(); + return true; + } catch (PhabricatorCustomFieldImplementationIncompleteException $ex) { + return false; + } } public function getValueForStorage() { diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldBlueprints.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldBlueprints.php new file mode 100644 index 0000000000..ad2bb62d81 --- /dev/null +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldBlueprints.php @@ -0,0 +1,41 @@ +decodeValue($xaction->getOldValue()); + $new = $this->decodeValue($xaction->getNewValue()); + + DrydockAuthorization::applyAuthorizationChanges( + $this->getViewer(), + $xaction->getObjectPHID(), + $old, + $new); + } + + public function renderPropertyViewValue(array $handles) { + $value = $this->getFieldValue(); + if (!$value) { + return phutil_tag('em', array(), pht('No authorized blueprints.')); + } + + return id(new DrydockObjectAuthorizationView()) + ->setUser($this->getViewer()) + ->setObjectPHID($this->getObject()->getPHID()) + ->setBlueprintPHIDs($value); + } + + + +} diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php index ecaf67caa9..81b94aff31 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php @@ -158,22 +158,9 @@ abstract class PhabricatorStandardCustomFieldPHIDs $add = array_diff($new, $old); - if (!$add) { - continue; - } - - $objects = id(new PhabricatorObjectQuery()) - ->setViewer($editor->getActor()) - ->withPHIDs($add) - ->execute(); - $objects = mpull($objects, null, 'getPHID'); - - $invalid = array(); - foreach ($add as $phid) { - if (empty($objects[$phid])) { - $invalid[] = $phid; - } - } + $invalid = PhabricatorObjectQuery::loadInvalidPHIDsForViewer( + $editor->getActor(), + $add); if ($invalid) { $error = new PhabricatorApplicationTransactionValidationError( @@ -217,7 +204,7 @@ abstract class PhabricatorStandardCustomFieldPHIDs return array(); } - private function decodeValue($value) { + protected function decodeValue($value) { $value = json_decode($value); if (!is_array($value)) { $value = array(); diff --git a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php index beda39c21c..a3c1833468 100644 --- a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php +++ b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php @@ -1403,6 +1403,23 @@ final class PhabricatorUSEnglishTranslation 'Waiting %s seconds for lease to activate.', ), + '%s changed %s automation blueprint(s), added %s: %s; removed %s: %s.' => + '%s changed automation blueprints, added: %4$s; removed: %6$s.', + + '%s added %s automation blueprint(s): %s.' => array( + array( + '%s added an automation blueprint: %3$s.', + '%s added automation blueprints: %3$s.', + ), + ), + + '%s removed %s automation blueprint(s): %s.' => array( + array( + '%s removed an automation blueprint: %3$s.', + '%s removed automation blueprints: %3$s.', + ), + ), + ); } diff --git a/src/infrastructure/markup/PhabricatorMarkupOneOff.php b/src/infrastructure/markup/PhabricatorMarkupOneOff.php index 7f1708745c..0bfba1722f 100644 --- a/src/infrastructure/markup/PhabricatorMarkupOneOff.php +++ b/src/infrastructure/markup/PhabricatorMarkupOneOff.php @@ -1,16 +1,7 @@ setContent($some_content), - * 'default', - * $viewer); - * - * This is less efficient than batching rendering, but appropriate for small - * amounts of one-off text in form instructions. + * DEPRECATED. Use @{class:PHUIRemarkupView}. */ final class PhabricatorMarkupOneOff extends Phobject diff --git a/src/infrastructure/markup/view/PHUIRemarkupView.php b/src/infrastructure/markup/view/PHUIRemarkupView.php new file mode 100644 index 0000000000..0272c9c3a9 --- /dev/null +++ b/src/infrastructure/markup/view/PHUIRemarkupView.php @@ -0,0 +1,32 @@ +appendChild($fancy_text); + * + */ +final class PHUIRemarkupView extends AphrontView { + + private $corpus; + + public function __construct(PhabricatorUser $viewer, $corpus) { + $this->setUser($viewer); + $this->corpus = $corpus; + } + + public function render() { + $viewer = $this->getUser(); + $corpus = $this->corpus; + + return PhabricatorMarkupEngine::renderOneObject( + id(new PhabricatorMarkupOneOff()) + ->setContent($corpus), + 'default', + $viewer); + } + +} diff --git a/src/infrastructure/util/PhabricatorSlug.php b/src/infrastructure/util/PhabricatorSlug.php index 53330391b1..fd169914fe 100644 --- a/src/infrastructure/util/PhabricatorSlug.php +++ b/src/infrastructure/util/PhabricatorSlug.php @@ -2,17 +2,49 @@ final class PhabricatorSlug extends Phobject { - public static function normalize($slug) { + public static function normalizeProjectSlug($slug) { + $slug = str_replace('/', ' ', $slug); + $slug = self::normalize($slug, $hashtag = true); + return rtrim($slug, '/'); + } + + public static function normalize($slug, $hashtag = false) { $slug = preg_replace('@/+@', '/', $slug); $slug = trim($slug, '/'); $slug = phutil_utf8_strtolower($slug); - $slug = preg_replace("@[\\x00-\\x19#%&+=\\\\?<> ]+@", '_', $slug); + + $ban = + // Ban control characters since users can't type them and they create + // various other problems with parsing and rendering. + "\\x00-\\x19". + + // Ban characters with special meanings in URIs (and spaces), since we + // want slugs to produce nice URIs. + "#%&+=?". + " ". + + // Ban backslashes and various brackets for parsing and URI quality. + "\\\\". + "<>{}\\[\\]". + + // Ban single and double quotes since they can mess up URIs. + "'". + '"'; + + // In hashtag mode (used for Project hashtags), ban additional characters + // which cause parsing problems. + if ($hashtag) { + $ban .= '`~!@$^*,:;(|)'; + } + + $slug = preg_replace('(['.$ban.']+)', '_', $slug); $slug = preg_replace('@_+@', '_', $slug); + $parts = explode('/', $slug); + // Remove leading and trailing underscores from each component, if the // component has not been reduced to a single underscore. For example, "a?" // converts to "a", but "??" converts to "_". - $parts = explode('/', $slug); foreach ($parts as $key => $part) { if ($part != '_') { $parts[$key] = trim($part, '_'); diff --git a/src/infrastructure/util/__tests__/PhabricatorSlugTestCase.php b/src/infrastructure/util/__tests__/PhabricatorSlugTestCase.php index 0f96a735e6..6a801bb54c 100644 --- a/src/infrastructure/util/__tests__/PhabricatorSlugTestCase.php +++ b/src/infrastructure/util/__tests__/PhabricatorSlugTestCase.php @@ -34,6 +34,9 @@ final class PhabricatorSlugTestCase extends PhabricatorTestCase { 'a/??/c' => 'a/_/c/', 'a/?b/c' => 'a/b/c/', 'a/b?/c' => 'a/b/c/', + 'a - b' => 'a_-_b/', + 'a[b]' => 'a_b/', + 'ab!' => 'ab!/', ); foreach ($slugs as $slug => $normal) { @@ -44,6 +47,24 @@ final class PhabricatorSlugTestCase extends PhabricatorTestCase { } } + public function testProjectSlugs() { + $slugs = array( + 'a:b' => 'a_b', + 'a!b' => 'a_b', + 'a - b' => 'a_-_b', + '' => '', + 'Demonology: HSA (Hexes, Signs, Alchemy)' => + 'demonology_hsa_hexes_signs_alchemy', + ); + + foreach ($slugs as $slug => $normal) { + $this->assertEqual( + $normal, + PhabricatorSlug::normalizeProjectSlug($slug), + pht('Hashtag normalization of "%s"', $slug)); + } + } + public function testSlugAncestry() { $slugs = array( '/' => array(), diff --git a/src/view/phui/PHUITimelineEventView.php b/src/view/phui/PHUITimelineEventView.php index 519cbdf768..f9f1d4d3b4 100644 --- a/src/view/phui/PHUITimelineEventView.php +++ b/src/view/phui/PHUITimelineEventView.php @@ -374,10 +374,11 @@ final class PHUITimelineEventView extends AphrontView { $badges = null; if ($image_uri) { $image = phutil_tag( - 'div', + ($this->userHandle->getURI()) ? 'a' : 'div', array( 'style' => 'background-image: url('.$image_uri.')', 'class' => 'phui-timeline-image', + 'href' => $this->userHandle->getURI(), ), ''); if ($this->badges) { diff --git a/webroot/rsrc/css/application/diffusion/diffusion-source.css b/webroot/rsrc/css/application/diffusion/diffusion-source.css index abf68e3aeb..ca52d53a79 100644 --- a/webroot/rsrc/css/application/diffusion/diffusion-source.css +++ b/webroot/rsrc/css/application/diffusion/diffusion-source.css @@ -4,13 +4,12 @@ .diffusion-source { width: 100%; - font-family: "Monaco", Consolas, monospace; - font-size: 10px; + font: 10px/13px "Menlo", "Consolas", "Monaco", monospace; background: #fff; } .diffusion-source tr.phabricator-source-highlight { - background: #ffff00; + background: {$sh-yellowbackground}; } .diffusion-source th { @@ -19,7 +18,6 @@ background: {$lightgreybackground}; color: {$bluetext}; border-right: 1px solid {$thinblueborder}; - font-size: {$smallestfontsize}; } .diffusion-source td { diff --git a/webroot/rsrc/css/phui/phui-timeline-view.css b/webroot/rsrc/css/phui/phui-timeline-view.css index de6658f7dc..d69df93953 100644 --- a/webroot/rsrc/css/phui/phui-timeline-view.css +++ b/webroot/rsrc/css/phui/phui-timeline-view.css @@ -102,6 +102,7 @@ border-radius: 3px; box-shadow: {$borderinset}; background-size: 100%; + display: block; } .device-desktop .phui-timeline-major-event .phui-timeline-image {