diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index a08a4d5f4b..cd07a4e727 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1,2361 +1,2375 @@ 2, 'class' => array( 'Aphront304Response' => 'aphront/response/Aphront304Response.php', 'Aphront400Response' => 'aphront/response/Aphront400Response.php', 'Aphront403Response' => 'aphront/response/Aphront403Response.php', 'Aphront404Response' => 'aphront/response/Aphront404Response.php', 'AphrontAjaxResponse' => 'aphront/response/AphrontAjaxResponse.php', 'AphrontApplicationConfiguration' => 'aphront/configuration/AphrontApplicationConfiguration.php', 'AphrontAttachedFileView' => 'view/control/AphrontAttachedFileView.php', 'AphrontCSRFException' => 'aphront/exception/AphrontCSRFException.php', 'AphrontCalendarEventView' => 'applications/calendar/view/AphrontCalendarEventView.php', 'AphrontCalendarMonthView' => 'applications/calendar/view/AphrontCalendarMonthView.php', 'AphrontContextBarView' => 'view/layout/AphrontContextBarView.php', 'AphrontController' => 'aphront/AphrontController.php', 'AphrontCrumbsView' => 'view/layout/AphrontCrumbsView.php', 'AphrontCursorPagerView' => 'view/control/AphrontCursorPagerView.php', 'AphrontDefaultApplicationConfiguration' => 'aphront/configuration/AphrontDefaultApplicationConfiguration.php', 'AphrontDialogResponse' => 'aphront/response/AphrontDialogResponse.php', 'AphrontDialogView' => 'view/AphrontDialogView.php', 'AphrontErrorView' => 'view/form/AphrontErrorView.php', 'AphrontException' => 'aphront/exception/AphrontException.php', 'AphrontFilePreviewView' => 'view/layout/AphrontFilePreviewView.php', 'AphrontFileResponse' => 'aphront/response/AphrontFileResponse.php', 'AphrontFormCheckboxControl' => 'view/form/control/AphrontFormCheckboxControl.php', 'AphrontFormControl' => 'view/form/control/AphrontFormControl.php', 'AphrontFormDateControl' => 'view/form/control/AphrontFormDateControl.php', 'AphrontFormDividerControl' => 'view/form/control/AphrontFormDividerControl.php', 'AphrontFormDragAndDropUploadControl' => 'view/form/control/AphrontFormDragAndDropUploadControl.php', 'AphrontFormFileControl' => 'view/form/control/AphrontFormFileControl.php', 'AphrontFormImageControl' => 'view/form/control/AphrontFormImageControl.php', 'AphrontFormInsetView' => 'view/form/AphrontFormInsetView.php', 'AphrontFormLayoutView' => 'view/form/AphrontFormLayoutView.php', 'AphrontFormMarkupControl' => 'view/form/control/AphrontFormMarkupControl.php', 'AphrontFormPasswordControl' => 'view/form/control/AphrontFormPasswordControl.php', 'AphrontFormPolicyControl' => 'view/form/control/AphrontFormPolicyControl.php', 'AphrontFormRadioButtonControl' => 'view/form/control/AphrontFormRadioButtonControl.php', 'AphrontFormRecaptchaControl' => 'view/form/control/AphrontFormRecaptchaControl.php', 'AphrontFormSelectControl' => 'view/form/control/AphrontFormSelectControl.php', 'AphrontFormStaticControl' => 'view/form/control/AphrontFormStaticControl.php', 'AphrontFormSubmitControl' => 'view/form/control/AphrontFormSubmitControl.php', 'AphrontFormTextAreaControl' => 'view/form/control/AphrontFormTextAreaControl.php', 'AphrontFormTextControl' => 'view/form/control/AphrontFormTextControl.php', 'AphrontFormToggleButtonsControl' => 'view/form/control/AphrontFormToggleButtonsControl.php', 'AphrontFormTokenizerControl' => 'view/form/control/AphrontFormTokenizerControl.php', 'AphrontFormView' => 'view/form/AphrontFormView.php', 'AphrontHTTPSink' => 'aphront/sink/AphrontHTTPSink.php', 'AphrontHTTPSinkTestCase' => 'aphront/sink/__tests__/AphrontHTTPSinkTestCase.php', 'AphrontHeadsupActionListView' => 'view/layout/headsup/AphrontHeadsupActionListView.php', 'AphrontHeadsupActionView' => 'view/layout/headsup/AphrontHeadsupActionView.php', 'AphrontHeadsupView' => 'view/layout/headsup/AphrontHeadsupView.php', 'AphrontIsolatedDatabaseConnectionTestCase' => 'infrastructure/storage/__tests__/AphrontIsolatedDatabaseConnectionTestCase.php', 'AphrontIsolatedHTTPSink' => 'aphront/sink/AphrontIsolatedHTTPSink.php', 'AphrontJSONResponse' => 'aphront/response/AphrontJSONResponse.php', 'AphrontJavelinView' => 'view/AphrontJavelinView.php', 'AphrontKeyboardShortcutsAvailableView' => 'view/widget/AphrontKeyboardShortcutsAvailableView.php', 'AphrontListFilterView' => 'view/layout/AphrontListFilterView.php', 'AphrontMiniPanelView' => 'view/layout/AphrontMiniPanelView.php', 'AphrontMoreView' => 'view/layout/AphrontMoreView.php', 'AphrontMySQLDatabaseConnectionTestCase' => 'infrastructure/storage/__tests__/AphrontMySQLDatabaseConnectionTestCase.php', 'AphrontNullView' => 'view/AphrontNullView.php', 'AphrontPHPHTTPSink' => 'aphront/sink/AphrontPHPHTTPSink.php', 'AphrontPageView' => 'view/page/AphrontPageView.php', 'AphrontPagerView' => 'view/control/AphrontPagerView.php', 'AphrontPanelView' => 'view/layout/AphrontPanelView.php', 'AphrontPlainTextResponse' => 'aphront/response/AphrontPlainTextResponse.php', 'AphrontProxyResponse' => 'aphront/response/AphrontProxyResponse.php', 'AphrontRedirectException' => 'aphront/exception/AphrontRedirectException.php', 'AphrontRedirectResponse' => 'aphront/response/AphrontRedirectResponse.php', 'AphrontReloadResponse' => 'aphront/response/AphrontReloadResponse.php', 'AphrontRequest' => 'aphront/AphrontRequest.php', 'AphrontRequestFailureView' => 'view/page/AphrontRequestFailureView.php', 'AphrontRequestTestCase' => 'aphront/__tests__/AphrontRequestTestCase.php', 'AphrontResponse' => 'aphront/response/AphrontResponse.php', 'AphrontSideNavFilterView' => 'view/layout/AphrontSideNavFilterView.php', 'AphrontSideNavView' => 'view/layout/AphrontSideNavView.php', 'AphrontTableView' => 'view/control/AphrontTableView.php', 'AphrontTokenizerTemplateView' => 'view/control/AphrontTokenizerTemplateView.php', 'AphrontTypeaheadTemplateView' => 'view/control/AphrontTypeaheadTemplateView.php', 'AphrontURIMapper' => 'aphront/AphrontURIMapper.php', 'AphrontUsageException' => 'aphront/exception/AphrontUsageException.php', 'AphrontView' => 'view/AphrontView.php', 'AphrontWebpageResponse' => 'aphront/response/AphrontWebpageResponse.php', 'CelerityAPI' => 'infrastructure/celerity/CelerityAPI.php', 'CelerityResourceController' => 'infrastructure/celerity/CelerityResourceController.php', 'CelerityResourceGraph' => 'infrastructure/celerity/CelerityResourceGraph.php', 'CelerityResourceMap' => 'infrastructure/celerity/CelerityResourceMap.php', 'CelerityResourceTransformer' => 'infrastructure/celerity/CelerityResourceTransformer.php', 'CelerityResourceTransformerTestCase' => 'infrastructure/celerity/__tests__/CelerityResourceTransformerTestCase.php', 'CelerityStaticResourceResponse' => 'infrastructure/celerity/CelerityStaticResourceResponse.php', 'ConduitAPIMethod' => 'applications/conduit/method/ConduitAPIMethod.php', 'ConduitAPIRequest' => 'applications/conduit/protocol/ConduitAPIRequest.php', 'ConduitAPIResponse' => 'applications/conduit/protocol/ConduitAPIResponse.php', 'ConduitAPI_arcanist_Method' => 'applications/conduit/method/arcanist/ConduitAPI_arcanist_Method.php', 'ConduitAPI_arcanist_projectinfo_Method' => 'applications/conduit/method/arcanist/ConduitAPI_arcanist_projectinfo_Method.php', 'ConduitAPI_audit_Method' => 'applications/conduit/method/audit/ConduitAPI_audit_Method.php', 'ConduitAPI_audit_query_Method' => 'applications/conduit/method/audit/ConduitAPI_audit_query_Method.php', 'ConduitAPI_chatlog_Method' => 'applications/conduit/method/chatlog/ConduitAPI_chatlog_Method.php', 'ConduitAPI_chatlog_query_Method' => 'applications/conduit/method/chatlog/ConduitAPI_chatlog_query_Method.php', 'ConduitAPI_chatlog_record_Method' => 'applications/conduit/method/chatlog/ConduitAPI_chatlog_record_Method.php', 'ConduitAPI_conduit_connect_Method' => 'applications/conduit/method/conduit/ConduitAPI_conduit_connect_Method.php', 'ConduitAPI_conduit_getcertificate_Method' => 'applications/conduit/method/conduit/ConduitAPI_conduit_getcertificate_Method.php', 'ConduitAPI_conduit_ping_Method' => 'applications/conduit/method/conduit/ConduitAPI_conduit_ping_Method.php', 'ConduitAPI_daemon_launched_Method' => 'applications/conduit/method/daemon/ConduitAPI_daemon_launched_Method.php', 'ConduitAPI_daemon_log_Method' => 'applications/conduit/method/daemon/ConduitAPI_daemon_log_Method.php', 'ConduitAPI_daemon_setstatus_Method' => 'applications/conduit/method/daemon/ConduitAPI_daemon_setstatus_Method.php', 'ConduitAPI_differential_Method' => 'applications/conduit/method/differential/ConduitAPI_differential_Method.php', 'ConduitAPI_differential_close_Method' => 'applications/conduit/method/differential/ConduitAPI_differential_close_Method.php', 'ConduitAPI_differential_createcomment_Method' => 'applications/conduit/method/differential/ConduitAPI_differential_createcomment_Method.php', 'ConduitAPI_differential_creatediff_Method' => 'applications/conduit/method/differential/ConduitAPI_differential_creatediff_Method.php', 'ConduitAPI_differential_createinline_Method' => 'applications/conduit/method/differential/ConduitAPI_differential_createinline_Method.php', 'ConduitAPI_differential_createrawdiff_Method' => 'applications/conduit/method/differential/ConduitAPI_differential_createrawdiff_Method.php', 'ConduitAPI_differential_createrevision_Method' => 'applications/conduit/method/differential/ConduitAPI_differential_createrevision_Method.php', 'ConduitAPI_differential_find_Method' => 'applications/conduit/method/differential/ConduitAPI_differential_find_Method.php', 'ConduitAPI_differential_finishpostponedlinters_Method' => 'applications/conduit/method/differential/ConduitAPI_differential_finishpostponedlinters_Method.php', 'ConduitAPI_differential_getalldiffs_Method' => 'applications/conduit/method/differential/ConduitAPI_differential_getalldiffs_Method.php', 'ConduitAPI_differential_getcommitmessage_Method' => 'applications/conduit/method/differential/ConduitAPI_differential_getcommitmessage_Method.php', 'ConduitAPI_differential_getcommitpaths_Method' => 'applications/conduit/method/differential/ConduitAPI_differential_getcommitpaths_Method.php', 'ConduitAPI_differential_getdiff_Method' => 'applications/conduit/method/differential/ConduitAPI_differential_getdiff_Method.php', 'ConduitAPI_differential_getrevision_Method' => 'applications/conduit/method/differential/ConduitAPI_differential_getrevision_Method.php', 'ConduitAPI_differential_getrevisioncomments_Method' => 'applications/conduit/method/differential/ConduitAPI_differential_getrevisioncomments_Method.php', 'ConduitAPI_differential_getrevisionfeedback_Method' => 'applications/conduit/method/differential/ConduitAPI_differential_getrevisionfeedback_Method.php', 'ConduitAPI_differential_markcommitted_Method' => 'applications/conduit/method/differential/ConduitAPI_differential_markcommitted_Method.php', 'ConduitAPI_differential_parsecommitmessage_Method' => 'applications/conduit/method/differential/ConduitAPI_differential_parsecommitmessage_Method.php', 'ConduitAPI_differential_query_Method' => 'applications/conduit/method/differential/ConduitAPI_differential_query_Method.php', 'ConduitAPI_differential_setdiffproperty_Method' => 'applications/conduit/method/differential/ConduitAPI_differential_setdiffproperty_Method.php', 'ConduitAPI_differential_updaterevision_Method' => 'applications/conduit/method/differential/ConduitAPI_differential_updaterevision_Method.php', 'ConduitAPI_differential_updatetaskrevisionassoc_Method' => 'applications/conduit/method/differential/ConduitAPI_differential_updatetaskrevisionassoc_Method.php', 'ConduitAPI_differential_updateunitresults_Method' => 'applications/conduit/method/differential/ConduitAPI_differential_updateunitresults_Method.php', 'ConduitAPI_diffusion_findsymbols_Method' => 'applications/conduit/method/diffusion/ConduitAPI_diffusion_findsymbols_Method.php', 'ConduitAPI_diffusion_getcommits_Method' => 'applications/conduit/method/diffusion/ConduitAPI_diffusion_getcommits_Method.php', 'ConduitAPI_diffusion_getrecentcommitsbypath_Method' => 'applications/conduit/method/diffusion/ConduitAPI_diffusion_getrecentcommitsbypath_Method.php', 'ConduitAPI_feed_publish_Method' => 'applications/conduit/method/feed/ConduitAPI_feed_publish_Method.php', 'ConduitAPI_feed_query_Method' => 'applications/conduit/method/feed/ConduitAPI_feed_query_Method.php', 'ConduitAPI_file_download_Method' => 'applications/conduit/method/file/ConduitAPI_file_download_Method.php', 'ConduitAPI_file_info_Method' => 'applications/conduit/method/file/ConduitAPI_file_info_Method.php', 'ConduitAPI_file_upload_Method' => 'applications/conduit/method/file/ConduitAPI_file_upload_Method.php', 'ConduitAPI_flag_Method' => 'applications/conduit/method/flag/ConduitAPI_flag_Method.php', 'ConduitAPI_flag_delete_Method' => 'applications/conduit/method/flag/ConduitAPI_flag_delete_Method.php', 'ConduitAPI_flag_edit_Method' => 'applications/conduit/method/flag/ConduitAPI_flag_edit_Method.php', 'ConduitAPI_flag_query_Method' => 'applications/conduit/method/flag/ConduitAPI_flag_query_Method.php', 'ConduitAPI_macro_Method' => 'applications/conduit/method/macro/ConduitAPI_macro_Method.php', 'ConduitAPI_macro_query_Method' => 'applications/conduit/method/macro/ConduitAPI_macro_query_Method.php', 'ConduitAPI_maniphest_Method' => 'applications/conduit/method/maniphest/ConduitAPI_maniphest_Method.php', 'ConduitAPI_maniphest_createtask_Method' => 'applications/conduit/method/maniphest/ConduitAPI_maniphest_createtask_Method.php', 'ConduitAPI_maniphest_find_Method' => 'applications/conduit/method/maniphest/ConduitAPI_maniphest_find_Method.php', 'ConduitAPI_maniphest_gettasktransactions_Method' => 'applications/conduit/method/maniphest/ConduitAPI_maniphest_gettasktransactions_Method.php', 'ConduitAPI_maniphest_info_Method' => 'applications/conduit/method/maniphest/ConduitAPI_maniphest_info_Method.php', 'ConduitAPI_maniphest_query_Method' => 'applications/conduit/method/maniphest/ConduitAPI_maniphest_query_Method.php', 'ConduitAPI_maniphest_update_Method' => 'applications/conduit/method/maniphest/ConduitAPI_maniphest_update_Method.php', 'ConduitAPI_owners_query_Method' => 'applications/conduit/method/owners/ConduitAPI_owners_query_Method.php', 'ConduitAPI_paste_Method' => 'applications/paste/conduit/ConduitAPI_paste_Method.php', 'ConduitAPI_paste_create_Method' => 'applications/paste/conduit/ConduitAPI_paste_create_Method.php', 'ConduitAPI_paste_info_Method' => 'applications/paste/conduit/ConduitAPI_paste_info_Method.php', 'ConduitAPI_paste_query_Method' => 'applications/paste/conduit/ConduitAPI_paste_query_Method.php', 'ConduitAPI_phid_Method' => 'applications/conduit/method/phid/ConduitAPI_phid_Method.php', 'ConduitAPI_phid_info_Method' => 'applications/conduit/method/phid/ConduitAPI_phid_info_Method.php', 'ConduitAPI_phid_lookup_Method' => 'applications/conduit/method/phid/ConduitAPI_phid_lookup_Method.php', 'ConduitAPI_phid_query_Method' => 'applications/conduit/method/phid/ConduitAPI_phid_query_Method.php', 'ConduitAPI_phpast_getast_Method' => 'applications/conduit/method/phpast/ConduitAPI_phpast_getast_Method.php', 'ConduitAPI_phpast_version_Method' => 'applications/conduit/method/phpast/ConduitAPI_phpast_version_Method.php', 'ConduitAPI_phriction_Method' => 'applications/conduit/method/phriction/ConduitAPI_phriction_Method.php', 'ConduitAPI_phriction_edit_Method' => 'applications/conduit/method/phriction/ConduitAPI_phriction_edit_Method.php', 'ConduitAPI_phriction_history_Method' => 'applications/conduit/method/phriction/ConduitAPI_phriction_history_Method.php', 'ConduitAPI_phriction_info_Method' => 'applications/conduit/method/phriction/ConduitAPI_phriction_info_Method.php', 'ConduitAPI_project_Method' => 'applications/conduit/method/project/ConduitAPI_project_Method.php', 'ConduitAPI_project_query_Method' => 'applications/conduit/method/project/ConduitAPI_project_query_Method.php', 'ConduitAPI_remarkup_process_Method' => 'applications/conduit/method/remarkup/ConduitAPI_remarkup_process_Method.php', 'ConduitAPI_repository_Method' => 'applications/conduit/method/repository/ConduitAPI_repository_Method.php', 'ConduitAPI_repository_create_Method' => 'applications/conduit/method/repository/ConduitAPI_repository_create_Method.php', 'ConduitAPI_repository_query_Method' => 'applications/conduit/method/repository/ConduitAPI_repository_query_Method.php', 'ConduitAPI_slowvote_info_Method' => 'applications/conduit/method/slowvote/ConduitAPI_slowvote_info_Method.php', 'ConduitAPI_user_Method' => 'applications/conduit/method/user/ConduitAPI_user_Method.php', 'ConduitAPI_user_addstatus_Method' => 'applications/conduit/method/user/ConduitAPI_user_addstatus_Method.php', 'ConduitAPI_user_disable_Method' => 'applications/conduit/method/user/ConduitAPI_user_disable_Method.php', 'ConduitAPI_user_enable_Method' => 'applications/conduit/method/user/ConduitAPI_user_enable_Method.php', 'ConduitAPI_user_find_Method' => 'applications/conduit/method/user/ConduitAPI_user_find_Method.php', 'ConduitAPI_user_info_Method' => 'applications/conduit/method/user/ConduitAPI_user_info_Method.php', 'ConduitAPI_user_query_Method' => 'applications/conduit/method/user/ConduitAPI_user_query_Method.php', 'ConduitAPI_user_removestatus_Method' => 'applications/conduit/method/user/ConduitAPI_user_removestatus_Method.php', 'ConduitAPI_user_whoami_Method' => 'applications/conduit/method/user/ConduitAPI_user_whoami_Method.php', 'ConduitCall' => 'applications/conduit/call/ConduitCall.php', 'ConduitCallTestCase' => 'applications/conduit/call/__tests__/ConduitCallTestCase.php', 'ConduitException' => 'applications/conduit/protocol/ConduitException.php', 'DarkConsoleConfigPlugin' => 'aphront/console/plugin/DarkConsoleConfigPlugin.php', 'DarkConsoleController' => 'aphront/console/DarkConsoleController.php', 'DarkConsoleCore' => 'aphront/console/DarkConsoleCore.php', 'DarkConsoleErrorLogPlugin' => 'aphront/console/plugin/DarkConsoleErrorLogPlugin.php', 'DarkConsoleErrorLogPluginAPI' => 'aphront/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php', 'DarkConsoleEventPlugin' => 'aphront/console/plugin/DarkConsoleEventPlugin.php', 'DarkConsoleEventPluginAPI' => 'aphront/console/plugin/event/DarkConsoleEventPluginAPI.php', 'DarkConsolePlugin' => 'aphront/console/plugin/DarkConsolePlugin.php', 'DarkConsoleRequestPlugin' => 'aphront/console/plugin/DarkConsoleRequestPlugin.php', 'DarkConsoleServicesPlugin' => 'aphront/console/plugin/DarkConsoleServicesPlugin.php', 'DarkConsoleXHProfPlugin' => 'aphront/console/plugin/DarkConsoleXHProfPlugin.php', 'DarkConsoleXHProfPluginAPI' => 'aphront/console/plugin/xhprof/DarkConsoleXHProfPluginAPI.php', 'DatabaseConfigurationProvider' => 'infrastructure/storage/configuration/DatabaseConfigurationProvider.php', 'DefaultDatabaseConfigurationProvider' => 'infrastructure/storage/configuration/DefaultDatabaseConfigurationProvider.php', 'DifferentialAction' => 'applications/differential/constants/DifferentialAction.php', 'DifferentialActionHasNoEffectException' => 'applications/differential/exception/DifferentialActionHasNoEffectException.php', 'DifferentialAddCommentView' => 'applications/differential/view/DifferentialAddCommentView.php', 'DifferentialAffectedPath' => 'applications/differential/storage/DifferentialAffectedPath.php', 'DifferentialApplyPatchFieldSpecification' => 'applications/differential/field/specification/DifferentialApplyPatchFieldSpecification.php', 'DifferentialArcanistProjectFieldSpecification' => 'applications/differential/field/specification/DifferentialArcanistProjectFieldSpecification.php', 'DifferentialAuditorsFieldSpecification' => 'applications/differential/field/specification/DifferentialAuditorsFieldSpecification.php', 'DifferentialAuthorFieldSpecification' => 'applications/differential/field/specification/DifferentialAuthorFieldSpecification.php', 'DifferentialAuxiliaryField' => 'applications/differential/storage/DifferentialAuxiliaryField.php', 'DifferentialBlameRevisionFieldSpecification' => 'applications/differential/field/specification/DifferentialBlameRevisionFieldSpecification.php', 'DifferentialBranchFieldSpecification' => 'applications/differential/field/specification/DifferentialBranchFieldSpecification.php', 'DifferentialCCWelcomeMail' => 'applications/differential/mail/DifferentialCCWelcomeMail.php', 'DifferentialCCsFieldSpecification' => 'applications/differential/field/specification/DifferentialCCsFieldSpecification.php', 'DifferentialChangeSetTestCase' => 'applications/differential/storage/__tests__/DifferentialChangesetTestCase.php', 'DifferentialChangeType' => 'applications/differential/constants/DifferentialChangeType.php', 'DifferentialChangeset' => 'applications/differential/storage/DifferentialChangeset.php', 'DifferentialChangesetDetailView' => 'applications/differential/view/DifferentialChangesetDetailView.php', 'DifferentialChangesetListView' => 'applications/differential/view/DifferentialChangesetListView.php', 'DifferentialChangesetParser' => 'applications/differential/parser/DifferentialChangesetParser.php', 'DifferentialChangesetParserTestCase' => 'applications/differential/parser/__tests__/DifferentialChangesetParserTestCase.php', 'DifferentialChangesetViewController' => 'applications/differential/controller/DifferentialChangesetViewController.php', 'DifferentialComment' => 'applications/differential/storage/DifferentialComment.php', 'DifferentialCommentEditor' => 'applications/differential/editor/DifferentialCommentEditor.php', 'DifferentialCommentMail' => 'applications/differential/mail/DifferentialCommentMail.php', 'DifferentialCommentPreviewController' => 'applications/differential/controller/DifferentialCommentPreviewController.php', 'DifferentialCommentSaveController' => 'applications/differential/controller/DifferentialCommentSaveController.php', 'DifferentialCommitsFieldSpecification' => 'applications/differential/field/specification/DifferentialCommitsFieldSpecification.php', 'DifferentialController' => 'applications/differential/controller/DifferentialController.php', 'DifferentialDAO' => 'applications/differential/storage/DifferentialDAO.php', 'DifferentialDateCreatedFieldSpecification' => 'applications/differential/field/specification/DifferentialDateCreatedFieldSpecification.php', 'DifferentialDateModifiedFieldSpecification' => 'applications/differential/field/specification/DifferentialDateModifiedFieldSpecification.php', 'DifferentialDefaultFieldSelector' => 'applications/differential/field/selector/DifferentialDefaultFieldSelector.php', 'DifferentialDependenciesFieldSpecification' => 'applications/differential/field/specification/DifferentialDependenciesFieldSpecification.php', 'DifferentialDependsOnFieldSpecification' => 'applications/differential/field/specification/DifferentialDependsOnFieldSpecification.php', 'DifferentialDiff' => 'applications/differential/storage/DifferentialDiff.php', 'DifferentialDiffContentMail' => 'applications/differential/mail/DifferentialDiffContentMail.php', 'DifferentialDiffCreateController' => 'applications/differential/controller/DifferentialDiffCreateController.php', 'DifferentialDiffProperty' => 'applications/differential/storage/DifferentialDiffProperty.php', 'DifferentialDiffTableOfContentsView' => 'applications/differential/view/DifferentialDiffTableOfContentsView.php', 'DifferentialDiffViewController' => 'applications/differential/controller/DifferentialDiffViewController.php', 'DifferentialException' => 'applications/differential/exception/DifferentialException.php', 'DifferentialExceptionMail' => 'applications/differential/mail/DifferentialExceptionMail.php', 'DifferentialExportPatchFieldSpecification' => 'applications/differential/field/specification/DifferentialExportPatchFieldSpecification.php', 'DifferentialFieldDataNotAvailableException' => 'applications/differential/field/exception/DifferentialFieldDataNotAvailableException.php', 'DifferentialFieldParseException' => 'applications/differential/field/exception/DifferentialFieldParseException.php', 'DifferentialFieldSelector' => 'applications/differential/field/selector/DifferentialFieldSelector.php', 'DifferentialFieldSpecification' => 'applications/differential/field/specification/DifferentialFieldSpecification.php', 'DifferentialFieldSpecificationIncompleteException' => 'applications/differential/field/exception/DifferentialFieldSpecificationIncompleteException.php', 'DifferentialFieldValidationException' => 'applications/differential/field/exception/DifferentialFieldValidationException.php', 'DifferentialFreeformFieldSpecification' => 'applications/differential/field/specification/DifferentialFreeformFieldSpecification.php', 'DifferentialGitSVNIDFieldSpecification' => 'applications/differential/field/specification/DifferentialGitSVNIDFieldSpecification.php', 'DifferentialHostFieldSpecification' => 'applications/differential/field/specification/DifferentialHostFieldSpecification.php', 'DifferentialHunk' => 'applications/differential/storage/DifferentialHunk.php', 'DifferentialHunkTestCase' => 'applications/differential/storage/__tests__/DifferentialHunkTestCase.php', 'DifferentialInlineComment' => 'applications/differential/storage/DifferentialInlineComment.php', 'DifferentialInlineCommentEditController' => 'applications/differential/controller/DifferentialInlineCommentEditController.php', 'DifferentialInlineCommentEditView' => 'applications/differential/view/DifferentialInlineCommentEditView.php', 'DifferentialInlineCommentPreviewController' => 'applications/differential/controller/DifferentialInlineCommentPreviewController.php', 'DifferentialInlineCommentView' => 'applications/differential/view/DifferentialInlineCommentView.php', 'DifferentialLinesFieldSpecification' => 'applications/differential/field/specification/DifferentialLinesFieldSpecification.php', 'DifferentialLintFieldSpecification' => 'applications/differential/field/specification/DifferentialLintFieldSpecification.php', 'DifferentialLintStatus' => 'applications/differential/constants/DifferentialLintStatus.php', 'DifferentialLocalCommitsView' => 'applications/differential/view/DifferentialLocalCommitsView.php', 'DifferentialMail' => 'applications/differential/mail/DifferentialMail.php', 'DifferentialMailPhase' => 'applications/differential/constants/DifferentialMailPhase.php', 'DifferentialManiphestTasksFieldSpecification' => 'applications/differential/field/specification/DifferentialManiphestTasksFieldSpecification.php', 'DifferentialNewDiffMail' => 'applications/differential/mail/DifferentialNewDiffMail.php', 'DifferentialPathFieldSpecification' => 'applications/differential/field/specification/DifferentialPathFieldSpecification.php', 'DifferentialPrimaryPaneView' => 'applications/differential/view/DifferentialPrimaryPaneView.php', 'DifferentialReplyHandler' => 'applications/differential/DifferentialReplyHandler.php', 'DifferentialResultsTableView' => 'applications/differential/view/DifferentialResultsTableView.php', 'DifferentialRevertPlanFieldSpecification' => 'applications/differential/field/specification/DifferentialRevertPlanFieldSpecification.php', 'DifferentialReviewRequestMail' => 'applications/differential/mail/DifferentialReviewRequestMail.php', 'DifferentialReviewedByFieldSpecification' => 'applications/differential/field/specification/DifferentialReviewedByFieldSpecification.php', 'DifferentialReviewerStats' => 'applications/differential/stats/DifferentialReviewerStats.php', 'DifferentialReviewerStatsTestCase' => 'applications/differential/stats/__tests__/DifferentialReviewerStatsTestCase.php', 'DifferentialReviewersFieldSpecification' => 'applications/differential/field/specification/DifferentialReviewersFieldSpecification.php', 'DifferentialRevision' => 'applications/differential/storage/DifferentialRevision.php', 'DifferentialRevisionCommentListView' => 'applications/differential/view/DifferentialRevisionCommentListView.php', 'DifferentialRevisionCommentView' => 'applications/differential/view/DifferentialRevisionCommentView.php', 'DifferentialRevisionControlSystem' => 'applications/differential/constants/DifferentialRevisionControlSystem.php', 'DifferentialRevisionDetailRenderer' => 'applications/differential/controller/DifferentialRevisionDetailRenderer.php', 'DifferentialRevisionDetailView' => 'applications/differential/view/DifferentialRevisionDetailView.php', 'DifferentialRevisionEditController' => 'applications/differential/controller/DifferentialRevisionEditController.php', 'DifferentialRevisionEditor' => 'applications/differential/editor/DifferentialRevisionEditor.php', 'DifferentialRevisionIDFieldParserTestCase' => 'applications/differential/field/specification/__tests__/DifferentialRevisionIDFieldParserTestCase.php', 'DifferentialRevisionIDFieldSpecification' => 'applications/differential/field/specification/DifferentialRevisionIDFieldSpecification.php', 'DifferentialRevisionListController' => 'applications/differential/controller/DifferentialRevisionListController.php', 'DifferentialRevisionListData' => 'applications/differential/data/DifferentialRevisionListData.php', 'DifferentialRevisionListView' => 'applications/differential/view/DifferentialRevisionListView.php', 'DifferentialRevisionQuery' => 'applications/differential/query/DifferentialRevisionQuery.php', 'DifferentialRevisionStatsController' => 'applications/differential/controller/DifferentialRevisionStatsController.php', 'DifferentialRevisionStatsView' => 'applications/differential/view/DifferentialRevisionStatsView.php', 'DifferentialRevisionStatusFieldSpecification' => 'applications/differential/field/specification/DifferentialRevisionStatusFieldSpecification.php', 'DifferentialRevisionUpdateHistoryView' => 'applications/differential/view/DifferentialRevisionUpdateHistoryView.php', 'DifferentialRevisionViewController' => 'applications/differential/controller/DifferentialRevisionViewController.php', 'DifferentialSubscribeController' => 'applications/differential/controller/DifferentialSubscribeController.php', 'DifferentialSummaryFieldSpecification' => 'applications/differential/field/specification/DifferentialSummaryFieldSpecification.php', 'DifferentialTasksAttacher' => 'applications/differential/DifferentialTasksAttacher.php', 'DifferentialTestPlanFieldSpecification' => 'applications/differential/field/specification/DifferentialTestPlanFieldSpecification.php', 'DifferentialTitleFieldSpecification' => 'applications/differential/field/specification/DifferentialTitleFieldSpecification.php', 'DifferentialUnitFieldSpecification' => 'applications/differential/field/specification/DifferentialUnitFieldSpecification.php', 'DifferentialUnitStatus' => 'applications/differential/constants/DifferentialUnitStatus.php', 'DifferentialUnitTestResult' => 'applications/differential/constants/DifferentialUnitTestResult.php', 'DiffusionBranchInformation' => 'applications/diffusion/data/DiffusionBranchInformation.php', 'DiffusionBranchQuery' => 'applications/diffusion/query/branch/DiffusionBranchQuery.php', 'DiffusionBranchTableController' => 'applications/diffusion/controller/DiffusionBranchTableController.php', 'DiffusionBranchTableView' => 'applications/diffusion/view/DiffusionBranchTableView.php', 'DiffusionBrowseController' => 'applications/diffusion/controller/DiffusionBrowseController.php', 'DiffusionBrowseFileController' => 'applications/diffusion/controller/DiffusionBrowseFileController.php', 'DiffusionBrowseQuery' => 'applications/diffusion/query/browse/DiffusionBrowseQuery.php', 'DiffusionBrowseTableView' => 'applications/diffusion/view/DiffusionBrowseTableView.php', 'DiffusionChangeController' => 'applications/diffusion/controller/DiffusionChangeController.php', 'DiffusionCommentListView' => 'applications/diffusion/view/DiffusionCommentListView.php', 'DiffusionCommentView' => 'applications/diffusion/view/DiffusionCommentView.php', 'DiffusionCommitBranchesController' => 'applications/diffusion/controller/DiffusionCommitBranchesController.php', 'DiffusionCommitChangeTableView' => 'applications/diffusion/view/DiffusionCommitChangeTableView.php', 'DiffusionCommitController' => 'applications/diffusion/controller/DiffusionCommitController.php', 'DiffusionCommitEditController' => 'applications/diffusion/controller/DiffusionCommitEditController.php', 'DiffusionCommitParentsQuery' => 'applications/diffusion/query/parents/DiffusionCommitParentsQuery.php', 'DiffusionCommitTagsController' => 'applications/diffusion/controller/DiffusionCommitTagsController.php', 'DiffusionCommitTagsQuery' => 'applications/diffusion/query/committags/DiffusionCommitTagsQuery.php', 'DiffusionContainsQuery' => 'applications/diffusion/query/contains/DiffusionContainsQuery.php', 'DiffusionController' => 'applications/diffusion/controller/DiffusionController.php', 'DiffusionDiffController' => 'applications/diffusion/controller/DiffusionDiffController.php', 'DiffusionDiffQuery' => 'applications/diffusion/query/diff/DiffusionDiffQuery.php', 'DiffusionEmptyResultView' => 'applications/diffusion/view/DiffusionEmptyResultView.php', 'DiffusionExistsQuery' => 'applications/diffusion/query/exists/DiffusionExistsQuery.php', 'DiffusionExternalController' => 'applications/diffusion/controller/DiffusionExternalController.php', 'DiffusionFileContent' => 'applications/diffusion/data/DiffusionFileContent.php', 'DiffusionFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionFileContentQuery.php', 'DiffusionGitBranchQuery' => 'applications/diffusion/query/branch/DiffusionGitBranchQuery.php', 'DiffusionGitBranchQueryTestCase' => 'applications/diffusion/query/branch/__tests__/DiffusionGitBranchQueryTestCase.php', 'DiffusionGitBrowseQuery' => 'applications/diffusion/query/browse/DiffusionGitBrowseQuery.php', 'DiffusionGitCommitParentsQuery' => 'applications/diffusion/query/parents/DiffusionGitCommitParentsQuery.php', 'DiffusionGitCommitTagsQuery' => 'applications/diffusion/query/committags/DiffusionGitCommitTagsQuery.php', 'DiffusionGitContainsQuery' => 'applications/diffusion/query/contains/DiffusionGitContainsQuery.php', 'DiffusionGitDiffQuery' => 'applications/diffusion/query/diff/DiffusionGitDiffQuery.php', 'DiffusionGitExistsQuery' => 'applications/diffusion/query/exists/DiffusionGitExistsQuery.php', 'DiffusionGitFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionGitFileContentQuery.php', 'DiffusionGitHistoryQuery' => 'applications/diffusion/query/history/DiffusionGitHistoryQuery.php', 'DiffusionGitLastModifiedQuery' => 'applications/diffusion/query/lastmodified/DiffusionGitLastModifiedQuery.php', 'DiffusionGitMergedCommitsQuery' => 'applications/diffusion/query/mergedcommits/DiffusionGitMergedCommitsQuery.php', 'DiffusionGitRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionGitRawDiffQuery.php', 'DiffusionGitRequest' => 'applications/diffusion/request/DiffusionGitRequest.php', 'DiffusionGitTagListQuery' => 'applications/diffusion/query/taglist/DiffusionGitTagListQuery.php', 'DiffusionHistoryController' => 'applications/diffusion/controller/DiffusionHistoryController.php', 'DiffusionHistoryQuery' => 'applications/diffusion/query/history/DiffusionHistoryQuery.php', 'DiffusionHistoryTableView' => 'applications/diffusion/view/DiffusionHistoryTableView.php', 'DiffusionHomeController' => 'applications/diffusion/controller/DiffusionHomeController.php', 'DiffusionInlineCommentController' => 'applications/diffusion/controller/DiffusionInlineCommentController.php', 'DiffusionInlineCommentPreviewController' => 'applications/diffusion/controller/DiffusionInlineCommentPreviewController.php', 'DiffusionLastModifiedController' => 'applications/diffusion/controller/DiffusionLastModifiedController.php', 'DiffusionLastModifiedQuery' => 'applications/diffusion/query/lastmodified/DiffusionLastModifiedQuery.php', 'DiffusionMercurialBranchQuery' => 'applications/diffusion/query/branch/DiffusionMercurialBranchQuery.php', 'DiffusionMercurialBrowseQuery' => 'applications/diffusion/query/browse/DiffusionMercurialBrowseQuery.php', 'DiffusionMercurialCommitParentsQuery' => 'applications/diffusion/query/parents/DiffusionMercurialCommitParentsQuery.php', 'DiffusionMercurialCommitTagsQuery' => 'applications/diffusion/query/committags/DiffusionMercurialCommitTagsQuery.php', 'DiffusionMercurialContainsQuery' => 'applications/diffusion/query/contains/DiffusionMercurialContainsQuery.php', 'DiffusionMercurialDiffQuery' => 'applications/diffusion/query/diff/DiffusionMercurialDiffQuery.php', 'DiffusionMercurialExistsQuery' => 'applications/diffusion/query/exists/DiffusionMercurialExistsQuery.php', 'DiffusionMercurialFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionMercurialFileContentQuery.php', 'DiffusionMercurialHistoryQuery' => 'applications/diffusion/query/history/DiffusionMercurialHistoryQuery.php', 'DiffusionMercurialLastModifiedQuery' => 'applications/diffusion/query/lastmodified/DiffusionMercurialLastModifiedQuery.php', 'DiffusionMercurialMergedCommitsQuery' => 'applications/diffusion/query/mergedcommits/DiffusionMercurialMergedCommitsQuery.php', 'DiffusionMercurialRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionMercurialRawDiffQuery.php', 'DiffusionMercurialRequest' => 'applications/diffusion/request/DiffusionMercurialRequest.php', 'DiffusionMercurialTagListQuery' => 'applications/diffusion/query/taglist/DiffusionMercurialTagListQuery.php', 'DiffusionMergedCommitsQuery' => 'applications/diffusion/query/mergedcommits/DiffusionMergedCommitsQuery.php', 'DiffusionPathChange' => 'applications/diffusion/data/DiffusionPathChange.php', 'DiffusionPathChangeQuery' => 'applications/diffusion/query/pathchange/DiffusionPathChangeQuery.php', 'DiffusionPathCompleteController' => 'applications/diffusion/controller/DiffusionPathCompleteController.php', 'DiffusionPathIDQuery' => 'applications/diffusion/query/pathid/DiffusionPathIDQuery.php', 'DiffusionPathQuery' => 'applications/diffusion/query/DiffusionPathQuery.php', 'DiffusionPathQueryTestCase' => 'applications/diffusion/query/pathid/__tests__/DiffusionPathQueryTestCase.php', 'DiffusionPathValidateController' => 'applications/diffusion/controller/DiffusionPathValidateController.php', 'DiffusionQuery' => 'applications/diffusion/query/DiffusionQuery.php', 'DiffusionRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionRawDiffQuery.php', 'DiffusionRenameHistoryQuery' => 'applications/diffusion/query/DiffusionRenameHistoryQuery.php', 'DiffusionRepositoryController' => 'applications/diffusion/controller/DiffusionRepositoryController.php', 'DiffusionRepositoryPath' => 'applications/diffusion/data/DiffusionRepositoryPath.php', 'DiffusionRepositoryTag' => 'applications/diffusion/DiffusionRepositoryTag.php', 'DiffusionRequest' => 'applications/diffusion/request/DiffusionRequest.php', 'DiffusionSetupException' => 'applications/diffusion/exception/DiffusionSetupException.php', 'DiffusionSvnBrowseQuery' => 'applications/diffusion/query/browse/DiffusionSvnBrowseQuery.php', 'DiffusionSvnCommitParentsQuery' => 'applications/diffusion/query/parents/DiffusionSvnCommitParentsQuery.php', 'DiffusionSvnCommitTagsQuery' => 'applications/diffusion/query/committags/DiffusionSvnCommitTagsQuery.php', 'DiffusionSvnContainsQuery' => 'applications/diffusion/query/contains/DiffusionSvnContainsQuery.php', 'DiffusionSvnDiffQuery' => 'applications/diffusion/query/diff/DiffusionSvnDiffQuery.php', 'DiffusionSvnExistsQuery' => 'applications/diffusion/query/exists/DiffusionSvnExistsQuery.php', 'DiffusionSvnFileContentQuery' => 'applications/diffusion/query/filecontent/DiffusionSvnFileContentQuery.php', 'DiffusionSvnHistoryQuery' => 'applications/diffusion/query/history/DiffusionSvnHistoryQuery.php', 'DiffusionSvnLastModifiedQuery' => 'applications/diffusion/query/lastmodified/DiffusionSvnLastModifiedQuery.php', 'DiffusionSvnMergedCommitsQuery' => 'applications/diffusion/query/mergedcommits/DiffusionSvnMergedCommitsQuery.php', 'DiffusionSvnRawDiffQuery' => 'applications/diffusion/query/rawdiff/DiffusionSvnRawDiffQuery.php', 'DiffusionSvnRequest' => 'applications/diffusion/request/DiffusionSvnRequest.php', 'DiffusionSvnTagListQuery' => 'applications/diffusion/query/taglist/DiffusionSvnTagListQuery.php', 'DiffusionSymbolController' => 'applications/diffusion/controller/DiffusionSymbolController.php', 'DiffusionSymbolQuery' => 'applications/diffusion/query/DiffusionSymbolQuery.php', 'DiffusionTagListController' => 'applications/diffusion/controller/DiffusionTagListController.php', 'DiffusionTagListQuery' => 'applications/diffusion/query/taglist/DiffusionTagListQuery.php', 'DiffusionTagListView' => 'applications/diffusion/view/DiffusionTagListView.php', 'DiffusionURITestCase' => 'applications/diffusion/request/__tests__/DiffusionURITestCase.php', 'DiffusionView' => 'applications/diffusion/view/DiffusionView.php', 'DivinerListController' => 'applications/diviner/controller/DivinerListController.php', 'DrydockAllocator' => 'applications/drydock/allocator/DrydockAllocator.php', 'DrydockAllocatorWorker' => 'applications/drydock/allocator/DrydockAllocatorWorker.php', 'DrydockApacheWebrootBlueprint' => 'applications/drydock/blueprint/webroot/DrydockApacheWebrootBlueprint.php', 'DrydockApacheWebrootInterface' => 'applications/drydock/interface/webroot/DrydockApacheWebrootInterface.php', 'DrydockBlueprint' => 'applications/drydock/blueprint/DrydockBlueprint.php', 'DrydockCommandInterface' => 'applications/drydock/interface/command/DrydockCommandInterface.php', 'DrydockConstants' => 'applications/drydock/constants/DrydockConstants.php', 'DrydockController' => 'applications/drydock/controller/DrydockController.php', 'DrydockDAO' => 'applications/drydock/storage/DrydockDAO.php', 'DrydockEC2HostBlueprint' => 'applications/drydock/blueprint/DrydockEC2HostBlueprint.php', 'DrydockInterface' => 'applications/drydock/interface/DrydockInterface.php', 'DrydockLease' => 'applications/drydock/storage/DrydockLease.php', 'DrydockLeaseListController' => 'applications/drydock/controller/DrydockLeaseListController.php', 'DrydockLeaseStatus' => 'applications/drydock/constants/DrydockLeaseStatus.php', 'DrydockLocalCommandInterface' => 'applications/drydock/interface/command/DrydockLocalCommandInterface.php', 'DrydockLocalHostBlueprint' => 'applications/drydock/blueprint/DrydockLocalHostBlueprint.php', 'DrydockLog' => 'applications/drydock/storage/DrydockLog.php', 'DrydockLogController' => 'applications/drydock/controller/DrydockLogController.php', 'DrydockLogQuery' => 'applications/drydock/query/DrydockLogQuery.php', 'DrydockPhabricatorApplicationBlueprint' => 'applications/drydock/blueprint/application/DrydockPhabricatorApplicationBlueprint.php', 'DrydockRemoteHostBlueprint' => 'applications/drydock/blueprint/DrydockRemoteHostBlueprint.php', 'DrydockResource' => 'applications/drydock/storage/DrydockResource.php', 'DrydockResourceAllocateController' => 'applications/drydock/controller/DrydockResourceAllocateController.php', 'DrydockResourceListController' => 'applications/drydock/controller/DrydockResourceListController.php', 'DrydockResourceStatus' => 'applications/drydock/constants/DrydockResourceStatus.php', 'DrydockSSHCommandInterface' => 'applications/drydock/interface/command/DrydockSSHCommandInterface.php', 'DrydockWebrootInterface' => 'applications/drydock/interface/webroot/DrydockWebrootInterface.php', 'HarbormasterDAO' => 'applications/harbormaster/storage/HarbormasterDAO.php', 'HarbormasterObject' => 'applications/harbormaster/storage/HarbormasterObject.php', 'HarbormasterScratchTable' => 'applications/harbormaster/storage/HarbormasterScratchTable.php', 'HeraldAction' => 'applications/herald/storage/HeraldAction.php', 'HeraldActionConfig' => 'applications/herald/config/HeraldActionConfig.php', 'HeraldApplyTranscript' => 'applications/herald/storage/transcript/HeraldApplyTranscript.php', 'HeraldCommitAdapter' => 'applications/herald/adapter/HeraldCommitAdapter.php', 'HeraldCondition' => 'applications/herald/storage/HeraldCondition.php', 'HeraldConditionConfig' => 'applications/herald/config/HeraldConditionConfig.php', 'HeraldConditionTranscript' => 'applications/herald/storage/transcript/HeraldConditionTranscript.php', 'HeraldContentTypeConfig' => 'applications/herald/config/HeraldContentTypeConfig.php', 'HeraldController' => 'applications/herald/controller/HeraldController.php', 'HeraldDAO' => 'applications/herald/storage/HeraldDAO.php', 'HeraldDeleteController' => 'applications/herald/controller/HeraldDeleteController.php', 'HeraldDifferentialRevisionAdapter' => 'applications/herald/adapter/HeraldDifferentialRevisionAdapter.php', 'HeraldDryRunAdapter' => 'applications/herald/adapter/HeraldDryRunAdapter.php', 'HeraldEditLogQuery' => 'applications/herald/query/HeraldEditLogQuery.php', 'HeraldEffect' => 'applications/herald/engine/HeraldEffect.php', 'HeraldEngine' => 'applications/herald/engine/HeraldEngine.php', 'HeraldFieldConfig' => 'applications/herald/config/HeraldFieldConfig.php', 'HeraldHomeController' => 'applications/herald/controller/HeraldHomeController.php', 'HeraldInvalidConditionException' => 'applications/herald/engine/engine/HeraldInvalidConditionException.php', 'HeraldInvalidFieldException' => 'applications/herald/engine/engine/HeraldInvalidFieldException.php', 'HeraldNewController' => 'applications/herald/controller/HeraldNewController.php', 'HeraldObjectAdapter' => 'applications/herald/adapter/HeraldObjectAdapter.php', 'HeraldObjectTranscript' => 'applications/herald/storage/transcript/HeraldObjectTranscript.php', 'HeraldRecursiveConditionsException' => 'applications/herald/engine/engine/HeraldRecursiveConditionsException.php', 'HeraldRepetitionPolicyConfig' => 'applications/herald/config/HeraldRepetitionPolicyConfig.php', 'HeraldRule' => 'applications/herald/storage/HeraldRule.php', 'HeraldRuleController' => 'applications/herald/controller/HeraldRuleController.php', 'HeraldRuleEdit' => 'applications/herald/storage/HeraldRuleEdit.php', 'HeraldRuleEditHistoryController' => 'applications/herald/controller/HeraldRuleEditHistoryController.php', 'HeraldRuleEditHistoryView' => 'applications/herald/view/HeraldRuleEditHistoryView.php', 'HeraldRuleListView' => 'applications/herald/view/HeraldRuleListView.php', 'HeraldRuleQuery' => 'applications/herald/query/HeraldRuleQuery.php', 'HeraldRuleTranscript' => 'applications/herald/storage/transcript/HeraldRuleTranscript.php', 'HeraldRuleTypeConfig' => 'applications/herald/config/HeraldRuleTypeConfig.php', 'HeraldTestConsoleController' => 'applications/herald/controller/HeraldTestConsoleController.php', 'HeraldTranscript' => 'applications/herald/storage/transcript/HeraldTranscript.php', 'HeraldTranscriptController' => 'applications/herald/controller/HeraldTranscriptController.php', 'HeraldTranscriptListController' => 'applications/herald/controller/HeraldTranscriptListController.php', 'HeraldValueTypeConfig' => 'applications/herald/config/HeraldValueTypeConfig.php', 'Javelin' => 'infrastructure/javelin/Javelin.php', 'JavelinReactorExample' => 'applications/uiexample/examples/JavelinReactorExample.php', 'JavelinUIExample' => 'applications/uiexample/examples/JavelinUIExample.php', 'JavelinViewExample' => 'applications/uiexample/examples/JavelinViewExample.php', 'JavelinViewExampleServerView' => 'applications/uiexample/examples/JavelinViewExampleServerView.php', 'LiskDAO' => 'infrastructure/storage/lisk/LiskDAO.php', 'LiskDAOSet' => 'infrastructure/storage/lisk/LiskDAOSet.php', 'LiskEphemeralObjectException' => 'infrastructure/storage/lisk/LiskEphemeralObjectException.php', 'LiskFixtureTestCase' => 'infrastructure/storage/lisk/__tests__/LiskFixtureTestCase.php', 'LiskIsolationTestCase' => 'infrastructure/storage/lisk/__tests__/LiskIsolationTestCase.php', 'LiskIsolationTestDAO' => 'infrastructure/storage/lisk/__tests__/LiskIsolationTestDAO.php', 'LiskIsolationTestDAOException' => 'infrastructure/storage/lisk/__tests__/LiskIsolationTestDAOException.php', 'LiskMigrationIterator' => 'infrastructure/storage/lisk/LiskMigrationIterator.php', 'ManiphestAction' => 'applications/maniphest/constants/ManiphestAction.php', 'ManiphestAuxiliaryFieldDefaultSpecification' => 'applications/maniphest/auxiliaryfield/ManiphestAuxiliaryFieldDefaultSpecification.php', 'ManiphestAuxiliaryFieldSpecification' => 'applications/maniphest/auxiliaryfield/ManiphestAuxiliaryFieldSpecification.php', 'ManiphestAuxiliaryFieldTypeException' => 'applications/maniphest/auxiliaryfield/ManiphestAuxiliaryFieldTypeException.php', 'ManiphestAuxiliaryFieldValidationException' => 'applications/maniphest/auxiliaryfield/ManiphestAuxiliaryFieldValidationException.php', 'ManiphestBatchEditController' => 'applications/maniphest/controller/ManiphestBatchEditController.php', 'ManiphestConstants' => 'applications/maniphest/constants/ManiphestConstants.php', 'ManiphestController' => 'applications/maniphest/controller/ManiphestController.php', 'ManiphestDAO' => 'applications/maniphest/storage/ManiphestDAO.php', 'ManiphestDefaultTaskExtensions' => 'applications/maniphest/extensions/ManiphestDefaultTaskExtensions.php', 'ManiphestEdgeEventListener' => 'applications/maniphest/event/ManiphestEdgeEventListener.php', 'ManiphestExportController' => 'applications/maniphest/controller/ManiphestExportController.php', 'ManiphestReplyHandler' => 'applications/maniphest/ManiphestReplyHandler.php', 'ManiphestReportController' => 'applications/maniphest/controller/ManiphestReportController.php', 'ManiphestSavedQuery' => 'applications/maniphest/storage/ManiphestSavedQuery.php', 'ManiphestSavedQueryDeleteController' => 'applications/maniphest/controller/ManiphestSavedQueryDeleteController.php', 'ManiphestSavedQueryEditController' => 'applications/maniphest/controller/ManiphestSavedQueryEditController.php', 'ManiphestSavedQueryListController' => 'applications/maniphest/controller/ManiphestSavedQueryListController.php', 'ManiphestSubpriorityController' => 'applications/maniphest/controller/ManiphestSubpriorityController.php', 'ManiphestTask' => 'applications/maniphest/storage/ManiphestTask.php', 'ManiphestTaskAuxiliaryStorage' => 'applications/maniphest/storage/ManiphestTaskAuxiliaryStorage.php', 'ManiphestTaskDescriptionChangeController' => 'applications/maniphest/controller/ManiphestTaskDescriptionChangeController.php', 'ManiphestTaskDescriptionPreviewController' => 'applications/maniphest/controller/ManiphestTaskDescriptionPreviewController.php', 'ManiphestTaskDetailController' => 'applications/maniphest/controller/ManiphestTaskDetailController.php', 'ManiphestTaskEditController' => 'applications/maniphest/controller/ManiphestTaskEditController.php', 'ManiphestTaskExtensions' => 'applications/maniphest/extensions/ManiphestTaskExtensions.php', 'ManiphestTaskListController' => 'applications/maniphest/controller/ManiphestTaskListController.php', 'ManiphestTaskListView' => 'applications/maniphest/view/ManiphestTaskListView.php', 'ManiphestTaskOwner' => 'applications/maniphest/constants/ManiphestTaskOwner.php', 'ManiphestTaskPriority' => 'applications/maniphest/constants/ManiphestTaskPriority.php', 'ManiphestTaskProject' => 'applications/maniphest/storage/ManiphestTaskProject.php', 'ManiphestTaskProjectsView' => 'applications/maniphest/view/ManiphestTaskProjectsView.php', 'ManiphestTaskQuery' => 'applications/maniphest/ManiphestTaskQuery.php', 'ManiphestTaskStatus' => 'applications/maniphest/constants/ManiphestTaskStatus.php', 'ManiphestTaskSubscriber' => 'applications/maniphest/storage/ManiphestTaskSubscriber.php', 'ManiphestTaskSummaryView' => 'applications/maniphest/view/ManiphestTaskSummaryView.php', 'ManiphestTransaction' => 'applications/maniphest/storage/ManiphestTransaction.php', 'ManiphestTransactionDetailView' => 'applications/maniphest/view/ManiphestTransactionDetailView.php', 'ManiphestTransactionEditor' => 'applications/maniphest/editor/ManiphestTransactionEditor.php', 'ManiphestTransactionListView' => 'applications/maniphest/view/ManiphestTransactionListView.php', 'ManiphestTransactionPreviewController' => 'applications/maniphest/controller/ManiphestTransactionPreviewController.php', 'ManiphestTransactionSaveController' => 'applications/maniphest/controller/ManiphestTransactionSaveController.php', 'ManiphestTransactionType' => 'applications/maniphest/constants/ManiphestTransactionType.php', 'ManiphestView' => 'applications/maniphest/view/ManiphestView.php', 'MetaMTAConstants' => 'applications/metamta/constants/MetaMTAConstants.php', 'MetaMTANotificationType' => 'applications/metamta/constants/MetaMTANotificationType.php', 'OwnersPackageReplyHandler' => 'applications/owners/OwnersPackageReplyHandler.php', 'PackageCreateMail' => 'applications/owners/mail/PackageCreateMail.php', 'PackageDeleteMail' => 'applications/owners/mail/PackageDeleteMail.php', 'PackageMail' => 'applications/owners/mail/PackageMail.php', 'PackageModifyMail' => 'applications/owners/mail/PackageModifyMail.php', 'Phabricator404Controller' => 'applications/base/controller/Phabricator404Controller.php', 'PhabricatorAccessLog' => 'infrastructure/PhabricatorAccessLog.php', 'PhabricatorActionListExample' => 'applications/uiexample/examples/PhabricatorActionListExample.php', 'PhabricatorActionListView' => 'view/layout/PhabricatorActionListView.php', 'PhabricatorActionView' => 'view/layout/PhabricatorActionView.php', 'PhabricatorAnchorView' => 'view/layout/PhabricatorAnchorView.php', 'PhabricatorApplication' => 'applications/base/PhabricatorApplication.php', 'PhabricatorApplicationApplications' => 'applications/meta/application/PhabricatorApplicationApplications.php', 'PhabricatorApplicationAudit' => 'applications/audit/application/PhabricatorApplicationAudit.php', 'PhabricatorApplicationAuth' => 'applications/auth/application/PhabricatorApplicationAuth.php', 'PhabricatorApplicationConduit' => 'applications/conduit/application/PhabricatorApplicationConduit.php', 'PhabricatorApplicationCountdown' => 'applications/countdown/application/PhabricatorApplicationCountdown.php', 'PhabricatorApplicationDaemons' => 'applications/daemon/application/PhabricatorApplicationDaemons.php', 'PhabricatorApplicationDifferential' => 'applications/differential/application/PhabricatorApplicationDifferential.php', 'PhabricatorApplicationDiffusion' => 'applications/diffusion/application/PhabricatorApplicationDiffusion.php', 'PhabricatorApplicationDiviner' => 'applications/diviner/application/PhabricatorApplicationDiviner.php', 'PhabricatorApplicationFact' => 'applications/fact/application/PhabricatorApplicationFact.php', 'PhabricatorApplicationFiles' => 'applications/files/application/PhabricatorApplicationFiles.php', 'PhabricatorApplicationFlags' => 'applications/flag/application/PhabricatorApplicationFlags.php', 'PhabricatorApplicationHerald' => 'applications/herald/application/PhabricatorApplicationHerald.php', 'PhabricatorApplicationLaunchView' => 'applications/meta/view/PhabricatorApplicationLaunchView.php', 'PhabricatorApplicationMacro' => 'applications/macro/application/PhabricatorApplicationMacro.php', 'PhabricatorApplicationMailingLists' => 'applications/mailinglists/application/PhabricatorApplicationMailingLists.php', 'PhabricatorApplicationManiphest' => 'applications/maniphest/application/PhabricatorApplicationManiphest.php', 'PhabricatorApplicationMetaMTA' => 'applications/metamta/application/PhabricatorApplicationMetaMTA.php', 'PhabricatorApplicationOwners' => 'applications/owners/application/PhabricatorApplicationOwners.php', 'PhabricatorApplicationPHID' => 'applications/phid/application/PhabricatorApplicationPHID.php', 'PhabricatorApplicationPHPAST' => 'applications/xhpastview/application/PhabricatorApplicationPHPAST.php', 'PhabricatorApplicationPaste' => 'applications/paste/application/PhabricatorApplicationPaste.php', 'PhabricatorApplicationPeople' => 'applications/people/application/PhabricatorApplicationPeople.php', 'PhabricatorApplicationPhame' => 'applications/phame/application/PhabricatorApplicationPhame.php', 'PhabricatorApplicationPhriction' => 'applications/phriction/application/PhabricatorApplicationPhriction.php', 'PhabricatorApplicationPonder' => 'applications/ponder/application/PhabricatorApplicationPonder.php', 'PhabricatorApplicationProject' => 'applications/project/application/PhabricatorApplicationProject.php', 'PhabricatorApplicationRepositories' => 'applications/repository/application/PhabricatorApplicationRepositories.php', 'PhabricatorApplicationSettings' => 'applications/settings/application/PhabricatorApplicationSettings.php', 'PhabricatorApplicationSlowvote' => 'applications/slowvote/application/PhabricatorApplicationSlowvote.php', 'PhabricatorApplicationStatusView' => 'applications/meta/view/PhabricatorApplicationStatusView.php', 'PhabricatorApplicationSubscriptions' => 'applications/subscriptions/application/PhabricatorApplicationSubscriptions.php', 'PhabricatorApplicationUIExamples' => 'applications/uiexample/application/PhabricatorApplicationUIExamples.php', 'PhabricatorApplicationsListController' => 'applications/meta/controller/PhabricatorApplicationsListController.php', 'PhabricatorAuditActionConstants' => 'applications/audit/constants/PhabricatorAuditActionConstants.php', 'PhabricatorAuditAddCommentController' => 'applications/audit/controller/PhabricatorAuditAddCommentController.php', 'PhabricatorAuditComment' => 'applications/audit/storage/PhabricatorAuditComment.php', 'PhabricatorAuditCommentEditor' => 'applications/audit/editor/PhabricatorAuditCommentEditor.php', 'PhabricatorAuditCommitListView' => 'applications/audit/view/PhabricatorAuditCommitListView.php', 'PhabricatorAuditCommitQuery' => 'applications/audit/query/PhabricatorAuditCommitQuery.php', 'PhabricatorAuditCommitStatusConstants' => 'applications/audit/constants/PhabricatorAuditCommitStatusConstants.php', 'PhabricatorAuditController' => 'applications/audit/controller/PhabricatorAuditController.php', 'PhabricatorAuditDAO' => 'applications/audit/storage/PhabricatorAuditDAO.php', 'PhabricatorAuditInlineComment' => 'applications/audit/storage/PhabricatorAuditInlineComment.php', 'PhabricatorAuditListController' => 'applications/audit/controller/PhabricatorAuditListController.php', 'PhabricatorAuditListView' => 'applications/audit/view/PhabricatorAuditListView.php', 'PhabricatorAuditPreviewController' => 'applications/audit/controller/PhabricatorAuditPreviewController.php', 'PhabricatorAuditQuery' => 'applications/audit/query/PhabricatorAuditQuery.php', 'PhabricatorAuditReplyHandler' => 'applications/audit/PhabricatorAuditReplyHandler.php', 'PhabricatorAuditStatusConstants' => 'applications/audit/constants/PhabricatorAuditStatusConstants.php', 'PhabricatorAuthController' => 'applications/auth/controller/PhabricatorAuthController.php', 'PhabricatorBaseEnglishTranslation' => 'infrastructure/internationalization/PhabricatorBaseEnglishTranslation.php', 'PhabricatorBuiltinPatchList' => 'infrastructure/storage/patch/PhabricatorBuiltinPatchList.php', 'PhabricatorCacheDAO' => 'applications/cache/storage/PhabricatorCacheDAO.php', 'PhabricatorCalendarBrowseController' => 'applications/calendar/controller/PhabricatorCalendarBrowseController.php', 'PhabricatorCalendarController' => 'applications/calendar/controller/PhabricatorCalendarController.php', 'PhabricatorCalendarDAO' => 'applications/calendar/storage/PhabricatorCalendarDAO.php', 'PhabricatorCalendarHoliday' => 'applications/calendar/storage/PhabricatorCalendarHoliday.php', 'PhabricatorCalendarHolidayTestCase' => 'applications/calendar/storage/__tests__/PhabricatorCalendarHolidayTestCase.php', 'PhabricatorChangesetResponse' => 'infrastructure/diff/PhabricatorChangesetResponse.php', 'PhabricatorChatLogChannelListController' => 'applications/chatlog/controller/PhabricatorChatLogChannelListController.php', 'PhabricatorChatLogChannelLogController' => 'applications/chatlog/controller/PhabricatorChatLogChannelLogController.php', 'PhabricatorChatLogConstants' => 'applications/chatlog/constants/PhabricatorChatLogConstants.php', 'PhabricatorChatLogController' => 'applications/chatlog/controller/PhabricatorChatLogController.php', 'PhabricatorChatLogDAO' => 'applications/chatlog/storage/PhabricatorChatLogDAO.php', 'PhabricatorChatLogEvent' => 'applications/chatlog/storage/PhabricatorChatLogEvent.php', 'PhabricatorChatLogEventType' => 'applications/chatlog/constants/PhabricatorChatLogEventType.php', 'PhabricatorChatLogQuery' => 'applications/chatlog/PhabricatorChatLogQuery.php', 'PhabricatorConduitAPIController' => 'applications/conduit/controller/PhabricatorConduitAPIController.php', 'PhabricatorConduitCertificateToken' => 'applications/conduit/storage/PhabricatorConduitCertificateToken.php', 'PhabricatorConduitConnectionLog' => 'applications/conduit/storage/PhabricatorConduitConnectionLog.php', 'PhabricatorConduitConsoleController' => 'applications/conduit/controller/PhabricatorConduitConsoleController.php', 'PhabricatorConduitController' => 'applications/conduit/controller/PhabricatorConduitController.php', 'PhabricatorConduitDAO' => 'applications/conduit/storage/PhabricatorConduitDAO.php', 'PhabricatorConduitListController' => 'applications/conduit/controller/PhabricatorConduitListController.php', 'PhabricatorConduitLogController' => 'applications/conduit/controller/PhabricatorConduitLogController.php', 'PhabricatorConduitMethodCallLog' => 'applications/conduit/storage/PhabricatorConduitMethodCallLog.php', 'PhabricatorConduitTokenController' => 'applications/conduit/controller/PhabricatorConduitTokenController.php', 'PhabricatorContentSource' => 'applications/metamta/contentsource/PhabricatorContentSource.php', 'PhabricatorContentSourceView' => 'applications/metamta/contentsource/PhabricatorContentSourceView.php', 'PhabricatorController' => 'applications/base/controller/PhabricatorController.php', 'PhabricatorCountdownController' => 'applications/countdown/controller/PhabricatorCountdownController.php', 'PhabricatorCountdownDAO' => 'applications/countdown/storage/PhabricatorCountdownDAO.php', 'PhabricatorCountdownDeleteController' => 'applications/countdown/controller/PhabricatorCountdownDeleteController.php', 'PhabricatorCountdownEditController' => 'applications/countdown/controller/PhabricatorCountdownEditController.php', 'PhabricatorCountdownListController' => 'applications/countdown/controller/PhabricatorCountdownListController.php', 'PhabricatorCountdownViewController' => 'applications/countdown/controller/PhabricatorCountdownViewController.php', 'PhabricatorCursorPagedPolicyAwareQuery' => 'infrastructure/query/policy/PhabricatorCursorPagedPolicyAwareQuery.php', 'PhabricatorDaemon' => 'infrastructure/daemon/PhabricatorDaemon.php', 'PhabricatorDaemonCombinedLogController' => 'applications/daemon/controller/PhabricatorDaemonCombinedLogController.php', 'PhabricatorDaemonConsoleController' => 'applications/daemon/controller/PhabricatorDaemonConsoleController.php', 'PhabricatorDaemonControl' => 'infrastructure/daemon/PhabricatorDaemonControl.php', 'PhabricatorDaemonController' => 'applications/daemon/controller/PhabricatorDaemonController.php', 'PhabricatorDaemonDAO' => 'infrastructure/daemon/storage/PhabricatorDaemonDAO.php', 'PhabricatorDaemonLog' => 'infrastructure/daemon/storage/PhabricatorDaemonLog.php', 'PhabricatorDaemonLogEvent' => 'infrastructure/daemon/storage/PhabricatorDaemonLogEvent.php', 'PhabricatorDaemonLogEventsView' => 'applications/daemon/view/PhabricatorDaemonLogEventsView.php', 'PhabricatorDaemonLogListController' => 'applications/daemon/controller/PhabricatorDaemonLogListController.php', 'PhabricatorDaemonLogListView' => 'applications/daemon/view/PhabricatorDaemonLogListView.php', 'PhabricatorDaemonLogViewController' => 'applications/daemon/controller/PhabricatorDaemonLogViewController.php', 'PhabricatorDaemonReference' => 'infrastructure/daemon/control/PhabricatorDaemonReference.php', 'PhabricatorDaemonTimelineConsoleController' => 'applications/daemon/controller/PhabricatorDaemonTimelineConsoleController.php', 'PhabricatorDaemonTimelineEventController' => 'applications/daemon/controller/PhabricatorDaemonTimelineEventController.php', 'PhabricatorDefaultFileStorageEngineSelector' => 'applications/files/engineselector/PhabricatorDefaultFileStorageEngineSelector.php', 'PhabricatorDefaultSearchEngineSelector' => 'applications/search/selector/PhabricatorDefaultSearchEngineSelector.php', 'PhabricatorDifferenceEngine' => 'infrastructure/diff/PhabricatorDifferenceEngine.php', 'PhabricatorDirectoryController' => 'applications/directory/controller/PhabricatorDirectoryController.php', 'PhabricatorDirectoryMainController' => 'applications/directory/controller/PhabricatorDirectoryMainController.php', 'PhabricatorDisabledUserController' => 'applications/auth/controller/PhabricatorDisabledUserController.php', 'PhabricatorDraft' => 'applications/draft/storage/PhabricatorDraft.php', 'PhabricatorDraftDAO' => 'applications/draft/storage/PhabricatorDraftDAO.php', 'PhabricatorEdgeConfig' => 'infrastructure/edges/constants/PhabricatorEdgeConfig.php', 'PhabricatorEdgeConstants' => 'infrastructure/edges/constants/PhabricatorEdgeConstants.php', 'PhabricatorEdgeCycleException' => 'infrastructure/edges/exception/PhabricatorEdgeCycleException.php', 'PhabricatorEdgeEditor' => 'infrastructure/edges/editor/PhabricatorEdgeEditor.php', 'PhabricatorEdgeGraph' => 'infrastructure/edges/util/PhabricatorEdgeGraph.php', 'PhabricatorEdgeQuery' => 'infrastructure/edges/query/PhabricatorEdgeQuery.php', 'PhabricatorEdgeTestCase' => 'infrastructure/edges/__tests__/PhabricatorEdgeTestCase.php', + 'PhabricatorEditor' => 'infrastructure/PhabricatorEditor.php', 'PhabricatorEmailLoginController' => 'applications/auth/controller/PhabricatorEmailLoginController.php', 'PhabricatorEmailTokenController' => 'applications/auth/controller/PhabricatorEmailTokenController.php', 'PhabricatorEmailVerificationController' => 'applications/people/controller/PhabricatorEmailVerificationController.php', 'PhabricatorEnglishTranslation' => 'infrastructure/internationalization/PhabricatorEnglishTranslation.php', 'PhabricatorEnv' => 'infrastructure/PhabricatorEnv.php', 'PhabricatorEnvTestCase' => 'infrastructure/__tests__/PhabricatorEnvTestCase.php', 'PhabricatorErrorExample' => 'applications/uiexample/examples/PhabricatorErrorExample.php', 'PhabricatorEvent' => 'infrastructure/events/PhabricatorEvent.php', 'PhabricatorEventEngine' => 'infrastructure/events/PhabricatorEventEngine.php', 'PhabricatorEventType' => 'infrastructure/events/constant/PhabricatorEventType.php', 'PhabricatorExampleEventListener' => 'infrastructure/events/PhabricatorExampleEventListener.php', 'PhabricatorFactAggregate' => 'applications/fact/storage/PhabricatorFactAggregate.php', 'PhabricatorFactChartController' => 'applications/fact/controller/PhabricatorFactChartController.php', 'PhabricatorFactController' => 'applications/fact/controller/PhabricatorFactController.php', 'PhabricatorFactCountEngine' => 'applications/fact/engine/PhabricatorFactCountEngine.php', 'PhabricatorFactCursor' => 'applications/fact/storage/PhabricatorFactCursor.php', 'PhabricatorFactDAO' => 'applications/fact/storage/PhabricatorFactDAO.php', 'PhabricatorFactDaemon' => 'applications/fact/daemon/PhabricatorFactDaemon.php', 'PhabricatorFactEngine' => 'applications/fact/engine/PhabricatorFactEngine.php', 'PhabricatorFactHomeController' => 'applications/fact/controller/PhabricatorFactHomeController.php', 'PhabricatorFactLastUpdatedEngine' => 'applications/fact/engine/PhabricatorFactLastUpdatedEngine.php', 'PhabricatorFactManagementAnalyzeWorkflow' => 'applications/fact/management/PhabricatorFactManagementAnalyzeWorkflow.php', 'PhabricatorFactManagementCursorsWorkflow' => 'applications/fact/management/PhabricatorFactManagementCursorsWorkflow.php', 'PhabricatorFactManagementDestroyWorkflow' => 'applications/fact/management/PhabricatorFactManagementDestroyWorkflow.php', 'PhabricatorFactManagementListWorkflow' => 'applications/fact/management/PhabricatorFactManagementListWorkflow.php', 'PhabricatorFactManagementStatusWorkflow' => 'applications/fact/management/PhabricatorFactManagementStatusWorkflow.php', 'PhabricatorFactManagementWorkflow' => 'applications/fact/management/PhabricatorFactManagementWorkflow.php', 'PhabricatorFactRaw' => 'applications/fact/storage/PhabricatorFactRaw.php', 'PhabricatorFactSimpleSpec' => 'applications/fact/spec/PhabricatorFactSimpleSpec.php', 'PhabricatorFactSpec' => 'applications/fact/spec/PhabricatorFactSpec.php', 'PhabricatorFactUpdateIterator' => 'applications/fact/extract/PhabricatorFactUpdateIterator.php', 'PhabricatorFeedBuilder' => 'applications/feed/builder/PhabricatorFeedBuilder.php', 'PhabricatorFeedConstants' => 'applications/feed/constants/PhabricatorFeedConstants.php', 'PhabricatorFeedController' => 'applications/feed/controller/PhabricatorFeedController.php', 'PhabricatorFeedDAO' => 'applications/feed/storage/PhabricatorFeedDAO.php', 'PhabricatorFeedPublicStreamController' => 'applications/feed/controller/PhabricatorFeedPublicStreamController.php', 'PhabricatorFeedQuery' => 'applications/feed/PhabricatorFeedQuery.php', 'PhabricatorFeedStory' => 'applications/feed/story/PhabricatorFeedStory.php', 'PhabricatorFeedStoryAggregate' => 'applications/feed/story/PhabricatorFeedStoryAggregate.php', 'PhabricatorFeedStoryAudit' => 'applications/feed/story/PhabricatorFeedStoryAudit.php', 'PhabricatorFeedStoryCommit' => 'applications/feed/story/PhabricatorFeedStoryCommit.php', 'PhabricatorFeedStoryData' => 'applications/feed/storage/PhabricatorFeedStoryData.php', 'PhabricatorFeedStoryDifferential' => 'applications/feed/story/PhabricatorFeedStoryDifferential.php', 'PhabricatorFeedStoryDifferentialAggregate' => 'applications/feed/story/PhabricatorFeedStoryDifferentialAggregate.php', 'PhabricatorFeedStoryManiphest' => 'applications/feed/story/PhabricatorFeedStoryManiphest.php', 'PhabricatorFeedStoryManiphestAggregate' => 'applications/feed/story/PhabricatorFeedStoryManiphestAggregate.php', 'PhabricatorFeedStoryNotification' => 'applications/notification/storage/PhabricatorFeedStoryNotification.php', 'PhabricatorFeedStoryPhriction' => 'applications/feed/story/PhabricatorFeedStoryPhriction.php', 'PhabricatorFeedStoryProject' => 'applications/feed/story/PhabricatorFeedStoryProject.php', 'PhabricatorFeedStoryPublisher' => 'applications/feed/PhabricatorFeedStoryPublisher.php', 'PhabricatorFeedStoryReference' => 'applications/feed/storage/PhabricatorFeedStoryReference.php', 'PhabricatorFeedStoryStatus' => 'applications/feed/story/PhabricatorFeedStoryStatus.php', 'PhabricatorFeedStoryTypeConstants' => 'applications/feed/constants/PhabricatorFeedStoryTypeConstants.php', 'PhabricatorFeedStoryUnknown' => 'applications/feed/story/PhabricatorFeedStoryUnknown.php', 'PhabricatorFeedStoryView' => 'applications/feed/view/PhabricatorFeedStoryView.php', 'PhabricatorFeedView' => 'applications/feed/view/PhabricatorFeedView.php', 'PhabricatorFile' => 'applications/files/storage/PhabricatorFile.php', 'PhabricatorFileController' => 'applications/files/controller/PhabricatorFileController.php', 'PhabricatorFileDAO' => 'applications/files/storage/PhabricatorFileDAO.php', 'PhabricatorFileDataController' => 'applications/files/controller/PhabricatorFileDataController.php', 'PhabricatorFileDeleteController' => 'applications/files/controller/PhabricatorFileDeleteController.php', 'PhabricatorFileDropUploadController' => 'applications/files/controller/PhabricatorFileDropUploadController.php', 'PhabricatorFileImageMacro' => 'applications/macro/storage/PhabricatorFileImageMacro.php', 'PhabricatorFileInfoController' => 'applications/files/controller/PhabricatorFileInfoController.php', 'PhabricatorFileListController' => 'applications/files/controller/PhabricatorFileListController.php', 'PhabricatorFileProxyController' => 'applications/files/controller/PhabricatorFileProxyController.php', 'PhabricatorFileProxyImage' => 'applications/files/storage/PhabricatorFileProxyImage.php', 'PhabricatorFileShortcutController' => 'applications/files/controller/PhabricatorFileShortcutController.php', 'PhabricatorFileSideNavView' => 'applications/files/view/PhabricatorFileSideNavView.php', 'PhabricatorFileStorageBlob' => 'applications/files/storage/PhabricatorFileStorageBlob.php', 'PhabricatorFileStorageConfigurationException' => 'applications/files/exception/PhabricatorFileStorageConfigurationException.php', 'PhabricatorFileStorageEngine' => 'applications/files/engine/PhabricatorFileStorageEngine.php', 'PhabricatorFileStorageEngineSelector' => 'applications/files/engineselector/PhabricatorFileStorageEngineSelector.php', 'PhabricatorFileTransformController' => 'applications/files/controller/PhabricatorFileTransformController.php', 'PhabricatorFileUploadController' => 'applications/files/controller/PhabricatorFileUploadController.php', 'PhabricatorFileUploadException' => 'applications/files/exception/PhabricatorFileUploadException.php', 'PhabricatorFileUploadView' => 'applications/files/view/PhabricatorFileUploadView.php', 'PhabricatorFlag' => 'applications/flag/storage/PhabricatorFlag.php', 'PhabricatorFlagColor' => 'applications/flag/constants/PhabricatorFlagColor.php', 'PhabricatorFlagConstants' => 'applications/flag/constants/PhabricatorFlagConstants.php', 'PhabricatorFlagController' => 'applications/flag/controller/PhabricatorFlagController.php', 'PhabricatorFlagDAO' => 'applications/flag/storage/PhabricatorFlagDAO.php', 'PhabricatorFlagDeleteController' => 'applications/flag/controller/PhabricatorFlagDeleteController.php', 'PhabricatorFlagEditController' => 'applications/flag/controller/PhabricatorFlagEditController.php', 'PhabricatorFlagListController' => 'applications/flag/controller/PhabricatorFlagListController.php', 'PhabricatorFlagListView' => 'applications/flag/view/PhabricatorFlagListView.php', 'PhabricatorFlagQuery' => 'applications/flag/query/PhabricatorFlagQuery.php', 'PhabricatorFlagsUIEventListener' => 'applications/flag/events/PhabricatorFlagsUIEventListener.php', 'PhabricatorFormExample' => 'applications/uiexample/examples/PhabricatorFormExample.php', 'PhabricatorGarbageCollectorDaemon' => 'infrastructure/daemon/PhabricatorGarbageCollectorDaemon.php', 'PhabricatorGitGraphStream' => 'applications/repository/daemon/PhabricatorGitGraphStream.php', 'PhabricatorGlobalLock' => 'infrastructure/util/PhabricatorGlobalLock.php', 'PhabricatorGoodForNothingWorker' => 'infrastructure/daemon/workers/worker/PhabricatorGoodForNothingWorker.php', 'PhabricatorHandleObjectSelectorDataView' => 'applications/phid/handle/view/PhabricatorHandleObjectSelectorDataView.php', 'PhabricatorHash' => 'infrastructure/util/PhabricatorHash.php', 'PhabricatorHeaderView' => 'view/layout/PhabricatorHeaderView.php', 'PhabricatorHelpController' => 'applications/help/controller/PhabricatorHelpController.php', 'PhabricatorHelpKeyboardShortcutController' => 'applications/help/controller/PhabricatorHelpKeyboardShortcutController.php', 'PhabricatorIRCBot' => 'infrastructure/daemon/irc/PhabricatorIRCBot.php', 'PhabricatorIRCDifferentialNotificationHandler' => 'infrastructure/daemon/irc/handler/PhabricatorIRCDifferentialNotificationHandler.php', 'PhabricatorIRCHandler' => 'infrastructure/daemon/irc/handler/PhabricatorIRCHandler.php', 'PhabricatorIRCLogHandler' => 'infrastructure/daemon/irc/handler/PhabricatorIRCLogHandler.php', 'PhabricatorIRCMacroHandler' => 'infrastructure/daemon/irc/handler/PhabricatorIRCMacroHandler.php', 'PhabricatorIRCMessage' => 'infrastructure/daemon/irc/PhabricatorIRCMessage.php', 'PhabricatorIRCObjectNameHandler' => 'infrastructure/daemon/irc/handler/PhabricatorIRCObjectNameHandler.php', 'PhabricatorIRCProtocolHandler' => 'infrastructure/daemon/irc/handler/PhabricatorIRCProtocolHandler.php', 'PhabricatorIRCWhatsNewHandler' => 'infrastructure/daemon/irc/handler/PhabricatorIRCWhatsNewHandler.php', 'PhabricatorImageTransformer' => 'applications/files/PhabricatorImageTransformer.php', 'PhabricatorInfrastructureTestCase' => 'infrastructure/__tests__/PhabricatorInfrastructureTestCase.php', 'PhabricatorInlineCommentController' => 'infrastructure/diff/PhabricatorInlineCommentController.php', 'PhabricatorInlineCommentInterface' => 'infrastructure/diff/interface/PhabricatorInlineCommentInterface.php', 'PhabricatorInlineCommentPreviewController' => 'infrastructure/diff/PhabricatorInlineCommentPreviewController.php', 'PhabricatorInlineSummaryView' => 'infrastructure/diff/view/PhabricatorInlineSummaryView.php', 'PhabricatorJavelinLinter' => 'infrastructure/lint/linter/PhabricatorJavelinLinter.php', 'PhabricatorJumpNavHandler' => 'applications/search/engine/PhabricatorJumpNavHandler.php', 'PhabricatorLDAPLoginController' => 'applications/auth/controller/PhabricatorLDAPLoginController.php', 'PhabricatorLDAPProvider' => 'applications/auth/ldap/PhabricatorLDAPProvider.php', 'PhabricatorLDAPRegistrationController' => 'applications/auth/controller/PhabricatorLDAPRegistrationController.php', 'PhabricatorLDAPUnknownUserException' => 'applications/auth/ldap/PhabricatorLDAPUnknownUserException.php', 'PhabricatorLDAPUnlinkController' => 'applications/auth/controller/PhabricatorLDAPUnlinkController.php', 'PhabricatorLintEngine' => 'infrastructure/lint/PhabricatorLintEngine.php', 'PhabricatorLiskDAO' => 'infrastructure/storage/lisk/PhabricatorLiskDAO.php', 'PhabricatorLocalDiskFileStorageEngine' => 'applications/files/engine/PhabricatorLocalDiskFileStorageEngine.php', 'PhabricatorLocalTimeTestCase' => 'view/__tests__/PhabricatorLocalTimeTestCase.php', 'PhabricatorLoginController' => 'applications/auth/controller/PhabricatorLoginController.php', 'PhabricatorLoginValidateController' => 'applications/auth/controller/PhabricatorLoginValidateController.php', 'PhabricatorLogoutController' => 'applications/auth/controller/PhabricatorLogoutController.php', 'PhabricatorMacroController' => 'applications/macro/controller/PhabricatorMacroController.php', 'PhabricatorMacroDeleteController' => 'applications/macro/controller/PhabricatorMacroDeleteController.php', 'PhabricatorMacroEditController' => 'applications/macro/controller/PhabricatorMacroEditController.php', 'PhabricatorMacroListController' => 'applications/macro/controller/PhabricatorMacroListController.php', 'PhabricatorMailImplementationAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationAdapter.php', 'PhabricatorMailImplementationAmazonSESAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationAmazonSESAdapter.php', 'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php', 'PhabricatorMailImplementationSendGridAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php', 'PhabricatorMailImplementationTestAdapter' => 'applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php', 'PhabricatorMailReplyHandler' => 'applications/metamta/replyhandler/PhabricatorMailReplyHandler.php', 'PhabricatorMailingListsEditController' => 'applications/mailinglists/controller/PhabricatorMailingListsEditController.php', 'PhabricatorMailingListsListController' => 'applications/mailinglists/controller/PhabricatorMailingListsListController.php', 'PhabricatorMainMenuGroupView' => 'view/page/menu/PhabricatorMainMenuGroupView.php', 'PhabricatorMainMenuIconView' => 'view/page/menu/PhabricatorMainMenuIconView.php', 'PhabricatorMainMenuSearchView' => 'view/page/menu/PhabricatorMainMenuSearchView.php', 'PhabricatorMainMenuView' => 'view/page/menu/PhabricatorMainMenuView.php', 'PhabricatorMarkupCache' => 'applications/cache/storage/PhabricatorMarkupCache.php', 'PhabricatorMarkupEngine' => 'infrastructure/markup/PhabricatorMarkupEngine.php', 'PhabricatorMarkupInterface' => 'infrastructure/markup/PhabricatorMarkupInterface.php', 'PhabricatorMercurialGraphStream' => 'applications/repository/daemon/PhabricatorMercurialGraphStream.php', 'PhabricatorMetaMTAAttachment' => 'applications/metamta/storage/PhabricatorMetaMTAAttachment.php', 'PhabricatorMetaMTAController' => 'applications/metamta/controller/PhabricatorMetaMTAController.php', 'PhabricatorMetaMTADAO' => 'applications/metamta/storage/PhabricatorMetaMTADAO.php', 'PhabricatorMetaMTAEmailBodyParser' => 'applications/metamta/PhabricatorMetaMTAEmailBodyParser.php', 'PhabricatorMetaMTAEmailBodyParserTestCase' => 'applications/metamta/__tests__/PhabricatorMetaMTAEmailBodyParserTestCase.php', 'PhabricatorMetaMTAListController' => 'applications/metamta/controller/PhabricatorMetaMTAListController.php', 'PhabricatorMetaMTAMail' => 'applications/metamta/storage/PhabricatorMetaMTAMail.php', 'PhabricatorMetaMTAMailBody' => 'applications/metamta/view/PhabricatorMetaMTAMailBody.php', 'PhabricatorMetaMTAMailBodyTestCase' => 'applications/metamta/view/__tests__/PhabricatorMetaMTAMailBodyTestCase.php', 'PhabricatorMetaMTAMailTestCase' => 'applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php', 'PhabricatorMetaMTAMailingList' => 'applications/mailinglists/storage/PhabricatorMetaMTAMailingList.php', 'PhabricatorMetaMTAReceiveController' => 'applications/metamta/controller/PhabricatorMetaMTAReceiveController.php', 'PhabricatorMetaMTAReceivedListController' => 'applications/metamta/controller/PhabricatorMetaMTAReceivedListController.php', 'PhabricatorMetaMTAReceivedMail' => 'applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php', 'PhabricatorMetaMTASendController' => 'applications/metamta/controller/PhabricatorMetaMTASendController.php', 'PhabricatorMetaMTASendGridReceiveController' => 'applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php', 'PhabricatorMetaMTAViewController' => 'applications/metamta/controller/PhabricatorMetaMTAViewController.php', 'PhabricatorMetaMTAWorker' => 'applications/metamta/PhabricatorMetaMTAWorker.php', 'PhabricatorMustVerifyEmailController' => 'applications/auth/controller/PhabricatorMustVerifyEmailController.php', 'PhabricatorMySQLFileStorageEngine' => 'applications/files/engine/PhabricatorMySQLFileStorageEngine.php', 'PhabricatorNotificationBuilder' => 'applications/notification/builder/PhabricatorNotificationBuilder.php', 'PhabricatorNotificationClearController' => 'applications/notification/controller/PhabricatorNotificationClearController.php', 'PhabricatorNotificationController' => 'applications/notification/controller/PhabricatorNotificationController.php', 'PhabricatorNotificationIndividualController' => 'applications/notification/controller/PhabricatorNotificationIndividualController.php', 'PhabricatorNotificationListController' => 'applications/notification/controller/PhabricatorNotificationListController.php', 'PhabricatorNotificationPanelController' => 'applications/notification/controller/PhabricatorNotificationPanelController.php', 'PhabricatorNotificationQuery' => 'applications/notification/PhabricatorNotificationQuery.php', 'PhabricatorNotificationStatusController' => 'applications/notification/controller/PhabricatorNotificationStatusController.php', 'PhabricatorNotificationStoryView' => 'applications/notification/view/PhabricatorNotificationStoryView.php', 'PhabricatorNotificationView' => 'applications/notification/view/PhabricatorNotificationView.php', 'PhabricatorOAuthClientAuthorization' => 'applications/oauthserver/storage/PhabricatorOAuthClientAuthorization.php', 'PhabricatorOAuthClientAuthorizationBaseController' => 'applications/oauthserver/controller/clientauthorization/PhabricatorOAuthClientAuthorizationBaseController.php', 'PhabricatorOAuthClientAuthorizationDeleteController' => 'applications/oauthserver/controller/clientauthorization/PhabricatorOAuthClientAuthorizationDeleteController.php', 'PhabricatorOAuthClientAuthorizationEditController' => 'applications/oauthserver/controller/clientauthorization/PhabricatorOAuthClientAuthorizationEditController.php', 'PhabricatorOAuthClientAuthorizationListController' => 'applications/oauthserver/controller/clientauthorization/PhabricatorOAuthClientAuthorizationListController.php', 'PhabricatorOAuthClientAuthorizationQuery' => 'applications/oauthserver/query/PhabricatorOAuthClientAuthorizationQuery.php', 'PhabricatorOAuthClientBaseController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientBaseController.php', 'PhabricatorOAuthClientDeleteController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientDeleteController.php', 'PhabricatorOAuthClientEditController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientEditController.php', 'PhabricatorOAuthClientListController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientListController.php', 'PhabricatorOAuthClientViewController' => 'applications/oauthserver/controller/client/PhabricatorOAuthClientViewController.php', 'PhabricatorOAuthDefaultRegistrationController' => 'applications/auth/controller/oauthregistration/PhabricatorOAuthDefaultRegistrationController.php', 'PhabricatorOAuthDiagnosticsController' => 'applications/auth/controller/PhabricatorOAuthDiagnosticsController.php', 'PhabricatorOAuthFailureView' => 'applications/auth/view/PhabricatorOAuthFailureView.php', 'PhabricatorOAuthLoginController' => 'applications/auth/controller/PhabricatorOAuthLoginController.php', 'PhabricatorOAuthProvider' => 'applications/auth/oauth/provider/PhabricatorOAuthProvider.php', 'PhabricatorOAuthProviderDisqus' => 'applications/auth/oauth/provider/PhabricatorOAuthProviderDisqus.php', 'PhabricatorOAuthProviderException' => 'applications/auth/oauth/provider/PhabricatorOAuthProviderException.php', 'PhabricatorOAuthProviderFacebook' => 'applications/auth/oauth/provider/PhabricatorOAuthProviderFacebook.php', 'PhabricatorOAuthProviderGitHub' => 'applications/auth/oauth/provider/PhabricatorOAuthProviderGitHub.php', 'PhabricatorOAuthProviderGoogle' => 'applications/auth/oauth/provider/PhabricatorOAuthProviderGoogle.php', 'PhabricatorOAuthProviderPhabricator' => 'applications/auth/oauth/provider/PhabricatorOAuthProviderPhabricator.php', 'PhabricatorOAuthRegistrationController' => 'applications/auth/controller/oauthregistration/PhabricatorOAuthRegistrationController.php', 'PhabricatorOAuthResponse' => 'applications/oauthserver/PhabricatorOAuthResponse.php', 'PhabricatorOAuthServer' => 'applications/oauthserver/PhabricatorOAuthServer.php', 'PhabricatorOAuthServerAccessToken' => 'applications/oauthserver/storage/PhabricatorOAuthServerAccessToken.php', 'PhabricatorOAuthServerAuthController' => 'applications/oauthserver/controller/PhabricatorOAuthServerAuthController.php', 'PhabricatorOAuthServerAuthorizationCode' => 'applications/oauthserver/storage/PhabricatorOAuthServerAuthorizationCode.php', 'PhabricatorOAuthServerClient' => 'applications/oauthserver/storage/PhabricatorOAuthServerClient.php', 'PhabricatorOAuthServerClientQuery' => 'applications/oauthserver/query/PhabricatorOAuthServerClientQuery.php', 'PhabricatorOAuthServerController' => 'applications/oauthserver/controller/PhabricatorOAuthServerController.php', 'PhabricatorOAuthServerDAO' => 'applications/oauthserver/storage/PhabricatorOAuthServerDAO.php', 'PhabricatorOAuthServerScope' => 'applications/oauthserver/PhabricatorOAuthServerScope.php', 'PhabricatorOAuthServerTestCase' => 'applications/oauthserver/__tests__/PhabricatorOAuthServerTestCase.php', 'PhabricatorOAuthServerTestController' => 'applications/oauthserver/controller/PhabricatorOAuthServerTestController.php', 'PhabricatorOAuthServerTokenController' => 'applications/oauthserver/controller/PhabricatorOAuthServerTokenController.php', 'PhabricatorOAuthUnlinkController' => 'applications/auth/controller/PhabricatorOAuthUnlinkController.php', 'PhabricatorObjectHandle' => 'applications/phid/PhabricatorObjectHandle.php', 'PhabricatorObjectHandleConstants' => 'applications/phid/handle/const/PhabricatorObjectHandleConstants.php', 'PhabricatorObjectHandleData' => 'applications/phid/handle/PhabricatorObjectHandleData.php', 'PhabricatorObjectHandleStatus' => 'applications/phid/handle/const/PhabricatorObjectHandleStatus.php', 'PhabricatorObjectItemListView' => 'view/layout/PhabricatorObjectItemListView.php', 'PhabricatorObjectItemView' => 'view/layout/PhabricatorObjectItemView.php', 'PhabricatorObjectListView' => 'view/control/PhabricatorObjectListView.php', 'PhabricatorObjectSelectorDialog' => 'view/control/PhabricatorObjectSelectorDialog.php', 'PhabricatorOffsetPagedQuery' => 'infrastructure/query/PhabricatorOffsetPagedQuery.php', 'PhabricatorOwnerPathQuery' => 'applications/owners/query/PhabricatorOwnerPathQuery.php', 'PhabricatorOwnersController' => 'applications/owners/controller/PhabricatorOwnersController.php', 'PhabricatorOwnersDAO' => 'applications/owners/storage/PhabricatorOwnersDAO.php', 'PhabricatorOwnersDeleteController' => 'applications/owners/controller/PhabricatorOwnersDeleteController.php', 'PhabricatorOwnersDetailController' => 'applications/owners/controller/PhabricatorOwnersDetailController.php', 'PhabricatorOwnersEditController' => 'applications/owners/controller/PhabricatorOwnersEditController.php', 'PhabricatorOwnersListController' => 'applications/owners/controller/PhabricatorOwnersListController.php', 'PhabricatorOwnersOwner' => 'applications/owners/storage/PhabricatorOwnersOwner.php', 'PhabricatorOwnersPackage' => 'applications/owners/storage/PhabricatorOwnersPackage.php', 'PhabricatorOwnersPackageQuery' => 'applications/owners/query/PhabricatorOwnersPackageQuery.php', 'PhabricatorOwnersPath' => 'applications/owners/storage/PhabricatorOwnersPath.php', 'PhabricatorPHID' => 'applications/phid/storage/PhabricatorPHID.php', 'PhabricatorPHIDConstants' => 'applications/phid/PhabricatorPHIDConstants.php', 'PhabricatorPHIDController' => 'applications/phid/controller/PhabricatorPHIDController.php', 'PhabricatorPHIDLookupController' => 'applications/phid/controller/PhabricatorPHIDLookupController.php', 'PhabricatorPaste' => 'applications/paste/storage/PhabricatorPaste.php', 'PhabricatorPasteController' => 'applications/paste/controller/PhabricatorPasteController.php', 'PhabricatorPasteDAO' => 'applications/paste/storage/PhabricatorPasteDAO.php', 'PhabricatorPasteEditController' => 'applications/paste/controller/PhabricatorPasteEditController.php', 'PhabricatorPasteListController' => 'applications/paste/controller/PhabricatorPasteListController.php', 'PhabricatorPasteQuery' => 'applications/paste/query/PhabricatorPasteQuery.php', 'PhabricatorPasteViewController' => 'applications/paste/controller/PhabricatorPasteViewController.php', 'PhabricatorPeopleController' => 'applications/people/controller/PhabricatorPeopleController.php', 'PhabricatorPeopleEditController' => 'applications/people/controller/PhabricatorPeopleEditController.php', 'PhabricatorPeopleLdapController' => 'applications/people/controller/PhabricatorPeopleLdapController.php', 'PhabricatorPeopleListController' => 'applications/people/controller/PhabricatorPeopleListController.php', 'PhabricatorPeopleLogsController' => 'applications/people/controller/PhabricatorPeopleLogsController.php', 'PhabricatorPeopleProfileController' => 'applications/people/controller/PhabricatorPeopleProfileController.php', 'PhabricatorPeopleQuery' => 'applications/people/PhabricatorPeopleQuery.php', 'PhabricatorPinboardItemView' => 'view/layout/PhabricatorPinboardItemView.php', 'PhabricatorPinboardView' => 'view/layout/PhabricatorPinboardView.php', 'PhabricatorPolicies' => 'applications/policy/constants/PhabricatorPolicies.php', 'PhabricatorPolicy' => 'applications/policy/filter/PhabricatorPolicy.php', 'PhabricatorPolicyAwareQuery' => 'infrastructure/query/policy/PhabricatorPolicyAwareQuery.php', 'PhabricatorPolicyAwareTestQuery' => 'applications/policy/__tests__/PhabricatorPolicyAwareTestQuery.php', 'PhabricatorPolicyCapability' => 'applications/policy/constants/PhabricatorPolicyCapability.php', 'PhabricatorPolicyConstants' => 'applications/policy/constants/PhabricatorPolicyConstants.php', 'PhabricatorPolicyException' => 'applications/policy/exception/PhabricatorPolicyException.php', 'PhabricatorPolicyFilter' => 'applications/policy/filter/PhabricatorPolicyFilter.php', 'PhabricatorPolicyInterface' => 'applications/policy/interface/PhabricatorPolicyInterface.php', 'PhabricatorPolicyQuery' => 'applications/policy/query/PhabricatorPolicyQuery.php', 'PhabricatorPolicyTestCase' => 'applications/policy/__tests__/PhabricatorPolicyTestCase.php', 'PhabricatorPolicyTestObject' => 'applications/policy/__tests__/PhabricatorPolicyTestObject.php', 'PhabricatorPolicyType' => 'applications/policy/constants/PhabricatorPolicyType.php', 'PhabricatorProfileHeaderView' => 'view/layout/PhabricatorProfileHeaderView.php', 'PhabricatorProject' => 'applications/project/storage/PhabricatorProject.php', 'PhabricatorProjectConstants' => 'applications/project/constants/PhabricatorProjectConstants.php', 'PhabricatorProjectController' => 'applications/project/controller/PhabricatorProjectController.php', 'PhabricatorProjectCreateController' => 'applications/project/controller/PhabricatorProjectCreateController.php', 'PhabricatorProjectDAO' => 'applications/project/storage/PhabricatorProjectDAO.php', 'PhabricatorProjectEditor' => 'applications/project/editor/PhabricatorProjectEditor.php', 'PhabricatorProjectEditorTestCase' => 'applications/project/editor/__tests__/PhabricatorProjectEditorTestCase.php', 'PhabricatorProjectListController' => 'applications/project/controller/PhabricatorProjectListController.php', 'PhabricatorProjectMembersEditController' => 'applications/project/controller/PhabricatorProjectMembersEditController.php', 'PhabricatorProjectNameCollisionException' => 'applications/project/exception/PhabricatorProjectNameCollisionException.php', 'PhabricatorProjectProfile' => 'applications/project/storage/PhabricatorProjectProfile.php', 'PhabricatorProjectProfileController' => 'applications/project/controller/PhabricatorProjectProfileController.php', 'PhabricatorProjectProfileEditController' => 'applications/project/controller/PhabricatorProjectProfileEditController.php', 'PhabricatorProjectQuery' => 'applications/project/query/PhabricatorProjectQuery.php', 'PhabricatorProjectStatus' => 'applications/project/constants/PhabricatorProjectStatus.php', 'PhabricatorProjectTransaction' => 'applications/project/storage/PhabricatorProjectTransaction.php', 'PhabricatorProjectTransactionType' => 'applications/project/constants/PhabricatorProjectTransactionType.php', 'PhabricatorProjectUpdateController' => 'applications/project/controller/PhabricatorProjectUpdateController.php', 'PhabricatorPropertyListView' => 'view/layout/PhabricatorPropertyListView.php', 'PhabricatorQuery' => 'infrastructure/query/PhabricatorQuery.php', 'PhabricatorRedirectController' => 'applications/base/controller/PhabricatorRedirectController.php', 'PhabricatorRefreshCSRFController' => 'applications/auth/controller/PhabricatorRefreshCSRFController.php', 'PhabricatorRemarkupControl' => 'view/form/control/PhabricatorRemarkupControl.php', 'PhabricatorRemarkupRuleCountdown' => 'infrastructure/markup/rule/PhabricatorRemarkupRuleCountdown.php', 'PhabricatorRemarkupRuleDifferential' => 'infrastructure/markup/rule/PhabricatorRemarkupRuleDifferential.php', 'PhabricatorRemarkupRuleDifferentialHandle' => 'infrastructure/markup/rule/handle/PhabricatorRemarkupRuleDifferentialHandle.php', 'PhabricatorRemarkupRuleDiffusion' => 'infrastructure/markup/rule/PhabricatorRemarkupRuleDiffusion.php', 'PhabricatorRemarkupRuleEmbedFile' => 'infrastructure/markup/rule/PhabricatorRemarkupRuleEmbedFile.php', 'PhabricatorRemarkupRuleImageMacro' => 'infrastructure/markup/rule/PhabricatorRemarkupRuleImageMacro.php', 'PhabricatorRemarkupRuleManiphest' => 'infrastructure/markup/rule/PhabricatorRemarkupRuleManiphest.php', 'PhabricatorRemarkupRuleManiphestHandle' => 'infrastructure/markup/rule/handle/PhabricatorRemarkupRuleManiphestHandle.php', 'PhabricatorRemarkupRuleMention' => 'infrastructure/markup/rule/PhabricatorRemarkupRuleMention.php', 'PhabricatorRemarkupRuleObjectHandle' => 'infrastructure/markup/rule/PhabricatorRemarkupRuleObjectHandle.php', 'PhabricatorRemarkupRuleObjectName' => 'infrastructure/markup/rule/PhabricatorRemarkupRuleObjectName.php', 'PhabricatorRemarkupRulePaste' => 'infrastructure/markup/rule/PhabricatorRemarkupRulePaste.php', 'PhabricatorRemarkupRulePhriction' => 'infrastructure/markup/rule/PhabricatorRemarkupRulePhriction.php', 'PhabricatorRemarkupRuleProxyImage' => 'infrastructure/markup/rule/PhabricatorRemarkupRuleProxyImage.php', 'PhabricatorRemarkupRuleYoutube' => 'infrastructure/markup/rule/PhabricatorRemarkupRuleYoutube.php', 'PhabricatorRepository' => 'applications/repository/storage/PhabricatorRepository.php', 'PhabricatorRepositoryArcanistProject' => 'applications/repository/storage/PhabricatorRepositoryArcanistProject.php', 'PhabricatorRepositoryArcanistProjectDeleteController' => 'applications/repository/controller/PhabricatorRepositoryArcanistProjectDeleteController.php', 'PhabricatorRepositoryArcanistProjectEditController' => 'applications/repository/controller/PhabricatorRepositoryArcanistProjectEditController.php', 'PhabricatorRepositoryAuditRequest' => 'applications/repository/storage/PhabricatorRepositoryAuditRequest.php', 'PhabricatorRepositoryCommit' => 'applications/repository/storage/PhabricatorRepositoryCommit.php', 'PhabricatorRepositoryCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php', 'PhabricatorRepositoryCommitData' => 'applications/repository/storage/PhabricatorRepositoryCommitData.php', 'PhabricatorRepositoryCommitHeraldWorker' => 'applications/repository/worker/PhabricatorRepositoryCommitHeraldWorker.php', 'PhabricatorRepositoryCommitMessageDetailParser' => 'applications/repository/parser/PhabricatorRepositoryCommitMessageDetailParser.php', 'PhabricatorRepositoryCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php', 'PhabricatorRepositoryCommitOwnersWorker' => 'applications/repository/worker/PhabricatorRepositoryCommitOwnersWorker.php', 'PhabricatorRepositoryCommitParserWorker' => 'applications/repository/worker/PhabricatorRepositoryCommitParserWorker.php', 'PhabricatorRepositoryController' => 'applications/repository/controller/PhabricatorRepositoryController.php', 'PhabricatorRepositoryCreateController' => 'applications/repository/controller/PhabricatorRepositoryCreateController.php', 'PhabricatorRepositoryDAO' => 'applications/repository/storage/PhabricatorRepositoryDAO.php', 'PhabricatorRepositoryDefaultCommitMessageDetailParser' => 'applications/repository/parser/PhabricatorRepositoryDefaultCommitMessageDetailParser.php', 'PhabricatorRepositoryDeleteController' => 'applications/repository/controller/PhabricatorRepositoryDeleteController.php', 'PhabricatorRepositoryEditController' => 'applications/repository/controller/PhabricatorRepositoryEditController.php', 'PhabricatorRepositoryGitCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositoryGitCommitChangeParserWorker.php', 'PhabricatorRepositoryGitCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositoryGitCommitMessageParserWorker.php', 'PhabricatorRepositoryListController' => 'applications/repository/controller/PhabricatorRepositoryListController.php', 'PhabricatorRepositoryManagementDiscoverWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementDiscoverWorkflow.php', 'PhabricatorRepositoryManagementListWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementListWorkflow.php', 'PhabricatorRepositoryManagementPullWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementPullWorkflow.php', 'PhabricatorRepositoryManagementWorkflow' => 'applications/repository/management/PhabricatorRepositoryManagementWorkflow.php', 'PhabricatorRepositoryMercurialCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositoryMercurialCommitChangeParserWorker.php', 'PhabricatorRepositoryMercurialCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositoryMercurialCommitMessageParserWorker.php', 'PhabricatorRepositoryPullLocalDaemon' => 'applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php', 'PhabricatorRepositoryPullLocalDaemonTestCase' => 'applications/repository/daemon/__tests__/PhabricatorRepositoryPullLocalDaemonTestCase.php', 'PhabricatorRepositoryShortcut' => 'applications/repository/storage/PhabricatorRepositoryShortcut.php', 'PhabricatorRepositorySvnCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositorySvnCommitChangeParserWorker.php', 'PhabricatorRepositorySvnCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositorySvnCommitMessageParserWorker.php', 'PhabricatorRepositorySymbol' => 'applications/repository/storage/PhabricatorRepositorySymbol.php', 'PhabricatorRepositoryTestCase' => 'applications/repository/storage/__tests__/PhabricatorRepositoryTestCase.php', 'PhabricatorRepositoryType' => 'applications/repository/constants/PhabricatorRepositoryType.php', 'PhabricatorRequestOverseer' => 'infrastructure/PhabricatorRequestOverseer.php', 'PhabricatorS3FileStorageEngine' => 'applications/files/engine/PhabricatorS3FileStorageEngine.php', 'PhabricatorSQLPatchList' => 'infrastructure/storage/patch/PhabricatorSQLPatchList.php', 'PhabricatorScopedEnv' => 'infrastructure/PhabricatorScopedEnv.php', 'PhabricatorSearchAbstractDocument' => 'applications/search/index/PhabricatorSearchAbstractDocument.php', 'PhabricatorSearchAttachController' => 'applications/search/controller/PhabricatorSearchAttachController.php', 'PhabricatorSearchBaseController' => 'applications/search/controller/PhabricatorSearchBaseController.php', 'PhabricatorSearchCommitIndexer' => 'applications/search/index/indexer/PhabricatorSearchCommitIndexer.php', 'PhabricatorSearchController' => 'applications/search/controller/PhabricatorSearchController.php', 'PhabricatorSearchDAO' => 'applications/search/storage/PhabricatorSearchDAO.php', 'PhabricatorSearchDifferentialIndexer' => 'applications/search/index/indexer/PhabricatorSearchDifferentialIndexer.php', 'PhabricatorSearchDocument' => 'applications/search/storage/document/PhabricatorSearchDocument.php', 'PhabricatorSearchDocumentField' => 'applications/search/storage/document/PhabricatorSearchDocumentField.php', 'PhabricatorSearchDocumentIndexer' => 'applications/search/index/indexer/PhabricatorSearchDocumentIndexer.php', 'PhabricatorSearchDocumentRelationship' => 'applications/search/storage/document/PhabricatorSearchDocumentRelationship.php', 'PhabricatorSearchEngine' => 'applications/search/engine/PhabricatorSearchEngine.php', 'PhabricatorSearchEngineElastic' => 'applications/search/engine/PhabricatorSearchEngineElastic.php', 'PhabricatorSearchEngineMySQL' => 'applications/search/engine/PhabricatorSearchEngineMySQL.php', 'PhabricatorSearchEngineSelector' => 'applications/search/selector/PhabricatorSearchEngineSelector.php', 'PhabricatorSearchField' => 'applications/search/constants/PhabricatorSearchField.php', 'PhabricatorSearchIndexController' => 'applications/search/controller/PhabricatorSearchIndexController.php', 'PhabricatorSearchManiphestIndexer' => 'applications/search/index/indexer/PhabricatorSearchManiphestIndexer.php', 'PhabricatorSearchPhrictionIndexer' => 'applications/search/index/indexer/PhabricatorSearchPhrictionIndexer.php', 'PhabricatorSearchPonderIndexer' => 'applications/ponder/search/PhabricatorSearchPonderIndexer.php', 'PhabricatorSearchQuery' => 'applications/search/storage/PhabricatorSearchQuery.php', 'PhabricatorSearchRelationship' => 'applications/search/constants/PhabricatorSearchRelationship.php', 'PhabricatorSearchResultView' => 'applications/search/view/PhabricatorSearchResultView.php', 'PhabricatorSearchScope' => 'applications/search/constants/PhabricatorSearchScope.php', 'PhabricatorSearchSelectController' => 'applications/search/controller/PhabricatorSearchSelectController.php', 'PhabricatorSearchUserIndexer' => 'applications/search/index/indexer/PhabricatorSearchUserIndexer.php', 'PhabricatorSettingsAdjustController' => 'applications/settings/controller/PhabricatorSettingsAdjustController.php', 'PhabricatorSettingsMainController' => 'applications/settings/controller/PhabricatorSettingsMainController.php', 'PhabricatorSettingsPanel' => 'applications/settings/panel/PhabricatorSettingsPanel.php', 'PhabricatorSettingsPanelAccount' => 'applications/settings/panel/PhabricatorSettingsPanelAccount.php', 'PhabricatorSettingsPanelConduit' => 'applications/settings/panel/PhabricatorSettingsPanelConduit.php', 'PhabricatorSettingsPanelDisplayPreferences' => 'applications/settings/panel/PhabricatorSettingsPanelDisplayPreferences.php', 'PhabricatorSettingsPanelEmailAddresses' => 'applications/settings/panel/PhabricatorSettingsPanelEmailAddresses.php', 'PhabricatorSettingsPanelEmailPreferences' => 'applications/settings/panel/PhabricatorSettingsPanelEmailPreferences.php', 'PhabricatorSettingsPanelLDAP' => 'applications/settings/panel/PhabricatorSettingsPanelLDAP.php', 'PhabricatorSettingsPanelOAuth' => 'applications/settings/panel/PhabricatorSettingsPanelOAuth.php', 'PhabricatorSettingsPanelPassword' => 'applications/settings/panel/PhabricatorSettingsPanelPassword.php', 'PhabricatorSettingsPanelProfile' => 'applications/settings/panel/PhabricatorSettingsPanelProfile.php', 'PhabricatorSettingsPanelSSHKeys' => 'applications/settings/panel/PhabricatorSettingsPanelSSHKeys.php', 'PhabricatorSettingsPanelSearchPreferences' => 'applications/settings/panel/PhabricatorSettingsPanelSearchPreferences.php', 'PhabricatorSetup' => 'infrastructure/PhabricatorSetup.php', 'PhabricatorSlowvoteChoice' => 'applications/slowvote/storage/PhabricatorSlowvoteChoice.php', 'PhabricatorSlowvoteComment' => 'applications/slowvote/storage/PhabricatorSlowvoteComment.php', 'PhabricatorSlowvoteController' => 'applications/slowvote/controller/PhabricatorSlowvoteController.php', 'PhabricatorSlowvoteCreateController' => 'applications/slowvote/controller/PhabricatorSlowvoteCreateController.php', 'PhabricatorSlowvoteDAO' => 'applications/slowvote/storage/PhabricatorSlowvoteDAO.php', 'PhabricatorSlowvoteListController' => 'applications/slowvote/controller/PhabricatorSlowvoteListController.php', 'PhabricatorSlowvoteOption' => 'applications/slowvote/storage/PhabricatorSlowvoteOption.php', 'PhabricatorSlowvotePoll' => 'applications/slowvote/storage/PhabricatorSlowvotePoll.php', 'PhabricatorSlowvotePollController' => 'applications/slowvote/controller/PhabricatorSlowvotePollController.php', 'PhabricatorSlug' => 'infrastructure/util/PhabricatorSlug.php', 'PhabricatorSlugTestCase' => 'infrastructure/util/__tests__/PhabricatorSlugTestCase.php', 'PhabricatorSortTableExample' => 'applications/uiexample/examples/PhabricatorSortTableExample.php', 'PhabricatorSourceCodeView' => 'view/layout/PhabricatorSourceCodeView.php', 'PhabricatorStandardPageView' => 'view/page/PhabricatorStandardPageView.php', 'PhabricatorStatusController' => 'applications/status/PhabricatorStatusController.php', 'PhabricatorStorageFixtureScopeGuard' => 'infrastructure/testing/fixture/PhabricatorStorageFixtureScopeGuard.php', 'PhabricatorStorageManagementAPI' => 'infrastructure/storage/management/PhabricatorStorageManagementAPI.php', 'PhabricatorStorageManagementDatabasesWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementDatabasesWorkflow.php', 'PhabricatorStorageManagementDestroyWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php', 'PhabricatorStorageManagementDumpWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementDumpWorkflow.php', 'PhabricatorStorageManagementStatusWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementStatusWorkflow.php', 'PhabricatorStorageManagementUpgradeWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementUpgradeWorkflow.php', 'PhabricatorStorageManagementWorkflow' => 'infrastructure/storage/management/workflow/PhabricatorStorageManagementWorkflow.php', 'PhabricatorStoragePatch' => 'infrastructure/storage/management/PhabricatorStoragePatch.php', 'PhabricatorSubscribableInterface' => 'applications/subscriptions/interface/PhabricatorSubscribableInterface.php', 'PhabricatorSubscribersQuery' => 'applications/subscriptions/query/PhabricatorSubscribersQuery.php', 'PhabricatorSubscriptionsEditController' => 'applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php', 'PhabricatorSubscriptionsEditor' => 'applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php', 'PhabricatorSubscriptionsUIEventListener' => 'applications/subscriptions/events/PhabricatorSubscriptionsUIEventListener.php', 'PhabricatorSymbolNameLinter' => 'infrastructure/lint/hook/PhabricatorSymbolNameLinter.php', 'PhabricatorSyntaxHighlighter' => 'infrastructure/markup/PhabricatorSyntaxHighlighter.php', 'PhabricatorTaskmasterDaemon' => 'infrastructure/daemon/workers/PhabricatorTaskmasterDaemon.php', 'PhabricatorTestCase' => 'infrastructure/testing/PhabricatorTestCase.php', 'PhabricatorTimelineCursor' => 'infrastructure/daemon/timeline/storage/PhabricatorTimelineCursor.php', 'PhabricatorTimelineDAO' => 'infrastructure/daemon/timeline/storage/PhabricatorTimelineDAO.php', 'PhabricatorTimelineEvent' => 'infrastructure/daemon/timeline/storage/PhabricatorTimelineEvent.php', 'PhabricatorTimelineEventData' => 'infrastructure/daemon/timeline/storage/PhabricatorTimelineEventData.php', 'PhabricatorTimelineIterator' => 'infrastructure/daemon/timeline/cursor/PhabricatorTimelineIterator.php', 'PhabricatorTimer' => 'applications/countdown/storage/PhabricatorTimer.php', 'PhabricatorTransactionView' => 'view/layout/PhabricatorTransactionView.php', 'PhabricatorTransformedFile' => 'applications/files/storage/PhabricatorTransformedFile.php', 'PhabricatorTranslation' => 'infrastructure/internationalization/PhabricatorTranslation.php', 'PhabricatorTrivialTestCase' => 'infrastructure/testing/__tests__/PhabricatorTrivialTestCase.php', 'PhabricatorTypeaheadCommonDatasourceController' => 'applications/typeahead/controller/PhabricatorTypeaheadCommonDatasourceController.php', 'PhabricatorTypeaheadDatasourceController' => 'applications/typeahead/controller/PhabricatorTypeaheadDatasourceController.php', 'PhabricatorTypeaheadResult' => 'applications/typeahead/storage/PhabricatorTypeaheadResult.php', 'PhabricatorUIExample' => 'applications/uiexample/examples/PhabricatorUIExample.php', 'PhabricatorUIExampleRenderController' => 'applications/uiexample/controller/PhabricatorUIExampleRenderController.php', 'PhabricatorUIListFilterExample' => 'applications/uiexample/examples/PhabricatorUIListFilterExample.php', 'PhabricatorUINotificationExample' => 'applications/uiexample/examples/PhabricatorUINotificationExample.php', 'PhabricatorUIPagerExample' => 'applications/uiexample/examples/PhabricatorUIPagerExample.php', 'PhabricatorUITooltipExample' => 'applications/uiexample/examples/PhabricatorUITooltipExample.php', 'PhabricatorUnitsTestCase' => 'view/__tests__/PhabricatorUnitsTestCase.php', 'PhabricatorUser' => 'applications/people/storage/PhabricatorUser.php', 'PhabricatorUserDAO' => 'applications/people/storage/PhabricatorUserDAO.php', 'PhabricatorUserEditor' => 'applications/people/PhabricatorUserEditor.php', 'PhabricatorUserEmail' => 'applications/people/storage/PhabricatorUserEmail.php', 'PhabricatorUserLDAPInfo' => 'applications/people/storage/PhabricatorUserLDAPInfo.php', 'PhabricatorUserLog' => 'applications/people/storage/PhabricatorUserLog.php', 'PhabricatorUserOAuthInfo' => 'applications/people/storage/PhabricatorUserOAuthInfo.php', 'PhabricatorUserPreferences' => 'applications/settings/storage/PhabricatorUserPreferences.php', 'PhabricatorUserProfile' => 'applications/people/storage/PhabricatorUserProfile.php', 'PhabricatorUserSSHKey' => 'applications/settings/storage/PhabricatorUserSSHKey.php', 'PhabricatorUserStatus' => 'applications/people/storage/PhabricatorUserStatus.php', 'PhabricatorUserTestCase' => 'applications/people/storage/__tests__/PhabricatorUserTestCase.php', 'PhabricatorWorker' => 'infrastructure/daemon/workers/PhabricatorWorker.php', 'PhabricatorWorkerDAO' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerDAO.php', 'PhabricatorWorkerTask' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTask.php', 'PhabricatorWorkerTaskData' => 'infrastructure/daemon/workers/storage/PhabricatorWorkerTaskData.php', 'PhabricatorWorkerTaskDetailController' => 'applications/daemon/controller/PhabricatorWorkerTaskDetailController.php', 'PhabricatorWorkerTaskUpdateController' => 'applications/daemon/controller/PhabricatorWorkerTaskUpdateController.php', 'PhabricatorXHPASTViewController' => 'applications/xhpastview/controller/PhabricatorXHPASTViewController.php', 'PhabricatorXHPASTViewDAO' => 'applications/xhpastview/storage/PhabricatorXHPASTViewDAO.php', 'PhabricatorXHPASTViewFrameController' => 'applications/xhpastview/controller/PhabricatorXHPASTViewFrameController.php', 'PhabricatorXHPASTViewFramesetController' => 'applications/xhpastview/controller/PhabricatorXHPASTViewFramesetController.php', 'PhabricatorXHPASTViewInputController' => 'applications/xhpastview/controller/PhabricatorXHPASTViewInputController.php', 'PhabricatorXHPASTViewPanelController' => 'applications/xhpastview/controller/PhabricatorXHPASTViewPanelController.php', 'PhabricatorXHPASTViewParseTree' => 'applications/xhpastview/storage/PhabricatorXHPASTViewParseTree.php', 'PhabricatorXHPASTViewRunController' => 'applications/xhpastview/controller/PhabricatorXHPASTViewRunController.php', 'PhabricatorXHPASTViewStreamController' => 'applications/xhpastview/controller/PhabricatorXHPASTViewStreamController.php', 'PhabricatorXHPASTViewTreeController' => 'applications/xhpastview/controller/PhabricatorXHPASTViewTreeController.php', 'PhabricatorXHProfController' => 'applications/xhprof/controller/PhabricatorXHProfController.php', 'PhabricatorXHProfDAO' => 'applications/xhprof/storage/PhabricatorXHProfDAO.php', 'PhabricatorXHProfProfileController' => 'applications/xhprof/controller/PhabricatorXHProfProfileController.php', 'PhabricatorXHProfProfileSymbolView' => 'applications/xhprof/view/PhabricatorXHProfProfileSymbolView.php', 'PhabricatorXHProfProfileTopLevelView' => 'applications/xhprof/view/PhabricatorXHProfProfileTopLevelView.php', 'PhabricatorXHProfProfileView' => 'applications/xhprof/view/PhabricatorXHProfProfileView.php', 'PhabricatorXHProfSample' => 'applications/xhprof/storage/PhabricatorXHProfSample.php', 'PhabricatorXHProfSampleListController' => 'applications/xhprof/controller/PhabricatorXHProfSampleListController.php', 'PhabricatorXHProfSampleListView' => 'applications/xhprof/view/PhabricatorXHProfSampleListView.php', 'PhameAllBlogListController' => 'applications/phame/controller/blog/list/PhameAllBlogListController.php', 'PhameAllPostListController' => 'applications/phame/controller/post/list/PhameAllPostListController.php', 'PhameBlog' => 'applications/phame/storage/PhameBlog.php', 'PhameBlogDeleteController' => 'applications/phame/controller/blog/PhameBlogDeleteController.php', 'PhameBlogDetailView' => 'applications/phame/view/PhameBlogDetailView.php', 'PhameBlogEditController' => 'applications/phame/controller/blog/PhameBlogEditController.php', 'PhameBlogListBaseController' => 'applications/phame/controller/blog/list/PhameBlogListBaseController.php', 'PhameBlogListView' => 'applications/phame/view/PhameBlogListView.php', 'PhameBlogQuery' => 'applications/phame/query/PhameBlogQuery.php', 'PhameBlogViewController' => 'applications/phame/controller/blog/PhameBlogViewController.php', 'PhameBloggerPostListController' => 'applications/phame/controller/post/list/PhameBloggerPostListController.php', 'PhameController' => 'applications/phame/controller/PhameController.php', 'PhameDAO' => 'applications/phame/storage/PhameDAO.php', 'PhameDraftListController' => 'applications/phame/controller/post/list/PhameDraftListController.php', 'PhamePost' => 'applications/phame/storage/PhamePost.php', 'PhamePostDeleteController' => 'applications/phame/controller/post/PhamePostDeleteController.php', 'PhamePostDetailView' => 'applications/phame/view/PhamePostDetailView.php', 'PhamePostEditController' => 'applications/phame/controller/post/PhamePostEditController.php', 'PhamePostListBaseController' => 'applications/phame/controller/post/list/PhamePostListBaseController.php', 'PhamePostListView' => 'applications/phame/view/PhamePostListView.php', 'PhamePostPreviewController' => 'applications/phame/controller/post/PhamePostPreviewController.php', 'PhamePostQuery' => 'applications/phame/query/PhamePostQuery.php', 'PhamePostViewController' => 'applications/phame/controller/post/PhamePostViewController.php', 'PhameUserBlogListController' => 'applications/phame/controller/blog/list/PhameUserBlogListController.php', 'PhameUserPostListController' => 'applications/phame/controller/post/list/PhameUserPostListController.php', 'PhortuneMonthYearExpiryControl' => 'applications/phortune/control/PhortuneMonthYearExpiryControl.php', 'PhortuneStripeBaseController' => 'applications/phortune/stripe/controller/PhortuneStripeBaseController.php', 'PhortuneStripePaymentFormView' => 'applications/phortune/stripe/view/PhortuneStripePaymentFormView.php', 'PhortuneStripeTestPaymentFormController' => 'applications/phortune/stripe/controller/PhortuneStripeTestPaymentFormController.php', 'PhrictionActionConstants' => 'applications/phriction/constants/PhrictionActionConstants.php', 'PhrictionChangeType' => 'applications/phriction/constants/PhrictionChangeType.php', 'PhrictionConstants' => 'applications/phriction/constants/PhrictionConstants.php', 'PhrictionContent' => 'applications/phriction/storage/PhrictionContent.php', 'PhrictionController' => 'applications/phriction/controller/PhrictionController.php', 'PhrictionDAO' => 'applications/phriction/storage/PhrictionDAO.php', 'PhrictionDeleteController' => 'applications/phriction/controller/PhrictionDeleteController.php', 'PhrictionDiffController' => 'applications/phriction/controller/PhrictionDiffController.php', 'PhrictionDocument' => 'applications/phriction/storage/PhrictionDocument.php', 'PhrictionDocumentController' => 'applications/phriction/controller/PhrictionDocumentController.php', 'PhrictionDocumentEditor' => 'applications/phriction/editor/PhrictionDocumentEditor.php', 'PhrictionDocumentPreviewController' => 'applications/phriction/controller/PhrictionDocumentPreviewController.php', 'PhrictionDocumentStatus' => 'applications/phriction/constants/PhrictionDocumentStatus.php', 'PhrictionDocumentTestCase' => 'applications/phriction/storage/__tests__/PhrictionDocumentTestCase.php', 'PhrictionEditController' => 'applications/phriction/controller/PhrictionEditController.php', 'PhrictionHistoryController' => 'applications/phriction/controller/PhrictionHistoryController.php', 'PhrictionListController' => 'applications/phriction/controller/PhrictionListController.php', 'PonderAddAnswerView' => 'applications/ponder/view/PonderAddAnswerView.php', 'PonderAddCommentView' => 'applications/ponder/view/PonderAddCommentView.php', 'PonderAnswer' => 'applications/ponder/storage/PonderAnswer.php', 'PonderAnswerEditor' => 'applications/ponder/editor/PonderAnswerEditor.php', 'PonderAnswerListView' => 'applications/ponder/view/PonderAnswerListView.php', 'PonderAnswerPreviewController' => 'applications/ponder/controller/PonderAnswerPreviewController.php', 'PonderAnswerQuery' => 'applications/ponder/query/PonderAnswerQuery.php', 'PonderAnswerSaveController' => 'applications/ponder/controller/PonderAnswerSaveController.php', 'PonderAnswerViewController' => 'applications/ponder/controller/PonderAnswerViewController.php', 'PonderAnsweredMail' => 'applications/ponder/mail/PonderAnsweredMail.php', 'PonderComment' => 'applications/ponder/storage/PonderComment.php', 'PonderCommentEditor' => 'applications/ponder/editor/PonderCommentEditor.php', 'PonderCommentListView' => 'applications/ponder/view/PonderCommentListView.php', 'PonderCommentMail' => 'applications/ponder/mail/PonderCommentMail.php', 'PonderCommentQuery' => 'applications/ponder/query/PonderCommentQuery.php', 'PonderCommentSaveController' => 'applications/ponder/controller/PonderCommentSaveController.php', 'PonderConstants' => 'applications/ponder/PonderConstants.php', 'PonderController' => 'applications/ponder/controller/PonderController.php', 'PonderDAO' => 'applications/ponder/storage/PonderDAO.php', 'PonderFeedController' => 'applications/ponder/controller/PonderFeedController.php', 'PonderMail' => 'applications/ponder/mail/PonderMail.php', 'PonderMentionMail' => 'applications/ponder/mail/PonderMentionMail.php', 'PonderPostBodyView' => 'applications/ponder/view/PonderPostBodyView.php', 'PonderQuestion' => 'applications/ponder/storage/PonderQuestion.php', 'PonderQuestionAskController' => 'applications/ponder/controller/PonderQuestionAskController.php', 'PonderQuestionDetailView' => 'applications/ponder/view/PonderQuestionDetailView.php', 'PonderQuestionEditor' => 'applications/ponder/editor/PonderQuestionEditor.php', 'PonderQuestionPreviewController' => 'applications/ponder/controller/PonderQuestionPreviewController.php', 'PonderQuestionQuery' => 'applications/ponder/query/PonderQuestionQuery.php', 'PonderQuestionSummaryView' => 'applications/ponder/view/PonderQuestionSummaryView.php', 'PonderQuestionViewController' => 'applications/ponder/controller/PonderQuestionViewController.php', 'PonderReplyHandler' => 'applications/ponder/PonderReplyHandler.php', 'PonderRuleQuestion' => 'infrastructure/markup/rule/PonderRuleQuestion.php', 'PonderUserProfileView' => 'applications/ponder/view/PonderUserProfileView.php', 'PonderVotableInterface' => 'applications/ponder/storage/PonderVotableInterface.php', 'PonderVotableView' => 'applications/ponder/view/PonderVotableView.php', 'PonderVoteEditor' => 'applications/ponder/editor/PonderVoteEditor.php', 'PonderVoteSaveController' => 'applications/ponder/controller/PonderVoteSaveController.php', 'QueryFormattingTestCase' => 'infrastructure/storage/__tests__/QueryFormattingTestCase.php', ), 'function' => array( '_phabricator_date_format' => 'view/viewutils.php', 'celerity_generate_unique_node_id' => 'infrastructure/celerity/api.php', 'celerity_get_resource_uri' => 'infrastructure/celerity/api.php', 'celerity_register_resource_map' => 'infrastructure/celerity/map.php', 'javelin_render_tag' => 'infrastructure/javelin/markup.php', 'phabricator_date' => 'view/viewutils.php', 'phabricator_datetime' => 'view/viewutils.php', 'phabricator_format_bytes' => 'view/viewutils.php', 'phabricator_format_local_time' => 'view/viewutils.php', 'phabricator_format_relative_time' => 'view/viewutils.php', 'phabricator_format_relative_time_detailed' => 'view/viewutils.php', 'phabricator_format_units_generic' => 'view/viewutils.php', 'phabricator_on_relative_date' => 'view/viewutils.php', 'phabricator_parse_bytes' => 'view/viewutils.php', 'phabricator_relative_date' => 'view/viewutils.php', 'phabricator_render_form' => 'infrastructure/javelin/markup.php', 'phabricator_time' => 'view/viewutils.php', 'phid_get_type' => 'applications/phid/utils.php', 'phid_group_by_type' => 'applications/phid/utils.php', 'require_celerity_resource' => 'infrastructure/celerity/api.php', ), 'xmap' => array( 'Aphront304Response' => 'AphrontResponse', 'Aphront400Response' => 'AphrontResponse', 'Aphront403Response' => 'AphrontWebpageResponse', 'Aphront404Response' => 'AphrontWebpageResponse', 'AphrontAjaxResponse' => 'AphrontResponse', 'AphrontAttachedFileView' => 'AphrontView', 'AphrontCSRFException' => 'AphrontException', 'AphrontCalendarEventView' => 'AphrontView', 'AphrontCalendarMonthView' => 'AphrontView', 'AphrontContextBarView' => 'AphrontView', 'AphrontCrumbsView' => 'AphrontView', 'AphrontCursorPagerView' => 'AphrontView', 'AphrontDefaultApplicationConfiguration' => 'AphrontApplicationConfiguration', 'AphrontDialogResponse' => 'AphrontResponse', 'AphrontDialogView' => 'AphrontView', 'AphrontErrorView' => 'AphrontView', 'AphrontException' => 'Exception', 'AphrontFilePreviewView' => 'AphrontView', 'AphrontFileResponse' => 'AphrontResponse', 'AphrontFormCheckboxControl' => 'AphrontFormControl', 'AphrontFormControl' => 'AphrontView', 'AphrontFormDateControl' => 'AphrontFormControl', 'AphrontFormDividerControl' => 'AphrontFormControl', 'AphrontFormDragAndDropUploadControl' => 'AphrontFormControl', 'AphrontFormFileControl' => 'AphrontFormControl', 'AphrontFormImageControl' => 'AphrontFormControl', 'AphrontFormInsetView' => 'AphrontView', 'AphrontFormLayoutView' => 'AphrontView', 'AphrontFormMarkupControl' => 'AphrontFormControl', 'AphrontFormPasswordControl' => 'AphrontFormControl', 'AphrontFormPolicyControl' => 'AphrontFormControl', 'AphrontFormRadioButtonControl' => 'AphrontFormControl', 'AphrontFormRecaptchaControl' => 'AphrontFormControl', 'AphrontFormSelectControl' => 'AphrontFormControl', 'AphrontFormStaticControl' => 'AphrontFormControl', 'AphrontFormSubmitControl' => 'AphrontFormControl', 'AphrontFormTextAreaControl' => 'AphrontFormControl', 'AphrontFormTextControl' => 'AphrontFormControl', 'AphrontFormToggleButtonsControl' => 'AphrontFormControl', 'AphrontFormTokenizerControl' => 'AphrontFormControl', 'AphrontFormView' => 'AphrontView', 'AphrontHTTPSinkTestCase' => 'PhabricatorTestCase', 'AphrontHeadsupActionListView' => 'AphrontView', 'AphrontHeadsupActionView' => 'AphrontView', 'AphrontHeadsupView' => 'AphrontView', 'AphrontIsolatedDatabaseConnectionTestCase' => 'PhabricatorTestCase', 'AphrontIsolatedHTTPSink' => 'AphrontHTTPSink', 'AphrontJSONResponse' => 'AphrontResponse', 'AphrontJavelinView' => 'AphrontView', 'AphrontKeyboardShortcutsAvailableView' => 'AphrontView', 'AphrontListFilterView' => 'AphrontView', 'AphrontMiniPanelView' => 'AphrontView', 'AphrontMoreView' => 'AphrontView', 'AphrontMySQLDatabaseConnectionTestCase' => 'PhabricatorTestCase', 'AphrontNullView' => 'AphrontView', 'AphrontPHPHTTPSink' => 'AphrontHTTPSink', 'AphrontPageView' => 'AphrontView', 'AphrontPagerView' => 'AphrontView', 'AphrontPanelView' => 'AphrontView', 'AphrontPlainTextResponse' => 'AphrontResponse', 'AphrontProxyResponse' => 'AphrontResponse', 'AphrontRedirectException' => 'AphrontException', 'AphrontRedirectResponse' => 'AphrontResponse', 'AphrontReloadResponse' => 'AphrontRedirectResponse', 'AphrontRequestFailureView' => 'AphrontView', 'AphrontRequestTestCase' => 'PhabricatorTestCase', 'AphrontSideNavFilterView' => 'AphrontView', 'AphrontSideNavView' => 'AphrontView', 'AphrontTableView' => 'AphrontView', 'AphrontTokenizerTemplateView' => 'AphrontView', 'AphrontTypeaheadTemplateView' => 'AphrontView', 'AphrontUsageException' => 'AphrontException', 'AphrontWebpageResponse' => 'AphrontResponse', 'CelerityResourceController' => 'PhabricatorController', 'CelerityResourceGraph' => 'AbstractDirectedGraph', 'CelerityResourceTransformerTestCase' => 'PhabricatorTestCase', 'ConduitAPI_arcanist_Method' => 'ConduitAPIMethod', 'ConduitAPI_arcanist_projectinfo_Method' => 'ConduitAPI_arcanist_Method', 'ConduitAPI_audit_Method' => 'ConduitAPIMethod', 'ConduitAPI_audit_query_Method' => 'ConduitAPI_audit_Method', 'ConduitAPI_chatlog_Method' => 'ConduitAPIMethod', 'ConduitAPI_chatlog_query_Method' => 'ConduitAPI_chatlog_Method', 'ConduitAPI_chatlog_record_Method' => 'ConduitAPI_chatlog_Method', 'ConduitAPI_conduit_connect_Method' => 'ConduitAPIMethod', 'ConduitAPI_conduit_getcertificate_Method' => 'ConduitAPIMethod', 'ConduitAPI_conduit_ping_Method' => 'ConduitAPIMethod', 'ConduitAPI_daemon_launched_Method' => 'ConduitAPIMethod', 'ConduitAPI_daemon_log_Method' => 'ConduitAPIMethod', 'ConduitAPI_daemon_setstatus_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_close_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_createcomment_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_creatediff_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_createinline_Method' => 'ConduitAPI_differential_Method', 'ConduitAPI_differential_createrawdiff_Method' => 'ConduitAPI_differential_Method', 'ConduitAPI_differential_createrevision_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_find_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_finishpostponedlinters_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_getalldiffs_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_getcommitmessage_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_getcommitpaths_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_getdiff_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_getrevision_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_getrevisioncomments_Method' => 'ConduitAPI_differential_Method', 'ConduitAPI_differential_getrevisionfeedback_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_markcommitted_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_parsecommitmessage_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_query_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_setdiffproperty_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_updaterevision_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_updatetaskrevisionassoc_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_updateunitresults_Method' => 'ConduitAPIMethod', 'ConduitAPI_diffusion_findsymbols_Method' => 'ConduitAPIMethod', 'ConduitAPI_diffusion_getcommits_Method' => 'ConduitAPIMethod', 'ConduitAPI_diffusion_getrecentcommitsbypath_Method' => 'ConduitAPIMethod', 'ConduitAPI_feed_publish_Method' => 'ConduitAPIMethod', 'ConduitAPI_feed_query_Method' => 'ConduitAPIMethod', 'ConduitAPI_file_download_Method' => 'ConduitAPIMethod', 'ConduitAPI_file_info_Method' => 'ConduitAPIMethod', 'ConduitAPI_file_upload_Method' => 'ConduitAPIMethod', 'ConduitAPI_flag_Method' => 'ConduitAPIMethod', 'ConduitAPI_flag_delete_Method' => 'ConduitAPI_flag_Method', 'ConduitAPI_flag_edit_Method' => 'ConduitAPI_flag_Method', 'ConduitAPI_flag_query_Method' => 'ConduitAPI_flag_Method', 'ConduitAPI_macro_Method' => 'ConduitAPIMethod', 'ConduitAPI_macro_query_Method' => 'ConduitAPI_macro_Method', 'ConduitAPI_maniphest_Method' => 'ConduitAPIMethod', 'ConduitAPI_maniphest_createtask_Method' => 'ConduitAPI_maniphest_Method', 'ConduitAPI_maniphest_find_Method' => 'ConduitAPI_maniphest_query_Method', 'ConduitAPI_maniphest_gettasktransactions_Method' => 'ConduitAPI_maniphest_Method', 'ConduitAPI_maniphest_info_Method' => 'ConduitAPI_maniphest_Method', 'ConduitAPI_maniphest_query_Method' => 'ConduitAPI_maniphest_Method', 'ConduitAPI_maniphest_update_Method' => 'ConduitAPI_maniphest_Method', 'ConduitAPI_owners_query_Method' => 'ConduitAPIMethod', 'ConduitAPI_paste_Method' => 'ConduitAPIMethod', 'ConduitAPI_paste_create_Method' => 'ConduitAPI_paste_Method', 'ConduitAPI_paste_info_Method' => 'ConduitAPI_paste_Method', 'ConduitAPI_paste_query_Method' => 'ConduitAPI_paste_Method', 'ConduitAPI_phid_Method' => 'ConduitAPIMethod', 'ConduitAPI_phid_info_Method' => 'ConduitAPI_phid_Method', 'ConduitAPI_phid_lookup_Method' => 'ConduitAPI_phid_Method', 'ConduitAPI_phid_query_Method' => 'ConduitAPI_phid_Method', 'ConduitAPI_phpast_getast_Method' => 'ConduitAPIMethod', 'ConduitAPI_phpast_version_Method' => 'ConduitAPIMethod', 'ConduitAPI_phriction_Method' => 'ConduitAPIMethod', 'ConduitAPI_phriction_edit_Method' => 'ConduitAPI_phriction_Method', 'ConduitAPI_phriction_history_Method' => 'ConduitAPI_phriction_Method', 'ConduitAPI_phriction_info_Method' => 'ConduitAPI_phriction_Method', 'ConduitAPI_project_Method' => 'ConduitAPIMethod', 'ConduitAPI_project_query_Method' => 'ConduitAPI_project_Method', 'ConduitAPI_remarkup_process_Method' => 'ConduitAPIMethod', 'ConduitAPI_repository_Method' => 'ConduitAPIMethod', 'ConduitAPI_repository_create_Method' => 'ConduitAPI_repository_Method', 'ConduitAPI_repository_query_Method' => 'ConduitAPI_repository_Method', 'ConduitAPI_slowvote_info_Method' => 'ConduitAPIMethod', 'ConduitAPI_user_Method' => 'ConduitAPIMethod', 'ConduitAPI_user_addstatus_Method' => 'ConduitAPI_user_Method', 'ConduitAPI_user_disable_Method' => 'ConduitAPI_user_Method', 'ConduitAPI_user_enable_Method' => 'ConduitAPI_user_Method', 'ConduitAPI_user_find_Method' => 'ConduitAPI_user_Method', 'ConduitAPI_user_info_Method' => 'ConduitAPI_user_Method', 'ConduitAPI_user_query_Method' => 'ConduitAPI_user_Method', 'ConduitAPI_user_removestatus_Method' => 'ConduitAPI_user_Method', 'ConduitAPI_user_whoami_Method' => 'ConduitAPI_user_Method', 'ConduitCallTestCase' => 'PhabricatorTestCase', 'ConduitException' => 'Exception', 'DarkConsoleConfigPlugin' => 'DarkConsolePlugin', 'DarkConsoleController' => 'PhabricatorController', 'DarkConsoleErrorLogPlugin' => 'DarkConsolePlugin', 'DarkConsoleEventPlugin' => 'DarkConsolePlugin', 'DarkConsoleEventPluginAPI' => 'PhutilEventListener', 'DarkConsoleRequestPlugin' => 'DarkConsolePlugin', 'DarkConsoleServicesPlugin' => 'DarkConsolePlugin', 'DarkConsoleXHProfPlugin' => 'DarkConsolePlugin', 'DefaultDatabaseConfigurationProvider' => 'DatabaseConfigurationProvider', 'DifferentialActionHasNoEffectException' => 'DifferentialException', 'DifferentialAddCommentView' => 'AphrontView', 'DifferentialAffectedPath' => 'DifferentialDAO', 'DifferentialApplyPatchFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialArcanistProjectFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialAuditorsFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialAuthorFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialAuxiliaryField' => 'DifferentialDAO', 'DifferentialBlameRevisionFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialBranchFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialCCWelcomeMail' => 'DifferentialReviewRequestMail', 'DifferentialCCsFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialChangeSetTestCase' => 'PhabricatorTestCase', 'DifferentialChangeset' => 'DifferentialDAO', 'DifferentialChangesetDetailView' => 'AphrontView', 'DifferentialChangesetListView' => 'AphrontView', 'DifferentialChangesetParserTestCase' => 'ArcanistPhutilTestCase', 'DifferentialChangesetViewController' => 'DifferentialController', 'DifferentialComment' => 'DifferentialDAO', + 'DifferentialCommentEditor' => 'PhabricatorEditor', 'DifferentialCommentMail' => 'DifferentialMail', 'DifferentialCommentPreviewController' => 'DifferentialController', 'DifferentialCommentSaveController' => 'DifferentialController', 'DifferentialCommitsFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialController' => 'PhabricatorController', 'DifferentialDAO' => 'PhabricatorLiskDAO', 'DifferentialDateCreatedFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialDateModifiedFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialDefaultFieldSelector' => 'DifferentialFieldSelector', 'DifferentialDependenciesFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialDependsOnFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialDiff' => 'DifferentialDAO', 'DifferentialDiffContentMail' => 'DifferentialMail', 'DifferentialDiffCreateController' => 'DifferentialController', 'DifferentialDiffProperty' => 'DifferentialDAO', 'DifferentialDiffTableOfContentsView' => 'AphrontView', 'DifferentialDiffViewController' => 'DifferentialController', 'DifferentialException' => 'Exception', 'DifferentialExceptionMail' => 'DifferentialMail', 'DifferentialExportPatchFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialFieldDataNotAvailableException' => 'Exception', 'DifferentialFieldParseException' => 'Exception', 'DifferentialFieldSpecificationIncompleteException' => 'Exception', 'DifferentialFieldValidationException' => 'Exception', 'DifferentialFreeformFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialGitSVNIDFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialHostFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialHunk' => 'DifferentialDAO', 'DifferentialHunkTestCase' => 'ArcanistPhutilTestCase', 'DifferentialInlineComment' => array( 0 => 'DifferentialDAO', 1 => 'PhabricatorInlineCommentInterface', ), 'DifferentialInlineCommentEditController' => 'PhabricatorInlineCommentController', 'DifferentialInlineCommentEditView' => 'AphrontView', 'DifferentialInlineCommentPreviewController' => 'PhabricatorInlineCommentPreviewController', 'DifferentialInlineCommentView' => 'AphrontView', 'DifferentialLinesFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialLintFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialLocalCommitsView' => 'AphrontView', 'DifferentialManiphestTasksFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialNewDiffMail' => 'DifferentialReviewRequestMail', 'DifferentialPathFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialPrimaryPaneView' => 'AphrontView', 'DifferentialReplyHandler' => 'PhabricatorMailReplyHandler', 'DifferentialResultsTableView' => 'AphrontView', 'DifferentialRevertPlanFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialReviewRequestMail' => 'DifferentialMail', 'DifferentialReviewedByFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialReviewerStatsTestCase' => 'PhabricatorTestCase', 'DifferentialReviewersFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialRevision' => 'DifferentialDAO', 'DifferentialRevisionCommentListView' => 'AphrontView', 'DifferentialRevisionCommentView' => 'AphrontView', 'DifferentialRevisionDetailView' => 'AphrontView', 'DifferentialRevisionEditController' => 'DifferentialController', + 'DifferentialRevisionEditor' => 'PhabricatorEditor', 'DifferentialRevisionIDFieldParserTestCase' => 'PhabricatorTestCase', 'DifferentialRevisionIDFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialRevisionListController' => 'DifferentialController', 'DifferentialRevisionListView' => 'AphrontView', 'DifferentialRevisionStatsController' => 'DifferentialController', 'DifferentialRevisionStatsView' => 'AphrontView', 'DifferentialRevisionStatusFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialRevisionUpdateHistoryView' => 'AphrontView', 'DifferentialRevisionViewController' => 'DifferentialController', 'DifferentialSubscribeController' => 'DifferentialController', 'DifferentialSummaryFieldSpecification' => 'DifferentialFreeformFieldSpecification', 'DifferentialTestPlanFieldSpecification' => 'DifferentialFieldSpecification', 'DifferentialTitleFieldSpecification' => 'DifferentialFreeformFieldSpecification', 'DifferentialUnitFieldSpecification' => 'DifferentialFieldSpecification', 'DiffusionBranchTableController' => 'DiffusionController', 'DiffusionBranchTableView' => 'DiffusionView', 'DiffusionBrowseController' => 'DiffusionController', 'DiffusionBrowseFileController' => 'DiffusionController', 'DiffusionBrowseTableView' => 'DiffusionView', 'DiffusionChangeController' => 'DiffusionController', 'DiffusionCommentListView' => 'AphrontView', 'DiffusionCommentView' => 'AphrontView', 'DiffusionCommitBranchesController' => 'DiffusionController', 'DiffusionCommitChangeTableView' => 'DiffusionView', 'DiffusionCommitController' => 'DiffusionController', 'DiffusionCommitEditController' => 'DiffusionController', 'DiffusionCommitParentsQuery' => 'DiffusionQuery', 'DiffusionCommitTagsController' => 'DiffusionController', 'DiffusionCommitTagsQuery' => 'DiffusionQuery', 'DiffusionContainsQuery' => 'DiffusionQuery', 'DiffusionController' => 'PhabricatorController', 'DiffusionDiffController' => 'DiffusionController', 'DiffusionDiffQuery' => 'DiffusionQuery', 'DiffusionEmptyResultView' => 'DiffusionView', 'DiffusionExistsQuery' => 'DiffusionQuery', 'DiffusionExternalController' => 'DiffusionController', 'DiffusionFileContentQuery' => 'DiffusionQuery', 'DiffusionGitBranchQuery' => 'DiffusionBranchQuery', 'DiffusionGitBranchQueryTestCase' => 'PhabricatorTestCase', 'DiffusionGitBrowseQuery' => 'DiffusionBrowseQuery', 'DiffusionGitCommitParentsQuery' => 'DiffusionCommitParentsQuery', 'DiffusionGitCommitTagsQuery' => 'DiffusionCommitTagsQuery', 'DiffusionGitContainsQuery' => 'DiffusionContainsQuery', 'DiffusionGitDiffQuery' => 'DiffusionDiffQuery', 'DiffusionGitExistsQuery' => 'DiffusionExistsQuery', 'DiffusionGitFileContentQuery' => 'DiffusionFileContentQuery', 'DiffusionGitHistoryQuery' => 'DiffusionHistoryQuery', 'DiffusionGitLastModifiedQuery' => 'DiffusionLastModifiedQuery', 'DiffusionGitMergedCommitsQuery' => 'DiffusionMergedCommitsQuery', 'DiffusionGitRawDiffQuery' => 'DiffusionRawDiffQuery', 'DiffusionGitRequest' => 'DiffusionRequest', 'DiffusionGitTagListQuery' => 'DiffusionTagListQuery', 'DiffusionHistoryController' => 'DiffusionController', 'DiffusionHistoryQuery' => 'DiffusionQuery', 'DiffusionHistoryTableView' => 'DiffusionView', 'DiffusionHomeController' => 'DiffusionController', 'DiffusionInlineCommentController' => 'PhabricatorInlineCommentController', 'DiffusionInlineCommentPreviewController' => 'PhabricatorInlineCommentPreviewController', 'DiffusionLastModifiedController' => 'DiffusionController', 'DiffusionLastModifiedQuery' => 'DiffusionQuery', 'DiffusionMercurialBranchQuery' => 'DiffusionBranchQuery', 'DiffusionMercurialBrowseQuery' => 'DiffusionBrowseQuery', 'DiffusionMercurialCommitParentsQuery' => 'DiffusionCommitParentsQuery', 'DiffusionMercurialCommitTagsQuery' => 'DiffusionCommitTagsQuery', 'DiffusionMercurialContainsQuery' => 'DiffusionContainsQuery', 'DiffusionMercurialDiffQuery' => 'DiffusionDiffQuery', 'DiffusionMercurialExistsQuery' => 'DiffusionExistsQuery', 'DiffusionMercurialFileContentQuery' => 'DiffusionFileContentQuery', 'DiffusionMercurialHistoryQuery' => 'DiffusionHistoryQuery', 'DiffusionMercurialLastModifiedQuery' => 'DiffusionLastModifiedQuery', 'DiffusionMercurialMergedCommitsQuery' => 'DiffusionMergedCommitsQuery', 'DiffusionMercurialRawDiffQuery' => 'DiffusionRawDiffQuery', 'DiffusionMercurialRequest' => 'DiffusionRequest', 'DiffusionMercurialTagListQuery' => 'DiffusionTagListQuery', 'DiffusionMergedCommitsQuery' => 'DiffusionQuery', 'DiffusionPathCompleteController' => 'DiffusionController', 'DiffusionPathQueryTestCase' => 'PhabricatorTestCase', 'DiffusionPathValidateController' => 'DiffusionController', 'DiffusionRawDiffQuery' => 'DiffusionQuery', 'DiffusionRepositoryController' => 'DiffusionController', 'DiffusionSetupException' => 'AphrontUsageException', 'DiffusionSvnBrowseQuery' => 'DiffusionBrowseQuery', 'DiffusionSvnCommitParentsQuery' => 'DiffusionCommitParentsQuery', 'DiffusionSvnCommitTagsQuery' => 'DiffusionCommitTagsQuery', 'DiffusionSvnContainsQuery' => 'DiffusionContainsQuery', 'DiffusionSvnDiffQuery' => 'DiffusionDiffQuery', 'DiffusionSvnExistsQuery' => 'DiffusionExistsQuery', 'DiffusionSvnFileContentQuery' => 'DiffusionFileContentQuery', 'DiffusionSvnHistoryQuery' => 'DiffusionHistoryQuery', 'DiffusionSvnLastModifiedQuery' => 'DiffusionLastModifiedQuery', 'DiffusionSvnMergedCommitsQuery' => 'DiffusionMergedCommitsQuery', 'DiffusionSvnRawDiffQuery' => 'DiffusionRawDiffQuery', 'DiffusionSvnRequest' => 'DiffusionRequest', 'DiffusionSvnTagListQuery' => 'DiffusionTagListQuery', 'DiffusionSymbolController' => 'DiffusionController', 'DiffusionSymbolQuery' => 'PhabricatorOffsetPagedQuery', 'DiffusionTagListController' => 'DiffusionController', 'DiffusionTagListQuery' => 'DiffusionQuery', 'DiffusionTagListView' => 'DiffusionView', 'DiffusionURITestCase' => 'ArcanistPhutilTestCase', 'DiffusionView' => 'AphrontView', 'DivinerListController' => 'PhabricatorController', 'DrydockAllocatorWorker' => 'PhabricatorWorker', 'DrydockApacheWebrootBlueprint' => 'DrydockBlueprint', 'DrydockApacheWebrootInterface' => 'DrydockWebrootInterface', 'DrydockCommandInterface' => 'DrydockInterface', 'DrydockController' => 'PhabricatorController', 'DrydockDAO' => 'PhabricatorLiskDAO', 'DrydockEC2HostBlueprint' => 'DrydockRemoteHostBlueprint', 'DrydockLease' => 'DrydockDAO', 'DrydockLeaseListController' => 'DrydockController', 'DrydockLeaseStatus' => 'DrydockConstants', 'DrydockLocalCommandInterface' => 'DrydockCommandInterface', 'DrydockLocalHostBlueprint' => 'DrydockBlueprint', 'DrydockLog' => 'DrydockDAO', 'DrydockLogController' => 'DrydockController', 'DrydockLogQuery' => 'PhabricatorOffsetPagedQuery', 'DrydockPhabricatorApplicationBlueprint' => 'DrydockBlueprint', 'DrydockRemoteHostBlueprint' => 'DrydockBlueprint', 'DrydockResource' => 'DrydockDAO', 'DrydockResourceAllocateController' => 'DrydockController', 'DrydockResourceListController' => 'DrydockController', 'DrydockResourceStatus' => 'DrydockConstants', 'DrydockSSHCommandInterface' => 'DrydockCommandInterface', 'DrydockWebrootInterface' => 'DrydockInterface', 'HarbormasterDAO' => 'PhabricatorLiskDAO', 'HarbormasterObject' => 'HarbormasterDAO', 'HarbormasterScratchTable' => 'HarbormasterDAO', 'HeraldAction' => 'HeraldDAO', 'HeraldApplyTranscript' => 'HeraldDAO', 'HeraldCommitAdapter' => 'HeraldObjectAdapter', 'HeraldCondition' => 'HeraldDAO', 'HeraldController' => 'PhabricatorController', 'HeraldDAO' => 'PhabricatorLiskDAO', 'HeraldDeleteController' => 'HeraldController', 'HeraldDifferentialRevisionAdapter' => 'HeraldObjectAdapter', 'HeraldDryRunAdapter' => 'HeraldObjectAdapter', 'HeraldEditLogQuery' => 'PhabricatorOffsetPagedQuery', 'HeraldHomeController' => 'HeraldController', 'HeraldInvalidConditionException' => 'Exception', 'HeraldInvalidFieldException' => 'Exception', 'HeraldNewController' => 'HeraldController', 'HeraldRecursiveConditionsException' => 'Exception', 'HeraldRule' => 'HeraldDAO', 'HeraldRuleController' => 'HeraldController', 'HeraldRuleEdit' => 'HeraldDAO', 'HeraldRuleEditHistoryController' => 'HeraldController', 'HeraldRuleEditHistoryView' => 'AphrontView', 'HeraldRuleListView' => 'AphrontView', 'HeraldRuleQuery' => 'PhabricatorOffsetPagedQuery', 'HeraldTestConsoleController' => 'HeraldController', 'HeraldTranscript' => 'HeraldDAO', 'HeraldTranscriptController' => 'HeraldController', 'HeraldTranscriptListController' => 'HeraldController', 'JavelinReactorExample' => 'PhabricatorUIExample', 'JavelinUIExample' => 'PhabricatorUIExample', 'JavelinViewExample' => 'PhabricatorUIExample', 'JavelinViewExampleServerView' => 'AphrontView', 'LiskEphemeralObjectException' => 'Exception', 'LiskFixtureTestCase' => 'PhabricatorTestCase', 'LiskIsolationTestCase' => 'PhabricatorTestCase', 'LiskIsolationTestDAO' => 'LiskDAO', 'LiskIsolationTestDAOException' => 'Exception', 'LiskMigrationIterator' => 'PhutilBufferedIterator', 'ManiphestAction' => 'ManiphestConstants', 'ManiphestAuxiliaryFieldDefaultSpecification' => 'ManiphestAuxiliaryFieldSpecification', 'ManiphestAuxiliaryFieldTypeException' => 'Exception', 'ManiphestAuxiliaryFieldValidationException' => 'Exception', 'ManiphestBatchEditController' => 'ManiphestController', 'ManiphestController' => 'PhabricatorController', 'ManiphestDAO' => 'PhabricatorLiskDAO', 'ManiphestDefaultTaskExtensions' => 'ManiphestTaskExtensions', 'ManiphestEdgeEventListener' => 'PhutilEventListener', 'ManiphestExportController' => 'ManiphestController', 'ManiphestReplyHandler' => 'PhabricatorMailReplyHandler', 'ManiphestReportController' => 'ManiphestController', 'ManiphestSavedQuery' => 'ManiphestDAO', 'ManiphestSavedQueryDeleteController' => 'ManiphestController', 'ManiphestSavedQueryEditController' => 'ManiphestController', 'ManiphestSavedQueryListController' => 'ManiphestController', 'ManiphestSubpriorityController' => 'ManiphestController', 'ManiphestTask' => array( 0 => 'ManiphestDAO', 1 => 'PhabricatorMarkupInterface', ), 'ManiphestTaskAuxiliaryStorage' => 'ManiphestDAO', 'ManiphestTaskDescriptionChangeController' => 'ManiphestController', 'ManiphestTaskDescriptionPreviewController' => 'ManiphestController', 'ManiphestTaskDetailController' => 'ManiphestController', 'ManiphestTaskEditController' => 'ManiphestController', 'ManiphestTaskListController' => 'ManiphestController', 'ManiphestTaskListView' => 'ManiphestView', 'ManiphestTaskOwner' => 'ManiphestConstants', 'ManiphestTaskPriority' => 'ManiphestConstants', 'ManiphestTaskProject' => 'ManiphestDAO', 'ManiphestTaskProjectsView' => 'ManiphestView', 'ManiphestTaskQuery' => 'PhabricatorQuery', 'ManiphestTaskStatus' => 'ManiphestConstants', 'ManiphestTaskSubscriber' => 'ManiphestDAO', 'ManiphestTaskSummaryView' => 'ManiphestView', 'ManiphestTransaction' => array( 0 => 'ManiphestDAO', 1 => 'PhabricatorMarkupInterface', ), 'ManiphestTransactionDetailView' => 'ManiphestView', + 'ManiphestTransactionEditor' => 'PhabricatorEditor', 'ManiphestTransactionListView' => 'ManiphestView', 'ManiphestTransactionPreviewController' => 'ManiphestController', 'ManiphestTransactionSaveController' => 'ManiphestController', 'ManiphestTransactionType' => 'ManiphestConstants', 'ManiphestView' => 'AphrontView', 'MetaMTANotificationType' => 'MetaMTAConstants', 'OwnersPackageReplyHandler' => 'PhabricatorMailReplyHandler', 'PackageCreateMail' => 'PackageMail', 'PackageDeleteMail' => 'PackageMail', 'PackageModifyMail' => 'PackageMail', 'Phabricator404Controller' => 'PhabricatorController', 'PhabricatorActionListExample' => 'PhabricatorUIExample', 'PhabricatorActionListView' => 'AphrontView', 'PhabricatorActionView' => 'AphrontView', 'PhabricatorAnchorView' => 'AphrontView', 'PhabricatorApplicationApplications' => 'PhabricatorApplication', 'PhabricatorApplicationAudit' => 'PhabricatorApplication', 'PhabricatorApplicationAuth' => 'PhabricatorApplication', 'PhabricatorApplicationConduit' => 'PhabricatorApplication', 'PhabricatorApplicationCountdown' => 'PhabricatorApplication', 'PhabricatorApplicationDaemons' => 'PhabricatorApplication', 'PhabricatorApplicationDifferential' => 'PhabricatorApplication', 'PhabricatorApplicationDiffusion' => 'PhabricatorApplication', 'PhabricatorApplicationDiviner' => 'PhabricatorApplication', 'PhabricatorApplicationFact' => 'PhabricatorApplication', 'PhabricatorApplicationFiles' => 'PhabricatorApplication', 'PhabricatorApplicationFlags' => 'PhabricatorApplication', 'PhabricatorApplicationHerald' => 'PhabricatorApplication', 'PhabricatorApplicationLaunchView' => 'AphrontView', 'PhabricatorApplicationMacro' => 'PhabricatorApplication', 'PhabricatorApplicationMailingLists' => 'PhabricatorApplication', 'PhabricatorApplicationManiphest' => 'PhabricatorApplication', 'PhabricatorApplicationMetaMTA' => 'PhabricatorApplication', 'PhabricatorApplicationOwners' => 'PhabricatorApplication', 'PhabricatorApplicationPHID' => 'PhabricatorApplication', 'PhabricatorApplicationPHPAST' => 'PhabricatorApplication', 'PhabricatorApplicationPaste' => 'PhabricatorApplication', 'PhabricatorApplicationPeople' => 'PhabricatorApplication', 'PhabricatorApplicationPhame' => 'PhabricatorApplication', 'PhabricatorApplicationPhriction' => 'PhabricatorApplication', 'PhabricatorApplicationPonder' => 'PhabricatorApplication', 'PhabricatorApplicationProject' => 'PhabricatorApplication', 'PhabricatorApplicationRepositories' => 'PhabricatorApplication', 'PhabricatorApplicationSettings' => 'PhabricatorApplication', 'PhabricatorApplicationSlowvote' => 'PhabricatorApplication', 'PhabricatorApplicationStatusView' => 'AphrontView', 'PhabricatorApplicationSubscriptions' => 'PhabricatorApplication', 'PhabricatorApplicationUIExamples' => 'PhabricatorApplication', 'PhabricatorApplicationsListController' => 'PhabricatorController', 'PhabricatorAuditAddCommentController' => 'PhabricatorAuditController', 'PhabricatorAuditComment' => 'PhabricatorAuditDAO', + 'PhabricatorAuditCommentEditor' => 'PhabricatorEditor', 'PhabricatorAuditCommitListView' => 'AphrontView', 'PhabricatorAuditController' => 'PhabricatorController', 'PhabricatorAuditDAO' => 'PhabricatorLiskDAO', 'PhabricatorAuditInlineComment' => array( 0 => 'PhabricatorAuditDAO', 1 => 'PhabricatorInlineCommentInterface', ), 'PhabricatorAuditListController' => 'PhabricatorAuditController', 'PhabricatorAuditListView' => 'AphrontView', 'PhabricatorAuditPreviewController' => 'PhabricatorAuditController', 'PhabricatorAuditReplyHandler' => 'PhabricatorMailReplyHandler', 'PhabricatorAuthController' => 'PhabricatorController', 'PhabricatorBaseEnglishTranslation' => 'PhabricatorTranslation', 'PhabricatorBuiltinPatchList' => 'PhabricatorSQLPatchList', 'PhabricatorCacheDAO' => 'PhabricatorLiskDAO', 'PhabricatorCalendarBrowseController' => 'PhabricatorCalendarController', 'PhabricatorCalendarController' => 'PhabricatorController', 'PhabricatorCalendarDAO' => 'PhabricatorLiskDAO', 'PhabricatorCalendarHoliday' => 'PhabricatorCalendarDAO', 'PhabricatorCalendarHolidayTestCase' => 'PhabricatorTestCase', 'PhabricatorChangesetResponse' => 'AphrontProxyResponse', 'PhabricatorChatLogChannelListController' => 'PhabricatorChatLogController', 'PhabricatorChatLogChannelLogController' => 'PhabricatorChatLogController', 'PhabricatorChatLogController' => 'PhabricatorController', 'PhabricatorChatLogDAO' => 'PhabricatorLiskDAO', 'PhabricatorChatLogEvent' => array( 0 => 'PhabricatorChatLogDAO', 1 => 'PhabricatorPolicyInterface', ), 'PhabricatorChatLogEventType' => 'PhabricatorChatLogConstants', 'PhabricatorChatLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorConduitAPIController' => 'PhabricatorConduitController', 'PhabricatorConduitCertificateToken' => 'PhabricatorConduitDAO', 'PhabricatorConduitConnectionLog' => 'PhabricatorConduitDAO', 'PhabricatorConduitConsoleController' => 'PhabricatorConduitController', 'PhabricatorConduitController' => 'PhabricatorController', 'PhabricatorConduitDAO' => 'PhabricatorLiskDAO', 'PhabricatorConduitListController' => 'PhabricatorConduitController', 'PhabricatorConduitLogController' => 'PhabricatorConduitController', 'PhabricatorConduitMethodCallLog' => 'PhabricatorConduitDAO', 'PhabricatorConduitTokenController' => 'PhabricatorConduitController', 'PhabricatorContentSourceView' => 'AphrontView', 'PhabricatorController' => 'AphrontController', 'PhabricatorCountdownController' => 'PhabricatorController', 'PhabricatorCountdownDAO' => 'PhabricatorLiskDAO', 'PhabricatorCountdownDeleteController' => 'PhabricatorCountdownController', 'PhabricatorCountdownEditController' => 'PhabricatorCountdownController', 'PhabricatorCountdownListController' => 'PhabricatorCountdownController', 'PhabricatorCountdownViewController' => 'PhabricatorCountdownController', 'PhabricatorCursorPagedPolicyAwareQuery' => 'PhabricatorPolicyAwareQuery', 'PhabricatorDaemon' => 'PhutilDaemon', 'PhabricatorDaemonCombinedLogController' => 'PhabricatorDaemonController', 'PhabricatorDaemonConsoleController' => 'PhabricatorDaemonController', 'PhabricatorDaemonController' => 'PhabricatorController', 'PhabricatorDaemonDAO' => 'PhabricatorLiskDAO', 'PhabricatorDaemonLog' => 'PhabricatorDaemonDAO', 'PhabricatorDaemonLogEvent' => 'PhabricatorDaemonDAO', 'PhabricatorDaemonLogEventsView' => 'AphrontView', 'PhabricatorDaemonLogListController' => 'PhabricatorDaemonController', 'PhabricatorDaemonLogListView' => 'AphrontView', 'PhabricatorDaemonLogViewController' => 'PhabricatorDaemonController', 'PhabricatorDaemonTimelineConsoleController' => 'PhabricatorDaemonController', 'PhabricatorDaemonTimelineEventController' => 'PhabricatorDaemonController', 'PhabricatorDefaultFileStorageEngineSelector' => 'PhabricatorFileStorageEngineSelector', 'PhabricatorDefaultSearchEngineSelector' => 'PhabricatorSearchEngineSelector', 'PhabricatorDirectoryController' => 'PhabricatorController', 'PhabricatorDirectoryMainController' => 'PhabricatorDirectoryController', 'PhabricatorDisabledUserController' => 'PhabricatorAuthController', 'PhabricatorDraft' => 'PhabricatorDraftDAO', 'PhabricatorDraftDAO' => 'PhabricatorLiskDAO', 'PhabricatorEdgeConfig' => 'PhabricatorEdgeConstants', 'PhabricatorEdgeCycleException' => 'Exception', + 'PhabricatorEdgeEditor' => 'PhabricatorEditor', 'PhabricatorEdgeGraph' => 'AbstractDirectedGraph', 'PhabricatorEdgeQuery' => 'PhabricatorQuery', 'PhabricatorEdgeTestCase' => 'PhabricatorTestCase', 'PhabricatorEmailLoginController' => 'PhabricatorAuthController', 'PhabricatorEmailTokenController' => 'PhabricatorAuthController', 'PhabricatorEmailVerificationController' => 'PhabricatorPeopleController', 'PhabricatorEnglishTranslation' => 'PhabricatorBaseEnglishTranslation', 'PhabricatorEnvTestCase' => 'PhabricatorTestCase', 'PhabricatorErrorExample' => 'PhabricatorUIExample', 'PhabricatorEvent' => 'PhutilEvent', 'PhabricatorEventType' => 'PhutilEventType', 'PhabricatorExampleEventListener' => 'PhutilEventListener', 'PhabricatorFactAggregate' => 'PhabricatorFactDAO', 'PhabricatorFactChartController' => 'PhabricatorFactController', 'PhabricatorFactController' => 'PhabricatorController', 'PhabricatorFactCountEngine' => 'PhabricatorFactEngine', 'PhabricatorFactCursor' => 'PhabricatorFactDAO', 'PhabricatorFactDAO' => 'PhabricatorLiskDAO', 'PhabricatorFactDaemon' => 'PhabricatorDaemon', 'PhabricatorFactHomeController' => 'PhabricatorFactController', 'PhabricatorFactLastUpdatedEngine' => 'PhabricatorFactEngine', 'PhabricatorFactManagementAnalyzeWorkflow' => 'PhabricatorFactManagementWorkflow', 'PhabricatorFactManagementCursorsWorkflow' => 'PhabricatorFactManagementWorkflow', 'PhabricatorFactManagementDestroyWorkflow' => 'PhabricatorFactManagementWorkflow', 'PhabricatorFactManagementListWorkflow' => 'PhabricatorFactManagementWorkflow', 'PhabricatorFactManagementStatusWorkflow' => 'PhabricatorFactManagementWorkflow', 'PhabricatorFactManagementWorkflow' => 'PhutilArgumentWorkflow', 'PhabricatorFactRaw' => 'PhabricatorFactDAO', 'PhabricatorFactSimpleSpec' => 'PhabricatorFactSpec', 'PhabricatorFactUpdateIterator' => 'PhutilBufferedIterator', 'PhabricatorFeedController' => 'PhabricatorController', 'PhabricatorFeedDAO' => 'PhabricatorLiskDAO', 'PhabricatorFeedPublicStreamController' => 'PhabricatorFeedController', 'PhabricatorFeedQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorFeedStory' => 'PhabricatorPolicyInterface', 'PhabricatorFeedStoryAggregate' => 'PhabricatorFeedStory', 'PhabricatorFeedStoryAudit' => 'PhabricatorFeedStory', 'PhabricatorFeedStoryCommit' => 'PhabricatorFeedStory', 'PhabricatorFeedStoryData' => 'PhabricatorFeedDAO', 'PhabricatorFeedStoryDifferential' => 'PhabricatorFeedStory', 'PhabricatorFeedStoryDifferentialAggregate' => 'PhabricatorFeedStoryAggregate', 'PhabricatorFeedStoryManiphest' => 'PhabricatorFeedStory', 'PhabricatorFeedStoryManiphestAggregate' => 'PhabricatorFeedStoryAggregate', 'PhabricatorFeedStoryNotification' => 'PhabricatorFeedDAO', 'PhabricatorFeedStoryPhriction' => 'PhabricatorFeedStory', 'PhabricatorFeedStoryProject' => 'PhabricatorFeedStory', 'PhabricatorFeedStoryReference' => 'PhabricatorFeedDAO', 'PhabricatorFeedStoryStatus' => 'PhabricatorFeedStory', 'PhabricatorFeedStoryTypeConstants' => 'PhabricatorFeedConstants', 'PhabricatorFeedStoryUnknown' => 'PhabricatorFeedStory', 'PhabricatorFeedStoryView' => 'PhabricatorFeedView', 'PhabricatorFeedView' => 'AphrontView', 'PhabricatorFile' => 'PhabricatorFileDAO', 'PhabricatorFileController' => 'PhabricatorController', 'PhabricatorFileDAO' => 'PhabricatorLiskDAO', 'PhabricatorFileDataController' => 'PhabricatorFileController', 'PhabricatorFileDeleteController' => 'PhabricatorFileController', 'PhabricatorFileDropUploadController' => 'PhabricatorFileController', 'PhabricatorFileImageMacro' => 'PhabricatorFileDAO', 'PhabricatorFileInfoController' => 'PhabricatorFileController', 'PhabricatorFileListController' => 'PhabricatorFileController', 'PhabricatorFileProxyController' => 'PhabricatorFileController', 'PhabricatorFileProxyImage' => 'PhabricatorFileDAO', 'PhabricatorFileShortcutController' => 'PhabricatorFileController', 'PhabricatorFileSideNavView' => 'AphrontView', 'PhabricatorFileStorageBlob' => 'PhabricatorFileDAO', 'PhabricatorFileStorageConfigurationException' => 'Exception', 'PhabricatorFileTransformController' => 'PhabricatorFileController', 'PhabricatorFileUploadController' => 'PhabricatorFileController', 'PhabricatorFileUploadException' => 'Exception', 'PhabricatorFileUploadView' => 'AphrontView', 'PhabricatorFlag' => 'PhabricatorFlagDAO', 'PhabricatorFlagColor' => 'PhabricatorFlagConstants', 'PhabricatorFlagController' => 'PhabricatorController', 'PhabricatorFlagDAO' => 'PhabricatorLiskDAO', 'PhabricatorFlagDeleteController' => 'PhabricatorFlagController', 'PhabricatorFlagEditController' => 'PhabricatorFlagController', 'PhabricatorFlagListController' => 'PhabricatorFlagController', 'PhabricatorFlagListView' => 'AphrontView', 'PhabricatorFlagsUIEventListener' => 'PhutilEventListener', 'PhabricatorFormExample' => 'PhabricatorUIExample', 'PhabricatorGarbageCollectorDaemon' => 'PhabricatorDaemon', 'PhabricatorGlobalLock' => 'PhutilLock', 'PhabricatorGoodForNothingWorker' => 'PhabricatorWorker', 'PhabricatorHeaderView' => 'AphrontView', 'PhabricatorHelpController' => 'PhabricatorController', 'PhabricatorHelpKeyboardShortcutController' => 'PhabricatorHelpController', 'PhabricatorIRCBot' => 'PhabricatorDaemon', 'PhabricatorIRCDifferentialNotificationHandler' => 'PhabricatorIRCHandler', 'PhabricatorIRCLogHandler' => 'PhabricatorIRCHandler', 'PhabricatorIRCMacroHandler' => 'PhabricatorIRCHandler', 'PhabricatorIRCObjectNameHandler' => 'PhabricatorIRCHandler', 'PhabricatorIRCProtocolHandler' => 'PhabricatorIRCHandler', 'PhabricatorIRCWhatsNewHandler' => 'PhabricatorIRCHandler', 'PhabricatorInfrastructureTestCase' => 'PhabricatorTestCase', 'PhabricatorInlineCommentController' => 'PhabricatorController', 'PhabricatorInlineCommentPreviewController' => 'PhabricatorController', 'PhabricatorInlineSummaryView' => 'AphrontView', 'PhabricatorJavelinLinter' => 'ArcanistLinter', 'PhabricatorLDAPLoginController' => 'PhabricatorAuthController', 'PhabricatorLDAPRegistrationController' => 'PhabricatorAuthController', 'PhabricatorLDAPUnknownUserException' => 'Exception', 'PhabricatorLDAPUnlinkController' => 'PhabricatorAuthController', 'PhabricatorLintEngine' => 'PhutilLintEngine', 'PhabricatorLiskDAO' => 'LiskDAO', 'PhabricatorLocalDiskFileStorageEngine' => 'PhabricatorFileStorageEngine', 'PhabricatorLocalTimeTestCase' => 'PhabricatorTestCase', 'PhabricatorLoginController' => 'PhabricatorAuthController', 'PhabricatorLoginValidateController' => 'PhabricatorAuthController', 'PhabricatorLogoutController' => 'PhabricatorAuthController', 'PhabricatorMacroController' => 'PhabricatorController', 'PhabricatorMacroDeleteController' => 'PhabricatorMacroController', 'PhabricatorMacroEditController' => 'PhabricatorMacroController', 'PhabricatorMacroListController' => 'PhabricatorMacroController', 'PhabricatorMailImplementationAmazonSESAdapter' => 'PhabricatorMailImplementationPHPMailerLiteAdapter', 'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'PhabricatorMailImplementationAdapter', 'PhabricatorMailImplementationSendGridAdapter' => 'PhabricatorMailImplementationAdapter', 'PhabricatorMailImplementationTestAdapter' => 'PhabricatorMailImplementationAdapter', 'PhabricatorMailingListsEditController' => 'PhabricatorController', 'PhabricatorMailingListsListController' => 'PhabricatorController', 'PhabricatorMainMenuGroupView' => 'AphrontView', 'PhabricatorMainMenuIconView' => 'AphrontView', 'PhabricatorMainMenuSearchView' => 'AphrontView', 'PhabricatorMainMenuView' => 'AphrontView', 'PhabricatorMarkupCache' => 'PhabricatorCacheDAO', 'PhabricatorMetaMTAController' => 'PhabricatorController', 'PhabricatorMetaMTADAO' => 'PhabricatorLiskDAO', 'PhabricatorMetaMTAEmailBodyParserTestCase' => 'PhabricatorTestCase', 'PhabricatorMetaMTAListController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAMail' => 'PhabricatorMetaMTADAO', 'PhabricatorMetaMTAMailBodyTestCase' => 'PhabricatorTestCase', 'PhabricatorMetaMTAMailTestCase' => 'PhabricatorTestCase', 'PhabricatorMetaMTAMailingList' => 'PhabricatorMetaMTADAO', 'PhabricatorMetaMTAReceiveController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAReceivedListController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAReceivedMail' => 'PhabricatorMetaMTADAO', 'PhabricatorMetaMTASendController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTASendGridReceiveController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAViewController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAWorker' => 'PhabricatorWorker', 'PhabricatorMustVerifyEmailController' => 'PhabricatorAuthController', 'PhabricatorMySQLFileStorageEngine' => 'PhabricatorFileStorageEngine', 'PhabricatorNotificationClearController' => 'PhabricatorNotificationController', 'PhabricatorNotificationController' => 'PhabricatorController', 'PhabricatorNotificationIndividualController' => 'PhabricatorNotificationController', 'PhabricatorNotificationListController' => 'PhabricatorNotificationController', 'PhabricatorNotificationPanelController' => 'PhabricatorNotificationController', 'PhabricatorNotificationQuery' => 'PhabricatorOffsetPagedQuery', 'PhabricatorNotificationStatusController' => 'PhabricatorNotificationController', 'PhabricatorNotificationStoryView' => 'PhabricatorNotificationView', 'PhabricatorNotificationView' => 'AphrontView', 'PhabricatorOAuthClientAuthorization' => 'PhabricatorOAuthServerDAO', 'PhabricatorOAuthClientAuthorizationBaseController' => 'PhabricatorOAuthServerController', 'PhabricatorOAuthClientAuthorizationDeleteController' => 'PhabricatorOAuthClientAuthorizationBaseController', 'PhabricatorOAuthClientAuthorizationEditController' => 'PhabricatorOAuthClientAuthorizationBaseController', 'PhabricatorOAuthClientAuthorizationListController' => 'PhabricatorOAuthClientAuthorizationBaseController', 'PhabricatorOAuthClientAuthorizationQuery' => 'PhabricatorOffsetPagedQuery', 'PhabricatorOAuthClientBaseController' => 'PhabricatorOAuthServerController', 'PhabricatorOAuthClientDeleteController' => 'PhabricatorOAuthClientBaseController', 'PhabricatorOAuthClientEditController' => 'PhabricatorOAuthClientBaseController', 'PhabricatorOAuthClientListController' => 'PhabricatorOAuthClientBaseController', 'PhabricatorOAuthClientViewController' => 'PhabricatorOAuthClientBaseController', 'PhabricatorOAuthDefaultRegistrationController' => 'PhabricatorOAuthRegistrationController', 'PhabricatorOAuthDiagnosticsController' => 'PhabricatorAuthController', 'PhabricatorOAuthFailureView' => 'AphrontView', 'PhabricatorOAuthLoginController' => 'PhabricatorAuthController', 'PhabricatorOAuthProviderDisqus' => 'PhabricatorOAuthProvider', 'PhabricatorOAuthProviderException' => 'Exception', 'PhabricatorOAuthProviderFacebook' => 'PhabricatorOAuthProvider', 'PhabricatorOAuthProviderGitHub' => 'PhabricatorOAuthProvider', 'PhabricatorOAuthProviderGoogle' => 'PhabricatorOAuthProvider', 'PhabricatorOAuthProviderPhabricator' => 'PhabricatorOAuthProvider', 'PhabricatorOAuthRegistrationController' => 'PhabricatorAuthController', 'PhabricatorOAuthResponse' => 'AphrontResponse', 'PhabricatorOAuthServerAccessToken' => 'PhabricatorOAuthServerDAO', 'PhabricatorOAuthServerAuthController' => 'PhabricatorAuthController', 'PhabricatorOAuthServerAuthorizationCode' => 'PhabricatorOAuthServerDAO', 'PhabricatorOAuthServerClient' => 'PhabricatorOAuthServerDAO', 'PhabricatorOAuthServerClientQuery' => 'PhabricatorOffsetPagedQuery', 'PhabricatorOAuthServerController' => 'PhabricatorController', 'PhabricatorOAuthServerDAO' => 'PhabricatorLiskDAO', 'PhabricatorOAuthServerTestCase' => 'PhabricatorTestCase', 'PhabricatorOAuthServerTestController' => 'PhabricatorOAuthServerController', 'PhabricatorOAuthServerTokenController' => 'PhabricatorAuthController', 'PhabricatorOAuthUnlinkController' => 'PhabricatorAuthController', 'PhabricatorObjectHandleStatus' => 'PhabricatorObjectHandleConstants', 'PhabricatorObjectItemListView' => 'AphrontView', 'PhabricatorObjectItemView' => 'AphrontView', 'PhabricatorObjectListView' => 'AphrontView', 'PhabricatorOffsetPagedQuery' => 'PhabricatorQuery', 'PhabricatorOwnersController' => 'PhabricatorController', 'PhabricatorOwnersDAO' => 'PhabricatorLiskDAO', 'PhabricatorOwnersDeleteController' => 'PhabricatorOwnersController', 'PhabricatorOwnersDetailController' => 'PhabricatorOwnersController', 'PhabricatorOwnersEditController' => 'PhabricatorOwnersController', 'PhabricatorOwnersListController' => 'PhabricatorOwnersController', 'PhabricatorOwnersOwner' => 'PhabricatorOwnersDAO', 'PhabricatorOwnersPackage' => array( 0 => 'PhabricatorOwnersDAO', 1 => 'PhabricatorPolicyInterface', ), 'PhabricatorOwnersPackageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorOwnersPath' => 'PhabricatorOwnersDAO', 'PhabricatorPHIDController' => 'PhabricatorController', 'PhabricatorPHIDLookupController' => 'PhabricatorPHIDController', 'PhabricatorPaste' => array( 0 => 'PhabricatorPasteDAO', 1 => 'PhabricatorPolicyInterface', ), 'PhabricatorPasteController' => 'PhabricatorController', 'PhabricatorPasteDAO' => 'PhabricatorLiskDAO', 'PhabricatorPasteEditController' => 'PhabricatorPasteController', 'PhabricatorPasteListController' => 'PhabricatorPasteController', 'PhabricatorPasteQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorPasteViewController' => 'PhabricatorPasteController', 'PhabricatorPeopleController' => 'PhabricatorController', 'PhabricatorPeopleEditController' => 'PhabricatorPeopleController', 'PhabricatorPeopleLdapController' => 'PhabricatorPeopleController', 'PhabricatorPeopleListController' => 'PhabricatorPeopleController', 'PhabricatorPeopleLogsController' => 'PhabricatorPeopleController', 'PhabricatorPeopleProfileController' => 'PhabricatorPeopleController', 'PhabricatorPeopleQuery' => 'PhabricatorOffsetPagedQuery', 'PhabricatorPinboardItemView' => 'AphrontView', 'PhabricatorPinboardView' => 'AphrontView', 'PhabricatorPolicies' => 'PhabricatorPolicyConstants', 'PhabricatorPolicyAwareQuery' => 'PhabricatorOffsetPagedQuery', 'PhabricatorPolicyAwareTestQuery' => 'PhabricatorPolicyAwareQuery', 'PhabricatorPolicyCapability' => 'PhabricatorPolicyConstants', 'PhabricatorPolicyException' => 'Exception', 'PhabricatorPolicyQuery' => 'PhabricatorQuery', 'PhabricatorPolicyTestCase' => 'PhabricatorTestCase', 'PhabricatorPolicyTestObject' => 'PhabricatorPolicyInterface', 'PhabricatorPolicyType' => 'PhabricatorPolicyConstants', 'PhabricatorProfileHeaderView' => 'AphrontView', 'PhabricatorProject' => array( 0 => 'PhabricatorProjectDAO', 1 => 'PhabricatorPolicyInterface', ), 'PhabricatorProjectController' => 'PhabricatorController', 'PhabricatorProjectCreateController' => 'PhabricatorProjectController', 'PhabricatorProjectDAO' => 'PhabricatorLiskDAO', + 'PhabricatorProjectEditor' => 'PhabricatorEditor', 'PhabricatorProjectEditorTestCase' => 'PhabricatorTestCase', 'PhabricatorProjectListController' => 'PhabricatorProjectController', 'PhabricatorProjectMembersEditController' => 'PhabricatorProjectController', 'PhabricatorProjectNameCollisionException' => 'Exception', 'PhabricatorProjectProfile' => 'PhabricatorProjectDAO', 'PhabricatorProjectProfileController' => 'PhabricatorProjectController', 'PhabricatorProjectProfileEditController' => 'PhabricatorProjectController', 'PhabricatorProjectQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorProjectTransaction' => 'PhabricatorProjectDAO', 'PhabricatorProjectTransactionType' => 'PhabricatorProjectConstants', 'PhabricatorProjectUpdateController' => 'PhabricatorProjectController', 'PhabricatorPropertyListView' => 'AphrontView', 'PhabricatorRedirectController' => 'PhabricatorController', 'PhabricatorRefreshCSRFController' => 'PhabricatorAuthController', 'PhabricatorRemarkupControl' => 'AphrontFormTextAreaControl', 'PhabricatorRemarkupRuleCountdown' => 'PhutilRemarkupRule', 'PhabricatorRemarkupRuleDifferential' => 'PhabricatorRemarkupRuleObjectName', 'PhabricatorRemarkupRuleDifferentialHandle' => 'PhabricatorRemarkupRuleObjectHandle', 'PhabricatorRemarkupRuleDiffusion' => 'PhutilRemarkupRule', 'PhabricatorRemarkupRuleEmbedFile' => 'PhutilRemarkupRule', 'PhabricatorRemarkupRuleImageMacro' => 'PhutilRemarkupRule', 'PhabricatorRemarkupRuleManiphest' => 'PhabricatorRemarkupRuleObjectName', 'PhabricatorRemarkupRuleManiphestHandle' => 'PhabricatorRemarkupRuleObjectHandle', 'PhabricatorRemarkupRuleMention' => 'PhutilRemarkupRule', 'PhabricatorRemarkupRuleObjectHandle' => 'PhutilRemarkupRule', 'PhabricatorRemarkupRuleObjectName' => 'PhutilRemarkupRule', 'PhabricatorRemarkupRulePaste' => 'PhabricatorRemarkupRuleObjectName', 'PhabricatorRemarkupRulePhriction' => 'PhutilRemarkupRule', 'PhabricatorRemarkupRuleProxyImage' => 'PhutilRemarkupRule', 'PhabricatorRemarkupRuleYoutube' => 'PhutilRemarkupRule', 'PhabricatorRepository' => 'PhabricatorRepositoryDAO', 'PhabricatorRepositoryArcanistProject' => 'PhabricatorRepositoryDAO', 'PhabricatorRepositoryArcanistProjectDeleteController' => 'PhabricatorRepositoryController', 'PhabricatorRepositoryArcanistProjectEditController' => 'PhabricatorRepositoryController', 'PhabricatorRepositoryAuditRequest' => 'PhabricatorRepositoryDAO', 'PhabricatorRepositoryCommit' => 'PhabricatorRepositoryDAO', 'PhabricatorRepositoryCommitChangeParserWorker' => 'PhabricatorRepositoryCommitParserWorker', 'PhabricatorRepositoryCommitData' => 'PhabricatorRepositoryDAO', 'PhabricatorRepositoryCommitHeraldWorker' => 'PhabricatorRepositoryCommitParserWorker', 'PhabricatorRepositoryCommitMessageParserWorker' => 'PhabricatorRepositoryCommitParserWorker', 'PhabricatorRepositoryCommitOwnersWorker' => 'PhabricatorRepositoryCommitParserWorker', 'PhabricatorRepositoryCommitParserWorker' => 'PhabricatorWorker', 'PhabricatorRepositoryController' => 'PhabricatorController', 'PhabricatorRepositoryCreateController' => 'PhabricatorRepositoryController', 'PhabricatorRepositoryDAO' => 'PhabricatorLiskDAO', 'PhabricatorRepositoryDefaultCommitMessageDetailParser' => 'PhabricatorRepositoryCommitMessageDetailParser', 'PhabricatorRepositoryDeleteController' => 'PhabricatorRepositoryController', 'PhabricatorRepositoryEditController' => 'PhabricatorRepositoryController', 'PhabricatorRepositoryGitCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker', 'PhabricatorRepositoryGitCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker', 'PhabricatorRepositoryListController' => 'PhabricatorRepositoryController', 'PhabricatorRepositoryManagementDiscoverWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementListWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementPullWorkflow' => 'PhabricatorRepositoryManagementWorkflow', 'PhabricatorRepositoryManagementWorkflow' => 'PhutilArgumentWorkflow', 'PhabricatorRepositoryMercurialCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker', 'PhabricatorRepositoryMercurialCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker', 'PhabricatorRepositoryPullLocalDaemon' => 'PhabricatorDaemon', 'PhabricatorRepositoryPullLocalDaemonTestCase' => 'PhabricatorTestCase', 'PhabricatorRepositoryShortcut' => 'PhabricatorRepositoryDAO', 'PhabricatorRepositorySvnCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker', 'PhabricatorRepositorySvnCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker', 'PhabricatorRepositorySymbol' => 'PhabricatorRepositoryDAO', 'PhabricatorRepositoryTestCase' => 'PhabricatorTestCase', 'PhabricatorS3FileStorageEngine' => 'PhabricatorFileStorageEngine', 'PhabricatorSearchAttachController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchBaseController' => 'PhabricatorController', 'PhabricatorSearchCommitIndexer' => 'PhabricatorSearchDocumentIndexer', 'PhabricatorSearchController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchDAO' => 'PhabricatorLiskDAO', 'PhabricatorSearchDifferentialIndexer' => 'PhabricatorSearchDocumentIndexer', 'PhabricatorSearchDocument' => 'PhabricatorSearchDAO', 'PhabricatorSearchDocumentField' => 'PhabricatorSearchDAO', 'PhabricatorSearchDocumentRelationship' => 'PhabricatorSearchDAO', 'PhabricatorSearchEngineElastic' => 'PhabricatorSearchEngine', 'PhabricatorSearchEngineMySQL' => 'PhabricatorSearchEngine', 'PhabricatorSearchIndexController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchManiphestIndexer' => 'PhabricatorSearchDocumentIndexer', 'PhabricatorSearchPhrictionIndexer' => 'PhabricatorSearchDocumentIndexer', 'PhabricatorSearchPonderIndexer' => 'PhabricatorSearchDocumentIndexer', 'PhabricatorSearchQuery' => 'PhabricatorSearchDAO', 'PhabricatorSearchResultView' => 'AphrontView', 'PhabricatorSearchSelectController' => 'PhabricatorSearchBaseController', 'PhabricatorSearchUserIndexer' => 'PhabricatorSearchDocumentIndexer', 'PhabricatorSettingsAdjustController' => 'PhabricatorController', 'PhabricatorSettingsMainController' => 'PhabricatorController', 'PhabricatorSettingsPanelAccount' => 'PhabricatorSettingsPanel', 'PhabricatorSettingsPanelConduit' => 'PhabricatorSettingsPanel', 'PhabricatorSettingsPanelDisplayPreferences' => 'PhabricatorSettingsPanel', 'PhabricatorSettingsPanelEmailAddresses' => 'PhabricatorSettingsPanel', 'PhabricatorSettingsPanelEmailPreferences' => 'PhabricatorSettingsPanel', 'PhabricatorSettingsPanelLDAP' => 'PhabricatorSettingsPanel', 'PhabricatorSettingsPanelOAuth' => 'PhabricatorSettingsPanel', 'PhabricatorSettingsPanelPassword' => 'PhabricatorSettingsPanel', 'PhabricatorSettingsPanelProfile' => 'PhabricatorSettingsPanel', 'PhabricatorSettingsPanelSSHKeys' => 'PhabricatorSettingsPanel', 'PhabricatorSettingsPanelSearchPreferences' => 'PhabricatorSettingsPanel', 'PhabricatorSlowvoteChoice' => 'PhabricatorSlowvoteDAO', 'PhabricatorSlowvoteComment' => 'PhabricatorSlowvoteDAO', 'PhabricatorSlowvoteController' => 'PhabricatorController', 'PhabricatorSlowvoteCreateController' => 'PhabricatorSlowvoteController', 'PhabricatorSlowvoteDAO' => 'PhabricatorLiskDAO', 'PhabricatorSlowvoteListController' => 'PhabricatorSlowvoteController', 'PhabricatorSlowvoteOption' => 'PhabricatorSlowvoteDAO', 'PhabricatorSlowvotePoll' => 'PhabricatorSlowvoteDAO', 'PhabricatorSlowvotePollController' => 'PhabricatorSlowvoteController', 'PhabricatorSlugTestCase' => 'PhabricatorTestCase', 'PhabricatorSortTableExample' => 'PhabricatorUIExample', 'PhabricatorSourceCodeView' => 'AphrontView', 'PhabricatorStandardPageView' => 'AphrontPageView', 'PhabricatorStatusController' => 'PhabricatorController', 'PhabricatorStorageManagementDatabasesWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementDestroyWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementDumpWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementStatusWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementUpgradeWorkflow' => 'PhabricatorStorageManagementWorkflow', 'PhabricatorStorageManagementWorkflow' => 'PhutilArgumentWorkflow', 'PhabricatorSubscribersQuery' => 'PhabricatorQuery', 'PhabricatorSubscriptionsEditController' => 'PhabricatorController', + 'PhabricatorSubscriptionsEditor' => 'PhabricatorEditor', 'PhabricatorSubscriptionsUIEventListener' => 'PhutilEventListener', 'PhabricatorSymbolNameLinter' => 'ArcanistXHPASTLintNamingHook', 'PhabricatorTaskmasterDaemon' => 'PhabricatorDaemon', 'PhabricatorTestCase' => 'ArcanistPhutilTestCase', 'PhabricatorTimelineCursor' => 'PhabricatorTimelineDAO', 'PhabricatorTimelineDAO' => 'PhabricatorLiskDAO', 'PhabricatorTimelineEvent' => 'PhabricatorTimelineDAO', 'PhabricatorTimelineEventData' => 'PhabricatorTimelineDAO', 'PhabricatorTimelineIterator' => 'Iterator', 'PhabricatorTimer' => 'PhabricatorCountdownDAO', 'PhabricatorTransactionView' => 'AphrontView', 'PhabricatorTransformedFile' => 'PhabricatorFileDAO', 'PhabricatorTrivialTestCase' => 'PhabricatorTestCase', 'PhabricatorTypeaheadCommonDatasourceController' => 'PhabricatorTypeaheadDatasourceController', 'PhabricatorTypeaheadDatasourceController' => 'PhabricatorController', 'PhabricatorUIExampleRenderController' => 'PhabricatorController', 'PhabricatorUIListFilterExample' => 'PhabricatorUIExample', 'PhabricatorUINotificationExample' => 'PhabricatorUIExample', 'PhabricatorUIPagerExample' => 'PhabricatorUIExample', 'PhabricatorUITooltipExample' => 'PhabricatorUIExample', 'PhabricatorUnitsTestCase' => 'PhabricatorTestCase', 'PhabricatorUser' => array( 0 => 'PhabricatorUserDAO', 1 => 'PhutilPerson', ), 'PhabricatorUserDAO' => 'PhabricatorLiskDAO', + 'PhabricatorUserEditor' => 'PhabricatorEditor', 'PhabricatorUserEmail' => 'PhabricatorUserDAO', 'PhabricatorUserLDAPInfo' => 'PhabricatorUserDAO', 'PhabricatorUserLog' => 'PhabricatorUserDAO', 'PhabricatorUserOAuthInfo' => 'PhabricatorUserDAO', 'PhabricatorUserPreferences' => 'PhabricatorUserDAO', 'PhabricatorUserProfile' => 'PhabricatorUserDAO', 'PhabricatorUserSSHKey' => 'PhabricatorUserDAO', 'PhabricatorUserStatus' => 'PhabricatorUserDAO', 'PhabricatorUserTestCase' => 'PhabricatorTestCase', 'PhabricatorWorkerDAO' => 'PhabricatorLiskDAO', 'PhabricatorWorkerTask' => 'PhabricatorWorkerDAO', 'PhabricatorWorkerTaskData' => 'PhabricatorWorkerDAO', 'PhabricatorWorkerTaskDetailController' => 'PhabricatorDaemonController', 'PhabricatorWorkerTaskUpdateController' => 'PhabricatorDaemonController', 'PhabricatorXHPASTViewController' => 'PhabricatorController', 'PhabricatorXHPASTViewDAO' => 'PhabricatorLiskDAO', 'PhabricatorXHPASTViewFrameController' => 'PhabricatorXHPASTViewController', 'PhabricatorXHPASTViewFramesetController' => 'PhabricatorXHPASTViewController', 'PhabricatorXHPASTViewInputController' => 'PhabricatorXHPASTViewPanelController', 'PhabricatorXHPASTViewPanelController' => 'PhabricatorXHPASTViewController', 'PhabricatorXHPASTViewParseTree' => 'PhabricatorXHPASTViewDAO', 'PhabricatorXHPASTViewRunController' => 'PhabricatorXHPASTViewController', 'PhabricatorXHPASTViewStreamController' => 'PhabricatorXHPASTViewPanelController', 'PhabricatorXHPASTViewTreeController' => 'PhabricatorXHPASTViewPanelController', 'PhabricatorXHProfController' => 'PhabricatorController', 'PhabricatorXHProfDAO' => 'PhabricatorLiskDAO', 'PhabricatorXHProfProfileController' => 'PhabricatorXHProfController', 'PhabricatorXHProfProfileSymbolView' => 'PhabricatorXHProfProfileView', 'PhabricatorXHProfProfileTopLevelView' => 'PhabricatorXHProfProfileView', 'PhabricatorXHProfProfileView' => 'AphrontView', 'PhabricatorXHProfSample' => 'PhabricatorXHProfDAO', 'PhabricatorXHProfSampleListController' => 'PhabricatorXHProfController', 'PhabricatorXHProfSampleListView' => 'AphrontView', 'PhameAllBlogListController' => 'PhameBlogListBaseController', 'PhameAllPostListController' => 'PhamePostListBaseController', 'PhameBlog' => 'PhameDAO', 'PhameBlogDeleteController' => 'PhameController', 'PhameBlogDetailView' => 'AphrontView', 'PhameBlogEditController' => 'PhameController', 'PhameBlogListBaseController' => 'PhameController', 'PhameBlogListView' => 'AphrontView', 'PhameBlogQuery' => 'PhabricatorOffsetPagedQuery', 'PhameBlogViewController' => 'PhameController', 'PhameBloggerPostListController' => 'PhamePostListBaseController', 'PhameController' => 'PhabricatorController', 'PhameDAO' => 'PhabricatorLiskDAO', 'PhameDraftListController' => 'PhamePostListBaseController', 'PhamePost' => 'PhameDAO', 'PhamePostDeleteController' => 'PhameController', 'PhamePostDetailView' => 'AphrontView', 'PhamePostEditController' => 'PhameController', 'PhamePostListBaseController' => 'PhameController', 'PhamePostListView' => 'AphrontView', 'PhamePostPreviewController' => 'PhameController', 'PhamePostQuery' => 'PhabricatorOffsetPagedQuery', 'PhamePostViewController' => 'PhameController', 'PhameUserBlogListController' => 'PhameBlogListBaseController', 'PhameUserPostListController' => 'PhamePostListBaseController', 'PhortuneMonthYearExpiryControl' => 'AphrontFormControl', 'PhortuneStripeBaseController' => 'PhabricatorController', 'PhortuneStripePaymentFormView' => 'AphrontView', 'PhortuneStripeTestPaymentFormController' => 'PhortuneStripeBaseController', 'PhrictionActionConstants' => 'PhrictionConstants', 'PhrictionChangeType' => 'PhrictionConstants', 'PhrictionContent' => array( 0 => 'PhrictionDAO', 1 => 'PhabricatorMarkupInterface', ), 'PhrictionController' => 'PhabricatorController', 'PhrictionDAO' => 'PhabricatorLiskDAO', 'PhrictionDeleteController' => 'PhrictionController', 'PhrictionDiffController' => 'PhrictionController', 'PhrictionDocument' => 'PhrictionDAO', 'PhrictionDocumentController' => 'PhrictionController', + 'PhrictionDocumentEditor' => 'PhabricatorEditor', 'PhrictionDocumentPreviewController' => 'PhrictionController', 'PhrictionDocumentStatus' => 'PhrictionConstants', 'PhrictionDocumentTestCase' => 'PhabricatorTestCase', 'PhrictionEditController' => 'PhrictionController', 'PhrictionHistoryController' => 'PhrictionController', 'PhrictionListController' => 'PhrictionController', 'PonderAddAnswerView' => 'AphrontView', 'PonderAddCommentView' => 'AphrontView', 'PonderAnswer' => array( 0 => 'PonderDAO', 1 => 'PhabricatorMarkupInterface', 2 => 'PonderVotableInterface', ), + 'PonderAnswerEditor' => 'PhabricatorEditor', 'PonderAnswerListView' => 'AphrontView', 'PonderAnswerPreviewController' => 'PonderController', 'PonderAnswerQuery' => 'PhabricatorOffsetPagedQuery', 'PonderAnswerSaveController' => 'PonderController', 'PonderAnswerViewController' => 'PonderController', 'PonderAnsweredMail' => 'PonderMail', 'PonderComment' => array( 0 => 'PonderDAO', 1 => 'PhabricatorMarkupInterface', ), + 'PonderCommentEditor' => 'PhabricatorEditor', 'PonderCommentListView' => 'AphrontView', 'PonderCommentMail' => 'PonderMail', 'PonderCommentQuery' => 'PhabricatorQuery', 'PonderCommentSaveController' => 'PonderController', 'PonderController' => 'PhabricatorController', 'PonderDAO' => 'PhabricatorLiskDAO', 'PonderFeedController' => 'PonderController', 'PonderMentionMail' => 'PonderMail', 'PonderPostBodyView' => 'AphrontView', 'PonderQuestion' => array( 0 => 'PonderDAO', 1 => 'PhabricatorMarkupInterface', 2 => 'PonderVotableInterface', 3 => 'PhabricatorSubscribableInterface', ), 'PonderQuestionAskController' => 'PonderController', 'PonderQuestionDetailView' => 'AphrontView', + 'PonderQuestionEditor' => 'PhabricatorEditor', 'PonderQuestionPreviewController' => 'PonderController', 'PonderQuestionQuery' => 'PhabricatorOffsetPagedQuery', 'PonderQuestionSummaryView' => 'AphrontView', 'PonderQuestionViewController' => 'PonderController', 'PonderReplyHandler' => 'PhabricatorMailReplyHandler', 'PonderRuleQuestion' => 'PhabricatorRemarkupRuleObjectName', 'PonderUserProfileView' => 'AphrontView', 'PonderVotableView' => 'AphrontView', + 'PonderVoteEditor' => 'PhabricatorEditor', 'PonderVoteSaveController' => 'PonderController', 'QueryFormattingTestCase' => 'PhabricatorTestCase', ), )); diff --git a/src/applications/audit/PhabricatorAuditReplyHandler.php b/src/applications/audit/PhabricatorAuditReplyHandler.php index 32393c7e80..c9dd80ea3a 100644 --- a/src/applications/audit/PhabricatorAuditReplyHandler.php +++ b/src/applications/audit/PhabricatorAuditReplyHandler.php @@ -1,68 +1,70 @@ getDefaultPrivateReplyHandlerEmailAddress($handle, 'C'); } public function getPublicReplyHandlerEmailAddress() { return $this->getDefaultPublicReplyHandlerEmailAddress('C'); } public function getReplyHandlerDomain() { return PhabricatorEnv::getEnvConfig( 'metamta.diffusion.reply-handler-domain'); } public function getReplyHandlerInstructions() { if ($this->supportsReplies()) { return "Reply to comment."; } else { return null; } } protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) { $commit = $this->getMailReceiver(); $actor = $this->getActor(); // TODO: Support !raise, !accept, etc. // TODO: Content sources. $comment = id(new PhabricatorAuditComment()) ->setAction(PhabricatorAuditActionConstants::COMMENT) ->setContent($mail->getCleanTextBody()); $editor = new PhabricatorAuditCommentEditor($commit); - $editor->setUser($actor); + $editor->setActor($actor); + $editor->setExcludeMailRecipientPHIDs( + $this->getExcludeMailRecipientPHIDs()); $editor->addComment($comment); } } diff --git a/src/applications/audit/controller/PhabricatorAuditAddCommentController.php b/src/applications/audit/controller/PhabricatorAuditAddCommentController.php index d0180d4785..3fdb9f4b94 100644 --- a/src/applications/audit/controller/PhabricatorAuditAddCommentController.php +++ b/src/applications/audit/controller/PhabricatorAuditAddCommentController.php @@ -1,84 +1,84 @@ getRequest(); $user = $request->getUser(); if (!$request->isFormPost()) { return new Aphront403Response(); } $commit_phid = $request->getStr('commit'); $commit = id(new PhabricatorRepositoryCommit())->loadOneWhere( 'phid = %s', $commit_phid); if (!$commit) { return new Aphront404Response(); } $phids = array($commit_phid); $action = $request->getStr('action'); $comment = id(new PhabricatorAuditComment()) ->setAction($action) ->setContent($request->getStr('content')); // make sure we only add auditors or ccs if the action matches switch ($action) { case 'add_auditors': $auditors = $request->getArr('auditors'); $ccs = array(); break; case 'add_ccs': $auditors = array(); $ccs = $request->getArr('ccs'); break; default: $auditors = array(); $ccs = array(); break; } id(new PhabricatorAuditCommentEditor($commit)) - ->setUser($user) + ->setActor($user) ->setAttachInlineComments(true) ->addAuditors($auditors) ->addCCs($ccs) ->addComment($comment); $handles = $this->loadViewerHandles($phids); $uri = $handles[$commit_phid]->getURI(); $draft = id(new PhabricatorDraft())->loadOneWhere( 'authorPHID = %s AND draftKey = %s', $user->getPHID(), 'diffusion-audit-'.$commit->getID()); if ($draft) { $draft->delete(); } return id(new AphrontRedirectResponse())->setURI($uri); } } diff --git a/src/applications/audit/editor/PhabricatorAuditCommentEditor.php b/src/applications/audit/editor/PhabricatorAuditCommentEditor.php index 93d431219a..2cb45c1523 100644 --- a/src/applications/audit/editor/PhabricatorAuditCommentEditor.php +++ b/src/applications/audit/editor/PhabricatorAuditCommentEditor.php @@ -1,534 +1,527 @@ commit = $commit; return $this; } - public function setUser(PhabricatorUser $user) { - $this->user = $user; - return $this; - } - public function addAuditors(array $auditor_phids) { $this->auditors = array_merge($this->auditors, $auditor_phids); return $this; } public function addCCs(array $cc_phids) { $this->ccs = array_merge($this->ccs, $cc_phids); return $this; } public function setAttachInlineComments($attach_inline_comments) { $this->attachInlineComments = $attach_inline_comments; return $this; } public function addComment(PhabricatorAuditComment $comment) { $commit = $this->commit; - $user = $this->user; + $actor = $this->getActor(); $other_comments = id(new PhabricatorAuditComment())->loadAllWhere( 'targetPHID = %s', $commit->getPHID()); $inline_comments = array(); if ($this->attachInlineComments) { $inline_comments = id(new PhabricatorAuditInlineComment())->loadAllWhere( 'authorPHID = %s AND commitPHID = %s AND auditCommentID IS NULL', - $user->getPHID(), + $actor->getPHID(), $commit->getPHID()); } $comment - ->setActorPHID($user->getPHID()) + ->setActorPHID($actor->getPHID()) ->setTargetPHID($commit->getPHID()) ->save(); $content_blocks = array($comment->getContent()); if ($inline_comments) { foreach ($inline_comments as $inline) { $inline->setAuditCommentID($comment->getID()); $inline->save(); $content_blocks[] = $inline->getContent(); } } $ccs = $this->ccs; $auditors = $this->auditors; $metadata = $comment->getMetadata(); $metacc = array(); // Find any "@mentions" in the content blocks. $mention_ccs = PhabricatorMarkupEngine::extractPHIDsFromMentions( $content_blocks); if ($mention_ccs) { $metacc = idx( $metadata, PhabricatorAuditComment::METADATA_ADDED_CCS, array()); foreach ($mention_ccs as $cc_phid) { $metacc[] = $cc_phid; } } if ($metacc) { $ccs = array_merge($ccs, $metacc); } - // When a user submits an audit comment, we update all the audit requests + // When an actor submits an audit comment, we update all the audit requests // they have authority over to reflect the most recent status. The general // idea here is that if audit has triggered for, e.g., several packages, but // a user owns all of them, they can clear the audit requirement in one go // without auditing the commit for each trigger. - $audit_phids = self::loadAuditPHIDsForUser($this->user); + $audit_phids = self::loadAuditPHIDsForUser($actor); $audit_phids = array_fill_keys($audit_phids, true); $requests = id(new PhabricatorRepositoryAuditRequest()) ->loadAllWhere( 'commitPHID = %s', $commit->getPHID()); $action = $comment->getAction(); // TODO: We should validate the action, currently we allow anyone to, e.g., // close an audit if they muck with form parameters. I'll followup with this // and handle the no-effect cases (e.g., closing and already-closed audit). - $user_is_author = ($user->getPHID() == $commit->getAuthorPHID()); + $actor_is_author = ($actor->getPHID() == $commit->getAuthorPHID()); if ($action == PhabricatorAuditActionConstants::CLOSE) { // "Close" means wipe out all the concerns. $concerned_status = PhabricatorAuditStatusConstants::CONCERNED; foreach ($requests as $request) { if ($request->getAuditStatus() == $concerned_status) { $request->setAuditStatus(PhabricatorAuditStatusConstants::CLOSED); $request->save(); } } } else if ($action == PhabricatorAuditActionConstants::RESIGN) { // "Resign" has unusual rules for writing user rows, only affects the // user row (never package/project rows), and always affects the user // row (other actions don't, if they were able to affect a package/project // row). - $user_request = null; + $actor_request = null; foreach ($requests as $request) { - if ($request->getAuditorPHID() == $user->getPHID()) { - $user_request = $request; + if ($request->getAuditorPHID() == $actor->getPHID()) { + $actor_request = $request; break; } } - if (!$user_request) { - $user_request = id(new PhabricatorRepositoryAuditRequest()) + if (!$actor_request) { + $actor_request = id(new PhabricatorRepositoryAuditRequest()) ->setCommitPHID($commit->getPHID()) - ->setAuditorPHID($user->getPHID()) + ->setAuditorPHID($actor->getPHID()) ->setAuditReasons(array("Resigned")); } - $user_request + $actor_request ->setAuditStatus(PhabricatorAuditStatusConstants::RESIGNED) ->save(); - $requests[] = $user_request; + $requests[] = $actor_request; } else { $have_any_requests = false; foreach ($requests as $request) { if (empty($audit_phids[$request->getAuditorPHID()])) { continue; } - $request_is_for_user = ($request->getAuditorPHID() == $user->getPHID()); + $request_is_for_actor = + ($request->getAuditorPHID() == $actor->getPHID()); $have_any_requests = true; $new_status = null; switch ($action) { case PhabricatorAuditActionConstants::COMMENT: case PhabricatorAuditActionConstants::ADD_CCS: case PhabricatorAuditActionConstants::ADD_AUDITORS: // Commenting or adding cc's/auditors doesn't change status. break; case PhabricatorAuditActionConstants::ACCEPT: - if (!$user_is_author || $request_is_for_user) { + if (!$actor_is_author || $request_is_for_actor) { // When modifying your own commits, you act only on behalf of // yourself, not your packages/projects -- the idea being that // you can't accept your own commits. $new_status = PhabricatorAuditStatusConstants::ACCEPTED; } break; case PhabricatorAuditActionConstants::CONCERN: - if (!$user_is_author || $request_is_for_user) { + if (!$actor_is_author || $request_is_for_actor) { // See above. $new_status = PhabricatorAuditStatusConstants::CONCERNED; } break; default: throw new Exception("Unknown action '{$action}'!"); } if ($new_status !== null) { $request->setAuditStatus($new_status); $request->save(); } } - // If the user has no current authority over any audit trigger, make a + // If the actor has no current authority over any audit trigger, make a // new one to represent their audit state. if (!$have_any_requests) { $new_status = null; switch ($action) { case PhabricatorAuditActionConstants::COMMENT: case PhabricatorAuditActionConstants::ADD_CCS: case PhabricatorAuditActionConstants::ADD_AUDITORS: $new_status = PhabricatorAuditStatusConstants::AUDIT_NOT_REQUIRED; break; case PhabricatorAuditActionConstants::ACCEPT: $new_status = PhabricatorAuditStatusConstants::ACCEPTED; break; case PhabricatorAuditActionConstants::CONCERN: $new_status = PhabricatorAuditStatusConstants::CONCERNED; break; case PhabricatorAuditActionConstants::CLOSE: // Impossible to reach this block with 'close'. default: throw new Exception("Unknown or invalid action '{$action}'!"); } $request = id(new PhabricatorRepositoryAuditRequest()) ->setCommitPHID($commit->getPHID()) - ->setAuditorPHID($user->getPHID()) + ->setAuditorPHID($actor->getPHID()) ->setAuditStatus($new_status) ->setAuditReasons(array("Voluntary Participant")) ->save(); $requests[] = $request; } } $requests_by_auditor = mpull($requests, null, 'getAuditorPHID'); $requests_phids = array_keys($requests_by_auditor); $ccs = array_diff($ccs, $requests_phids); $auditors = array_diff($auditors, $requests_phids); if ($action == PhabricatorAuditActionConstants::ADD_CCS) { if ($ccs) { $metadata[PhabricatorAuditComment::METADATA_ADDED_CCS] = $ccs; $comment->setMetaData($metadata); } else { $comment->setAction(PhabricatorAuditActionConstants::COMMENT); } } if ($action == PhabricatorAuditActionConstants::ADD_AUDITORS) { if ($auditors) { $metadata[PhabricatorAuditComment::METADATA_ADDED_AUDITORS] = $auditors; $comment->setMetaData($metadata); } else { $comment->setAction(PhabricatorAuditActionConstants::COMMENT); } } $comment->save(); if ($auditors) { foreach ($auditors as $auditor_phid) { $audit_requested = PhabricatorAuditStatusConstants::AUDIT_REQUESTED; $requests[] = id (new PhabricatorRepositoryAuditRequest()) ->setCommitPHID($commit->getPHID()) ->setAuditorPHID($auditor_phid) ->setAuditStatus($audit_requested) ->setAuditReasons( - array('Added by ' . $user->getUsername())) + array('Added by ' . $actor->getUsername())) ->save(); } } if ($ccs) { foreach ($ccs as $cc_phid) { $audit_cc = PhabricatorAuditStatusConstants::CC; $requests[] = id (new PhabricatorRepositoryAuditRequest()) ->setCommitPHID($commit->getPHID()) ->setAuditorPHID($cc_phid) ->setAuditStatus($audit_cc) ->setAuditReasons( - array('Added by ' . $user->getUsername())) + array('Added by ' . $actor->getUsername())) ->save(); } } $commit->updateAuditStatus($requests); $commit->save(); $this->publishFeedStory($comment, array_keys($audit_phids)); PhabricatorSearchCommitIndexer::indexCommit($commit); $this->sendMail($comment, $other_comments, $inline_comments, $requests); } /** * Load the PHIDs for all objects the user has the authority to act as an * audit for. This includes themselves, and any packages they are an owner * of. */ public static function loadAuditPHIDsForUser(PhabricatorUser $user) { $phids = array(); // TODO: This method doesn't really use the right viewer, but in practice we // never issue this query of this type on behalf of another user and are // unlikely to do so in the future. This entire method should be refactored // into a Query class, however, and then we should use a proper viewer. // The user can audit on their own behalf. $phids[$user->getPHID()] = true; $owned_packages = id(new PhabricatorOwnersPackageQuery()) ->setViewer($user) ->withOwnerPHIDs(array($user->getPHID())) ->execute(); foreach ($owned_packages as $package) { $phids[$package->getPHID()] = true; } // The user can audit on behalf of all projects they are a member of. - $query = new PhabricatorProjectQuery(); - - // TODO: As above. - $query->setViewer($user); - - $query->withMemberPHIDs(array($user->getPHID())); - $projects = $query->execute(); + $projects = id(new PhabricatorProjectQuery()) + ->setViewer($user) + ->withMemberPHIDs(array($user->getPHID())) + ->execute(); foreach ($projects as $project) { $phids[$project->getPHID()] = true; } return array_keys($phids); } private function publishFeedStory( PhabricatorAuditComment $comment, array $more_phids) { $commit = $this->commit; - $user = $this->user; + $actor = $this->getActor(); $related_phids = array_merge( array( - $user->getPHID(), + $actor->getPHID(), $commit->getPHID(), ), $more_phids); id(new PhabricatorFeedStoryPublisher()) ->setRelatedPHIDs($related_phids) - ->setStoryAuthorPHID($user->getPHID()) + ->setStoryAuthorPHID($actor->getPHID()) ->setStoryTime(time()) ->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_AUDIT) ->setStoryData( array( 'commitPHID' => $commit->getPHID(), 'action' => $comment->getAction(), 'content' => $comment->getContent(), )) ->publish(); } private function sendMail( PhabricatorAuditComment $comment, array $other_comments, array $inline_comments, array $requests) { assert_instances_of($other_comments, 'PhabricatorAuditComment'); assert_instances_of($inline_comments, 'PhabricatorInlineCommentInterface'); $commit = $this->commit; $data = $commit->loadCommitData(); $summary = $data->getSummary(); $commit_phid = $commit->getPHID(); $phids = array($commit_phid); $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles(); $handle = $handles[$commit_phid]; $name = $handle->getName(); $map = array( PhabricatorAuditActionConstants::CONCERN => 'Raised Concern', PhabricatorAuditActionConstants::ACCEPT => 'Accepted', PhabricatorAuditActionConstants::RESIGN => 'Resigned', PhabricatorAuditActionConstants::CLOSE => 'Closed', PhabricatorAuditActionConstants::ADD_CCS => 'Added CCs', PhabricatorAuditActionConstants::ADD_AUDITORS => 'Added Auditors', ); $verb = idx($map, $comment->getAction(), 'Commented On'); $reply_handler = self::newReplyHandlerForCommit($commit); $prefix = PhabricatorEnv::getEnvConfig('metamta.diffusion.subject-prefix'); $repository = id(new PhabricatorRepository()) ->load($commit->getRepositoryID()); $threading = self::getMailThreading($repository, $commit); list($thread_id, $thread_topic) = $threading; $body = $this->renderMailBody( $comment, "{$name}: {$summary}", $handle, $reply_handler, $inline_comments); $email_to = array(); $email_cc = array(); $author_phid = $data->getCommitDetail('authorPHID'); if ($author_phid) { $email_to[] = $author_phid; } $email_cc = array(); foreach ($other_comments as $other_comment) { $email_cc[] = $other_comment->getActorPHID(); } foreach ($requests as $request) { if ($request->getAuditStatus() == PhabricatorAuditStatusConstants::CC) { $email_cc[] = $request->getAuditorPHID(); } } $phids = array_merge($email_to, $email_cc); $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles(); // NOTE: Always set $is_new to false, because the "first" mail in the // thread is the Herald notification of the commit. $is_new = false; $template = id(new PhabricatorMetaMTAMail()) ->setSubject("{$name}: {$summary}") ->setSubjectPrefix($prefix) ->setVarySubjectPrefix("[{$verb}]") ->setFrom($comment->getActorPHID()) ->setThreadID($thread_id, $is_new) ->addHeader('Thread-Topic', $thread_topic) ->setRelatedPHID($commit->getPHID()) + ->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs()) ->setIsBulk(true) ->setBody($body); $mails = $reply_handler->multiplexMail( $template, array_select_keys($handles, $email_to), array_select_keys($handles, $email_cc)); foreach ($mails as $mail) { $mail->saveAndSend(); } } public static function getMailThreading( PhabricatorRepository $repository, PhabricatorRepositoryCommit $commit) { return array( 'diffusion-audit-'.$commit->getPHID(), 'Commit r'.$repository->getCallsign().$commit->getCommitIdentifier(), ); } public static function newReplyHandlerForCommit($commit) { $reply_handler = PhabricatorEnv::newObjectFromConfig( 'metamta.diffusion.reply-handler'); $reply_handler->setMailReceiver($commit); return $reply_handler; } private function renderMailBody( PhabricatorAuditComment $comment, $cname, PhabricatorObjectHandle $handle, PhabricatorMailReplyHandler $reply_handler, array $inline_comments) { assert_instances_of($inline_comments, 'PhabricatorInlineCommentInterface'); $commit = $this->commit; - $user = $this->user; - $name = $user->getUsername(); + $actor = $this->getActor(); + $name = $actor->getUsername(); $verb = PhabricatorAuditActionConstants::getActionPastTenseVerb( $comment->getAction()); $body = new PhabricatorMetaMTAMailBody(); $body->addRawSection("{$name} {$verb} commit {$cname}."); $body->addRawSection($comment->getContent()); if ($inline_comments) { $block = array(); $path_map = id(new DiffusionPathQuery()) ->withPathIDs(mpull($inline_comments, 'getPathID')) ->execute(); $path_map = ipull($path_map, 'path', 'id'); foreach ($inline_comments as $inline) { $path = idx($path_map, $inline->getPathID()); if ($path === null) { continue; } $start = $inline->getLineNumber(); $len = $inline->getLineLength(); if ($len) { $range = $start.'-'.($start + $len); } else { $range = $start; } $content = $inline->getContent(); $block[] = "{$path}:{$range} {$content}"; } $body->addTextSection(pht('INLINE COMMENTS'), implode("\n", $block)); } $body->addTextSection( pht('COMMIT'), PhabricatorEnv::getProductionURI($handle->getURI())); $body->addReplySection($reply_handler->getReplyHandlerInstructions()); return $body->render(); } } diff --git a/src/applications/conduit/method/differential/ConduitAPI_differential_close_Method.php b/src/applications/conduit/method/differential/ConduitAPI_differential_close_Method.php index bcfe5a67f0..894347ed45 100644 --- a/src/applications/conduit/method/differential/ConduitAPI_differential_close_Method.php +++ b/src/applications/conduit/method/differential/ConduitAPI_differential_close_Method.php @@ -1,77 +1,77 @@ 'required int', ); } public function defineReturnType() { return 'void'; } public function defineErrorTypes() { return array( 'ERR_NOT_FOUND' => 'Revision was not found.', ); } protected function execute(ConduitAPIRequest $request) { $id = $request->getValue('revisionID'); $revision = id(new DifferentialRevision())->load($id); if (!$revision) { throw new ConduitException('ERR_NOT_FOUND'); } if ($revision->getStatus() == ArcanistDifferentialRevisionStatus::CLOSED) { // This can occur if someone runs 'close-revision' and hits a race, or // they have a remote hook installed but don't have the // 'remote_hook_installed' flag set, or similar. In any case, just treat // it as a no-op rather than adding another "X closed this revision" // message to the revision comments. return; } $revision->loadRelationships(); $editor = new DifferentialCommentEditor( $revision, - $request->getUser()->getPHID(), DifferentialAction::ACTION_CLOSE); + $editor->setActor($request->getUser()); $editor->save(); $revision->setStatus(ArcanistDifferentialRevisionStatus::CLOSED); $revision->setDateCommitted(time()); $revision->save(); return; } } diff --git a/src/applications/conduit/method/differential/ConduitAPI_differential_createcomment_Method.php b/src/applications/conduit/method/differential/ConduitAPI_differential_createcomment_Method.php index 0eacd2efc2..93b7f9a0c2 100644 --- a/src/applications/conduit/method/differential/ConduitAPI_differential_createcomment_Method.php +++ b/src/applications/conduit/method/differential/ConduitAPI_differential_createcomment_Method.php @@ -1,79 +1,79 @@ 'required revisionid', 'message' => 'optional string', 'action' => 'optional string', 'silent' => 'optional bool', ); } public function defineReturnType() { return 'nonempty dict'; } public function defineErrorTypes() { return array( 'ERR_BAD_REVISION' => 'Bad revision ID.', ); } protected function execute(ConduitAPIRequest $request) { $revision = id(new DifferentialRevision())->load( $request->getValue('revision_id')); if (!$revision) { throw new ConduitException('ERR_BAD_REVISION'); } $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_CONDUIT, array()); $action = $request->getValue('action'); if (!$action) { $action = 'none'; } $editor = new DifferentialCommentEditor( $revision, - $request->getUser()->getPHID(), $action); + $editor->setActor($request->getUser()); $editor->setContentSource($content_source); $editor->setMessage($request->getValue('message')); $editor->setNoEmail($request->getValue('silent')); $editor->save(); return array( 'revisionid' => $revision->getID(), 'uri' => PhabricatorEnv::getURI('/D'.$revision->getID()), ); } } diff --git a/src/applications/conduit/method/differential/ConduitAPI_differential_createrevision_Method.php b/src/applications/conduit/method/differential/ConduitAPI_differential_createrevision_Method.php index a6ca59fbbd..dd3321f0c0 100644 --- a/src/applications/conduit/method/differential/ConduitAPI_differential_createrevision_Method.php +++ b/src/applications/conduit/method/differential/ConduitAPI_differential_createrevision_Method.php @@ -1,65 +1,65 @@ 'required diffid', 'fields' => 'required dict', ); } public function defineReturnType() { return 'nonempty dict'; } public function defineErrorTypes() { return array( 'ERR_BAD_DIFF' => 'Bad diff ID.', ); } protected function execute(ConduitAPIRequest $request) { $fields = $request->getValue('fields'); $diff = id(new DifferentialDiff())->load($request->getValue('diffid')); if (!$diff) { throw new ConduitException('ERR_BAD_DIFF'); } $revision = DifferentialRevisionEditor::newRevisionFromConduitWithDiff( $fields, $diff, - $request->getUser()->getPHID()); + $request->getUser()); return array( 'revisionid' => $revision->getID(), 'uri' => PhabricatorEnv::getURI('/D'.$revision->getID()), ); } } diff --git a/src/applications/conduit/method/differential/ConduitAPI_differential_markcommitted_Method.php b/src/applications/conduit/method/differential/ConduitAPI_differential_markcommitted_Method.php index 47163a3561..8c836421e1 100644 --- a/src/applications/conduit/method/differential/ConduitAPI_differential_markcommitted_Method.php +++ b/src/applications/conduit/method/differential/ConduitAPI_differential_markcommitted_Method.php @@ -1,75 +1,75 @@ 'required revision_id', ); } public function defineReturnType() { return 'void'; } public function defineErrorTypes() { return array( 'ERR_NOT_FOUND' => 'Revision was not found.', ); } protected function execute(ConduitAPIRequest $request) { $id = $request->getValue('revision_id'); $revision = id(new DifferentialRevision())->load($id); if (!$revision) { throw new ConduitException('ERR_NOT_FOUND'); } if ($revision->getStatus() == ArcanistDifferentialRevisionStatus::CLOSED) { return; } $revision->loadRelationships(); $editor = new DifferentialCommentEditor( $revision, - $request->getUser()->getPHID(), DifferentialAction::ACTION_CLOSE); + $editor->setActor($request->getUser()); $editor->save(); } } diff --git a/src/applications/conduit/method/differential/ConduitAPI_differential_updaterevision_Method.php b/src/applications/conduit/method/differential/ConduitAPI_differential_updaterevision_Method.php index 19b9338af0..1a94a74063 100644 --- a/src/applications/conduit/method/differential/ConduitAPI_differential_updaterevision_Method.php +++ b/src/applications/conduit/method/differential/ConduitAPI_differential_updaterevision_Method.php @@ -1,90 +1,90 @@ 'required revisionid', 'diffid' => 'required diffid', 'fields' => 'required dict', 'message' => 'required string', ); } public function defineReturnType() { return 'nonempty dict'; } public function defineErrorTypes() { return array( 'ERR_BAD_DIFF' => 'Bad diff ID.', 'ERR_BAD_REVISION' => 'Bad revision ID.', 'ERR_WRONG_USER' => 'You are not the author of this revision.', 'ERR_CLOSED' => 'This revision has already been closed.', ); } protected function execute(ConduitAPIRequest $request) { $diff = id(new DifferentialDiff())->load($request->getValue('diffid')); if (!$diff) { throw new ConduitException('ERR_BAD_DIFF'); } $revision = id(new DifferentialRevision())->load($request->getValue('id')); if (!$revision) { throw new ConduitException('ERR_BAD_REVISION'); } if ($request->getUser()->getPHID() !== $revision->getAuthorPHID()) { throw new ConduitException('ERR_WRONG_USER'); } if ($revision->getStatus() == ArcanistDifferentialRevisionStatus::CLOSED) { throw new ConduitException('ERR_CLOSED'); } $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_CONDUIT, array()); $editor = new DifferentialRevisionEditor( - $revision, - $revision->getAuthorPHID()); + $revision); + $editor->setActor($request->getUser()); $editor->setContentSource($content_source); $fields = $request->getValue('fields'); $editor->copyFieldsFromConduit($fields); $editor->addDiff($diff, $request->getValue('message')); $editor->save(); return array( 'revisionid' => $revision->getID(), 'uri' => PhabricatorEnv::getURI('/D'.$revision->getID()), ); } } diff --git a/src/applications/conduit/method/maniphest/ConduitAPI_maniphest_Method.php b/src/applications/conduit/method/maniphest/ConduitAPI_maniphest_Method.php index 2855d221a6..be9e18c127 100644 --- a/src/applications/conduit/method/maniphest/ConduitAPI_maniphest_Method.php +++ b/src/applications/conduit/method/maniphest/ConduitAPI_maniphest_Method.php @@ -1,274 +1,275 @@ 'Missing or malformed parameter.' ); } protected function buildTaskInfoDictionary(ManiphestTask $task) { $results = $this->buildTaskInfoDictionaries(array($task)); return idx($results, $task->getPHID()); } protected function getTaskFields($is_new) { $fields = array(); if (!$is_new) { $fields += array( 'id' => 'optional int', 'phid' => 'optional int', ); } $fields += array( 'title' => $is_new ? 'required string' : 'optional string', 'description' => 'optional string', 'ownerPHID' => 'optional phid', 'ccPHIDs' => 'optional list', 'priority' => 'optional int', 'projectPHIDs' => 'optional list', 'filePHIDs' => 'optional list', 'auxiliary' => 'optional dict', ); if (!$is_new) { $fields += array( 'status' => 'optional int', 'comments' => 'optional string', ); } return $fields; } protected function applyRequest( ManiphestTask $task, ConduitAPIRequest $request, $is_new) { $changes = array(); if ($is_new) { $task->setTitle((string)$request->getValue('title')); $task->setDescription((string)$request->getValue('description')); $changes[ManiphestTransactionType::TYPE_STATUS] = ManiphestTaskStatus::STATUS_OPEN; } else { $comments = $request->getValue('comments'); if (!$is_new && $comments !== null) { $changes[ManiphestTransactionType::TYPE_NONE] = null; } $title = $request->getValue('title'); if ($title !== null) { $changes[ManiphestTransactionType::TYPE_TITLE] = $title; } $desc = $request->getValue('description'); if ($desc !== null) { $changes[ManiphestTransactionType::TYPE_DESCRIPTION] = $desc; } $status = $request->getValue('status'); if ($status !== null) { $valid_statuses = ManiphestTaskStatus::getTaskStatusMap(); if (!isset($valid_statuses[$status])) { throw id(new ConduitException('ERR-INVALID-PARAMETER')) ->setErrorDescription('Status set to invalid value.'); } $changes[ManiphestTransactionType::TYPE_STATUS] = $status; } } $priority = $request->getValue('priority'); if ($priority !== null) { $valid_priorities = ManiphestTaskPriority::getTaskPriorityMap(); if (!isset($valid_priorities[$priority])) { throw id(new ConduitException('ERR-INVALID-PARAMETER')) ->setErrorDescription('Priority set to invalid value.'); } $changes[ManiphestTransactionType::TYPE_PRIORITY] = $priority; } $owner_phid = $request->getValue('ownerPHID'); if ($owner_phid !== null) { $this->validatePHIDList(array($owner_phid), PhabricatorPHIDConstants::PHID_TYPE_USER, 'ownerPHID'); $changes[ManiphestTransactionType::TYPE_OWNER] = $owner_phid; } $ccs = $request->getValue('ccPHIDs'); if ($ccs !== null) { $this->validatePHIDList($ccs, PhabricatorPHIDConstants::PHID_TYPE_USER, 'ccPHIDS'); $changes[ManiphestTransactionType::TYPE_CCS] = $ccs; } $project_phids = $request->getValue('projectPHIDs'); if ($project_phids !== null) { $this->validatePHIDList($project_phids, PhabricatorPHIDConstants::PHID_TYPE_PROJ, 'projectPHIDS'); $changes[ManiphestTransactionType::TYPE_PROJECTS] = $project_phids; } $file_phids = $request->getValue('filePHIDs'); if ($file_phids !== null) { $this->validatePHIDList($file_phids, PhabricatorPHIDConstants::PHID_TYPE_FILE, 'filePHIDS'); $file_map = array_fill_keys($file_phids, true); $attached = $task->getAttached(); $attached[PhabricatorPHIDConstants::PHID_TYPE_FILE] = $file_map; $changes[ManiphestTransactionType::TYPE_ATTACH] = $attached; } $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_CONDUIT, array()); $template = new ManiphestTransaction(); $template->setContentSource($content_source); $template->setAuthorPHID($request->getUser()->getPHID()); $transactions = array(); foreach ($changes as $type => $value) { $transaction = clone $template; $transaction->setTransactionType($type); $transaction->setNewValue($value); if ($type == ManiphestTransactionType::TYPE_NONE) { $transaction->setComments($comments); } $transactions[] = $transaction; } $auxiliary = $request->getValue('auxiliary'); if ($auxiliary) { $task->loadAndAttachAuxiliaryAttributes(); foreach ($auxiliary as $aux_key => $aux_value) { $transaction = clone $template; $transaction->setTransactionType( ManiphestTransactionType::TYPE_AUXILIARY); $transaction->setMetadataValue('aux:key', $aux_key); $transaction->setNewValue($aux_value); $transactions[] = $transaction; } } $event = new PhabricatorEvent( PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK, array( 'task' => $task, 'new' => $is_new, 'transactions' => $transactions, )); $event->setUser($request->getUser()); $event->setConduitRequest($request); PhutilEventEngine::dispatchEvent($event); $task = $event->getValue('task'); $transactions = $event->getValue('transactions'); $editor = new ManiphestTransactionEditor(); + $editor->setActor($request->getUser()); $editor->applyTransactions($task, $transactions); $event = new PhabricatorEvent( PhabricatorEventType::TYPE_MANIPHEST_DIDEDITTASK, array( 'task' => $task, 'new' => $is_new, 'transactions' => $transactions, )); $event->setUser($request->getUser()); $event->setConduitRequest($request); PhutilEventEngine::dispatchEvent($event); } protected function buildTaskInfoDictionaries(array $tasks) { assert_instances_of($tasks, 'ManiphestTask'); if (!$tasks) { return array(); } $all_aux = id(new ManiphestTaskAuxiliaryStorage())->loadAllWhere( 'taskPHID in (%Ls)', mpull($tasks, 'getPHID')); $all_aux = mgroup($all_aux, 'getTaskPHID'); $result = array(); foreach ($tasks as $task) { $auxiliary = idx($all_aux, $task->getPHID(), array()); $auxiliary = mpull($auxiliary, 'getValue', 'getName'); $result[$task->getPHID()] = array( 'id' => $task->getID(), 'phid' => $task->getPHID(), 'authorPHID' => $task->getAuthorPHID(), 'ownerPHID' => $task->getOwnerPHID(), 'ccPHIDs' => $task->getCCPHIDs(), 'status' => $task->getStatus(), 'priority' => ManiphestTaskPriority::getTaskPriorityName( $task->getPriority()), 'title' => $task->getTitle(), 'description' => $task->getDescription(), 'projectPHIDs' => $task->getProjectPHIDs(), 'uri' => PhabricatorEnv::getProductionURI('/T'.$task->getID()), 'auxiliary' => $auxiliary, 'objectName' => 'T'.$task->getID(), 'dateCreated' => $task->getDateCreated(), 'dateModified' => $task->getDateModified(), ); } return $result; } /** * Note this is a temporary stop gap since its easy to make malformed Tasks. * Long-term, the values set in @{method:defineParamTypes} will be used to * validate data implicitly within the larger Conduit application. * * TODO -- remove this in favor of generalized Conduit hotness */ private function validatePHIDList(array $phid_list, $phid_type, $field) { $phid_groups = phid_group_by_type($phid_list); unset($phid_groups[$phid_type]); if (!empty($phid_groups)) { throw id(new ConduitException('ERR-INVALID-PARAMETER')) ->setErrorDescription( 'One or more PHIDs were invalid for '.$field.'.' ); } return true; } } diff --git a/src/applications/conduit/method/phriction/ConduitAPI_phriction_edit_Method.php b/src/applications/conduit/method/phriction/ConduitAPI_phriction_edit_Method.php index 708822891e..8d22a2f9bb 100644 --- a/src/applications/conduit/method/phriction/ConduitAPI_phriction_edit_Method.php +++ b/src/applications/conduit/method/phriction/ConduitAPI_phriction_edit_Method.php @@ -1,60 +1,60 @@ 'required string', 'title' => 'optional string', 'content' => 'optional string', 'description' => 'optional string', ); } public function defineReturnType() { return 'nonempty dict'; } public function defineErrorTypes() { return array( ); } protected function execute(ConduitAPIRequest $request) { $slug = $request->getValue('slug'); $editor = id(PhrictionDocumentEditor::newForSlug($slug)) - ->setUser($request->getUser()) + ->setActor($request->getUser()) ->setTitle($request->getValue('title')) ->setContent($request->getValue('content')) ->setDescription($request->getvalue('description')) ->save(); return $this->buildDocumentInfoDictionary($editor->getDocument()); } } diff --git a/src/applications/differential/DifferentialReplyHandler.php b/src/applications/differential/DifferentialReplyHandler.php index fc935b7691..49e03155a1 100644 --- a/src/applications/differential/DifferentialReplyHandler.php +++ b/src/applications/differential/DifferentialReplyHandler.php @@ -1,186 +1,188 @@ getDefaultPrivateReplyHandlerEmailAddress($handle, 'D'); } public function getPublicReplyHandlerEmailAddress() { return $this->getDefaultPublicReplyHandlerEmailAddress('D'); } public function getReplyHandlerDomain() { return PhabricatorEnv::getEnvConfig( 'metamta.differential.reply-handler-domain'); } /* * Generate text like the following from the supported commands. * " * * ACTIONS * Reply to comment, or !accept, !reject, !abandon, !resign, !reclaim. * * " */ public function getReplyHandlerInstructions() { if (!$this->supportsReplies()) { return null; } $supported_commands = $this->getSupportedCommands(); $text = ''; if (empty($supported_commands)) { return $text; } $comment_command_printed = false; if (in_array(DifferentialAction::ACTION_COMMENT, $supported_commands)) { $text .= 'Reply to comment'; $comment_command_printed = true; $supported_commands = array_diff( $supported_commands, array(DifferentialAction::ACTION_COMMENT)); } if (!empty($supported_commands)) { if ($comment_command_printed) { $text .= ', or '; } $modified_commands = array(); foreach ($supported_commands as $command) { $modified_commands[] = '!'.$command; } $text .= implode(', ', $modified_commands); } $text .= "."; return $text; } public function getSupportedCommands() { $actions = array( DifferentialAction::ACTION_COMMENT, DifferentialAction::ACTION_REJECT, DifferentialAction::ACTION_ABANDON, DifferentialAction::ACTION_RECLAIM, DifferentialAction::ACTION_RESIGN, DifferentialAction::ACTION_RETHINK, 'unsubscribe', ); if (PhabricatorEnv::getEnvConfig('differential.enable-email-accept')) { $actions[] = DifferentialAction::ACTION_ACCEPT; } return $actions; } protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) { $this->receivedMail = $mail; $this->handleAction($mail->getCleanTextBody()); } public function handleAction($body) { // all commands start with a bang and separated from the body by a newline // to make sure that actual feedback text couldn't trigger an action. // unrecognized commands will be parsed as part of the comment. $command = DifferentialAction::ACTION_COMMENT; $supported_commands = $this->getSupportedCommands(); $regex = "/\A\n*!(" . implode('|', $supported_commands) . ")\n*/"; $matches = array(); if (preg_match($regex, $body, $matches)) { $command = $matches[1]; $body = trim(str_replace('!' . $command, '', $body)); } $actor = $this->getActor(); if (!$actor) { throw new Exception('No actor is set for the reply action.'); } switch ($command) { case 'unsubscribe': $this->unsubscribeUser($this->getMailReceiver(), $actor); // TODO: Send the user a confirmation email? return null; } try { $editor = new DifferentialCommentEditor( $this->getMailReceiver(), - $actor->getPHID(), $command); + $editor->setActor($actor); + $editor->setExcludeMailRecipientPHIDs( + $this->getExcludeMailRecipientPHIDs()); // NOTE: We have to be careful about this because Facebook's // implementation jumps straight into handleAction() and will not have // a PhabricatorMetaMTAReceivedMail object. if ($this->receivedMail) { $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_EMAIL, array( 'id' => $this->receivedMail->getID(), )); $editor->setContentSource($content_source); $editor->setParentMessageID($this->receivedMail->getMessageID()); } $editor->setMessage($body); $comment = $editor->save(); return $comment->getID(); } catch (Exception $ex) { $exception_mail = new DifferentialExceptionMail( $this->getMailReceiver(), $ex, $this->receivedMail->getRawTextBody()); $exception_mail->setToPHIDs(array($this->getActor()->getPHID())); $exception_mail->send(); throw $ex; } } private function unsubscribeUser( DifferentialRevision $revision, PhabricatorUser $user) { $revision->loadRelationships(); DifferentialRevisionEditor::removeCCAndUpdateRevision( $revision, $user->getPHID(), $user->getPHID()); } } diff --git a/src/applications/differential/controller/DifferentialCommentSaveController.php b/src/applications/differential/controller/DifferentialCommentSaveController.php index cff04d2259..f5af856df9 100644 --- a/src/applications/differential/controller/DifferentialCommentSaveController.php +++ b/src/applications/differential/controller/DifferentialCommentSaveController.php @@ -1,102 +1,102 @@ getRequest(); if (!$request->isFormPost()) { return new Aphront400Response(); } $revision_id = $request->getInt('revision_id'); $revision = id(new DifferentialRevision())->load($revision_id); if (!$revision) { return new Aphront400Response(); } $comment = $request->getStr('comment'); $action = $request->getStr('action'); $reviewers = $request->getArr('reviewers'); $ccs = $request->getArr('ccs'); $editor = new DifferentialCommentEditor( $revision, - $request->getUser()->getPHID(), $action); $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_WEB, array( 'ip' => $request->getRemoteAddr(), )); try { $editor + ->setActor($request->getUser()) ->setMessage($comment) ->setContentSource($content_source) ->setAttachInlineComments(true) ->setAddedReviewers($reviewers) ->setAddedCCs($ccs) ->save(); } catch (DifferentialActionHasNoEffectException $no_effect) { $has_inlines = id(new DifferentialInlineComment())->loadAllWhere( 'authorPHID = %s AND revisionID = %d AND commentID IS NULL', $request->getUser()->getPHID(), $revision->getID()); $dialog = new AphrontDialogView(); $dialog->setUser($request->getUser()); $dialog->addCancelButton('/D'.$revision_id); $dialog->addHiddenInput('revision_id', $revision_id); $dialog->addHiddenInput('action', 'none'); $dialog->addHiddenInput('reviewers', $reviewers); $dialog->addHiddenInput('ccs', $ccs); $dialog->addHiddenInput('comment', $comment); $dialog->setTitle('Action Has No Effect'); $dialog->appendChild( '

'.phutil_escape_html($no_effect->getMessage()).'

'); if (strlen($comment) || $has_inlines) { $dialog->addSubmitButton('Post as Comment'); $dialog->appendChild('
'); $dialog->appendChild( '

Do you want to post your feedback anyway, as a normal '. 'comment?

'); } return id(new AphrontDialogResponse())->setDialog($dialog); } // TODO: Diff change detection? $draft = id(new PhabricatorDraft())->loadOneWhere( 'authorPHID = %s AND draftKey = %s', $request->getUser()->getPHID(), 'differential-comment-'.$revision->getID()); if ($draft) { $draft->delete(); } return id(new AphrontRedirectResponse()) ->setURI('/D'.$revision->getID()); } } diff --git a/src/applications/differential/controller/DifferentialRevisionEditController.php b/src/applications/differential/controller/DifferentialRevisionEditController.php index 3442bfeabf..c71c3f5082 100644 --- a/src/applications/differential/controller/DifferentialRevisionEditController.php +++ b/src/applications/differential/controller/DifferentialRevisionEditController.php @@ -1,192 +1,191 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); if (!$this->id) { $this->id = $request->getInt('revisionID'); } if ($this->id) { $revision = id(new DifferentialRevision())->load($this->id); if (!$revision) { return new Aphront404Response(); } } else { $revision = new DifferentialRevision(); } $revision->loadRelationships(); $aux_fields = $this->loadAuxiliaryFields($revision); $diff_id = $request->getInt('diffID'); if ($diff_id) { $diff = id(new DifferentialDiff())->load($diff_id); if (!$diff) { return new Aphront404Response(); } if ($diff->getRevisionID()) { // TODO: Redirect? throw new Exception("This diff is already attached to a revision!"); } } else { $diff = null; } $errors = array(); if ($request->isFormPost() && !$request->getStr('viaDiffView')) { - $user_phid = $request->getUser()->getPHID(); - foreach ($aux_fields as $aux_field) { $aux_field->setValueFromRequest($request); try { $aux_field->validateField(); } catch (DifferentialFieldValidationException $ex) { $errors[] = $ex->getMessage(); } } if (!$errors) { - $editor = new DifferentialRevisionEditor($revision, $user_phid); + $editor = new DifferentialRevisionEditor($revision); + $editor->setActor($request->getUser()); if ($diff) { $editor->addDiff($diff, $request->getStr('comments')); } $editor->setAuxiliaryFields($aux_fields); $editor->save(); return id(new AphrontRedirectResponse()) ->setURI('/D'.$revision->getID()); } } $aux_phids = array(); foreach ($aux_fields as $key => $aux_field) { $aux_phids[$key] = $aux_field->getRequiredHandlePHIDsForRevisionEdit(); } $phids = array_mergev($aux_phids); $phids = array_unique($phids); $handles = $this->loadViewerHandles($phids); foreach ($aux_fields as $key => $aux_field) { $aux_field->setHandles(array_select_keys($handles, $aux_phids[$key])); } $form = new AphrontFormView(); $form->setUser($request->getUser()); if ($diff) { $form->addHiddenInput('diffID', $diff->getID()); } if ($revision->getID()) { $form->setAction('/differential/revision/edit/'.$revision->getID().'/'); } else { $form->setAction('/differential/revision/edit/'); } $error_view = null; if ($errors) { $error_view = id(new AphrontErrorView()) ->setTitle('Form Errors') ->setErrors($errors); } if ($diff && $revision->getID()) { $form ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel('Comments') ->setName('comments') ->setCaption("Explain what's new in this diff.") ->setValue($request->getStr('comments'))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue('Save')) ->appendChild( id(new AphrontFormDividerControl())); } foreach ($aux_fields as $aux_field) { $control = $aux_field->renderEditControl(); if ($control) { $form->appendChild($control); } } $submit = id(new AphrontFormSubmitControl()) ->setValue('Save'); if ($diff) { $submit->addCancelButton('/differential/diff/'.$diff->getID().'/'); } else { $submit->addCancelButton('/D'.$revision->getID()); } $form->appendChild($submit); $panel = new AphrontPanelView(); if ($revision->getID()) { if ($diff) { $panel->setHeader('Update Differential Revision'); } else { $panel->setHeader('Edit Differential Revision'); } } else { $panel->setHeader('Create New Differential Revision'); } $panel->appendChild($form); $panel->setWidth(AphrontPanelView::WIDTH_FORM); return $this->buildStandardPageResponse( array($error_view, $panel), array( 'title' => 'Edit Differential Revision', )); } private function loadAuxiliaryFields(DifferentialRevision $revision) { $user = $this->getRequest()->getUser(); $aux_fields = DifferentialFieldSelector::newSelector() ->getFieldSpecifications(); foreach ($aux_fields as $key => $aux_field) { $aux_field->setRevision($revision); if (!$aux_field->shouldAppearOnEdit()) { unset($aux_fields[$key]); } else { $aux_field->setUser($user); } } return DifferentialAuxiliaryField::loadFromStorage( $revision, $aux_fields); } } diff --git a/src/applications/differential/editor/DifferentialCommentEditor.php b/src/applications/differential/editor/DifferentialCommentEditor.php index 82817cd730..db2e699410 100644 --- a/src/applications/differential/editor/DifferentialCommentEditor.php +++ b/src/applications/differential/editor/DifferentialCommentEditor.php @@ -1,681 +1,680 @@ revision = $revision; - $this->actorPHID = $actor_phid; $this->action = $action; } public function setParentMessageID($parent_message_id) { $this->parentMessageID = $parent_message_id; return $this; } public function setMessage($message) { $this->message = $message; return $this; } public function setAttachInlineComments($attach) { $this->attachInlineComments = $attach; return $this; } public function setChangedByCommit($changed_by_commit) { $this->changedByCommit = $changed_by_commit; return $this; } public function getChangedByCommit() { return $this->changedByCommit; } public function setAddedReviewers(array $added_reviewers) { $this->addedReviewers = $added_reviewers; return $this; } public function getAddedReviewers() { return $this->addedReviewers; } public function setRemovedReviewers(array $removeded_reviewers) { $this->removedReviewers = $removeded_reviewers; return $this; } public function getRemovedReviewers() { return $this->removedReviewers; } public function setAddedCCs($added_ccs) { $this->addedCCs = $added_ccs; return $this; } public function getAddedCCs() { return $this->addedCCs; } public function setContentSource(PhabricatorContentSource $content_source) { $this->contentSource = $content_source; return $this; } public function setIsDaemonWorkflow($is_daemon) { $this->isDaemonWorkflow = $is_daemon; return $this; } public function setNoEmail($no_email) { $this->noEmail = $no_email; return $this; } public function save() { - $revision = $this->revision; - $action = $this->action; - $actor_phid = $this->actorPHID; - $actor = id(new PhabricatorUser())->loadOneWhere('PHID = %s', $actor_phid); - $actor_is_author = ($actor_phid == $revision->getAuthorPHID()); - $allow_self_accept = PhabricatorEnv::getEnvConfig( + $actor = $this->requireActor(); + $revision = $this->revision; + $action = $this->action; + $actor_phid = $actor->getPHID(); + $actor_is_author = ($actor_phid == $revision->getAuthorPHID()); + $allow_self_accept = PhabricatorEnv::getEnvConfig( 'differential.allow-self-accept', false); $always_allow_close = PhabricatorEnv::getEnvConfig( 'differential.always-allow-close', false); - $revision_status = $revision->getStatus(); + $revision_status = $revision->getStatus(); $revision->loadRelationships(); $reviewer_phids = $revision->getReviewers(); if ($reviewer_phids) { $reviewer_phids = array_combine($reviewer_phids, $reviewer_phids); } $metadata = array(); $inline_comments = array(); if ($this->attachInlineComments) { $inline_comments = id(new DifferentialInlineComment())->loadAllWhere( 'authorPHID = %s AND revisionID = %d AND commentID IS NULL', - $this->actorPHID, + $actor_phid, $revision->getID()); } switch ($action) { case DifferentialAction::ACTION_COMMENT: if (!$this->message && !$inline_comments) { throw new DifferentialActionHasNoEffectException( "You are submitting an empty comment with no action: ". "you must act on the revision or post a comment."); } break; case DifferentialAction::ACTION_RESIGN: if ($actor_is_author) { throw new Exception('You can not resign from your own revision!'); } if (empty($reviewer_phids[$actor_phid])) { throw new DifferentialActionHasNoEffectException( "You can not resign from this revision because you are not ". "a reviewer."); } DifferentialRevisionEditor::alterReviewers( $revision, $reviewer_phids, $rem = array($actor_phid), $add = array(), $actor_phid); break; case DifferentialAction::ACTION_ABANDON: if (!$actor_is_author) { throw new Exception('You can only abandon your own revisions.'); } if ($revision_status == ArcanistDifferentialRevisionStatus::CLOSED) { throw new DifferentialActionHasNoEffectException( "You can not abandon this revision because it has already ". "been closed."); } if ($revision_status == ArcanistDifferentialRevisionStatus::ABANDONED) { throw new DifferentialActionHasNoEffectException( "You can not abandon this revision because it has already ". "been abandoned."); } $revision->setStatus(ArcanistDifferentialRevisionStatus::ABANDONED); break; case DifferentialAction::ACTION_ACCEPT: if ($actor_is_author && !$allow_self_accept) { throw new Exception('You can not accept your own revision.'); } if (($revision_status != ArcanistDifferentialRevisionStatus::NEEDS_REVIEW) && ($revision_status != ArcanistDifferentialRevisionStatus::NEEDS_REVISION)) { switch ($revision_status) { case ArcanistDifferentialRevisionStatus::ACCEPTED: throw new DifferentialActionHasNoEffectException( "You can not accept this revision because someone else ". "already accepted it."); case ArcanistDifferentialRevisionStatus::ABANDONED: throw new DifferentialActionHasNoEffectException( "You can not accept this revision because it has been ". "abandoned."); case ArcanistDifferentialRevisionStatus::CLOSED: throw new DifferentialActionHasNoEffectException( "You can not accept this revision because it has already ". "been closed."); default: throw new Exception( "Unexpected revision state '{$revision_status}'!"); } } $revision ->setStatus(ArcanistDifferentialRevisionStatus::ACCEPTED); if (!isset($reviewer_phids[$actor_phid])) { DifferentialRevisionEditor::alterReviewers( $revision, $reviewer_phids, $rem = array(), $add = array($actor_phid), $actor_phid); } break; case DifferentialAction::ACTION_REQUEST: if (!$actor_is_author) { throw new Exception('You must own a revision to request review.'); } switch ($revision_status) { case ArcanistDifferentialRevisionStatus::ACCEPTED: case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: $revision->setStatus( ArcanistDifferentialRevisionStatus::NEEDS_REVIEW); break; case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: throw new DifferentialActionHasNoEffectException( "You can not request review of this revision because it has ". "been abandoned."); case ArcanistDifferentialRevisionStatus::ABANDONED: throw new DifferentialActionHasNoEffectException( "You can not request review of this revision because it has ". "been abandoned."); case ArcanistDifferentialRevisionStatus::CLOSED: throw new DifferentialActionHasNoEffectException( "You can not request review of this revision because it has ". "already been closed."); default: throw new Exception( "Unexpected revision state '{$revision_status}'!"); } list($added_reviewers, $ignored) = $this->alterReviewers(); if ($added_reviewers) { $key = DifferentialComment::METADATA_ADDED_REVIEWERS; $metadata[$key] = $added_reviewers; } break; case DifferentialAction::ACTION_REJECT: if ($actor_is_author) { throw new Exception( 'You can not request changes to your own revision.'); } switch ($revision_status) { case ArcanistDifferentialRevisionStatus::ACCEPTED: case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: // NOTE: We allow you to reject an already-rejected revision // because it doesn't create any ambiguity and avoids a rather // needless dialog. break; case ArcanistDifferentialRevisionStatus::ABANDONED: throw new DifferentialActionHasNoEffectException( "You can not request changes to this revision because it has ". "been abandoned."); case ArcanistDifferentialRevisionStatus::CLOSED: throw new DifferentialActionHasNoEffectException( "You can not request changes to this revision because it has ". "already been closed."); default: throw new Exception( "Unexpected revision state '{$revision_status}'!"); } if (!isset($reviewer_phids[$actor_phid])) { DifferentialRevisionEditor::alterReviewers( $revision, $reviewer_phids, $rem = array(), $add = array($actor_phid), $actor_phid); } $revision ->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVISION); break; case DifferentialAction::ACTION_RETHINK: if (!$actor_is_author) { throw new Exception( "You can not plan changes to somebody else's revision"); } switch ($revision_status) { case ArcanistDifferentialRevisionStatus::ACCEPTED: case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: break; case ArcanistDifferentialRevisionStatus::ABANDONED: throw new DifferentialActionHasNoEffectException( "You can not plan changes to this revision because it has ". "been abandoned."); case ArcanistDifferentialRevisionStatus::CLOSED: throw new DifferentialActionHasNoEffectException( "You can not plan changes to this revision because it has ". "already been closed."); default: throw new Exception( "Unexpected revision state '{$revision_status}'!"); } $revision ->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVISION); break; case DifferentialAction::ACTION_RECLAIM: if (!$actor_is_author) { throw new Exception('You can not reclaim a revision you do not own.'); } if ($revision_status != ArcanistDifferentialRevisionStatus::ABANDONED) { throw new DifferentialActionHasNoEffectException( "You can not reclaim this revision because it is not abandoned."); } $revision ->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVIEW); break; case DifferentialAction::ACTION_CLOSE: // NOTE: The daemons can mark things closed from any state. We treat // them as completely authoritative. if (!$this->isDaemonWorkflow) { if (!$actor_is_author && !$always_allow_close) { throw new Exception( "You can not mark a revision you don't own as closed."); } $status_closed = ArcanistDifferentialRevisionStatus::CLOSED; $status_accepted = ArcanistDifferentialRevisionStatus::ACCEPTED; if ($revision_status == $status_closed) { throw new DifferentialActionHasNoEffectException( "You can not mark this revision as closed because it has ". "already been marked as closed."); } if ($revision_status != $status_accepted) { throw new DifferentialActionHasNoEffectException( "You can not mark this revision as closed because it is ". "has not been accepted."); } } if (!$revision->getDateCommitted()) { $revision->setDateCommitted(time()); } $revision->setStatus(ArcanistDifferentialRevisionStatus::CLOSED); break; case DifferentialAction::ACTION_ADDREVIEWERS: list($added_reviewers, $ignored) = $this->alterReviewers(); if ($added_reviewers) { $key = DifferentialComment::METADATA_ADDED_REVIEWERS; $metadata[$key] = $added_reviewers; } else { $user_tried_to_add = count($this->getAddedReviewers()); if ($user_tried_to_add == 0) { throw new DifferentialActionHasNoEffectException( "You can not add reviewers, because you did not specify any ". "reviewers."); } else if ($user_tried_to_add == 1) { throw new DifferentialActionHasNoEffectException( "You can not add that reviewer, because they are already an ". "author or reviewer."); } else { throw new DifferentialActionHasNoEffectException( "You can not add those reviewers, because they are all already ". "authors or reviewers."); } } break; case DifferentialAction::ACTION_ADDCCS: $added_ccs = $this->getAddedCCs(); $user_tried_to_add = count($added_ccs); $added_ccs = $this->filterAddedCCs($added_ccs); if ($added_ccs) { foreach ($added_ccs as $cc) { DifferentialRevisionEditor::addCC( $revision, $cc, - $this->actorPHID); + $actor_phid); } $key = DifferentialComment::METADATA_ADDED_CCS; $metadata[$key] = $added_ccs; } else { if ($user_tried_to_add == 0) { throw new DifferentialActionHasNoEffectException( "You can not add CCs, because you did not specify any ". "CCs."); } else if ($user_tried_to_add == 1) { throw new DifferentialActionHasNoEffectException( "You can not add that CC, because they are already an ". "author, reviewer or CC."); } else { throw new DifferentialActionHasNoEffectException( "You can not add those CCs, because they are all already ". "authors, reviewers or CCs."); } } break; case DifferentialAction::ACTION_CLAIM: if ($actor_is_author) { throw new Exception("You can not commandeer your own revision."); } switch ($revision_status) { case ArcanistDifferentialRevisionStatus::CLOSED: throw new DifferentialActionHasNoEffectException( "You can not commandeer this revision because it has ". "already been closed."); break; } $this->setAddedReviewers(array($revision->getAuthorPHID())); $this->setRemovedReviewers(array($actor_phid)); // NOTE: Set the new author PHID before calling addReviewers(), since it // doesn't permit the author to become a reviewer. $revision->setAuthorPHID($actor_phid); list($added_reviewers, $removed_reviewers) = $this->alterReviewers(); if ($added_reviewers) { $key = DifferentialComment::METADATA_ADDED_REVIEWERS; $metadata[$key] = $added_reviewers; } if ($removed_reviewers) { $key = DifferentialComment::METADATA_REMOVED_REVIEWERS; $metadata[$key] = $removed_reviewers; } break; default: throw new Exception('Unsupported action.'); } // Update information about reviewer in charge. if ($action == DifferentialAction::ACTION_ACCEPT || $action == DifferentialAction::ACTION_REJECT) { $revision->setLastReviewerPHID($actor_phid); } // TODO: Call beginReadLocking() prior to loading the revision. $revision->openTransaction(); // Always save the revision (even if we didn't actually change any of its // properties) so that it jumps to the top of the revision list when sorted // by "updated". Notably, this allows "ping" comments to push it to the // top of the action list. $revision->save(); if ($action != DifferentialAction::ACTION_RESIGN) { DifferentialRevisionEditor::addCC( $revision, - $this->actorPHID, - $this->actorPHID); + $actor_phid, + $actor_phid); } $comment = id(new DifferentialComment()) - ->setAuthorPHID($this->actorPHID) + ->setAuthorPHID($actor_phid) ->setRevisionID($revision->getID()) ->setAction($action) ->setContent((string)$this->message) ->setMetadata($metadata); if ($this->contentSource) { $comment->setContentSource($this->contentSource); } $comment->save(); $changesets = array(); if ($inline_comments) { $load_ids = mpull($inline_comments, 'getChangesetID'); if ($load_ids) { $load_ids = array_unique($load_ids); $changesets = id(new DifferentialChangeset())->loadAllWhere( 'id in (%Ld)', $load_ids); } foreach ($inline_comments as $inline) { $inline->setCommentID($comment->getID()); $inline->save(); } } // Find any "@mentions" in the comment blocks. $content_blocks = array($comment->getContent()); foreach ($inline_comments as $inline) { $content_blocks[] = $inline->getContent(); } $mention_ccs = PhabricatorMarkupEngine::extractPHIDsFromMentions( $content_blocks); if ($mention_ccs) { $mention_ccs = $this->filterAddedCCs($mention_ccs); if ($mention_ccs) { $metadata = $comment->getMetadata(); $metacc = idx( $metadata, DifferentialComment::METADATA_ADDED_CCS, array()); foreach ($mention_ccs as $cc_phid) { DifferentialRevisionEditor::addCC( $revision, $cc_phid, - $this->actorPHID); + $actor_phid); $metacc[] = $cc_phid; } $metadata[DifferentialComment::METADATA_ADDED_CCS] = $metacc; $comment->setMetadata($metadata); $comment->save(); } } $revision->saveTransaction(); - $phids = array($this->actorPHID); + $phids = array($actor_phid); $handles = id(new PhabricatorObjectHandleData($phids)) ->loadHandles(); - $actor_handle = $handles[$this->actorPHID]; + $actor_handle = $handles[$actor_phid]; $xherald_header = HeraldTranscript::loadXHeraldRulesHeader( $revision->getPHID()); if (!$this->noEmail) { id(new DifferentialCommentMail( $revision, $actor_handle, $comment, $changesets, $inline_comments)) + ->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs()) ->setToPHIDs( array_merge( $revision->getReviewers(), array($revision->getAuthorPHID()))) ->setCCPHIDs($revision->getCCPHIDs()) ->setChangedByCommit($this->getChangedByCommit()) ->setXHeraldRulesHeader($xherald_header) ->setParentMessageID($this->parentMessageID) ->send(); } $event_data = array( 'revision_id' => $revision->getID(), 'revision_phid' => $revision->getPHID(), 'revision_name' => $revision->getTitle(), 'revision_author_phid' => $revision->getAuthorPHID(), 'action' => $comment->getAction(), 'feedback_content' => $comment->getContent(), - 'actor_phid' => $this->actorPHID, + 'actor_phid' => $actor_phid, ); id(new PhabricatorTimelineEvent('difx', $event_data)) ->recordEvent(); // TODO: Move to a daemon? id(new PhabricatorFeedStoryPublisher()) ->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_DIFFERENTIAL) ->setStoryData($event_data) ->setStoryTime(time()) - ->setStoryAuthorPHID($this->actorPHID) + ->setStoryAuthorPHID($actor_phid) ->setRelatedPHIDs( array( $revision->getPHID(), - $this->actorPHID, + $actor_phid, $revision->getAuthorPHID(), )) ->setPrimaryObjectPHID($revision->getPHID()) ->setSubscribedPHIDs( array_merge( array($revision->getAuthorPHID()), $revision->getReviewers(), $revision->getCCPHIDs())) ->publish(); // TODO: Move to a daemon? PhabricatorSearchDifferentialIndexer::indexRevision($revision); return $comment; } private function filterAddedCCs(array $ccs) { $revision = $this->revision; $current_ccs = $revision->getCCPHIDs(); $current_ccs = array_fill_keys($current_ccs, true); $reviewer_phids = $revision->getReviewers(); $reviewer_phids = array_fill_keys($reviewer_phids, true); foreach ($ccs as $key => $cc) { if (isset($current_ccs[$cc])) { unset($ccs[$key]); } if (isset($reviewer_phids[$cc])) { unset($ccs[$key]); } if ($cc == $revision->getAuthorPHID()) { unset($ccs[$key]); } } return $ccs; } private function alterReviewers() { - $revision = $this->revision; - $added_reviewers = $this->getAddedReviewers(); + $actor_phid = $this->getActor()->getPHID(); + $revision = $this->revision; + $added_reviewers = $this->getAddedReviewers(); $removed_reviewers = $this->getRemovedReviewers(); - $reviewer_phids = $revision->getReviewers(); + $reviewer_phids = $revision->getReviewers(); $reviewer_phids_map = array_fill_keys($reviewer_phids, true); foreach ($added_reviewers as $k => $user_phid) { if ($user_phid == $revision->getAuthorPHID()) { unset($added_reviewers[$k]); } if (isset($reviewer_phids_map[$user_phid])) { unset($added_reviewers[$k]); } } foreach ($removed_reviewers as $k => $user_phid) { if (!isset($reviewer_phids_map[$user_phid])) { unset($removed_reviewers[$k]); } } $added_reviewers = array_unique($added_reviewers); $removed_reviewers = array_unique($removed_reviewers); if ($added_reviewers) { DifferentialRevisionEditor::alterReviewers( $revision, $reviewer_phids, $removed_reviewers, $added_reviewers, - $this->actorPHID); + $actor_phid); } return array($added_reviewers, $removed_reviewers); } } diff --git a/src/applications/differential/editor/DifferentialRevisionEditor.php b/src/applications/differential/editor/DifferentialRevisionEditor.php index 62c1c3b64e..5436d99fff 100644 --- a/src/applications/differential/editor/DifferentialRevisionEditor.php +++ b/src/applications/differential/editor/DifferentialRevisionEditor.php @@ -1,968 +1,962 @@ revision = $revision; - $this->actorPHID = $actor_phid; } public static function newRevisionFromConduitWithDiff( array $fields, DifferentialDiff $diff, - $user_phid) { + PhabricatorUser $actor) { $revision = new DifferentialRevision(); $revision->setPHID($revision->generatePHID()); - - $revision->setAuthorPHID($user_phid); + $revision->setAuthorPHID($actor->getPHID()); $revision->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVIEW); - $editor = new DifferentialRevisionEditor($revision, $user_phid); - + $editor = new DifferentialRevisionEditor($revision); + $editor->setActor($actor); $editor->copyFieldsFromConduit($fields); $editor->addDiff($diff, null); $editor->save(); return $revision; } public function copyFieldsFromConduit(array $fields) { + $actor = $this->getActor(); $revision = $this->revision; $revision->loadRelationships(); $aux_fields = DifferentialFieldSelector::newSelector() ->getFieldSpecifications(); - $user = id(new PhabricatorUser())->loadOneWhere( - 'phid = %s', - $this->actorPHID); - foreach ($aux_fields as $key => $aux_field) { $aux_field->setRevision($revision); - $aux_field->setUser($user); + $aux_field->setUser($actor); if (!$aux_field->shouldAppearOnCommitMessage()) { unset($aux_fields[$key]); } } $aux_fields = mpull($aux_fields, null, 'getCommitMessageKey'); foreach ($fields as $field => $value) { if (empty($aux_fields[$field])) { throw new Exception( "Parsed commit message contains unrecognized field '{$field}'."); } $aux_fields[$field]->setValueFromParsedCommitMessage($value); } foreach ($aux_fields as $aux_field) { $aux_field->validateField(); } $aux_fields = array_values($aux_fields); $this->setAuxiliaryFields($aux_fields); } public function setAuxiliaryFields(array $auxiliary_fields) { assert_instances_of($auxiliary_fields, 'DifferentialFieldSpecification'); $this->auxiliaryFields = $auxiliary_fields; return $this; } public function getRevision() { return $this->revision; } public function setReviewers(array $reviewers) { $this->reviewers = $reviewers; return $this; } public function setCCPHIDs(array $cc) { $this->cc = $cc; return $this; } public function setContentSource(PhabricatorContentSource $content_source) { $this->contentSource = $content_source; return $this; } public function addDiff(DifferentialDiff $diff, $comments) { if ($diff->getRevisionID() && $diff->getRevisionID() != $this->getRevision()->getID()) { $diff_id = (int)$diff->getID(); $targ_id = (int)$this->getRevision()->getID(); $real_id = (int)$diff->getRevisionID(); throw new Exception( "Can not attach diff #{$diff_id} to Revision D{$targ_id}, it is ". "already attached to D{$real_id}."); } $this->diff = $diff; $this->comments = $comments; return $this; } protected function getDiff() { return $this->diff; } protected function getComments() { return $this->comments; } protected function getActorPHID() { return $this->actorPHID; } public function isNewRevision() { return !$this->getRevision()->getID(); } /** * A silent update does not trigger Herald rules or send emails. This is used * for auto-amends at commit time. */ public function setSilentUpdate($silent) { $this->silentUpdate = $silent; return $this; } public function save() { $revision = $this->getRevision(); $is_new = $this->isNewRevision(); if ($is_new) { $this->initializeNewRevision($revision); } $revision->loadRelationships(); $this->willWriteRevision(); if ($this->reviewers === null) { $this->reviewers = $revision->getReviewers(); } if ($this->cc === null) { $this->cc = $revision->getCCPHIDs(); } $diff = $this->getDiff(); if ($diff) { $revision->setLineCount($diff->getLineCount()); } // Save the revision, to generate its ID and PHID if it is new. We need // the ID/PHID in order to record them in Herald transcripts, but don't // want to hold a transaction open while running Herald because it is // potentially somewhat slow. The downside is that we may end up with a // saved revision/diff pair without appropriate CCs. We could be better // about this -- for example: // // - Herald can't affect reviewers, so we could compute them before // opening the transaction and then save them in the transaction. // - Herald doesn't *really* need PHIDs to compute its effects, we could // run it before saving these objects and then hand over the PHIDs later. // // But this should address the problem of orphaned revisions, which is // currently the only problem we experience in practice. $revision->openTransaction(); if ($diff) { $revision->setBranchName($diff->getBranch()); $revision->setArcanistProjectPHID($diff->getArcanistProjectPHID()); } $revision->save(); if ($diff) { $diff->setRevisionID($revision->getID()); $diff->save(); } $revision->saveTransaction(); // We're going to build up three dictionaries: $add, $rem, and $stable. The // $add dictionary has added reviewers/CCs. The $rem dictionary has // reviewers/CCs who have been removed, and the $stable array is // reviewers/CCs who haven't changed. We're going to send new reviewers/CCs // a different ("welcome") email than we send stable reviewers/CCs. $old = array( 'rev' => array_fill_keys($revision->getReviewers(), true), 'ccs' => array_fill_keys($revision->getCCPHIDs(), true), ); $xscript_header = null; $xscript_uri = null; $new = array( 'rev' => array_fill_keys($this->reviewers, true), 'ccs' => array_fill_keys($this->cc, true), ); $rem_ccs = array(); $xscript_phid = null; if ($diff) { $adapter = new HeraldDifferentialRevisionAdapter( $revision, $diff); $adapter->setExplicitCCs($new['ccs']); $adapter->setExplicitReviewers($new['rev']); $adapter->setForbiddenCCs($revision->getUnsubscribedPHIDs()); $xscript = HeraldEngine::loadAndApplyRules($adapter); $xscript_uri = '/herald/transcript/'.$xscript->getID().'/'; $xscript_phid = $xscript->getPHID(); $xscript_header = $xscript->getXHeraldRulesHeader(); $xscript_header = HeraldTranscript::saveXHeraldRulesHeader( $revision->getPHID(), $xscript_header); $sub = array( 'rev' => array(), 'ccs' => $adapter->getCCsAddedByHerald(), ); $rem_ccs = $adapter->getCCsRemovedByHerald(); } else { $sub = array( 'rev' => array(), 'ccs' => array(), ); } // Remove any CCs which are prevented by Herald rules. $sub['ccs'] = array_diff_key($sub['ccs'], $rem_ccs); $new['ccs'] = array_diff_key($new['ccs'], $rem_ccs); $add = array(); $rem = array(); $stable = array(); foreach (array('rev', 'ccs') as $key) { $add[$key] = array(); if ($new[$key] !== null) { $add[$key] += array_diff_key($new[$key], $old[$key]); } $add[$key] += array_diff_key($sub[$key], $old[$key]); $combined = $sub[$key]; if ($new[$key] !== null) { $combined += $new[$key]; } $rem[$key] = array_diff_key($old[$key], $combined); $stable[$key] = array_diff_key($old[$key], $add[$key] + $rem[$key]); } self::alterReviewers( $revision, $this->reviewers, array_keys($rem['rev']), array_keys($add['rev']), $this->actorPHID); // We want to attribute new CCs to a "reasonPHID", representing the reason // they were added. This is either a user (if some user explicitly CCs // them, or uses "Add CCs...") or a Herald transcript PHID, indicating that // they were added by a Herald rule. if ($add['ccs'] || $rem['ccs']) { $reasons = array(); foreach ($add['ccs'] as $phid => $ignored) { if (empty($new['ccs'][$phid])) { $reasons[$phid] = $xscript_phid; } else { $reasons[$phid] = $this->actorPHID; } } foreach ($rem['ccs'] as $phid => $ignored) { if (empty($new['ccs'][$phid])) { $reasons[$phid] = $this->actorPHID; } else { $reasons[$phid] = $xscript_phid; } } } else { $reasons = $this->actorPHID; } self::alterCCs( $revision, $this->cc, array_keys($rem['ccs']), array_keys($add['ccs']), $reasons); $this->updateAuxiliaryFields(); // Add the author and users included from Herald rules to the relevant set // of users so they get a copy of the email. if (!$this->silentUpdate) { if ($is_new) { $add['rev'][$this->getActorPHID()] = true; if ($diff) { $add['rev'] += $adapter->getEmailPHIDsAddedByHerald(); } } else { $stable['rev'][$this->getActorPHID()] = true; if ($diff) { $stable['rev'] += $adapter->getEmailPHIDsAddedByHerald(); } } } $mail = array(); $phids = array($this->getActorPHID()); $handles = id(new PhabricatorObjectHandleData($phids)) ->loadHandles(); $actor_handle = $handles[$this->getActorPHID()]; $changesets = null; $comment = null; if ($diff) { $changesets = $diff->loadChangesets(); // TODO: This should probably be in DifferentialFeedbackEditor? if (!$is_new) { $comment = $this->createComment(); } if ($comment) { $mail[] = id(new DifferentialNewDiffMail( $revision, $actor_handle, $changesets)) ->setIsFirstMailAboutRevision($is_new) ->setIsFirstMailToRecipients($is_new) ->setComments($this->getComments()) ->setToPHIDs(array_keys($stable['rev'])) ->setCCPHIDs(array_keys($stable['ccs'])); } // Save the changes we made above. $diff->setDescription(preg_replace('/\n.*/s', '', $this->getComments())); $diff->save(); $this->updateAffectedPathTable($revision, $diff, $changesets); $this->updateRevisionHashTable($revision, $diff); // An updated diff should require review, as long as it's not closed // or accepted. The "accepted" status is "sticky" to encourage courtesy // re-diffs after someone accepts with minor changes/suggestions. $status = $revision->getStatus(); if ($status != ArcanistDifferentialRevisionStatus::CLOSED && $status != ArcanistDifferentialRevisionStatus::ACCEPTED) { $revision->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVIEW); } } else { $diff = $revision->loadActiveDiff(); if ($diff) { $changesets = $diff->loadChangesets(); } else { $changesets = array(); } } $revision->save(); $this->didWriteRevision(); $event_data = array( 'revision_id' => $revision->getID(), 'revision_phid' => $revision->getPHID(), 'revision_name' => $revision->getTitle(), 'revision_author_phid' => $revision->getAuthorPHID(), 'action' => $is_new ? DifferentialAction::ACTION_CREATE : DifferentialAction::ACTION_UPDATE, 'feedback_content' => $is_new ? phutil_utf8_shorten($revision->getSummary(), 140) : $this->getComments(), 'actor_phid' => $revision->getAuthorPHID(), ); id(new PhabricatorTimelineEvent('difx', $event_data)) ->recordEvent(); id(new PhabricatorFeedStoryPublisher()) ->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_DIFFERENTIAL) ->setStoryData($event_data) ->setStoryTime(time()) ->setStoryAuthorPHID($revision->getAuthorPHID()) ->setRelatedPHIDs( array( $revision->getPHID(), $revision->getAuthorPHID(), )) ->setPrimaryObjectPHID($revision->getPHID()) ->setSubscribedPHIDs( array_merge( array($revision->getAuthorPHID()), $revision->getReviewers(), $revision->getCCPHIDs())) ->publish(); // TODO: Move this into a worker task thing. PhabricatorSearchDifferentialIndexer::indexRevision($revision); if ($this->silentUpdate) { return; } $revision->loadRelationships(); if ($add['rev']) { $message = id(new DifferentialNewDiffMail( $revision, $actor_handle, $changesets)) ->setIsFirstMailAboutRevision($is_new) ->setIsFirstMailToRecipients(true) ->setToPHIDs(array_keys($add['rev'])); if ($is_new) { // The first time we send an email about a revision, put the CCs in // the "CC:" field of the same "Review Requested" email that reviewers // get, so you don't get two initial emails if you're on a list that // is CC'd. $message->setCCPHIDs(array_keys($add['ccs'])); } $mail[] = $message; } // If we added CCs, we want to send them an email, but only if they were not // already a reviewer and were not added as one (in these cases, they got // a "NewDiff" mail, either in the past or just a moment ago). You can still // get two emails, but only if a revision is updated and you are added as a // reviewer at the same time a list you are on is added as a CC, which is // rare and reasonable. $implied_ccs = self::getImpliedCCs($revision); $implied_ccs = array_fill_keys($implied_ccs, true); $add['ccs'] = array_diff_key($add['ccs'], $implied_ccs); if (!$is_new && $add['ccs']) { $mail[] = id(new DifferentialCCWelcomeMail( $revision, $actor_handle, $changesets)) ->setIsFirstMailToRecipients(true) ->setToPHIDs(array_keys($add['ccs'])); } foreach ($mail as $message) { $message->setHeraldTranscriptURI($xscript_uri); $message->setXHeraldRulesHeader($xscript_header); $message->send(); } } public static function addCCAndUpdateRevision( $revision, $phid, $reason) { self::addCC($revision, $phid, $reason); $unsubscribed = $revision->getUnsubscribed(); if (isset($unsubscribed[$phid])) { unset($unsubscribed[$phid]); $revision->setUnsubscribed($unsubscribed); $revision->save(); } } public static function removeCCAndUpdateRevision( $revision, $phid, $reason) { self::removeCC($revision, $phid, $reason); $unsubscribed = $revision->getUnsubscribed(); if (empty($unsubscribed[$phid])) { $unsubscribed[$phid] = true; $revision->setUnsubscribed($unsubscribed); $revision->save(); } } public static function addCC( DifferentialRevision $revision, $phid, $reason) { return self::alterCCs( $revision, $revision->getCCPHIDs(), $rem = array(), $add = array($phid), $reason); } public static function removeCC( DifferentialRevision $revision, $phid, $reason) { return self::alterCCs( $revision, $revision->getCCPHIDs(), $rem = array($phid), $add = array(), $reason); } protected static function alterCCs( DifferentialRevision $revision, array $stable_phids, array $rem_phids, array $add_phids, $reason_phid) { $dont_add = self::getImpliedCCs($revision); $add_phids = array_diff($add_phids, $dont_add); return self::alterRelationships( $revision, $stable_phids, $rem_phids, $add_phids, $reason_phid, DifferentialRevision::RELATION_SUBSCRIBED); } private static function getImpliedCCs(DifferentialRevision $revision) { return array_merge( $revision->getReviewers(), array($revision->getAuthorPHID())); } public static function alterReviewers( DifferentialRevision $revision, array $stable_phids, array $rem_phids, array $add_phids, $reason_phid) { return self::alterRelationships( $revision, $stable_phids, $rem_phids, $add_phids, $reason_phid, DifferentialRevision::RELATION_REVIEWER); } private static function alterRelationships( DifferentialRevision $revision, array $stable_phids, array $rem_phids, array $add_phids, $reason_phid, $relation_type) { $rem_map = array_fill_keys($rem_phids, true); $add_map = array_fill_keys($add_phids, true); $seq_map = array_values($stable_phids); $seq_map = array_flip($seq_map); foreach ($rem_map as $phid => $ignored) { if (!isset($seq_map[$phid])) { $seq_map[$phid] = count($seq_map); } } foreach ($add_map as $phid => $ignored) { if (!isset($seq_map[$phid])) { $seq_map[$phid] = count($seq_map); } } $raw = $revision->getRawRelations($relation_type); $raw = ipull($raw, null, 'objectPHID'); $sequence = count($seq_map); foreach ($raw as $phid => $ignored) { if (isset($seq_map[$phid])) { $raw[$phid]['sequence'] = $seq_map[$phid]; } else { $raw[$phid]['sequence'] = $sequence++; } } $raw = isort($raw, 'sequence'); foreach ($raw as $phid => $ignored) { if (isset($rem_map[$phid])) { unset($raw[$phid]); } } foreach ($add_phids as $add) { $reason = is_array($reason_phid) ? idx($reason_phid, $add) : $reason_phid; $raw[$add] = array( 'objectPHID' => $add, 'sequence' => idx($seq_map, $add, $sequence++), 'reasonPHID' => $reason, ); } $conn_w = $revision->establishConnection('w'); $sql = array(); foreach ($raw as $relation) { $sql[] = qsprintf( $conn_w, '(%d, %s, %s, %d, %s)', $revision->getID(), $relation_type, $relation['objectPHID'], $relation['sequence'], $relation['reasonPHID']); } $conn_w->openTransaction(); queryfx( $conn_w, 'DELETE FROM %T WHERE revisionID = %d AND relation = %s', DifferentialRevision::RELATIONSHIP_TABLE, $revision->getID(), $relation_type); if ($sql) { queryfx( $conn_w, 'INSERT INTO %T (revisionID, relation, objectPHID, sequence, reasonPHID) VALUES %Q', DifferentialRevision::RELATIONSHIP_TABLE, implode(', ', $sql)); } $conn_w->saveTransaction(); $revision->loadRelationships(); } private function createComment() { $revision_id = $this->revision->getID(); $comment = id(new DifferentialComment()) ->setAuthorPHID($this->getActorPHID()) ->setRevisionID($revision_id) ->setContent($this->getComments()) ->setAction(DifferentialAction::ACTION_UPDATE) ->setMetadata( array( DifferentialComment::METADATA_DIFF_ID => $this->getDiff()->getID(), )); if ($this->contentSource) { $comment->setContentSource($this->contentSource); } $comment->save(); return $comment; } private function updateAuxiliaryFields() { $aux_map = array(); foreach ($this->auxiliaryFields as $aux_field) { $key = $aux_field->getStorageKey(); if ($key !== null) { $val = $aux_field->getValueForStorage(); $aux_map[$key] = $val; } } if (!$aux_map) { return; } $revision = $this->revision; $fields = id(new DifferentialAuxiliaryField())->loadAllWhere( 'revisionPHID = %s AND name IN (%Ls)', $revision->getPHID(), array_keys($aux_map)); $fields = mpull($fields, null, 'getName'); foreach ($aux_map as $key => $val) { $obj = idx($fields, $key); if (!strlen($val)) { // If the new value is empty, just delete the old row if one exists and // don't add a new row if it doesn't. if ($obj) { $obj->delete(); } } else { if (!$obj) { $obj = new DifferentialAuxiliaryField(); $obj->setRevisionPHID($revision->getPHID()); $obj->setName($key); } if ($obj->getValue() !== $val) { $obj->setValue($val); $obj->save(); } } } } private function willWriteRevision() { foreach ($this->auxiliaryFields as $aux_field) { $aux_field->willWriteRevision($this); } } private function didWriteRevision() { foreach ($this->auxiliaryFields as $aux_field) { $aux_field->didWriteRevision($this); } } /** * Update the table which links Differential revisions to paths they affect, * so Diffusion can efficiently find pending revisions for a given file. */ private function updateAffectedPathTable( DifferentialRevision $revision, DifferentialDiff $diff, array $changesets) { assert_instances_of($changesets, 'DifferentialChangeset'); $project = $diff->loadArcanistProject(); if (!$project) { // Probably an old revision from before projects. return; } $repository = $project->loadRepository(); if (!$repository) { // Probably no project <-> repository link, or the repository where the // project lives is untracked. return; } $path_prefix = null; $local_root = $diff->getSourceControlPath(); if ($local_root) { // We're in a working copy which supports subdirectory checkouts (e.g., // SVN) so we need to figure out what prefix we should add to each path // (e.g., trunk/projects/example/) to get the absolute path from the // root of the repository. DVCS systems like Git and Mercurial are not // affected. // Normalize both paths and check if the repository root is a prefix of // the local root. If so, throw it away. Note that this correctly handles // the case where the remote path is "/". $local_root = id(new PhutilURI($local_root))->getPath(); $local_root = rtrim($local_root, '/'); $repo_root = id(new PhutilURI($repository->getRemoteURI()))->getPath(); $repo_root = rtrim($repo_root, '/'); if (!strncmp($repo_root, $local_root, strlen($repo_root))) { $path_prefix = substr($local_root, strlen($repo_root)); } } $paths = array(); foreach ($changesets as $changeset) { $paths[] = $path_prefix.'/'.$changeset->getFilename(); } // Mark this as also touching all parent paths, so you can see all pending // changes to any file within a directory. $all_paths = array(); foreach ($paths as $local) { foreach (DiffusionPathIDQuery::expandPathToRoot($local) as $path) { $all_paths[$path] = true; } } $all_paths = array_keys($all_paths); $path_map = id(new DiffusionPathIDQuery($all_paths))->loadPathIDs(); $table = new DifferentialAffectedPath(); $conn_w = $table->establishConnection('w'); $sql = array(); foreach ($all_paths as $path) { $path_id = idx($path_map, $path); if (!$path_id) { // Don't bother creating these, it probably means we're either adding // a file (in which case having this row is irrelevant since Diffusion // won't be querying for it) or something is misconfigured (in which // case we'd just be writing garbage). continue; } $sql[] = qsprintf( $conn_w, '(%d, %d, %d, %d)', $repository->getID(), $path_id, time(), $revision->getID()); } queryfx( $conn_w, 'DELETE FROM %T WHERE revisionID = %d', $table->getTableName(), $revision->getID()); foreach (array_chunk($sql, 256) as $chunk) { queryfx( $conn_w, 'INSERT INTO %T (repositoryID, pathID, epoch, revisionID) VALUES %Q', $table->getTableName(), implode(', ', $chunk)); } } /** * Update the table connecting revisions to DVCS local hashes, so we can * identify revisions by commit/tree hashes. */ private function updateRevisionHashTable( DifferentialRevision $revision, DifferentialDiff $diff) { $vcs = $diff->getSourceControlSystem(); if ($vcs == DifferentialRevisionControlSystem::SVN) { // Subversion has no local commit or tree hash information, so we don't // have to do anything. return; } $property = id(new DifferentialDiffProperty())->loadOneWhere( 'diffID = %d AND name = %s', $diff->getID(), 'local:commits'); if (!$property) { return; } $hashes = array(); $data = $property->getData(); switch ($vcs) { case DifferentialRevisionControlSystem::GIT: foreach ($data as $commit) { $hashes[] = array( ArcanistDifferentialRevisionHash::HASH_GIT_COMMIT, $commit['commit'], ); $hashes[] = array( ArcanistDifferentialRevisionHash::HASH_GIT_TREE, $commit['tree'], ); } break; case DifferentialRevisionControlSystem::MERCURIAL: foreach ($data as $commit) { $hashes[] = array( ArcanistDifferentialRevisionHash::HASH_MERCURIAL_COMMIT, $commit['rev'], ); } break; } $conn_w = $revision->establishConnection('w'); $sql = array(); foreach ($hashes as $info) { list($type, $hash) = $info; $sql[] = qsprintf( $conn_w, '(%d, %s, %s)', $revision->getID(), $type, $hash); } queryfx( $conn_w, 'DELETE FROM %T WHERE revisionID = %d', ArcanistDifferentialRevisionHash::TABLE_NAME, $revision->getID()); if ($sql) { queryfx( $conn_w, 'INSERT INTO %T (revisionID, type, hash) VALUES %Q', ArcanistDifferentialRevisionHash::TABLE_NAME, implode(', ', $sql)); } } private function initializeNewRevision(DifferentialRevision $revision) { // These fields aren't nullable; set them to sensible defaults if they // haven't been configured. We're just doing this so we can generate an // ID for the revision if we don't have one already. $revision->setLineCount(0); if ($revision->getStatus() === null) { $revision->setStatus(ArcanistDifferentialRevisionStatus::NEEDS_REVIEW); } if ($revision->getTitle() === null) { $revision->setTitle('Untitled Revision'); } if ($revision->getAuthorPHID() === null) { $revision->setAuthorPHID($this->getActorPHID()); } if ($revision->getSummary() === null) { $revision->setSummary(''); } if ($revision->getTestPlan() === null) { $revision->setTestPlan(''); } } } diff --git a/src/applications/differential/field/specification/DifferentialFreeformFieldSpecification.php b/src/applications/differential/field/specification/DifferentialFreeformFieldSpecification.php index 4001219fc8..6b34a9e5db 100644 --- a/src/applications/differential/field/specification/DifferentialFreeformFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialFreeformFieldSpecification.php @@ -1,138 +1,138 @@ loadOneWhere( 'phid = %s', $data->getCommitDetail('authorPHID')); if (!$user) { return; } $prefixes = array( 'resolves' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, 'fixes' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, 'wontfix' => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX, 'wontfixes' => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX, 'spite' => ManiphestTaskStatus::STATUS_CLOSED_SPITE, 'spites' => ManiphestTaskStatus::STATUS_CLOSED_SPITE, 'invalidate' => ManiphestTaskStatus::STATUS_CLOSED_INVALID, 'invaldiates' => ManiphestTaskStatus::STATUS_CLOSED_INVALID, 'close' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, 'closes' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, 'ref' => null, 'refs' => null, 'references' => null, 'cf.' => null, ); $suffixes = array( 'as resolved' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, 'as fixed' => ManiphestTaskStatus::STATUS_CLOSED_RESOLVED, 'as wontfix' => ManiphestTaskStatus::STATUS_CLOSED_WONTFIX, 'as spite' => ManiphestTaskStatus::STATUS_CLOSED_SPITE, 'out of spite' => ManiphestTaskStatus::STATUS_CLOSED_SPITE, 'as invalid' => ManiphestTaskStatus::STATUS_CLOSED_INVALID, '' => null, ); $prefix_regex = array(); foreach ($prefixes as $prefix => $resolution) { $prefix_regex[] = preg_quote($prefix, '/'); } $prefix_regex = implode('|', $prefix_regex); $suffix_regex = array(); foreach ($suffixes as $suffix => $resolution) { $suffix_regex[] = preg_quote($suffix, '/'); } $suffix_regex = implode('|', $suffix_regex); $matches = null; $ok = preg_match_all( "/({$prefix_regex})\s+T(\d+)\s*({$suffix_regex})/i", $this->renderValueForCommitMessage($is_edit = false), $matches, PREG_SET_ORDER); if (!$ok) { return; } foreach ($matches as $set) { $prefix = strtolower($set[1]); $task_id = (int)$set[2]; $suffix = strtolower($set[3]); $status = idx($suffixes, $suffix); if (!$status) { $status = idx($prefixes, $prefix); } $tasks = id(new ManiphestTaskQuery()) ->withTaskIDs(array($task_id)) ->execute(); $task = idx($tasks, $task_id); if (!$task) { // Task doesn't exist, or the user can't see it. continue; } id(new PhabricatorEdgeEditor()) - ->setUser($user) + ->setActor($user) ->addEdge( $task->getPHID(), PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT, $commit->getPHID()) ->save(); if (!$status) { // Text like "Ref T123", don't change the task status. continue; } if ($task->getStatus() != ManiphestTaskStatus::STATUS_OPEN) { // Task is already closed. continue; } $commit_name = $repository->formatCommitName( $commit->getCommitIdentifier()); $call = new ConduitCall( 'maniphest.update', array( 'id' => $task->getID(), 'status' => $status, 'comments' => "Closed by commit {$commit_name}.", )); $call->setUser($user); $call->execute(); } } } diff --git a/src/applications/differential/field/specification/DifferentialManiphestTasksFieldSpecification.php b/src/applications/differential/field/specification/DifferentialManiphestTasksFieldSpecification.php index 136beceadc..b436b057e2 100644 --- a/src/applications/differential/field/specification/DifferentialManiphestTasksFieldSpecification.php +++ b/src/applications/differential/field/specification/DifferentialManiphestTasksFieldSpecification.php @@ -1,191 +1,191 @@ getManiphestTaskPHIDs(); } public function renderLabelForRevisionView() { return 'Maniphest Tasks:'; } public function renderValueForRevisionView() { $task_phids = $this->getManiphestTaskPHIDs(); if (!$task_phids) { return null; } $links = array(); foreach ($task_phids as $task_phid) { $links[] = $this->getHandle($task_phid)->renderLink(); } return implode('
', $links); } private function getManiphestTaskPHIDs() { $revision = $this->getRevision(); if (!$revision->getPHID()) { return array(); } return PhabricatorEdgeQuery::loadDestinationPHIDs( $revision->getPHID(), PhabricatorEdgeConfig::TYPE_DREV_HAS_RELATED_TASK); } /** * Attach the revision to the task(s) and the task(s) to the revision. * * @return void */ public function didWriteRevision(DifferentialRevisionEditor $editor) { $revision = $editor->getRevision(); $revision_phid = $revision->getPHID(); $edge_type = PhabricatorEdgeConfig::TYPE_DREV_HAS_RELATED_TASK; $old_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $revision_phid, $edge_type); $add_phids = $this->maniphestTasks; $rem_phids = array_diff($old_phids, $add_phids); $edge_editor = id(new PhabricatorEdgeEditor()) - ->setUser($this->getUser()); + ->setActor($this->getUser()); foreach ($add_phids as $phid) { $edge_editor->addEdge($revision_phid, $edge_type, $phid); } foreach ($rem_phids as $phid) { $edge_editor->removeEdge($revision_phid, $edge_type, $phid); } $edge_editor->save(); } protected function didSetRevision() { $this->maniphestTasks = $this->getManiphestTaskPHIDs(); } public function getRequiredHandlePHIDsForCommitMessage() { return $this->maniphestTasks; } public function shouldAppearOnCommitMessageTemplate() { return PhabricatorEnv::getEnvConfig('maniphest.enabled'); } public function shouldAppearOnCommitMessage() { return PhabricatorEnv::getEnvConfig('maniphest.enabled'); } public function getCommitMessageKey() { return 'maniphestTaskPHIDs'; } public function setValueFromParsedCommitMessage($value) { $this->maniphestTasks = nonempty($value, array()); return $this; } public function renderLabelForCommitMessage() { return 'Maniphest Tasks'; } public function getSupportedCommitMessageLabels() { return array( 'Maniphest Task', 'Maniphest Tasks', ); } public function renderValueForCommitMessage($is_edit) { if (!$this->maniphestTasks) { return null; } $names = array(); foreach ($this->maniphestTasks as $phid) { $handle = $this->getHandle($phid); $names[] = 'T'.$handle->getAlternateID(); } return implode(', ', $names); } public function parseValueFromCommitMessage($value) { $matches = null; preg_match_all('/T(\d+)/', $value, $matches); if (empty($matches[0])) { return array(); } $task_ids = $matches[1]; $tasks = id(new ManiphestTask()) ->loadAllWhere('id in (%Ld)', $task_ids); $task_phids = array(); $invalid = array(); foreach ($task_ids as $task_id) { $task = idx($tasks, $task_id); if (empty($task)) { $invalid[] = 'T'.$task_id; } else { $task_phids[] = $task->getPHID(); } } if ($invalid) { $what = pht('Maniphest Task(s)', count($invalid)); $invalid = implode(', ', $invalid); throw new DifferentialFieldParseException( "Commit message references nonexistent {$what}: {$invalid}."); } return $task_phids; } public function renderValueForMail($phase) { if ($phase == DifferentialMailPhase::COMMENT) { return null; } if (!$this->maniphestTasks) { return null; } $handles = id(new PhabricatorObjectHandleData($this->maniphestTasks)) ->loadHandles(); $body = array(); $body[] = 'MANIPHEST TASKS'; foreach ($handles as $handle) { $body[] = ' '.PhabricatorEnv::getProductionURI($handle->getURI()); } return implode("\n", $body); } } diff --git a/src/applications/differential/mail/DifferentialMail.php b/src/applications/differential/mail/DifferentialMail.php index f232186222..e94867af05 100644 --- a/src/applications/differential/mail/DifferentialMail.php +++ b/src/applications/differential/mail/DifferentialMail.php @@ -1,461 +1,451 @@ getRevision(); $title = $revision->getTitle(); $id = $revision->getID(); return "D{$id}: {$title}"; } abstract protected function renderVaryPrefix(); abstract protected function renderBody(); public function setActorHandle($actor_handle) { $this->actorHandle = $actor_handle; return $this; } public function getActorHandle() { return $this->actorHandle; } protected function getActorName() { $handle = $this->getActorHandle(); if ($handle) { return $handle->getName(); } return '???'; } public function setParentMessageID($parent_message_id) { $this->parentMessageID = $parent_message_id; return $this; } public function setXHeraldRulesHeader($header) { $this->heraldRulesHeader = $header; return $this; } public function send() { $to_phids = $this->getToPHIDs(); if (!$to_phids) { throw new Exception('No "To:" users provided!'); } $cc_phids = $this->getCCPHIDs(); $attachments = $this->buildAttachments(); $template = new PhabricatorMetaMTAMail(); $actor_handle = $this->getActorHandle(); $reply_handler = $this->getReplyHandler(); if ($actor_handle) { $template->setFrom($actor_handle->getPHID()); } $template ->setIsHTML($this->shouldMarkMailAsHTML()) ->setParentMessageID($this->parentMessageID) + ->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs()) ->addHeader('Thread-Topic', $this->getThreadTopic()); $template->setAttachments($attachments); $template->setThreadID( $this->getThreadID(), $this->isFirstMailAboutRevision()); if ($this->heraldRulesHeader) { $template->addHeader('X-Herald-Rules', $this->heraldRulesHeader); } $revision = $this->revision; if ($revision) { if ($revision->getAuthorPHID()) { $template->addHeader( 'X-Differential-Author', '<'.$revision->getAuthorPHID().'>'); } $reviewer_phids = $revision->getReviewers(); if ($reviewer_phids) { // Add several headers to support e-mail clients which are not able to // create rules using regular expressions or wildcards (namely Outlook). $template->addPHIDHeaders('X-Differential-Reviewer', $reviewer_phids); // Add it also as a list to allow matching of the first reviewer and // also for backwards compatibility. $template->addHeader( 'X-Differential-Reviewers', '<'.implode('>, <', $reviewer_phids).'>'); } if ($cc_phids) { $template->addPHIDHeaders('X-Differential-CC', $cc_phids); $template->addHeader( 'X-Differential-CCs', '<'.implode('>, <', $cc_phids).'>'); // Determine explicit CCs (those added by humans) and put them in a // header so users can differentiate between Herald CCs and human CCs. $relation_subscribed = DifferentialRevision::RELATION_SUBSCRIBED; $raw = $revision->getRawRelations($relation_subscribed); $reason_phids = ipull($raw, 'reasonPHID'); $reason_handles = id(new PhabricatorObjectHandleData($reason_phids)) ->loadHandles(); $explicit_cc = array(); foreach ($raw as $relation) { if (!$relation['reasonPHID']) { continue; } $type = $reason_handles[$relation['reasonPHID']]->getType(); if ($type == PhabricatorPHIDConstants::PHID_TYPE_USER) { $explicit_cc[] = $relation['objectPHID']; } } if ($explicit_cc) { $template->addPHIDHeaders('X-Differential-Explicit-CC', $explicit_cc); $template->addHeader( 'X-Differential-Explicit-CCs', '<'.implode('>, <', $explicit_cc).'>'); } } } $template->setIsBulk(true); $template->setRelatedPHID($this->getRevision()->getPHID()); $mailtags = $this->getMailTags(); if ($mailtags) { $template->setMailTags($mailtags); } $phids = array(); foreach ($to_phids as $phid) { $phids[$phid] = true; } foreach ($cc_phids as $phid) { $phids[$phid] = true; } $phids = array_keys($phids); $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles(); $objects = id(new PhabricatorObjectHandleData($phids))->loadObjects(); $to_handles = array_select_keys($handles, $to_phids); $cc_handles = array_select_keys($handles, $cc_phids); $this->prepareBody(); $mails = $reply_handler->multiplexMail($template, $to_handles, $cc_handles); $original_translator = PhutilTranslator::getInstance(); if (!PhabricatorMetaMTAMail::shouldMultiplexAllMail()) { $translation = PhabricatorEnv::newObjectFromConfig( 'translation.provider'); $translator = id(new PhutilTranslator()) ->setLanguage($translation->getLanguage()) ->addTranslations($translation->getTranslations()); } try { foreach ($mails as $mail) { if (PhabricatorMetaMTAMail::shouldMultiplexAllMail()) { $translation = newv($mail->getTranslation($objects), array()); $translator = id(new PhutilTranslator()) ->setLanguage($translation->getLanguage()) ->addTranslations($translation->getTranslations()); PhutilTranslator::setInstance($translator); } $body = $this->buildBody()."\n". $reply_handler->getRecipientsSummary($to_handles, $cc_handles); $mail ->setSubject($this->renderSubject()) ->setSubjectPrefix($this->getSubjectPrefix()) ->setVarySubjectPrefix($this->renderVaryPrefix()) ->setBody($body); $event = new PhabricatorEvent( PhabricatorEventType::TYPE_DIFFERENTIAL_WILLSENDMAIL, array( 'mail' => $mail, ) ); PhutilEventEngine::dispatchEvent($event); $mail = $event->getValue('mail'); $mail->saveAndSend(); } } catch (Exception $ex) { PhutilTranslator::setInstance($original_translator); throw $ex; } PhutilTranslator::setInstance($original_translator); } protected function getMailTags() { return array(); } protected function getSubjectPrefix() { return PhabricatorEnv::getEnvConfig('metamta.differential.subject-prefix'); } protected function shouldMarkMailAsHTML() { return false; } /** * @{method:buildBody} is called once for each e-mail recipient to allow * translating text to his language. This method can be used to load data that * don't need translation and use them later in @{method:buildBody}. * * @param * @return */ protected function prepareBody() { } protected function buildBody() { $main_body = $this->renderBody(); $body = new PhabricatorMetaMTAMailBody(); $body->addRawSection($main_body); $reply_handler = $this->getReplyHandler(); $body->addReplySection($reply_handler->getReplyHandlerInstructions()); if ($this->getHeraldTranscriptURI() && $this->isFirstMailToRecipients()) { $manage_uri = '/herald/view/differential/'; $xscript_uri = $this->getHeraldTranscriptURI(); $body->addHeraldSection($manage_uri, $xscript_uri); } return $body->render(); } /** * You can override this method in a subclass and return array of attachments * to be sent with the email. Each attachment is an instance of * PhabricatorMetaMTAAttachment. */ protected function buildAttachments() { return array(); } public function getReplyHandler() { if (!$this->replyHandler) { $this->replyHandler = self::newReplyHandlerForRevision($this->getRevision()); } return $this->replyHandler; } public static function newReplyHandlerForRevision( DifferentialRevision $revision) { $reply_handler = PhabricatorEnv::newObjectFromConfig( 'metamta.differential.reply-handler'); $reply_handler->setMailReceiver($revision); return $reply_handler; } protected function formatText($text) { $text = explode("\n", rtrim($text)); foreach ($text as &$line) { $line = rtrim(' '.$line); } unset($line); return implode("\n", $text); } - public function setToPHIDs(array $to) { - $this->to = $this->filterContactPHIDs($to); + public function setExcludeMailRecipientPHIDs(array $exclude) { + $this->excludePHIDs = $exclude; return $this; } - public function setCCPHIDs(array $cc) { - $this->cc = $this->filterContactPHIDs($cc); - return $this; + public function getExcludeMailRecipientPHIDs() { + return $this->excludePHIDs; } - protected function filterContactPHIDs(array $phids) { - return $phids; - - // TODO: actually do this? + public function setToPHIDs(array $to) { + $this->to = $to; + return $this; + } - // Differential revisions use Subscriptions for CCs, so any arbitrary - // PHID can end up CC'd to them. Only try to actually send email PHIDs - // which have ToolsHandle types that are marked emailable. If we don't - // filter here, sending the email will fail. -/* - $handles = array(); - prep(new ToolsHandleData($phids, $handles)); - foreach ($handles as $phid => $handle) { - if (!$handle->isEmailable()) { - unset($handles[$phid]); - } - } - return array_keys($handles); -*/ + public function setCCPHIDs(array $cc) { + $this->cc = $cc; + return $this; } protected function getToPHIDs() { return $this->to; } protected function getCCPHIDs() { return $this->cc; } public function setRevision($revision) { $this->revision = $revision; return $this; } public function getRevision() { return $this->revision; } protected function getThreadID() { $phid = $this->getRevision()->getPHID(); return "differential-rev-{$phid}-req"; } protected function getThreadTopic() { $id = $this->getRevision()->getID(); $title = $this->getRevision()->getOriginalTitle(); return "D{$id}: {$title}"; } public function setComment($comment) { $this->comment = $comment; return $this; } public function getComment() { return $this->comment; } public function setChangesets($changesets) { $this->changesets = $changesets; return $this; } public function getChangesets() { return $this->changesets; } public function setInlineComments(array $inline_comments) { assert_instances_of($inline_comments, 'PhabricatorInlineCommentInterface'); $this->inlineComments = $inline_comments; return $this; } public function getInlineComments() { return $this->inlineComments; } protected function renderAuxFields($phase) { $selector = DifferentialFieldSelector::newSelector(); $aux_fields = $selector->sortFieldsForMail( $selector->getFieldSpecifications()); $body = array(); foreach ($aux_fields as $field) { $field->setRevision($this->getRevision()); // TODO: Introduce and use getRequiredHandlePHIDsForMail() and load all // handles in prepareBody(). $text = $field->renderValueForMail($phase); if ($text !== null) { $body[] = $text; $body[] = null; } } return implode("\n", $body); } public function setIsFirstMailToRecipients($first) { $this->isFirstMailToRecipients = $first; return $this; } public function isFirstMailToRecipients() { return $this->isFirstMailToRecipients; } public function setIsFirstMailAboutRevision($first) { $this->isFirstMailAboutRevision = $first; return $this; } public function isFirstMailAboutRevision() { return $this->isFirstMailAboutRevision; } public function setHeraldTranscriptURI($herald_transcript_uri) { $this->heraldTranscriptURI = $herald_transcript_uri; return $this; } public function getHeraldTranscriptURI() { return $this->heraldTranscriptURI; } protected function renderHandleList(array $handles, array $phids) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $names = array(); foreach ($phids as $phid) { $names[] = $handles[$phid]->getName(); } return implode(', ', $names); } } diff --git a/src/applications/diffusion/controller/DiffusionCommitEditController.php b/src/applications/diffusion/controller/DiffusionCommitEditController.php index a1d712ef4f..c927c79bf9 100644 --- a/src/applications/diffusion/controller/DiffusionCommitEditController.php +++ b/src/applications/diffusion/controller/DiffusionCommitEditController.php @@ -1,111 +1,111 @@ diffusionRequest = DiffusionRequest::newFromDictionary($data); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $drequest = $this->getDiffusionRequest(); $callsign = $drequest->getRepository()->getCallsign(); $repository = $drequest->getRepository(); $commit = $drequest->loadCommit(); $page_title = 'Edit Diffusion Commit'; if (!$commit) { return new Aphront404Response(); } $commit_phid = $commit->getPHID(); $edge_type = PhabricatorEdgeConfig::TYPE_COMMIT_HAS_PROJECT; $current_proj_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $commit_phid, $edge_type ); $handles = $this->loadViewerHandles($current_proj_phids); $proj_t_values = mpull($handles, 'getFullName', 'getPHID'); if ($request->isFormPost()) { $proj_phids = $request->getArr('projects'); $new_proj_phids = array_values($proj_phids); $rem_proj_phids = array_diff($current_proj_phids, $new_proj_phids); $editor = id(new PhabricatorEdgeEditor()); - $editor->setUser($user); + $editor->setActor($user); foreach ($rem_proj_phids as $phid) { $editor->removeEdge($commit_phid, $edge_type, $phid); } foreach ($new_proj_phids as $phid) { $editor->addEdge($commit_phid, $edge_type, $phid); } $editor->save(); PhabricatorSearchCommitIndexer::indexCommit($commit); return id(new AphrontRedirectResponse()) ->setURI('/r'.$callsign.$commit->getCommitIdentifier()); } $tokenizer_id = celerity_generate_unique_node_id(); $form = id(new AphrontFormView()) ->setUser($user) ->setAction($request->getRequestURI()->getPath()) ->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel('Projects') ->setName('projects') ->setValue($proj_t_values) ->setID($tokenizer_id) ->setCaption( javelin_render_tag( 'a', array( 'href' => '/project/create/', 'mustcapture' => true, 'sigil' => 'project-create', ), 'Create New Project')) ->setDatasource('/typeahead/common/projects/'));; Javelin::initBehavior('project-create', array( 'tokenizerID' => $tokenizer_id, )); $submit = id(new AphrontFormSubmitControl()) ->setValue('Save') ->addCancelButton('/r'.$callsign.$commit->getCommitIdentifier()); $form->appendChild($submit); $panel = id(new AphrontPanelView()) ->setHeader('Edit Diffusion Commit') ->appendChild($form) ->setWidth(AphrontPanelView::WIDTH_FORM); return $this->buildStandardPageResponse( $panel, array( 'title' => $page_title, )); } } diff --git a/src/applications/maniphest/ManiphestReplyHandler.php b/src/applications/maniphest/ManiphestReplyHandler.php index 233845fb0b..423f36e1b4 100644 --- a/src/applications/maniphest/ManiphestReplyHandler.php +++ b/src/applications/maniphest/ManiphestReplyHandler.php @@ -1,185 +1,188 @@ getDefaultPrivateReplyHandlerEmailAddress($handle, 'T'); } public function getPublicReplyHandlerEmailAddress() { return $this->getDefaultPublicReplyHandlerEmailAddress('T'); } public function getReplyHandlerDomain() { return PhabricatorEnv::getEnvConfig( 'metamta.maniphest.reply-handler-domain'); } public function getReplyHandlerInstructions() { if ($this->supportsReplies()) { return "Reply to comment or attach files, or !close, !claim, or ". "!unsubscribe."; } else { return null; } } protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) { // NOTE: We'll drop in here on both the "reply to a task" and "create a // new task" workflows! Make sure you test both if you make changes! $task = $this->getMailReceiver(); $is_new_task = !$task->getID(); $user = $this->getActor(); $body = $mail->getCleanTextBody(); $body = trim($body); $xactions = array(); $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_EMAIL, array( 'id' => $mail->getID(), )); $template = new ManiphestTransaction(); $template->setContentSource($content_source); $template->setAuthorPHID($user->getPHID()); if ($is_new_task) { // If this is a new task, create a "User created this task." transaction // and then set the title and description. $xaction = clone $template; $xaction->setTransactionType(ManiphestTransactionType::TYPE_STATUS); $xaction->setNewValue(ManiphestTaskStatus::STATUS_OPEN); $xactions[] = $xaction; $task->setAuthorPHID($user->getPHID()); $task->setTitle(nonempty($mail->getSubject(), 'Untitled Task')); $task->setDescription($body); $task->setPriority(ManiphestTaskPriority::getDefaultPriority()); } else { $lines = explode("\n", trim($body)); $first_line = head($lines); $command = null; $matches = null; if (preg_match('/^!(\w+)/', $first_line, $matches)) { $lines = array_slice($lines, 1); $body = implode("\n", $lines); $body = trim($body); $command = $matches[1]; } $ttype = ManiphestTransactionType::TYPE_NONE; $new_value = null; switch ($command) { case 'close': $ttype = ManiphestTransactionType::TYPE_STATUS; $new_value = ManiphestTaskStatus::STATUS_CLOSED_RESOLVED; break; case 'claim': $ttype = ManiphestTransactionType::TYPE_OWNER; $new_value = $user->getPHID(); break; case 'unsubscribe': $ttype = ManiphestTransactionType::TYPE_CCS; $ccs = $task->getCCPHIDs(); foreach ($ccs as $k => $phid) { if ($phid == $user->getPHID()) { unset($ccs[$k]); } } $new_value = array_values($ccs); break; } $xaction = clone $template; $xaction->setTransactionType($ttype); $xaction->setNewValue($new_value); $xaction->setComments($body); $xactions[] = $xaction; } // TODO: We should look at CCs on the mail and add them as CCs. $files = $mail->getAttachments(); if ($files) { $file_xaction = clone $template; $file_xaction->setTransactionType(ManiphestTransactionType::TYPE_ATTACH); $phid_type = PhabricatorPHIDConstants::PHID_TYPE_FILE; $new = $task->getAttached(); foreach ($files as $file_phid) { $new[$phid_type][$file_phid] = array(); } $file_xaction->setNewValue($new); $xactions[] = $file_xaction; } $event = new PhabricatorEvent( PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK, array( 'task' => $task, 'mail' => $mail, 'new' => $is_new_task, 'transactions' => $xactions, )); $event->setUser($user); PhutilEventEngine::dispatchEvent($event); $task = $event->getValue('task'); $xactions = $event->getValue('transactions'); $editor = new ManiphestTransactionEditor(); + $editor->setActor($user); $editor->setParentMessageID($mail->getMessageID()); + $editor->setExcludeMailRecipientPHIDs( + $this->getExcludeMailRecipientPHIDs()); $editor->applyTransactions($task, $xactions); $event = new PhabricatorEvent( PhabricatorEventType::TYPE_MANIPHEST_DIDEDITTASK, array( 'task' => $task, 'new' => $is_new_task, 'transactions' => $xactions, )); $event->setUser($user); PhutilEventEngine::dispatchEvent($event); } } diff --git a/src/applications/maniphest/controller/ManiphestBatchEditController.php b/src/applications/maniphest/controller/ManiphestBatchEditController.php index 32b0fde384..bf538e1185 100644 --- a/src/applications/maniphest/controller/ManiphestBatchEditController.php +++ b/src/applications/maniphest/controller/ManiphestBatchEditController.php @@ -1,310 +1,311 @@ getRequest(); $user = $request->getUser(); $task_ids = $request->getArr('batch'); $tasks = id(new ManiphestTask())->loadAllWhere( 'id IN (%Ld)', $task_ids); $actions = $request->getStr('actions'); if ($actions) { $actions = json_decode($actions, true); } if ($request->isFormPost() && is_array($actions)) { foreach ($tasks as $task) { $xactions = $this->buildTransactions($actions, $task); if ($xactions) { $editor = new ManiphestTransactionEditor(); + $editor->setActor($user); $editor->applyTransactions($task, $xactions); } } $task_ids = implode(',', mpull($tasks, 'getID')); return id(new AphrontRedirectResponse()) ->setURI('/maniphest/view/custom/?s=oc&tasks='.$task_ids); } $panel = new AphrontPanelView(); $panel->setHeader('Maniphest Batch Editor'); $handle_phids = mpull($tasks, 'getOwnerPHID'); $handles = $this->loadViewerHandles($handle_phids); $list = new ManiphestTaskListView(); $list->setTasks($tasks); $list->setUser($user); $list->setHandles($handles); $template = new AphrontTokenizerTemplateView(); $template = $template->render(); require_celerity_resource('maniphest-batch-editor'); Javelin::initBehavior( 'maniphest-batch-editor', array( 'root' => 'maniphest-batch-edit-form', 'tokenizerTemplate' => $template, 'sources' => array( 'project' => array( 'src' => '/typeahead/common/projects/', 'placeholder' => 'Type a project name...', ), 'owner' => array( 'src' => '/typeahead/common/searchowner/', 'placeholder' => 'Type a user name...', 'limit' => 1, ), ), 'input' => 'batch-form-actions', 'priorityMap' => ManiphestTaskPriority::getTaskPriorityMap(), 'statusMap' => ManiphestTaskStatus::getTaskStatusMap(), )); $form = new AphrontFormView(); $form->setUser($user); $form->setID('maniphest-batch-edit-form'); foreach ($tasks as $task) { $form->appendChild( phutil_render_tag( 'input', array( 'type' => 'hidden', 'name' => 'batch[]', 'value' => $task->getID(), ), null)); } $form->appendChild( phutil_render_tag( 'input', array( 'type' => 'hidden', 'name' => 'actions', 'id' => 'batch-form-actions', ), null)); $form->appendChild('

These tasks will be edited:

'); $form->appendChild($list); $form->appendChild( id(new AphrontFormInsetView()) ->setTitle('Actions') ->setRightButton(javelin_render_tag( 'a', array( 'href' => '#', 'class' => 'button green', 'sigil' => 'add-action', 'mustcapture' => true, ), 'Add Another Action')) ->setContent(javelin_render_tag( 'table', array( 'sigil' => 'maniphest-batch-actions', 'class' => 'maniphest-batch-actions-table', ), ''))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue('Update Tasks') ->addCancelButton('/maniphest/', 'Done')); $panel->appendChild($form); return $this->buildStandardPageResponse( $panel, array( 'title' => 'Batch Editor', )); } private function buildTransactions($actions, ManiphestTask $task) { $value_map = array(); $type_map = array( 'add_comment' => ManiphestTransactionType::TYPE_NONE, 'assign' => ManiphestTransactionType::TYPE_OWNER, 'status' => ManiphestTransactionType::TYPE_STATUS, 'priority' => ManiphestTransactionType::TYPE_PRIORITY, 'add_project' => ManiphestTransactionType::TYPE_PROJECTS, 'remove_project' => ManiphestTransactionType::TYPE_PROJECTS, ); $edge_edit_types = array( 'add_project' => true, 'remove_project' => true, ); $xactions = array(); foreach ($actions as $action) { if (empty($type_map[$action['action']])) { throw new Exception("Unknown batch edit action '{$action}'!"); } $type = $type_map[$action['action']]; // Figure out the current value, possibly after modifications by other // batch actions of the same type. For example, if the user chooses to // "Add Comment" twice, we should add both comments. More notably, if the // user chooses "Remove Project..." and also "Add Project...", we should // avoid restoring the removed project in the second transaction. if (array_key_exists($type, $value_map)) { $current = $value_map[$type]; } else { switch ($type) { case ManiphestTransactionType::TYPE_NONE: $current = null; break; case ManiphestTransactionType::TYPE_OWNER: $current = $task->getOwnerPHID(); break; case ManiphestTransactionType::TYPE_STATUS: $current = $task->getStatus(); break; case ManiphestTransactionType::TYPE_PRIORITY: $current = $task->getPriority(); break; case ManiphestTransactionType::TYPE_PROJECTS: $current = $task->getProjectPHIDs(); break; } } // Check if the value is meaningful / provided, and normalize it if // necessary. This discards, e.g., empty comments and empty owner // changes. $value = $action['value']; switch ($type) { case ManiphestTransactionType::TYPE_NONE: if (!strlen($value)) { continue 2; } break; case ManiphestTransactionType::TYPE_OWNER: if (empty($value)) { continue 2; } $value = head($value); if ($value === ManiphestTaskOwner::OWNER_UP_FOR_GRABS) { $value = null; } break; case ManiphestTransactionType::TYPE_PROJECTS: if (empty($value)) { continue 2; } break; } // If the edit doesn't change anything, go to the next action. This // check is only valid for changes like "owner", "status", etc, not // for edge edits, because we should still apply an edit like // "Remove Projects: A, B" to a task with projects "A, B". if (empty($edge_edit_types[$action['action']])) { if ($value == $current) { continue; } } // Apply the value change; for most edits this is just replacement, but // some need to merge the current and edited values (add/remove project). switch ($type) { case ManiphestTransactionType::TYPE_NONE: if (strlen($current)) { $value = $current."\n\n".$value; } break; case ManiphestTransactionType::TYPE_PROJECTS: $is_remove = ($action['action'] == 'remove_project'); $current = array_fill_keys($current, true); $value = array_fill_keys($value, true); $new = $current; $did_something = false; if ($is_remove) { foreach ($value as $phid => $ignored) { if (isset($new[$phid])) { unset($new[$phid]); $did_something = true; } } } else { foreach ($value as $phid => $ignored) { if (empty($new[$phid])) { $new[$phid] = true; $did_something = true; } } } if (!$did_something) { continue 2; } $value = array_keys($new); break; } $value_map[$type] = $value; } $template = new ManiphestTransaction(); $template->setAuthorPHID($this->getRequest()->getUser()->getPHID()); // TODO: Set content source to "batch edit". foreach ($value_map as $type => $value) { $xaction = clone $template; $xaction->setTransactionType($type); switch ($type) { case ManiphestTransactionType::TYPE_NONE: $xaction->setComments($value); break; default: $xaction->setNewValue($value); break; } $xactions[] = $xaction; } return $xactions; } } diff --git a/src/applications/maniphest/controller/ManiphestSubpriorityController.php b/src/applications/maniphest/controller/ManiphestSubpriorityController.php index 48fdf17b23..5793ec214e 100644 --- a/src/applications/maniphest/controller/ManiphestSubpriorityController.php +++ b/src/applications/maniphest/controller/ManiphestSubpriorityController.php @@ -1,79 +1,80 @@ getRequest(); if (!$request->validateCSRF()) { return new Aphront403Response(); } $task = id(new ManiphestTask())->load($request->getInt('task')); if (!$task) { return new Aphront404Response(); } if ($request->getInt('after')) { $after_task = id(new ManiphestTask())->load($request->getInt('after')); if (!$after_task) { return new Aphront404Response(); } $after_pri = $after_task->getPriority(); $after_sub = $after_task->getSubpriority(); } else { $after_pri = $request->getInt('priority'); $after_sub = null; } $new_sub = ManiphestTransactionEditor::getNextSubpriority( $after_pri, $after_sub); if ($after_pri != $task->getPriority()) { $xaction = new ManiphestTransaction(); $xaction->setAuthorPHID($request->getUser()->getPHID()); // TODO: Content source? $xaction->setTransactionType(ManiphestTransactionType::TYPE_PRIORITY); $xaction->setNewValue($after_pri); $editor = new ManiphestTransactionEditor(); + $editor->setActor($request->getUser()); $editor->applyTransactions($task, array($xaction)); } $task->setSubpriority($new_sub); $task->save(); $pri_class = ManiphestTaskSummaryView::getPriorityClass( $task->getPriority()); $class = 'maniphest-task-handle maniphest-active-handle '.$pri_class; $response = array( 'className' => $class, ); return id(new AphrontAjaxResponse())->setContent($response); } } diff --git a/src/applications/maniphest/controller/ManiphestTaskEditController.php b/src/applications/maniphest/controller/ManiphestTaskEditController.php index b298e16232..96a1704757 100644 --- a/src/applications/maniphest/controller/ManiphestTaskEditController.php +++ b/src/applications/maniphest/controller/ManiphestTaskEditController.php @@ -1,567 +1,568 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $files = array(); $parent_task = null; $template_id = null; if ($this->id) { $task = id(new ManiphestTask())->load($this->id); if (!$task) { return new Aphront404Response(); } } else { $task = new ManiphestTask(); $task->setPriority(ManiphestTaskPriority::getDefaultPriority()); $task->setAuthorPHID($user->getPHID()); // These allow task creation with defaults. if (!$request->isFormPost()) { $task->setTitle($request->getStr('title')); $default_projects = $request->getStr('projects'); if ($default_projects) { $task->setProjectPHIDs(explode(';', $default_projects)); } } $file_phids = $request->getArr('files', array()); if (!$file_phids) { // Allow a single 'file' key instead, mostly since Mac OS X urlencodes // square brackets in URLs when passed to 'open', so you can't 'open' // a URL like '?files[]=xyz' and have PHP interpret it correctly. $phid = $request->getStr('file'); if ($phid) { $file_phids = array($phid); } } if ($file_phids) { $files = id(new PhabricatorFile())->loadAllWhere( 'phid IN (%Ls)', $file_phids); } $template_id = $request->getInt('template'); // You can only have a parent task if you're creating a new task. $parent_id = $request->getInt('parent'); if ($parent_id) { $parent_task = id(new ManiphestTask())->load($parent_id); } } $errors = array(); $e_title = true; $extensions = ManiphestTaskExtensions::newExtensions(); $aux_fields = $extensions->getAuxiliaryFieldSpecifications(); if ($request->isFormPost()) { $changes = array(); $new_title = $request->getStr('title'); $new_desc = $request->getStr('description'); $new_status = $request->getStr('status'); $workflow = ''; if ($task->getID()) { if ($new_title != $task->getTitle()) { $changes[ManiphestTransactionType::TYPE_TITLE] = $new_title; } if ($new_desc != $task->getDescription()) { $changes[ManiphestTransactionType::TYPE_DESCRIPTION] = $new_desc; } if ($new_status != $task->getStatus()) { $changes[ManiphestTransactionType::TYPE_STATUS] = $new_status; } } else { $task->setTitle($new_title); $task->setDescription($new_desc); $changes[ManiphestTransactionType::TYPE_STATUS] = ManiphestTaskStatus::STATUS_OPEN; $workflow = 'create'; } $owner_tokenizer = $request->getArr('assigned_to'); $owner_phid = reset($owner_tokenizer); if (!strlen($new_title)) { $e_title = 'Required'; $errors[] = 'Title is required.'; } foreach ($aux_fields as $aux_field) { $aux_field->setValueFromRequest($request); if ($aux_field->isRequired() && !strlen($aux_field->getValue())) { $errors[] = $aux_field->getLabel() . ' is required.'; $aux_field->setError('Required'); } if (strlen($aux_field->getValue())) { try { $aux_field->validate(); } catch (Exception $e) { $errors[] = $e->getMessage(); $aux_field->setError('Invalid'); } } } if ($errors) { $task->setPriority($request->getInt('priority')); $task->setOwnerPHID($owner_phid); $task->setCCPHIDs($request->getArr('cc')); $task->setProjectPHIDs($request->getArr('projects')); } else { if ($request->getInt('priority') != $task->getPriority()) { $changes[ManiphestTransactionType::TYPE_PRIORITY] = $request->getInt('priority'); } if ($owner_phid != $task->getOwnerPHID()) { $changes[ManiphestTransactionType::TYPE_OWNER] = $owner_phid; } if ($request->getArr('cc') != $task->getCCPHIDs()) { $changes[ManiphestTransactionType::TYPE_CCS] = $request->getArr('cc'); } $new_proj_arr = $request->getArr('projects'); $new_proj_arr = array_values($new_proj_arr); sort($new_proj_arr); $cur_proj_arr = $task->getProjectPHIDs(); $cur_proj_arr = array_values($cur_proj_arr); sort($cur_proj_arr); if ($new_proj_arr != $cur_proj_arr) { $changes[ManiphestTransactionType::TYPE_PROJECTS] = $new_proj_arr; } if ($files) { $file_map = mpull($files, 'getPHID'); $file_map = array_fill_keys($file_map, array()); $changes[ManiphestTransactionType::TYPE_ATTACH] = array( PhabricatorPHIDConstants::PHID_TYPE_FILE => $file_map, ); } $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_WEB, array( 'ip' => $request->getRemoteAddr(), )); $template = new ManiphestTransaction(); $template->setAuthorPHID($user->getPHID()); $template->setContentSource($content_source); $transactions = array(); foreach ($changes as $type => $value) { $transaction = clone $template; $transaction->setTransactionType($type); $transaction->setNewValue($value); $transactions[] = $transaction; } if ($aux_fields) { $task->loadAndAttachAuxiliaryAttributes(); foreach ($aux_fields as $aux_field) { $transaction = clone $template; $transaction->setTransactionType( ManiphestTransactionType::TYPE_AUXILIARY); $aux_key = $aux_field->getAuxiliaryKey(); $transaction->setMetadataValue('aux:key', $aux_key); $transaction->setNewValue($aux_field->getValueForStorage()); $transactions[] = $transaction; } } if ($transactions) { $is_new = !$task->getID(); $event = new PhabricatorEvent( PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK, array( 'task' => $task, 'new' => $is_new, 'transactions' => $transactions, )); $event->setUser($user); $event->setAphrontRequest($request); PhutilEventEngine::dispatchEvent($event); $task = $event->getValue('task'); $transactions = $event->getValue('transactions'); $editor = new ManiphestTransactionEditor(); + $editor->setActor($user); $editor->setAuxiliaryFields($aux_fields); $editor->applyTransactions($task, $transactions); $event = new PhabricatorEvent( PhabricatorEventType::TYPE_MANIPHEST_DIDEDITTASK, array( 'task' => $task, 'new' => $is_new, 'transactions' => $transactions, )); $event->setUser($user); $event->setAphrontRequest($request); PhutilEventEngine::dispatchEvent($event); } if ($parent_task) { id(new PhabricatorEdgeEditor()) ->setUser($user) ->addEdge( $parent_task->getPHID(), PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK, $task->getPHID()) ->save(); $workflow = $parent_task->getID(); } $redirect_uri = '/T'.$task->getID(); if ($workflow) { $redirect_uri .= '?workflow='.$workflow; } return id(new AphrontRedirectResponse()) ->setURI($redirect_uri); } } else { if ($aux_fields) { $task->loadAndAttachAuxiliaryAttributes(); foreach ($aux_fields as $aux_field) { $aux_key = $aux_field->getAuxiliaryKey(); $value = $task->getAuxiliaryAttribute($aux_key); $aux_field->setValueFromStorage($value); } } if (!$task->getID()) { $task->setCCPHIDs(array( $user->getPHID(), )); if ($template_id) { $template_task = id(new ManiphestTask())->load($template_id); if ($template_task) { $task->setCCPHIDs($template_task->getCCPHIDs()); $task->setProjectPHIDs($template_task->getProjectPHIDs()); $task->setOwnerPHID($template_task->getOwnerPHID()); $task->setPriority($template_task->getPriority()); if ($aux_fields) { $template_task->loadAndAttachAuxiliaryAttributes(); foreach ($aux_fields as $aux_field) { if (!$aux_field->shouldCopyWhenCreatingSimilarTask()) { continue; } $aux_key = $aux_field->getAuxiliaryKey(); $value = $template_task->getAuxiliaryAttribute($aux_key); $aux_field->setValueFromStorage($value); } } } } } } $phids = array_merge( array($task->getOwnerPHID()), $task->getCCPHIDs(), $task->getProjectPHIDs()); if ($parent_task) { $phids[] = $parent_task->getPHID(); } $phids = array_filter($phids); $phids = array_unique($phids); $handles = $this->loadViewerHandles($phids); $tvalues = mpull($handles, 'getFullName', 'getPHID'); $error_view = null; if ($errors) { $error_view = new AphrontErrorView(); $error_view->setErrors($errors); $error_view->setTitle('Form Errors'); } $priority_map = ManiphestTaskPriority::getTaskPriorityMap(); if ($task->getOwnerPHID()) { $assigned_value = array( $task->getOwnerPHID() => $handles[$task->getOwnerPHID()]->getFullName(), ); } else { $assigned_value = array(); } if ($task->getCCPHIDs()) { $cc_value = array_select_keys($tvalues, $task->getCCPHIDs()); } else { $cc_value = array(); } if ($task->getProjectPHIDs()) { $projects_value = array_select_keys($tvalues, $task->getProjectPHIDs()); } else { $projects_value = array(); } $cancel_id = nonempty($task->getID(), $template_id); if ($cancel_id) { $cancel_uri = '/T'.$cancel_id; } else { $cancel_uri = '/maniphest/'; } if ($task->getID()) { $button_name = 'Save Task'; $header_name = 'Edit Task'; } else if ($parent_task) { $cancel_uri = '/T'.$parent_task->getID(); $button_name = 'Create Task'; $header_name = 'Create New Subtask'; } else { $button_name = 'Create Task'; $header_name = 'Create New Task'; } require_celerity_resource('maniphest-task-edit-css'); $project_tokenizer_id = celerity_generate_unique_node_id(); $form = new AphrontFormView(); $form ->setUser($user) ->setAction($request->getRequestURI()->getPath()) ->addHiddenInput('template', $template_id); if ($parent_task) { $form ->appendChild( id(new AphrontFormStaticControl()) ->setLabel('Parent Task') ->setValue($handles[$parent_task->getPHID()]->getFullName())) ->addHiddenInput('parent', $parent_task->getID()); } $form ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel('Title') ->setName('title') ->setError($e_title) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT) ->setValue($task->getTitle())); if ($task->getID()) { // Only show this in "edit" mode, not "create" mode, since creating a // non-open task is kind of silly and it would just clutter up the // "create" interface. $form ->appendChild( id(new AphrontFormSelectControl()) ->setLabel('Status') ->setName('status') ->setValue($task->getStatus()) ->setOptions(ManiphestTaskStatus::getTaskStatusMap())); } $form ->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel('Assigned To') ->setName('assigned_to') ->setValue($assigned_value) ->setUser($user) ->setDatasource('/typeahead/common/users/') ->setLimit(1)) ->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel('CC') ->setName('cc') ->setValue($cc_value) ->setUser($user) ->setDatasource('/typeahead/common/mailable/')) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel('Priority') ->setName('priority') ->setOptions($priority_map) ->setValue($task->getPriority())) ->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel('Projects') ->setName('projects') ->setValue($projects_value) ->setID($project_tokenizer_id) ->setCaption( javelin_render_tag( 'a', array( 'href' => '/project/create/', 'mustcapture' => true, 'sigil' => 'project-create', ), 'Create New Project')) ->setDatasource('/typeahead/common/projects/')); if ($aux_fields) { foreach ($aux_fields as $aux_field) { if ($aux_field->isRequired() && !$aux_field->getError() && !$aux_field->getValue()) { $aux_field->setError(true); } $aux_control = $aux_field->renderControl(); $form->appendChild($aux_control); } } require_celerity_resource('aphront-error-view-css'); Javelin::initBehavior('project-create', array( 'tokenizerID' => $project_tokenizer_id, )); if ($files) { $file_display = array(); foreach ($files as $file) { $file_display[] = phutil_escape_html($file->getName()); } $file_display = implode('
', $file_display); $form->appendChild( id(new AphrontFormMarkupControl()) ->setLabel('Files') ->setValue($file_display)); foreach ($files as $ii => $file) { $form->addHiddenInput('files['.$ii.']', $file->getPHID()); } } $description_control = new PhabricatorRemarkupControl(); // "Upsell" creating tasks via email in create flows if the instance is // configured for this awesomeness. $email_create = PhabricatorEnv::getEnvConfig( 'metamta.maniphest.public-create-email'); if (!$task->getID() && $email_create) { $email_hint = 'You can also create tasks by sending an email to: '. ''.phutil_escape_html($email_create).''; $description_control->setCaption($email_hint); } $description_control ->setLabel('Description') ->setName('description') ->setID('description-textarea') ->setValue($task->getDescription()); $form ->appendChild($description_control); if (!$task->getID()) { $form ->appendChild( id(new AphrontFormDragAndDropUploadControl()) ->setLabel('Attached Files') ->setName('files') ->setActivatedClass('aphront-panel-view-drag-and-drop')); } $form ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) ->setValue($button_name)); $panel = new AphrontPanelView(); $panel->setWidth(AphrontPanelView::WIDTH_FULL); $panel->setHeader($header_name); $panel->appendChild($form); $description_preview_panel = '
Description Preview
Loading preview...
'; Javelin::initBehavior( 'maniphest-description-preview', array( 'preview' => 'description-preview', 'textarea' => 'description-textarea', 'uri' => '/maniphest/task/descriptionpreview/', )); if ($task->getID()) { $page_objects = array( $task->getPHID() ); } else { $page_objects = array(); } return $this->buildStandardPageResponse( array( $error_view, $panel, $description_preview_panel ), array( 'title' => $header_name, 'pageObjects' => $page_objects, )); } } diff --git a/src/applications/maniphest/controller/ManiphestTransactionSaveController.php b/src/applications/maniphest/controller/ManiphestTransactionSaveController.php index eb90e74470..18b98c21f4 100644 --- a/src/applications/maniphest/controller/ManiphestTransactionSaveController.php +++ b/src/applications/maniphest/controller/ManiphestTransactionSaveController.php @@ -1,261 +1,262 @@ getRequest(); $user = $request->getUser(); $task = id(new ManiphestTask())->load($request->getStr('taskID')); if (!$task) { return new Aphront404Response(); } $transactions = array(); $action = $request->getStr('action'); // If we have drag-and-dropped files, attach them first in a separate // transaction. These can come in on any transaction type, which is why we // handle them separately. $files = array(); // Look for drag-and-drop uploads first. $file_phids = $request->getArr('files'); if ($file_phids) { $files = id(new PhabricatorFile())->loadAllWhere( 'phid in (%Ls)', $file_phids); } // This means "attach a file" even though we store other types of data // as 'attached'. if ($action == ManiphestTransactionType::TYPE_ATTACH) { if (!empty($_FILES['file'])) { $err = idx($_FILES['file'], 'error'); if ($err != UPLOAD_ERR_NO_FILE) { $file = PhabricatorFile::newFromPHPUpload( $_FILES['file'], array( 'authorPHID' => $user->getPHID(), )); $files[] = $file; } } } // If we had explicit or drag-and-drop files, create a transaction // for those before we deal with whatever else might have happened. $file_transaction = null; if ($files) { $files = mpull($files, 'getPHID', 'getPHID'); $new = $task->getAttached(); foreach ($files as $phid) { if (empty($new[PhabricatorPHIDConstants::PHID_TYPE_FILE])) { $new[PhabricatorPHIDConstants::PHID_TYPE_FILE] = array(); } $new[PhabricatorPHIDConstants::PHID_TYPE_FILE][$phid] = array(); } $transaction = new ManiphestTransaction(); $transaction ->setAuthorPHID($user->getPHID()) ->setTransactionType(ManiphestTransactionType::TYPE_ATTACH); $transaction->setNewValue($new); $transactions[] = $transaction; $file_transaction = $transaction; } // Compute new CCs added by @mentions. Several things can cause CCs to // be added as side effects: mentions, explicit CCs, users who aren't // CC'd interacting with the task, and ownership changes. We build up a // list of all the CCs and then construct a transaction for them at the // end if necessary. $added_ccs = PhabricatorMarkupEngine::extractPHIDsFromMentions( array( $request->getStr('comments'), )); $cc_transaction = new ManiphestTransaction(); $cc_transaction ->setAuthorPHID($user->getPHID()) ->setTransactionType(ManiphestTransactionType::TYPE_CCS); $force_cc_transaction = false; $transaction = new ManiphestTransaction(); $transaction ->setAuthorPHID($user->getPHID()) ->setComments($request->getStr('comments')) ->setTransactionType($action); switch ($action) { case ManiphestTransactionType::TYPE_STATUS: $transaction->setNewValue($request->getStr('resolution')); break; case ManiphestTransactionType::TYPE_OWNER: $assign_to = $request->getArr('assign_to'); $assign_to = reset($assign_to); $transaction->setNewValue($assign_to); break; case ManiphestTransactionType::TYPE_PROJECTS: $projects = $request->getArr('projects'); $projects = array_merge($projects, $task->getProjectPHIDs()); $projects = array_filter($projects); $projects = array_unique($projects); $transaction->setNewValue($projects); break; case ManiphestTransactionType::TYPE_CCS: // Accumulate the new explicit CCs into the array that we'll add in // the CC transaction later. $added_ccs = array_merge($added_ccs, $request->getArr('ccs')); // Transfer any comments over to the CC transaction. $cc_transaction->setComments($transaction->getComments()); // Make sure we include this transaction, even if the user didn't // actually add any CC's, because we'll discard their comment otherwise. $force_cc_transaction = true; // Throw away the primary transaction. $transaction = null; break; case ManiphestTransactionType::TYPE_PRIORITY: $transaction->setNewValue($request->getInt('priority')); break; case ManiphestTransactionType::TYPE_NONE: case ManiphestTransactionType::TYPE_ATTACH: // If we have a file transaction, just get rid of this secondary // transaction and put the comments on it instead. if ($file_transaction) { $file_transaction->setComments($transaction->getComments()); $transaction = null; } break; default: throw new Exception('unknown action'); } if ($transaction) { $transactions[] = $transaction; } // When you interact with a task, we add you to the CC list so you get // further updates, and possibly assign the task to you if you took an // ownership action (closing it) but it's currently unowned. We also move // previous owners to CC if ownership changes. Detect all these conditions // and create side-effect transactions for them. $implicitly_claimed = false; switch ($action) { case ManiphestTransactionType::TYPE_OWNER: if ($task->getOwnerPHID() == $transaction->getNewValue()) { // If this is actually no-op, don't generate the side effect. break; } // Otherwise, when a task is reassigned, move the previous owner to CC. $added_ccs[] = $task->getOwnerPHID(); break; case ManiphestTransactionType::TYPE_STATUS: if (!$task->getOwnerPHID() && $request->getStr('resolution') != ManiphestTaskStatus::STATUS_OPEN) { // Closing an unassigned task. Assign the user as the owner of // this task. $assign = new ManiphestTransaction(); $assign->setAuthorPHID($user->getPHID()); $assign->setTransactionType(ManiphestTransactionType::TYPE_OWNER); $assign->setNewValue($user->getPHID()); $transactions[] = $assign; $implicitly_claimed = true; } break; } $user_owns_task = false; if ($implicitly_claimed) { $user_owns_task = true; } else { if ($action == ManiphestTransactionType::TYPE_OWNER) { if ($transaction->getNewValue() == $user->getPHID()) { $user_owns_task = true; } } else if ($task->getOwnerPHID() == $user->getPHID()) { $user_owns_task = true; } } if (!$user_owns_task) { // If we aren't making the user the new task owner and they aren't the // existing task owner, add them to CC. $added_ccs[] = $user->getPHID(); } if ($added_ccs || $force_cc_transaction) { // We've added CCs, so include a CC transaction. It's safe to do this even // if we're just "adding" CCs which already exist, because the // ManiphestTransactionEditor is smart enough to ignore them. $all_ccs = array_merge($task->getCCPHIDs(), $added_ccs); $cc_transaction->setNewValue($all_ccs); $transactions[] = $cc_transaction; } $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_WEB, array( 'ip' => $request->getRemoteAddr(), )); foreach ($transactions as $transaction) { $transaction->setContentSource($content_source); } $event = new PhabricatorEvent( PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK, array( 'task' => $task, 'new' => false, 'transactions' => $transactions, )); $event->setUser($user); $event->setAphrontRequest($request); PhutilEventEngine::dispatchEvent($event); $task = $event->getValue('task'); $transactions = $event->getValue('transactions'); $editor = new ManiphestTransactionEditor(); + $editor->setActor($user); $editor->applyTransactions($task, $transactions); $draft = id(new PhabricatorDraft())->loadOneWhere( 'authorPHID = %s AND draftKey = %s', $user->getPHID(), $task->getPHID()); if ($draft) { $draft->delete(); } return id(new AphrontRedirectResponse()) ->setURI('/T'.$task->getID()); } } diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index 8ced7f62c3..0570a200e9 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -1,435 +1,447 @@ auxiliaryFields = $fields; return $this; } public function setParentMessageID($parent_message_id) { $this->parentMessageID = $parent_message_id; return $this; } + public function setExcludePHIDs(array $exclude) { + $this->excludePHIDs = $exclude; + return $this; + } + + public function getExcludePHIDs() { + return $this->excludePHIDs; + } + public function applyTransactions(ManiphestTask $task, array $transactions) { assert_instances_of($transactions, 'ManiphestTransaction'); $email_cc = $task->getCCPHIDs(); $email_to = array(); $email_to[] = $task->getOwnerPHID(); $pri_changed = $this->isCreate($transactions); foreach ($transactions as $key => $transaction) { $type = $transaction->getTransactionType(); $new = $transaction->getNewValue(); $email_to[] = $transaction->getAuthorPHID(); $value_is_phid_set = false; switch ($type) { case ManiphestTransactionType::TYPE_NONE: $old = null; break; case ManiphestTransactionType::TYPE_STATUS: $old = $task->getStatus(); break; case ManiphestTransactionType::TYPE_OWNER: $old = $task->getOwnerPHID(); break; case ManiphestTransactionType::TYPE_CCS: $old = $task->getCCPHIDs(); $value_is_phid_set = true; break; case ManiphestTransactionType::TYPE_PRIORITY: $old = $task->getPriority(); break; case ManiphestTransactionType::TYPE_EDGE: $old = $transaction->getOldValue(); break; case ManiphestTransactionType::TYPE_ATTACH: $old = $task->getAttached(); break; case ManiphestTransactionType::TYPE_TITLE: $old = $task->getTitle(); break; case ManiphestTransactionType::TYPE_DESCRIPTION: $old = $task->getDescription(); break; case ManiphestTransactionType::TYPE_PROJECTS: $old = $task->getProjectPHIDs(); $value_is_phid_set = true; break; case ManiphestTransactionType::TYPE_AUXILIARY: $aux_key = $transaction->getMetadataValue('aux:key'); if (!$aux_key) { throw new Exception( "Expected 'aux:key' metadata on TYPE_AUXILIARY transaction."); } $old = $task->getAuxiliaryAttribute($aux_key); break; default: throw new Exception('Unknown action type.'); } $old_cmp = $old; $new_cmp = $new; if ($value_is_phid_set) { // Normalize the old and new values if they are PHID sets so we don't // get any no-op transactions where the values differ only by keys, // order, duplicates, etc. if (is_array($old)) { $old = array_filter($old); $old = array_unique($old); sort($old); $old = array_values($old); $old_cmp = $old; } if (is_array($new)) { $new = array_filter($new); $new = array_unique($new); $transaction->setNewValue($new); $new_cmp = $new; sort($new_cmp); $new_cmp = array_values($new_cmp); } } if (($old !== null) && ($old_cmp == $new_cmp)) { if (count($transactions) > 1 && !$transaction->hasComments()) { // If we have at least one other transaction and this one isn't // doing anything and doesn't have any comments, just throw it // away. unset($transactions[$key]); continue; } else { $transaction->setOldValue(null); $transaction->setNewValue(null); $transaction->setTransactionType(ManiphestTransactionType::TYPE_NONE); } } else { switch ($type) { case ManiphestTransactionType::TYPE_NONE: break; case ManiphestTransactionType::TYPE_STATUS: $task->setStatus($new); break; case ManiphestTransactionType::TYPE_OWNER: if ($new) { $handles = id(new PhabricatorObjectHandleData(array($new))) ->loadHandles(); $task->setOwnerOrdering($handles[$new]->getName()); } else { $task->setOwnerOrdering(null); } $task->setOwnerPHID($new); break; case ManiphestTransactionType::TYPE_CCS: $task->setCCPHIDs($new); break; case ManiphestTransactionType::TYPE_PRIORITY: $task->setPriority($new); $pri_changed = true; break; case ManiphestTransactionType::TYPE_ATTACH: $task->setAttached($new); break; case ManiphestTransactionType::TYPE_TITLE: $task->setTitle($new); break; case ManiphestTransactionType::TYPE_DESCRIPTION: $task->setDescription($new); break; case ManiphestTransactionType::TYPE_PROJECTS: $task->setProjectPHIDs($new); break; case ManiphestTransactionType::TYPE_AUXILIARY: $aux_key = $transaction->getMetadataValue('aux:key'); $task->setAuxiliaryAttribute($aux_key, $new); break; case ManiphestTransactionType::TYPE_EDGE: // Edge edits are accomplished through PhabricatorEdgeEditor, which // has authority. break; default: throw new Exception('Unknown action type.'); } $transaction->setOldValue($old); $transaction->setNewValue($new); } } if ($pri_changed) { $subpriority = ManiphestTransactionEditor::getNextSubpriority( $task->getPriority(), null); $task->setSubpriority($subpriority); } $task->save(); foreach ($transactions as $transaction) { $transaction->setTaskID($task->getID()); $transaction->save(); } $email_to[] = $task->getOwnerPHID(); $email_cc = array_merge( $email_cc, $task->getCCPHIDs()); $this->publishFeedStory($task, $transactions); // TODO: Do this offline via timeline PhabricatorSearchManiphestIndexer::indexTask($task); $this->sendEmail($task, $transactions, $email_to, $email_cc); } protected function getSubjectPrefix() { return PhabricatorEnv::getEnvConfig('metamta.maniphest.subject-prefix'); } private function sendEmail($task, $transactions, $email_to, $email_cc) { + $exclude = $this->getExcludePHIDs(); $email_to = array_filter(array_unique($email_to)); $email_cc = array_filter(array_unique($email_cc)); $phids = array(); foreach ($transactions as $transaction) { foreach ($transaction->extractPHIDs() as $phid) { $phids[$phid] = true; } } foreach ($email_to as $phid) { $phids[$phid] = true; } foreach ($email_cc as $phid) { $phids[$phid] = true; } $phids = array_keys($phids); $handles = id(new PhabricatorObjectHandleData($phids)) ->loadHandles(); $view = new ManiphestTransactionDetailView(); $view->setTransactionGroup($transactions); $view->setHandles($handles); $view->setAuxiliaryFields($this->auxiliaryFields); list($action, $main_body) = $view->renderForEmail($with_date = false); $is_create = $this->isCreate($transactions); $task_uri = PhabricatorEnv::getURI('/T'.$task->getID()); $reply_handler = $this->buildReplyHandler($task); $body = new PhabricatorMetaMTAMailBody(); $body->addRawSection($main_body); if ($is_create) { $body->addTextSection(pht('TASK DESCRIPTION'), $task->getDescription()); } $body->addTextSection(pht('TASK DETAIL'), $task_uri); $body->addReplySection($reply_handler->getReplyHandlerInstructions()); $thread_id = 'maniphest-task-'.$task->getPHID(); $task_id = $task->getID(); $title = $task->getTitle(); $mailtags = $this->getMailTags($transactions); $template = id(new PhabricatorMetaMTAMail()) ->setSubject("T{$task_id}: {$title}") ->setSubjectPrefix($this->getSubjectPrefix()) ->setVarySubjectPrefix("[{$action}]") ->setFrom($transaction->getAuthorPHID()) ->setParentMessageID($this->parentMessageID) ->addHeader('Thread-Topic', "T{$task_id}: ".$task->getOriginalTitle()) ->setThreadID($thread_id, $is_create) ->setRelatedPHID($task->getPHID()) + ->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs()) ->setIsBulk(true) ->setMailTags($mailtags) ->setBody($body->render()); $mails = $reply_handler->multiplexMail( $template, array_select_keys($handles, $email_to), array_select_keys($handles, $email_cc)); foreach ($mails as $mail) { $mail->saveAndSend(); } } public function buildReplyHandler(ManiphestTask $task) { $handler_object = PhabricatorEnv::newObjectFromConfig( 'metamta.maniphest.reply-handler'); $handler_object->setMailReceiver($task); return $handler_object; } private function publishFeedStory(ManiphestTask $task, array $transactions) { assert_instances_of($transactions, 'ManiphestTransaction'); $actions = array(ManiphestAction::ACTION_UPDATE); $comments = null; foreach ($transactions as $transaction) { if ($transaction->hasComments()) { $comments = $transaction->getComments(); } $type = $transaction->getTransactionType(); switch ($type) { case ManiphestTransactionType::TYPE_OWNER: $actions[] = ManiphestAction::ACTION_ASSIGN; break; case ManiphestTransactionType::TYPE_STATUS: if ($task->getStatus() != ManiphestTaskStatus::STATUS_OPEN) { $actions[] = ManiphestAction::ACTION_CLOSE; } else if ($this->isCreate($transactions)) { $actions[] = ManiphestAction::ACTION_CREATE; } else { $actions[] = ManiphestAction::ACTION_REOPEN; } break; default: $actions[] = $type; break; } } $action_type = ManiphestAction::selectStrongestAction($actions); $owner_phid = $task->getOwnerPHID(); $actor_phid = head($transactions)->getAuthorPHID(); $author_phid = $task->getAuthorPHID(); id(new PhabricatorFeedStoryPublisher()) ->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_MANIPHEST) ->setStoryData(array( 'taskPHID' => $task->getPHID(), 'transactionIDs' => mpull($transactions, 'getID'), 'ownerPHID' => $owner_phid, 'action' => $action_type, 'comments' => $comments, 'description' => $task->getDescription(), )) ->setStoryTime(time()) ->setStoryAuthorPHID($actor_phid) ->setRelatedPHIDs( array_merge( array_filter( array( $task->getPHID(), $author_phid, $actor_phid, $owner_phid, )), $task->getProjectPHIDs())) ->setPrimaryObjectPHID($task->getPHID()) ->setSubscribedPHIDs( array_merge( array_filter( array( $author_phid, $owner_phid, $actor_phid)), $task->getCCPHIDs())) ->publish(); } private function isCreate(array $transactions) { assert_instances_of($transactions, 'ManiphestTransaction'); $is_create = false; foreach ($transactions as $transaction) { $type = $transaction->getTransactionType(); if (($type == ManiphestTransactionType::TYPE_STATUS) && ($transaction->getOldValue() === null) && ($transaction->getNewValue() == ManiphestTaskStatus::STATUS_OPEN)) { $is_create = true; } } return $is_create; } private function getMailTags(array $transactions) { assert_instances_of($transactions, 'ManiphestTransaction'); $tags = array(); foreach ($transactions as $xaction) { switch ($xaction->getTransactionType()) { case ManiphestTransactionType::TYPE_CCS: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_CC; break; case ManiphestTransactionType::TYPE_PROJECTS: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_PROJECTS; break; case ManiphestTransactionType::TYPE_PRIORITY: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_PRIORITY; break; default: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_OTHER; break; } if ($xaction->hasComments()) { $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_COMMENT; } } return array_unique($tags); } public static function getNextSubpriority($pri, $sub) { if ($sub === null) { $next = id(new ManiphestTask())->loadOneWhere( 'priority = %d ORDER BY subpriority ASC LIMIT 1', $pri); if ($next) { return $next->getSubpriority() - ((double)(2 << 16)); } } else { $next = id(new ManiphestTask())->loadOneWhere( 'priority = %d AND subpriority > %s ORDER BY subpriority ASC LIMIT 1', $pri, $sub); if ($next) { return ($sub + $next->getSubpriority()) / 2; } } return (double)(2 << 32); } } diff --git a/src/applications/maniphest/event/ManiphestEdgeEventListener.php b/src/applications/maniphest/event/ManiphestEdgeEventListener.php index 9ba10311ad..73f0c56711 100644 --- a/src/applications/maniphest/event/ManiphestEdgeEventListener.php +++ b/src/applications/maniphest/event/ManiphestEdgeEventListener.php @@ -1,145 +1,146 @@ listen(PhabricatorEventType::TYPE_EDGE_WILLEDITEDGES); $this->listen(PhabricatorEventType::TYPE_EDGE_DIDEDITEDGES); } public function handleEvent(PhutilEvent $event) { switch ($event->getType()) { case PhabricatorEventType::TYPE_EDGE_WILLEDITEDGES: return $this->handleWillEditEvent($event); case PhabricatorEventType::TYPE_EDGE_DIDEDITEDGES: return $this->handleDidEditEvent($event); } } private function handleWillEditEvent(PhutilEvent $event) { // NOTE: Everything is namespaced by `id` so that we aren't left in an // inconsistent state if an edit fails to complete (e.g., something throws) // or an edit happens inside another edit. $id = $event->getValue('id'); $edges = $this->loadAllEdges($event); $tasks = array(); if ($edges) { $tasks = id(new ManiphestTask())->loadAllWhere( 'phid IN (%Ls)', array_keys($edges)); $tasks = mpull($tasks, null, 'getPHID'); } $this->edges[$id] = $edges; $this->tasks[$id] = $tasks; } private function handleDidEditEvent(PhutilEvent $event) { $id = $event->getValue('id'); $old_edges = $this->edges[$id]; $tasks = $this->tasks[$id]; unset($this->edges[$id]); unset($this->tasks[$id]); $new_edges = $this->loadAllEdges($event); $editor = new ManiphestTransactionEditor(); + $editor->setActor($event->getUser()); foreach ($tasks as $phid => $task) { $xactions = array(); $old = $old_edges[$phid]; $new = $new_edges[$phid]; $types = array_keys($old + $new); foreach ($types as $type) { $old_type = idx($old, $type, array()); $new_type = idx($new, $type, array()); if ($old_type === $new_type) { continue; } $xactions[] = id(new ManiphestTransaction()) ->setTransactionType(ManiphestTransactionType::TYPE_EDGE) ->setOldValue($old_type) ->setNewValue($new_type) ->setMetadataValue('edge:type', $type) ->setAuthorPHID($event->getUser()->getPHID()); } if ($xactions) { $editor->applyTransactions($task, $xactions); } } } private function filterEdgesBySourceType(array $edges, $type) { foreach ($edges as $key => $edge) { if ($edge['src_type'] !== $type) { unset($edges[$key]); } } return $edges; } private function loadAllEdges(PhutilEvent $event) { $add_edges = $event->getValue('add'); $rem_edges = $event->getValue('rem'); $type_task = PhabricatorPHIDConstants::PHID_TYPE_TASK; $all_edges = array_merge($add_edges, $rem_edges); $all_edges = $this->filterEdgesBySourceType($all_edges, $type_task); if (!$all_edges) { return; } $all_tasks = array(); $all_types = array(); foreach ($all_edges as $edge) { $all_tasks[$edge['src']] = true; $all_types[$edge['type']] = true; } $all_tasks = array_keys($all_tasks); $all_types = array_keys($all_types); return id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($all_tasks) ->withEdgeTypes($all_types) ->needEdgeData(true) ->execute(); } } diff --git a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php index 3c74108c3e..02429d9d36 100644 --- a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php +++ b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php @@ -1,298 +1,308 @@ validateMailReceiver($mail_receiver); $this->mailReceiver = $mail_receiver; return $this; } final public function getMailReceiver() { return $this->mailReceiver; } final public function setActor(PhabricatorUser $actor) { $this->actor = $actor; return $this; } final public function getActor() { return $this->actor; } + final public function setExcludeMailRecipientPHIDs(array $exclude) { + $this->excludePHIDs = $exclude; + return $this; + } + + final public function getExcludeMailRecipientPHIDs() { + return $this->excludePHIDs; + } + abstract public function validateMailReceiver($mail_receiver); abstract public function getPrivateReplyHandlerEmailAddress( PhabricatorObjectHandle $handle); abstract public function getReplyHandlerDomain(); abstract public function getReplyHandlerInstructions(); abstract protected function receiveEmail( PhabricatorMetaMTAReceivedMail $mail); public function processEmail(PhabricatorMetaMTAReceivedMail $mail) { $error = $this->sanityCheckEmail($mail); if ($error) { if ($this->shouldSendErrorEmail($mail)) { $this->sendErrorEmail($error, $mail); } return null; } return $this->receiveEmail($mail); } private function sanityCheckEmail(PhabricatorMetaMTAReceivedMail $mail) { $body = $mail->getCleanTextBody(); if (empty($body)) { return 'Empty email body. Email should begin with an !action and / or '. 'text to comment. Inline replies and signatures are ignored.'; } return null; } /** * Only send an error email if the user is talking to just Phabricator. We * can assume if there is only one To address it is a Phabricator address * since this code is running and everything. */ private function shouldSendErrorEmail(PhabricatorMetaMTAReceivedMail $mail) { return count($mail->getToAddresses() == 1) && count($mail->getCCAddresses() == 0); } private function sendErrorEmail($error, PhabricatorMetaMTAReceivedMail $mail) { $template = new PhabricatorMetaMTAMail(); $template->setSubject('Exception: unable to process your mail request'); $template->setBody($this->buildErrorMailBody($error, $mail)); $template->setRelatedPHID($mail->getRelatedPHID()); $phid = $this->getActor()->getPHID(); $tos = array( $phid => PhabricatorObjectHandleData::loadOneHandle($phid) ); $mails = $this->multiplexMail($template, $tos, array()); foreach ($mails as $email) { $email->saveAndSend(); } return true; } private function buildErrorMailBody($error, PhabricatorMetaMTAReceivedMail $mail) { $original_body = $mail->getRawTextBody(); $main_body = <<addRawSection($main_body); $body->addReplySection($this->getReplyHandlerInstructions()); return $body->render(); } public function supportsPrivateReplies() { return (bool)$this->getReplyHandlerDomain() && !$this->supportsPublicReplies(); } public function supportsPublicReplies() { if (!PhabricatorEnv::getEnvConfig('metamta.public-replies')) { return false; } if (!$this->getReplyHandlerDomain()) { return false; } return (bool)$this->getPublicReplyHandlerEmailAddress(); } final public function supportsReplies() { return $this->supportsPrivateReplies() || $this->supportsPublicReplies(); } public function getPublicReplyHandlerEmailAddress() { return null; } final public function getRecipientsSummary( array $to_handles, array $cc_handles) { assert_instances_of($to_handles, 'PhabricatorObjectHandle'); assert_instances_of($cc_handles, 'PhabricatorObjectHandle'); $body = ''; if (PhabricatorEnv::getEnvConfig('metamta.recipients.show-hints')) { if ($to_handles) { $body .= "To: ".implode(', ', mpull($to_handles, 'getName'))."\n"; } if ($cc_handles) { $body .= "Cc: ".implode(', ', mpull($cc_handles, 'getName'))."\n"; } } return $body; } final public function multiplexMail( PhabricatorMetaMTAMail $mail_template, array $to_handles, array $cc_handles) { assert_instances_of($to_handles, 'PhabricatorObjectHandle'); assert_instances_of($cc_handles, 'PhabricatorObjectHandle'); $result = array(); // If MetaMTA is configured to always multiplex, skip the single-email // case. if (!PhabricatorMetaMTAMail::shouldMultiplexAllMail()) { // If private replies are not supported, simply send one email to all // recipients and CCs. This covers cases where we have no reply handler, // or we have a public reply handler. if (!$this->supportsPrivateReplies()) { $mail = clone $mail_template; $mail->addTos(mpull($to_handles, 'getPHID')); $mail->addCCs(mpull($cc_handles, 'getPHID')); if ($this->supportsPublicReplies()) { $reply_to = $this->getPublicReplyHandlerEmailAddress(); $mail->setReplyTo($reply_to); } $result[] = $mail; return $result; } } $tos = mpull($to_handles, null, 'getPHID'); $ccs = mpull($cc_handles, null, 'getPHID'); // Merge all the recipients together. TODO: We could keep the CCs as real // CCs and send to a "noreply@domain.com" type address, but keep it simple // for now. $recipients = $tos + $ccs; // When multiplexing mail, explicitly include To/Cc information in the // message body and headers. $mail_template = clone $mail_template; $mail_template->addPHIDHeaders('X-Phabricator-To', array_keys($tos)); $mail_template->addPHIDHeaders('X-Phabricator-Cc', array_keys($ccs)); $body = $mail_template->getBody(); $body .= "\n"; $body .= $this->getRecipientsSummary($to_handles, $cc_handles); foreach ($recipients as $phid => $recipient) { $mail = clone $mail_template; if (isset($to_handles[$phid])) { $mail->addTos(array($phid)); } else if (isset($cc_handles[$phid])) { $mail->addCCs(array($phid)); } else { // not good - they should be a to or a cc continue; } $mail->setBody($body); $reply_to = null; if (!$reply_to && $this->supportsPrivateReplies()) { $reply_to = $this->getPrivateReplyHandlerEmailAddress($recipient); } if (!$reply_to && $this->supportsPublicReplies()) { $reply_to = $this->getPublicReplyHandlerEmailAddress(); } if ($reply_to) { $mail->setReplyTo($reply_to); } $result[] = $mail; } return $result; } protected function getDefaultPublicReplyHandlerEmailAddress($prefix) { $receiver = $this->getMailReceiver(); $receiver_id = $receiver->getID(); $domain = $this->getReplyHandlerDomain(); // We compute a hash using the object's own PHID to prevent an attacker // from blindly interacting with objects that they haven't ever received // mail about by just sending to D1@, D2@, etc... $hash = PhabricatorMetaMTAReceivedMail::computeMailHash( $receiver->getMailKey(), $receiver->getPHID()); $address = "{$prefix}{$receiver_id}+public+{$hash}@{$domain}"; return $this->getSingleReplyHandlerPrefix($address); } protected function getSingleReplyHandlerPrefix($address) { $single_handle_prefix = PhabricatorEnv::getEnvConfig( 'metamta.single-reply-handler-prefix'); return ($single_handle_prefix) ? $single_handle_prefix . '+' . $address : $address; } protected function getDefaultPrivateReplyHandlerEmailAddress( PhabricatorObjectHandle $handle, $prefix) { if ($handle->getType() != PhabricatorPHIDConstants::PHID_TYPE_USER) { // You must be a real user to get a private reply handler address. return null; } $receiver = $this->getMailReceiver(); $receiver_id = $receiver->getID(); $user_id = $handle->getAlternateID(); $hash = PhabricatorMetaMTAReceivedMail::computeMailHash( $receiver->getMailKey(), $handle->getPHID()); $domain = $this->getReplyHandlerDomain(); $address = "{$prefix}{$receiver_id}+{$user_id}+{$hash}@{$domain}"; return $this->getSingleReplyHandlerPrefix($address); } } diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php index da7d2ef889..78df0ef773 100644 --- a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php @@ -1,829 +1,839 @@ status = self::STATUS_QUEUE; $this->retryCount = 0; $this->nextRetry = time(); $this->parameters = array(); parent::__construct(); } public function getConfiguration() { return array( self::CONFIG_SERIALIZATION => array( 'parameters' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } protected function setParam($param, $value) { $this->parameters[$param] = $value; return $this; } protected function getParam($param) { return idx($this->parameters, $param); } /** * Set tags (@{class:MetaMTANotificationType} constants) which identify the * content of this mail in a general way. These tags are used to allow users * to opt out of receiving certain types of mail, like updates when a task's * projects change. * * @param list List of @{class:MetaMTANotificationType} constants. * @return this */ public function setMailTags(array $tags) { $this->setParam('mailtags', $tags); return $this; } /** * In Gmail, conversations will be broken if you reply to a thread and the * server sends back a response without referencing your Message-ID, even if * it references a Message-ID earlier in the thread. To avoid this, use the * parent email's message ID explicitly if it's available. This overwrites the * "In-Reply-To" and "References" headers we would otherwise generate. This * needs to be set whenever an action is triggered by an email message. See * T251 for more details. * * @param string The "Message-ID" of the email which precedes this one. * @return this */ public function setParentMessageID($id) { $this->setParam('parent-message-id', $id); return $this; } public function getParentMessageID() { return $this->getParam('parent-message-id'); } public function getSubject() { return $this->getParam('subject'); } public function addTos(array $phids) { $phids = array_unique($phids); $this->setParam('to', $phids); return $this; } public function addRawTos(array $raw_email) { $this->setParam('raw-to', $raw_email); return $this; } public function addCCs(array $phids) { $phids = array_unique($phids); $this->setParam('cc', $phids); return $this; } + public function setExcludeMailRecipientPHIDs($exclude) { + $this->excludePHIDs = $exclude; + return $this; + } + private function getExcludeMailRecipientPHIDs() { + return $this->excludePHIDs; + } + public function getTranslation(array $objects) { $default_translation = PhabricatorEnv::getEnvConfig('translation.provider'); $return = null; $recipients = array_merge( idx($this->parameters, 'to', array()), idx($this->parameters, 'cc', array())); foreach (array_select_keys($objects, $recipients) as $object) { $translation = null; if ($object instanceof PhabricatorUser) { $translation = $object->getTranslation(); } if (!$translation) { $translation = $default_translation; } if ($return && $translation != $return) { return $default_translation; } $return = $translation; } if (!$return) { $return = $default_translation; } return $return; } public function addPHIDHeaders($name, array $phids) { foreach ($phids as $phid) { $this->addHeader($name, '<'.$phid.'>'); } return $this; } public function addHeader($name, $value) { $this->parameters['headers'][] = array($name, $value); return $this; } public function addAttachment(PhabricatorMetaMTAAttachment $attachment) { $this->parameters['attachments'][] = $attachment; return $this; } public function getAttachments() { return $this->getParam('attachments'); } public function setAttachments(array $attachments) { assert_instances_of($attachments, 'PhabricatorMetaMTAAttachment'); $this->setParam('attachments', $attachments); return $this; } public function setFrom($from) { $this->setParam('from', $from); return $this; } public function setReplyTo($reply_to) { $this->setParam('reply-to', $reply_to); return $this; } public function setSubject($subject) { $this->setParam('subject', $subject); return $this; } public function setSubjectPrefix($prefix) { $this->setParam('subject-prefix', $prefix); return $this; } public function setVarySubjectPrefix($prefix) { $this->setParam('vary-subject-prefix', $prefix); return $this; } public function setBody($body) { $this->setParam('body', $body); return $this; } public function getBody() { return $this->getParam('body'); } public function setIsHTML($html) { $this->setParam('is-html', $html); return $this; } public function getSimulatedFailureCount() { return nonempty($this->getParam('simulated-failures'), 0); } public function setSimulatedFailureCount($count) { $this->setParam('simulated-failures', $count); return $this; } public function getWorkerTaskID() { return $this->getParam('worker-task'); } public function setWorkerTaskID($id) { $this->setParam('worker-task', $id); return $this; } /** * Flag that this is an auto-generated bulk message and should have bulk * headers added to it if appropriate. Broadly, this means some flavor of * "Precedence: bulk" or similar, but is implementation and configuration * dependent. * * @param bool True if the mail is automated bulk mail. * @return this */ public function setIsBulk($is_bulk) { $this->setParam('is-bulk', $is_bulk); return $this; } /** * Use this method to set an ID used for message threading. MetaMTA will * set appropriate headers (Message-ID, In-Reply-To, References and * Thread-Index) based on the capabilities of the underlying mailer. * * @param string Unique identifier, appropriate for use in a Message-ID, * In-Reply-To or References headers. * @param bool If true, indicates this is the first message in the thread. * @return this */ public function setThreadID($thread_id, $is_first_message = false) { $this->setParam('thread-id', $thread_id); $this->setParam('is-first-message', $is_first_message); return $this; } /** * Save a newly created mail to the database and attempt to send it * immediately if the server is configured for immediate sends. When * applications generate new mail they should generally use this method to * deliver it. If the server doesn't use immediate sends, this has the same * effect as calling save(): the mail will eventually be delivered by the * MetaMTA daemon. * * @return this */ public function saveAndSend() { $ret = null; if (PhabricatorEnv::getEnvConfig('metamta.send-immediately')) { $ret = $this->sendNow(); } else { $ret = $this->save(); } return $ret; } protected function didWriteData() { parent::didWriteData(); if (!$this->getWorkerTaskID()) { $mailer_task = new PhabricatorWorkerTask(); $mailer_task->setTaskClass('PhabricatorMetaMTAWorker'); $mailer_task->setData($this->getID()); $mailer_task->save(); $this->setWorkerTaskID($mailer_task->getID()); $this->save(); } } public function buildDefaultMailer() { return PhabricatorEnv::newObjectFromConfig('metamta.mail-adapter'); } /** * Attempt to deliver an email immediately, in this process. * * @param bool Try to deliver this email even if it has already been * delivered or is in backoff after a failed delivery attempt. * @param PhabricatorMailImplementationAdapter Use a specific mail adapter, * instead of the default. * * @return void */ public function sendNow( $force_send = false, PhabricatorMailImplementationAdapter $mailer = null) { if ($mailer === null) { $mailer = $this->buildDefaultMailer(); } if (!$force_send) { if ($this->getStatus() != self::STATUS_QUEUE) { throw new Exception("Trying to send an already-sent mail!"); } if (time() < $this->getNextRetry()) { throw new Exception("Trying to send an email before next retry!"); } } try { $parameters = $this->parameters; $phids = array(); foreach ($parameters as $key => $value) { switch ($key) { case 'from': case 'to': case 'cc': if (!is_array($value)) { $value = array($value); } foreach (array_filter($value) as $phid) { $type = phid_get_type($phid); $phids[$phid] = $type; } break; } } $this->loadEmailAndNameDataFromPHIDs($phids); - $exclude = array(); + $exclude = array_fill_keys($this->getExcludeMailRecipientPHIDs(), true); $params = $this->parameters; $default = PhabricatorEnv::getEnvConfig('metamta.default-address'); if (empty($params['from'])) { $mailer->setFrom($default); } else { $from = $params['from']; // If the user has set their preferences to not send them email about // things they do, exclude them from being on To or Cc. $from_user = id(new PhabricatorUser())->loadOneWhere( 'phid = %s', $from); if ($from_user) { $pref_key = PhabricatorUserPreferences::PREFERENCE_NO_SELF_MAIL; $exclude_self = $from_user ->loadPreferences() ->getPreference($pref_key); if ($exclude_self) { $exclude[$from] = true; } } if (!PhabricatorEnv::getEnvConfig('metamta.can-send-as-user')) { if (empty($params['reply-to'])) { $params['reply-to'] = $phids[$from]['email']; $params['reply-to-name'] = $phids[$from]['name']; } $mailer->setFrom( $default, $phids[$from]['name']); unset($params['from']); } } $is_first = idx($params, 'is-first-message'); unset($params['is-first-message']); $is_threaded = (bool)idx($params, 'thread-id'); $reply_to_name = idx($params, 'reply-to-name', ''); unset($params['reply-to-name']); $add_cc = array(); $add_to = array(); foreach ($params as $key => $value) { switch ($key) { case 'from': $mailer->setFrom($phids[$from]['email']); break; case 'reply-to': $mailer->addReplyTo($value, $reply_to_name); break; case 'to': $to_emails = $this->filterSendable($value, $phids, $exclude); if ($to_emails) { $add_to = array_merge($add_to, $to_emails); } break; case 'raw-to': $add_to = array_merge($add_to, $value); break; case 'cc': $cc_emails = $this->filterSendable($value, $phids, $exclude); if ($cc_emails) { $add_cc = $cc_emails; } break; case 'headers': foreach ($value as $pair) { list($header_key, $header_value) = $pair; // NOTE: If we have \n in a header, SES rejects the email. $header_value = str_replace("\n", " ", $header_value); $mailer->addHeader($header_key, $header_value); } break; case 'attachments': foreach ($value as $attachment) { $mailer->addAttachment( $attachment->getData(), $attachment->getFilename(), $attachment->getMimeType() ); } break; case 'body': $mailer->setBody($value); break; case 'subject': // Only try to use preferences if everything is multiplexed, so we // get consistent behavior. $use_prefs = self::shouldMultiplexAllMail(); $prefs = null; if ($use_prefs) { // If multiplexing is enabled, some recipients will be in "Cc" // rather than "To". We'll move them to "To" later (or supply a // dummy "To") but need to look for the recipient in either the // "To" or "Cc" fields here. $target_phid = head(idx($params, 'to', array())); if (!$target_phid) { $target_phid = head(idx($params, 'cc', array())); } if ($target_phid) { $user = id(new PhabricatorUser())->loadOneWhere( 'phid = %s', $target_phid); if ($user) { $prefs = $user->loadPreferences(); } } } $subject = array(); if ($is_threaded) { $add_re = PhabricatorEnv::getEnvConfig('metamta.re-prefix'); if ($prefs) { $add_re = $prefs->getPreference( PhabricatorUserPreferences::PREFERENCE_RE_PREFIX, $add_re); } if ($add_re) { $subject[] = 'Re:'; } } $subject[] = trim(idx($params, 'subject-prefix')); $vary_prefix = idx($params, 'vary-subject-prefix'); if ($vary_prefix != '') { $use_subject = PhabricatorEnv::getEnvConfig( 'metamta.vary-subjects'); if ($prefs) { $use_subject = $prefs->getPreference( PhabricatorUserPreferences::PREFERENCE_VARY_SUBJECT, $use_subject); } if ($use_subject) { $subject[] = $vary_prefix; } } $subject[] = $value; $mailer->setSubject(implode(' ', array_filter($subject))); break; case 'is-html': if ($value) { $mailer->setIsHTML(true); } break; case 'is-bulk': if ($value) { if (PhabricatorEnv::getEnvConfig('metamta.precedence-bulk')) { $mailer->addHeader('Precedence', 'bulk'); } } break; case 'thread-id': // NOTE: Gmail freaks out about In-Reply-To and References which // aren't in the form ""; this is also required // by RFC 2822, although some clients are more liberal in what they // accept. $domain = PhabricatorEnv::getEnvConfig('metamta.domain'); $value = '<'.$value.'@'.$domain.'>'; if ($is_first && $mailer->supportsMessageIDHeader()) { $mailer->addHeader('Message-ID', $value); } else { $in_reply_to = $value; $references = array($value); $parent_id = $this->getParentMessageID(); if ($parent_id) { $in_reply_to = $parent_id; // By RFC 2822, the most immediate parent should appear last // in the "References" header, so this order is intentional. $references[] = $parent_id; } $references = implode(' ', $references); $mailer->addHeader('In-Reply-To', $in_reply_to); $mailer->addHeader('References', $references); } $thread_index = $this->generateThreadIndex($value, $is_first); $mailer->addHeader('Thread-Index', $thread_index); break; case 'mailtags': // Handled below. break; case 'subject-prefix': case 'vary-subject-prefix': // Handled above. break; default: // Just discard. } } $mailer->addHeader('X-Phabricator-Sent-This-Message', 'Yes'); $mailer->addHeader('X-Mail-Transport-Agent', 'MetaMTA'); // Some clients respect this to suppress OOF and other auto-responses. $mailer->addHeader('X-Auto-Response-Suppress', 'All'); // If the message has mailtags, filter out any recipients who don't want // to receive this type of mail. $mailtags = $this->getParam('mailtags'); if ($mailtags && ($add_to || $add_cc)) { $tag_header = array(); foreach ($mailtags as $mailtag) { $tag_header[] = '<'.$mailtag.'>'; } $tag_header = implode(', ', $tag_header); $mailer->addHeader('X-Phabricator-Mail-Tags', $tag_header); $exclude = array(); $all_recipients = array_merge( array_keys($add_to), array_keys($add_cc)); $all_prefs = id(new PhabricatorUserPreferences())->loadAllWhere( 'userPHID in (%Ls)', $all_recipients); $all_prefs = mpull($all_prefs, null, 'getUserPHID'); foreach ($all_recipients as $recipient) { $prefs = idx($all_prefs, $recipient); if (!$prefs) { continue; } $user_mailtags = $prefs->getPreference( PhabricatorUserPreferences::PREFERENCE_MAILTAGS, array()); // The user must have elected to receive mail for at least one // of the mailtags. $send = false; foreach ($mailtags as $tag) { if (idx($user_mailtags, $tag, true)) { $send = true; break; } } if (!$send) { $exclude[$recipient] = true; } } $add_to = array_diff_key($add_to, $exclude); $add_cc = array_diff_key($add_cc, $exclude); } if (!$add_to && !$add_cc) { $this->setStatus(self::STATUS_VOID); $this->setMessage( "Message has no valid recipients: all To/CC are disabled or ". "configured not to receive this mail."); return $this->save(); } // Some mailers require a valid "To:" in order to deliver mail. If we // don't have any "To:", try to fill it in with a placeholder "To:". // If that also fails, move the "Cc:" line to "To:". if (!$add_to) { $placeholder_key = 'metamta.placeholder-to-recipient'; $placeholder = PhabricatorEnv::getEnvConfig($placeholder_key); if ($placeholder !== null) { $add_to = array($placeholder); } else { $add_to = $add_cc; $add_cc = array(); } } $mailer->addTos($add_to); if ($add_cc) { $mailer->addCCs($add_cc); } } catch (Exception $ex) { $this->setStatus(self::STATUS_FAIL); $this->setMessage($ex->getMessage()); return $this->save(); } if ($this->getRetryCount() < $this->getSimulatedFailureCount()) { $ok = false; $error = 'Simulated failure.'; } else { try { $ok = $mailer->send(); $error = null; } catch (Exception $ex) { $ok = false; $error = $ex->getMessage()."\n".$ex->getTraceAsString(); } } if (!$ok) { $this->setMessage($error); if ($this->getRetryCount() > self::MAX_RETRIES) { $this->setStatus(self::STATUS_FAIL); } else { $this->setRetryCount($this->getRetryCount() + 1); $next_retry = time() + ($this->getRetryCount() * self::RETRY_DELAY); $this->setNextRetry($next_retry); } } else { $this->setStatus(self::STATUS_SENT); } return $this->save(); } public static function getReadableStatus($status_code) { static $readable = array( self::STATUS_QUEUE => "Queued for Delivery", self::STATUS_FAIL => "Delivery Failed", self::STATUS_SENT => "Sent", self::STATUS_VOID => "Void", ); $status_code = coalesce($status_code, '?'); return idx($readable, $status_code, $status_code); } private function generateThreadIndex($seed, $is_first_mail) { // When threading, Outlook ignores the 'References' and 'In-Reply-To' // headers that most clients use. Instead, it uses a custom 'Thread-Index' // header. The format of this header is something like this (from // camel-exchange-folder.c in Evolution Exchange): /* A new post to a folder gets a 27-byte-long thread index. (The value * is apparently unique but meaningless.) Each reply to a post gets a * 32-byte-long thread index whose first 27 bytes are the same as the * parent's thread index. Each reply to any of those gets a * 37-byte-long thread index, etc. The Thread-Index header contains a * base64 representation of this value. */ // The specific implementation uses a 27-byte header for the first email // a recipient receives, and a random 5-byte suffix (32 bytes total) // thereafter. This means that all the replies are (incorrectly) siblings, // but it would be very difficult to keep track of the entire tree and this // gets us reasonable client behavior. $base = substr(md5($seed), 0, 27); if (!$is_first_mail) { // Not totally sure, but it seems like outlook orders replies by // thread-index rather than timestamp, so to get these to show up in the // right order we use the time as the last 4 bytes. $base .= ' '.pack('N', time()); } return base64_encode($base); } private function loadEmailAndNameDataFromPHIDs(array &$phids) { $users = array(); $mlsts = array(); // first iteration - group by types to do data fetches foreach ($phids as $phid => $type) { switch ($type) { case PhabricatorPHIDConstants::PHID_TYPE_USER: $users[] = $phid; break; case PhabricatorPHIDConstants::PHID_TYPE_MLST: $mlsts[] = $phid; break; } } $user_emails = array(); if ($users) { $user_emails = id(new PhabricatorUserEmail())->loadAllWhere( 'userPHID IN (%Ls) AND isPrimary = 1', $users); $users = id(new PhabricatorUser())->loadAllWhere( 'phid IN (%Ls)', $users); $user_emails = mpull($user_emails, null, 'getUserPHID'); $users = mpull($users, null, 'getPHID'); } if ($mlsts) { $mlsts = id(new PhabricatorMetaMTAMailingList())->loadAllWhere( 'phid IN (%Ls)', $mlsts); $mlsts = mpull($mlsts, null, 'getPHID'); } // second iteration - create entries for each phid $default = PhabricatorEnv::getEnvConfig('metamta.default-address'); foreach ($phids as $phid => &$value) { $name = ''; $email = $default; $is_mailable = false; switch ($value) { case PhabricatorPHIDConstants::PHID_TYPE_USER: $user = $users[$phid]; if ($user) { $name = $this->getUserName($user); $is_mailable = !$user->getIsDisabled() && !$user->getIsSystemAgent(); } $email = $user_emails[$phid] ? $user_emails[$phid]->getAddress() : $default; break; case PhabricatorPHIDConstants::PHID_TYPE_MLST: $mlst = $mlsts[$phid]; if ($mlst) { $name = $mlst->getName(); $email = $mlst->getEmail(); $is_mailable = true; } break; } $value = array( 'name' => $name, 'email' => $email, 'mailable' => $is_mailable, ); } } /** * Small helper function to make sure we format the username properly as * specified by the `metamta.user-address-format` configuration value. */ private function getUserName($user) { $format = PhabricatorEnv::getEnvConfig( 'metamta.user-address-format', 'full' ); switch ($format) { case 'short': $name = $user->getUserName(); break; case 'real': $name = $user->getRealName(); break; case 'full': default: $name = $user->getFullName(); break; } return $name; } private function filterSendable($value, $phids, $exclude) { $result = array(); foreach ($value as $phid) { if (isset($exclude[$phid])) { continue; } if (isset($phids[$phid]) && $phids[$phid]['mailable']) { $result[$phid] = $phids[$phid]['email']; } } return $result; } public static function shouldMultiplexAllMail() { return PhabricatorEnv::getEnvConfig('metamta.one-mail-per-recipient'); } } diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php index 59736d0538..2ddd902644 100644 --- a/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php @@ -1,369 +1,392 @@ array( 'headers' => self::SERIALIZATION_JSON, 'bodies' => self::SERIALIZATION_JSON, 'attachments' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function setHeaders(array $headers) { // Normalize headers to lowercase. $normalized = array(); foreach ($headers as $name => $value) { $normalized[strtolower($name)] = $value; } $this->headers = $normalized; return $this; } public function getMessageID() { return idx($this->headers, 'message-id'); } public function getSubject() { return idx($this->headers, 'subject'); } public function getCCAddresses() { return $this->getRawEmailAddresses(idx($this->headers, 'cc')); } public function getToAddresses() { return $this->getRawEmailAddresses(idx($this->headers, 'to')); } + private function loadExcludeMailRecipientPHIDs() { + $addresses = array_merge( + $this->getToAddresses(), + $this->getCCAddresses() + ); + + $users = id(new PhabricatorUserEmail()) + ->loadAllWhere('address IN (%Ls)', $addresses); + $user_phids = mpull($users, 'getUserPHID'); + + $mailing_lists = id(new PhabricatorMetaMTAMailingList()) + ->loadAllWhere('email in (%Ls)', $addresses); + $mailing_list_phids = mpull($mailing_lists, 'getPHID'); + + return array_merge($user_phids, $mailing_list_phids); + } + /** * Parses "to" addresses, looking for a public create email address * first and if not found parsing the "to" address for reply handler * information: receiver name, user id, and hash. */ private function getPhabricatorToInformation() { // Only one "public" create address so far $create_task = PhabricatorEnv::getEnvConfig( 'metamta.maniphest.public-create-email'); // For replies, look for an object address with a format like: // D291+291+b0a41ca848d66dcc@example.com $single_handle_prefix = PhabricatorEnv::getEnvConfig( 'metamta.single-reply-handler-prefix'); $prefixPattern = ($single_handle_prefix) ? preg_quote($single_handle_prefix, '/') . '\+' : ''; $pattern = "/^{$prefixPattern}((?:D|T|C)\d+)\+([\w]+)\+([a-f0-9]{16})@/U"; $phabricator_address = null; $receiver_name = null; $user_id = null; $hash = null; foreach ($this->getToAddresses() as $address) { if ($address == $create_task) { $phabricator_address = $address; // it's okay to stop here because we just need to map a create // address to an application and don't need / won't have more // information in these cases. break; } $matches = null; $ok = preg_match( $pattern, $address, $matches); if ($ok) { $phabricator_address = $address; $receiver_name = $matches[1]; $user_id = $matches[2]; $hash = $matches[3]; break; } } return array( $phabricator_address, $receiver_name, $user_id, $hash ); } public function processReceivedMail() { // If Phabricator sent the mail, always drop it immediately. This prevents // loops where, e.g., the public bug address is also a user email address // and creating a bug sends them an email, which loops. $is_phabricator_mail = idx( $this->headers, 'x-phabricator-sent-this-message'); if ($is_phabricator_mail) { $message = "Ignoring email with 'X-Phabricator-Sent-This-Message' ". "header to avoid loops."; return $this->setMessage($message)->save(); } list($to, $receiver_name, $user_id, $hash) = $this->getPhabricatorToInformation(); if (!$to) { $raw_to = idx($this->headers, 'to'); return $this->setMessage("Unrecognized 'to' format: {$raw_to}")->save(); } $from = idx($this->headers, 'from'); // TODO -- make this a switch statement / better if / when we add more // public create email addresses! $create_task = PhabricatorEnv::getEnvConfig( 'metamta.maniphest.public-create-email'); if ($create_task && $to == $create_task) { $receiver = new ManiphestTask(); $user = $this->lookupPublicUser(); if ($user) { $this->setAuthorPHID($user->getPHID()); } else { $default_author = PhabricatorEnv::getEnvConfig( 'metamta.maniphest.default-public-author'); if ($default_author) { $user = id(new PhabricatorUser())->loadOneWhere( 'username = %s', $default_author); if ($user) { $receiver->setOriginalEmailSource($from); } else { throw new Exception( "Phabricator is misconfigured, the configuration key ". "'metamta.maniphest.default-public-author' is set to user ". "'{$default_author}' but that user does not exist."); } } else { // TODO: We should probably bounce these since from the user's // perspective their email vanishes into a black hole. return $this->setMessage("Invalid public user '{$from}'.")->save(); } } $receiver->setAuthorPHID($user->getPHID()); $receiver->setPriority(ManiphestTaskPriority::PRIORITY_TRIAGE); $editor = new ManiphestTransactionEditor(); + $editor->setActor($user); $handler = $editor->buildReplyHandler($receiver); $handler->setActor($user); + $handler->setExcludeMailRecipientPHIDs( + $this->loadExcludeMailRecipientPHIDs()); $handler->processEmail($this); $this->setRelatedPHID($receiver->getPHID()); $this->setMessage('OK'); return $this->save(); } if ($user_id == 'public') { if (!PhabricatorEnv::getEnvConfig('metamta.public-replies')) { return $this->setMessage("Public replies not enabled.")->save(); } $user = $this->lookupPublicUser(); if (!$user) { return $this->setMessage("Invalid public user '{$from}'.")->save(); } $use_user_hash = false; } else { $user = id(new PhabricatorUser())->load($user_id); if (!$user) { return $this->setMessage("Invalid private user '{$user_id}'.")->save(); } $use_user_hash = true; } if ($user->getIsDisabled()) { return $this->setMessage("User '{$user_id}' is disabled")->save(); } $this->setAuthorPHID($user->getPHID()); $receiver = self::loadReceiverObject($receiver_name); if (!$receiver) { return $this->setMessage("Invalid object '{$receiver_name}'")->save(); } $this->setRelatedPHID($receiver->getPHID()); if ($use_user_hash) { // This is a private reply-to address, check that the user hash is // correct. $check_phid = $user->getPHID(); } else { // This is a public reply-to address, check that the object hash is // correct. $check_phid = $receiver->getPHID(); } $expect_hash = self::computeMailHash($receiver->getMailKey(), $check_phid); // See note at computeOldMailHash(). $old_hash = self::computeOldMailHash($receiver->getMailKey(), $check_phid); if ($expect_hash != $hash && $old_hash != $hash) { return $this->setMessage("Invalid mail hash!")->save(); } if ($receiver instanceof ManiphestTask) { $editor = new ManiphestTransactionEditor(); + $editor->setActor($user); $handler = $editor->buildReplyHandler($receiver); } else if ($receiver instanceof DifferentialRevision) { $handler = DifferentialMail::newReplyHandlerForRevision($receiver); } else if ($receiver instanceof PhabricatorRepositoryCommit) { $handler = PhabricatorAuditCommentEditor::newReplyHandlerForCommit( $receiver); } $handler->setActor($user); + $handler->setExcludeMailRecipientPHIDs( + $this->loadExcludeMailRecipientPHIDs()); $handler->processEmail($this); $this->setMessage('OK'); return $this->save(); } public function getCleanTextBody() { $body = idx($this->bodies, 'text'); $parser = new PhabricatorMetaMTAEmailBodyParser(); return $parser->stripTextBody($body); } public function getRawTextBody() { return idx($this->bodies, 'text'); } public static function loadReceiverObject($receiver_name) { if (!$receiver_name) { return null; } $receiver_type = $receiver_name[0]; $receiver_id = substr($receiver_name, 1); $class_obj = null; switch ($receiver_type) { case 'T': $class_obj = new ManiphestTask(); break; case 'D': $class_obj = new DifferentialRevision(); break; case 'C': $class_obj = new PhabricatorRepositoryCommit(); break; default: return null; } return $class_obj->load($receiver_id); } public static function computeMailHash($mail_key, $phid) { $global_mail_key = PhabricatorEnv::getEnvConfig('phabricator.mail-key'); $hash = PhabricatorHash::digest($mail_key.$global_mail_key.$phid); return substr($hash, 0, 16); } public static function computeOldMailHash($mail_key, $phid) { // TODO: Remove this method entirely in a couple of months. We've moved from // plain sha1 to sha1+hmac to make the codebase more auditable for good uses // of hash functions, but still accept the old hashes on email replies to // avoid breaking things. Once we've been sending only hmac hashes for a // while, remove this and start rejecting old hashes. See T547. $global_mail_key = PhabricatorEnv::getEnvConfig('phabricator.mail-key'); $hash = sha1($mail_key.$global_mail_key.$phid); return substr($hash, 0, 16); } /** * Strip an email address down to the actual user@domain.tld part if * necessary, since sometimes it will have formatting like * '"Abraham Lincoln" '. */ private function getRawEmailAddress($address) { $matches = null; $ok = preg_match('/<(.*)>/', $address, $matches); if ($ok) { $address = $matches[1]; } return $address; } private function getRawEmailAddresses($addresses) { $raw_addresses = array(); foreach (explode(',', $addresses) as $address) { $raw_addresses[] = $this->getRawEmailAddress($address); } return $raw_addresses; } private function lookupPublicUser() { $from = idx($this->headers, 'from'); $from = $this->getRawEmailAddress($from); $user = PhabricatorUser::loadOneWithEmailAddress($from); // If Phabricator is configured to allow "Reply-To" authentication, try // the "Reply-To" address if we failed to match the "From" address. $config_key = 'metamta.insecure-auth-with-reply-to'; $allow_reply_to = PhabricatorEnv::getEnvConfig($config_key); if (!$user && $allow_reply_to) { $reply_to = idx($this->headers, 'reply-to'); $reply_to = $this->getRawEmailAddress($reply_to); if ($reply_to) { $user = PhabricatorUser::loadOneWithEmailAddress($reply_to); } } return $user; } } diff --git a/src/applications/people/PhabricatorUserEditor.php b/src/applications/people/PhabricatorUserEditor.php index 5e6fd70544..9f811e42c5 100644 --- a/src/applications/people/PhabricatorUserEditor.php +++ b/src/applications/people/PhabricatorUserEditor.php @@ -1,582 +1,558 @@ actor = $actor; - return $this; - } - - /* -( Creating and Editing Users )----------------------------------------- */ /** * @task edit */ public function createNewUser( PhabricatorUser $user, PhabricatorUserEmail $email) { if ($user->getID()) { throw new Exception("User has already been created!"); } if ($email->getID()) { throw new Exception("Email has already been created!"); } if (!PhabricatorUser::validateUsername($user->getUsername())) { $valid = PhabricatorUser::describeValidUsername(); throw new Exception("Username is invalid! {$valid}"); } // Always set a new user's email address to primary. $email->setIsPrimary(1); $this->willAddEmail($email); $user->openTransaction(); try { $user->save(); $email->setUserPHID($user->getPHID()); $email->save(); } catch (AphrontQueryDuplicateKeyException $ex) { // We might have written the user but failed to write the email; if // so, erase the IDs we attached. $user->setID(null); $user->setPHID(null); $user->killTransaction(); throw $ex; } $log = PhabricatorUserLog::newLog( - $this->actor, + $this->getActor(), $user, PhabricatorUserLog::ACTION_CREATE); $log->setNewValue($email->getAddress()); $log->save(); $user->saveTransaction(); return $this; } /** * @task edit */ public function updateUser( PhabricatorUser $user, PhabricatorUserEmail $email = null) { if (!$user->getID()) { throw new Exception("User has not been created yet!"); } $actor = $this->requireActor(); $user->openTransaction(); $user->save(); if ($email) { $email->save(); } $log = PhabricatorUserLog::newLog( $actor, $user, PhabricatorUserLog::ACTION_EDIT); $log->save(); $user->saveTransaction(); return $this; } /** * @task edit */ public function changePassword( PhabricatorUser $user, PhutilOpaqueEnvelope $envelope) { if (!$user->getID()) { throw new Exception("User has not been created yet!"); } $user->openTransaction(); $user->reload(); $user->setPassword($envelope); $user->save(); $log = PhabricatorUserLog::newLog( - $this->actor, + $this->getActor(), $user, PhabricatorUserLog::ACTION_CHANGE_PASSWORD); $log->save(); $user->saveTransaction(); } /** * @task edit */ public function changeUsername(PhabricatorUser $user, $username) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception("User has not been created yet!"); } if (!PhabricatorUser::validateUsername($username)) { $valid = PhabricatorUser::describeValidUsername(); throw new Exception("Username is invalid! {$valid}"); } $old_username = $user->getUsername(); $user->openTransaction(); $user->reload(); $user->setUsername($username); try { $user->save(); } catch (AphrontQueryDuplicateKeyException $ex) { $user->setUsername($old_username); $user->killTransaction(); throw $ex; } $log = PhabricatorUserLog::newLog( - $this->actor, + $actor, $user, PhabricatorUserLog::ACTION_CHANGE_USERNAME); $log->setOldValue($old_username); $log->setNewValue($username); $log->save(); $user->saveTransaction(); $user->sendUsernameChangeEmail($actor, $old_username); } /* -( Editing Roles )------------------------------------------------------ */ /** * @task role */ public function makeAdminUser(PhabricatorUser $user, $admin) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception("User has not been created yet!"); } $user->openTransaction(); $user->beginWriteLocking(); $user->reload(); if ($user->getIsAdmin() == $admin) { $user->endWriteLocking(); $user->killTransaction(); return $this; } $log = PhabricatorUserLog::newLog( $actor, $user, PhabricatorUserLog::ACTION_ADMIN); $log->setOldValue($user->getIsAdmin()); $log->setNewValue($admin); $user->setIsAdmin($admin); $user->save(); $log->save(); $user->endWriteLocking(); $user->saveTransaction(); return $this; } /** * @task role */ public function makeSystemAgentUser(PhabricatorUser $user, $system_agent) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception("User has not been created yet!"); } $user->openTransaction(); $user->beginWriteLocking(); $user->reload(); if ($user->getIsSystemAgent() == $system_agent) { $user->endWriteLocking(); $user->killTransaction(); return $this; } $log = PhabricatorUserLog::newLog( $actor, $user, PhabricatorUserLog::ACTION_SYSTEM_AGENT); $log->setOldValue($user->getIsSystemAgent()); $log->setNewValue($system_agent); $user->setIsSystemAgent($system_agent); $user->save(); $log->save(); $user->endWriteLocking(); $user->saveTransaction(); return $this; } /** * @task role */ public function disableUser(PhabricatorUser $user, $disable) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception("User has not been created yet!"); } $user->openTransaction(); $user->beginWriteLocking(); $user->reload(); if ($user->getIsDisabled() == $disable) { $user->endWriteLocking(); $user->killTransaction(); return $this; } $log = PhabricatorUserLog::newLog( $actor, $user, PhabricatorUserLog::ACTION_DISABLE); $log->setOldValue($user->getIsDisabled()); $log->setNewValue($disable); $user->setIsDisabled($disable); $user->save(); $log->save(); $user->endWriteLocking(); $user->saveTransaction(); return $this; } /** * @task role */ public function deleteUser(PhabricatorUser $user, $disable) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception("User has not been created yet!"); } if ($actor->getPHID() == $user->getPHID()) { throw new Exception("You can not delete yourself!"); } $user->openTransaction(); $ldaps = id(new PhabricatorUserLDAPInfo())->loadAllWhere( 'userID = %d', $user->getID()); foreach ($ldaps as $ldap) { $ldap->delete(); } $oauths = id(new PhabricatorUserOAuthInfo())->loadAllWhere( 'userID = %d', $user->getID()); foreach ($oauths as $oauth) { $oauth->delete(); } $prefs = id(new PhabricatorUserPreferences())->loadAllWhere( 'userPHID = %s', $user->getPHID()); foreach ($prefs as $pref) { $pref->delete(); } $profiles = id(new PhabricatorUserProfile())->loadAllWhere( 'userPHID = %s', $user->getPHID()); foreach ($profiles as $profile) { $profile->delete(); } $keys = id(new PhabricatorUserSSHKey())->loadAllWhere( 'userPHID = %s', $user->getPHID()); foreach ($keys as $key) { $key->delete(); } $emails = id(new PhabricatorUserEmail())->loadAllWhere( 'userPHID = %s', $user->getPHID()); foreach ($emails as $email) { $email->delete(); } $log = PhabricatorUserLog::newLog( $actor, $user, PhabricatorUserLog::ACTION_DELETE); $log->save(); $user->delete(); $user->saveTransaction(); return $this; } /* -( Adding, Removing and Changing Email )-------------------------------- */ /** * @task email */ public function addEmail( PhabricatorUser $user, PhabricatorUserEmail $email) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception("User has not been created yet!"); } if ($email->getID()) { throw new Exception("Email has already been created!"); } // Use changePrimaryEmail() to change primary email. $email->setIsPrimary(0); $email->setUserPHID($user->getPHID()); $this->willAddEmail($email); $user->openTransaction(); $user->beginWriteLocking(); $user->reload(); try { $email->save(); } catch (AphrontQueryDuplicateKeyException $ex) { $user->endWriteLocking(); $user->killTransaction(); throw $ex; } $log = PhabricatorUserLog::newLog( - $this->actor, + $actor, $user, PhabricatorUserLog::ACTION_EMAIL_ADD); $log->setNewValue($email->getAddress()); $log->save(); $user->endWriteLocking(); $user->saveTransaction(); return $this; } /** * @task email */ public function removeEmail( PhabricatorUser $user, PhabricatorUserEmail $email) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception("User has not been created yet!"); } if (!$email->getID()) { throw new Exception("Email has not been created yet!"); } $user->openTransaction(); $user->beginWriteLocking(); $user->reload(); $email->reload(); if ($email->getIsPrimary()) { throw new Exception("Can't remove primary email!"); } if ($email->getUserPHID() != $user->getPHID()) { throw new Exception("Email not owned by user!"); } $email->delete(); $log = PhabricatorUserLog::newLog( - $this->actor, + $actor, $user, PhabricatorUserLog::ACTION_EMAIL_REMOVE); $log->setOldValue($email->getAddress()); $log->save(); $user->endWriteLocking(); $user->saveTransaction(); return $this; } /** * @task email */ public function changePrimaryEmail( PhabricatorUser $user, PhabricatorUserEmail $email) { $actor = $this->requireActor(); if (!$user->getID()) { throw new Exception("User has not been created yet!"); } if (!$email->getID()) { throw new Exception("Email has not been created yet!"); } $user->openTransaction(); $user->beginWriteLocking(); $user->reload(); $email->reload(); if ($email->getUserPHID() != $user->getPHID()) { throw new Exception("User does not own email!"); } if ($email->getIsPrimary()) { throw new Exception("Email is already primary!"); } if (!$email->getIsVerified()) { throw new Exception("Email is not verified!"); } $old_primary = $user->loadPrimaryEmail(); if ($old_primary) { $old_primary->setIsPrimary(0); $old_primary->save(); } $email->setIsPrimary(1); $email->save(); $log = PhabricatorUserLog::newLog( $actor, $user, PhabricatorUserLog::ACTION_EMAIL_PRIMARY); $log->setOldValue($old_primary ? $old_primary->getAddress() : null); $log->setNewValue($email->getAddress()); $log->save(); $user->endWriteLocking(); $user->saveTransaction(); if ($old_primary) { $old_primary->sendOldPrimaryEmail($user, $email); } $email->sendNewPrimaryEmail($user); return $this; } /* -( Internals )---------------------------------------------------------- */ - /** - * @task internal - */ - private function requireActor() { - if (!$this->actor) { - throw new Exception("User edit requires actor!"); - } - return $this->actor; - } - - /** * @task internal */ private function willAddEmail(PhabricatorUserEmail $email) { // Hard check before write to prevent creation of disallowed email // addresses. Normally, the application does checks and raises more // user friendly errors for us, but we omit the courtesy checks on some // pathways like administrative scripts for simplicity. if (!PhabricatorUserEmail::isAllowedAddress($email->getAddress())) { throw new Exception(PhabricatorUserEmail::describeAllowedAddresses()); } } } diff --git a/src/applications/phame/controller/blog/PhameBlogDeleteController.php b/src/applications/phame/controller/blog/PhameBlogDeleteController.php index 0eb2ef6512..2743cc4d06 100644 --- a/src/applications/phame/controller/blog/PhameBlogDeleteController.php +++ b/src/applications/phame/controller/blog/PhameBlogDeleteController.php @@ -1,115 +1,115 @@ phid = $phid; return $this; } private function getBlogPHID() { return $this->phid; } protected function getSideNavFilter() { return 'blog/delete/'.$this->getBlogPHID(); } protected function getSideNavExtraBlogFilters() { $filters = array( array('key' => $this->getSideNavFilter(), 'name' => 'Delete Blog') ); return $filters; } public function willProcessRequest(array $data) { $phid = $data['phid']; $this->setBlogPHID($phid); } public function processRequest() { $blogger_edge_type = PhabricatorEdgeConfig::TYPE_BLOG_HAS_BLOGGER; $post_edge_type = PhabricatorEdgeConfig::TYPE_BLOG_HAS_POST; $request = $this->getRequest(); $user = $request->getUser(); $blog_phid = $this->getBlogPHID(); $blogs = id(new PhameBlogQuery()) ->withPHIDs(array($blog_phid)) ->execute(); $blog = reset($blogs); if (empty($blog)) { return new Aphront404Response(); } $phids = array($blog_phid); $edge_types = array( PhabricatorEdgeConfig::TYPE_BLOG_HAS_BLOGGER, PhabricatorEdgeConfig::TYPE_BLOG_HAS_POST, ); $edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($phids) ->withEdgeTypes($edge_types) ->execute(); $blogger_edges = $edges[$blog_phid][$blogger_edge_type]; // TODO -- make this check use a policy if (!isset($blogger_edges[$user->getPHID()]) && !$user->isAdmin()) { return new Aphront403Response(); } $edit_uri = $blog->getEditURI(); if ($request->isFormPost()) { $blogger_phids = array_keys($blogger_edges); $post_edges = $edges[$blog_phid][$post_edge_type]; $post_phids = array_keys($post_edges); - $editor = id(new PhabricatorEdgeEditor()); - $editor->setUser($user); + $editor = id(new PhabricatorEdgeEditor()) + ->setActor($user); foreach ($blogger_phids as $phid) { $editor->removeEdge($blog_phid, $blogger_edge_type, $phid); } foreach ($post_phids as $phid) { $editor->removeEdge($blog_phid, $post_edge_type, $phid); } $editor->save(); $blog->delete(); return id(new AphrontRedirectResponse()) ->setURI('/phame/blog/?deleted'); } $dialog = id(new AphrontDialogView()) ->setUser($user) ->setTitle('Delete blog?') ->appendChild('Really delete this blog? It will be gone forever.') ->addSubmitButton('Delete') ->addCancelButton($edit_uri); return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/phame/controller/blog/PhameBlogEditController.php b/src/applications/phame/controller/blog/PhameBlogEditController.php index 7aa6ba4328..9d55abc8be 100644 --- a/src/applications/phame/controller/blog/PhameBlogEditController.php +++ b/src/applications/phame/controller/blog/PhameBlogEditController.php @@ -1,260 +1,260 @@ phid = $phid; return $this; } private function getBlogPHID() { return $this->phid; } private function setIsBlogEdit($is_blog_edit) { $this->isBlogEdit = $is_blog_edit; return $this; } private function isBlogEdit() { return $this->isBlogEdit; } protected function getSideNavFilter() { if ($this->isBlogEdit()) { $filter = 'blog/edit/'.$this->getBlogPHID(); } else { $filter = 'blog/new'; } return $filter; } protected function getSideNavBlogFilters() { $filters = parent::getSideNavBlogFilters(); if ($this->isBlogEdit()) { $filter = array('key' => 'blog/edit/'.$this->getBlogPHID(), 'name' => 'Edit Blog'); $filters[] = $filter; } else { $filter = array('key' => 'blog/new', 'name' => 'New Blog'); array_unshift($filters, $filter); } return $filters; } public function willProcessRequest(array $data) { $phid = idx($data, 'phid'); $this->setBlogPHID($phid); $this->setIsBlogEdit((bool)$phid); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $e_name = null; $e_bloggers = null; $e_custom_domain = null; $errors = array(); if ($this->isBlogEdit()) { $blogs = id(new PhameBlogQuery()) ->withPHIDs(array($this->getBlogPHID())) ->execute(); $blog = reset($blogs); if (empty($blog)) { return new Aphront404Response(); } $bloggers = $blog->loadBloggers()->getBloggers(); // TODO -- make this check use a policy if (!isset($bloggers[$user->getPHID()]) && !$user->isAdmin()) { return new Aphront403Response(); } $blogger_tokens = mpull($bloggers, 'getFullName', 'getPHID'); $submit_button = 'Save Changes'; $delete_button = javelin_render_tag( 'a', array( 'href' => $blog->getDeleteURI(), 'class' => 'grey button', 'sigil' => 'workflow', ), 'Delete Blog'); $page_title = 'Edit Blog'; } else { $blog = id(new PhameBlog()) ->setCreatorPHID($user->getPHID()); $blogger_tokens = array($user->getPHID() => $user->getFullName()); $submit_button = 'Create Blog'; $delete_button = null; $page_title = 'Create Blog'; } if ($request->isFormPost()) { $saved = true; $name = $request->getStr('name'); $description = $request->getStr('description'); $blogger_arr = $request->getArr('bloggers'); $custom_domain = $request->getStr('custom_domain'); if (empty($blogger_arr)) { $error = 'Bloggers must be nonempty.'; if ($this->isBlogEdit()) { $error .= ' To delete the blog, use the delete button.'; } else { $error .= ' A blog cannot exist without bloggers.'; } $e_bloggers = 'Required'; $errors[] = $error; } $new_bloggers = array_values($blogger_arr); if ($this->isBlogEdit()) { $old_bloggers = array_keys($blogger_tokens); } else { $old_bloggers = array(); } if (empty($name)) { $errors[] = 'Name must be nonempty.'; $e_name = 'Required'; } $blog->setName($name); $blog->setDescription($description); if (!empty($custom_domain)) { $error = $blog->validateCustomDomain($custom_domain); if ($error) { $errors[] = $error; $e_custom_domain = 'Invalid'; } $blog->setDomain($custom_domain); } if (empty($errors)) { $blog->save(); $add_phids = $new_bloggers; $rem_phids = array_diff($old_bloggers, $new_bloggers); $editor = new PhabricatorEdgeEditor(); $edge_type = PhabricatorEdgeConfig::TYPE_BLOG_HAS_BLOGGER; - $editor->setUser($user); + $editor->setActor($user); foreach ($add_phids as $phid) { $editor->addEdge($blog->getPHID(), $edge_type, $phid); } foreach ($rem_phids as $phid) { $editor->removeEdge($blog->getPHID(), $edge_type, $phid); } $editor->save(); } else { $saved = false; } if ($saved) { $uri = new PhutilURI($blog->getViewURI()); if ($this->isBlogEdit()) { $uri->setQueryParam('edit', true); } else { $uri->setQueryParam('new', true); } return id(new AphrontRedirectResponse()) ->setURI($uri); } } $panel = new AphrontPanelView(); $panel->setHeader($page_title); $panel->setWidth(AphrontPanelView::WIDTH_FULL); if ($delete_button) { $panel->addButton($delete_button); } $form = id(new AphrontFormView()) ->setUser($user) ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Name') ->setName('name') ->setValue($blog->getName()) ->setID('blog-name') ->setError($e_name) ) ->appendChild( id(new PhabricatorRemarkupControl()) ->setLabel('Description') ->setName('description') ->setValue($blog->getDescription()) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setID('blog-description') ) ->appendChild( id(new AphrontFormTokenizerControl()) ->setLabel('Bloggers') ->setName('bloggers') ->setValue($blogger_tokens) ->setUser($user) ->setDatasource('/typeahead/common/users/') ->setError($e_bloggers) ) ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Custom Domain') ->setName('custom_domain') ->setValue($blog->getDomain()) ->setCaption('Must include at least one dot (.), e.g. '. 'blog.example.com') ->setError($e_custom_domain) ) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton('/phame/blog/') ->setValue($submit_button) ); $panel->appendChild($form); if ($errors) { $error_view = id(new AphrontErrorView()) ->setTitle('Errors saving blog.') ->setErrors($errors); } else { $error_view = null; } $this->setShowSideNav(true); return $this->buildStandardPageResponse( array( $error_view, $panel, ), array( 'title' => $page_title, )); } } diff --git a/src/applications/phame/controller/post/PhamePostDeleteController.php b/src/applications/phame/controller/post/PhamePostDeleteController.php index ed94af410c..3e0db52cbf 100644 --- a/src/applications/phame/controller/post/PhamePostDeleteController.php +++ b/src/applications/phame/controller/post/PhamePostDeleteController.php @@ -1,88 +1,88 @@ phid = $phid; return $this; } private function getPostPHID() { return $this->phid; } public function willProcessRequest(array $data) { $phid = $data['phid']; $this->setPostPHID($phid); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $post_phid = $this->getPostPHID(); $posts = id(new PhamePostQuery()) ->withPHIDs(array($post_phid)) ->execute(); $post = reset($posts); if (empty($post)) { return new Aphront404Response(); } if ($post->getBloggerPHID() != $user->getPHID()) { return new Aphront403Response(); } $post_noun = $post->getHumanName(); if ($request->isFormPost()) { $edge_type = PhabricatorEdgeConfig::TYPE_POST_HAS_BLOG; $edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs(array($post_phid)) ->withEdgeTypes(array($edge_type)) ->execute(); $blog_edges = $edges[$post_phid][$edge_type]; $blog_phids = array_keys($blog_edges); - $editor = id(new PhabricatorEdgeEditor()); - $editor->setUser($user); + $editor = id(new PhabricatorEdgeEditor()) + ->setActor($user); foreach ($blog_phids as $phid) { $editor->removeEdge($post_phid, $edge_type, $phid); } $editor->save(); $post->delete(); return id(new AphrontRedirectResponse()) ->setURI('/phame/'.$post_noun.'/?deleted'); } $edit_uri = $post->getEditURI(); $dialog = id(new AphrontDialogView()) ->setUser($user) ->setTitle('Delete '.$post_noun.'?') ->appendChild('Really delete this '.$post_noun.'? '. 'It will be gone forever.') ->addSubmitButton('Delete') ->addCancelButton($edit_uri); return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/phame/controller/post/PhamePostEditController.php b/src/applications/phame/controller/post/PhamePostEditController.php index 1f18b1a74d..f094c5779d 100644 --- a/src/applications/phame/controller/post/PhamePostEditController.php +++ b/src/applications/phame/controller/post/PhamePostEditController.php @@ -1,423 +1,423 @@ post = $post; return $this; } private function getPost() { return $this->post; } private function setPostPHID($phid) { $this->phid = $phid; return $this; } private function getPostPHID() { return $this->phid; } private function setIsPostEdit($is_post_edit) { $this->isPostEdit = $is_post_edit; return $this; } private function isPostEdit() { return $this->isPostEdit; } private function setUserBlogs(array $blogs) { assert_instances_of($blogs, 'PhameBlog'); $this->userBlogs = $blogs; return $this; } private function getUserBlogs() { return $this->userBlogs; } private function setPostBlogs(array $blogs) { assert_instances_of($blogs, 'PhameBlog'); $this->postBlogs = $blogs; return $this; } private function getPostBlogs() { return $this->postBlogs; } protected function getSideNavFilter() { if ($this->isPostEdit()) { $post_noun = $this->getPost()->getHumanName(); $filter = $post_noun.'/edit/'.$this->getPostPHID(); } else { $filter = 'post/new'; } return $filter; } protected function getSideNavExtraPostFilters() { if ($this->isPostEdit() && !$this->getPost()->isDraft()) { $filters = array( array('key' => 'post/edit/'.$this->getPostPHID(), 'name' => 'Edit Post') ); } else { $filters = array(); } return $filters; } protected function getSideNavExtraDraftFilters() { if ($this->isPostEdit() && $this->getPost()->isDraft()) { $filters = array( array('key' => 'draft/edit/'.$this->getPostPHID(), 'name' => 'Edit Draft') ); } else { $filters = array(); } return $filters; } public function willProcessRequest(array $data) { $phid = idx($data, 'phid'); $this->setPostPHID($phid); $this->setIsPostEdit((bool) $phid); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $e_phame_title = null; $e_title = null; $errors = array(); if ($this->isPostEdit()) { $posts = id(new PhamePostQuery()) ->withPHIDs(array($this->getPostPHID())) ->execute(); $post = reset($posts); if (empty($post)) { return new Aphront404Response(); } if ($post->getBloggerPHID() != $user->getPHID()) { return new Aphront403Response(); } $post_noun = ucfirst($post->getHumanName()); $cancel_uri = $post->getViewURI($user->getUsername()); $submit_button = 'Save Changes'; $delete_button = javelin_render_tag( 'a', array( 'href' => $post->getDeleteURI(), 'class' => 'grey button', 'sigil' => 'workflow', ), 'Delete '.$post_noun); $page_title = 'Edit '.$post_noun; } else { $post = id(new PhamePost()) ->setBloggerPHID($user->getPHID()) ->setVisibility(PhamePost::VISIBILITY_DRAFT); $cancel_uri = '/phame/draft/'; $submit_button = 'Create Draft'; $delete_button = null; $page_title = 'Create Draft'; } $this->setPost($post); $this->loadEdgesAndBlogs(); if ($request->isFormPost()) { $saved = true; $visibility = $request->getInt('visibility'); $comments = $request->getStr('comments_widget'); $data = array('comments_widget' => $comments); $phame_title = $request->getStr('phame_title'); $phame_title = PhabricatorSlug::normalize($phame_title); $title = $request->getStr('title'); $post->setTitle($title); $post->setPhameTitle($phame_title); $post->setBody($request->getStr('body')); $post->setVisibility($visibility); $post->setConfigData($data); // only publish once...! if ($visibility == PhamePost::VISIBILITY_PUBLISHED) { if (!$post->getDatePublished()) { $post->setDatePublished(time()); } // this is basically a cast of null to 0 if its a new post } else if (!$post->getDatePublished()) { $post->setDatePublished(0); } if ($phame_title == '/') { $errors[] = 'Phame title must be nonempty.'; $e_phame_title = 'Required'; } if (empty($title)) { $errors[] = 'Title must be nonempty.'; $e_title = 'Required'; } $blogs_published = array_keys($this->getPostBlogs()); $blogs_to_publish = array(); $blogs_to_depublish = array(); if ($visibility == PhamePost::VISIBILITY_PUBLISHED) { $blogs_arr = $request->getArr('blogs'); $blogs_to_publish = array_values($blogs_arr); $blogs_to_depublish = array_diff($blogs_published, $blogs_to_publish); } else { $blogs_to_depublish = $blogs_published; } if (empty($errors)) { try { $post->save(); $editor = new PhabricatorEdgeEditor(); $edge_type = PhabricatorEdgeConfig::TYPE_POST_HAS_BLOG; - $editor->setUser($user); + $editor->setActor($user); foreach ($blogs_to_publish as $phid) { $editor->addEdge($post->getPHID(), $edge_type, $phid); } foreach ($blogs_to_depublish as $phid) { $editor->removeEdge($post->getPHID(), $edge_type, $phid); } $editor->save(); } catch (AphrontQueryDuplicateKeyException $e) { $saved = false; $e_phame_title = 'Not Unique'; $errors[] = 'Another post already uses this slug. '. 'Each post must have a unique slug.'; } } else { $saved = false; } if ($saved) { $uri = new PhutilURI($post->getViewURI($user->getUsername())); $uri->setQueryParam('saved', true); return id(new AphrontRedirectResponse()) ->setURI($uri); } } $panel = new AphrontPanelView(); $panel->setHeader($page_title); $panel->setWidth(AphrontPanelView::WIDTH_FULL); if ($delete_button) { $panel->addButton($delete_button); } $form = id(new AphrontFormView()) ->setUser($user) ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Title') ->setName('title') ->setValue($post->getTitle()) ->setID('post-title') ->setError($e_title) ) ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Phame Title') ->setName('phame_title') ->setValue(rtrim($post->getPhameTitle(), '/')) ->setID('post-phame-title') ->setCaption('Up to 64 alphanumeric characters '. 'with underscores for spaces. '. 'Formatting is enforced.') ->setError($e_phame_title) ) ->appendChild( id(new PhabricatorRemarkupControl()) ->setLabel('Body') ->setName('body') ->setValue($post->getBody()) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setID('post-body') ) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel('Visibility') ->setName('visibility') ->setValue($post->getVisibility()) ->setOptions(PhamePost::getVisibilityOptionsForSelect()) ->setID('post-visibility') ) ->appendChild( $this->getBlogCheckboxControl($post) ) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel('Comments Widget') ->setName('comments_widget') ->setvalue($post->getCommentsWidget()) ->setOptions($post->getCommentsWidgetOptionsForSelect()) ) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) ->setValue($submit_button) ); $panel->appendChild($form); $preview_panel = '
Post Preview
Loading preview...
'; Javelin::initBehavior( 'phame-post-preview', array( 'preview' => 'post-preview', 'body' => 'post-body', 'title' => 'post-title', 'phame_title' => 'post-phame-title', 'uri' => '/phame/post/preview/', )); $visibility_data = array( 'select_id' => 'post-visibility', 'current' => $post->getVisibility(), 'published' => PhamePost::VISIBILITY_PUBLISHED, 'draft' => PhamePost::VISIBILITY_DRAFT, 'change_uri' => $post->getChangeVisibilityURI(), ); $blogs_data = array( 'checkbox_id' => 'post-blogs', 'have_published' => (bool) count($this->getPostBlogs()) ); Javelin::initBehavior( 'phame-post-blogs', array( 'blogs' => $blogs_data, 'visibility' => $visibility_data, )); if ($errors) { $error_view = id(new AphrontErrorView()) ->setTitle('Errors saving post.') ->setErrors($errors); } else { $error_view = null; } $this->setShowSideNav(true); return $this->buildStandardPageResponse( array( $error_view, $panel, $preview_panel, ), array( 'title' => $page_title, )); } private function getBlogCheckboxControl(PhamePost $post) { if ($post->getVisibility() == PhamePost::VISIBILITY_PUBLISHED) { $control_style = null; } else { $control_style = 'display: none'; } $control = id(new AphrontFormCheckboxControl()) ->setLabel('Blogs') ->setControlID('post-blogs') ->setControlStyle($control_style); $post_blogs = $this->getPostBlogs(); $user_blogs = $this->getUserBlogs(); $all_blogs = $post_blogs + $user_blogs; $all_blogs = msort($all_blogs, 'getName'); foreach ($all_blogs as $phid => $blog) { $control->addCheckbox( 'blogs[]', $blog->getPHID(), $blog->getName(), isset($post_blogs[$phid]) ); } return $control; } private function loadEdgesAndBlogs() { $edge_types = array(PhabricatorEdgeConfig::TYPE_BLOGGER_HAS_BLOG); $blogger_phid = $this->getRequest()->getUser()->getPHID(); $phids = array($blogger_phid); if ($this->isPostEdit()) { $edge_types[] = PhabricatorEdgeConfig::TYPE_POST_HAS_BLOG; $phids[] = $this->getPostPHID(); } $edges = id(new PhabricatorEdgeQuery()) ->withSourcePHIDs($phids) ->withEdgeTypes($edge_types) ->execute(); $all_blogs_assoc = array(); foreach ($phids as $phid) { foreach ($edge_types as $type) { $all_blogs_assoc += $edges[$phid][$type]; } } $blogs = id(new PhameBlogQuery()) ->withPHIDs(array_keys($all_blogs_assoc)) ->execute(); $blogs = mpull($blogs, null, 'getPHID'); $user_blogs = array_intersect_key( $blogs, $edges[$blogger_phid][PhabricatorEdgeConfig::TYPE_BLOGGER_HAS_BLOG] ); if ($this->isPostEdit()) { $post_blogs = array_intersect_key( $blogs, $edges[$this->getPostPHID()][PhabricatorEdgeConfig::TYPE_POST_HAS_BLOG] ); } else { $post_blogs = array(); } $this->setUserBlogs($user_blogs); $this->setPostBlogs($post_blogs); } } diff --git a/src/applications/phriction/controller/PhrictionDeleteController.php b/src/applications/phriction/controller/PhrictionDeleteController.php index ada62b50a5..7f2fdd1c75 100644 --- a/src/applications/phriction/controller/PhrictionDeleteController.php +++ b/src/applications/phriction/controller/PhrictionDeleteController.php @@ -1,61 +1,61 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $document = id(new PhrictionDocument())->load($this->id); if (!$document) { return new Aphront404Response(); } $document_uri = PhrictionDocument::getSlugURI($document->getSlug()); if ($request->isFormPost()) { $editor = id(PhrictionDocumentEditor::newForSlug($document->getSlug())) - ->setUser($user) + ->setActor($user) ->delete(); return id(new AphrontRedirectResponse())->setURI($document_uri); } $dialog = id(new AphrontDialogView()) ->setUser($user) ->setTitle('Delete document?') ->appendChild( 'Really delete this document? You can recover it later by reverting '. 'to a previous version.') ->addSubmitButton('Delete') ->addCancelButton($document_uri); return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/phriction/controller/PhrictionEditController.php b/src/applications/phriction/controller/PhrictionEditController.php index adfcbbd8f8..79b9188681 100644 --- a/src/applications/phriction/controller/PhrictionEditController.php +++ b/src/applications/phriction/controller/PhrictionEditController.php @@ -1,279 +1,279 @@ id = idx($data, 'id'); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); if ($this->id) { $document = id(new PhrictionDocument())->load($this->id); if (!$document) { return new Aphront404Response(); } $revert = $request->getInt('revert'); if ($revert) { $content = id(new PhrictionContent())->loadOneWhere( 'documentID = %d AND version = %d', $document->getID(), $revert); if (!$content) { return new Aphront404Response(); } } else { $content = id(new PhrictionContent())->load($document->getContentID()); } } else { $slug = $request->getStr('slug'); $slug = PhabricatorSlug::normalize($slug); if (!$slug) { return new Aphront404Response(); } $document = id(new PhrictionDocument())->loadOneWhere( 'slug = %s', $slug); if ($document) { $content = id(new PhrictionContent())->load($document->getContentID()); } else { if (PhrictionDocument::isProjectSlug($slug)) { $project = id(new PhabricatorProject())->loadOneWhere( 'phrictionSlug = %s', PhrictionDocument::getProjectSlugIdentifier($slug)); if (!$project) { return new Aphront404Response(); } } $document = new PhrictionDocument(); $document->setSlug($slug); $content = new PhrictionContent(); $content->setSlug($slug); $default_title = PhabricatorSlug::getDefaultTitle($slug); $content->setTitle($default_title); } } if ($request->getBool('nodraft')) { $draft = null; $draft_key = null; } else { if ($document->getPHID()) { $draft_key = $document->getPHID().':'.$content->getVersion(); } else { $draft_key = 'phriction:'.$content->getSlug(); } $draft = id(new PhabricatorDraft())->loadOneWhere( 'authorPHID = %s AND draftKey = %s', $user->getPHID(), $draft_key); } require_celerity_resource('phriction-document-css'); $e_title = true; $notes = null; $errors = array(); if ($request->isFormPost()) { $title = $request->getStr('title'); $notes = $request->getStr('description'); if (!strlen($title)) { $e_title = 'Required'; $errors[] = 'Document title is required.'; } else { $e_title = null; } if ($document->getID()) { if ($content->getTitle() == $title && $content->getContent() == $request->getStr('content')) { $dialog = new AphrontDialogView(); $dialog->setUser($user); $dialog->setTitle('No Edits'); $dialog->appendChild( '

You did not make any changes to the document.

'); $dialog->addCancelButton($request->getRequestURI()); return id(new AphrontDialogResponse())->setDialog($dialog); } } if (!count($errors)) { $editor = id(PhrictionDocumentEditor::newForSlug($document->getSlug())) - ->setUser($user) + ->setActor($user) ->setTitle($title) ->setContent($request->getStr('content')) ->setDescription($notes); $editor->save(); if ($draft) { $draft->delete(); } $uri = PhrictionDocument::getSlugURI($document->getSlug()); return id(new AphrontRedirectResponse())->setURI($uri); } } $error_view = null; if ($errors) { $error_view = id(new AphrontErrorView()) ->setTitle('Form Errors') ->setErrors($errors); } if ($document->getID()) { $panel_header = 'Edit Phriction Document'; $submit_button = 'Save Changes'; $delete_button = phutil_render_tag( 'a', array( 'href' => '/phriction/delete/'.$document->getID().'/', 'class' => 'grey button', ), 'Delete Document'); } else { $panel_header = 'Create New Phriction Document'; $submit_button = 'Create Document'; $delete_button = null; } $uri = $document->getSlug(); $uri = PhrictionDocument::getSlugURI($uri); $uri = PhabricatorEnv::getProductionURI($uri); $cancel_uri = PhrictionDocument::getSlugURI($document->getSlug()); if ($draft && strlen($draft->getDraft()) && ($draft->getDraft() != $content->getContent())) { $content_text = $draft->getDraft(); $discard = phutil_render_tag( 'a', array( 'href' => $request->getRequestURI()->alter('nodraft', true), ), 'discard this draft'); $draft_note = new AphrontErrorView(); $draft_note->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $draft_note->setTitle('Recovered Draft'); $draft_note->appendChild( '

Showing a saved draft of your edits, you can '.$discard.'.

'); } else { $content_text = $content->getContent(); $draft_note = null; } $form = id(new AphrontFormView()) ->setUser($user) ->setWorkflow(true) ->setAction($request->getRequestURI()->getPath()) ->addHiddenInput('slug', $document->getSlug()) ->addHiddenInput('nodraft', $request->getBool('nodraft')) ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Title') ->setValue($content->getTitle()) ->setError($e_title) ->setName('title')) ->appendChild( id(new AphrontFormStaticControl()) ->setLabel('URI') ->setValue($uri)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setLabel('Content') ->setValue($content_text) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_TALL) ->setName('content') ->setID('document-textarea')) ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Edit Notes') ->setValue($notes) ->setError(null) ->setName('description')) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($cancel_uri) ->setValue($submit_button)); $panel = id(new AphrontPanelView()) ->setWidth(AphrontPanelView::WIDTH_WIDE) ->setHeader($panel_header) ->appendChild($form); if ($delete_button) { $panel->addButton($delete_button); } $preview_panel = '
Document Preview
Loading preview...
'; Javelin::initBehavior( 'phriction-document-preview', array( 'preview' => 'document-preview', 'textarea' => 'document-textarea', 'uri' => '/phriction/preview/?draftkey='.$draft_key, )); return $this->buildStandardPageResponse( array( $draft_note, $error_view, $panel, $preview_panel, ), array( 'title' => 'Edit Document', )); } } diff --git a/src/applications/phriction/editor/PhrictionDocumentEditor.php b/src/applications/phriction/editor/PhrictionDocumentEditor.php index 7e0903efa0..adb9f52ac8 100644 --- a/src/applications/phriction/editor/PhrictionDocumentEditor.php +++ b/src/applications/phriction/editor/PhrictionDocumentEditor.php @@ -1,241 +1,230 @@ } public static function newForSlug($slug) { $slug = PhabricatorSlug::normalize($slug); $document = id(new PhrictionDocument())->loadOneWhere( 'slug = %s', $slug); $content = null; if ($document) { $content = id(new PhrictionContent())->load($document->getContentID()); } else { $document = new PhrictionDocument(); $document->setSlug($slug); } if (!$content) { $default_title = PhabricatorSlug::getDefaultTitle($slug); $content = new PhrictionContent(); $content->setSlug($slug); $content->setTitle($default_title); $content->setContent(''); } $obj = new PhrictionDocumentEditor(); $obj->document = $document; $obj->content = $content; return $obj; } - public function setUser(PhabricatorUser $user) { - $this->user = $user; - return $this; - } - public function setTitle($title) { $this->newTitle = $title; return $this; } public function setContent($content) { $this->newContent = $content; return $this; } public function setDescription($description) { $this->description = $description; return $this; } public function getDocument() { return $this->document; } public function delete() { - if (!$this->user) { - throw new Exception("Call setUser() before deleting a document!"); - } + $actor = $this->requireActor(); // TODO: Should we do anything about deleting an already-deleted document? // We currently allow it. $document = $this->document; $content = $this->content; $new_content = $this->buildContentTemplate($document, $content); $new_content->setChangeType(PhrictionChangeType::CHANGE_DELETE); $new_content->setContent(''); return $this->updateDocument($document, $content, $new_content); } public function save() { - if (!$this->user) { - throw new Exception("Call setUser() before updating a document!"); - } + $actor = $this->requireActor(); if ($this->newContent === '') { // If this is an edit which deletes all the content, just treat it as // a delete. NOTE: null means "don't change the content", not "delete // the page"! Thus the strict type check. return $this->delete(); } $document = $this->document; $content = $this->content; $new_content = $this->buildContentTemplate($document, $content); return $this->updateDocument($document, $content, $new_content); } private function buildContentTemplate( PhrictionDocument $document, PhrictionContent $content) { $new_content = new PhrictionContent(); $new_content->setSlug($document->getSlug()); - $new_content->setAuthorPHID($this->user->getPHID()); + $new_content->setAuthorPHID($this->getActor()->getPHID()); $new_content->setChangeType(PhrictionChangeType::CHANGE_EDIT); $new_content->setTitle( coalesce( $this->newTitle, $content->getTitle())); $new_content->setContent( coalesce( $this->newContent, $content->getContent())); if (strlen($this->description)) { $new_content->setDescription($this->description); } return $new_content; } private function updateDocument($document, $content, $new_content) { $is_new = false; if (!$document->getID()) { $is_new = true; } $new_content->setVersion($content->getVersion() + 1); $change_type = $new_content->getChangeType(); switch ($change_type) { case PhrictionChangeType::CHANGE_EDIT: $doc_status = PhrictionDocumentStatus::STATUS_EXISTS; $feed_action = $is_new ? PhrictionActionConstants::ACTION_CREATE : PhrictionActionConstants::ACTION_EDIT; break; case PhrictionChangeType::CHANGE_DELETE: $doc_status = PhrictionDocumentStatus::STATUS_DELETED; $feed_action = PhrictionActionConstants::ACTION_DELETE; if ($is_new) { throw new Exception( "You can not delete a document which doesn't exist yet!"); } break; default: throw new Exception( "Unsupported content change type '{$change_type}'!"); } $document->setStatus($doc_status); // TODO: This should be transactional. if ($is_new) { $document->save(); } $new_content->setDocumentID($document->getID()); $new_content->save(); $document->setContentID($new_content->getID()); $document->save(); $document->attachContent($new_content); PhabricatorSearchPhrictionIndexer::indexDocument($document); $project_phid = null; $slug = $document->getSlug(); if (PhrictionDocument::isProjectSlug($slug)) { $project = id(new PhabricatorProject())->loadOneWhere( 'phrictionSlug = %s', PhrictionDocument::getProjectSlugIdentifier($slug)); if ($project) { $project_phid = $project->getPHID(); } } $related_phids = array( $document->getPHID(), - $this->user->getPHID(), + $this->getActor()->getPHID(), ); if ($project_phid) { $related_phids[] = $project_phid; } id(new PhabricatorFeedStoryPublisher()) ->setRelatedPHIDs($related_phids) - ->setStoryAuthorPHID($this->user->getPHID()) + ->setStoryAuthorPHID($this->getActor()->getPHID()) ->setStoryTime(time()) ->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_PHRICTION) ->setStoryData( array( 'phid' => $document->getPHID(), 'action' => $feed_action, 'content' => phutil_utf8_shorten($new_content->getContent(), 140), 'project' => $project_phid, )) ->publish(); return $this; } } diff --git a/src/applications/ponder/controller/PonderAnswerSaveController.php b/src/applications/ponder/controller/PonderAnswerSaveController.php index 00f0e61a4d..a62dfcf224 100644 --- a/src/applications/ponder/controller/PonderAnswerSaveController.php +++ b/src/applications/ponder/controller/PonderAnswerSaveController.php @@ -1,71 +1,71 @@ getRequest(); if (!$request->isFormPost()) { return new Aphront400Response(); } $user = $request->getUser(); $question_id = $request->getInt('question_id'); $question = PonderQuestionQuery::loadSingle($user, $question_id); if (!$question) { return new Aphront404Response(); } $answer = $request->getStr('answer'); // Only want answers with some non whitespace content if (!strlen(trim($answer))) { $dialog = new AphrontDialogView(); $dialog->setUser($request->getUser()); $dialog->setTitle('Empty answer'); $dialog->appendChild('

Your answer must not be empty.

'); $dialog->addCancelButton('/Q'.$question_id); return id(new AphrontDialogResponse())->setDialog($dialog); } $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_WEB, array( 'ip' => $request->getRemoteAddr(), )); $res = new PonderAnswer(); $res ->setContent($answer) ->setAuthorPHID($user->getPHID()) ->setVoteCount(0) ->setQuestionID($question_id) ->setContentSource($content_source); id(new PonderAnswerEditor()) - ->setUser($user) + ->setActor($user) ->setQuestion($question) ->setAnswer($res) ->saveAnswer(); return id(new AphrontRedirectResponse())->setURI( id(new PhutilURI('/Q'. $question->getID()))); } } diff --git a/src/applications/ponder/controller/PonderCommentSaveController.php b/src/applications/ponder/controller/PonderCommentSaveController.php index 5c1688e131..d852a99345 100644 --- a/src/applications/ponder/controller/PonderCommentSaveController.php +++ b/src/applications/ponder/controller/PonderCommentSaveController.php @@ -1,72 +1,72 @@ getRequest(); if (!$request->isFormPost()) { return new Aphront400Response(); } $user = $request->getUser(); $question_id = $request->getInt('question_id'); $question = PonderQuestionQuery::loadSingle($user, $question_id); if (!$question) { return new Aphront404Response(); } $target = $request->getStr('target'); $objects = id(new PhabricatorObjectHandleData(array($target))) ->loadHandles(); if (!$objects) { return new Aphront404Response(); } $content = $request->getStr('content'); if (!strlen(trim($content))) { $dialog = new AphrontDialogView(); $dialog->setUser($request->getUser()); $dialog->setTitle('Empty comment'); $dialog->appendChild('

Your comment must not be empty.

'); $dialog->addCancelButton('/Q'.$question_id); return id(new AphrontDialogResponse())->setDialog($dialog); } $res = new PonderComment(); $res ->setContent($content) ->setAuthorPHID($user->getPHID()) ->setTargetPHID($target); id(new PonderCommentEditor()) ->setQuestion($question) ->setComment($res) ->setTargetPHID($target) - ->setUser($user) + ->setActor($user) ->save(); return id(new AphrontRedirectResponse()) ->setURI( id(new PhutilURI('/Q'. $question->getID()))); } } diff --git a/src/applications/ponder/controller/PonderQuestionAskController.php b/src/applications/ponder/controller/PonderQuestionAskController.php index 5f1d59461a..e3b0d8282a 100644 --- a/src/applications/ponder/controller/PonderQuestionAskController.php +++ b/src/applications/ponder/controller/PonderQuestionAskController.php @@ -1,130 +1,130 @@ getRequest(); $user = $request->getUser(); $question = id(new PonderQuestion()) ->setAuthorPHID($user->getPHID()) ->setVoteCount(0) ->setAnswerCount(0) ->setHeat(0.0); $errors = array(); $e_title = true; if ($request->isFormPost()) { $question->setTitle($request->getStr('title')); $question->setContent($request->getStr('content')); $len = phutil_utf8_strlen($question->getTitle()); if ($len < 1) { $errors[] = pht('Title must not be empty.'); $e_title = pht('Required'); } else if ($len > 255) { $errors[] = pht('Title is too long.'); $e_title = pht('Too Long'); } if (!$errors) { $content_source = PhabricatorContentSource::newForSource( PhabricatorContentSource::SOURCE_WEB, array( 'ip' => $request->getRemoteAddr(), )); $question->setContentSource($content_source); id(new PonderQuestionEditor()) ->setQuestion($question) - ->setUser($user) + ->setActor($user) ->save(); return id(new AphrontRedirectResponse()) ->setURI('/Q'.$question->getID()); } } $error_view = null; if ($errors) { $error_view = id(new AphrontErrorView()) ->setTitle('Form Errors') ->setErrors($errors); } $header = id(new PhabricatorHeaderView())->setHeader(pht('Ask Question')); $form = id(new AphrontFormView()) ->setUser($user) ->setFlexible(true) ->appendChild( id(new AphrontFormTextControl()) ->setLabel(pht('Question')) ->setName('title') ->setValue($question->getTitle()) ->setError($e_title)) ->appendChild( id(new PhabricatorRemarkupControl()) ->setName('content') ->setID('content') ->setValue($question->getContent()) ->setLabel(pht('Description'))) ->appendChild( id(new AphrontFormSubmitControl()) ->setValue('Ask Away!')); $preview = '
'. '
'. ''. pht('Loading question preview...'). ''. '
'. '
'; Javelin::initBehavior( 'ponder-feedback-preview', array( 'uri' => '/ponder/question/preview/', 'content' => 'content', 'preview' => 'question-preview', 'question_id' => null )); $nav = $this->buildSideNavView($question); $nav->selectFilter($question->getID() ? null : 'question/ask'); $nav->appendChild( array( $header, $error_view, $form, $preview, )); return $this->buildApplicationPage( $nav, array( 'device' => true, 'title' => 'Ask a Question', ) ); } } diff --git a/src/applications/ponder/controller/PonderVoteSaveController.php b/src/applications/ponder/controller/PonderVoteSaveController.php index 6d58ef946d..28174eb787 100644 --- a/src/applications/ponder/controller/PonderVoteSaveController.php +++ b/src/applications/ponder/controller/PonderVoteSaveController.php @@ -1,58 +1,58 @@ kind = $data['kind']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $newvote = $request->getInt("vote"); $phid = $request->getStr("phid"); if (1 < $newvote || $newvote < -1) { return new Aphront400Response(); } $target = null; if ($this->kind == "question") { $target = PonderQuestionQuery::loadSingleByPHID($user, $phid); } else if ($this->kind == "answer") { $target = PonderAnswerQuery::loadSingleByPHID($user, $phid); } if (!$target) { return new Aphront404Response(); } $editor = id(new PonderVoteEditor()) ->setVotable($target) - ->setUser($user) + ->setActor($user) ->setVote($newvote) ->saveVote(); return id(new AphrontAjaxResponse())->setContent("."); } } diff --git a/src/applications/ponder/editor/PonderAnswerEditor.php b/src/applications/ponder/editor/PonderAnswerEditor.php index 9225873da8..66fb8e3b89 100644 --- a/src/applications/ponder/editor/PonderAnswerEditor.php +++ b/src/applications/ponder/editor/PonderAnswerEditor.php @@ -1,124 +1,116 @@ question = $question; return $this; } public function setAnswer($answer) { $this->answer = $answer; return $this; } - public function setUser(PhabricatorUser $user) { - $this->viewer = $user; - return $this; - } - public function saveAnswer() { - if (!$this->viewer) { - throw new Exception("Must set user before saving question"); - } + $actor = $this->requireActor(); if (!$this->question) { throw new Exception("Must set question before saving answer"); } if (!$this->answer) { throw new Exception("Must set answer before saving it"); } $question = $this->question; $answer = $this->answer; - $viewer = $this->viewer; $conn = $answer->establishConnection('w'); $trans = $conn->openTransaction(); $trans->beginReadLocking(); $question->reload(); queryfx($conn, 'UPDATE %T as t SET t.`answerCount` = t.`answerCount` + 1 WHERE t.`PHID` = %s', $question->getTableName(), $question->getPHID()); $answer->setQuestionID($question->getID()); $answer->save(); $trans->endReadLocking(); $trans->saveTransaction(); $question->attachRelated(); PhabricatorSearchPonderIndexer::indexQuestion($question); // subscribe author and @mentions $subeditor = id(new PhabricatorSubscriptionsEditor()) ->setObject($question) - ->setUser($viewer); + ->setActor($actor); $subeditor->subscribeExplicit(array($answer->getAuthorPHID())); $content = $answer->getContent(); $at_mention_phids = PhabricatorMarkupEngine::extractPHIDsFromMentions( array($content) ); $subeditor->subscribeImplicit($at_mention_phids); $subeditor->save(); if ($this->shouldEmail) { // now load subscribers, including implicitly-added @mention victims $subscribers = PhabricatorSubscribersQuery ::loadSubscribersForPHID($question->getPHID()); // @mention emails (but not for anyone who has explicitly unsubscribed) if (array_intersect($at_mention_phids, $subscribers)) { id(new PonderMentionMail( $question, $answer, - $viewer)) + $actor)) ->setToPHIDs($at_mention_phids) ->send(); } $other_subs = array_diff( $subscribers, $at_mention_phids ); // 'Answered' emails for subscribers who are not @mentiond (and excluding // author depending on their MetaMTA settings). if ($other_subs) { id(new PonderAnsweredMail( $question, $answer, - $viewer)) + $actor)) ->setToPHIDs($other_subs) ->send(); } } } } diff --git a/src/applications/ponder/editor/PonderCommentEditor.php b/src/applications/ponder/editor/PonderCommentEditor.php index f51bcb9e0c..de28add69a 100644 --- a/src/applications/ponder/editor/PonderCommentEditor.php +++ b/src/applications/ponder/editor/PonderCommentEditor.php @@ -1,133 +1,123 @@ comment = $comment; return $this; } public function setQuestion(PonderQuestion $question) { $this->question = $question; return $this; } public function setTargetPHID($target) { $this->targetPHID = $target; return $this; } - public function setUser(PhabricatorUser $user) { - $this->viewer = $user; - return $this; - } - public function save() { + $actor = $this->requireActor(); if (!$this->comment) { throw new Exception("Must set comment before saving it"); } if (!$this->question) { throw new Exception("Must set question before saving comment"); } if (!$this->targetPHID) { throw new Exception("Must set target before saving comment"); } - if (!$this->viewer) { - throw new Exception("Must set viewer before saving comment"); - } $comment = $this->comment; $question = $this->question; $target = $this->targetPHID; - $viewer = $this->viewer; $comment->save(); $question->attachRelated(); PhabricatorSearchPonderIndexer::indexQuestion($question); // subscribe author and @mentions $subeditor = id(new PhabricatorSubscriptionsEditor()) ->setObject($question) - ->setUser($viewer); + ->setActor($actor); $subeditor->subscribeExplicit(array($comment->getAuthorPHID())); $content = $comment->getContent(); $at_mention_phids = PhabricatorMarkupEngine::extractPHIDsFromMentions( array($content) ); $subeditor->subscribeImplicit($at_mention_phids); $subeditor->save(); if ($this->shouldEmail) { // now load subscribers, including implicitly-added @mention victims $subscribers = PhabricatorSubscribersQuery ::loadSubscribersForPHID($question->getPHID()); // @mention emails (but not for anyone who has explicitly unsubscribed) if (array_intersect($at_mention_phids, $subscribers)) { id(new PonderMentionMail( $question, $comment, - $viewer)) + $actor)) ->setToPHIDs($at_mention_phids) ->send(); } if ($target === $question->getPHID()) { $target = $question; } else { $answers_by_phid = mgroup($question->getAnswers(), 'getPHID'); $target = head($answers_by_phid[$target]); } // only send emails to others in the same thread $thread = mpull($target->getComments(), 'getAuthorPHID'); $thread[] = $target->getAuthorPHID(); $thread[] = $question->getAuthorPHID(); $other_subs = array_diff( array_intersect($thread, $subscribers), $at_mention_phids ); // 'Comment' emails for subscribers who are in the same comment thread, // including the author of the parent question and/or answer, excluding // @mentions (and excluding the author, depending on their MetaMTA // settings). if ($other_subs) { id(new PonderCommentMail( $question, $comment, - $viewer)) + $actor)) ->setToPHIDs($other_subs) ->send(); } } } } diff --git a/src/applications/ponder/editor/PonderQuestionEditor.php b/src/applications/ponder/editor/PonderQuestionEditor.php index 08ec5e5236..b37a39a5ca 100644 --- a/src/applications/ponder/editor/PonderQuestionEditor.php +++ b/src/applications/ponder/editor/PonderQuestionEditor.php @@ -1,80 +1,70 @@ question = $question; return $this; } - public function setUser(PhabricatorUser $user) { - $this->viewer = $user; - return $this; - } - public function setShouldEmail($se) { $this->shouldEmail = $se; return $this; } public function save() { - if (!$this->viewer) { - throw new Exception("Must set user before saving question"); - } + $actor = $this->requireActor(); if (!$this->question) { throw new Exception("Must set question before saving it"); } - $viewer = $this->viewer; $question = $this->question; $question->save(); // search index $question->attachRelated(); PhabricatorSearchPonderIndexer::indexQuestion($question); // subscribe author and @mentions $subeditor = id(new PhabricatorSubscriptionsEditor()) ->setObject($question) - ->setUser($viewer); + ->setActor($actor); $subeditor->subscribeExplicit(array($question->getAuthorPHID())); $content = $question->getContent(); $at_mention_phids = PhabricatorMarkupEngine::extractPHIDsFromMentions( array($content) ); $subeditor->subscribeImplicit($at_mention_phids); $subeditor->save(); if ($this->shouldEmail && $at_mention_phids) { id(new PonderMentionMail( $question, $question, - $viewer)) + $actor)) ->setToPHIDs($at_mention_phids) ->send(); } } } diff --git a/src/applications/ponder/editor/PonderVoteEditor.php b/src/applications/ponder/editor/PonderVoteEditor.php index 4a6090d497..35623f81d2 100644 --- a/src/applications/ponder/editor/PonderVoteEditor.php +++ b/src/applications/ponder/editor/PonderVoteEditor.php @@ -1,104 +1,94 @@ answer = $answer; return $this; } public function setVotable($votable) { $this->votable = $votable; return $this; } - public function setUser($user) { - $this->user = $user; - return $this; - } - public function setVote($vote) { $this->vote = $vote; return $this; } public function saveVote() { + $actor = $this->requireActor(); if (!$this->votable) { throw new Exception("Must set votable before saving vote"); } - if (!$this->user) { - throw new Exception("Must set user before saving vote"); - } - $user = $this->user; $votable = $this->votable; $newvote = $this->vote; // prepare vote add, or update if this user is amending an // earlier vote $editor = id(new PhabricatorEdgeEditor()) - ->setUser($user) + ->setActor($actor) ->addEdge( - $user->getPHID(), + $actor->getPHID(), $votable->getUserVoteEdgeType(), $votable->getVotablePHID(), array('data' => $newvote)) ->removeEdge( - $user->getPHID(), + $actor->getPHID(), $votable->getUserVoteEdgeType(), $votable->getVotablePHID()); $conn = $votable->establishConnection('w'); $trans = $conn->openTransaction(); $trans->beginReadLocking(); $votable->reload(); $curvote = (int)PhabricatorEdgeQuery::loadSingleEdgeData( - $user->getPHID(), + $actor->getPHID(), $votable->getUserVoteEdgeType(), $votable->getVotablePHID()); if (!$curvote) { $curvote = PonderConstants::NONE_VOTE; } // adjust votable's score by this much $delta = $newvote - $curvote; queryfx($conn, 'UPDATE %T as t SET t.`voteCount` = t.`voteCount` + %d WHERE t.`PHID` = %s', $votable->getTableName(), $delta, $votable->getVotablePHID()); $editor->save(); $trans->endReadLocking(); $trans->saveTransaction(); } } diff --git a/src/applications/project/controller/PhabricatorProjectCreateController.php b/src/applications/project/controller/PhabricatorProjectCreateController.php index c93ec5f6b4..4f184b061d 100644 --- a/src/applications/project/controller/PhabricatorProjectCreateController.php +++ b/src/applications/project/controller/PhabricatorProjectCreateController.php @@ -1,142 +1,142 @@ getRequest(); $user = $request->getUser(); $project = new PhabricatorProject(); $project->setAuthorPHID($user->getPHID()); $profile = new PhabricatorProjectProfile(); $e_name = true; $errors = array(); if ($request->isFormPost()) { try { $xactions = array(); $xaction = new PhabricatorProjectTransaction(); $xaction->setTransactionType( PhabricatorProjectTransactionType::TYPE_NAME); $xaction->setNewValue($request->getStr('name')); $xactions[] = $xaction; $xaction = new PhabricatorProjectTransaction(); $xaction->setTransactionType( PhabricatorProjectTransactionType::TYPE_MEMBERS); $xaction->setNewValue(array($user->getPHID())); $xactions[] = $xaction; $editor = new PhabricatorProjectEditor($project); - $editor->setUser($user); + $editor->setActor($user); $editor->applyTransactions($xactions); } catch (PhabricatorProjectNameCollisionException $ex) { $e_name = 'Not Unique'; $errors[] = $ex->getMessage(); } $profile->setBlurb($request->getStr('blurb')); if (!$errors) { $project->save(); $profile->setProjectPHID($project->getPHID()); $profile->save(); if ($request->isAjax()) { return id(new AphrontAjaxResponse()) ->setContent(array( 'phid' => $project->getPHID(), 'name' => $project->getName(), )); } else { return id(new AphrontRedirectResponse()) ->setURI('/project/view/'.$project->getID().'/'); } } } $error_view = null; if ($errors) { $error_view = new AphrontErrorView(); $error_view->setTitle('Form Errors'); $error_view->setErrors($errors); } if ($request->isAjax()) { $form = new AphrontFormLayoutView(); } else { $form = new AphrontFormView(); $form->setUser($user); } $form ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Name') ->setName('name') ->setValue($project->getName()) ->setError($e_name)) ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel('Blurb') ->setName('blurb') ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT) ->setValue($profile->getBlurb())); if ($request->isAjax()) { $dialog = id(new AphrontDialogView()) ->setUser($user) ->setWidth(AphrontDialogView::WIDTH_FORM) ->setTitle('Create a New Project') ->appendChild($error_view) ->appendChild($form) ->addSubmitButton('Create Project') ->addCancelButton('/project/'); return id(new AphrontDialogResponse())->setDialog($dialog); } else { $form ->appendChild( id(new AphrontFormSubmitControl()) ->setValue('Create') ->addCancelButton('/project/')); $panel = new AphrontPanelView(); $panel ->setWidth(AphrontPanelView::WIDTH_FORM) ->setHeader('Create a New Project') ->appendChild($form); return $this->buildStandardPageResponse( array( $error_view, $panel, ), array( 'title' => 'Create new Project', )); } } } diff --git a/src/applications/project/controller/PhabricatorProjectMembersEditController.php b/src/applications/project/controller/PhabricatorProjectMembersEditController.php index f2dcae0482..3942755b0e 100644 --- a/src/applications/project/controller/PhabricatorProjectMembersEditController.php +++ b/src/applications/project/controller/PhabricatorProjectMembersEditController.php @@ -1,181 +1,181 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $project = id(new PhabricatorProjectQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$project) { return new Aphront404Response(); } $profile = $project->loadProfile(); if (empty($profile)) { $profile = new PhabricatorProjectProfile(); } $member_phids = $project->loadMemberPHIDs(); $errors = array(); if ($request->isFormPost()) { $changed_something = false; $member_map = array_fill_keys($member_phids, true); $remove = $request->getStr('remove'); if ($remove) { if (isset($member_map[$remove])) { unset($member_map[$remove]); $changed_something = true; } } else { $new_members = $request->getArr('phids'); foreach ($new_members as $member) { if (empty($member_map[$member])) { $member_map[$member] = true; $changed_something = true; } } } $xactions = array(); if ($changed_something) { $xaction = new PhabricatorProjectTransaction(); $xaction->setTransactionType( PhabricatorProjectTransactionType::TYPE_MEMBERS); $xaction->setNewValue(array_keys($member_map)); $xactions[] = $xaction; } if ($xactions) { $editor = new PhabricatorProjectEditor($project); - $editor->setUser($user); + $editor->setActor($user); $editor->applyTransactions($xactions); } return id(new AphrontRedirectResponse()) ->setURI($request->getRequestURI()); } $member_phids = array_reverse($member_phids); $handles = $this->loadViewerHandles($member_phids); $state = array(); foreach ($handles as $handle) { $state[] = array( 'phid' => $handle->getPHID(), 'name' => $handle->getFullName(), ); } $header_name = 'Edit Members'; $title = 'Edit Members'; $list = $this->renderMemberList($handles); $form = new AphrontFormView(); $form ->setUser($user) ->appendChild( id(new AphrontFormTokenizerControl()) ->setName('phids') ->setLabel('Add Members') ->setDatasource('/typeahead/common/users/')) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton('/project/view/'.$project->getID().'/') ->setValue('Add Members')); $faux_form = id(new AphrontFormLayoutView()) ->setBackgroundShading(true) ->setPadded(true) ->appendChild( id(new AphrontFormInsetView()) ->setTitle('Current Members ('.count($handles).')') ->appendChild($list)); $panel = new AphrontPanelView(); $panel->setHeader($header_name); $panel->setWidth(AphrontPanelView::WIDTH_FORM); $panel->appendChild($form); $panel->appendChild('
'); $panel->appendChild($faux_form); $nav = $this->buildLocalNavigation($project); $nav->selectFilter('members'); $nav->appendChild($panel); return $this->buildStandardPageResponse( $nav, array( 'title' => $title, )); } private function renderMemberList(array $handles) { $request = $this->getRequest(); $user = $request->getUser(); $list = id(new PhabricatorObjectListView()) ->setHandles($handles); foreach ($handles as $handle) { $hidden_input = phutil_render_tag( 'input', array( 'type' => 'hidden', 'name' => 'remove', 'value' => $handle->getPHID(), ), ''); $button = javelin_render_tag( 'button', array( 'class' => 'grey', ), pht('Remove')); $list->addButton( $handle, phabricator_render_form( $user, array( 'method' => 'POST', 'action' => $request->getRequestURI(), ), $hidden_input.$button)); } return $list; } } diff --git a/src/applications/project/controller/PhabricatorProjectProfileEditController.php b/src/applications/project/controller/PhabricatorProjectProfileEditController.php index a288cd735d..e782c94b9b 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileEditController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileEditController.php @@ -1,254 +1,254 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $project = id(new PhabricatorProjectQuery()) ->setViewer($user) ->withIDs(array($this->id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$project) { return new Aphront404Response(); } $profile = $project->loadProfile(); if (empty($profile)) { $profile = new PhabricatorProjectProfile(); } $img_src = $profile->loadProfileImageURI(); $options = PhabricatorProjectStatus::getStatusMap(); $supported_formats = PhabricatorFile::getTransformableImageFormats(); $e_name = true; $e_image = null; $errors = array(); if ($request->isFormPost()) { try { $xactions = array(); $xaction = new PhabricatorProjectTransaction(); $xaction->setTransactionType( PhabricatorProjectTransactionType::TYPE_NAME); $xaction->setNewValue($request->getStr('name')); $xactions[] = $xaction; $xaction = new PhabricatorProjectTransaction(); $xaction->setTransactionType( PhabricatorProjectTransactionType::TYPE_STATUS); $xaction->setNewValue($request->getStr('status')); $xactions[] = $xaction; $xaction = new PhabricatorProjectTransaction(); $xaction->setTransactionType( PhabricatorProjectTransactionType::TYPE_CAN_VIEW); $xaction->setNewValue($request->getStr('can_view')); $xactions[] = $xaction; $xaction = new PhabricatorProjectTransaction(); $xaction->setTransactionType( PhabricatorProjectTransactionType::TYPE_CAN_EDIT); $xaction->setNewValue($request->getStr('can_edit')); $xactions[] = $xaction; $xaction = new PhabricatorProjectTransaction(); $xaction->setTransactionType( PhabricatorProjectTransactionType::TYPE_CAN_JOIN); $xaction->setNewValue($request->getStr('can_join')); $xactions[] = $xaction; $editor = new PhabricatorProjectEditor($project); - $editor->setUser($user); + $editor->setActor($user); $editor->applyTransactions($xactions); } catch (PhabricatorProjectNameCollisionException $ex) { $e_name = 'Not Unique'; $errors[] = $ex->getMessage(); } $profile->setBlurb($request->getStr('blurb')); if (!strlen($project->getName())) { $e_name = 'Required'; $errors[] = 'Project name is required.'; } else { $e_name = null; } $default_image = $request->getExists('default_image'); if ($default_image) { $profile->setProfileImagePHID(null); } else if (!empty($_FILES['image'])) { $err = idx($_FILES['image'], 'error'); if ($err != UPLOAD_ERR_NO_FILE) { $file = PhabricatorFile::newFromPHPUpload( $_FILES['image'], array( 'authorPHID' => $user->getPHID(), )); $okay = $file->isTransformableImage(); if ($okay) { $xformer = new PhabricatorImageTransformer(); $xformed = $xformer->executeThumbTransform( $file, $x = 50, $y = 50); $profile->setProfileImagePHID($xformed->getPHID()); } else { $e_image = 'Not Supported'; $errors[] = 'This server only supports these image formats: '. implode(', ', $supported_formats).'.'; } } } if (!$errors) { $project->save(); $profile->setProjectPHID($project->getPHID()); $profile->save(); return id(new AphrontRedirectResponse()) ->setURI('/project/view/'.$project->getID().'/'); } } $error_view = null; if ($errors) { $error_view = new AphrontErrorView(); $error_view->setTitle('Form Errors'); $error_view->setErrors($errors); } $header_name = 'Edit Project'; $title = 'Edit Project'; $action = '/project/edit/'.$project->getID().'/'; $policies = id(new PhabricatorPolicyQuery()) ->setViewer($user) ->setObject($project) ->execute(); $form = new AphrontFormView(); $form ->setID('project-edit-form') ->setUser($user) ->setAction($action) ->setEncType('multipart/form-data') ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Name') ->setName('name') ->setValue($project->getName()) ->setError($e_name)) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel('Project Status') ->setName('status') ->setOptions($options) ->setValue($project->getStatus())) ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel('Blurb') ->setName('blurb') ->setValue($profile->getBlurb())) ->appendChild( '

NOTE: Policy settings are not '. 'yet fully implemented. Some interfaces still ignore these settings, '. 'particularly "Visible To".

') ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setName('can_view') ->setCaption('Members can always view a project.') ->setPolicyObject($project) ->setPolicies($policies) ->setCapability(PhabricatorPolicyCapability::CAN_VIEW)) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setName('can_edit') ->setPolicyObject($project) ->setPolicies($policies) ->setCapability(PhabricatorPolicyCapability::CAN_EDIT)) ->appendChild( id(new AphrontFormPolicyControl()) ->setUser($user) ->setName('can_join') ->setCaption( 'Users who can edit a project can always join a project.') ->setPolicyObject($project) ->setPolicies($policies) ->setCapability(PhabricatorPolicyCapability::CAN_JOIN)) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel('Profile Image') ->setValue( phutil_render_tag( 'img', array( 'src' => $img_src, )))) ->appendChild( id(new AphrontFormImageControl()) ->setLabel('Change Image') ->setName('image') ->setError($e_image) ->setCaption('Supported formats: '.implode(', ', $supported_formats))) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton('/project/view/'.$project->getID().'/') ->setValue('Save')); $panel = new AphrontPanelView(); $panel->setHeader($header_name); $panel->setWidth(AphrontPanelView::WIDTH_FORM); $panel->appendChild($form); $nav = $this->buildLocalNavigation($project); $nav->selectFilter('edit'); $nav->appendChild( array( $error_view, $panel, )); return $this->buildStandardPageResponse( $nav, array( 'title' => $title, )); } } diff --git a/src/applications/project/editor/PhabricatorProjectEditor.php b/src/applications/project/editor/PhabricatorProjectEditor.php index 3282520aa4..edce444584 100644 --- a/src/applications/project/editor/PhabricatorProjectEditor.php +++ b/src/applications/project/editor/PhabricatorProjectEditor.php @@ -1,394 +1,385 @@ getMemberPHIDs(); $members[] = $user->getPHID(); self::applyOneTransaction( $project, $user, PhabricatorProjectTransactionType::TYPE_MEMBERS, $members); } public static function applyLeaveProject( PhabricatorProject $project, PhabricatorUser $user) { $members = array_fill_keys($project->getMemberPHIDs(), true); unset($members[$user->getPHID()]); $members = array_keys($members); self::applyOneTransaction( $project, $user, PhabricatorProjectTransactionType::TYPE_MEMBERS, $members); } private static function applyOneTransaction( PhabricatorProject $project, PhabricatorUser $user, $type, $new_value) { $xaction = new PhabricatorProjectTransaction(); $xaction->setTransactionType($type); $xaction->setNewValue($new_value); $editor = new PhabricatorProjectEditor($project); - $editor->setUser($user); + $editor->setActor($user); $editor->applyTransactions(array($xaction)); } public function __construct(PhabricatorProject $project) { $this->project = $project; } - public function setUser(PhabricatorUser $user) { - $this->user = $user; - return $this; - } - public function applyTransactions(array $transactions) { assert_instances_of($transactions, 'PhabricatorProjectTransaction'); - if (!$this->user) { - throw new Exception('Call setUser() before save()!'); - } - $user = $this->user; + $actor = $this->requireActor(); $project = $this->project; $is_new = !$project->getID(); if ($is_new) { - $project->setAuthorPHID($user->getPHID()); + $project->setAuthorPHID($actor->getPHID()); } foreach ($transactions as $key => $xaction) { $this->setTransactionOldValue($project, $xaction); if (!$this->transactionHasEffect($xaction)) { unset($transactions[$key]); continue; } } if (!$is_new) { // You must be able to view a project in order to edit it in any capacity. PhabricatorPolicyFilter::requireCapability( - $user, + $actor, $project, PhabricatorPolicyCapability::CAN_VIEW); $need_edit = false; $need_join = false; foreach ($transactions as $key => $xaction) { if ($this->getTransactionRequiresEditCapability($xaction)) { $need_edit = true; } if ($this->getTransactionRequiresJoinCapability($xaction)) { $need_join = true; } } if ($need_edit) { PhabricatorPolicyFilter::requireCapability( - $user, + $actor, $project, PhabricatorPolicyCapability::CAN_EDIT); } if ($need_join) { PhabricatorPolicyFilter::requireCapability( - $user, + $actor, $project, PhabricatorPolicyCapability::CAN_JOIN); } } if (!$transactions) { return $this; } foreach ($transactions as $xaction) { $this->applyTransactionEffect($project, $xaction); } try { $project->openTransaction(); $project->save(); $edge_type = PhabricatorEdgeConfig::TYPE_PROJ_MEMBER; $editor = new PhabricatorEdgeEditor(); - $editor->setUser($this->user); + $editor->setActor($actor); foreach ($this->remEdges as $phid) { $editor->removeEdge($project->getPHID(), $edge_type, $phid); } foreach ($this->addEdges as $phid) { $editor->addEdge($project->getPHID(), $edge_type, $phid); } $editor->save(); foreach ($transactions as $xaction) { - $xaction->setAuthorPHID($user->getPHID()); + $xaction->setAuthorPHID($actor->getPHID()); $xaction->setProjectID($project->getID()); $xaction->save(); } $project->saveTransaction(); foreach ($transactions as $xaction) { $this->publishTransactionStory($project, $xaction); } } catch (AphrontQueryDuplicateKeyException $ex) { // We already validated the slug, but might race. Try again to see if // that's the issue. If it is, we'll throw a more specific exception. If // not, throw the original exception. $this->validateName($project); throw $ex; } // TODO: If we rename a project, we should move its Phriction page. Do // that once Phriction supports document moves. return $this; } private function validateName(PhabricatorProject $project) { $slug = $project->getPhrictionSlug(); $name = $project->getName(); if ($slug == '/') { throw new PhabricatorProjectNameCollisionException( "Project names must be unique and contain some letters or numbers."); } $id = $project->getID(); $collision = id(new PhabricatorProject())->loadOneWhere( '(name = %s OR phrictionSlug = %s) AND id %Q %nd', $name, $slug, $id ? '!=' : 'IS NOT', $id ? $id : null); if ($collision) { $other_name = $collision->getName(); $other_id = $collision->getID(); throw new PhabricatorProjectNameCollisionException( "Project names must be unique. The name '{$name}' is too similar to ". "the name of another project, '{$other_name}' (Project ID: ". "{$other_id}). Choose a unique name."); } } private function setTransactionOldValue( PhabricatorProject $project, PhabricatorProjectTransaction $xaction) { $type = $xaction->getTransactionType(); switch ($type) { case PhabricatorProjectTransactionType::TYPE_NAME: $xaction->setOldValue($project->getName()); break; case PhabricatorProjectTransactionType::TYPE_STATUS: $xaction->setOldValue($project->getStatus()); break; case PhabricatorProjectTransactionType::TYPE_MEMBERS: $member_phids = $project->loadMemberPHIDs(); $project->attachMemberPHIDs($member_phids); $old_value = array_values($member_phids); $xaction->setOldValue($old_value); $new_value = $xaction->getNewValue(); $new_value = array_filter($new_value); $new_value = array_unique($new_value); $new_value = array_values($new_value); $xaction->setNewValue($new_value); break; case PhabricatorProjectTransactionType::TYPE_CAN_VIEW: $xaction->setOldValue($project->getViewPolicy()); break; case PhabricatorProjectTransactionType::TYPE_CAN_EDIT: $xaction->setOldValue($project->getEditPolicy()); break; case PhabricatorProjectTransactionType::TYPE_CAN_JOIN: $xaction->setOldValue($project->getJoinPolicy()); break; default: throw new Exception("Unknown transaction type '{$type}'!"); } return $this; } private function applyTransactionEffect( PhabricatorProject $project, PhabricatorProjectTransaction $xaction) { $type = $xaction->getTransactionType(); switch ($type) { case PhabricatorProjectTransactionType::TYPE_NAME: $project->setName($xaction->getNewValue()); $project->setPhrictionSlug($xaction->getNewValue()); $this->validateName($project); break; case PhabricatorProjectTransactionType::TYPE_STATUS: $project->setStatus($xaction->getNewValue()); break; case PhabricatorProjectTransactionType::TYPE_MEMBERS: $old = array_fill_keys($xaction->getOldValue(), true); $new = array_fill_keys($xaction->getNewValue(), true); $this->addEdges = array_keys(array_diff_key($new, $old)); $this->remEdges = array_keys(array_diff_key($old, $new)); break; case PhabricatorProjectTransactionType::TYPE_CAN_VIEW: $project->setViewPolicy($xaction->getNewValue()); break; case PhabricatorProjectTransactionType::TYPE_CAN_EDIT: $project->setEditPolicy($xaction->getNewValue()); // You can't edit away your ability to edit the project. PhabricatorPolicyFilter::mustRetainCapability( - $this->user, + $this->getActor(), $project, PhabricatorPolicyCapability::CAN_EDIT); break; case PhabricatorProjectTransactionType::TYPE_CAN_JOIN: $project->setJoinPolicy($xaction->getNewValue()); break; default: throw new Exception("Unknown transaction type '{$type}'!"); } } private function publishTransactionStory( PhabricatorProject $project, PhabricatorProjectTransaction $xaction) { $related_phids = array( $project->getPHID(), $xaction->getAuthorPHID(), ); id(new PhabricatorFeedStoryPublisher()) ->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_PROJECT) ->setStoryData( array( 'projectPHID' => $project->getPHID(), 'transactionID' => $xaction->getID(), 'type' => $xaction->getTransactionType(), 'old' => $xaction->getOldValue(), 'new' => $xaction->getNewValue(), )) ->setStoryTime(time()) ->setStoryAuthorPHID($xaction->getAuthorPHID()) ->setRelatedPHIDs($related_phids) ->publish(); } private function transactionHasEffect( PhabricatorProjectTransaction $xaction) { return ($xaction->getOldValue() !== $xaction->getNewValue()); } /** * All transactions except joining or leaving a project require edit * capability. */ private function getTransactionRequiresEditCapability( PhabricatorProjectTransaction $xaction) { return ($this->isJoinOrLeaveTransaction($xaction) === null); } /** * Joining a project requires the join capability. Anyone leave a project. */ private function getTransactionRequiresJoinCapability( PhabricatorProjectTransaction $xaction) { $type = $this->isJoinOrLeaveTransaction($xaction); return ($type == 'join'); } /** * Returns 'join' if this transaction causes the acting user ONLY to join the * project. * * Returns 'leave' if this transaction causes the acting user ONLY to leave * the project. * * Returns null in all other cases. */ private function isJoinOrLeaveTransaction( PhabricatorProjectTransaction $xaction) { $type = $xaction->getTransactionType(); if ($type != PhabricatorProjectTransactionType::TYPE_MEMBERS) { return null; } switch ($type) { case PhabricatorProjectTransactionType::TYPE_MEMBERS: $old = $xaction->getOldValue(); $new = $xaction->getNewValue(); $add = array_diff($new, $old); $rem = array_diff($old, $new); if (count($add) > 1) { return null; } else if (count($add) == 1) { - if (reset($add) != $this->user->getPHID()) { + if (reset($add) != $this->getActor()->getPHID()) { return null; } else { return 'join'; } } if (count($rem) > 1) { return null; } else if (count($rem) == 1) { - if (reset($rem) != $this->user->getPHID()) { + if (reset($rem) != $this->getActor()->getPHID()) { return null; } else { return 'leave'; } } break; } return true; } } diff --git a/src/applications/project/editor/__tests__/PhabricatorProjectEditorTestCase.php b/src/applications/project/editor/__tests__/PhabricatorProjectEditorTestCase.php index 649a7c8361..7f45383c4e 100644 --- a/src/applications/project/editor/__tests__/PhabricatorProjectEditorTestCase.php +++ b/src/applications/project/editor/__tests__/PhabricatorProjectEditorTestCase.php @@ -1,292 +1,292 @@ true, ); } public function testViewProject() { $user = $this->createUser(); $user->save(); $user2 = $this->createUser(); $user2->save(); $proj = $this->createProject(); $proj->setAuthorPHID($user->getPHID()); $proj->save(); $proj = $this->refreshProject($proj, $user, true); PhabricatorProjectEditor::applyJoinProject($proj, $user); $proj->setViewPolicy(PhabricatorPolicies::POLICY_USER); $proj->save(); $can_view = PhabricatorPolicyCapability::CAN_VIEW; // When the view policy is set to "users", any user can see the project. $this->assertEqual( true, (bool)$this->refreshProject($proj, $user)); $this->assertEqual( true, (bool)$this->refreshProject($proj, $user2)); // When the view policy is set to "no one", members can still see the // project. $proj->setViewPolicy(PhabricatorPolicies::POLICY_NOONE); $proj->save(); $this->assertEqual( true, (bool)$this->refreshProject($proj, $user)); $this->assertEqual( false, (bool)$this->refreshProject($proj, $user2)); } public function testEditProject() { $user = $this->createUser(); $user->save(); $user2 = $this->createUser(); $user2->save(); $proj = $this->createProject(); $proj->setAuthorPHID($user->getPHID()); $proj->save(); // When edit and view policies are set to "user", anyone can edit. $proj->setViewPolicy(PhabricatorPolicies::POLICY_USER); $proj->setEditPolicy(PhabricatorPolicies::POLICY_USER); $proj->save(); $this->assertEqual( true, $this->attemptProjectEdit($proj, $user)); // When edit policy is set to "no one", no one can edit. $proj->setEditPolicy(PhabricatorPolicies::POLICY_NOONE); $proj->save(); $caught = null; try { $this->attemptProjectEdit($proj, $user); } catch (Exception $ex) { $caught = $ex; } $this->assertEqual(true, ($caught instanceof Exception)); } private function attemptProjectEdit( PhabricatorProject $proj, PhabricatorUser $user, $skip_refresh = false) { $proj = $this->refreshProject($proj, $user, true); $new_name = $proj->getName().' '.mt_rand(); $xaction = new PhabricatorProjectTransaction(); $xaction->setTransactionType(PhabricatorProjectTransactionType::TYPE_NAME); $xaction->setNewValue($new_name); $editor = new PhabricatorProjectEditor($proj); - $editor->setUser($user); + $editor->setActor($user); $editor->applyTransactions(array($xaction)); return true; } public function testJoinLeaveProject() { $user = $this->createUser(); $user->save(); $proj = $this->createProjectWithNewAuthor(); $proj->save(); $proj = $this->refreshProject($proj, $user, true); $this->assertEqual( true, (bool)$proj, 'Assumption that projects are default visible to any user when created.'); $this->assertEqual( false, $proj->isUserMember($user->getPHID()), 'Arbitrary user not member of project.'); // Join the project. PhabricatorProjectEditor::applyJoinProject($proj, $user); $proj = $this->refreshProject($proj, $user, true); $this->assertEqual(true, (bool)$proj); $this->assertEqual( true, $proj->isUserMember($user->getPHID()), 'Join works.'); // Join the project again. PhabricatorProjectEditor::applyJoinProject($proj, $user); $proj = $this->refreshProject($proj, $user, true); $this->assertEqual(true, (bool)$proj); $this->assertEqual( true, $proj->isUserMember($user->getPHID()), 'Joining an already-joined project is a no-op.'); // Leave the project. PhabricatorProjectEditor::applyLeaveProject($proj, $user); $proj = $this->refreshProject($proj, $user, true); $this->assertEqual(true, (bool)$proj); $this->assertEqual( false, $proj->isUserMember($user->getPHID()), 'Leave works.'); // Leave the project again. PhabricatorProjectEditor::applyLeaveProject($proj, $user); $proj = $this->refreshProject($proj, $user, true); $this->assertEqual(true, (bool)$proj); $this->assertEqual( false, $proj->isUserMember($user->getPHID()), 'Leaving an already-left project is a no-op.'); // If a user can't edit or join a project, joining fails. $proj->setEditPolicy(PhabricatorPolicies::POLICY_NOONE); $proj->setJoinPolicy(PhabricatorPolicies::POLICY_NOONE); $proj->save(); $proj = $this->refreshProject($proj, $user, true); $caught = null; try { PhabricatorProjectEditor::applyJoinProject($proj, $user); } catch (Exception $ex) { $caught = $ex; } $this->assertEqual(true, ($ex instanceof Exception)); // If a user can edit a project, they can join. $proj->setEditPolicy(PhabricatorPolicies::POLICY_USER); $proj->setJoinPolicy(PhabricatorPolicies::POLICY_NOONE); $proj->save(); $proj = $this->refreshProject($proj, $user, true); PhabricatorProjectEditor::applyJoinProject($proj, $user); $proj = $this->refreshProject($proj, $user, true); $this->assertEqual( true, $proj->isUserMember($user->getPHID()), 'Join allowed with edit permission.'); PhabricatorProjectEditor::applyLeaveProject($proj, $user); // If a user can join a project, they can join, even if they can't edit. $proj->setEditPolicy(PhabricatorPolicies::POLICY_NOONE); $proj->setJoinPolicy(PhabricatorPolicies::POLICY_USER); $proj->save(); $proj = $this->refreshProject($proj, $user, true); PhabricatorProjectEditor::applyJoinProject($proj, $user); $proj = $this->refreshProject($proj, $user, true); $this->assertEqual( true, $proj->isUserMember($user->getPHID()), 'Join allowed with join permission.'); // A user can leave a project even if they can't edit it or join. $proj->setEditPolicy(PhabricatorPolicies::POLICY_NOONE); $proj->setJoinPolicy(PhabricatorPolicies::POLICY_NOONE); $proj->save(); $proj = $this->refreshProject($proj, $user, true); PhabricatorProjectEditor::applyLeaveProject($proj, $user); $proj = $this->refreshProject($proj, $user, true); $this->assertEqual( false, $proj->isUserMember($user->getPHID()), 'Leave allowed without any permission.'); } private function refreshProject( PhabricatorProject $project, PhabricatorUser $viewer, $need_members = false) { $results = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->needMembers($need_members) ->withIDs(array($project->getID())) ->execute(); if ($results) { return head($results); } else { return null; } } private function createProject() { $project = new PhabricatorProject(); $project->setName('Test Project '.mt_rand()); return $project; } private function createProjectWithNewAuthor() { $author = $this->createUser(); $author->save(); $project = $this->createProject(); $project->setAuthorPHID($author->getPHID()); return $project; } private function createUser() { $rand = mt_rand(); $user = new PhabricatorUser(); $user->setUsername('unittestuser'.$rand); $user->setRealName('Unit Test User '.$rand); return $user; } } diff --git a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php index 2a4083776e..5f8e21f455 100644 --- a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php +++ b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php @@ -1,444 +1,446 @@ commit; $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere( 'commitID = %d', $commit->getID()); if (!$data) { $data = new PhabricatorRepositoryCommitData(); } $data->setCommitID($commit->getID()); $data->setAuthorName($author); $data->setCommitMessage($message); if ($committer) { $data->setCommitDetail('committer', $committer); } $repository = $this->repository; $detail_parser = $repository->getDetail( 'detail-parser', 'PhabricatorRepositoryDefaultCommitMessageDetailParser'); if ($detail_parser) { $parser_obj = newv($detail_parser, array($commit, $data)); $parser_obj->parseCommitDetails(); } $author_phid = $this->lookupUser( $commit, $data->getAuthorName(), $data->getCommitDetail('authorPHID')); $data->setCommitDetail('authorPHID', $author_phid); $committer_phid = $this->lookupUser( $commit, $data->getCommitDetail('committer'), $data->getCommitDetail('committerPHID')); $data->setCommitDetail('committerPHID', $committer_phid); if ($author_phid != $commit->getAuthorPHID()) { $commit->setAuthorPHID($author_phid); $commit->save(); } $conn_w = id(new DifferentialRevision())->establishConnection('w'); // NOTE: The `differential_commit` table has a unique ID on `commitPHID`, // preventing more than one revision from being associated with a commit. // Generally this is good and desirable, but with the advent of hash // tracking we may end up in a situation where we match several different // revisions. We just kind of ignore this and pick one, we might want to // revisit this and do something differently. (If we match several revisions // someone probably did something very silly, though.) $revision = null; $should_autoclose = $repository->shouldAutocloseCommit($commit, $data); $revision_id = $data->getCommitDetail('differential.revisionID'); if (!$revision_id) { $hashes = $this->getCommitHashes( $this->repository, $this->commit); if ($hashes) { $query = new DifferentialRevisionQuery(); $query->withCommitHashes($hashes); $revisions = $query->execute(); if (!empty($revisions)) { $revision = $this->identifyBestRevision($revisions); $revision_id = $revision->getID(); } } } if ($revision_id) { $lock = PhabricatorGlobalLock::newLock(get_class($this).':'.$revision_id); $lock->lock(5 * 60); $revision = id(new DifferentialRevision())->load($revision_id); if ($revision) { $revision->loadRelationships(); queryfx( $conn_w, 'INSERT IGNORE INTO %T (revisionID, commitPHID) VALUES (%d, %s)', DifferentialRevision::TABLE_COMMIT, $revision->getID(), $commit->getPHID()); $status_closed = ArcanistDifferentialRevisionStatus::CLOSED; $should_close = ($revision->getStatus() != $status_closed) && $should_autoclose; if ($should_close) { $actor_phid = nonempty( $committer_phid, $author_phid, $revision->getAuthorPHID()); + $actor = id(new PhabricatorUser()) + ->loadOneWhere('phid = %s', $actor_phid); $diff = $this->attachToRevision($revision, $actor_phid); $revision->setDateCommitted($commit->getEpoch()); $editor = new DifferentialCommentEditor( $revision, - $actor_phid, DifferentialAction::ACTION_CLOSE); + $editor->setActor($actor); $editor->setIsDaemonWorkflow(true); $vs_diff = $this->loadChangedByCommit($diff); if ($vs_diff) { $data->setCommitDetail('vsDiff', $vs_diff->getID()); $changed_by_commit = PhabricatorEnv::getProductionURI( '/D'.$revision->getID(). '?vs='.$vs_diff->getID(). '&id='.$diff->getID(). '#toc'); $editor->setChangedByCommit($changed_by_commit); } $commit_name = $repository->formatCommitName( $commit->getCommitIdentifier()); $committer_name = $this->loadUserName( $committer_phid, $data->getCommitDetail('committer')); $author_name = $this->loadUserName( $author_phid, $data->getAuthorName()); $info = array(); $info[] = "authored by {$author_name}"; if ($committer_name && ($committer_name != $author_name)) { $info[] = "committed by {$committer_name}"; } $info = implode(', ', $info); $editor ->setMessage("Closed by commit {$commit_name} ({$info}).") ->save(); } } $lock->unlock(); } if ($should_autoclose && $author_phid) { $user = id(new PhabricatorUser())->loadOneWhere( 'phid = %s', $author_phid); $call = new ConduitCall( 'differential.parsecommitmessage', array( 'corpus' => $message, 'partial' => true, )); $call->setUser($user); $result = $call->execute(); $field_values = $result['fields']; $fields = DifferentialFieldSelector::newSelector() ->getFieldSpecifications(); foreach ($fields as $key => $field) { if (!$field->shouldAppearOnCommitMessage()) { continue; } $field->setUser($user); $value = idx($field_values, $field->getCommitMessageKey()); $field->setValueFromParsedCommitMessage($value); if ($revision) { $field->setRevision($revision); } $field->didParseCommit($repository, $commit, $data); } } $data->save(); } private function loadUserName($user_phid, $default) { if (!$user_phid) { return $default; } $handle = PhabricatorObjectHandleData::loadOneHandle($user_phid); return '@'.$handle->getName(); } private function attachToRevision( DifferentialRevision $revision, $actor_phid) { $drequest = DiffusionRequest::newFromDictionary(array( 'repository' => $this->repository, 'commit' => $this->commit->getCommitIdentifier(), )); $raw_diff = DiffusionRawDiffQuery::newFromDiffusionRequest($drequest) ->loadRawDiff(); // TODO: Support adds, deletes and moves under SVN. $changes = id(new ArcanistDiffParser())->parseDiff($raw_diff); $diff = DifferentialDiff::newFromRawChanges($changes) ->setRevisionID($revision->getID()) ->setAuthorPHID($actor_phid) ->setCreationMethod('commit') ->setSourceControlSystem($this->repository->getVersionControlSystem()) ->setLintStatus(DifferentialLintStatus::LINT_SKIP) ->setUnitStatus(DifferentialUnitStatus::UNIT_SKIP) ->setDateCreated($this->commit->getEpoch()) ->setDescription( 'Commit r'. $this->repository->getCallsign(). $this->commit->getCommitIdentifier()); // TODO: This is not correct in SVN where one repository can have multiple // Arcanist projects. $arcanist_project = id(new PhabricatorRepositoryArcanistProject()) ->loadOneWhere('repositoryID = %d LIMIT 1', $this->repository->getID()); if ($arcanist_project) { $diff->setArcanistProjectPHID($arcanist_project->getPHID()); } $parents = DiffusionCommitParentsQuery::newFromDiffusionRequest($drequest) ->loadParents(); if ($parents) { $diff->setSourceControlBaseRevision(head_key($parents)); } // TODO: Attach binary files. $revision->setLineCount($diff->getLineCount()); return $diff->save(); } private function loadChangedByCommit(DifferentialDiff $diff) { $repository = $this->repository; $vs_changesets = array(); $vs_diff = id(new DifferentialDiff())->loadOneWhere( 'revisionID = %d AND creationMethod != %s ORDER BY id DESC LIMIT 1', $diff->getRevisionID(), 'commit'); foreach ($vs_diff->loadChangesets() as $changeset) { $path = $changeset->getAbsoluteRepositoryPath($repository, $vs_diff); $path = ltrim($path, '/'); $vs_changesets[$path] = $changeset; } $changesets = array(); foreach ($diff->getChangesets() as $changeset) { $path = $changeset->getAbsoluteRepositoryPath($repository, $diff); $path = ltrim($path, '/'); $changesets[$path] = $changeset; } if (array_fill_keys(array_keys($changesets), true) != array_fill_keys(array_keys($vs_changesets), true)) { return $vs_diff; } $hunks = id(new DifferentialHunk())->loadAllWhere( 'changesetID IN (%Ld)', mpull($vs_changesets, 'getID')); $hunks = mgroup($hunks, 'getChangesetID'); foreach ($vs_changesets as $changeset) { $changeset->attachHunks(idx($hunks, $changeset->getID(), array())); } $file_phids = array(); foreach ($vs_changesets as $changeset) { $metadata = $changeset->getMetadata(); $file_phid = idx($metadata, 'new:binary-phid'); if ($file_phid) { $file_phids[$file_phid] = $file_phid; } } $files = array(); if ($file_phids) { $files = id(new PhabricatorFile())->loadAllWhere( 'phid IN (%Ls)', $file_phids); $files = mpull($files, null, 'getPHID'); } foreach ($changesets as $path => $changeset) { $vs_changeset = $vs_changesets[$path]; $file_phid = idx($vs_changeset->getMetadata(), 'new:binary-phid'); if ($file_phid) { if (!isset($files[$file_phid])) { return $vs_diff; } $drequest = DiffusionRequest::newFromDictionary(array( 'repository' => $this->repository, 'commit' => $this->commit->getCommitIdentifier(), 'path' => $path, )); $corpus = DiffusionFileContentQuery::newFromDiffusionRequest($drequest) ->loadFileContent() ->getCorpus(); if ($files[$file_phid]->loadFileData() != $corpus) { return $vs_diff; } } else { $context = implode("\n", $changeset->makeChangesWithContext()); $vs_context = implode("\n", $vs_changeset->makeChangesWithContext()); // We couldn't just compare $context and $vs_context because following // diffs will be considered different: // // -(empty line) // -echo 'test'; // (empty line) // // (empty line) // -echo "test"; // -(empty line) $hunk = id(new DifferentialHunk())->setChanges($context); $vs_hunk = id(new DifferentialHunk())->setChanges($vs_context); if ($hunk->makeOldFile() != $vs_hunk->makeOldFile() || $hunk->makeNewFile() != $vs_hunk->makeNewFile()) { return $vs_diff; } } } return null; } /** * When querying for revisions by hash, more than one revision may be found. * This function identifies the "best" revision from such a set. Typically, * there is only one revision found. Otherwise, we try to pick an accepted * revision first, followed by an open revision, and otherwise we go with a * closed or abandoned revision as a last resort. */ private function identifyBestRevision(array $revisions) { assert_instances_of($revisions, 'DifferentialRevision'); // get the simplest, common case out of the way if (count($revisions) == 1) { return reset($revisions); } $first_choice = array(); $second_choice = array(); $third_choice = array(); foreach ($revisions as $revision) { switch ($revision->getStatus()) { // "Accepted" revisions -- ostensibly what we're looking for! case ArcanistDifferentialRevisionStatus::ACCEPTED: $first_choice[] = $revision; break; // "Open" revisions case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW: case ArcanistDifferentialRevisionStatus::NEEDS_REVISION: $second_choice[] = $revision; break; // default is a wtf? here default: case ArcanistDifferentialRevisionStatus::ABANDONED: case ArcanistDifferentialRevisionStatus::CLOSED: $third_choice[] = $revision; break; } } // go down the ladder like a bro at last call if (!empty($first_choice)) { return $this->identifyMostRecentRevision($first_choice); } if (!empty($second_choice)) { return $this->identifyMostRecentRevision($second_choice); } if (!empty($third_choice)) { return $this->identifyMostRecentRevision($third_choice); } } /** * Given a set of revisions, returns the revision with the latest * updated time. This is ostensibly the most recent revision. */ private function identifyMostRecentRevision(array $revisions) { assert_instances_of($revisions, 'DifferentialRevision'); $revisions = msort($revisions, 'getDateModified'); return end($revisions); } /** * Emit an event so installs can do custom lookup of commit authors who may * not be naturally resolvable. */ private function lookupUser( PhabricatorRepositoryCommit $commit, $query, $guess) { $type = PhabricatorEventType::TYPE_DIFFUSION_LOOKUPUSER; $data = array( 'commit' => $commit, 'query' => $query, 'result' => $guess, ); $event = new PhabricatorEvent($type, $data); PhutilEventEngine::dispatchEvent($event); return $event->getValue('result'); } } diff --git a/src/applications/search/controller/PhabricatorSearchAttachController.php b/src/applications/search/controller/PhabricatorSearchAttachController.php index 1237c0d898..32b5b87ec2 100644 --- a/src/applications/search/controller/PhabricatorSearchAttachController.php +++ b/src/applications/search/controller/PhabricatorSearchAttachController.php @@ -1,292 +1,293 @@ phid = $data['phid']; $this->type = $data['type']; $this->action = idx($data, 'action', self::ACTION_ATTACH); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $handle_data = new PhabricatorObjectHandleData(array($this->phid)); $handles = $handle_data->loadHandles(); $handle = $handles[$this->phid]; $object_type = $handle->getType(); $attach_type = $this->type; $objects = $handle_data->loadObjects(); $object = idx($objects, $this->phid); if (!$object) { return new Aphront404Response(); } $edge_type = null; switch ($this->action) { case self::ACTION_EDGE: case self::ACTION_DEPENDENCIES: case self::ACTION_ATTACH: $edge_type = $this->getEdgeType($object_type, $attach_type); break; } if ($request->isFormPost()) { $phids = explode(';', $request->getStr('phids')); $phids = array_filter($phids); $phids = array_values($phids); if ($edge_type) { $old_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $this->phid, $edge_type); $add_phids = $phids; $rem_phids = array_diff($old_phids, $add_phids); $editor = id(new PhabricatorEdgeEditor()); - $editor->setUser($user); + $editor->setActor($user); foreach ($add_phids as $phid) { $editor->addEdge($this->phid, $edge_type, $phid); } foreach ($rem_phids as $phid) { $editor->removeEdge($this->phid, $edge_type, $phid); } try { $editor->save(); } catch (PhabricatorEdgeCycleException $ex) { $this->raiseGraphCycleException($ex); } return id(new AphrontReloadResponse())->setURI($handle->getURI()); } else { return $this->performMerge($object, $handle, $phids); } } else { if ($edge_type) { $phids = PhabricatorEdgeQuery::loadDestinationPHIDs( $this->phid, $edge_type); } else { // This is a merge. $phids = array(); } } $strings = $this->getStrings(); $handles = $this->loadViewerHandles($phids); $obj_dialog = new PhabricatorObjectSelectorDialog(); $obj_dialog ->setUser($user) ->setHandles($handles) ->setFilters(array( 'assigned' => 'Assigned to Me', 'created' => 'Created By Me', 'open' => 'All Open '.$strings['target_plural_noun'], 'all' => 'All '.$strings['target_plural_noun'], )) ->setSelectedFilter($strings['selected']) ->setExcluded($this->phid) ->setCancelURI($handle->getURI()) ->setSearchURI('/search/select/'.$attach_type.'/') ->setTitle($strings['title']) ->setHeader($strings['header']) ->setButtonText($strings['button']) ->setInstructions($strings['instructions']); $dialog = $obj_dialog->buildDialog(); return id(new AphrontDialogResponse())->setDialog($dialog); } private function performMerge( ManiphestTask $task, PhabricatorObjectHandle $handle, array $phids) { $user = $this->getRequest()->getUser(); $response = id(new AphrontReloadResponse())->setURI($handle->getURI()); $phids = array_fill_keys($phids, true); unset($phids[$task->getPHID()]); // Prevent merging a task into itself. if (!$phids) { return $response; } $targets = id(new ManiphestTask())->loadAllWhere( 'phid in (%Ls) ORDER BY id ASC', array_keys($phids)); if (empty($targets)) { return $response; } $editor = new ManiphestTransactionEditor(); + $editor->setActor($user); $task_names = array(); $merge_into_name = 'T'.$task->getID(); $cc_vector = array(); $cc_vector[] = $task->getCCPHIDs(); foreach ($targets as $target) { $cc_vector[] = $target->getCCPHIDs(); $cc_vector[] = array( $target->getAuthorPHID(), $target->getOwnerPHID()); $close_task = id(new ManiphestTransaction()) ->setAuthorPHID($user->getPHID()) ->setTransactionType(ManiphestTransactionType::TYPE_STATUS) ->setNewValue(ManiphestTaskStatus::STATUS_CLOSED_DUPLICATE) ->setComments("\xE2\x9C\x98 Merged into {$merge_into_name}."); $editor->applyTransactions($target, array($close_task)); $task_names[] = 'T'.$target->getID(); } $all_ccs = array_mergev($cc_vector); $all_ccs = array_filter($all_ccs); $all_ccs = array_unique($all_ccs); $task_names = implode(', ', $task_names); $add_ccs = id(new ManiphestTransaction()) ->setAuthorPHID($user->getPHID()) ->setTransactionType(ManiphestTransactionType::TYPE_CCS) ->setNewValue($all_ccs) ->setComments("\xE2\x97\x80 Merged tasks: {$task_names}."); $editor->applyTransactions($task, array($add_ccs)); return $response; } private function getStrings() { switch ($this->type) { case PhabricatorPHIDConstants::PHID_TYPE_DREV: $noun = 'Revisions'; $selected = 'created'; break; case PhabricatorPHIDConstants::PHID_TYPE_TASK: $noun = 'Tasks'; $selected = 'assigned'; break; case PhabricatorPHIDConstants::PHID_TYPE_CMIT: $noun = 'Commits'; $selected = 'created'; break; } switch ($this->action) { case self::ACTION_EDGE: case self::ACTION_ATTACH: $dialog_title = "Manage Attached {$noun}"; $header_text = "Currently Attached {$noun}"; $button_text = "Save {$noun}"; $instructions = null; break; case self::ACTION_MERGE: $dialog_title = "Merge Duplicate Tasks"; $header_text = "Tasks To Merge"; $button_text = "Merge {$noun}"; $instructions = "These tasks will be merged into the current task and then closed. ". "The current task will grow stronger."; break; case self::ACTION_DEPENDENCIES: $dialog_title = "Edit Dependencies"; $header_text = "Current Dependencies"; $button_text = "Save Dependencies"; $instructions = null; break; } return array( 'target_plural_noun' => $noun, 'selected' => $selected, 'title' => $dialog_title, 'header' => $header_text, 'button' => $button_text, 'instructions' => $instructions, ); } private function getEdgeType($src_type, $dst_type) { $t_cmit = PhabricatorPHIDConstants::PHID_TYPE_CMIT; $t_task = PhabricatorPHIDConstants::PHID_TYPE_TASK; $t_drev = PhabricatorPHIDConstants::PHID_TYPE_DREV; $map = array( $t_cmit => array( $t_task => PhabricatorEdgeConfig::TYPE_COMMIT_HAS_TASK, ), $t_task => array( $t_cmit => PhabricatorEdgeConfig::TYPE_TASK_HAS_COMMIT, $t_task => PhabricatorEdgeConfig::TYPE_TASK_DEPENDS_ON_TASK, $t_drev => PhabricatorEdgeConfig::TYPE_TASK_HAS_RELATED_DREV, ), $t_drev => array( $t_drev => PhabricatorEdgeConfig::TYPE_DREV_DEPENDS_ON_DREV, $t_task => PhabricatorEdgeConfig::TYPE_DREV_HAS_RELATED_TASK, ), ); if (empty($map[$src_type][$dst_type])) { return null; } return $map[$src_type][$dst_type]; } private function raiseGraphCycleException(PhabricatorEdgeCycleException $ex) { $cycle = $ex->getCycle(); $handles = $this->loadViewerHandles($cycle); $names = array(); foreach ($cycle as $cycle_phid) { $names[] = $handles[$cycle_phid]->getFullName(); } $names = implode(" \xE2\x86\x92 ", $names); throw new Exception( "You can not create that dependency, because it would create a ". "circular dependency: {$names}."); } } diff --git a/src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php b/src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php index bcfe19a368..78078e2079 100644 --- a/src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php +++ b/src/applications/subscriptions/controller/PhabricatorSubscriptionsEditController.php @@ -1,106 +1,106 @@ phid = idx($data, 'phid'); $this->action = idx($data, 'action'); } public function processRequest() { $request = $this->getRequest(); if (!$request->isFormPost()) { return new Aphront400Response(); } switch ($this->action) { case 'add': $is_add = true; break; case 'delete': $is_add = false; break; default: return new Aphront400Response(); } $user = $request->getUser(); $phid = $this->phid; // TODO: This is a policy test because `loadObjects()` is not currently // policy-aware. Once it is, we can collapse this. $handle = PhabricatorObjectHandleData::loadOneHandle($phid, $user); if (!$handle->isComplete()) { return new Aphront404Response(); } $objects = id(new PhabricatorObjectHandleData(array($phid))) ->loadObjects(); $object = idx($objects, $phid); if (!($object instanceof PhabricatorSubscribableInterface)) { return $this->buildErrorResponse( pht('Bad Object'), pht('This object is not subscribable.'), $handle->getURI()); } if ($object->isAutomaticallySubscribed($user->getPHID())) { return $this->buildErrorResponse( pht('Automatically Subscribed'), pht('You are automatically subscribed to this object.'), $handle->getURI()); } $editor = id(new PhabricatorSubscriptionsEditor()) - ->setUser($user) + ->setActor($user) ->setObject($object); if ($is_add) { $editor->subscribeExplicit(array($user->getPHID()), $explicit = true); } else { $editor->unsubscribe(array($user->getPHID())); } $editor->save(); // TODO: We should just render the "Unsubscribe" action and swap it out // in the document for Ajax requests. return id(new AphrontReloadResponse())->setURI($handle->getURI()); } private function buildErrorResponse($title, $message, $uri) { $request = $this->getRequest(); $user = $request->getUser(); $dialog = id(new AphrontDialogView()) ->setUser($user) ->setTitle($title) ->appendChild($message) ->addCancelButton($uri); return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php b/src/applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php index 8b01034fbf..37329f7940 100644 --- a/src/applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php +++ b/src/applications/subscriptions/editor/PhabricatorSubscriptionsEditor.php @@ -1,128 +1,119 @@ object = $object; return $this; } - public function setUser(PhabricatorUser $user) { - $this->user = $user; - return $this; - } - - /** * Add explicit subscribers. These subscribers have explicitly subscribed * (or been subscribed) to the object, and will be added even if they * had previously unsubscribed. * * @param list List of PHIDs to explicitly subscribe. * @return this */ public function subscribeExplicit(array $phids) { $this->explicitSubscribePHIDs += array_fill_keys($phids, true); return $this; } /** * Add implicit subscribers. These subscribers have taken some action which * implicitly subscribes them (e.g., adding a comment) but it will be * suppressed if they've previously unsubscribed from the object. * * @param list List of PHIDs to implicitly subscribe. * @return this */ public function subscribeImplicit(array $phids) { $this->implicitSubscribePHIDs += array_fill_keys($phids, true); return $this; } /** * Unsubscribe PHIDs and mark them as unsubscribed, so implicit subscriptions * will not resubscribe them. * * @param list List of PHIDs to unsubscribe. * @return this */ public function unsubscribe(array $phids) { $this->unsubscribePHIDs += array_fill_keys($phids, true); return $this; } public function save() { if (!$this->object) { throw new Exception('Call setObject() before save()!'); } - if (!$this->user) { - throw new Exception('Call setUser() before save()!'); - } + $actor = $this->requireActor(); $src = $this->object->getPHID(); if ($this->implicitSubscribePHIDs) { $unsub = PhabricatorEdgeQuery::loadDestinationPHIDs( $src, PhabricatorEdgeConfig::TYPE_OBJECT_HAS_UNSUBSCRIBER); $unsub = array_fill_keys($unsub, true); $this->implicitSubscribePHIDs = array_diff_key( $this->implicitSubscribePHIDs, $unsub); } $add = $this->implicitSubscribePHIDs + $this->explicitSubscribePHIDs; $del = $this->unsubscribePHIDs; // If a PHID is marked for both subscription and unsubscription, treat // unsubscription as the stronger action. $add = array_diff_key($add, $del); if ($add || $del) { $u_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_UNSUBSCRIBER; $s_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_SUBSCRIBER; $editor = id(new PhabricatorEdgeEditor()) - ->setUser($this->user); + ->setActor($actor); foreach ($add as $phid => $ignored) { $editor->removeEdge($src, $u_type, $phid); $editor->addEdge($src, $s_type, $phid); } foreach ($del as $phid => $ignored) { $editor->removeEdge($src, $s_type, $phid); $editor->addEdge($src, $u_type, $phid); } $editor->save(); } } } diff --git a/src/infrastructure/PhabricatorEditor.php b/src/infrastructure/PhabricatorEditor.php new file mode 100644 index 0000000000..df6e901541 --- /dev/null +++ b/src/infrastructure/PhabricatorEditor.php @@ -0,0 +1,47 @@ +user = $user; + return $this; + } + final protected function getActor() { + return $this->user; + } + final protected function requireActor() { + $actor = $this->getActor(); + if (!$actor) { + throw new Exception('You must setActor()!'); + } + return $actor; + } + + final public function setExcludeMailRecipientPHIDs($phids) { + $this->excludeMailRecipientPHIDs = $phids; + return $this; + } + final protected function getExcludeMailRecipientPHIDs() { + return $this->excludeMailRecipientPHIDs; + } + +} diff --git a/src/infrastructure/edges/__tests__/PhabricatorEdgeTestCase.php b/src/infrastructure/edges/__tests__/PhabricatorEdgeTestCase.php index ac07e3aaeb..0327b29337 100644 --- a/src/infrastructure/edges/__tests__/PhabricatorEdgeTestCase.php +++ b/src/infrastructure/edges/__tests__/PhabricatorEdgeTestCase.php @@ -1,81 +1,81 @@ true, ); } public function testCycleDetection() { // The editor should detect that this introduces a cycle and prevent the // edit. $user = new PhabricatorUser(); $obj1 = id(new HarbormasterObject())->save(); $obj2 = id(new HarbormasterObject())->save(); $phid1 = $obj1->getPHID(); $phid2 = $obj2->getPHID(); $editor = id(new PhabricatorEdgeEditor()) - ->setUser($user) + ->setActor($user) ->addEdge($phid1, PhabricatorEdgeConfig::TYPE_TEST_NO_CYCLE, $phid2) ->addEdge($phid2, PhabricatorEdgeConfig::TYPE_TEST_NO_CYCLE, $phid1); $caught = null; try { $editor->save(); } catch (Exception $ex) { $caught = $ex; } $this->assertEqual( true, $caught instanceof Exception); // The first edit should go through (no cycle), bu the second one should // fail (it introduces a cycle). $editor = id(new PhabricatorEdgeEditor()) - ->setUser($user) + ->setActor($user) ->addEdge($phid1, PhabricatorEdgeConfig::TYPE_TEST_NO_CYCLE, $phid2) ->save(); $editor = id(new PhabricatorEdgeEditor()) - ->setUser($user) + ->setActor($user) ->addEdge($phid2, PhabricatorEdgeConfig::TYPE_TEST_NO_CYCLE, $phid1); $caught = null; try { $editor->save(); } catch (Exception $ex) { $caught = $ex; } $this->assertEqual( true, $caught instanceof Exception); } } diff --git a/src/infrastructure/edges/editor/PhabricatorEdgeEditor.php b/src/infrastructure/edges/editor/PhabricatorEdgeEditor.php index 2287ca7edc..0428b4091b 100644 --- a/src/infrastructure/edges/editor/PhabricatorEdgeEditor.php +++ b/src/infrastructure/edges/editor/PhabricatorEdgeEditor.php @@ -1,460 +1,454 @@ addEdge($src, $type, $dst) - * ->setUser($user) + * ->setActor($user) * ->save(); * * @task edit Editing Edges * @task cycles Cycle Prevention * @task internal Internals */ -final class PhabricatorEdgeEditor { +final class PhabricatorEdgeEditor extends PhabricatorEditor { private $addEdges = array(); private $remEdges = array(); private $openTransactions = array(); - private $user; private $suppressEvents; - public function setUser(PhabricatorUser $user) { - $this->user = $user; - return $this; - } - /* -( Editing Edges )------------------------------------------------------ */ /** * Add a new edge (possibly also adding its inverse). Changes take effect when * you call @{method:save}. If the edge already exists, it will not be * overwritten. Removals queued with @{method:removeEdge} are executed before * adds, so the effect of removing and adding the same edge is to overwrite * any existing edge. * * The `$options` parameter accepts these values: * * - `data` Optional, data to write onto the edge. * - `inverse_data` Optional, data to write on the inverse edge. If not * provided, `data` will be written. * * @param phid Source object PHID. * @param const Edge type constant. * @param phid Destination object PHID. * @param map Options map (see documentation). * @return this * * @task edit */ public function addEdge($src, $type, $dst, array $options = array()) { foreach ($this->buildEdgeSpecs($src, $type, $dst, $options) as $spec) { $this->addEdges[] = $spec; } return $this; } /** * Remove an edge (possibly also removing its inverse). Changes take effect * when you call @{method:save}. If an edge does not exist, the removal * will be ignored. Edges are added after edges are removed, so the effect of * a remove plus an add is to overwrite. * * @param phid Source object PHID. * @param const Edge type constant. * @param phid Destination object PHID. * @return this * * @task edit */ public function removeEdge($src, $type, $dst) { foreach ($this->buildEdgeSpecs($src, $type, $dst) as $spec) { $this->remEdges[] = $spec; } return $this; } /** * Apply edge additions and removals queued by @{method:addEdge} and * @{method:removeEdge}. Note that transactions are opened, all additions and * removals are executed, and then transactions are saved. Thus, in some cases * it may be slightly more efficient to perform multiple edit operations * (e.g., adds followed by removals) if their outcomes are not dependent, * since transactions will not be held open as long. * * @return this * @task edit */ public function save() { $cycle_types = $this->getPreventCyclesEdgeTypes(); $locks = array(); $caught = null; try { // NOTE: We write edge data first, before doing any transactions, since // it's OK if we just leave it hanging out in space unattached to // anything. $this->writeEdgeData(); // If we're going to perform cycle detection, lock the edge type before // doing edits. if ($cycle_types) { $src_phids = ipull($this->addEdges, 'src'); foreach ($cycle_types as $cycle_type) { $key = 'edge.cycle:'.$cycle_type; $locks[] = PhabricatorGlobalLock::newLock($key)->lock(15); } } static $id = 0; $id++; $this->sendEvent($id, PhabricatorEventType::TYPE_EDGE_WILLEDITEDGES); // NOTE: Removes first, then adds, so that "remove + add" is a useful // operation meaning "overwrite". $this->executeRemoves(); $this->executeAdds(); foreach ($cycle_types as $cycle_type) { $this->detectCycles($src_phids, $cycle_type); } $this->sendEvent($id, PhabricatorEventType::TYPE_EDGE_DIDEDITEDGES); $this->saveTransactions(); } catch (Exception $ex) { $caught = $ex; } if ($caught) { $this->killTransactions(); } foreach ($locks as $lock) { $lock->unlock(); } if ($caught) { throw $caught; } } /* -( Internals )---------------------------------------------------------- */ /** * Build the specification for an edge operation, and possibly build its * inverse as well. * * @task internal */ private function buildEdgeSpecs($src, $type, $dst, array $options = array()) { $data = array(); if (!empty($options['data'])) { $data['data'] = $options['data']; } $src_type = phid_get_type($src); $dst_type = phid_get_type($dst); $specs = array(); $specs[] = array( 'src' => $src, 'src_type' => $src_type, 'dst' => $dst, 'dst_type' => $dst_type, 'type' => $type, 'data' => $data, ); $inverse = PhabricatorEdgeConfig::getInverse($type); if ($inverse) { // If `inverse_data` is set, overwrite the edge data. Normally, just // write the same data to the inverse edge. if (array_key_exists('inverse_data', $options)) { $data['data'] = $options['inverse_data']; } $specs[] = array( 'src' => $dst, 'src_type' => $dst_type, 'dst' => $src, 'dst_type' => $src_type, 'type' => $inverse, 'data' => $data, ); } return $specs; } /** * Write edge data. * * @task internal */ private function writeEdgeData() { $adds = $this->addEdges; $writes = array(); foreach ($adds as $key => $edge) { if ($edge['data']) { $writes[] = array($key, $edge['src_type'], json_encode($edge['data'])); } } foreach ($writes as $write) { list($key, $src_type, $data) = $write; $conn_w = PhabricatorEdgeConfig::establishConnection($src_type, 'w'); queryfx( $conn_w, 'INSERT INTO %T (data) VALUES (%s)', PhabricatorEdgeConfig::TABLE_NAME_EDGEDATA, $data); $this->addEdges[$key]['data_id'] = $conn_w->getInsertID(); } } /** * Add queued edges. * * @task internal */ private function executeAdds() { $adds = $this->addEdges; $adds = igroup($adds, 'src_type'); // Assign stable sequence numbers to each edge, so we have a consistent // ordering across edges by source and type. foreach ($adds as $src_type => $edges) { $edges_by_src = igroup($edges, 'src'); foreach ($edges_by_src as $src => $src_edges) { $seq = 0; foreach ($src_edges as $key => $edge) { $src_edges[$key]['seq'] = $seq++; $src_edges[$key]['dateCreated'] = time(); } $edges_by_src[$src] = $src_edges; } $adds[$src_type] = array_mergev($edges_by_src); } $inserts = array(); foreach ($adds as $src_type => $edges) { $conn_w = PhabricatorEdgeConfig::establishConnection($src_type, 'w'); $sql = array(); foreach ($edges as $edge) { $sql[] = qsprintf( $conn_w, '(%s, %d, %s, %d, %d, %nd)', $edge['src'], $edge['type'], $edge['dst'], $edge['dateCreated'], $edge['seq'], idx($edge, 'data_id')); } $inserts[] = array($conn_w, $sql); } foreach ($inserts as $insert) { list($conn_w, $sql) = $insert; $conn_w->openTransaction(); $this->openTransactions[] = $conn_w; foreach (array_chunk($sql, 256) as $chunk) { queryfx( $conn_w, 'INSERT IGNORE INTO %T (src, type, dst, dateCreated, seq, dataID) VALUES %Q', PhabricatorEdgeConfig::TABLE_NAME_EDGE, implode(', ', $chunk)); } } } /** * Remove queued edges. * * @task internal */ private function executeRemoves() { $rems = $this->remEdges; $rems = igroup($rems, 'src_type'); $deletes = array(); foreach ($rems as $src_type => $edges) { $conn_w = PhabricatorEdgeConfig::establishConnection($src_type, 'w'); $sql = array(); foreach ($edges as $edge) { $sql[] = qsprintf( $conn_w, '(%s, %d, %s)', $edge['src'], $edge['type'], $edge['dst']); } $deletes[] = array($conn_w, $sql); } foreach ($deletes as $delete) { list($conn_w, $sql) = $delete; $conn_w->openTransaction(); $this->openTransactions[] = $conn_w; foreach (array_chunk($sql, 256) as $chunk) { queryfx( $conn_w, 'DELETE FROM %T WHERE (src, type, dst) IN (%Q)', PhabricatorEdgeConfig::TABLE_NAME_EDGE, implode(', ', $chunk)); } } } /** * Save open transactions. * * @task internal */ private function saveTransactions() { foreach ($this->openTransactions as $key => $conn_w) { $conn_w->saveTransaction(); unset($this->openTransactions[$key]); } } private function killTransactions() { foreach ($this->openTransactions as $key => $conn_w) { $conn_w->killTransaction(); unset($this->openTransactions[$key]); } } /** * Suppress edge edit events. This prevents listeners from making updates in * response to edits, and is primarily useful when performing migrations. You * should not normally need to use it. * * @param bool True to supress events related to edits. * @return this * @task internal */ public function setSuppressEvents($suppress) { $this->suppressEvents = $suppress; return $this; } private function sendEvent($edit_id, $event_type) { if ($this->suppressEvents) { return; } $event = new PhabricatorEvent( $event_type, array( 'id' => $edit_id, 'add' => $this->addEdges, 'rem' => $this->remEdges, )); - $event->setUser($this->user); + $event->setUser($this->getActor()); PhutilEventEngine::dispatchEvent($event); } /* -( Cycle Prevention )--------------------------------------------------- */ /** * Get a list of all edge types which are being added, and which we should * prevent cycles on. * * @return list List of edge types which should have cycles prevented. * @task cycle */ private function getPreventCyclesEdgeTypes() { $edge_types = array(); foreach ($this->addEdges as $edge) { $edge_types[$edge['type']] = true; } foreach ($edge_types as $type => $ignored) { if (!PhabricatorEdgeConfig::shouldPreventCycles($type)) { unset($edge_types[$type]); } } return array_keys($edge_types); } /** * Detect graph cycles of a given edge type. If the edit introduces a cycle, * a @{class:PhabricatorEdgeCycleException} is thrown with details. * * @return void * @task cycle */ private function detectCycles(array $phids, $edge_type) { // For simplicity, we just seed the graph with the affected nodes rather // than seeding it with their edges. To do this, we just add synthetic // edges from an imaginary '' node to the known edges. $graph = id(new PhabricatorEdgeGraph()) ->setEdgeType($edge_type) ->addNodes( array( '' => $phids, )) ->loadGraph(); foreach ($phids as $phid) { $cycle = $graph->detectCycles($phid); if ($cycle) { throw new PhabricatorEdgeCycleException($edge_type, $cycle); } } } }