diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 1ad3565809..32985c76c0 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1525,10 +1525,6 @@ phutil_register_library_map(array( 'ManiphestEditProjectsCapability' => 'applications/maniphest/capability/ManiphestEditProjectsCapability.php', 'ManiphestEditStatusCapability' => 'applications/maniphest/capability/ManiphestEditStatusCapability.php', 'ManiphestEmailCommand' => 'applications/maniphest/command/ManiphestEmailCommand.php', - 'ManiphestExcelDefaultFormat' => 'applications/maniphest/export/ManiphestExcelDefaultFormat.php', - 'ManiphestExcelFormat' => 'applications/maniphest/export/ManiphestExcelFormat.php', - 'ManiphestExcelFormatTestCase' => 'applications/maniphest/export/__tests__/ManiphestExcelFormatTestCase.php', - 'ManiphestExportController' => 'applications/maniphest/controller/ManiphestExportController.php', 'ManiphestGetTaskTransactionsConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestGetTaskTransactionsConduitAPIMethod.php', 'ManiphestHovercardEngineExtension' => 'applications/maniphest/engineextension/ManiphestHovercardEngineExtension.php', 'ManiphestInfoConduitAPIMethod' => 'applications/maniphest/conduit/ManiphestInfoConduitAPIMethod.php', @@ -2230,9 +2226,10 @@ phutil_register_library_map(array( 'PhabricatorBulkContentSource' => 'infrastructure/daemon/contentsource/PhabricatorBulkContentSource.php', 'PhabricatorBulkEditGroup' => 'applications/transactions/bulk/PhabricatorBulkEditGroup.php', 'PhabricatorBulkEngine' => 'applications/transactions/bulk/PhabricatorBulkEngine.php', + 'PhabricatorBulkManagementExportWorkflow' => 'applications/transactions/bulk/management/PhabricatorBulkManagementExportWorkflow.php', 'PhabricatorBulkManagementMakeSilentWorkflow' => 'applications/transactions/bulk/management/PhabricatorBulkManagementMakeSilentWorkflow.php', 'PhabricatorBulkManagementWorkflow' => 'applications/transactions/bulk/management/PhabricatorBulkManagementWorkflow.php', - 'PhabricatorCSVExportFormat' => 'infrastructure/export/PhabricatorCSVExportFormat.php', + 'PhabricatorCSVExportFormat' => 'infrastructure/export/format/PhabricatorCSVExportFormat.php', 'PhabricatorCacheDAO' => 'applications/cache/storage/PhabricatorCacheDAO.php', 'PhabricatorCacheEngine' => 'applications/system/engine/PhabricatorCacheEngine.php', 'PhabricatorCacheEngineExtension' => 'applications/system/engine/PhabricatorCacheEngineExtension.php', @@ -2583,6 +2580,7 @@ phutil_register_library_map(array( 'PhabricatorCustomFieldEditEngineExtension' => 'infrastructure/customfield/engineextension/PhabricatorCustomFieldEditEngineExtension.php', 'PhabricatorCustomFieldEditField' => 'infrastructure/customfield/editor/PhabricatorCustomFieldEditField.php', 'PhabricatorCustomFieldEditType' => 'infrastructure/customfield/editor/PhabricatorCustomFieldEditType.php', + 'PhabricatorCustomFieldExportEngineExtension' => 'infrastructure/export/engine/PhabricatorCustomFieldExportEngineExtension.php', 'PhabricatorCustomFieldFulltextEngineExtension' => 'infrastructure/customfield/engineextension/PhabricatorCustomFieldFulltextEngineExtension.php', 'PhabricatorCustomFieldHeraldAction' => 'infrastructure/customfield/herald/PhabricatorCustomFieldHeraldAction.php', 'PhabricatorCustomFieldHeraldActionGroup' => 'infrastructure/customfield/herald/PhabricatorCustomFieldHeraldActionGroup.php', @@ -2840,15 +2838,20 @@ phutil_register_library_map(array( 'PhabricatorEnv' => 'infrastructure/env/PhabricatorEnv.php', 'PhabricatorEnvTestCase' => 'infrastructure/env/__tests__/PhabricatorEnvTestCase.php', 'PhabricatorEpochEditField' => 'applications/transactions/editfield/PhabricatorEpochEditField.php', - 'PhabricatorEpochExportField' => 'infrastructure/export/PhabricatorEpochExportField.php', + 'PhabricatorEpochExportField' => 'infrastructure/export/field/PhabricatorEpochExportField.php', 'PhabricatorEvent' => 'infrastructure/events/PhabricatorEvent.php', 'PhabricatorEventEngine' => 'infrastructure/events/PhabricatorEventEngine.php', 'PhabricatorEventListener' => 'infrastructure/events/PhabricatorEventListener.php', 'PhabricatorEventType' => 'infrastructure/events/constant/PhabricatorEventType.php', 'PhabricatorExampleEventListener' => 'infrastructure/events/PhabricatorExampleEventListener.php', + 'PhabricatorExcelExportFormat' => 'infrastructure/export/format/PhabricatorExcelExportFormat.php', 'PhabricatorExecFutureFileUploadSource' => 'applications/files/uploadsource/PhabricatorExecFutureFileUploadSource.php', - 'PhabricatorExportField' => 'infrastructure/export/PhabricatorExportField.php', - 'PhabricatorExportFormat' => 'infrastructure/export/PhabricatorExportFormat.php', + 'PhabricatorExportEngine' => 'infrastructure/export/engine/PhabricatorExportEngine.php', + 'PhabricatorExportEngineBulkJobType' => 'infrastructure/export/engine/PhabricatorExportEngineBulkJobType.php', + 'PhabricatorExportEngineExtension' => 'infrastructure/export/engine/PhabricatorExportEngineExtension.php', + 'PhabricatorExportField' => 'infrastructure/export/field/PhabricatorExportField.php', + 'PhabricatorExportFormat' => 'infrastructure/export/format/PhabricatorExportFormat.php', + 'PhabricatorExportFormatSetting' => 'infrastructure/export/engine/PhabricatorExportFormatSetting.php', 'PhabricatorExtendedPolicyInterface' => 'applications/policy/interface/PhabricatorExtendedPolicyInterface.php', 'PhabricatorExtendingPhabricatorConfigOptions' => 'applications/config/option/PhabricatorExtendingPhabricatorConfigOptions.php', 'PhabricatorExtensionsSetupCheck' => 'applications/config/check/PhabricatorExtensionsSetupCheck.php', @@ -3069,7 +3072,7 @@ phutil_register_library_map(array( 'PhabricatorHomeProfileMenuItem' => 'applications/home/menuitem/PhabricatorHomeProfileMenuItem.php', 'PhabricatorHovercardEngineExtension' => 'applications/search/engineextension/PhabricatorHovercardEngineExtension.php', 'PhabricatorHovercardEngineExtensionModule' => 'applications/search/engineextension/PhabricatorHovercardEngineExtensionModule.php', - 'PhabricatorIDExportField' => 'infrastructure/export/PhabricatorIDExportField.php', + 'PhabricatorIDExportField' => 'infrastructure/export/field/PhabricatorIDExportField.php', 'PhabricatorIDsSearchEngineExtension' => 'applications/search/engineextension/PhabricatorIDsSearchEngineExtension.php', 'PhabricatorIDsSearchField' => 'applications/search/field/PhabricatorIDsSearchField.php', 'PhabricatorIconDatasource' => 'applications/files/typeahead/PhabricatorIconDatasource.php', @@ -3093,7 +3096,7 @@ phutil_register_library_map(array( 'PhabricatorInlineSummaryView' => 'infrastructure/diff/view/PhabricatorInlineSummaryView.php', 'PhabricatorInstructionsEditField' => 'applications/transactions/editfield/PhabricatorInstructionsEditField.php', 'PhabricatorIntConfigType' => 'applications/config/type/PhabricatorIntConfigType.php', - 'PhabricatorIntExportField' => 'infrastructure/export/PhabricatorIntExportField.php', + 'PhabricatorIntExportField' => 'infrastructure/export/field/PhabricatorIntExportField.php', 'PhabricatorInternalSetting' => 'applications/settings/setting/PhabricatorInternalSetting.php', 'PhabricatorInternationalizationManagementExtractWorkflow' => 'infrastructure/internationalization/management/PhabricatorInternationalizationManagementExtractWorkflow.php', 'PhabricatorInternationalizationManagementWorkflow' => 'infrastructure/internationalization/management/PhabricatorInternationalizationManagementWorkflow.php', @@ -3103,7 +3106,7 @@ phutil_register_library_map(array( 'PhabricatorIteratorFileUploadSource' => 'applications/files/uploadsource/PhabricatorIteratorFileUploadSource.php', 'PhabricatorJIRAAuthProvider' => 'applications/auth/provider/PhabricatorJIRAAuthProvider.php', 'PhabricatorJSONConfigType' => 'applications/config/type/PhabricatorJSONConfigType.php', - 'PhabricatorJSONExportFormat' => 'infrastructure/export/PhabricatorJSONExportFormat.php', + 'PhabricatorJSONExportFormat' => 'infrastructure/export/format/PhabricatorJSONExportFormat.php', 'PhabricatorJavelinLinter' => 'infrastructure/lint/linter/PhabricatorJavelinLinter.php', 'PhabricatorJiraIssueHasObjectEdgeType' => 'applications/doorkeeper/edge/PhabricatorJiraIssueHasObjectEdgeType.php', 'PhabricatorJumpNavHandler' => 'applications/search/engine/PhabricatorJumpNavHandler.php', @@ -3126,9 +3129,11 @@ phutil_register_library_map(array( 'PhabricatorLipsumManagementWorkflow' => 'applications/lipsum/management/PhabricatorLipsumManagementWorkflow.php', 'PhabricatorLipsumMondrianArtist' => 'applications/lipsum/image/PhabricatorLipsumMondrianArtist.php', 'PhabricatorLiskDAO' => 'infrastructure/storage/lisk/PhabricatorLiskDAO.php', + 'PhabricatorLiskExportEngineExtension' => 'infrastructure/export/engine/PhabricatorLiskExportEngineExtension.php', 'PhabricatorLiskFulltextEngineExtension' => 'applications/search/engineextension/PhabricatorLiskFulltextEngineExtension.php', 'PhabricatorLiskSearchEngineExtension' => 'applications/search/engineextension/PhabricatorLiskSearchEngineExtension.php', 'PhabricatorLiskSerializer' => 'infrastructure/storage/lisk/PhabricatorLiskSerializer.php', + 'PhabricatorListExportField' => 'infrastructure/export/field/PhabricatorListExportField.php', 'PhabricatorLocalDiskFileStorageEngine' => 'applications/files/engine/PhabricatorLocalDiskFileStorageEngine.php', 'PhabricatorLocalTimeTestCase' => 'view/__tests__/PhabricatorLocalTimeTestCase.php', 'PhabricatorLocaleScopeGuard' => 'infrastructure/internationalization/scope/PhabricatorLocaleScopeGuard.php', @@ -3423,10 +3428,11 @@ phutil_register_library_map(array( 'PhabricatorPHDConfigOptions' => 'applications/config/option/PhabricatorPHDConfigOptions.php', 'PhabricatorPHID' => 'applications/phid/storage/PhabricatorPHID.php', 'PhabricatorPHIDConstants' => 'applications/phid/PhabricatorPHIDConstants.php', - 'PhabricatorPHIDExportField' => 'infrastructure/export/PhabricatorPHIDExportField.php', + 'PhabricatorPHIDExportField' => 'infrastructure/export/field/PhabricatorPHIDExportField.php', 'PhabricatorPHIDInterface' => 'applications/phid/interface/PhabricatorPHIDInterface.php', 'PhabricatorPHIDListEditField' => 'applications/transactions/editfield/PhabricatorPHIDListEditField.php', 'PhabricatorPHIDListEditType' => 'applications/transactions/edittype/PhabricatorPHIDListEditType.php', + 'PhabricatorPHIDListExportField' => 'infrastructure/export/field/PhabricatorPHIDListExportField.php', 'PhabricatorPHIDResolver' => 'applications/phid/resolver/PhabricatorPHIDResolver.php', 'PhabricatorPHIDType' => 'applications/phid/type/PhabricatorPHIDType.php', 'PhabricatorPHIDTypeTestCase' => 'applications/phid/type/__tests__/PhabricatorPHIDTypeTestCase.php', @@ -3835,6 +3841,7 @@ phutil_register_library_map(array( 'PhabricatorProjectsCurtainExtension' => 'applications/project/engineextension/PhabricatorProjectsCurtainExtension.php', 'PhabricatorProjectsEditEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsEditEngineExtension.php', 'PhabricatorProjectsEditField' => 'applications/transactions/editfield/PhabricatorProjectsEditField.php', + 'PhabricatorProjectsExportEngineExtension' => 'infrastructure/export/engine/PhabricatorProjectsExportEngineExtension.php', 'PhabricatorProjectsFulltextEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsFulltextEngineExtension.php', 'PhabricatorProjectsMembersSearchEngineAttachment' => 'applications/project/engineextension/PhabricatorProjectsMembersSearchEngineAttachment.php', 'PhabricatorProjectsMembershipIndexEngineExtension' => 'applications/project/engineextension/PhabricatorProjectsMembershipIndexEngineExtension.php', @@ -4126,6 +4133,7 @@ phutil_register_library_map(array( 'PhabricatorSpacesController' => 'applications/spaces/controller/PhabricatorSpacesController.php', 'PhabricatorSpacesDAO' => 'applications/spaces/storage/PhabricatorSpacesDAO.php', 'PhabricatorSpacesEditController' => 'applications/spaces/controller/PhabricatorSpacesEditController.php', + 'PhabricatorSpacesExportEngineExtension' => 'infrastructure/export/engine/PhabricatorSpacesExportEngineExtension.php', 'PhabricatorSpacesInterface' => 'applications/spaces/interface/PhabricatorSpacesInterface.php', 'PhabricatorSpacesListController' => 'applications/spaces/controller/PhabricatorSpacesListController.php', 'PhabricatorSpacesNamespace' => 'applications/spaces/storage/PhabricatorSpacesNamespace.php', @@ -4189,9 +4197,10 @@ phutil_register_library_map(array( 'PhabricatorStorageSchemaSpec' => 'infrastructure/storage/schema/PhabricatorStorageSchemaSpec.php', 'PhabricatorStorageSetupCheck' => 'applications/config/check/PhabricatorStorageSetupCheck.php', 'PhabricatorStringConfigType' => 'applications/config/type/PhabricatorStringConfigType.php', - 'PhabricatorStringExportField' => 'infrastructure/export/PhabricatorStringExportField.php', + 'PhabricatorStringExportField' => 'infrastructure/export/field/PhabricatorStringExportField.php', 'PhabricatorStringListConfigType' => 'applications/config/type/PhabricatorStringListConfigType.php', 'PhabricatorStringListEditField' => 'applications/transactions/editfield/PhabricatorStringListEditField.php', + 'PhabricatorStringListExportField' => 'infrastructure/export/field/PhabricatorStringListExportField.php', 'PhabricatorStringSetting' => 'applications/settings/setting/PhabricatorStringSetting.php', 'PhabricatorSubmitEditField' => 'applications/transactions/editfield/PhabricatorSubmitEditField.php', 'PhabricatorSubscribableInterface' => 'applications/subscriptions/interface/PhabricatorSubscribableInterface.php', @@ -4206,6 +4215,7 @@ phutil_register_library_map(array( 'PhabricatorSubscriptionsEditController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php', 'PhabricatorSubscriptionsEditEngineExtension' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsEditEngineExtension.php', 'PhabricatorSubscriptionsEditor' => 'applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php', + 'PhabricatorSubscriptionsExportEngineExtension' => 'infrastructure/export/engine/PhabricatorSubscriptionsExportEngineExtension.php', 'PhabricatorSubscriptionsFulltextEngineExtension' => 'applications/subscriptions/engineextension/PhabricatorSubscriptionsFulltextEngineExtension.php', 'PhabricatorSubscriptionsHeraldAction' => 'applications/subscriptions/herald/PhabricatorSubscriptionsHeraldAction.php', 'PhabricatorSubscriptionsListController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsListController.php', @@ -4253,7 +4263,7 @@ phutil_register_library_map(array( 'PhabricatorTextAreaEditField' => 'applications/transactions/editfield/PhabricatorTextAreaEditField.php', 'PhabricatorTextConfigType' => 'applications/config/type/PhabricatorTextConfigType.php', 'PhabricatorTextEditField' => 'applications/transactions/editfield/PhabricatorTextEditField.php', - 'PhabricatorTextExportFormat' => 'infrastructure/export/PhabricatorTextExportFormat.php', + 'PhabricatorTextExportFormat' => 'infrastructure/export/format/PhabricatorTextExportFormat.php', 'PhabricatorTextListConfigType' => 'applications/config/type/PhabricatorTextListConfigType.php', 'PhabricatorTime' => 'infrastructure/time/PhabricatorTime.php', 'PhabricatorTimeFormatSetting' => 'applications/settings/setting/PhabricatorTimeFormatSetting.php', @@ -4318,6 +4328,7 @@ phutil_register_library_map(array( 'PhabricatorUIExample' => 'applications/uiexample/examples/PhabricatorUIExample.php', 'PhabricatorUIExampleRenderController' => 'applications/uiexample/controller/PhabricatorUIExampleRenderController.php', 'PhabricatorUIExamplesApplication' => 'applications/uiexample/application/PhabricatorUIExamplesApplication.php', + 'PhabricatorURIExportField' => 'infrastructure/export/field/PhabricatorURIExportField.php', 'PhabricatorUSEnglishTranslation' => 'infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php', 'PhabricatorUnifiedDiffsSetting' => 'applications/settings/setting/PhabricatorUnifiedDiffsSetting.php', 'PhabricatorUnitTestContentSource' => 'infrastructure/contentsource/PhabricatorUnitTestContentSource.php', @@ -4411,6 +4422,7 @@ phutil_register_library_map(array( 'PhabricatorWorkerManagementWorkflow' => 'infrastructure/daemon/workers/management/PhabricatorWorkerManagementWorkflow.php', 'PhabricatorWorkerPermanentFailureException' => 'infrastructure/daemon/workers/exception/PhabricatorWorkerPermanentFailureException.php', 'PhabricatorWorkerSchemaSpec' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerSchemaSpec.php', + 'PhabricatorWorkerSingleBulkJobType' => 'infrastructure/daemon/workers/bulk/PhabricatorWorkerSingleBulkJobType.php', 'PhabricatorWorkerTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTask.php', 'PhabricatorWorkerTaskData' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTaskData.php', 'PhabricatorWorkerTaskDetailController' => 'applications/daemon/controller/PhabricatorWorkerTaskDetailController.php', @@ -6768,10 +6780,6 @@ phutil_register_library_map(array( 'ManiphestEditProjectsCapability' => 'PhabricatorPolicyCapability', 'ManiphestEditStatusCapability' => 'PhabricatorPolicyCapability', 'ManiphestEmailCommand' => 'MetaMTAEmailTransactionCommand', - 'ManiphestExcelDefaultFormat' => 'ManiphestExcelFormat', - 'ManiphestExcelFormat' => 'Phobject', - 'ManiphestExcelFormatTestCase' => 'PhabricatorTestCase', - 'ManiphestExportController' => 'ManiphestController', 'ManiphestGetTaskTransactionsConduitAPIMethod' => 'ManiphestConduitAPIMethod', 'ManiphestHovercardEngineExtension' => 'PhabricatorHovercardEngineExtension', 'ManiphestInfoConduitAPIMethod' => 'ManiphestConduitAPIMethod', @@ -7572,6 +7580,7 @@ phutil_register_library_map(array( 'PhabricatorBulkContentSource' => 'PhabricatorContentSource', 'PhabricatorBulkEditGroup' => 'Phobject', 'PhabricatorBulkEngine' => 'Phobject', + 'PhabricatorBulkManagementExportWorkflow' => 'PhabricatorBulkManagementWorkflow', 'PhabricatorBulkManagementMakeSilentWorkflow' => 'PhabricatorBulkManagementWorkflow', 'PhabricatorBulkManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorCSVExportFormat' => 'PhabricatorExportFormat', @@ -7991,6 +8000,7 @@ phutil_register_library_map(array( 'PhabricatorCustomFieldEditEngineExtension' => 'PhabricatorEditEngineExtension', 'PhabricatorCustomFieldEditField' => 'PhabricatorEditField', 'PhabricatorCustomFieldEditType' => 'PhabricatorEditType', + 'PhabricatorCustomFieldExportEngineExtension' => 'PhabricatorExportEngineExtension', 'PhabricatorCustomFieldFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension', 'PhabricatorCustomFieldHeraldAction' => 'HeraldAction', 'PhabricatorCustomFieldHeraldActionGroup' => 'HeraldActionGroup', @@ -8279,9 +8289,14 @@ phutil_register_library_map(array( 'PhabricatorEventListener' => 'PhutilEventListener', 'PhabricatorEventType' => 'PhutilEventType', 'PhabricatorExampleEventListener' => 'PhabricatorEventListener', + 'PhabricatorExcelExportFormat' => 'PhabricatorExportFormat', 'PhabricatorExecFutureFileUploadSource' => 'PhabricatorFileUploadSource', + 'PhabricatorExportEngine' => 'Phobject', + 'PhabricatorExportEngineBulkJobType' => 'PhabricatorWorkerSingleBulkJobType', + 'PhabricatorExportEngineExtension' => 'Phobject', 'PhabricatorExportField' => 'Phobject', 'PhabricatorExportFormat' => 'Phobject', + 'PhabricatorExportFormatSetting' => 'PhabricatorInternalSetting', 'PhabricatorExtendingPhabricatorConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorExtensionsSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorExternalAccount' => array( @@ -8599,9 +8614,11 @@ phutil_register_library_map(array( 'PhabricatorLipsumManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorLipsumMondrianArtist' => 'PhabricatorLipsumArtist', 'PhabricatorLiskDAO' => 'LiskDAO', + 'PhabricatorLiskExportEngineExtension' => 'PhabricatorExportEngineExtension', 'PhabricatorLiskFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension', 'PhabricatorLiskSearchEngineExtension' => 'PhabricatorSearchEngineExtension', 'PhabricatorLiskSerializer' => 'Phobject', + 'PhabricatorListExportField' => 'PhabricatorExportField', 'PhabricatorLocalDiskFileStorageEngine' => 'PhabricatorFileStorageEngine', 'PhabricatorLocalTimeTestCase' => 'PhabricatorTestCase', 'PhabricatorLocaleScopeGuard' => 'Phobject', @@ -8939,6 +8956,7 @@ phutil_register_library_map(array( 'PhabricatorPHIDExportField' => 'PhabricatorExportField', 'PhabricatorPHIDListEditField' => 'PhabricatorEditField', 'PhabricatorPHIDListEditType' => 'PhabricatorEditType', + 'PhabricatorPHIDListExportField' => 'PhabricatorListExportField', 'PhabricatorPHIDResolver' => 'Phobject', 'PhabricatorPHIDType' => 'Phobject', 'PhabricatorPHIDTypeTestCase' => 'PhutilTestCase', @@ -9439,6 +9457,7 @@ phutil_register_library_map(array( 'PhabricatorProjectsCurtainExtension' => 'PHUICurtainExtension', 'PhabricatorProjectsEditEngineExtension' => 'PhabricatorEditEngineExtension', 'PhabricatorProjectsEditField' => 'PhabricatorTokenizerEditField', + 'PhabricatorProjectsExportEngineExtension' => 'PhabricatorExportEngineExtension', 'PhabricatorProjectsFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension', 'PhabricatorProjectsMembersSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'PhabricatorProjectsMembershipIndexEngineExtension' => 'PhabricatorIndexEngineExtension', @@ -9806,6 +9825,7 @@ phutil_register_library_map(array( 'PhabricatorSpacesController' => 'PhabricatorController', 'PhabricatorSpacesDAO' => 'PhabricatorLiskDAO', 'PhabricatorSpacesEditController' => 'PhabricatorSpacesController', + 'PhabricatorSpacesExportEngineExtension' => 'PhabricatorExportEngineExtension', 'PhabricatorSpacesInterface' => 'PhabricatorPHIDInterface', 'PhabricatorSpacesListController' => 'PhabricatorSpacesController', 'PhabricatorSpacesNamespace' => array( @@ -9879,6 +9899,7 @@ phutil_register_library_map(array( 'PhabricatorStringExportField' => 'PhabricatorExportField', 'PhabricatorStringListConfigType' => 'PhabricatorTextListConfigType', 'PhabricatorStringListEditField' => 'PhabricatorEditField', + 'PhabricatorStringListExportField' => 'PhabricatorListExportField', 'PhabricatorStringSetting' => 'PhabricatorSetting', 'PhabricatorSubmitEditField' => 'PhabricatorEditField', 'PhabricatorSubscribedToObjectEdgeType' => 'PhabricatorEdgeType', @@ -9892,6 +9913,7 @@ phutil_register_library_map(array( 'PhabricatorSubscriptionsEditController' => 'PhabricatorController', 'PhabricatorSubscriptionsEditEngineExtension' => 'PhabricatorEditEngineExtension', 'PhabricatorSubscriptionsEditor' => 'PhabricatorEditor', + 'PhabricatorSubscriptionsExportEngineExtension' => 'PhabricatorExportEngineExtension', 'PhabricatorSubscriptionsFulltextEngineExtension' => 'PhabricatorFulltextEngineExtension', 'PhabricatorSubscriptionsHeraldAction' => 'HeraldAction', 'PhabricatorSubscriptionsListController' => 'PhabricatorController', @@ -10015,6 +10037,7 @@ phutil_register_library_map(array( 'PhabricatorUIExample' => 'Phobject', 'PhabricatorUIExampleRenderController' => 'PhabricatorController', 'PhabricatorUIExamplesApplication' => 'PhabricatorApplication', + 'PhabricatorURIExportField' => 'PhabricatorExportField', 'PhabricatorUSEnglishTranslation' => 'PhutilTranslation', 'PhabricatorUnifiedDiffsSetting' => 'PhabricatorSelectSetting', 'PhabricatorUnitTestContentSource' => 'PhabricatorContentSource', @@ -10138,6 +10161,7 @@ phutil_register_library_map(array( 'PhabricatorWorkerManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorWorkerPermanentFailureException' => 'Exception', 'PhabricatorWorkerSchemaSpec' => 'PhabricatorConfigSchemaSpec', + 'PhabricatorWorkerSingleBulkJobType' => 'PhabricatorWorkerBulkJobType', 'PhabricatorWorkerTask' => 'PhabricatorWorkerDAO', 'PhabricatorWorkerTaskData' => 'PhabricatorWorkerDAO', 'PhabricatorWorkerTaskDetailController' => 'PhabricatorDaemonController', diff --git a/src/applications/audit/editor/PhabricatorAuditEditor.php b/src/applications/audit/editor/PhabricatorAuditEditor.php index 0cf6339239..049733f777 100644 --- a/src/applications/audit/editor/PhabricatorAuditEditor.php +++ b/src/applications/audit/editor/PhabricatorAuditEditor.php @@ -358,9 +358,17 @@ final class PhabricatorAuditEditor array $changes, PhutilMarkupEngine $engine) { - // we are only really trying to find unmentionable phids here... - // don't bother with this outside initial commit (i.e. create) - // transaction + $actor = $this->getActor(); + $result = array(); + + // Some interactions (like "Fixes Txxx" interacting with Maniphest) have + // already been processed, so we're only re-parsing them here to avoid + // generating an extra redundant mention. Other interactions are being + // processed for the first time. + + // We're only recognizing magic in the commit message itself, not in + // audit comments. + $is_commit = false; foreach ($xactions as $xaction) { switch ($xaction->getTransactionType()) { @@ -370,8 +378,6 @@ final class PhabricatorAuditEditor } } - // "result" is always an array.... - $result = array(); if (!$is_commit) { return $result; } @@ -403,6 +409,46 @@ final class PhabricatorAuditEditor ->withNames($monograms) ->execute(); $phid_map[] = mpull($objects, 'getPHID', 'getPHID'); + + + $reverts_refs = id(new DifferentialCustomFieldRevertsParser()) + ->parseCorpus($huge_block); + $reverts = array_mergev(ipull($reverts_refs, 'monograms')); + if ($reverts) { + // Only allow commits to revert other commits in the same repository. + $reverted_commits = id(new DiffusionCommitQuery()) + ->setViewer($actor) + ->withRepository($object->getRepository()) + ->withIdentifiers($reverts) + ->execute(); + + $reverted_revisions = id(new PhabricatorObjectQuery()) + ->setViewer($actor) + ->withNames($reverts) + ->withTypes( + array( + DifferentialRevisionPHIDType::TYPECONST, + )) + ->execute(); + + $reverted_phids = + mpull($reverted_commits, 'getPHID', 'getPHID') + + mpull($reverted_revisions, 'getPHID', 'getPHID'); + + // NOTE: Skip any write attempts if a user cleverly implies a commit + // reverts itself, although this would be exceptionally clever in Git + // or Mercurial. + unset($reverted_phids[$object->getPHID()]); + + $reverts_edge = DiffusionCommitRevertsCommitEdgeType::EDGECONST; + $result[] = id(new PhabricatorAuditTransaction()) + ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) + ->setMetadataValue('edge:type', $reverts_edge) + ->setNewValue(array('+' => $reverted_phids)); + + $phid_map[] = $reverted_phids; + } + $phid_map = array_mergev($phid_map); $this->setUnmentionablePHIDMap($phid_map); diff --git a/src/applications/conduit/parametertype/ConduitPHIDParameterType.php b/src/applications/conduit/parametertype/ConduitPHIDParameterType.php index 3bb45697dc..eafee4d453 100644 --- a/src/applications/conduit/parametertype/ConduitPHIDParameterType.php +++ b/src/applications/conduit/parametertype/ConduitPHIDParameterType.php @@ -3,9 +3,26 @@ final class ConduitPHIDParameterType extends ConduitParameterType { + private $isNullable; + + public function setIsNullable($is_nullable) { + $this->isNullable = $is_nullable; + return $this; + } + + public function getIsNullable() { + return $this->isNullable; + } + protected function getParameterValue(array $request, $key, $strict) { $value = parent::getParameterValue($request, $key, $strict); + if ($this->getIsNullable()) { + if ($value === null) { + return $value; + } + } + if (!is_string($value)) { $this->raiseValidationException( $request, @@ -17,7 +34,11 @@ final class ConduitPHIDParameterType } protected function getParameterTypeName() { - return 'phid'; + if ($this->getIsNullable()) { + return 'phid|null'; + } else { + return 'phid'; + } } protected function getParameterFormatDescriptions() { @@ -27,9 +48,15 @@ final class ConduitPHIDParameterType } protected function getParameterExamples() { - return array( + $examples = array( '"PHID-WXYZ-1111222233334444"', ); + + if ($this->getIsNullable()) { + $examples[] = 'null'; + } + + return $examples; } } diff --git a/src/applications/daemon/controller/PhabricatorDaemonBulkJobViewController.php b/src/applications/daemon/controller/PhabricatorDaemonBulkJobViewController.php index f7aa396e94..00fb297fe0 100644 --- a/src/applications/daemon/controller/PhabricatorDaemonBulkJobViewController.php +++ b/src/applications/daemon/controller/PhabricatorDaemonBulkJobViewController.php @@ -71,18 +71,10 @@ final class PhabricatorDaemonBulkJobViewController $viewer = $this->getViewer(); $curtain = $this->newCurtainView($job); - if ($job->isConfirming()) { - $continue_uri = $job->getMonitorURI(); - } else { - $continue_uri = $job->getDoneURI(); + foreach ($job->getCurtainActions($viewer) as $action) { + $curtain->addAction($action); } - $curtain->addAction( - id(new PhabricatorActionView()) - ->setHref($continue_uri) - ->setIcon('fa-arrow-circle-o-right') - ->setName(pht('Continue'))); - return $curtain; } diff --git a/src/applications/differential/editor/DifferentialTransactionEditor.php b/src/applications/differential/editor/DifferentialTransactionEditor.php index ecd1e6c95c..3a1537b01d 100644 --- a/src/applications/differential/editor/DifferentialTransactionEditor.php +++ b/src/applications/differential/editor/DifferentialTransactionEditor.php @@ -919,7 +919,44 @@ final class DifferentialTransactionEditor } } - $this->setUnmentionablePHIDMap(array_merge($task_phids, $rev_phids)); + $revert_refs = id(new DifferentialCustomFieldRevertsParser()) + ->parseCorpus($content_block); + + $revert_monograms = array(); + foreach ($revert_refs as $match) { + foreach ($match['monograms'] as $monogram) { + $revert_monograms[] = $monogram; + } + } + + if ($revert_monograms) { + $revert_objects = id(new PhabricatorObjectQuery()) + ->setViewer($this->getActor()) + ->withNames($revert_monograms) + ->withTypes( + array( + DifferentialRevisionPHIDType::TYPECONST, + PhabricatorRepositoryCommitPHIDType::TYPECONST, + )) + ->execute(); + + $revert_phids = mpull($revert_objects, 'getPHID', 'getPHID'); + + // Don't let an object revert itself, although other silly stuff like + // cycles of objects reverting each other is not prevented. + unset($revert_phids[$object->getPHID()]); + + $revert_type = DiffusionCommitRevertsCommitEdgeType::EDGECONST; + $edges[$revert_type] = $revert_phids; + } else { + $revert_phids = array(); + } + + $this->setUnmentionablePHIDMap( + array_merge( + $task_phids, + $rev_phids, + $revert_phids)); $result = array(); foreach ($edges as $type => $specs) { diff --git a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php index d932149a75..e619ecb1ad 100644 --- a/src/applications/diffusion/application/PhabricatorDiffusionApplication.php +++ b/src/applications/diffusion/application/PhabricatorDiffusionApplication.php @@ -121,7 +121,7 @@ final class PhabricatorDiffusionApplication extends PhabricatorApplication { $this->getEditRoutePattern('edit/') => 'DiffusionRepositoryEditController', 'pushlog/' => array( - '(?:query/(?P[^/]+)/)?' => 'DiffusionPushLogListController', + $this->getQueryRoutePattern() => 'DiffusionPushLogListController', 'view/(?P\d+)/' => 'DiffusionPushEventViewController', ), 'pulllog/' => array( diff --git a/src/applications/diffusion/controller/DiffusionPullLogListController.php b/src/applications/diffusion/controller/DiffusionPullLogListController.php index 29712be2a4..f3b570c815 100644 --- a/src/applications/diffusion/controller/DiffusionPullLogListController.php +++ b/src/applications/diffusion/controller/DiffusionPullLogListController.php @@ -9,4 +9,9 @@ final class DiffusionPullLogListController ->buildResponse(); } + protected function buildApplicationCrumbs() { + return parent::buildApplicationCrumbs() + ->addTextCrumb(pht('Pull Logs'), $this->getApplicationURI('pulllog/')); + } + } diff --git a/src/applications/diffusion/controller/DiffusionPushLogListController.php b/src/applications/diffusion/controller/DiffusionPushLogListController.php index 658e21674d..a6c612eebe 100644 --- a/src/applications/diffusion/controller/DiffusionPushLogListController.php +++ b/src/applications/diffusion/controller/DiffusionPushLogListController.php @@ -9,4 +9,9 @@ final class DiffusionPushLogListController ->buildResponse(); } + protected function buildApplicationCrumbs() { + return parent::buildApplicationCrumbs() + ->addTextCrumb(pht('Push Logs'), $this->getApplicationURI('pushlog/')); + } + } diff --git a/src/applications/diffusion/edge/DiffusionCommitRevertedByCommitEdgeType.php b/src/applications/diffusion/edge/DiffusionCommitRevertedByCommitEdgeType.php index 53c39e54c9..ae59a3b1e6 100644 --- a/src/applications/diffusion/edge/DiffusionCommitRevertedByCommitEdgeType.php +++ b/src/applications/diffusion/edge/DiffusionCommitRevertedByCommitEdgeType.php @@ -19,7 +19,7 @@ final class DiffusionCommitRevertedByCommitEdgeType $add_edges) { return pht( - '%s added %s reverting commit(s): %s.', + '%s added %s reverting change(s): %s.', $actor, $add_count, $add_edges); @@ -31,7 +31,7 @@ final class DiffusionCommitRevertedByCommitEdgeType $rem_edges) { return pht( - '%s removed %s reverting commit(s): %s.', + '%s removed %s reverting change(s): %s.', $actor, $rem_count, $rem_edges); @@ -46,7 +46,7 @@ final class DiffusionCommitRevertedByCommitEdgeType $rem_edges) { return pht( - '%s edited reverting commit(s), added %s: %s; removed %s: %s.', + '%s edited reverting change(s), added %s: %s; removed %s: %s.', $actor, $add_count, $add_edges, @@ -61,7 +61,7 @@ final class DiffusionCommitRevertedByCommitEdgeType $add_edges) { return pht( - '%s added %s reverting commit(s) for %s: %s.', + '%s added %s reverting change(s) for %s: %s.', $actor, $add_count, $object, @@ -75,7 +75,7 @@ final class DiffusionCommitRevertedByCommitEdgeType $rem_edges) { return pht( - '%s removed %s reverting commit(s) for %s: %s.', + '%s removed %s reverting change(s) for %s: %s.', $actor, $rem_count, $object, @@ -92,7 +92,7 @@ final class DiffusionCommitRevertedByCommitEdgeType $rem_edges) { return pht( - '%s edited reverting commit(s) for %s, added %s: %s; removed %s: %s.', + '%s edited reverting change(s) for %s, added %s: %s; removed %s: %s.', $actor, $object, $add_count, diff --git a/src/applications/diffusion/edge/DiffusionCommitRevertsCommitEdgeType.php b/src/applications/diffusion/edge/DiffusionCommitRevertsCommitEdgeType.php index 8f32797173..ee0223c966 100644 --- a/src/applications/diffusion/edge/DiffusionCommitRevertsCommitEdgeType.php +++ b/src/applications/diffusion/edge/DiffusionCommitRevertsCommitEdgeType.php @@ -22,7 +22,7 @@ final class DiffusionCommitRevertsCommitEdgeType extends PhabricatorEdgeType { $add_edges) { return pht( - '%s added %s reverted commit(s): %s.', + '%s added %s reverted change(s): %s.', $actor, $add_count, $add_edges); @@ -34,7 +34,7 @@ final class DiffusionCommitRevertsCommitEdgeType extends PhabricatorEdgeType { $rem_edges) { return pht( - '%s removed %s reverted commit(s): %s.', + '%s removed %s reverted change(s): %s.', $actor, $rem_count, $rem_edges); @@ -49,7 +49,7 @@ final class DiffusionCommitRevertsCommitEdgeType extends PhabricatorEdgeType { $rem_edges) { return pht( - '%s edited reverted commit(s), added %s: %s; removed %s: %s.', + '%s edited reverted change(s), added %s: %s; removed %s: %s.', $actor, $add_count, $add_edges, @@ -64,7 +64,7 @@ final class DiffusionCommitRevertsCommitEdgeType extends PhabricatorEdgeType { $add_edges) { return pht( - '%s added %s reverted commit(s) for %s: %s.', + '%s added %s reverted change(s) for %s: %s.', $actor, $add_count, $object, @@ -78,7 +78,7 @@ final class DiffusionCommitRevertsCommitEdgeType extends PhabricatorEdgeType { $rem_edges) { return pht( - '%s removed %s reverted commit(s) for %s: %s.', + '%s removed %s reverted change(s) for %s: %s.', $actor, $rem_count, $object, @@ -95,7 +95,7 @@ final class DiffusionCommitRevertsCommitEdgeType extends PhabricatorEdgeType { $rem_edges) { return pht( - '%s edited reverted commit(s) for %s, added %s: %s; removed %s: %s.', + '%s edited reverted change(s) for %s, added %s: %s; removed %s: %s.', $actor, $object, $add_count, diff --git a/src/applications/diffusion/herald/HeraldCommitAdapter.php b/src/applications/diffusion/herald/HeraldCommitAdapter.php index e0e15352b6..fc3c8c4f5b 100644 --- a/src/applications/diffusion/herald/HeraldCommitAdapter.php +++ b/src/applications/diffusion/herald/HeraldCommitAdapter.php @@ -135,13 +135,16 @@ final class HeraldCommitAdapter } public function loadAffectedPaths() { + $viewer = $this->getViewer(); + if ($this->affectedPaths === null) { $result = PhabricatorOwnerPathQuery::loadAffectedPaths( $this->getRepository(), $this->commit, - PhabricatorUser::getOmnipotentUser()); + $viewer); $this->affectedPaths = $result; } + return $this->affectedPaths; } @@ -172,6 +175,8 @@ final class HeraldCommitAdapter } public function loadDifferentialRevision() { + $viewer = $this->getViewer(); + if ($this->affectedRevision === null) { $this->affectedRevision = false; @@ -189,7 +194,7 @@ final class HeraldCommitAdapter $revision = id(new DifferentialRevisionQuery()) ->withIDs(array($revision_id)) - ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->setViewer($viewer) ->needReviewers(true) ->executeOne(); if ($revision) { @@ -197,6 +202,7 @@ final class HeraldCommitAdapter } } } + return $this->affectedRevision; } @@ -323,7 +329,7 @@ final class HeraldCommitAdapter } private function callConduit($method, array $params) { - $viewer = PhabricatorUser::getOmnipotentUser(); + $viewer = $this->getViewer(); $drequest = DiffusionRequest::newFromDictionary( array( diff --git a/src/applications/diffusion/query/DiffusionPullLogSearchEngine.php b/src/applications/diffusion/query/DiffusionPullLogSearchEngine.php index 8d6102d4eb..dfdfceb519 100644 --- a/src/applications/diffusion/query/DiffusionPullLogSearchEngine.php +++ b/src/applications/diffusion/query/DiffusionPullLogSearchEngine.php @@ -26,6 +26,12 @@ final class DiffusionPullLogSearchEngine $query->withPullerPHIDs($map['pullerPHIDs']); } + if ($map['createdStart'] || $map['createdEnd']) { + $query->withEpochBetween( + $map['createdStart'], + $map['createdEnd']); + } + return $query; } @@ -44,17 +50,19 @@ final class DiffusionPullLogSearchEngine ->setLabel(pht('Pullers')) ->setDescription( pht('Search for pull logs by specific users.')), + id(new PhabricatorSearchDateField()) + ->setLabel(pht('Created After')) + ->setKey('createdStart'), + id(new PhabricatorSearchDateField()) + ->setLabel(pht('Created Before')) + ->setKey('createdEnd'), ); } protected function newExportFields() { - return array( - id(new PhabricatorIDExportField()) - ->setKey('id') - ->setLabel(pht('ID')), - id(new PhabricatorPHIDExportField()) - ->setKey('phid') - ->setLabel(pht('PHID')), + $viewer = $this->requireViewer(); + + $fields = array( id(new PhabricatorPHIDExportField()) ->setKey('repositoryPHID') ->setLabel(pht('Repository PHID')), @@ -80,9 +88,17 @@ final class DiffusionPullLogSearchEngine ->setKey('date') ->setLabel(pht('Date')), ); + + if ($viewer->getIsAdmin()) { + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('remoteAddress') + ->setLabel(pht('Remote Address')); + } + + return $fields; } - public function newExport(array $events) { + protected function newExportData(array $events) { $viewer = $this->requireViewer(); $phids = array(); @@ -111,9 +127,7 @@ final class DiffusionPullLogSearchEngine $puller_name = null; } - $export[] = array( - 'id' => $event->getID(), - 'phid' => $event->getPHID(), + $map = array( 'repositoryPHID' => $repository_phid, 'repository' => $repository_name, 'pullerPHID' => $puller_phid, @@ -123,6 +137,12 @@ final class DiffusionPullLogSearchEngine 'code' => $event->getResultCode(), 'date' => $event->getEpoch(), ); + + if ($viewer->getIsAdmin()) { + $map['remoteAddress'] = $event->getRemoteAddress(); + } + + $export[] = $map; } return $export; diff --git a/src/applications/diffusion/view/DiffusionPullLogListView.php b/src/applications/diffusion/view/DiffusionPullLogListView.php index f2e3280eba..8df35e2922 100644 --- a/src/applications/diffusion/view/DiffusionPullLogListView.php +++ b/src/applications/diffusion/view/DiffusionPullLogListView.php @@ -22,24 +22,10 @@ final class DiffusionPullLogListView extends AphrontView { } $handles = $viewer->loadHandles($handle_phids); - // Figure out which repositories are editable. We only let you see remote - // IPs if you have edit capability on a repository. - $editable_repos = array(); - if ($events) { - $editable_repos = id(new PhabricatorRepositoryQuery()) - ->setViewer($viewer) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->withPHIDs(mpull($events, 'getRepositoryPHID')) - ->execute(); - $editable_repos = mpull($editable_repos, null, 'getPHID'); - } + // Only administrators can view remote addresses. + $remotes_visible = $viewer->getIsAdmin(); $rows = array(); - $any_host = false; foreach ($events as $event) { if ($event->getRepositoryPHID()) { $repository = $event->getRepository(); @@ -47,13 +33,10 @@ final class DiffusionPullLogListView extends AphrontView { $repository = null; } - // Reveal this if it's valid and the user can edit the repository. For - // invalid requests you currently have to go fishing in the database. - $remote_address = '-'; - if ($repository) { - if (isset($editable_repos[$event->getRepositoryPHID()])) { - $remote_address = $event->getRemoteAddress(); - } + if ($remotes_visible) { + $remote_address = $event->getRemoteAddress(); + } else { + $remote_address = null; } $event_id = $event->getID(); @@ -107,6 +90,13 @@ final class DiffusionPullLogListView extends AphrontView { '', 'n', 'right', + )) + ->setColumnVisibility( + array( + true, + true, + true, + $remotes_visible, )); return $table; diff --git a/src/applications/diffusion/view/DiffusionPushLogListView.php b/src/applications/diffusion/view/DiffusionPushLogListView.php index 303f0519f6..e101625752 100644 --- a/src/applications/diffusion/view/DiffusionPushLogListView.php +++ b/src/applications/diffusion/view/DiffusionPushLogListView.php @@ -25,31 +25,21 @@ final class DiffusionPushLogListView extends AphrontView { $handles = $viewer->loadHandles($handle_phids); - // Figure out which repositories are editable. We only let you see remote - // IPs if you have edit capability on a repository. - $editable_repos = array(); - if ($logs) { - $editable_repos = id(new PhabricatorRepositoryQuery()) - ->setViewer($viewer) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - PhabricatorPolicyCapability::CAN_EDIT, - )) - ->withPHIDs(mpull($logs, 'getRepositoryPHID')) - ->execute(); - $editable_repos = mpull($editable_repos, null, 'getPHID'); - } + // Only administrators can view remote addresses. + $remotes_visible = $viewer->getIsAdmin(); + + $flag_map = PhabricatorRepositoryPushLog::getFlagDisplayNames(); + $reject_map = PhabricatorRepositoryPushLog::getRejectCodeDisplayNames(); $rows = array(); $any_host = false; foreach ($logs as $log) { $repository = $log->getRepository(); - // Reveal this if it's valid and the user can edit the repository. - $remote_address = '-'; - if (isset($editable_repos[$log->getRepositoryPHID()])) { + if ($remotes_visible) { $remote_address = $log->getPushEvent()->getRemoteAddress(); + } else { + $remote_address = null; } $event_id = $log->getPushEvent()->getID(); @@ -72,6 +62,23 @@ final class DiffusionPushLogListView extends AphrontView { $device = null; } + $flags = $log->getChangeFlags(); + $flag_names = array(); + foreach ($flag_map as $flag_key => $flag_name) { + if (($flags & $flag_key) === $flag_key) { + $flag_names[] = $flag_name; + } + } + $flag_names = phutil_implode_html( + phutil_tag('br'), + $flag_names); + + $reject_code = $log->getPushEvent()->getRejectCode(); + $reject_label = idx( + $reject_map, + $reject_code, + pht('Unknown ("%s")', $reject_code)); + $rows[] = array( phutil_tag( 'a', @@ -98,10 +105,8 @@ final class DiffusionPushLogListView extends AphrontView { 'href' => $repository->getCommitURI($log->getRefNew()), ), $log->getRefNewShort()), - - // TODO: Make these human-readable. - $log->getChangeFlags(), - $log->getPushEvent()->getRejectCode(), + $flag_names, + $reject_label, $viewer->formatShortDateTime($log->getEpoch()), ); } @@ -120,7 +125,7 @@ final class DiffusionPushLogListView extends AphrontView { pht('Old'), pht('New'), pht('Flags'), - pht('Code'), + pht('Result'), pht('Date'), )) ->setColumnClasses( @@ -135,6 +140,8 @@ final class DiffusionPushLogListView extends AphrontView { 'wide', 'n', 'n', + '', + '', 'right', )) ->setColumnVisibility( @@ -142,7 +149,7 @@ final class DiffusionPushLogListView extends AphrontView { true, true, true, - true, + $remotes_visible, true, $any_host, )); diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php index d3dc5208d2..2b8624b9c0 100644 --- a/src/applications/files/storage/PhabricatorFile.php +++ b/src/applications/files/storage/PhabricatorFile.php @@ -272,8 +272,12 @@ final class PhabricatorFile extends PhabricatorFileDAO $file->setByteSize($length); // NOTE: Once we receive the first chunk, we'll detect its MIME type and - // update the parent file. This matters for large media files like video. - $file->setMimeType('application/octet-stream'); + // update the parent file if a MIME type hasn't been provided. This matters + // for large media files like video. + $mime_type = idx($params, 'mime-type'); + if (!strlen($mime_type)) { + $file->setMimeType('application/octet-stream'); + } $chunked_hash = idx($params, 'chunkedHash'); diff --git a/src/applications/files/uploadsource/PhabricatorFileUploadSource.php b/src/applications/files/uploadsource/PhabricatorFileUploadSource.php index bf213a417e..87a4486869 100644 --- a/src/applications/files/uploadsource/PhabricatorFileUploadSource.php +++ b/src/applications/files/uploadsource/PhabricatorFileUploadSource.php @@ -6,6 +6,8 @@ abstract class PhabricatorFileUploadSource private $name; private $relativeTTL; private $viewPolicy; + private $mimeType; + private $authorPHID; private $rope; private $data; @@ -51,6 +53,24 @@ abstract class PhabricatorFileUploadSource return $this->byteLimit; } + public function setMIMEType($mime_type) { + $this->mimeType = $mime_type; + return $this; + } + + public function getMIMEType() { + return $this->mimeType; + } + + public function setAuthorPHID($author_phid) { + $this->authorPHID = $author_phid; + return $this; + } + + public function getAuthorPHID() { + return $this->authorPHID; + } + public function uploadFile() { if (!$this->shouldChunkFile()) { return $this->writeSingleFile(); @@ -245,6 +265,16 @@ abstract class PhabricatorFileUploadSource $parameters['ttl.relative'] = $ttl; } + $mime_type = $this->getMimeType(); + if ($mime_type !== null) { + $parameters['mime-type'] = $mime_type; + } + + $author_phid = $this->getAuthorPHID(); + if ($author_phid !== null) { + $parameters['authorPHID'] = $author_phid; + } + return $parameters; } diff --git a/src/applications/maniphest/application/PhabricatorManiphestApplication.php b/src/applications/maniphest/application/PhabricatorManiphestApplication.php index 6e4ac0a8f6..0075770863 100644 --- a/src/applications/maniphest/application/PhabricatorManiphestApplication.php +++ b/src/applications/maniphest/application/PhabricatorManiphestApplication.php @@ -50,14 +50,13 @@ final class PhabricatorManiphestApplication extends PhabricatorApplication { return array( '/T(?P[1-9]\d*)' => 'ManiphestTaskDetailController', '/maniphest/' => array( - '(?:query/(?P[^/]+)/)?' => 'ManiphestTaskListController', + $this->getQueryRoutePattern() => 'ManiphestTaskListController', 'report/(?:(?P\w+)/)?' => 'ManiphestReportController', $this->getBulkRoutePattern('bulk/') => 'ManiphestBulkEditController', 'task/' => array( $this->getEditRoutePattern('edit/') => 'ManiphestTaskEditController', ), - 'export/(?P[^/]+)/' => 'ManiphestExportController', 'subpriority/' => 'ManiphestSubpriorityController', ), ); diff --git a/src/applications/maniphest/controller/ManiphestExportController.php b/src/applications/maniphest/controller/ManiphestExportController.php deleted file mode 100644 index 1201c50478..0000000000 --- a/src/applications/maniphest/controller/ManiphestExportController.php +++ /dev/null @@ -1,135 +0,0 @@ -getViewer(); - $key = $request->getURIData('key'); - - $ok = @include_once 'PHPExcel.php'; - if (!$ok) { - $dialog = $this->newDialog(); - - $inst1 = pht( - 'This system does not have PHPExcel installed. This software '. - 'component is required to export tasks to Excel. Have your system '. - 'administrator install it from:'); - - $inst2 = pht( - 'Your PHP "%s" needs to be updated to include the '. - 'PHPExcel Classes directory.', - 'include_path'); - - $dialog->setTitle(pht('Excel Export Not Configured')); - $dialog->appendChild(hsprintf( - '

%s

'. - '
'. - '

'. - ''. - 'https://github.com/PHPOffice/PHPExcel'. - ''. - '

'. - '
'. - '

%s

', - $inst1, - $inst2)); - - $dialog->addCancelButton('/maniphest/'); - return id(new AphrontDialogResponse())->setDialog($dialog); - } - - // TODO: PHPExcel has a dependency on the PHP zip extension. We should test - // for that here, since it fatals if we don't have the ZipArchive class. - - $saved = id(new PhabricatorSavedQueryQuery()) - ->setViewer($viewer) - ->withQueryKeys(array($key)) - ->executeOne(); - if (!$saved) { - $engine = id(new ManiphestTaskSearchEngine()) - ->setViewer($viewer); - if ($engine->isBuiltinQuery($key)) { - $saved = $engine->buildSavedQueryFromBuiltin($key); - } - if (!$saved) { - return new Aphront404Response(); - } - } - - $formats = ManiphestExcelFormat::loadAllFormats(); - $export_formats = array(); - foreach ($formats as $format_class => $format_object) { - $export_formats[$format_class] = $format_object->getName(); - } - - if (!$request->isDialogFormPost()) { - $dialog = new AphrontDialogView(); - $dialog->setUser($viewer); - - $dialog->setTitle(pht('Export Tasks to Excel')); - $dialog->appendChild( - phutil_tag( - 'p', - array(), - pht('Do you want to export the query results to Excel?'))); - - $form = id(new PHUIFormLayoutView()) - ->appendChild( - id(new AphrontFormSelectControl()) - ->setLabel(pht('Format:')) - ->setName('excel-format') - ->setOptions($export_formats)); - - $dialog->appendChild($form); - - $dialog->addCancelButton('/maniphest/'); - $dialog->addSubmitButton(pht('Export to Excel')); - return id(new AphrontDialogResponse())->setDialog($dialog); - } - - $format = idx($formats, $request->getStr('excel-format')); - if ($format === null) { - throw new Exception(pht('Excel format object not found.')); - } - - $saved->makeEphemeral(); - $saved->setParameter('limit', PHP_INT_MAX); - - $engine = id(new ManiphestTaskSearchEngine()) - ->setViewer($viewer); - - $query = $engine->buildQueryFromSavedQuery($saved); - $query->setViewer($viewer); - $tasks = $query->execute(); - - $all_projects = array_mergev(mpull($tasks, 'getProjectPHIDs')); - $all_assigned = mpull($tasks, 'getOwnerPHID'); - - $handles = id(new PhabricatorHandleQuery()) - ->setViewer($viewer) - ->withPHIDs(array_merge($all_projects, $all_assigned)) - ->execute(); - - $workbook = new PHPExcel(); - $format->buildWorkbook($workbook, $tasks, $handles, $viewer); - $writer = PHPExcel_IOFactory::createWriter($workbook, 'Excel2007'); - - ob_start(); - $writer->save('php://output'); - $data = ob_get_clean(); - - $mime = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; - - return id(new AphrontFileResponse()) - ->setMimeType($mime) - ->setDownload($format->getFileName().'.xlsx') - ->setContent($data); - } - -} diff --git a/src/applications/maniphest/editor/ManiphestEditEngine.php b/src/applications/maniphest/editor/ManiphestEditEngine.php index 359ba493d4..c270104034 100644 --- a/src/applications/maniphest/editor/ManiphestEditEngine.php +++ b/src/applications/maniphest/editor/ManiphestEditEngine.php @@ -196,6 +196,7 @@ EODOCS pht('New task owner, or `null` to unassign.')) ->setTransactionType(ManiphestTaskOwnerTransaction::TRANSACTIONTYPE) ->setIsCopyable(true) + ->setIsNullable(true) ->setSingleValue($object->getOwnerPHID()) ->setCommentActionLabel(pht('Assign / Claim')) ->setCommentActionValue($owner_value), diff --git a/src/applications/maniphest/export/ManiphestExcelDefaultFormat.php b/src/applications/maniphest/export/ManiphestExcelDefaultFormat.php deleted file mode 100644 index 1451ae504e..0000000000 --- a/src/applications/maniphest/export/ManiphestExcelDefaultFormat.php +++ /dev/null @@ -1,140 +0,0 @@ -setActiveSheetIndex(0); - $sheet->setTitle(pht('Tasks')); - - $widths = array( - null, - 15, - null, - 10, - 15, - 15, - 60, - 30, - 20, - 100, - ); - - foreach ($widths as $col => $width) { - if ($width !== null) { - $sheet->getColumnDimension($this->col($col))->setWidth($width); - } - } - - $status_map = ManiphestTaskStatus::getTaskStatusMap(); - $pri_map = ManiphestTaskPriority::getTaskPriorityMap(); - - $date_format = null; - - $rows = array(); - $rows[] = array( - pht('ID'), - pht('Owner'), - pht('Status'), - pht('Priority'), - pht('Date Created'), - pht('Date Updated'), - pht('Title'), - pht('Tags'), - pht('URI'), - pht('Description'), - ); - - $is_date = array( - false, - false, - false, - false, - true, - true, - false, - false, - false, - false, - ); - - $header_format = array( - 'font' => array( - 'bold' => true, - ), - ); - - foreach ($tasks as $task) { - $task_owner = null; - if ($task->getOwnerPHID()) { - $task_owner = $handles[$task->getOwnerPHID()]->getName(); - } - - $projects = array(); - foreach ($task->getProjectPHIDs() as $phid) { - $projects[] = $handles[$phid]->getName(); - } - $projects = implode(', ', $projects); - - $rows[] = array( - 'T'.$task->getID(), - $task_owner, - idx($status_map, $task->getStatus(), '?'), - idx($pri_map, $task->getPriority(), '?'), - $this->computeExcelDate($task->getDateCreated()), - $this->computeExcelDate($task->getDateModified()), - $task->getTitle(), - $projects, - PhabricatorEnv::getProductionURI('/T'.$task->getID()), - id(new PhutilUTF8StringTruncator()) - ->setMaximumBytes(512) - ->truncateString($task->getDescription()), - ); - } - - foreach ($rows as $row => $cols) { - foreach ($cols as $col => $spec) { - $cell_name = $this->col($col).($row + 1); - $cell = $sheet - ->setCellValue($cell_name, $spec, $return_cell = true); - - if ($row == 0) { - $sheet->getStyle($cell_name)->applyFromArray($header_format); - } - - if ($is_date[$col]) { - $code = PHPExcel_Style_NumberFormat::FORMAT_DATE_YYYYMMDD2; - $sheet - ->getStyle($cell_name) - ->getNumberFormat() - ->setFormatCode($code); - } else { - $cell->setDataType(PHPExcel_Cell_DataType::TYPE_STRING); - } - } - } - } - - private function col($n) { - return chr(ord('A') + $n); - } - -} diff --git a/src/applications/maniphest/export/ManiphestExcelFormat.php b/src/applications/maniphest/export/ManiphestExcelFormat.php deleted file mode 100644 index e455a63656..0000000000 --- a/src/applications/maniphest/export/ManiphestExcelFormat.php +++ /dev/null @@ -1,35 +0,0 @@ -setAncestorClass(__CLASS__) - ->setSortMethod('getOrder') - ->execute(); - } - - abstract public function getName(); - abstract public function getFileName(); - - public function getOrder() { - return 0; - } - - protected function computeExcelDate($epoch) { - $seconds_per_day = (60 * 60 * 24); - $offset = ($seconds_per_day * 25569); - - return ($epoch + $offset) / $seconds_per_day; - } - - /** - * @phutil-external-symbol class PHPExcel - */ - abstract public function buildWorkbook( - PHPExcel $workbook, - array $tasks, - array $handles, - PhabricatorUser $user); - -} diff --git a/src/applications/maniphest/export/__tests__/ManiphestExcelFormatTestCase.php b/src/applications/maniphest/export/__tests__/ManiphestExcelFormatTestCase.php deleted file mode 100644 index a3c312fcd2..0000000000 --- a/src/applications/maniphest/export/__tests__/ManiphestExcelFormatTestCase.php +++ /dev/null @@ -1,10 +0,0 @@ -assertTrue(true); - } - -} diff --git a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php index ec1956bd8e..ad668db376 100644 --- a/src/applications/maniphest/query/ManiphestTaskSearchEngine.php +++ b/src/applications/maniphest/query/ManiphestTaskSearchEngine.php @@ -432,4 +432,111 @@ final class ManiphestTaskSearchEngine return $view; } + + protected function newExportFields() { + $fields = array( + id(new PhabricatorStringExportField()) + ->setKey('monogram') + ->setLabel(pht('Monogram')), + id(new PhabricatorPHIDExportField()) + ->setKey('authorPHID') + ->setLabel(pht('Author PHID')), + id(new PhabricatorStringExportField()) + ->setKey('author') + ->setLabel(pht('Author')), + id(new PhabricatorPHIDExportField()) + ->setKey('ownerPHID') + ->setLabel(pht('Owner PHID')), + id(new PhabricatorStringExportField()) + ->setKey('owner') + ->setLabel(pht('Owner')), + id(new PhabricatorStringExportField()) + ->setKey('status') + ->setLabel(pht('Status')), + id(new PhabricatorStringExportField()) + ->setKey('statusName') + ->setLabel(pht('Status Name')), + id(new PhabricatorStringExportField()) + ->setKey('priority') + ->setLabel(pht('Priority')), + id(new PhabricatorStringExportField()) + ->setKey('priorityName') + ->setLabel(pht('Priority Name')), + id(new PhabricatorStringExportField()) + ->setKey('subtype') + ->setLabel('Subtype'), + id(new PhabricatorURIExportField()) + ->setKey('uri') + ->setLabel(pht('URI')), + id(new PhabricatorStringExportField()) + ->setKey('title') + ->setLabel(pht('Title')), + id(new PhabricatorStringExportField()) + ->setKey('description') + ->setLabel(pht('Description')), + ); + + if (ManiphestTaskPoints::getIsEnabled()) { + $fields[] = id(new PhabricatorIntExportField()) + ->setKey('points') + ->setLabel('Points'); + } + + return $fields; + } + + protected function newExportData(array $tasks) { + $viewer = $this->requireViewer(); + + $phids = array(); + foreach ($tasks as $task) { + $phids[] = $task->getAuthorPHID(); + $phids[] = $task->getOwnerPHID(); + } + $handles = $viewer->loadHandles($phids); + + $export = array(); + foreach ($tasks as $task) { + + $author_phid = $task->getAuthorPHID(); + if ($author_phid) { + $author_name = $handles[$author_phid]->getName(); + } else { + $author_name = null; + } + + $owner_phid = $task->getOwnerPHID(); + if ($owner_phid) { + $owner_name = $handles[$owner_phid]->getName(); + } else { + $owner_name = null; + } + + $status_value = $task->getStatus(); + $status_name = ManiphestTaskStatus::getTaskStatusName($status_value); + + $priority_value = $task->getPriority(); + $priority_name = ManiphestTaskPriority::getTaskPriorityName( + $priority_value); + + $export[] = array( + 'monogram' => $task->getMonogram(), + 'authorPHID' => $author_phid, + 'author' => $author_name, + 'ownerPHID' => $owner_phid, + 'owner' => $owner_name, + 'status' => $status_value, + 'statusName' => $status_name, + 'priority' => $priority_value, + 'priorityName' => $priority_name, + 'points' => $task->getPoints(), + 'subtype' => $task->getSubtype(), + 'title' => $task->getTitle(), + 'uri' => PhabricatorEnv::getProductionURI($task->getURI()), + 'description' => $task->getDescription(), + ); + } + + return $export; + } } diff --git a/src/applications/maniphest/view/ManiphestTaskResultListView.php b/src/applications/maniphest/view/ManiphestTaskResultListView.php index b4cf9a544c..6aafcbdccb 100644 --- a/src/applications/maniphest/view/ManiphestTaskResultListView.php +++ b/src/applications/maniphest/view/ManiphestTaskResultListView.php @@ -175,8 +175,7 @@ final class ManiphestTaskResultListView extends ManiphestView { } if (!$user->isLoggedIn()) { - // Don't show the batch editor or excel export for logged-out users. - // Technically we //could// let them export, but ehh. + // Don't show the batch editor for logged-out users. return null; } @@ -220,14 +219,6 @@ final class ManiphestTaskResultListView extends ManiphestView { ), pht("Bulk Edit Selected \xC2\xBB")); - $export = javelin_tag( - 'a', - array( - 'href' => '/maniphest/export/'.$saved_query->getQueryKey().'/', - 'class' => 'button button-grey', - ), - pht('Export to Excel')); - $hidden = phutil_tag( 'div', array( @@ -239,14 +230,12 @@ final class ManiphestTaskResultListView extends ManiphestView { ''. ''. ''. - ''. ''. ''. ''. '
%s%s%s%s%s%s
', $select_all, $select_none, - $export, '', $submit, $hidden); diff --git a/src/applications/people/application/PhabricatorPeopleApplication.php b/src/applications/people/application/PhabricatorPeopleApplication.php index 28405ca92c..6322b29b24 100644 --- a/src/applications/people/application/PhabricatorPeopleApplication.php +++ b/src/applications/people/application/PhabricatorPeopleApplication.php @@ -42,8 +42,9 @@ final class PhabricatorPeopleApplication extends PhabricatorApplication { return array( '/people/' => array( $this->getQueryRoutePattern() => 'PhabricatorPeopleListController', - 'logs/(?:query/(?P[^/]+)/)?' - => 'PhabricatorPeopleLogsController', + 'logs/' => array( + $this->getQueryRoutePattern() => 'PhabricatorPeopleLogsController', + ), 'invite/' => array( '(?:query/(?P[^/]+)/)?' => 'PhabricatorPeopleInviteListController', diff --git a/src/applications/people/query/PhabricatorPeopleLogQuery.php b/src/applications/people/query/PhabricatorPeopleLogQuery.php index 9bcdc53f49..fc6a87b335 100644 --- a/src/applications/people/query/PhabricatorPeopleLogQuery.php +++ b/src/applications/people/query/PhabricatorPeopleLogQuery.php @@ -9,6 +9,8 @@ final class PhabricatorPeopleLogQuery private $sessionKeys; private $actions; private $remoteAddressPrefix; + private $dateCreatedMin; + private $dateCreatedMax; public function withActorPHIDs(array $actor_phids) { $this->actorPHIDs = $actor_phids; @@ -40,70 +42,81 @@ final class PhabricatorPeopleLogQuery return $this; } - protected function loadPage() { - $table = new PhabricatorUserLog(); - $conn_r = $table->establishConnection('r'); - - $data = queryfx_all( - $conn_r, - 'SELECT * FROM %T %Q %Q %Q', - $table->getTableName(), - $this->buildWhereClause($conn_r), - $this->buildOrderClause($conn_r), - $this->buildLimitClause($conn_r)); - - return $table->loadAllFromArray($data); + public function withDateCreatedBetween($min, $max) { + $this->dateCreatedMin = $min; + $this->dateCreatedMax = $max; + return $this; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { - $where = array(); + public function newResultObject() { + return new PhabricatorUserLog(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); if ($this->actorPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'actorPHID IN (%Ls)', $this->actorPHIDs); } if ($this->userPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'userPHID IN (%Ls)', $this->userPHIDs); } if ($this->relatedPHIDs !== null) { $where[] = qsprintf( - $conn_r, - 'actorPHID IN (%Ls) OR userPHID IN (%Ls)', + $conn, + '(actorPHID IN (%Ls) OR userPHID IN (%Ls))', $this->relatedPHIDs, $this->relatedPHIDs); } if ($this->sessionKeys !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'session IN (%Ls)', $this->sessionKeys); } if ($this->actions !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'action IN (%Ls)', $this->actions); } if ($this->remoteAddressPrefix !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'remoteAddr LIKE %>', $this->remoteAddressPrefix); } - $where[] = $this->buildPagingClause($conn_r); + if ($this->dateCreatedMin !== null) { + $where[] = qsprintf( + $conn, + 'dateCreated >= %d', + $this->dateCreatedMin); + } - return $this->formatWhereClause($where); + if ($this->dateCreatedMax !== null) { + $where[] = qsprintf( + $conn, + 'dateCreated <= %d', + $this->dateCreatedMax); + } + + return $where; } public function getQueryApplicationClass() { diff --git a/src/applications/people/query/PhabricatorPeopleLogSearchEngine.php b/src/applications/people/query/PhabricatorPeopleLogSearchEngine.php index 851ef113d0..b052456cd3 100644 --- a/src/applications/people/query/PhabricatorPeopleLogSearchEngine.php +++ b/src/applications/people/query/PhabricatorPeopleLogSearchEngine.php @@ -15,34 +15,8 @@ final class PhabricatorPeopleLogSearchEngine return 500; } - public function buildSavedQueryFromRequest(AphrontRequest $request) { - $saved = new PhabricatorSavedQuery(); - - $saved->setParameter( - 'userPHIDs', - $this->readUsersFromRequest($request, 'users')); - - $saved->setParameter( - 'actorPHIDs', - $this->readUsersFromRequest($request, 'actors')); - - $saved->setParameter( - 'actions', - $this->readListFromRequest($request, 'actions')); - - $saved->setParameter( - 'ip', - $request->getStr('ip')); - - $saved->setParameter( - 'sessions', - $this->readListFromRequest($request, 'sessions')); - - return $saved; - } - - public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { - $query = id(new PhabricatorPeopleLogQuery()); + public function newQuery() { + $query = new PhabricatorPeopleLogQuery(); // NOTE: If the viewer isn't an administrator, always restrict the query to // related records. This echoes the policy logic of these logs. This is @@ -54,82 +28,73 @@ final class PhabricatorPeopleLogSearchEngine $query->withRelatedPHIDs(array($viewer->getPHID())); } - $actor_phids = $saved->getParameter('actorPHIDs', array()); - if ($actor_phids) { - $query->withActorPHIDs($actor_phids); + return $query; + } + + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); + + if ($map['userPHIDs']) { + $query->withUserPHIDs($map['userPHIDs']); } - $user_phids = $saved->getParameter('userPHIDs', array()); - if ($user_phids) { - $query->withUserPHIDs($user_phids); + if ($map['actorPHIDs']) { + $query->withActorPHIDs($map['actorPHIDs']); } - $actions = $saved->getParameter('actions', array()); - if ($actions) { - $query->withActions($actions); + if ($map['actions']) { + $query->withActions($map['actions']); } - $remote_prefix = $saved->getParameter('ip'); - if (strlen($remote_prefix)) { - $query->withRemoteAddressprefix($remote_prefix); + if (strlen($map['ip'])) { + $query->withRemoteAddressPrefix($map['ip']); } - $sessions = $saved->getParameter('sessions', array()); - if ($sessions) { - $query->withSessionKeys($sessions); + if ($map['sessions']) { + $query->withSessionKeys($map['sessions']); + } + + if ($map['createdStart'] || $map['createdEnd']) { + $query->withDateCreatedBetween( + $map['createdStart'], + $map['createdEnd']); } return $query; } - public function buildSearchForm( - AphrontFormView $form, - PhabricatorSavedQuery $saved) { - - $actor_phids = $saved->getParameter('actorPHIDs', array()); - $user_phids = $saved->getParameter('userPHIDs', array()); - - $actions = $saved->getParameter('actions', array()); - $remote_prefix = $saved->getParameter('ip'); - $sessions = $saved->getParameter('sessions', array()); - - $actions = array_fuse($actions); - $action_control = id(new AphrontFormCheckboxControl()) - ->setLabel(pht('Actions')); - $action_types = PhabricatorUserLog::getActionTypeMap(); - foreach ($action_types as $type => $label) { - $action_control->addCheckbox( - 'actions[]', - $type, - $label, - isset($actions[$label])); - } - - $form - ->appendControl( - id(new AphrontFormTokenizerControl()) - ->setDatasource(new PhabricatorPeopleDatasource()) - ->setName('actors') - ->setLabel(pht('Actors')) - ->setValue($actor_phids)) - ->appendControl( - id(new AphrontFormTokenizerControl()) - ->setDatasource(new PhabricatorPeopleDatasource()) - ->setName('users') - ->setLabel(pht('Users')) - ->setValue($user_phids)) - ->appendChild($action_control) - ->appendChild( - id(new AphrontFormTextControl()) - ->setLabel(pht('Filter IP')) - ->setName('ip') - ->setValue($remote_prefix)) - ->appendChild( - id(new AphrontFormTextControl()) - ->setLabel(pht('Sessions')) - ->setName('sessions') - ->setValue(implode(', ', $sessions))); - + protected function buildCustomSearchFields() { + return array( + id(new PhabricatorUsersSearchField()) + ->setKey('userPHIDs') + ->setAliases(array('users', 'user', 'userPHID')) + ->setLabel(pht('Users')) + ->setDescription(pht('Search for activity affecting specific users.')), + id(new PhabricatorUsersSearchField()) + ->setKey('actorPHIDs') + ->setAliases(array('actors', 'actor', 'actorPHID')) + ->setLabel(pht('Actors')) + ->setDescription(pht('Search for activity by specific users.')), + id(new PhabricatorSearchCheckboxesField()) + ->setKey('actions') + ->setLabel(pht('Actions')) + ->setDescription(pht('Search for particular types of activity.')) + ->setOptions(PhabricatorUserLog::getActionTypeMap()), + id(new PhabricatorSearchTextField()) + ->setKey('ip') + ->setLabel(pht('Filter IP')) + ->setDescription(pht('Search for actions by remote address.')), + id(new PhabricatorSearchStringListField()) + ->setKey('sessions') + ->setLabel(pht('Sessions')) + ->setDescription(pht('Search for activity in particular sessions.')), + id(new PhabricatorSearchDateField()) + ->setLabel(pht('Created After')) + ->setKey('createdStart'), + id(new PhabricatorSearchDateField()) + ->setLabel(pht('Created Before')) + ->setKey('createdEnd'), + ); } protected function getURI($path) { @@ -156,19 +121,6 @@ final class PhabricatorPeopleLogSearchEngine return parent::buildSavedQueryFromBuiltin($query_key); } - protected function getRequiredHandlePHIDsForResultList( - array $logs, - PhabricatorSavedQuery $query) { - - $phids = array(); - foreach ($logs as $log) { - $phids[$log->getActorPHID()] = true; - $phids[$log->getUserPHID()] = true; - } - - return array_keys($phids); - } - protected function renderResultList( array $logs, PhabricatorSavedQuery $query, @@ -179,16 +131,111 @@ final class PhabricatorPeopleLogSearchEngine $table = id(new PhabricatorUserLogView()) ->setUser($viewer) - ->setLogs($logs) - ->setHandles($handles); + ->setLogs($logs); if ($viewer->getIsAdmin()) { $table->setSearchBaseURI($this->getApplicationURI('logs/')); } - $result = new PhabricatorApplicationSearchResultView(); - $result->setTable($table); - - return $result; + return id(new PhabricatorApplicationSearchResultView()) + ->setTable($table); } + + protected function newExportFields() { + $viewer = $this->requireViewer(); + + $fields = array( + $fields[] = id(new PhabricatorPHIDExportField()) + ->setKey('actorPHID') + ->setLabel(pht('Actor PHID')), + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('actor') + ->setLabel(pht('Actor')), + $fields[] = id(new PhabricatorPHIDExportField()) + ->setKey('userPHID') + ->setLabel(pht('User PHID')), + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('user') + ->setLabel(pht('User')), + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('action') + ->setLabel(pht('Action')), + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('actionName') + ->setLabel(pht('Action Name')), + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('session') + ->setLabel(pht('Session')), + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('old') + ->setLabel(pht('Old Value')), + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('new') + ->setLabel(pht('New Value')), + ); + + if ($viewer->getIsAdmin()) { + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('remoteAddress') + ->setLabel(pht('Remote Address')); + } + + return $fields; + } + + protected function newExportData(array $logs) { + $viewer = $this->requireViewer(); + + + $phids = array(); + foreach ($logs as $log) { + $phids[] = $log->getUserPHID(); + $phids[] = $log->getActorPHID(); + } + $handles = $viewer->loadHandles($phids); + + $action_map = PhabricatorUserLog::getActionTypeMap(); + + $export = array(); + foreach ($logs as $log) { + + $user_phid = $log->getUserPHID(); + if ($user_phid) { + $user_name = $handles[$user_phid]->getName(); + } else { + $user_name = null; + } + + $actor_phid = $log->getActorPHID(); + if ($actor_phid) { + $actor_name = $handles[$actor_phid]->getName(); + } else { + $actor_name = null; + } + + $action = $log->getAction(); + $action_name = idx($action_map, $action, pht('Unknown ("%s")', $action)); + + $map = array( + 'actorPHID' => $actor_phid, + 'actor' => $actor_name, + 'userPHID' => $user_phid, + 'user' => $user_name, + 'action' => $action, + 'actionName' => $action_name, + 'session' => substr($log->getSession(), 0, 6), + 'old' => $log->getOldValue(), + 'new' => $log->getNewValue(), + ); + + if ($viewer->getIsAdmin()) { + $map['remoteAddress'] = $log->getRemoteAddr(); + } + + $export[] = $map; + } + + return $export; + } + } diff --git a/src/applications/people/query/PhabricatorPeopleSearchEngine.php b/src/applications/people/query/PhabricatorPeopleSearchEngine.php index db2256a8b8..57ed133df4 100644 --- a/src/applications/people/query/PhabricatorPeopleSearchEngine.php +++ b/src/applications/people/query/PhabricatorPeopleSearchEngine.php @@ -322,35 +322,23 @@ final class PhabricatorPeopleSearchEngine protected function newExportFields() { return array( - id(new PhabricatorIDExportField()) - ->setKey('id') - ->setLabel(pht('ID')), - id(new PhabricatorPHIDExportField()) - ->setKey('phid') - ->setLabel(pht('PHID')), id(new PhabricatorStringExportField()) ->setKey('username') ->setLabel(pht('Username')), id(new PhabricatorStringExportField()) ->setKey('realName') ->setLabel(pht('Real Name')), - id(new PhabricatorEpochExportField()) - ->setKey('created') - ->setLabel(pht('Date Created')), ); } - public function newExport(array $users) { + protected function newExportData(array $users) { $viewer = $this->requireViewer(); $export = array(); foreach ($users as $user) { $export[] = array( - 'id' => $user->getID(), - 'phid' => $user->getPHID(), 'username' => $user->getUsername(), 'realName' => $user->getRealName(), - 'created' => $user->getDateCreated(), ); } diff --git a/src/applications/people/view/PhabricatorUserLogView.php b/src/applications/people/view/PhabricatorUserLogView.php index d648b248f7..72a9378a1a 100644 --- a/src/applications/people/view/PhabricatorUserLogView.php +++ b/src/applications/people/view/PhabricatorUserLogView.php @@ -3,7 +3,6 @@ final class PhabricatorUserLogView extends AphrontView { private $logs; - private $handles; private $searchBaseURI; public function setSearchBaseURI($search_base_uri) { @@ -17,45 +16,79 @@ final class PhabricatorUserLogView extends AphrontView { return $this; } - public function setHandles(array $handles) { - assert_instances_of($handles, 'PhabricatorObjectHandle'); - $this->handles = $handles; - return $this; - } - public function render() { $logs = $this->logs; - $handles = $this->handles; $viewer = $this->getUser(); + $phids = array(); + foreach ($logs as $log) { + $phids[] = $log->getActorPHID(); + $phids[] = $log->getUserPHID(); + } + $handles = $viewer->loadHandles($phids); + $action_map = PhabricatorUserLog::getActionTypeMap(); $base_uri = $this->searchBaseURI; + $viewer_phid = $viewer->getPHID(); + $rows = array(); foreach ($logs as $log) { - $ip = $log->getRemoteAddr(); $session = substr($log->getSession(), 0, 6); - if ($base_uri) { - $ip = phutil_tag( - 'a', - array( - 'href' => $base_uri.'?ip='.$ip.'#R', - ), - $ip); + $actor_phid = $log->getActorPHID(); + $user_phid = $log->getUserPHID(); + + if ($viewer->getIsAdmin()) { + $can_see_ip = true; + } else if ($viewer_phid == $actor_phid) { + // You can see the address if you took the action. + $can_see_ip = true; + } else if (!$actor_phid && ($viewer_phid == $user_phid)) { + // You can see the address if it wasn't authenticated and applied + // to you (partial login). + $can_see_ip = true; + } else { + // You can't see the address when an administrator disables your + // account, since it's their address. + $can_see_ip = false; + } + + if ($can_see_ip) { + $ip = $log->getRemoteAddr(); + if ($base_uri) { + $ip = phutil_tag( + 'a', + array( + 'href' => $base_uri.'?ip='.$ip.'#R', + ), + $ip); + } + } else { + $ip = null; } $action = $log->getAction(); $action_name = idx($action_map, $action, $action); + if ($actor_phid) { + $actor_name = $handles[$actor_phid]->renderLink(); + } else { + $actor_name = null; + } + + if ($user_phid) { + $user_name = $handles[$user_phid]->renderLink(); + } else { + $user_name = null; + } + $rows[] = array( phabricator_date($log->getDateCreated(), $viewer), phabricator_time($log->getDateCreated(), $viewer), $action_name, - $log->getActorPHID() - ? $handles[$log->getActorPHID()]->getName() - : null, - $username = $handles[$log->getUserPHID()]->renderLink(), + $actor_name, + $user_name, $ip, $session, ); diff --git a/src/applications/repository/query/PhabricatorRepositoryPullEventQuery.php b/src/applications/repository/query/PhabricatorRepositoryPullEventQuery.php index af60ee0383..ce14a6f831 100644 --- a/src/applications/repository/query/PhabricatorRepositoryPullEventQuery.php +++ b/src/applications/repository/query/PhabricatorRepositoryPullEventQuery.php @@ -7,6 +7,8 @@ final class PhabricatorRepositoryPullEventQuery private $phids; private $repositoryPHIDs; private $pullerPHIDs; + private $epochMin; + private $epochMax; public function withIDs(array $ids) { $this->ids = $ids; @@ -28,6 +30,12 @@ final class PhabricatorRepositoryPullEventQuery return $this; } + public function withEpochBetween($min, $max) { + $this->epochMin = $min; + $this->epochMax = $max; + return $this; + } + public function newResultObject() { return new PhabricatorRepositoryPullEvent(); } @@ -103,6 +111,20 @@ final class PhabricatorRepositoryPullEventQuery $this->pullerPHIDs); } + if ($this->epochMin !== null) { + $where[] = qsprintf( + $conn, + 'epoch >= %d', + $this->epochMin); + } + + if ($this->epochMax !== null) { + $where[] = qsprintf( + $conn, + 'epoch <= %d', + $this->epochMax); + } + return $where; } diff --git a/src/applications/repository/query/PhabricatorRepositoryPushLogQuery.php b/src/applications/repository/query/PhabricatorRepositoryPushLogQuery.php index 94a0b6922e..cb097fae2f 100644 --- a/src/applications/repository/query/PhabricatorRepositoryPushLogQuery.php +++ b/src/applications/repository/query/PhabricatorRepositoryPushLogQuery.php @@ -10,6 +10,8 @@ final class PhabricatorRepositoryPushLogQuery private $refTypes; private $newRefs; private $pushEventPHIDs; + private $epochMin; + private $epochMax; public function withIDs(array $ids) { $this->ids = $ids; @@ -46,19 +48,18 @@ final class PhabricatorRepositoryPushLogQuery return $this; } + public function withEpochBetween($min, $max) { + $this->epochMin = $min; + $this->epochMax = $max; + return $this; + } + + public function newResultObject() { + return new PhabricatorRepositoryPushLog(); + } + protected function loadPage() { - $table = new PhabricatorRepositoryPushLog(); - $conn_r = $table->establishConnection('r'); - - $data = queryfx_all( - $conn_r, - 'SELECT * FROM %T %Q %Q %Q', - $table->getTableName(), - $this->buildWhereClause($conn_r), - $this->buildOrderClause($conn_r), - $this->buildLimitClause($conn_r)); - - return $table->loadAllFromArray($data); + return $this->loadStandardPage($this->newResultObject()); } protected function willFilterPage(array $logs) { @@ -82,61 +83,73 @@ final class PhabricatorRepositoryPushLogQuery return $logs; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { - $where = array(); + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); - if ($this->ids) { + if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } - if ($this->phids) { + if ($this->phids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'phid IN (%Ls)', $this->phids); } - if ($this->repositoryPHIDs) { + if ($this->repositoryPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'repositoryPHID IN (%Ls)', $this->repositoryPHIDs); } - if ($this->pusherPHIDs) { + if ($this->pusherPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'pusherPHID in (%Ls)', $this->pusherPHIDs); } - if ($this->pushEventPHIDs) { + if ($this->pushEventPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'pushEventPHID in (%Ls)', $this->pushEventPHIDs); } - if ($this->refTypes) { + if ($this->refTypes !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'refType IN (%Ls)', $this->refTypes); } - if ($this->newRefs) { + if ($this->newRefs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'refNew IN (%Ls)', $this->newRefs); } - $where[] = $this->buildPagingClause($conn_r); + if ($this->epochMin !== null) { + $where[] = qsprintf( + $conn, + 'epoch >= %d', + $this->epochMin); + } - return $this->formatWhereClause($where); + if ($this->epochMax !== null) { + $where[] = qsprintf( + $conn, + 'epoch <= %d', + $this->epochMax); + } + + return $where; } public function getQueryApplicationClass() { diff --git a/src/applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php b/src/applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php index d171b80999..8ad76987f9 100644 --- a/src/applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php +++ b/src/applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php @@ -26,6 +26,12 @@ final class PhabricatorRepositoryPushLogSearchEngine $query->withPusherPHIDs($map['pusherPHIDs']); } + if ($map['createdStart'] || $map['createdEnd']) { + $query->withEpochBetween( + $map['createdStart'], + $map['createdEnd']); + } + return $query; } @@ -44,6 +50,12 @@ final class PhabricatorRepositoryPushLogSearchEngine ->setLabel(pht('Pushers')) ->setDescription( pht('Search for pull logs by specific users.')), + id(new PhabricatorSearchDateField()) + ->setLabel(pht('Created After')) + ->setKey('createdStart'), + id(new PhabricatorSearchDateField()) + ->setLabel(pht('Created Before')) + ->setKey('createdEnd'), ); } @@ -82,4 +94,146 @@ final class PhabricatorRepositoryPushLogSearchEngine ->setTable($table); } + protected function newExportFields() { + $viewer = $this->requireViewer(); + + $fields = array( + $fields[] = id(new PhabricatorIDExportField()) + ->setKey('pushID') + ->setLabel(pht('Push ID')), + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('protocol') + ->setLabel(pht('Protocol')), + $fields[] = id(new PhabricatorPHIDExportField()) + ->setKey('repositoryPHID') + ->setLabel(pht('Repository PHID')), + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('repository') + ->setLabel(pht('Repository')), + $fields[] = id(new PhabricatorPHIDExportField()) + ->setKey('pusherPHID') + ->setLabel(pht('Pusher PHID')), + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('pusher') + ->setLabel(pht('Pusher')), + $fields[] = id(new PhabricatorPHIDExportField()) + ->setKey('devicePHID') + ->setLabel(pht('Device PHID')), + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('device') + ->setLabel(pht('Device')), + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('type') + ->setLabel(pht('Ref Type')), + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('name') + ->setLabel(pht('Ref Name')), + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('old') + ->setLabel(pht('Ref Old')), + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('new') + ->setLabel(pht('Ref New')), + $fields[] = id(new PhabricatorIntExportField()) + ->setKey('flags') + ->setLabel(pht('Flags')), + $fields[] = id(new PhabricatorStringListExportField()) + ->setKey('flagNames') + ->setLabel(pht('Flag Names')), + $fields[] = id(new PhabricatorIntExportField()) + ->setKey('result') + ->setLabel(pht('Result')), + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('resultName') + ->setLabel(pht('Result Name')), + ); + + if ($viewer->getIsAdmin()) { + $fields[] = id(new PhabricatorStringExportField()) + ->setKey('remoteAddress') + ->setLabel(pht('Remote Address')); + } + + return $fields; + } + + protected function newExportData(array $logs) { + $viewer = $this->requireViewer(); + + $phids = array(); + foreach ($logs as $log) { + $phids[] = $log->getPusherPHID(); + $phids[] = $log->getDevicePHID(); + $phids[] = $log->getPushEvent()->getRepositoryPHID(); + } + $handles = $viewer->loadHandles($phids); + + $flag_map = PhabricatorRepositoryPushLog::getFlagDisplayNames(); + $reject_map = PhabricatorRepositoryPushLog::getRejectCodeDisplayNames(); + + $export = array(); + foreach ($logs as $log) { + $event = $log->getPushEvent(); + + $repository_phid = $event->getRepositoryPHID(); + if ($repository_phid) { + $repository_name = $handles[$repository_phid]->getName(); + } else { + $repository_name = null; + } + + $pusher_phid = $log->getPusherPHID(); + if ($pusher_phid) { + $pusher_name = $handles[$pusher_phid]->getName(); + } else { + $pusher_name = null; + } + + $device_phid = $log->getDevicePHID(); + if ($device_phid) { + $device_name = $handles[$device_phid]->getName(); + } else { + $device_name = null; + } + + $flags = $log->getChangeFlags(); + $flag_names = array(); + foreach ($flag_map as $flag_key => $flag_name) { + if (($flags & $flag_key) === $flag_key) { + $flag_names[] = $flag_name; + } + } + + $result = $event->getRejectCode(); + $result_name = idx($reject_map, $result, pht('Unknown ("%s")', $result)); + + $map = array( + 'pushID' => $event->getID(), + 'protocol' => $event->getRemoteProtocol(), + 'repositoryPHID' => $repository_phid, + 'repository' => $repository_name, + 'pusherPHID' => $pusher_phid, + 'pusher' => $pusher_name, + 'devicePHID' => $device_phid, + 'device' => $device_name, + 'type' => $log->getRefType(), + 'name' => $log->getRefName(), + 'old' => $log->getRefOld(), + 'new' => $log->getRefNew(), + 'flags' => $flags, + 'flagNames' => $flag_names, + 'result' => $result, + 'resultName' => $result_name, + ); + + if ($viewer->getIsAdmin()) { + $map['remoteAddress'] = $event->getRemoteAddress(); + } + + $export[] = $map; + } + + return $export; + } + } diff --git a/src/applications/repository/storage/PhabricatorRepositoryPushLog.php b/src/applications/repository/storage/PhabricatorRepositoryPushLog.php index 4e099209c6..c2d3456da6 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryPushLog.php +++ b/src/applications/repository/storage/PhabricatorRepositoryPushLog.php @@ -55,6 +55,28 @@ final class PhabricatorRepositoryPushLog ->setPusherPHID($viewer->getPHID()); } + public static function getFlagDisplayNames() { + return array( + self::CHANGEFLAG_ADD => pht('Create'), + self::CHANGEFLAG_DELETE => pht('Delete'), + self::CHANGEFLAG_APPEND => pht('Append'), + self::CHANGEFLAG_REWRITE => pht('Rewrite'), + self::CHANGEFLAG_DANGEROUS => pht('Dangerous'), + self::CHANGEFLAG_ENORMOUS => pht('Enormous'), + ); + } + + public static function getRejectCodeDisplayNames() { + return array( + self::REJECT_ACCEPT => pht('Accepted'), + self::REJECT_DANGEROUS => pht('Rejected: Dangerous'), + self::REJECT_HERALD => pht('Rejected: Herald'), + self::REJECT_EXTERNAL => pht('Rejected: External Hook'), + self::REJECT_BROKEN => pht('Rejected: Broken'), + self::REJECT_ENORMOUS => pht('Rejected: Enormous'), + ); + } + public static function getHeraldChangeFlagConditionOptions() { return array( self::CHANGEFLAG_ADD => @@ -102,6 +124,9 @@ final class PhabricatorRepositoryPushLog 'key_pusher' => array( 'columns' => array('pusherPHID'), ), + 'key_epoch' => array( + 'columns' => array('epoch'), + ), ), ) + parent::getConfiguration(); } diff --git a/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php b/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php index 9fb6667924..818d1e8781 100644 --- a/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php +++ b/src/applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php @@ -15,6 +15,7 @@ final class PhabricatorRepositoryCommitHeraldWorker protected function parseCommit( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { + $viewer = PhabricatorUser::getOmnipotentUser(); if ($this->shouldSkipImportStep()) { // This worker has no followup tasks, so we can just bail out @@ -50,7 +51,7 @@ final class PhabricatorRepositoryCommitHeraldWorker id(new PhabricatorDiffusionApplication())->getPHID()); $editor = id(new PhabricatorAuditEditor()) - ->setActor(PhabricatorUser::getOmnipotentUser()) + ->setActor($viewer) ->setActingAsPHID($acting_as_phid) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true) @@ -69,29 +70,6 @@ final class PhabricatorRepositoryCommitHeraldWorker 'committerPHID' => $data->getCommitDetail('committerPHID'), )); - $reverts_refs = id(new DifferentialCustomFieldRevertsParser()) - ->parseCorpus($data->getCommitMessage()); - $reverts = array_mergev(ipull($reverts_refs, 'monograms')); - - if ($reverts) { - $reverted_commits = id(new DiffusionCommitQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withRepository($repository) - ->withIdentifiers($reverts) - ->execute(); - $reverted_commit_phids = mpull($reverted_commits, 'getPHID', 'getPHID'); - - // NOTE: Skip any write attempts if a user cleverly implies a commit - // reverts itself. - unset($reverted_commit_phids[$commit->getPHID()]); - - $reverts_edge = DiffusionCommitRevertsCommitEdgeType::EDGECONST; - $xactions[] = id(new PhabricatorAuditTransaction()) - ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) - ->setMetadataValue('edge:type', $reverts_edge) - ->setNewValue(array('+' => array_fuse($reverted_commit_phids))); - } - try { $raw_patch = $this->loadRawPatchText($repository, $commit); } catch (Exception $ex) { diff --git a/src/applications/search/controller/PhabricatorApplicationSearchController.php b/src/applications/search/controller/PhabricatorApplicationSearchController.php index d3b05d1e3e..cc8bcefff1 100644 --- a/src/applications/search/controller/PhabricatorApplicationSearchController.php +++ b/src/applications/search/controller/PhabricatorApplicationSearchController.php @@ -7,6 +7,7 @@ final class PhabricatorApplicationSearchController private $navigation; private $queryKey; private $preface; + private $activeQuery; public function setPreface($preface) { $this->preface = $preface; @@ -45,6 +46,14 @@ final class PhabricatorApplicationSearchController return $this->searchEngine; } + protected function getActiveQuery() { + if (!$this->activeQuery) { + throw new Exception(pht('There is no active query yet.')); + } + + return $this->activeQuery; + } + protected function validateDelegatingController() { $parent = $this->getDelegatingController(); @@ -154,10 +163,12 @@ final class PhabricatorApplicationSearchController $saved_query = $engine->buildSavedQueryFromRequest($request); // Save the query to generate a query key, so "Save Custom Query..." and - // other features like Maniphest's "Export..." work correctly. + // other features like "Bulk Edit" and "Export Data" work correctly. $engine->saveQuery($saved_query); } + $this->activeQuery = $saved_query; + $nav->selectFilter( 'query/'.$saved_query->getQueryKey(), 'query/advanced'); @@ -410,20 +421,72 @@ final class PhabricatorApplicationSearchController if ($named_query) { $filename = $named_query->getQueryName(); + $sheet_title = $named_query->getQueryName(); } else { $filename = $engine->getResultTypeDescription(); + $sheet_title = $engine->getResultTypeDescription(); } $filename = phutil_utf8_strtolower($filename); $filename = PhabricatorFile::normalizeFileName($filename); - $formats = PhabricatorExportFormat::getAllEnabledExportFormats(); - $format_options = mpull($formats, 'getExportFormatName'); + $all_formats = PhabricatorExportFormat::getAllExportFormats(); + + $available_options = array(); + $unavailable_options = array(); + $formats = array(); + $unavailable_formats = array(); + foreach ($all_formats as $key => $format) { + if ($format->isExportFormatEnabled()) { + $available_options[$key] = $format->getExportFormatName(); + $formats[$key] = $format; + } else { + $unavailable_options[$key] = pht( + '%s (Not Available)', + $format->getExportFormatName()); + $unavailable_formats[$key] = $format; + } + } + $format_options = $available_options + $unavailable_options; + + // Try to default to the format the user used last time. If you just + // exported to Excel, you probably want to export to Excel again. + $format_key = $this->readExportFormatPreference(); + if (!isset($formats[$format_key])) { + $format_key = head_key($format_options); + } + + // Check if this is a large result set or not. If we're exporting a + // large amount of data, we'll build the actual export file in the daemons. + + $threshold = 1000; + $query = $engine->buildQueryFromSavedQuery($saved_query); + $pager = $engine->newPagerForSavedQuery($saved_query); + $pager->setPageSize($threshold + 1); + $objects = $engine->executeQuery($query, $pager); + $object_count = count($objects); + $is_large_export = ($object_count > $threshold); $errors = array(); $e_format = null; if ($request->isFormPost()) { $format_key = $request->getStr('format'); + + if (isset($unavailable_formats[$format_key])) { + $unavailable = $unavailable_formats[$format_key]; + $instructions = $unavailable->getInstallInstructions(); + + $markup = id(new PHUIRemarkupView($viewer, $instructions)) + ->setRemarkupOption( + PHUIRemarkupView::OPTION_PRESERVE_LINEBREAKS, + false); + + return $this->newDialog() + ->setTitle(pht('Export Format Not Available')) + ->appendChild($markup) + ->addCancelButton($cancel_uri, pht('Done')); + } + $format = idx($formats, $format_key); if (!$format) { @@ -432,63 +495,33 @@ final class PhabricatorApplicationSearchController } if (!$errors) { - $query = $engine->buildQueryFromSavedQuery($saved_query); + $this->writeExportFormatPreference($format_key); - // NOTE: We aren't reading the pager from the request. Exports always - // affect the entire result set. - $pager = $engine->newPagerForSavedQuery($saved_query); - $pager->setPageSize(0x7FFFFFFF); + $export_engine = id(new PhabricatorExportEngine()) + ->setViewer($viewer) + ->setSearchEngine($engine) + ->setSavedQuery($saved_query) + ->setTitle($sheet_title) + ->setFilename($filename) + ->setExportFormat($format); - $objects = $engine->executeQuery($query, $pager); + if ($is_large_export) { + $job = $export_engine->newBulkJob($request); - $extension = $format->getFileExtension(); - $mime_type = $format->getMIMEContentType(); - $filename = $filename.'.'.$extension; + return id(new AphrontRedirectResponse()) + ->setURI($job->getMonitorURI()); + } else { + $file = $export_engine->exportFile(); - $format = clone $format; - $format->setViewer($viewer); - - $export_data = $engine->newExport($objects); - - if (count($export_data) !== count($objects)) { - throw new Exception( - pht( - 'Search engine exported the wrong number of objects, expected '. - '%s but got %s.', - phutil_count($objects), - phutil_count($export_data))); + return $this->newDialog() + ->setTitle(pht('Download Results')) + ->appendParagraph( + pht('Click the download button to download the exported data.')) + ->addCancelButton($cancel_uri, pht('Done')) + ->setSubmitURI($file->getDownloadURI()) + ->setDisableWorkflowOnSubmit(true) + ->addSubmitButton(pht('Download Data')); } - - $objects = array_values($objects); - $export_data = array_values($export_data); - - $field_list = $engine->newExportFieldList(); - $field_list = mpull($field_list, null, 'getKey'); - - for ($ii = 0; $ii < count($objects); $ii++) { - $format->addObject($objects[$ii], $field_list, $export_data[$ii]); - } - - $export_result = $format->newFileData(); - - $file = PhabricatorFile::newFromFileData( - $export_result, - array( - 'name' => $filename, - 'authorPHID' => $viewer->getPHID(), - 'ttl.relative' => phutil_units('15 minutes in seconds'), - 'viewPolicy' => PhabricatorPolicies::POLICY_NOONE, - 'mime-type' => $mime_type, - )); - - return $this->newDialog() - ->setTitle(pht('Download Results')) - ->appendParagraph( - pht('Click the download button to download the exported data.')) - ->addCancelButton($cancel_uri, pht('Done')) - ->setSubmitURI($file->getDownloadURI()) - ->setDisableWorkflowOnSubmit(true) - ->addSubmitButton(pht('Download Data')); } } @@ -499,6 +532,7 @@ final class PhabricatorApplicationSearchController ->setName('format') ->setLabel(pht('Format')) ->setError($e_format) + ->setValue($format_key) ->setOptions($format_options)); return $this->newDialog() @@ -844,10 +878,8 @@ final class PhabricatorApplicationSearchController $engine = $this->getSearchEngine(); $engine_class = get_class($engine); - $query_key = $this->getQueryKey(); - if (!$query_key) { - $query_key = $engine->getDefaultQueryKey(); - } + + $query_key = $this->getActiveQuery()->getQueryKey(); $can_use = $engine->canUseInPanelContext(); $is_installed = PhabricatorApplication::isClassInstalledForViewer( @@ -914,4 +946,32 @@ final class PhabricatorApplicationSearchController return true; } + private function readExportFormatPreference() { + $viewer = $this->getViewer(); + $export_key = PhabricatorPolicyFavoritesSetting::SETTINGKEY; + return $viewer->getUserSetting($export_key); + } + + private function writeExportFormatPreference($value) { + $viewer = $this->getViewer(); + $request = $this->getRequest(); + + if (!$viewer->isLoggedIn()) { + return; + } + + $export_key = PhabricatorPolicyFavoritesSetting::SETTINGKEY; + $preferences = PhabricatorUserPreferences::loadUserPreferences($viewer); + + $editor = id(new PhabricatorUserPreferencesEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true); + + $xactions = array(); + $xactions[] = $preferences->newTransaction($export_key, $value); + $editor->applyTransactions($preferences, $xactions); + } + } diff --git a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php index 33efd890bb..b808291a52 100644 --- a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php +++ b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php @@ -1455,11 +1455,145 @@ abstract class PhabricatorApplicationSearchEngine extends Phobject { } final public function newExportFieldList() { - return $this->newExportFields(); + $object = $this->newResultObject(); + + $builtin_fields = array( + id(new PhabricatorIDExportField()) + ->setKey('id') + ->setLabel(pht('ID')), + ); + + if ($object->getConfigOption(LiskDAO::CONFIG_AUX_PHID)) { + $builtin_fields[] = id(new PhabricatorPHIDExportField()) + ->setKey('phid') + ->setLabel(pht('PHID')); + } + + $fields = mpull($builtin_fields, null, 'getKey'); + + $export_fields = $this->newExportFields(); + foreach ($export_fields as $export_field) { + $key = $export_field->getKey(); + + if (isset($fields[$key])) { + throw new Exception( + pht( + 'Search engine ("%s") defines an export field with a key ("%s") '. + 'that collides with another field. Each field must have a '. + 'unique key.', + get_class($this), + $key)); + } + + $fields[$key] = $export_field; + } + + $extensions = $this->newExportExtensions(); + foreach ($extensions as $extension) { + $extension_fields = $extension->newExportFields(); + foreach ($extension_fields as $extension_field) { + $key = $extension_field->getKey(); + + if (isset($fields[$key])) { + throw new Exception( + pht( + 'Export engine extension ("%s") defines an export field with '. + 'a key ("%s") that collides with another field. Each field '. + 'must have a unique key.', + get_class($extension_field), + $key)); + } + + $fields[$key] = $extension_field; + } + } + + return $fields; + } + + final public function newExport(array $objects) { + $object = $this->newResultObject(); + $has_phid = $object->getConfigOption(LiskDAO::CONFIG_AUX_PHID); + + $objects = array_values($objects); + $n = count($objects); + + $maps = array(); + foreach ($objects as $object) { + $map = array( + 'id' => $object->getID(), + ); + + if ($has_phid) { + $map['phid'] = $object->getPHID(); + } + + $maps[] = $map; + } + + $export_data = $this->newExportData($objects); + $export_data = array_values($export_data); + if (count($export_data) !== count($objects)) { + throw new Exception( + pht( + 'Search engine ("%s") exported the wrong number of objects, '. + 'expected %s but got %s.', + get_class($this), + phutil_count($objects), + phutil_count($export_data))); + } + + for ($ii = 0; $ii < $n; $ii++) { + $maps[$ii] += $export_data[$ii]; + } + + $extensions = $this->newExportExtensions(); + foreach ($extensions as $extension) { + $extension_data = $extension->newExportData($objects); + $extension_data = array_values($extension_data); + if (count($export_data) !== count($objects)) { + throw new Exception( + pht( + 'Export engine extension ("%s") exported the wrong number of '. + 'objects, expected %s but got %s.', + get_class($extension), + phutil_count($objects), + phutil_count($export_data))); + } + + for ($ii = 0; $ii < $n; $ii++) { + $maps[$ii] += $extension_data[$ii]; + } + } + + return $maps; } protected function newExportFields() { return array(); } + protected function newExportData(array $objects) { + throw new PhutilMethodNotImplementedException(); + } + + private function newExportExtensions() { + $object = $this->newResultObject(); + $viewer = $this->requireViewer(); + + $extensions = PhabricatorExportEngineExtension::getAllExtensions(); + + $supported = array(); + foreach ($extensions as $extension) { + $extension = clone $extension; + $extension->setViewer($viewer); + + if ($extension->supportsObject($object)) { + $supported[] = $extension; + } + } + + return $supported; + } + } diff --git a/src/applications/settings/panel/PhabricatorActivitySettingsPanel.php b/src/applications/settings/panel/PhabricatorActivitySettingsPanel.php index 50f951d661..2759f3a26c 100644 --- a/src/applications/settings/panel/PhabricatorActivitySettingsPanel.php +++ b/src/applications/settings/panel/PhabricatorActivitySettingsPanel.php @@ -26,25 +26,9 @@ final class PhabricatorActivitySettingsPanel extends PhabricatorSettingsPanel { ->withRelatedPHIDs(array($user->getPHID())) ->executeWithCursorPager($pager); - $phids = array(); - foreach ($logs as $log) { - $phids[] = $log->getUserPHID(); - $phids[] = $log->getActorPHID(); - } - - if ($phids) { - $handles = id(new PhabricatorHandleQuery()) - ->setViewer($viewer) - ->withPHIDs($phids) - ->execute(); - } else { - $handles = array(); - } - $table = id(new PhabricatorUserLogView()) ->setUser($viewer) - ->setLogs($logs) - ->setHandles($handles); + ->setLogs($logs); $panel = $this->newBox(pht('Account Activity Logs'), $table); diff --git a/src/applications/transactions/bulk/management/PhabricatorBulkManagementExportWorkflow.php b/src/applications/transactions/bulk/management/PhabricatorBulkManagementExportWorkflow.php new file mode 100644 index 0000000000..cd0ed346fc --- /dev/null +++ b/src/applications/transactions/bulk/management/PhabricatorBulkManagementExportWorkflow.php @@ -0,0 +1,168 @@ +setName('export') + ->setExamples('**export** [options]') + ->setSynopsis( + pht('Export data to a flat file (JSON, CSV, Excel, etc).')) + ->setArguments( + array( + array( + 'name' => 'class', + 'param' => 'class', + 'help' => pht( + 'SearchEngine class to export data from.'), + ), + array( + 'name' => 'format', + 'param' => 'format', + 'help' => pht('Export format.'), + ), + array( + 'name' => 'query', + 'param' => 'key', + 'help' => pht( + 'Export the data selected by this query.'), + ), + array( + 'name' => 'output', + 'param' => 'path', + 'help' => pht( + 'Write output to a file. If omitted, output will be sent to '. + 'stdout.'), + ), + array( + 'name' => 'overwrite', + 'help' => pht( + 'If the output file already exists, overwrite it instead of '. + 'raising an error.'), + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $viewer = $this->getViewer(); + + $class = $args->getArg('class'); + + if (!strlen($class)) { + throw new PhutilArgumentUsageException( + pht( + 'Specify a search engine class to export data from with '. + '"--class".')); + } + + if (!is_subclass_of($class, 'PhabricatorApplicationSearchEngine')) { + throw new PhutilArgumentUsageException( + pht( + 'SearchEngine class ("%s") is unknown.', + $class)); + } + + $engine = newv($class, array()) + ->setViewer($viewer); + + if (!$engine->canExport()) { + throw new PhutilArgumentUsageException( + pht( + 'SearchEngine class ("%s") does not support data export.', + $class)); + } + + $query_key = $args->getArg('query'); + if (!strlen($query_key)) { + throw new PhutilArgumentUsageException( + pht( + 'Specify a query to export with "--query".')); + } + + if ($engine->isBuiltinQuery($query_key)) { + $saved_query = $engine->buildSavedQueryFromBuiltin($query_key); + } else if ($query_key) { + $saved_query = id(new PhabricatorSavedQueryQuery()) + ->setViewer($viewer) + ->withQueryKeys(array($query_key)) + ->executeOne(); + } else { + $saved_query = null; + } + + if (!$saved_query) { + throw new PhutilArgumentUsageException( + pht( + 'Failed to load saved query ("%s").', + $query_key)); + } + + $format_key = $args->getArg('format'); + if (!strlen($format_key)) { + throw new PhutilArgumentUsageException( + pht( + 'Specify an export format with "--format".')); + } + + $all_formats = PhabricatorExportFormat::getAllExportFormats(); + $format = idx($all_formats, $format_key); + if (!$format) { + throw new PhutilArgumentUsageException( + pht( + 'Unknown export format ("%s"). Known formats are: %s.', + $format_key, + implode(', ', array_keys($all_formats)))); + } + + if (!$format->isExportFormatEnabled()) { + throw new PhutilArgumentUsageException( + pht( + 'Export format ("%s") is not enabled.', + $format_key)); + } + + $is_overwrite = $args->getArg('overwrite'); + $output_path = $args->getArg('output'); + + if (!strlen($output_path) && $is_overwrite) { + throw new PhutilArgumentUsageException( + pht( + 'Flag "--overwrite" has no effect without "--output".')); + } + + if (!$is_overwrite) { + if (Filesystem::pathExists($output_path)) { + throw new PhutilArgumentUsageException( + pht( + 'Output path already exists. Use "--overwrite" to overwrite '. + 'it.')); + } + } + + $export_engine = id(new PhabricatorExportEngine()) + ->setViewer($viewer) + ->setTitle(pht('Export')) + ->setFilename(pht('export')) + ->setSearchEngine($engine) + ->setSavedQuery($saved_query) + ->setExportFormat($format); + + $file = $export_engine->exportFile(); + + $iterator = $file->getFileDataIterator(); + + if (strlen($output_path)) { + foreach ($iterator as $chunk) { + Filesystem::appendFile($output_path, $chunk); + } + } else { + foreach ($iterator as $chunk) { + echo $chunk; + } + } + + return 0; + } + +} diff --git a/src/applications/transactions/editfield/PhabricatorPHIDListEditField.php b/src/applications/transactions/editfield/PhabricatorPHIDListEditField.php index b084c142e5..d2b647b9aa 100644 --- a/src/applications/transactions/editfield/PhabricatorPHIDListEditField.php +++ b/src/applications/transactions/editfield/PhabricatorPHIDListEditField.php @@ -5,6 +5,7 @@ abstract class PhabricatorPHIDListEditField private $useEdgeTransactions; private $isSingleValue; + private $isNullable; public function setUseEdgeTransactions($use_edge_transactions) { $this->useEdgeTransactions = $use_edge_transactions; @@ -30,13 +31,23 @@ abstract class PhabricatorPHIDListEditField return $this->isSingleValue; } + public function setIsNullable($is_nullable) { + $this->isNullable = $is_nullable; + return $this; + } + + public function getIsNullable() { + return $this->isNullable; + } + protected function newHTTPParameterType() { return new AphrontPHIDListHTTPParameterType(); } protected function newConduitParameterType() { if ($this->getIsSingleValue()) { - return new ConduitPHIDParameterType(); + return id(new ConduitPHIDParameterType()) + ->setIsNullable($this->getIsNullable()); } else { return new ConduitPHIDListParameterType(); } @@ -99,7 +110,8 @@ abstract class PhabricatorPHIDListEditField } return id(new PhabricatorDatasourceEditType()) - ->setIsSingleValue($this->getIsSingleValue()); + ->setIsSingleValue($this->getIsSingleValue()) + ->setIsNullable($this->getIsNullable()); } protected function newBulkEditTypes() { diff --git a/src/applications/transactions/edittype/PhabricatorPHIDListEditType.php b/src/applications/transactions/edittype/PhabricatorPHIDListEditType.php index 07489521b6..763f6ff001 100644 --- a/src/applications/transactions/edittype/PhabricatorPHIDListEditType.php +++ b/src/applications/transactions/edittype/PhabricatorPHIDListEditType.php @@ -6,6 +6,7 @@ abstract class PhabricatorPHIDListEditType private $datasource; private $isSingleValue; private $defaultValue; + private $isNullable; public function setDatasource(PhabricatorTypeaheadDatasource $datasource) { $this->datasource = $datasource; @@ -30,16 +31,17 @@ abstract class PhabricatorPHIDListEditType return $this; } - public function getDefaultValue() { - return $this->defaultValue; + public function setIsNullable($is_nullable) { + $this->isNullable = $is_nullable; + return $this; } - public function getValueType() { - if ($this->getIsSingleValue()) { - return 'phid'; - } else { - return 'list'; - } + public function getIsNullable() { + return $this->isNullable; + } + + public function getDefaultValue() { + return $this->defaultValue; } protected function newConduitParameterType() { @@ -49,7 +51,8 @@ abstract class PhabricatorPHIDListEditType } if ($this->getIsSingleValue()) { - return new ConduitPHIDParameterType(); + return id(new ConduitPHIDParameterType()) + ->setIsNullable($this->getIsNullable()); } else { return new ConduitPHIDListParameterType(); } diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index 97b57009ec..d5c28b83bf 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -458,6 +458,12 @@ abstract class PhabricatorApplicationTransaction case PhabricatorTransactions::TYPE_JOIN_POLICY: return 'fa-lock'; case PhabricatorTransactions::TYPE_EDGE: + switch ($this->getMetadataValue('edge:type')) { + case DiffusionCommitRevertedByCommitEdgeType::EDGECONST: + return 'fa-undo'; + case DiffusionCommitRevertsCommitEdgeType::EDGECONST: + return 'fa-ambulance'; + } return 'fa-link'; case PhabricatorTransactions::TYPE_BUILDABLE: return 'fa-wrench'; @@ -496,6 +502,14 @@ abstract class PhabricatorApplicationTransaction return 'black'; } break; + case PhabricatorTransactions::TYPE_EDGE: + switch ($this->getMetadataValue('edge:type')) { + case DiffusionCommitRevertedByCommitEdgeType::EDGECONST: + return 'pink'; + case DiffusionCommitRevertsCommitEdgeType::EDGECONST: + return 'sky'; + } + break; case PhabricatorTransactions::TYPE_BUILDABLE: switch ($this->getNewValue()) { case HarbormasterBuildable::STATUS_PASSED: diff --git a/src/infrastructure/customfield/field/PhabricatorCustomField.php b/src/infrastructure/customfield/field/PhabricatorCustomField.php index c96f09f369..818bf119ff 100644 --- a/src/infrastructure/customfield/field/PhabricatorCustomField.php +++ b/src/infrastructure/customfield/field/PhabricatorCustomField.php @@ -35,6 +35,7 @@ abstract class PhabricatorCustomField extends Phobject { const ROLE_HERALD = 'herald'; const ROLE_EDITENGINE = 'EditEngine'; const ROLE_HERALDACTION = 'herald.action'; + const ROLE_EXPORT = 'export'; /* -( Building Applications with Custom Fields )--------------------------- */ @@ -299,6 +300,8 @@ abstract class PhabricatorCustomField extends Phobject { case self::ROLE_EDITENGINE: return $this->shouldAppearInEditView() || $this->shouldAppearInEditEngine(); + case self::ROLE_EXPORT: + return $this->shouldAppearInDataExport(); case self::ROLE_DEFAULT: return true; default: @@ -1362,6 +1365,46 @@ abstract class PhabricatorCustomField extends Phobject { } +/* -( Data Export )-------------------------------------------------------- */ + + + public function shouldAppearInDataExport() { + if ($this->proxy) { + return $this->proxy->shouldAppearInDataExport(); + } + + try { + $this->newExportFieldType(); + return true; + } catch (PhabricatorCustomFieldImplementationIncompleteException $ex) { + return false; + } + } + + public function newExportField() { + if ($this->proxy) { + return $this->proxy->newExportField(); + } + + return $this->newExportFieldType() + ->setLabel($this->getFieldName()); + } + + public function newExportData() { + if ($this->proxy) { + return $this->proxy->newExportData(); + } + throw new PhabricatorCustomFieldImplementationIncompleteException($this); + } + + protected function newExportFieldType() { + if ($this->proxy) { + return $this->proxy->newExportFieldType(); + } + throw new PhabricatorCustomFieldImplementationIncompleteException($this); + } + + /* -( Conduit )------------------------------------------------------------ */ diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php index f617d3a849..5bd6256b73 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomField.php @@ -496,5 +496,8 @@ abstract class PhabricatorStandardCustomField return $this->getFieldValue(); } + public function newExportData() { + return $this->getFieldValue(); + } } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldInt.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldInt.php index 4a3e45d757..f06c30d482 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldInt.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldInt.php @@ -124,4 +124,8 @@ final class PhabricatorStandardCustomFieldInt return new ConduitIntParameterType(); } + protected function newExportFieldType() { + return new PhabricatorIntExportField(); + } + } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php index 8242c477fd..56164bb7b5 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php @@ -76,4 +76,8 @@ final class PhabricatorStandardCustomFieldText return new ConduitStringParameterType(); } + protected function newExportFieldType() { + return new PhabricatorStringExportField(); + } + } diff --git a/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobType.php b/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobType.php index a5e29bc101..7854730bea 100644 --- a/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobType.php +++ b/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobType.php @@ -25,4 +25,24 @@ abstract class PhabricatorWorkerBulkJobType extends Phobject { ->execute(); } + public function getCurtainActions( + PhabricatorUser $viewer, + PhabricatorWorkerBulkJob $job) { + + if ($job->isConfirming()) { + $continue_uri = $job->getMonitorURI(); + } else { + $continue_uri = $job->getDoneURI(); + } + + $continue = id(new PhabricatorActionView()) + ->setHref($continue_uri) + ->setIcon('fa-arrow-circle-o-right') + ->setName(pht('Continue')); + + return array( + $continue, + ); + } + } diff --git a/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobWorker.php b/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobWorker.php index 9117060da6..d11f7e81af 100644 --- a/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobWorker.php +++ b/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerBulkJobWorker.php @@ -77,6 +77,10 @@ abstract class PhabricatorWorkerBulkJobWorker pht('Job actor does not have permission to edit job.')); } + // Allow the worker to fill user caches inline; bulk jobs occasionally + // need to access user preferences. + $actor->setAllowInlineCacheGeneration(true); + return $actor; } diff --git a/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerSingleBulkJobType.php b/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerSingleBulkJobType.php new file mode 100644 index 0000000000..f80411348c --- /dev/null +++ b/src/infrastructure/daemon/workers/bulk/PhabricatorWorkerSingleBulkJobType.php @@ -0,0 +1,27 @@ +getPHID()); + + return $tasks; + } + +} diff --git a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php index 40e43c2ec7..312281617d 100644 --- a/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php +++ b/src/infrastructure/daemon/workers/storage/PhabricatorWorkerBulkJob.php @@ -180,6 +180,10 @@ final class PhabricatorWorkerBulkJob return $this->getJobImplementation()->getJobName($this); } + public function getCurtainActions(PhabricatorUser $viewer) { + return $this->getJobImplementation()->getCurtainActions($viewer, $this); + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ diff --git a/src/infrastructure/export/PhabricatorEpochExportField.php b/src/infrastructure/export/PhabricatorEpochExportField.php deleted file mode 100644 index a19e60b50e..0000000000 --- a/src/infrastructure/export/PhabricatorEpochExportField.php +++ /dev/null @@ -1,27 +0,0 @@ -zone)) { - $this->zone = new DateTimeZone('UTC'); - } - - try { - $date = new DateTime('@'.$value); - } catch (Exception $ex) { - return null; - } - - $date->setTimezone($this->zone); - return $date->format('c'); - } - - public function getNaturalValue($value) { - return (int)$value; - } - -} diff --git a/src/infrastructure/export/PhabricatorIntExportField.php b/src/infrastructure/export/PhabricatorIntExportField.php deleted file mode 100644 index 3363f9b5d5..0000000000 --- a/src/infrastructure/export/PhabricatorIntExportField.php +++ /dev/null @@ -1,10 +0,0 @@ -object = $object; + return ($object instanceof PhabricatorCustomFieldInterface); + } + + public function newExportFields() { + $prototype = $this->object; + + $fields = $this->newCustomFields($prototype); + + $results = array(); + foreach ($fields as $field) { + $field_key = $field->getModernFieldKey(); + + $results[] = $field->newExportField() + ->setKey($field_key); + } + + return $results; + } + + public function newExportData(array $objects) { + $viewer = $this->getViewer(); + + $field_map = array(); + foreach ($objects as $object) { + $object_phid = $object->getPHID(); + + $fields = PhabricatorCustomField::getObjectFields( + $object, + PhabricatorCustomField::ROLE_EXPORT); + + $fields + ->setViewer($viewer) + ->readFieldsFromObject($object); + + $field_map[$object_phid] = $fields; + } + + $all_fields = array(); + foreach ($field_map as $field_list) { + foreach ($field_list->getFields() as $field) { + $all_fields[] = $field; + } + } + + id(new PhabricatorCustomFieldStorageQuery()) + ->addFields($all_fields) + ->execute(); + + $results = array(); + foreach ($objects as $object) { + $object_phid = $object->getPHID(); + $object_fields = $field_map[$object_phid]; + + $map = array(); + foreach ($object_fields->getFields() as $field) { + $key = $field->getModernFieldKey(); + $map[$key] = $field->newExportData(); + } + + $results[] = $map; + } + + return $results; + } + + private function newCustomFields($object) { + $fields = PhabricatorCustomField::getObjectFields( + $object, + PhabricatorCustomField::ROLE_EXPORT); + $fields->setViewer($this->getViewer()); + + return $fields->getFields(); + } + +} diff --git a/src/infrastructure/export/engine/PhabricatorExportEngine.php b/src/infrastructure/export/engine/PhabricatorExportEngine.php new file mode 100644 index 0000000000..f2c6a1270b --- /dev/null +++ b/src/infrastructure/export/engine/PhabricatorExportEngine.php @@ -0,0 +1,168 @@ +viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + public function setSearchEngine( + PhabricatorApplicationSearchEngine $search_engine) { + $this->searchEngine = $search_engine; + return $this; + } + + public function getSearchEngine() { + return $this->searchEngine; + } + + public function setSavedQuery(PhabricatorSavedQuery $saved_query) { + $this->savedQuery = $saved_query; + return $this; + } + + public function getSavedQuery() { + return $this->savedQuery; + } + + public function setExportFormat( + PhabricatorExportFormat $export_format) { + $this->exportFormat = $export_format; + return $this; + } + + public function getExportFormat() { + return $this->exportFormat; + } + + public function setFilename($filename) { + $this->filename = $filename; + return $this; + } + + public function getFilename() { + return $this->filename; + } + + public function setTitle($title) { + $this->title = $title; + return $this; + } + + public function getTitle() { + return $this->title; + } + + public function newBulkJob(AphrontRequest $request) { + $viewer = $this->getViewer(); + $engine = $this->getSearchEngine(); + $saved_query = $this->getSavedQuery(); + $format = $this->getExportFormat(); + + $params = array( + 'engineClass' => get_class($engine), + 'queryKey' => $saved_query->getQueryKey(), + 'formatKey' => $format->getExportFormatKey(), + 'title' => $this->getTitle(), + 'filename' => $this->getFilename(), + ); + + $job = PhabricatorWorkerBulkJob::initializeNewJob( + $viewer, + new PhabricatorExportEngineBulkJobType(), + $params); + + // We queue these jobs directly into STATUS_WAITING without requiring + // a confirmation from the user. + + $xactions = array(); + + $xactions[] = id(new PhabricatorWorkerBulkJobTransaction()) + ->setTransactionType(PhabricatorWorkerBulkJobTransaction::TYPE_STATUS) + ->setNewValue(PhabricatorWorkerBulkJob::STATUS_WAITING); + + $editor = id(new PhabricatorWorkerBulkJobEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnMissingFields(true) + ->applyTransactions($job, $xactions); + + return $job; + } + + public function exportFile() { + $viewer = $this->getViewer(); + $engine = $this->getSearchEngine(); + $saved_query = $this->getSavedQuery(); + $format = $this->getExportFormat(); + $title = $this->getTitle(); + $filename = $this->getFilename(); + + $query = $engine->buildQueryFromSavedQuery($saved_query); + + $extension = $format->getFileExtension(); + $mime_type = $format->getMIMEContentType(); + $filename = $filename.'.'.$extension; + + $format = id(clone $format) + ->setViewer($viewer) + ->setTitle($title); + + $field_list = $engine->newExportFieldList(); + $field_list = mpull($field_list, null, 'getKey'); + $format->addHeaders($field_list); + + // Iterate over the query results in large page so we don't have to hold + // too much stuff in memory. + $page_size = 1000; + $page_cursor = null; + do { + $pager = $engine->newPagerForSavedQuery($saved_query); + $pager->setPageSize($page_size); + + if ($page_cursor !== null) { + $pager->setAfterID($page_cursor); + } + + $objects = $engine->executeQuery($query, $pager); + $objects = array_values($objects); + $page_cursor = $pager->getNextPageID(); + + $export_data = $engine->newExport($objects); + for ($ii = 0; $ii < count($objects); $ii++) { + $format->addObject($objects[$ii], $field_list, $export_data[$ii]); + } + } while ($pager->getHasMoreResults()); + + $export_result = $format->newFileData(); + + // We have all the data in one big string and aren't actually + // streaming it, but pretending that we are allows us to actviate + // the chunk engine and store large files. + $iterator = new ArrayIterator(array($export_result)); + + $source = id(new PhabricatorIteratorFileUploadSource()) + ->setName($filename) + ->setViewPolicy(PhabricatorPolicies::POLICY_NOONE) + ->setMIMEType($mime_type) + ->setRelativeTTL(phutil_units('60 minutes in seconds')) + ->setAuthorPHID($viewer->getPHID()) + ->setIterator($iterator); + + return $source->uploadFile(); + } + +} diff --git a/src/infrastructure/export/engine/PhabricatorExportEngineBulkJobType.php b/src/infrastructure/export/engine/PhabricatorExportEngineBulkJobType.php new file mode 100644 index 0000000000..712127f479 --- /dev/null +++ b/src/infrastructure/export/engine/PhabricatorExportEngineBulkJobType.php @@ -0,0 +1,118 @@ +getParameter('filePHID'); + if (!$file_phid) { + $actions[] = id(new PhabricatorActionView()) + ->setHref('#') + ->setIcon('fa-download') + ->setDisabled(true) + ->setName(pht('Exporting Data...')); + } else { + $file = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs(array($file_phid)) + ->executeOne(); + if (!$file) { + $actions[] = id(new PhabricatorActionView()) + ->setHref('#') + ->setIcon('fa-download') + ->setDisabled(true) + ->setName(pht('Temporary File Expired')); + } else { + $actions[] = id(new PhabricatorActionView()) + ->setRenderAsForm(true) + ->setHref($file->getDownloadURI()) + ->setIcon('fa-download') + ->setName(pht('Download Data Export')); + } + } + + return $actions; + } + + + public function runTask( + PhabricatorUser $actor, + PhabricatorWorkerBulkJob $job, + PhabricatorWorkerBulkTask $task) { + + $engine_class = $job->getParameter('engineClass'); + if (!is_subclass_of($engine_class, 'PhabricatorApplicationSearchEngine')) { + throw new Exception( + pht( + 'Unknown search engine class "%s".', + $engine_class)); + } + + $engine = newv($engine_class, array()) + ->setViewer($actor); + + $query_key = $job->getParameter('queryKey'); + if ($engine->isBuiltinQuery($query_key)) { + $saved_query = $engine->buildSavedQueryFromBuiltin($query_key); + } else if ($query_key) { + $saved_query = id(new PhabricatorSavedQueryQuery()) + ->setViewer($actor) + ->withQueryKeys(array($query_key)) + ->executeOne(); + } else { + $saved_query = null; + } + + if (!$saved_query) { + throw new Exception( + pht( + 'Failed to load saved query ("%s").', + $query_key)); + } + + $format_key = $job->getParameter('formatKey'); + + $all_formats = PhabricatorExportFormat::getAllExportFormats(); + $format = idx($all_formats, $format_key); + if (!$format) { + throw new Exception( + pht( + 'Unknown export format ("%s").', + $format_key)); + } + + if (!$format->isExportFormatEnabled()) { + throw new Exception( + pht( + 'Export format ("%s") is not enabled.', + $format_key)); + } + + $export_engine = id(new PhabricatorExportEngine()) + ->setViewer($actor) + ->setTitle($job->getParameter('title')) + ->setFilename($job->getParameter('filename')) + ->setSearchEngine($engine) + ->setSavedQuery($saved_query) + ->setExportFormat($format); + + $file = $export_engine->exportFile(); + + $job + ->setParameter('filePHID', $file->getPHID()) + ->save(); + } + +} diff --git a/src/infrastructure/export/engine/PhabricatorExportEngineExtension.php b/src/infrastructure/export/engine/PhabricatorExportEngineExtension.php new file mode 100644 index 0000000000..01d4471ef2 --- /dev/null +++ b/src/infrastructure/export/engine/PhabricatorExportEngineExtension.php @@ -0,0 +1,31 @@ +getPhobjectClassConstant('EXTENSIONKEY'); + } + + final public function setViewer($viewer) { + $this->viewer = $viewer; + return $this; + } + + final public function getViewer() { + return $this->viewer; + } + + abstract public function supportsObject($object); + abstract public function newExportFields(); + abstract public function newExportData(array $objects); + + final public static function getAllExtensions() { + return id(new PhutilClassMapQuery()) + ->setAncestorClass(__CLASS__) + ->setUniqueMethod('getExtensionKey') + ->execute(); + } + +} diff --git a/src/infrastructure/export/engine/PhabricatorExportFormatSetting.php b/src/infrastructure/export/engine/PhabricatorExportFormatSetting.php new file mode 100644 index 0000000000..625a90719b --- /dev/null +++ b/src/infrastructure/export/engine/PhabricatorExportFormatSetting.php @@ -0,0 +1,16 @@ +getConfigOption(LiskDAO::CONFIG_TIMESTAMPS)) { + return false; + } + + return true; + } + + public function newExportFields() { + return array( + id(new PhabricatorEpochExportField()) + ->setKey('dateCreated') + ->setLabel(pht('Created')), + id(new PhabricatorEpochExportField()) + ->setKey('dateModified') + ->setLabel(pht('Modified')), + ); + } + + public function newExportData(array $objects) { + $map = array(); + foreach ($objects as $object) { + $map[] = array( + 'dateCreated' => $object->getDateCreated(), + 'dateModified' => $object->getDateModified(), + ); + } + return $map; + } + +} diff --git a/src/infrastructure/export/engine/PhabricatorProjectsExportEngineExtension.php b/src/infrastructure/export/engine/PhabricatorProjectsExportEngineExtension.php new file mode 100644 index 0000000000..eb3bca3a49 --- /dev/null +++ b/src/infrastructure/export/engine/PhabricatorProjectsExportEngineExtension.php @@ -0,0 +1,60 @@ +setKey('tagPHIDs') + ->setLabel(pht('Tag PHIDs')), + id(new PhabricatorStringListExportField()) + ->setKey('tags') + ->setLabel(pht('Tags')), + ); + } + + public function newExportData(array $objects) { + $viewer = $this->getViewer(); + + $object_phids = mpull($objects, 'getPHID'); + + $projects_query = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs($object_phids) + ->withEdgeTypes( + array( + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, + )); + $projects_query->execute(); + + $handles = $viewer->loadHandles($projects_query->getDestinationPHIDs()); + + $map = array(); + foreach ($objects as $object) { + $object_phid = $object->getPHID(); + + $project_phids = $projects_query->getDestinationPHIDs( + array($object_phid), + array(PhabricatorProjectObjectHasProjectEdgeType::EDGECONST)); + + $handle_list = $handles->newSublist($project_phids); + $handle_list = iterator_to_array($handle_list); + $handle_names = mpull($handle_list, 'getName'); + $handle_names = array_values($handle_names); + + $map[] = array( + 'tagPHIDs' => $project_phids, + 'tags' => $handle_names, + ); + } + + return $map; + } + +} diff --git a/src/infrastructure/export/engine/PhabricatorSpacesExportEngineExtension.php b/src/infrastructure/export/engine/PhabricatorSpacesExportEngineExtension.php new file mode 100644 index 0000000000..3e187bc8cd --- /dev/null +++ b/src/infrastructure/export/engine/PhabricatorSpacesExportEngineExtension.php @@ -0,0 +1,53 @@ +getViewer(); + + if (!PhabricatorSpacesNamespaceQuery::getViewerSpacesExist($viewer)) { + return false; + } + + return ($object instanceof PhabricatorSpacesInterface); + } + + public function newExportFields() { + return array( + id(new PhabricatorPHIDExportField()) + ->setKey('spacePHID') + ->setLabel(pht('Space PHID')), + id(new PhabricatorStringExportField()) + ->setKey('space') + ->setLabel(pht('Space')), + ); + } + + public function newExportData(array $objects) { + $viewer = $this->getViewer(); + + $space_phids = array(); + foreach ($objects as $object) { + $space_phids[] = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID( + $object); + } + $handles = $viewer->loadHandles($space_phids); + + $map = array(); + foreach ($objects as $object) { + $space_phid = PhabricatorSpacesNamespaceQuery::getObjectSpacePHID( + $object); + + $map[] = array( + 'spacePHID' => $space_phid, + 'space' => $handles[$space_phid]->getName(), + ); + } + + return $map; + } + +} diff --git a/src/infrastructure/export/engine/PhabricatorSubscriptionsExportEngineExtension.php b/src/infrastructure/export/engine/PhabricatorSubscriptionsExportEngineExtension.php new file mode 100644 index 0000000000..8aedb38fa8 --- /dev/null +++ b/src/infrastructure/export/engine/PhabricatorSubscriptionsExportEngineExtension.php @@ -0,0 +1,60 @@ +setKey('subscriberPHIDs') + ->setLabel(pht('Subscriber PHIDs')), + id(new PhabricatorStringListExportField()) + ->setKey('subscribers') + ->setLabel(pht('Subscribers')), + ); + } + + public function newExportData(array $objects) { + $viewer = $this->getViewer(); + + $object_phids = mpull($objects, 'getPHID'); + + $projects_query = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs($object_phids) + ->withEdgeTypes( + array( + PhabricatorObjectHasSubscriberEdgeType::EDGECONST, + )); + $projects_query->execute(); + + $handles = $viewer->loadHandles($projects_query->getDestinationPHIDs()); + + $map = array(); + foreach ($objects as $object) { + $object_phid = $object->getPHID(); + + $project_phids = $projects_query->getDestinationPHIDs( + array($object_phid), + array(PhabricatorObjectHasSubscriberEdgeType::EDGECONST)); + + $handle_list = $handles->newSublist($project_phids); + $handle_list = iterator_to_array($handle_list); + $handle_names = mpull($handle_list, 'getName'); + $handle_names = array_values($handle_names); + + $map[] = array( + 'subscriberPHIDs' => $project_phids, + 'subscribers' => $handle_names, + ); + } + + return $map; + } + +} diff --git a/src/infrastructure/export/field/PhabricatorEpochExportField.php b/src/infrastructure/export/field/PhabricatorEpochExportField.php new file mode 100644 index 0000000000..4dffde5aa8 --- /dev/null +++ b/src/infrastructure/export/field/PhabricatorEpochExportField.php @@ -0,0 +1,47 @@ +zone)) { + $this->zone = new DateTimeZone('UTC'); + } + + try { + $date = new DateTime('@'.$value); + } catch (Exception $ex) { + return null; + } + + $date->setTimezone($this->zone); + return $date->format('c'); + } + + public function getNaturalValue($value) { + return (int)$value; + } + + public function getPHPExcelValue($value) { + $epoch = $this->getNaturalValue($value); + + $seconds_per_day = phutil_units('1 day in seconds'); + $offset = ($seconds_per_day * 25569); + + return ($epoch + $offset) / $seconds_per_day; + } + + /** + * @phutil-external-symbol class PHPExcel_Style_NumberFormat + */ + public function formatPHPExcelCell($cell, $style) { + $code = PHPExcel_Style_NumberFormat::FORMAT_DATE_YYYYMMDD2; + + $style + ->getNumberFormat() + ->setFormatCode($code); + } + +} diff --git a/src/infrastructure/export/PhabricatorExportField.php b/src/infrastructure/export/field/PhabricatorExportField.php similarity index 50% rename from src/infrastructure/export/PhabricatorExportField.php rename to src/infrastructure/export/field/PhabricatorExportField.php index 3efb7a8b9a..7ee0918595 100644 --- a/src/infrastructure/export/PhabricatorExportField.php +++ b/src/infrastructure/export/field/PhabricatorExportField.php @@ -25,11 +25,32 @@ abstract class PhabricatorExportField } public function getTextValue($value) { - return (string)$this->getNaturalValue($value); + $natural_value = $this->getNaturalValue($value); + + if ($natural_value === null) { + return null; + } + + return (string)$natural_value; } public function getNaturalValue($value) { return $value; } + public function getPHPExcelValue($value) { + return $this->getTextValue($value); + } + + /** + * @phutil-external-symbol class PHPExcel_Cell_DataType + */ + public function formatPHPExcelCell($cell, $style) { + $cell->setDataType(PHPExcel_Cell_DataType::TYPE_STRING); + } + + public function getCharacterWidth() { + return 24; + } + } diff --git a/src/infrastructure/export/PhabricatorIDExportField.php b/src/infrastructure/export/field/PhabricatorIDExportField.php similarity index 72% rename from src/infrastructure/export/PhabricatorIDExportField.php rename to src/infrastructure/export/field/PhabricatorIDExportField.php index 5b29fdb21d..1ef3d53370 100644 --- a/src/infrastructure/export/PhabricatorIDExportField.php +++ b/src/infrastructure/export/field/PhabricatorIDExportField.php @@ -7,4 +7,8 @@ final class PhabricatorIDExportField return (int)$value; } + public function getCharacterWidth() { + return 12; + } + } diff --git a/src/infrastructure/export/field/PhabricatorIntExportField.php b/src/infrastructure/export/field/PhabricatorIntExportField.php new file mode 100644 index 0000000000..57f7e0ab29 --- /dev/null +++ b/src/infrastructure/export/field/PhabricatorIntExportField.php @@ -0,0 +1,25 @@ +setDataType(PHPExcel_Cell_DataType::TYPE_NUMERIC); + } + + public function getCharacterWidth() { + return 8; + } + +} diff --git a/src/infrastructure/export/field/PhabricatorListExportField.php b/src/infrastructure/export/field/PhabricatorListExportField.php new file mode 100644 index 0000000000..67c3e06ff5 --- /dev/null +++ b/src/infrastructure/export/field/PhabricatorListExportField.php @@ -0,0 +1,10 @@ +addRow($headers); + } + public function addObject($object, array $fields, array $map) { $values = array(); foreach ($fields as $key => $field) { $value = $map[$key]; $value = $field->getTextValue($value); + $values[] = $value; + } + + $this->addRow($values); + } + + private function addRow(array $values) { + $row = array(); + foreach ($values as $value) { + + // Excel is extremely interested in executing arbitrary code it finds in + // untrusted CSV files downloaded from the internet. When a cell looks + // like it might be too tempting for Excel to ignore, mangle the value + // to dissuade remote code execution. See T12800. + + if (preg_match('/^\s*[+=@-]/', $value)) { + $value = '(!) '.$value; + } if (preg_match('/\s|,|\"/', $value)) { $value = str_replace('"', '""', $value); $value = '"'.$value.'"'; } - $values[] = $value; + $row[] = $value; } - $this->rows[] = implode(',', $values); + $this->rows[] = implode(',', $row); } public function newFileData() { diff --git a/src/infrastructure/export/format/PhabricatorExcelExportFormat.php b/src/infrastructure/export/format/PhabricatorExcelExportFormat.php new file mode 100644 index 0000000000..2b0c787884 --- /dev/null +++ b/src/infrastructure/export/format/PhabricatorExcelExportFormat.php @@ -0,0 +1,168 @@ + https://github.com/PHPOffice/PHPExcel + +Briefly: + + - Clone that repository somewhere on the sever + (like `/path/to/example/PHPExcel`). + - Update your PHP `%s` setting (in `php.ini`) to include the PHPExcel + `Classes` directory (like `/path/to/example/PHPExcel/Classes`). +EOHELP + , + 'include_path'); + } + + public function getFileExtension() { + return 'xlsx'; + } + + public function getMIMEContentType() { + return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; + } + + /** + * @phutil-external-symbol class PHPExcel_Cell_DataType + */ + public function addHeaders(array $fields) { + $sheet = $this->getSheet(); + + $header_format = array( + 'font' => array( + 'bold' => true, + ), + ); + + $row = 1; + $col = 0; + foreach ($fields as $field) { + $cell_value = $field->getLabel(); + + $cell_name = $this->getCellName($col, $row); + + $cell = $sheet->setCellValue( + $cell_name, + $cell_value, + $return_cell = true); + + $sheet->getStyle($cell_name)->applyFromArray($header_format); + $cell->setDataType(PHPExcel_Cell_DataType::TYPE_STRING); + + $width = $field->getCharacterWidth(); + if ($width !== null) { + $col_name = $this->getCellName($col); + $sheet->getColumnDimension($col_name) + ->setWidth($width); + } + + $col++; + } + } + + public function addObject($object, array $fields, array $map) { + $sheet = $this->getSheet(); + + $col = 0; + foreach ($fields as $key => $field) { + $cell_value = $map[$key]; + $cell_value = $field->getPHPExcelValue($cell_value); + + $cell_name = $this->getCellName($col, $this->rowCursor); + + $cell = $sheet->setCellValue( + $cell_name, + $cell_value, + $return_cell = true); + + $style = $sheet->getStyle($cell_name); + $field->formatPHPExcelCell($cell, $style); + + $col++; + } + + $this->rowCursor++; + } + + /** + * @phutil-external-symbol class PHPExcel_IOFactory + */ + public function newFileData() { + $workbook = $this->getWorkbook(); + $writer = PHPExcel_IOFactory::createWriter($workbook, 'Excel2007'); + + ob_start(); + $writer->save('php://output'); + $data = ob_get_clean(); + + return $data; + } + + private function getWorkbook() { + if (!$this->workbook) { + $this->workbook = $this->newWorkbook(); + } + return $this->workbook; + } + + /** + * @phutil-external-symbol class PHPExcel + */ + private function newWorkbook() { + include_once 'PHPExcel.php'; + return new PHPExcel(); + } + + private function getSheet() { + if (!$this->sheet) { + $workbook = $this->getWorkbook(); + + $sheet = $workbook->setActiveSheetIndex(0); + $sheet->setTitle($this->getTitle()); + + $this->sheet = $sheet; + + // The row cursor starts on the second row, after the header row. + $this->rowCursor = 2; + } + + return $this->sheet; + } + + private function getCellName($col, $row = null) { + $col_name = chr(ord('A') + $col); + + if ($row === null) { + return $col_name; + } + + return $col_name.$row; + } + +} diff --git a/src/infrastructure/export/PhabricatorExportFormat.php b/src/infrastructure/export/format/PhabricatorExportFormat.php similarity index 77% rename from src/infrastructure/export/PhabricatorExportFormat.php rename to src/infrastructure/export/format/PhabricatorExportFormat.php index a1da4e90d8..4566814b9d 100644 --- a/src/infrastructure/export/PhabricatorExportFormat.php +++ b/src/infrastructure/export/format/PhabricatorExportFormat.php @@ -4,6 +4,7 @@ abstract class PhabricatorExportFormat extends Phobject { private $viewer; + private $title; final public function getExportFormatKey() { return $this->getPhobjectClassConstant('EXPORTKEY'); @@ -18,10 +19,23 @@ abstract class PhabricatorExportFormat return $this->viewer; } + final public function setTitle($title) { + $this->title = $title; + return $this; + } + + final public function getTitle() { + return $this->title; + } + abstract public function getExportFormatName(); abstract public function getMIMEContentType(); abstract public function getFileExtension(); + public function addHeaders(array $fields) { + return; + } + abstract public function addObject($object, array $fields, array $map); abstract public function newFileData(); @@ -36,16 +50,4 @@ abstract class PhabricatorExportFormat ->execute(); } - final public static function getAllEnabledExportFormats() { - $formats = self::getAllExportFormats(); - - foreach ($formats as $key => $format) { - if (!$format->isExportFormatEnabled()) { - unset($formats[$key]); - } - } - - return $formats; - } - } diff --git a/src/infrastructure/export/PhabricatorJSONExportFormat.php b/src/infrastructure/export/format/PhabricatorJSONExportFormat.php similarity index 100% rename from src/infrastructure/export/PhabricatorJSONExportFormat.php rename to src/infrastructure/export/format/PhabricatorJSONExportFormat.php diff --git a/src/infrastructure/export/PhabricatorTextExportFormat.php b/src/infrastructure/export/format/PhabricatorTextExportFormat.php similarity index 67% rename from src/infrastructure/export/PhabricatorTextExportFormat.php rename to src/infrastructure/export/format/PhabricatorTextExportFormat.php index ec308f2eb5..d51e199f91 100644 --- a/src/infrastructure/export/PhabricatorTextExportFormat.php +++ b/src/infrastructure/export/format/PhabricatorTextExportFormat.php @@ -23,17 +23,29 @@ final class PhabricatorTextExportFormat return 'text/plain'; } + public function addHeaders(array $fields) { + $headers = mpull($fields, 'getLabel'); + $this->addRow($headers); + } + public function addObject($object, array $fields, array $map) { $values = array(); foreach ($fields as $key => $field) { $value = $map[$key]; $value = $field->getTextValue($value); - $value = addcslashes($value, "\0..\37\\\177..\377"); - $values[] = $value; } - $this->rows[] = implode("\t", $values); + $this->addRow($values); + } + + private function addRow(array $values) { + $row = array(); + foreach ($values as $value) { + $row[] = addcslashes($value, "\0..\37\\\177..\377"); + } + + $this->rows[] = implode("\t", $row); } public function newFileData() { diff --git a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php index 40cedacf06..eb74f8debf 100644 --- a/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php +++ b/src/infrastructure/internationalization/translation/PhabricatorUSEnglishTranslation.php @@ -676,73 +676,73 @@ final class PhabricatorUSEnglishTranslation '%s edited commit(s), added %s: %s; removed %s: %s.' => '%s edited commits, added %3$s; removed %5$s.', - '%s added %s reverted commit(s): %s.' => array( + '%s added %s reverted change(s): %s.' => array( array( - '%s added a reverted commit: %3$s.', - '%s added reverted commits: %3$s.', + '%s added a reverted change: %3$s.', + '%s added reverted changes: %3$s.', ), ), - '%s removed %s reverted commit(s): %s.' => array( + '%s removed %s reverted change(s): %s.' => array( array( - '%s removed a reverted commit: %3$s.', - '%s removed reverted commits: %3$s.', + '%s removed a reverted change: %3$s.', + '%s removed reverted changes: %3$s.', ), ), - '%s edited reverted commit(s), added %s: %s; removed %s: %s.' => - '%s edited reverted commits, added %3$s; removed %5$s.', + '%s edited reverted change(s), added %s: %s; removed %s: %s.' => + '%s edited reverted changes, added %3$s; removed %5$s.', - '%s added %s reverted commit(s) for %s: %s.' => array( + '%s added %s reverted change(s) for %s: %s.' => array( array( - '%s added a reverted commit for %3$s: %4$s.', - '%s added reverted commits for %3$s: %4$s.', + '%s added a reverted change for %3$s: %4$s.', + '%s added reverted changes for %3$s: %4$s.', ), ), - '%s removed %s reverted commit(s) for %s: %s.' => array( + '%s removed %s reverted change(s) for %s: %s.' => array( array( - '%s removed a reverted commit for %3$s: %4$s.', - '%s removed reverted commits for %3$s: %4$s.', + '%s removed a reverted change for %3$s: %4$s.', + '%s removed reverted changes for %3$s: %4$s.', ), ), - '%s edited reverted commit(s) for %s, added %s: %s; removed %s: %s.' => - '%s edited reverted commits for %2$s, added %4$s; removed %6$s.', + '%s edited reverted change(s) for %s, added %s: %s; removed %s: %s.' => + '%s edited reverted changes for %2$s, added %4$s; removed %6$s.', - '%s added %s reverting commit(s): %s.' => array( + '%s added %s reverting change(s): %s.' => array( array( - '%s added a reverting commit: %3$s.', - '%s added reverting commits: %3$s.', + '%s added a reverting change: %3$s.', + '%s added reverting changes: %3$s.', ), ), - '%s removed %s reverting commit(s): %s.' => array( + '%s removed %s reverting change(s): %s.' => array( array( - '%s removed a reverting commit: %3$s.', - '%s removed reverting commits: %3$s.', + '%s removed a reverting change: %3$s.', + '%s removed reverting changes: %3$s.', ), ), - '%s edited reverting commit(s), added %s: %s; removed %s: %s.' => - '%s edited reverting commits, added %3$s; removed %5$s.', + '%s edited reverting change(s), added %s: %s; removed %s: %s.' => + '%s edited reverting changes, added %3$s; removed %5$s.', - '%s added %s reverting commit(s) for %s: %s.' => array( + '%s added %s reverting change(s) for %s: %s.' => array( array( - '%s added a reverting commit for %3$s: %4$s.', - '%s added reverting commits for %3$s: %4$s.', + '%s added a reverting change for %3$s: %4$s.', + '%s added reverting changes for %3$s: %4$s.', ), ), - '%s removed %s reverting commit(s) for %s: %s.' => array( + '%s removed %s reverting change(s) for %s: %s.' => array( array( - '%s removed a reverting commit for %3$s: %4$s.', - '%s removed reverting commits for %3$s: %4$s.', + '%s removed a reverting change for %3$s: %4$s.', + '%s removed reverting changes for %3$s: %4$s.', ), ), - '%s edited reverting commit(s) for %s, added %s: %s; removed %s: %s.' => - '%s edited reverting commits for %s, added %4$s; removed %6$s.', + '%s edited reverting change(s) for %s, added %s: %s; removed %s: %s.' => + '%s edited reverting changes for %s, added %4$s; removed %6$s.', '%s changed project member(s), added %d: %s; removed %d: %s.' => '%s changed project members, added %3$s; removed %5$s.', diff --git a/src/view/phui/PHUITimelineEventView.php b/src/view/phui/PHUITimelineEventView.php index 161dd0c944..e64b9b06b2 100644 --- a/src/view/phui/PHUITimelineEventView.php +++ b/src/view/phui/PHUITimelineEventView.php @@ -301,18 +301,14 @@ final class PHUITimelineEventView extends AphrontView { $menu = null; $items = array(); - $has_menu = false; if (!$this->getIsPreview() && !$this->getHideCommentOptions()) { foreach ($this->getEventGroup() as $event) { $items[] = $event->getMenuItems($this->anchor); - if ($event->hasChildren()) { - $has_menu = true; - } } $items = array_mergev($items); } - if ($items || $has_menu) { + if ($items) { $icon = id(new PHUIIconView()) ->setIcon('fa-caret-down'); $aural = javelin_tag( @@ -351,6 +347,8 @@ final class PHUITimelineEventView extends AphrontView { )); $has_menu = true; + } else { + $has_menu = false; } // Render "extra" information (timestamp, etc). diff --git a/webroot/rsrc/css/phui/phui-timeline-view.css b/webroot/rsrc/css/phui/phui-timeline-view.css index b0ae9c0044..6fae6802be 100644 --- a/webroot/rsrc/css/phui/phui-timeline-view.css +++ b/webroot/rsrc/css/phui/phui-timeline-view.css @@ -390,10 +390,6 @@ outline: none; } -.phui-timeline-menu .phui-icon-view { - color: {$lightgreytext}; -} - a.phui-timeline-menu .phui-icon-view { color: {$bluetext}; }