diff --git a/resources/celerity/map.php b/resources/celerity/map.php index d710846c2c..70c10b1d3c 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -9,7 +9,7 @@ return array( 'names' => array( 'conpherence.pkg.css' => '3c8a0668', 'conpherence.pkg.js' => '020aebcf', - 'core.pkg.css' => 'eef4903d', + 'core.pkg.css' => '242f9ce6', 'core.pkg.js' => '73a06a9f', 'differential.pkg.css' => '8d8360fb', 'differential.pkg.js' => '0b037a4f', @@ -141,7 +141,7 @@ return array( 'rsrc/css/phui/phui-big-info-view.css' => '362ad37b', 'rsrc/css/phui/phui-box.css' => '5ed3b8cb', 'rsrc/css/phui/phui-bulk-editor.css' => '374d5e30', - 'rsrc/css/phui/phui-chart.css' => '10135a9d', + 'rsrc/css/phui/phui-chart.css' => '14df9ae3', 'rsrc/css/phui/phui-cms.css' => '8c05c41e', 'rsrc/css/phui/phui-comment-form.css' => '68a2d99a', 'rsrc/css/phui/phui-comment-panel.css' => 'ec4e31c0', @@ -155,7 +155,7 @@ return array( 'rsrc/css/phui/phui-form-view.css' => 'a8e0a1ab', 'rsrc/css/phui/phui-form.css' => '159e2d9c', 'rsrc/css/phui/phui-head-thing.css' => 'd7f293df', - 'rsrc/css/phui/phui-header-view.css' => '285c9139', + 'rsrc/css/phui/phui-header-view.css' => 'b500eeea', 'rsrc/css/phui/phui-hovercard.css' => '6ca90fa0', 'rsrc/css/phui/phui-icon-set-selector.css' => '7aa5f3ec', 'rsrc/css/phui/phui-icon.css' => '4cbc684a', @@ -168,6 +168,7 @@ return array( 'rsrc/css/phui/phui-object-box.css' => 'f434b6be', 'rsrc/css/phui/phui-pager.css' => 'd022c7ad', 'rsrc/css/phui/phui-pinboard-view.css' => '1f08f5d8', + 'rsrc/css/phui/phui-policy-section-view.css' => '139fdc64', 'rsrc/css/phui/phui-property-list-view.css' => 'cad62236', 'rsrc/css/phui/phui-remarkup-preview.css' => '91767007', 'rsrc/css/phui/phui-segment-bar-view.css' => '5166b370', @@ -178,7 +179,7 @@ return array( 'rsrc/css/phui/phui-two-column-view.css' => '01e6991e', 'rsrc/css/phui/workboards/phui-workboard-color.css' => 'e86de308', 'rsrc/css/phui/workboards/phui-workboard.css' => '74fc9d98', - 'rsrc/css/phui/workboards/phui-workcard.css' => '9e9eb0df', + 'rsrc/css/phui/workboards/phui-workcard.css' => '913441b6', 'rsrc/css/phui/workboards/phui-workpanel.css' => '3ae89b20', 'rsrc/css/sprite-login.css' => '18b368a6', 'rsrc/css/sprite-tokens.css' => 'f1896dc5', @@ -391,14 +392,14 @@ return array( 'rsrc/js/application/diffusion/behavior-pull-lastmodified.js' => 'c715c123', 'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => '6a85bc5a', 'rsrc/js/application/drydock/drydock-live-operation-status.js' => '47a0728b', - 'rsrc/js/application/fact/Chart.js' => 'eec96de0', + 'rsrc/js/application/fact/Chart.js' => '52e3ff03', 'rsrc/js/application/fact/ChartCurtainView.js' => '86954222', 'rsrc/js/application/fact/ChartFunctionLabel.js' => '81de1dab', 'rsrc/js/application/files/behavior-document-engine.js' => '243d6c22', 'rsrc/js/application/files/behavior-icon-composer.js' => '38a6cedb', 'rsrc/js/application/files/behavior-launch-icon-composer.js' => 'a17b84f1', 'rsrc/js/application/harbormaster/behavior-harbormaster-log.js' => 'b347a301', - 'rsrc/js/application/herald/HeraldRuleEditor.js' => '27daef73', + 'rsrc/js/application/herald/HeraldRuleEditor.js' => '2633bef7', 'rsrc/js/application/herald/PathTypeahead.js' => 'ad486db3', 'rsrc/js/application/herald/herald-rule-editor.js' => '0922e81d', 'rsrc/js/application/maniphest/behavior-batch-selector.js' => '139ef688', @@ -572,7 +573,7 @@ return array( 'global-drag-and-drop-css' => '1d2713a4', 'harbormaster-css' => '8dfe16b2', 'herald-css' => '648d39e2', - 'herald-rule-editor' => '27daef73', + 'herald-rule-editor' => '2633bef7', 'herald-test-css' => 'e004176f', 'inline-comment-summary-css' => '81eb368d', 'javelin-aphlict' => '022516b4', @@ -700,7 +701,7 @@ return array( 'javelin-behavior-user-menu' => '60cd9241', 'javelin-behavior-view-placeholder' => 'a9942052', 'javelin-behavior-workflow' => '9623adc1', - 'javelin-chart' => 'eec96de0', + 'javelin-chart' => '52e3ff03', 'javelin-chart-curtain-view' => '86954222', 'javelin-chart-function-label' => '81de1dab', 'javelin-color' => '78f811c9', @@ -830,7 +831,7 @@ return array( 'phui-calendar-day-css' => '9597d706', 'phui-calendar-list-css' => 'ccd7e4e2', 'phui-calendar-month-css' => 'cb758c42', - 'phui-chart-css' => '10135a9d', + 'phui-chart-css' => '14df9ae3', 'phui-cms-css' => '8c05c41e', 'phui-comment-form-css' => '68a2d99a', 'phui-comment-panel-css' => 'ec4e31c0', @@ -845,7 +846,7 @@ return array( 'phui-form-css' => '159e2d9c', 'phui-form-view-css' => 'a8e0a1ab', 'phui-head-thing-view-css' => 'd7f293df', - 'phui-header-view-css' => '285c9139', + 'phui-header-view-css' => 'b500eeea', 'phui-hovercard' => '074f0783', 'phui-hovercard-view-css' => '6ca90fa0', 'phui-icon-set-selector-css' => '7aa5f3ec', @@ -866,6 +867,7 @@ return array( 'phui-oi-simple-ui-css' => '6a30fa46', 'phui-pager-css' => 'd022c7ad', 'phui-pinboard-view-css' => '1f08f5d8', + 'phui-policy-section-view-css' => '139fdc64', 'phui-property-list-view-css' => 'cad62236', 'phui-remarkup-preview-css' => '91767007', 'phui-segment-bar-view-css' => '5166b370', @@ -877,7 +879,7 @@ return array( 'phui-two-column-view-css' => '01e6991e', 'phui-workboard-color-css' => 'e86de308', 'phui-workboard-view-css' => '74fc9d98', - 'phui-workcard-view-css' => '9e9eb0df', + 'phui-workcard-view-css' => '913441b6', 'phui-workpanel-view-css' => '3ae89b20', 'phuix-action-list-view' => 'c68f183f', 'phuix-action-view' => 'aaa08f3b', @@ -1115,7 +1117,7 @@ return array( 'javelin-json', 'phabricator-draggable-list', ), - '27daef73' => array( + '2633bef7' => array( 'multirow-row-manager', 'javelin-install', 'javelin-util', @@ -1367,6 +1369,12 @@ return array( 'javelin-dom', 'javelin-fx', ), + '52e3ff03' => array( + 'phui-chart-css', + 'd3', + 'javelin-chart-curtain-view', + 'javelin-chart-function-label', + ), '541f81c3' => array( 'javelin-install', ), @@ -2128,12 +2136,6 @@ return array( 'phabricator-keyboard-shortcut', 'javelin-stratcom', ), - 'eec96de0' => array( - 'phui-chart-css', - 'd3', - 'javelin-chart-curtain-view', - 'javelin-chart-function-label', - ), 'ef836bf2' => array( 'javelin-behavior', 'javelin-dom', diff --git a/resources/sql/autopatches/20190909.herald.01.rebuild.php b/resources/sql/autopatches/20190909.herald.01.rebuild.php new file mode 100644 index 0000000000..a29b7d2f45 --- /dev/null +++ b/resources/sql/autopatches/20190909.herald.01.rebuild.php @@ -0,0 +1,3 @@ + 'applications/diffusion/ssh/DiffusionSSHWorkflow.php', 'DiffusionSearchQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionSearchQueryConduitAPIMethod.php', 'DiffusionServeController' => 'applications/diffusion/controller/DiffusionServeController.php', + 'DiffusionServiceRef' => 'applications/diffusion/ref/DiffusionServiceRef.php', 'DiffusionSetPasswordSettingsPanel' => 'applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php', 'DiffusionSetupException' => 'applications/diffusion/exception/DiffusionSetupException.php', 'DiffusionSourceHyperlinkEngineExtension' => 'applications/diffusion/engineextension/DiffusionSourceHyperlinkEngineExtension.php', @@ -3107,6 +3108,7 @@ phutil_register_library_map(array( 'PhabricatorDefaultRequestExceptionHandler' => 'aphront/handler/PhabricatorDefaultRequestExceptionHandler.php', 'PhabricatorDefaultSyntaxStyle' => 'infrastructure/syntax/PhabricatorDefaultSyntaxStyle.php', 'PhabricatorDefaultUnlockEngine' => 'applications/system/engine/PhabricatorDefaultUnlockEngine.php', + 'PhabricatorDemoChartEngine' => 'applications/fact/engine/PhabricatorDemoChartEngine.php', 'PhabricatorDestructibleCodex' => 'applications/system/codex/PhabricatorDestructibleCodex.php', 'PhabricatorDestructibleCodexInterface' => 'applications/system/interface/PhabricatorDestructibleCodexInterface.php', 'PhabricatorDestructibleInterface' => 'applications/system/interface/PhabricatorDestructibleInterface.php', @@ -3220,6 +3222,7 @@ phutil_register_library_map(array( 'PhabricatorEditEngineSettingsPanel' => 'applications/settings/panel/PhabricatorEditEngineSettingsPanel.php', 'PhabricatorEditEngineStaticCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineStaticCommentAction.php', 'PhabricatorEditEngineSubtype' => 'applications/transactions/editengine/PhabricatorEditEngineSubtype.php', + 'PhabricatorEditEngineSubtypeHeraldField' => 'applications/transactions/herald/PhabricatorEditEngineSubtypeHeraldField.php', 'PhabricatorEditEngineSubtypeInterface' => 'applications/transactions/editengine/PhabricatorEditEngineSubtypeInterface.php', 'PhabricatorEditEngineSubtypeMap' => 'applications/transactions/editengine/PhabricatorEditEngineSubtypeMap.php', 'PhabricatorEditEngineSubtypeTestCase' => 'applications/transactions/editengine/__tests__/PhabricatorEditEngineSubtypeTestCase.php', @@ -3441,8 +3444,10 @@ phutil_register_library_map(array( 'PhabricatorFlagDeleteController' => 'applications/flag/controller/PhabricatorFlagDeleteController.php', 'PhabricatorFlagDestructionEngineExtension' => 'applications/flag/engineextension/PhabricatorFlagDestructionEngineExtension.php', 'PhabricatorFlagEditController' => 'applications/flag/controller/PhabricatorFlagEditController.php', + 'PhabricatorFlagHeraldAction' => 'applications/flag/herald/PhabricatorFlagHeraldAction.php', 'PhabricatorFlagListController' => 'applications/flag/controller/PhabricatorFlagListController.php', 'PhabricatorFlagQuery' => 'applications/flag/query/PhabricatorFlagQuery.php', + 'PhabricatorFlagRemoveFlagHeraldAction' => 'applications/flag/herald/PhabricatorFlagRemoveFlagHeraldAction.php', 'PhabricatorFlagSearchEngine' => 'applications/flag/query/PhabricatorFlagSearchEngine.php', 'PhabricatorFlagSelectControl' => 'applications/flag/view/PhabricatorFlagSelectControl.php', 'PhabricatorFlaggableInterface' => 'applications/flag/interface/PhabricatorFlaggableInterface.php', @@ -4196,14 +4201,17 @@ phutil_register_library_map(array( 'PhabricatorPolicyManagementWorkflow' => 'applications/policy/management/PhabricatorPolicyManagementWorkflow.php', 'PhabricatorPolicyPHIDTypePolicy' => 'applications/policy/phid/PhabricatorPolicyPHIDTypePolicy.php', 'PhabricatorPolicyQuery' => 'applications/policy/query/PhabricatorPolicyQuery.php', + 'PhabricatorPolicyRef' => 'applications/policy/view/PhabricatorPolicyRef.php', 'PhabricatorPolicyRequestExceptionHandler' => 'aphront/handler/PhabricatorPolicyRequestExceptionHandler.php', 'PhabricatorPolicyRule' => 'applications/policy/rule/PhabricatorPolicyRule.php', + 'PhabricatorPolicyRulesView' => 'applications/policy/view/PhabricatorPolicyRulesView.php', 'PhabricatorPolicySearchEngineExtension' => 'applications/policy/engineextension/PhabricatorPolicySearchEngineExtension.php', 'PhabricatorPolicyStrengthConstants' => 'applications/policy/constants/PhabricatorPolicyStrengthConstants.php', 'PhabricatorPolicyTestCase' => 'applications/policy/__tests__/PhabricatorPolicyTestCase.php', 'PhabricatorPolicyTestObject' => 'applications/policy/__tests__/PhabricatorPolicyTestObject.php', 'PhabricatorPolicyType' => 'applications/policy/constants/PhabricatorPolicyType.php', 'PhabricatorPonderApplication' => 'applications/ponder/application/PhabricatorPonderApplication.php', + 'PhabricatorPreambleTestCase' => 'infrastructure/util/__tests__/PhabricatorPreambleTestCase.php', 'PhabricatorPrimaryEmailUserLogType' => 'applications/people/userlog/PhabricatorPrimaryEmailUserLogType.php', 'PhabricatorProfileMenuEditEngine' => 'applications/search/editor/PhabricatorProfileMenuEditEngine.php', 'PhabricatorProfileMenuEditor' => 'applications/search/editor/PhabricatorProfileMenuEditor.php', @@ -4220,6 +4228,7 @@ phutil_register_library_map(array( 'PhabricatorProfileMenuItemView' => 'applications/search/engine/PhabricatorProfileMenuItemView.php', 'PhabricatorProfileMenuItemViewList' => 'applications/search/engine/PhabricatorProfileMenuItemViewList.php', 'PhabricatorProject' => 'applications/project/storage/PhabricatorProject.php', + 'PhabricatorProjectActivityChartEngine' => 'applications/project/chart/PhabricatorProjectActivityChartEngine.php', 'PhabricatorProjectAddHeraldAction' => 'applications/project/herald/PhabricatorProjectAddHeraldAction.php', 'PhabricatorProjectApplication' => 'applications/project/application/PhabricatorProjectApplication.php', 'PhabricatorProjectArchiveController' => 'applications/project/controller/PhabricatorProjectArchiveController.php', @@ -4419,6 +4428,7 @@ phutil_register_library_map(array( 'PhabricatorProjectsWatchersSearchEngineAttachment' => 'applications/project/engineextension/PhabricatorProjectsWatchersSearchEngineAttachment.php', 'PhabricatorPronounSetting' => 'applications/settings/setting/PhabricatorPronounSetting.php', 'PhabricatorProtocolLog' => 'infrastructure/log/PhabricatorProtocolLog.php', + 'PhabricatorPureChartFunction' => 'applications/fact/chart/PhabricatorPureChartFunction.php', 'PhabricatorPygmentSetupCheck' => 'applications/config/check/PhabricatorPygmentSetupCheck.php', 'PhabricatorQuery' => 'infrastructure/query/PhabricatorQuery.php', 'PhabricatorQueryConstraint' => 'infrastructure/query/constraint/PhabricatorQueryConstraint.php', @@ -4657,6 +4667,7 @@ phutil_register_library_map(array( 'PhabricatorSearchScopeSetting' => 'applications/settings/setting/PhabricatorSearchScopeSetting.php', 'PhabricatorSearchSelectField' => 'applications/search/field/PhabricatorSearchSelectField.php', 'PhabricatorSearchService' => 'infrastructure/cluster/search/PhabricatorSearchService.php', + 'PhabricatorSearchSettingsPanel' => 'applications/settings/panel/PhabricatorSearchSettingsPanel.php', 'PhabricatorSearchStringListField' => 'applications/search/field/PhabricatorSearchStringListField.php', 'PhabricatorSearchSubscribersField' => 'applications/search/field/PhabricatorSearchSubscribersField.php', 'PhabricatorSearchTextField' => 'applications/search/field/PhabricatorSearchTextField.php', @@ -6974,6 +6985,7 @@ phutil_register_library_map(array( 'DiffusionSSHWorkflow' => 'PhabricatorSSHWorkflow', 'DiffusionSearchQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 'DiffusionServeController' => 'DiffusionController', + 'DiffusionServiceRef' => 'Phobject', 'DiffusionSetPasswordSettingsPanel' => 'PhabricatorSettingsPanel', 'DiffusionSetupException' => 'Exception', 'DiffusionSourceHyperlinkEngineExtension' => 'PhabricatorRemarkupHyperlinkEngineExtension', @@ -8299,7 +8311,7 @@ phutil_register_library_map(array( 'PhabricatorAccessLog' => 'Phobject', 'PhabricatorAccessLogConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorAccessibilitySetting' => 'PhabricatorSelectSetting', - 'PhabricatorAccumulateChartFunction' => 'PhabricatorChartFunction', + 'PhabricatorAccumulateChartFunction' => 'PhabricatorHigherOrderChartFunction', 'PhabricatorActionListView' => 'AphrontTagView', 'PhabricatorActionView' => 'AphrontView', 'PhabricatorActivitySettingsPanel' => 'PhabricatorSettingsPanel', @@ -9157,7 +9169,7 @@ phutil_register_library_map(array( 'PhabricatorConpherenceWidgetVisibleSetting' => 'PhabricatorInternalSetting', 'PhabricatorConsoleApplication' => 'PhabricatorApplication', 'PhabricatorConsoleContentSource' => 'PhabricatorContentSource', - 'PhabricatorConstantChartFunction' => 'PhabricatorChartFunction', + 'PhabricatorConstantChartFunction' => 'PhabricatorPureChartFunction', 'PhabricatorContactNumbersSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorContentSource' => 'Phobject', 'PhabricatorContentSourceModule' => 'PhabricatorConfigModule', @@ -9169,7 +9181,7 @@ phutil_register_library_map(array( 'PhabricatorCoreCreateTransaction' => 'PhabricatorCoreTransactionType', 'PhabricatorCoreTransactionType' => 'PhabricatorModularTransactionType', 'PhabricatorCoreVoidTransaction' => 'PhabricatorModularTransactionType', - 'PhabricatorCosChartFunction' => 'PhabricatorChartFunction', + 'PhabricatorCosChartFunction' => 'PhabricatorPureChartFunction', 'PhabricatorCountFact' => 'PhabricatorFact', 'PhabricatorCountdown' => array( 'PhabricatorCountdownDAO', @@ -9435,6 +9447,7 @@ phutil_register_library_map(array( 'PhabricatorDefaultRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler', 'PhabricatorDefaultSyntaxStyle' => 'PhabricatorSyntaxStyle', 'PhabricatorDefaultUnlockEngine' => 'PhabricatorUnlockEngine', + 'PhabricatorDemoChartEngine' => 'PhabricatorChartEngine', 'PhabricatorDestructibleCodex' => 'Phobject', 'PhabricatorDestructionEngine' => 'Phobject', 'PhabricatorDestructionEngineExtension' => 'Phobject', @@ -9553,6 +9566,7 @@ phutil_register_library_map(array( 'PhabricatorEditEngineSettingsPanel' => 'PhabricatorSettingsPanel', 'PhabricatorEditEngineStaticCommentAction' => 'PhabricatorEditEngineCommentAction', 'PhabricatorEditEngineSubtype' => 'Phobject', + 'PhabricatorEditEngineSubtypeHeraldField' => 'HeraldField', 'PhabricatorEditEngineSubtypeMap' => 'Phobject', 'PhabricatorEditEngineSubtypeTestCase' => 'PhabricatorTestCase', 'PhabricatorEditEngineSubtypeTransaction' => 'PhabricatorEditEngineTransactionType', @@ -9806,7 +9820,7 @@ phutil_register_library_map(array( 'PhabricatorFlagDAO', 'PhabricatorPolicyInterface', ), - 'PhabricatorFlagAddFlagHeraldAction' => 'HeraldAction', + 'PhabricatorFlagAddFlagHeraldAction' => 'PhabricatorFlagHeraldAction', 'PhabricatorFlagColor' => 'PhabricatorFlagConstants', 'PhabricatorFlagConstants' => 'Phobject', 'PhabricatorFlagController' => 'PhabricatorController', @@ -9814,8 +9828,10 @@ phutil_register_library_map(array( 'PhabricatorFlagDeleteController' => 'PhabricatorFlagController', 'PhabricatorFlagDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', 'PhabricatorFlagEditController' => 'PhabricatorFlagController', + 'PhabricatorFlagHeraldAction' => 'HeraldAction', 'PhabricatorFlagListController' => 'PhabricatorFlagController', 'PhabricatorFlagQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorFlagRemoveFlagHeraldAction' => 'PhabricatorFlagHeraldAction', 'PhabricatorFlagSearchEngine' => 'PhabricatorApplicationSearchEngine', 'PhabricatorFlagSelectControl' => 'AphrontFormControl', 'PhabricatorFlaggableInterface' => 'PhabricatorPHIDInterface', @@ -10067,7 +10083,7 @@ phutil_register_library_map(array( 'PhabricatorMarkupInterface', ), 'PhabricatorMarkupPreviewController' => 'PhabricatorController', - 'PhabricatorMaxChartFunction' => 'PhabricatorChartFunction', + 'PhabricatorMaxChartFunction' => 'PhabricatorPureChartFunction', 'PhabricatorMemeEngine' => 'Phobject', 'PhabricatorMemeRemarkupRule' => 'PhutilRemarkupRule', 'PhabricatorMentionRemarkupRule' => 'PhutilRemarkupRule', @@ -10134,7 +10150,7 @@ phutil_register_library_map(array( 'PhabricatorMetronome' => 'Phobject', 'PhabricatorMetronomeTestCase' => 'PhabricatorTestCase', 'PhabricatorMetronomicTriggerClock' => 'PhabricatorTriggerClock', - 'PhabricatorMinChartFunction' => 'PhabricatorChartFunction', + 'PhabricatorMinChartFunction' => 'PhabricatorPureChartFunction', 'PhabricatorModularTransaction' => 'PhabricatorApplicationTransaction', 'PhabricatorModularTransactionType' => 'Phobject', 'PhabricatorMonogramDatasourceEngineExtension' => 'PhabricatorDatasourceEngineExtension', @@ -10678,8 +10694,10 @@ phutil_register_library_map(array( 'PhabricatorPolicyManagementWorkflow' => 'PhabricatorManagementWorkflow', 'PhabricatorPolicyPHIDTypePolicy' => 'PhabricatorPHIDType', 'PhabricatorPolicyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorPolicyRef' => 'Phobject', 'PhabricatorPolicyRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler', 'PhabricatorPolicyRule' => 'Phobject', + 'PhabricatorPolicyRulesView' => 'AphrontView', 'PhabricatorPolicySearchEngineExtension' => 'PhabricatorSearchEngineExtension', 'PhabricatorPolicyStrengthConstants' => 'PhabricatorPolicyConstants', 'PhabricatorPolicyTestCase' => 'PhabricatorTestCase', @@ -10690,6 +10708,7 @@ phutil_register_library_map(array( ), 'PhabricatorPolicyType' => 'PhabricatorPolicyConstants', 'PhabricatorPonderApplication' => 'PhabricatorApplication', + 'PhabricatorPreambleTestCase' => 'PhabricatorTestCase', 'PhabricatorPrimaryEmailUserLogType' => 'PhabricatorUserLogType', 'PhabricatorProfileMenuEditEngine' => 'PhabricatorEditEngine', 'PhabricatorProfileMenuEditor' => 'PhabricatorApplicationTransactionEditor', @@ -10726,6 +10745,7 @@ phutil_register_library_map(array( 'PhabricatorSpacesInterface', 'PhabricatorEditEngineSubtypeInterface', ), + 'PhabricatorProjectActivityChartEngine' => 'PhabricatorChartEngine', 'PhabricatorProjectAddHeraldAction' => 'PhabricatorProjectHeraldAction', 'PhabricatorProjectApplication' => 'PhabricatorApplication', 'PhabricatorProjectArchiveController' => 'PhabricatorProjectController', @@ -10946,6 +10966,7 @@ phutil_register_library_map(array( 'PhabricatorProjectsWatchersSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', 'PhabricatorPronounSetting' => 'PhabricatorSelectSetting', 'PhabricatorProtocolLog' => 'Phobject', + 'PhabricatorPureChartFunction' => 'PhabricatorChartFunction', 'PhabricatorPygmentSetupCheck' => 'PhabricatorSetupCheck', 'PhabricatorQuery' => 'Phobject', 'PhabricatorQueryConstraint' => 'Phobject', @@ -11204,7 +11225,7 @@ phutil_register_library_map(array( 'PhabricatorPolicyInterface', ), 'PhabricatorSavedQueryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', - 'PhabricatorScaleChartFunction' => 'PhabricatorChartFunction', + 'PhabricatorScaleChartFunction' => 'PhabricatorPureChartFunction', 'PhabricatorScheduleTaskTriggerAction' => 'PhabricatorTriggerAction', 'PhabricatorScopedEnv' => 'Phobject', 'PhabricatorSearchAbstractDocument' => 'Phobject', @@ -11255,9 +11276,10 @@ phutil_register_library_map(array( 'PhabricatorSearchResultBucketGroup' => 'Phobject', 'PhabricatorSearchResultView' => 'AphrontView', 'PhabricatorSearchSchemaSpec' => 'PhabricatorConfigSchemaSpec', - 'PhabricatorSearchScopeSetting' => 'PhabricatorInternalSetting', + 'PhabricatorSearchScopeSetting' => 'PhabricatorSelectSetting', 'PhabricatorSearchSelectField' => 'PhabricatorSearchField', 'PhabricatorSearchService' => 'Phobject', + 'PhabricatorSearchSettingsPanel' => 'PhabricatorEditEngineSettingsPanel', 'PhabricatorSearchStringListField' => 'PhabricatorSearchField', 'PhabricatorSearchSubscribersField' => 'PhabricatorSearchTokenizerField', 'PhabricatorSearchTextField' => 'PhabricatorSearchField', @@ -11295,12 +11317,12 @@ phutil_register_library_map(array( 'PhabricatorSetupIssue' => 'Phobject', 'PhabricatorSetupIssueUIExample' => 'PhabricatorUIExample', 'PhabricatorSetupIssueView' => 'AphrontView', - 'PhabricatorShiftChartFunction' => 'PhabricatorChartFunction', + 'PhabricatorShiftChartFunction' => 'PhabricatorPureChartFunction', 'PhabricatorShortSite' => 'PhabricatorSite', 'PhabricatorShowFiletreeSetting' => 'PhabricatorSelectSetting', 'PhabricatorSignDocumentsUserLogType' => 'PhabricatorUserLogType', 'PhabricatorSimpleEditType' => 'PhabricatorEditType', - 'PhabricatorSinChartFunction' => 'PhabricatorChartFunction', + 'PhabricatorSinChartFunction' => 'PhabricatorPureChartFunction', 'PhabricatorSite' => 'AphrontSite', 'PhabricatorSlackAuthProvider' => 'PhabricatorOAuth2AuthProvider', 'PhabricatorSlowvoteApplication' => 'PhabricatorApplication', diff --git a/src/applications/auth/controller/PhabricatorLogoutController.php b/src/applications/auth/controller/PhabricatorLogoutController.php index dccf6bb45b..d71d080cbd 100644 --- a/src/applications/auth/controller/PhabricatorLogoutController.php +++ b/src/applications/auth/controller/PhabricatorLogoutController.php @@ -68,12 +68,42 @@ final class PhabricatorLogoutController ->setURI('/auth/loggedout/'); } + if ($viewer->getPHID()) { - return $this->newDialog() + $dialog = $this->newDialog() ->setTitle(pht('Log Out?')) - ->appendChild(pht('Are you sure you want to log out?')) - ->addSubmitButton(pht('Log Out')) + ->appendParagraph(pht('Are you sure you want to log out?')) ->addCancelButton('/'); + + $configs = id(new PhabricatorAuthProviderConfigQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->execute(); + if (!$configs) { + $dialog + ->appendRemarkup( + pht( + 'WARNING: You have not configured any authentication providers '. + 'yet, so your account has no login credentials. If you log out '. + 'now, you will not be able to log back in normally.')) + ->appendParagraph( + pht( + 'To enable the login flow, follow setup guidance and configure '. + 'at least one authentication provider, then associate '. + 'credentials with your account. After completing these steps, '. + 'you will be able to log out and log back in normally.')) + ->appendParagraph( + pht( + 'If you log out now, you can still regain access to your '. + 'account later by using the account recovery workflow. The '. + 'login screen will prompt you with recovery instructions.')); + + $button = pht('Log Out Anyway'); + } else { + $button = pht('Log Out'); + } + + $dialog->addSubmitButton($button); + return $dialog; } return id(new AphrontRedirectResponse())->setURI('/'); diff --git a/src/applications/auth/controller/config/PhabricatorAuthListController.php b/src/applications/auth/controller/config/PhabricatorAuthListController.php index 5d1d85cca6..b25c791e27 100644 --- a/src/applications/auth/controller/config/PhabricatorAuthListController.php +++ b/src/applications/auth/controller/config/PhabricatorAuthListController.php @@ -64,7 +64,7 @@ final class PhabricatorAuthListController array( 'href' => $this->getApplicationURI('config/new/'), ), - pht('Add Authentication Provider')))); + pht('Add Provider')))); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Login and Registration')); diff --git a/src/applications/config/check/PhabricatorMySQLSetupCheck.php b/src/applications/config/check/PhabricatorMySQLSetupCheck.php index a4048cbc33..de9a9e8b54 100644 --- a/src/applications/config/check/PhabricatorMySQLSetupCheck.php +++ b/src/applications/config/check/PhabricatorMySQLSetupCheck.php @@ -50,33 +50,50 @@ final class PhabricatorMySQLSetupCheck extends PhabricatorSetupCheck { if (!in_array('STRICT_ALL_TABLES', $modes)) { $summary = pht( 'MySQL is not in strict mode (on host "%s"), but using strict mode '. - 'is strongly encouraged.', + 'is recommended.', $host_name); $message = pht( - "On database host \"%s\", the global %s is not set to %s. ". - "It is strongly encouraged that you enable this mode when running ". - "Phabricator.\n\n". - "By default MySQL will silently ignore some types of errors, which ". - "can cause data loss and raise security concerns. Enabling strict ". - "mode makes MySQL raise an explicit error instead, and prevents this ". - "entire class of problems from doing any damage.\n\n". - "You can find more information about this mode (and how to configure ". - "it) in the MySQL manual. Usually, it is sufficient to add this to ". - "your %s file (in the %s section) and then restart %s:\n\n". - "%s\n". - "(Note that if you run other applications against the same database, ". - "they may not work in strict mode. Be careful about enabling it in ". - "these cases.)", + 'On database host "%s", the global "sql_mode" setting does not '. + 'include the "STRICT_ALL_TABLES" mode. Enabling this mode is '. + 'recommended to generally improve how MySQL handles certain errors.'. + "\n\n". + 'Without this mode enabled, MySQL will silently ignore some error '. + 'conditions, including inserts which attempt to store more data in '. + 'a column than actually fits. This behavior is usually undesirable '. + 'and can lead to data corruption (by truncating multibyte characters '. + 'in the middle), data loss (by discarding the data which does not '. + 'fit into the column), or security concerns (for example, by '. + 'truncating keys or credentials).'. + "\n\n". + 'Phabricator is developed and tested in "STRICT_ALL_TABLES" mode so '. + 'you should normally never encounter these situations, but may run '. + 'into them if you interact with the database directly, run '. + 'third-party code, develop extensions, or just encounter a bug in '. + 'the software.'. + "\n\n". + 'Enabling "STRICT_ALL_TABLES" makes MySQL raise an explicit error '. + 'if one of these unusual situations does occur. This is a safer '. + 'behavior and prevents these situations from causing secret, subtle, '. + 'and potentially serious issues later on.'. + "\n\n". + 'You can find more information about this mode (and how to configure '. + 'it) in the MySQL manual. Usually, it is sufficient to add this to '. + 'your "my.cnf" file (in the "[mysqld]" section) and then '. + 'restart "mysqld":'. + "\n\n". + '%s'. + "\n". + 'Note that if you run other applications against the same database, '. + 'they may not work in strict mode.'. + "\n\n". + 'If you can not or do not want to enable "STRICT_ALL_TABLES", you '. + 'can safely ignore this warning. Phabricator will work correctly '. + 'with this mode enabled or disabled.', $host_name, - phutil_tag('tt', array(), 'sql_mode'), - phutil_tag('tt', array(), 'STRICT_ALL_TABLES'), - phutil_tag('tt', array(), 'my.cnf'), - phutil_tag('tt', array(), '[mysqld]'), - phutil_tag('tt', array(), 'mysqld'), phutil_tag('pre', array(), 'sql_mode=STRICT_ALL_TABLES')); - $this->newIssue('mysql.mode') + $this->newIssue('sql_mode.strict') ->setName(pht('MySQL %s Mode Not Set', 'STRICT_ALL_TABLES')) ->setSummary($summary) ->setMessage($message) @@ -84,49 +101,6 @@ final class PhabricatorMySQLSetupCheck extends PhabricatorSetupCheck { ->addMySQLConfig('sql_mode'); } - if (in_array('ONLY_FULL_GROUP_BY', $modes)) { - $summary = pht( - 'MySQL is in ONLY_FULL_GROUP_BY mode (on host "%s"), but using this '. - 'mode is strongly discouraged.', - $host_name); - - $message = pht( - "On database host \"%s\", the global %s is set to %s. ". - "It is strongly encouraged that you disable this mode when running ". - "Phabricator.\n\n". - "With %s enabled, MySQL rejects queries for which the select list ". - "or (as of MySQL 5.0.23) %s list refer to nonaggregated columns ". - "that are not named in the %s clause. More importantly, Phabricator ". - "does not work properly with this mode enabled.\n\n". - "You can find more information about this mode (and how to configure ". - "it) in the MySQL manual. Usually, it is sufficient to change the %s ". - "in your %s file (in the %s section) and then restart %s:\n\n". - "%s\n". - "(Note that if you run other applications against the same database, ". - "they may not work with %s. Be careful about enabling ". - "it in these cases and consider migrating Phabricator to a different ". - "database.)", - $host_name, - phutil_tag('tt', array(), 'sql_mode'), - phutil_tag('tt', array(), 'ONLY_FULL_GROUP_BY'), - phutil_tag('tt', array(), 'ONLY_FULL_GROUP_BY'), - phutil_tag('tt', array(), 'HAVING'), - phutil_tag('tt', array(), 'GROUP BY'), - phutil_tag('tt', array(), 'sql_mode'), - phutil_tag('tt', array(), 'my.cnf'), - phutil_tag('tt', array(), '[mysqld]'), - phutil_tag('tt', array(), 'mysqld'), - phutil_tag('pre', array(), 'sql_mode=STRICT_ALL_TABLES'), - phutil_tag('tt', array(), 'ONLY_FULL_GROUP_BY')); - - $this->newIssue('mysql.mode') - ->setName(pht('MySQL %s Mode Set', 'ONLY_FULL_GROUP_BY')) - ->setSummary($summary) - ->setMessage($message) - ->setDatabaseRef($ref) - ->addMySQLConfig('sql_mode'); - } - $is_innodb_fulltext = false; $is_myisam_fulltext = false; if ($this->shouldUseMySQLSearchEngine()) { diff --git a/src/applications/config/management/PhabricatorConfigManagementSetWorkflow.php b/src/applications/config/management/PhabricatorConfigManagementSetWorkflow.php index 9eb83bd61e..d69e903bcc 100644 --- a/src/applications/config/management/PhabricatorConfigManagementSetWorkflow.php +++ b/src/applications/config/management/PhabricatorConfigManagementSetWorkflow.php @@ -140,11 +140,20 @@ final class PhabricatorConfigManagementSetWorkflow 'Wrote configuration key "%s" to database storage.', $key); } else { - $config_source = id(new PhabricatorConfigLocalSource()) - ->setKeys(array($key => $value)); + $config_source = new PhabricatorConfigLocalSource(); $local_path = $config_source->getReadablePath(); + try { + $config_source->setKeys(array($key => $value)); + } catch (FilesystemException $ex) { + throw new PhutilArgumentUsageException( + pht( + 'Local path "%s" is not writable. This file must be writable '. + 'so that "bin/config" can store configuration.', + Filesystem::readablePath($local_path))); + } + $write_message = pht( 'Wrote configuration key "%s" to local storage (in file "%s").', $key, diff --git a/src/applications/diffusion/ref/DiffusionServiceRef.php b/src/applications/diffusion/ref/DiffusionServiceRef.php new file mode 100644 index 0000000000..d6e6948e5d --- /dev/null +++ b/src/applications/diffusion/ref/DiffusionServiceRef.php @@ -0,0 +1,48 @@ +uri = $map['uri']; + $ref->isWritable = $map['writable']; + $ref->devicePHID = $map['devicePHID']; + $ref->protocol = $map['protocol']; + $ref->deviceName = $map['device']; + + return $ref; + } + + public function isWritable() { + return $this->isWritable; + } + + public function getDevicePHID() { + return $this->devicePHID; + } + + public function getURI() { + return $this->uri; + } + + public function getProtocol() { + return $this->protocol; + } + + public function getDeviceName() { + return $this->deviceName; + } + +} diff --git a/src/applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php index abf2a4323e..f59a9b58b4 100644 --- a/src/applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionGitReceivePackSSHWorkflow.php @@ -14,42 +14,33 @@ final class DiffusionGitReceivePackSSHWorkflow extends DiffusionGitSSHWorkflow { } protected function executeRepositoryOperations() { + // This is a write, and must have write access. + $this->requireWriteAccess(); + + $is_proxy = $this->shouldProxy(); + if ($is_proxy) { + return $this->executeRepositoryProxyOperations($for_write = true); + } + $host_wait_start = microtime(true); $repository = $this->getRepository(); $viewer = $this->getSSHUser(); $device = AlmanacKeys::getLiveDevice(); - // This is a write, and must have write access. - $this->requireWriteAccess(); - $cluster_engine = id(new DiffusionRepositoryClusterEngine()) ->setViewer($viewer) ->setRepository($repository) ->setLog($this); - $is_proxy = $this->shouldProxy(); - if ($is_proxy) { - $command = $this->getProxyCommand(true); - $did_write = false; + $command = csprintf('git-receive-pack %s', $repository->getLocalPath()); + $cluster_engine->synchronizeWorkingCopyBeforeWrite(); - if ($device) { - $this->writeClusterEngineLogMessage( - pht( - "# Push received by \"%s\", forwarding to cluster host.\n", - $device->getName())); - } - } else { - $command = csprintf('git-receive-pack %s', $repository->getLocalPath()); - $did_write = true; - $cluster_engine->synchronizeWorkingCopyBeforeWrite(); - - if ($device) { - $this->writeClusterEngineLogMessage( - pht( - "# Ready to receive on cluster host \"%s\".\n", - $device->getName())); - } + if ($device) { + $this->writeClusterEngineLogMessage( + pht( + "# Ready to receive on cluster host \"%s\".\n", + $device->getName())); } $log = $this->newProtocolLog($is_proxy); @@ -71,9 +62,7 @@ final class DiffusionGitReceivePackSSHWorkflow extends DiffusionGitSSHWorkflow { // We've committed the write (or rejected it), so we can release the lock // without waiting for the client to receive the acknowledgement. - if ($did_write) { - $cluster_engine->synchronizeWorkingCopyAfterWrite(); - } + $cluster_engine->synchronizeWorkingCopyAfterWrite(); if ($caught) { throw $caught; @@ -85,18 +74,16 @@ final class DiffusionGitReceivePackSSHWorkflow extends DiffusionGitSSHWorkflow { // When a repository is clustered, we reach this cleanup code on both // the proxy and the actual final endpoint node. Don't do more cleanup // or logging than we need to. - if ($did_write) { - $repository->writeStatusMessage( - PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE, - PhabricatorRepositoryStatusMessage::CODE_OKAY); + $repository->writeStatusMessage( + PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE, + PhabricatorRepositoryStatusMessage::CODE_OKAY); - $host_wait_end = microtime(true); + $host_wait_end = microtime(true); - $this->updatePushLogWithTimingInformation( - $this->getClusterEngineLogProperty('writeWait'), - $this->getClusterEngineLogProperty('readWait'), - ($host_wait_end - $host_wait_start)); - } + $this->updatePushLogWithTimingInformation( + $this->getClusterEngineLogProperty('writeWait'), + $this->getClusterEngineLogProperty('readWait'), + ($host_wait_end - $host_wait_start)); } return $err; diff --git a/src/applications/diffusion/ssh/DiffusionGitSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionGitSSHWorkflow.php index d9cc8063d5..292741e34d 100644 --- a/src/applications/diffusion/ssh/DiffusionGitSSHWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionGitSSHWorkflow.php @@ -8,6 +8,10 @@ abstract class DiffusionGitSSHWorkflow private $protocolLog; private $wireProtocol; + private $ioBytesRead = 0; + private $ioBytesWritten = 0; + private $requestAttempts = 0; + private $requestFailures = 0; protected function writeError($message) { // Git assumes we'll add our own newlines. @@ -98,6 +102,8 @@ abstract class DiffusionGitSSHWorkflow PhabricatorSSHPassthruCommand $command, $message) { + $this->ioBytesWritten += strlen($message); + $log = $this->getProtocolLog(); if ($log) { $log->didWriteBytes($message); @@ -125,7 +131,131 @@ abstract class DiffusionGitSSHWorkflow $message = $protocol->willReadBytes($message); } + // Note that bytes aren't counted until they're emittted by the protocol + // layer. This means the underlying command might emit bytes, but if they + // are buffered by the protocol layer they won't count as read bytes yet. + + $this->ioBytesRead += strlen($message); + return $message; } + final protected function getIOBytesRead() { + return $this->ioBytesRead; + } + + final protected function getIOBytesWritten() { + return $this->ioBytesWritten; + } + + final protected function executeRepositoryProxyOperations($for_write) { + $device = AlmanacKeys::getLiveDevice(); + + $refs = $this->getAlmanacServiceRefs($for_write); + $err = 1; + + while (true) { + $ref = head($refs); + + $command = $this->getProxyCommandForServiceRef($ref); + + if ($device) { + $this->writeClusterEngineLogMessage( + pht( + "# Request received by \"%s\", forwarding to cluster ". + "host \"%s\".\n", + $device->getName(), + $ref->getDeviceName())); + } + + $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command); + + $future = id(new ExecFuture('%C', $command)) + ->setEnv($this->getEnvironment()); + + $this->didBeginRequest(); + + $err = $this->newPassthruCommand() + ->setIOChannel($this->getIOChannel()) + ->setCommandChannelFromExecFuture($future) + ->execute(); + + // TODO: Currently, when proxying, we do not write an event log on the + // proxy. Perhaps we should write a "proxy log". This is not very useful + // for statistics or auditing, but could be useful for diagnostics. + // Marking the proxy logs as proxied (and recording devicePHID on all + // logs) would make differentiating between these use cases easier. + + if (!$err) { + $this->waitForGitClient(); + return $err; + } + + // Throw away this service: the request failed and we're treating the + // failure as persistent, so we don't want to retry another request to + // the same host. + array_shift($refs); + + $should_retry = $this->shouldRetryRequest($refs); + if (!$should_retry) { + return $err; + } + + // If we haven't bailed out yet, we'll retry the request with the next + // service. + } + + throw new Exception(pht('Reached an unreachable place.')); + } + + private function didBeginRequest() { + $this->requestAttempts++; + return $this; + } + + private function shouldRetryRequest(array $remaining_refs) { + $this->requestFailures++; + + if ($this->requestFailures > $this->requestAttempts) { + throw new Exception( + pht( + "Workflow has recorded more failures than attempts; there is a ". + "missing call to \"didBeginRequest()\".\n")); + } + + if (!$remaining_refs) { + $this->writeClusterEngineLogMessage( + pht( + "# All available services failed to serve the request, ". + "giving up.\n")); + return false; + } + + $read_len = $this->getIOBytesRead(); + if ($read_len) { + $this->writeClusterEngineLogMessage( + pht( + "# Client already read from service (%s bytes), unable to retry.\n", + new PhutilNumber($read_len))); + return false; + } + + $write_len = $this->getIOBytesWritten(); + if ($write_len) { + $this->writeClusterEngineLogMessage( + pht( + "# Client already wrote to service (%s bytes), unable to retry.\n", + new PhutilNumber($write_len))); + return false; + } + + $this->writeClusterEngineLogMessage( + pht( + "# Service request failed, retrying (making attempt %s of %s).\n", + new PhutilNumber($this->requestAttempts + 1), + new PhutilNumber($this->requestAttempts + count($remaining_refs)))); + + return true; + } + } diff --git a/src/applications/diffusion/ssh/DiffusionGitUploadPackSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionGitUploadPackSSHWorkflow.php index 7e1f4a4f33..57c43b5a12 100644 --- a/src/applications/diffusion/ssh/DiffusionGitUploadPackSSHWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionGitUploadPackSSHWorkflow.php @@ -1,6 +1,7 @@ setName('git-upload-pack'); @@ -14,39 +15,33 @@ final class DiffusionGitUploadPackSSHWorkflow extends DiffusionGitSSHWorkflow { } protected function executeRepositoryOperations() { - $repository = $this->getRepository(); + $is_proxy = $this->shouldProxy(); + if ($is_proxy) { + return $this->executeRepositoryProxyOperations($for_write = false); + } + $viewer = $this->getSSHUser(); + $repository = $this->getRepository(); $device = AlmanacKeys::getLiveDevice(); $skip_sync = $this->shouldSkipReadSynchronization(); - $is_proxy = $this->shouldProxy(); - if ($is_proxy) { - $command = $this->getProxyCommand(false); + $command = csprintf('git-upload-pack -- %s', $repository->getLocalPath()); + if (!$skip_sync) { + $cluster_engine = id(new DiffusionRepositoryClusterEngine()) + ->setViewer($viewer) + ->setRepository($repository) + ->setLog($this) + ->synchronizeWorkingCopyBeforeRead(); if ($device) { $this->writeClusterEngineLogMessage( pht( - "# Fetch received by \"%s\", forwarding to cluster host.\n", + "# Cleared to fetch on cluster host \"%s\".\n", $device->getName())); } - } else { - $command = csprintf('git-upload-pack -- %s', $repository->getLocalPath()); - if (!$skip_sync) { - $cluster_engine = id(new DiffusionRepositoryClusterEngine()) - ->setViewer($viewer) - ->setRepository($repository) - ->setLog($this) - ->synchronizeWorkingCopyBeforeRead(); - - if ($device) { - $this->writeClusterEngineLogMessage( - pht( - "# Cleared to fetch on cluster host \"%s\".\n", - $device->getName())); - } - } } + $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command); $pull_event = $this->newPullEvent(); @@ -60,14 +55,12 @@ final class DiffusionGitUploadPackSSHWorkflow extends DiffusionGitSSHWorkflow { $log->didStartSession($command); } - if (!$is_proxy) { - if (PhabricatorEnv::getEnvConfig('phabricator.show-prototypes')) { - $protocol = new DiffusionGitUploadPackWireProtocol(); - if ($log) { - $protocol->setProtocolLog($log); - } - $this->setWireProtocol($protocol); + if (PhabricatorEnv::getEnvConfig('phabricator.show-prototypes')) { + $protocol = new DiffusionGitUploadPackWireProtocol(); + if ($log) { + $protocol->setProtocolLog($log); } + $this->setWireProtocol($protocol); } $err = $this->newPassthruCommand() @@ -89,15 +82,7 @@ final class DiffusionGitUploadPackSSHWorkflow extends DiffusionGitSSHWorkflow { ->setResultCode(0); } - // TODO: Currently, when proxying, we do not write a log on the proxy. - // Perhaps we should write a "proxy log". This is not very useful for - // statistics or auditing, but could be useful for diagnostics. Marking - // the proxy logs as proxied (and recording devicePHID on all logs) would - // make differentiating between these use cases easier. - - if (!$is_proxy) { - $pull_event->save(); - } + $pull_event->save(); if (!$err) { $this->waitForGitClient(); diff --git a/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php b/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php index 08144eb0c9..358418f44c 100644 --- a/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php +++ b/src/applications/diffusion/ssh/DiffusionSSHWorkflow.php @@ -73,13 +73,13 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow { return $this->shouldProxy; } - protected function getProxyCommand($for_write) { + final protected function getAlmanacServiceRefs($for_write) { $viewer = $this->getSSHUser(); $repository = $this->getRepository(); $is_cluster_request = $this->getIsClusterRequest(); - $uri = $repository->getAlmanacServiceURI( + $refs = $repository->getAlmanacServiceRefs( $viewer, array( 'neverProxy' => $is_cluster_request, @@ -89,14 +89,28 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow { 'writable' => $for_write, )); - if (!$uri) { + if (!$refs) { throw new Exception( pht( 'Failed to generate an intracluster proxy URI even though this '. 'request was routed as a proxy request.')); } - $uri = new PhutilURI($uri); + return $refs; + } + + final protected function getProxyCommand($for_write) { + $refs = $this->getAlmanacServiceRefs($for_write); + + $ref = head($refs); + + return $this->getProxyCommandForServiceRef($ref); + } + + final protected function getProxyCommandForServiceRef( + DiffusionServiceRef $ref) { + + $uri = new PhutilURI($ref->getURI()); $username = AlmanacKeys::getClusterSSHUser(); if ($username === null) { diff --git a/src/applications/fact/chart/PhabricatorAccumulateChartFunction.php b/src/applications/fact/chart/PhabricatorAccumulateChartFunction.php index 6ffbb85da9..63570ef234 100644 --- a/src/applications/fact/chart/PhabricatorAccumulateChartFunction.php +++ b/src/applications/fact/chart/PhabricatorAccumulateChartFunction.php @@ -1,7 +1,7 @@ getArgument('x')->getDomain(); - } - - public function newInputValues(PhabricatorChartDataQuery $query) { - return $this->getArgument('x')->newInputValues($query); - } - public function evaluateFunction(array $xv) { // First, we're going to accumulate the underlying function. Then // we'll map the inputs through the accumulation. diff --git a/src/applications/fact/chart/PhabricatorChartDataset.php b/src/applications/fact/chart/PhabricatorChartDataset.php index 9faf02b740..093a742077 100644 --- a/src/applications/fact/chart/PhabricatorChartDataset.php +++ b/src/applications/fact/chart/PhabricatorChartDataset.php @@ -59,13 +59,6 @@ abstract class PhabricatorChartDataset return $dataset; } - final public function toDictionary() { - return array( - 'type' => $this->getDatasetTypeKey(), - 'functions' => mpull($this->getFunctions(), 'toDictionary'), - ); - } - final public function getChartDisplayData( PhabricatorChartDataQuery $data_query) { return $this->newChartDisplayData($data_query); @@ -75,4 +68,35 @@ abstract class PhabricatorChartDataset PhabricatorChartDataQuery $data_query); + final public function getTabularDisplayData( + PhabricatorChartDataQuery $data_query) { + $results = array(); + + $functions = $this->getFunctions(); + foreach ($functions as $function) { + $datapoints = $function->newDatapoints($data_query); + + $refs = $function->getDataRefs(ipull($datapoints, 'x')); + + foreach ($datapoints as $key => $point) { + $x = $point['x']; + + if (isset($refs[$x])) { + $xrefs = $refs[$x]; + } else { + $xrefs = array(); + } + + $datapoints[$key]['refs'] = $xrefs; + } + + $results[] = array( + 'data' => $datapoints, + ); + } + + return id(new PhabricatorChartDisplayData()) + ->setWireData($results); + } + } diff --git a/src/applications/fact/chart/PhabricatorChartFunction.php b/src/applications/fact/chart/PhabricatorChartFunction.php index ac7ab64650..3ddcd6aec0 100644 --- a/src/applications/fact/chart/PhabricatorChartFunction.php +++ b/src/applications/fact/chart/PhabricatorChartFunction.php @@ -60,6 +60,10 @@ abstract class PhabricatorChartFunction return $this->functionLabel; } + final public function getKey() { + return $this->getFunctionLabel()->getKey(); + } + final public static function newFromDictionary(array $map) { PhutilTypeSpec::checkMap( $map, @@ -86,13 +90,6 @@ abstract class PhabricatorChartFunction return $function; } - public function toDictionary() { - return array( - 'function' => $this->getFunctionKey(), - 'arguments' => $this->getArgumentParser()->getRawArguments(), - ); - } - public function getSubfunctions() { $result = array(); $result[] = $this; @@ -180,6 +177,8 @@ abstract class PhabricatorChartFunction } abstract public function evaluateFunction(array $xv); + abstract public function getDataRefs(array $xv); + abstract public function loadRefs(array $refs); public function getDomain() { return null; diff --git a/src/applications/fact/chart/PhabricatorChartFunctionLabel.php b/src/applications/fact/chart/PhabricatorChartFunctionLabel.php index ad85c49b71..fa3f65aa67 100644 --- a/src/applications/fact/chart/PhabricatorChartFunctionLabel.php +++ b/src/applications/fact/chart/PhabricatorChartFunctionLabel.php @@ -3,11 +3,21 @@ final class PhabricatorChartFunctionLabel extends Phobject { + private $key; private $name; private $color; private $icon; private $fillColor; + public function setKey($key) { + $this->key = $key; + return $this; + } + + public function getKey() { + return $this->key; + } + public function setName($name) { $this->name = $name; return $this; @@ -46,6 +56,7 @@ final class PhabricatorChartFunctionLabel public function toWireFormat() { return array( + 'key' => $this->getKey(), 'name' => $this->getName(), 'color' => $this->getColor(), 'icon' => $this->getIcon(), diff --git a/src/applications/fact/chart/PhabricatorChartStackedAreaDataset.php b/src/applications/fact/chart/PhabricatorChartStackedAreaDataset.php index 8bf4445984..2ba08ea1c9 100644 --- a/src/applications/fact/chart/PhabricatorChartStackedAreaDataset.php +++ b/src/applications/fact/chart/PhabricatorChartStackedAreaDataset.php @@ -5,24 +5,187 @@ final class PhabricatorChartStackedAreaDataset const DATASETKEY = 'stacked-area'; + private $stacks; + + public function setStacks(array $stacks) { + $this->stacks = $stacks; + return $this; + } + + public function getStacks() { + return $this->stacks; + } + protected function newChartDisplayData( PhabricatorChartDataQuery $data_query) { + $functions = $this->getFunctions(); + $functions = mpull($functions, null, 'getKey'); - $reversed_functions = array_reverse($functions, true); + $stacks = $this->getStacks(); - $function_points = array(); - foreach ($reversed_functions as $function_idx => $function) { - $function_points[$function_idx] = array(); + if (!$stacks) { + $stacks = array( + array_reverse(array_keys($functions), true), + ); + } - $datapoints = $function->newDatapoints($data_query); - foreach ($datapoints as $point) { - $x = $point['x']; - $function_points[$function_idx][$x] = $point; + $series = array(); + $raw_points = array(); + + foreach ($stacks as $stack) { + $stack_functions = array_select_keys($functions, $stack); + + $function_points = $this->getFunctionDatapoints( + $data_query, + $stack_functions); + + $stack_points = $function_points; + + $function_points = $this->getGeometry( + $data_query, + $function_points); + + $baseline = array(); + foreach ($function_points as $function_idx => $points) { + $bounds = array(); + foreach ($points as $x => $point) { + if (!isset($baseline[$x])) { + $baseline[$x] = 0; + } + + $y0 = $baseline[$x]; + $baseline[$x] += $point['y']; + $y1 = $baseline[$x]; + + $bounds[] = array( + 'x' => $x, + 'y0' => $y0, + 'y1' => $y1, + ); + + if (isset($stack_points[$function_idx][$x])) { + $stack_points[$function_idx][$x]['y1'] = $y1; + } + } + + $series[$function_idx] = $bounds; + } + + $raw_points += $stack_points; + } + + $series = array_select_keys($series, array_keys($functions)); + $series = array_values($series); + + $raw_points = array_select_keys($raw_points, array_keys($functions)); + $raw_points = array_values($raw_points); + + $range_min = null; + $range_max = null; + + foreach ($series as $geometry_list) { + foreach ($geometry_list as $geometry_item) { + $y0 = $geometry_item['y0']; + $y1 = $geometry_item['y1']; + + if ($range_min === null) { + $range_min = $y0; + } + $range_min = min($range_min, $y0, $y1); + + if ($range_max === null) { + $range_max = $y1; + } + $range_max = max($range_max, $y0, $y1); } } - $raw_points = $function_points; + // We're going to group multiple events into a single point if they have + // X values that are very close to one another. + // + // If the Y values are also close to one another (these points are near + // one another in a horizontal line), it can be hard to select any + // individual point with the mouse. + // + // Even if the Y values are not close together (the points are on a + // fairly steep slope up or down), it's usually better to be able to + // mouse over a single point at the top or bottom of the slope and get + // a summary of what's going on. + + $domain_max = $data_query->getMaximumValue(); + $domain_min = $data_query->getMinimumValue(); + $resolution = ($domain_max - $domain_min) / 100; + + $events = array(); + foreach ($raw_points as $function_idx => $points) { + $event_list = array(); + + $event_group = array(); + $head_event = null; + foreach ($points as $point) { + $x = $point['x']; + + if ($head_event === null) { + // We don't have any points yet, so start a new group. + $head_event = $x; + $event_group[] = $point; + } else if (($x - $head_event) <= $resolution) { + // This point is close to the first point in this group, so + // add it to the existing group. + $event_group[] = $point; + } else { + // This point is not close to the first point in the group, + // so create a new group. + $event_list[] = $event_group; + $head_event = $x; + $event_group = array($point); + } + } + + if ($event_group) { + $event_list[] = $event_group; + } + + $event_spec = array(); + foreach ($event_list as $key => $event_points) { + // NOTE: We're using the last point as the representative point so + // that you can learn about a section of a chart by hovering over + // the point to right of the section, which is more intuitive than + // other points. + $event = last($event_points); + + $event = $event + array( + 'n' => count($event_points), + ); + + $event_list[$key] = $event; + } + + $events[] = $event_list; + } + + $wire_labels = array(); + foreach ($functions as $function_key => $function) { + $label = $function->getFunctionLabel(); + $wire_labels[] = $label->toWireFormat(); + } + + $result = array( + 'type' => $this->getDatasetTypeKey(), + 'data' => $series, + 'events' => $events, + 'labels' => $wire_labels, + ); + + return id(new PhabricatorChartDisplayData()) + ->setWireData($result) + ->setRange(new PhabricatorChartInterval($range_min, $range_max)); + } + + private function getAllXValuesAsMap( + PhabricatorChartDataQuery $data_query, + array $point_lists) { // We need to define every function we're drawing at every point where // any of the functions we're drawing are defined. If we don't, we'll @@ -31,17 +194,54 @@ final class PhabricatorChartStackedAreaDataset // stacking the functions on top of one another. $must_define = array(); - foreach ($function_points as $function_idx => $points) { - foreach ($points as $x => $point) { + + $min = $data_query->getMinimumValue(); + $max = $data_query->getMaximumValue(); + $must_define[$max] = $max; + $must_define[$min] = $min; + + foreach ($point_lists as $point_list) { + foreach ($point_list as $x => $point) { $must_define[$x] = $x; } } + ksort($must_define); - foreach ($reversed_functions as $function_idx => $function) { + return $must_define; + } + + private function getFunctionDatapoints( + PhabricatorChartDataQuery $data_query, + array $functions) { + + assert_instances_of($functions, 'PhabricatorChartFunction'); + + $points = array(); + foreach ($functions as $idx => $function) { + $points[$idx] = array(); + + $datapoints = $function->newDatapoints($data_query); + foreach ($datapoints as $point) { + $x_value = $point['x']; + $points[$idx][$x_value] = $point; + } + } + + return $points; + } + + private function getGeometry( + PhabricatorChartDataQuery $data_query, + array $point_lists) { + + $must_define = $this->getAllXValuesAsMap($data_query, $point_lists); + + foreach ($point_lists as $idx => $points) { + $missing = array(); foreach ($must_define as $x) { - if (!isset($function_points[$function_idx][$x])) { + if (!isset($points[$x])) { $missing[$x] = true; } } @@ -50,8 +250,6 @@ final class PhabricatorChartStackedAreaDataset continue; } - $points = $function_points[$function_idx]; - $values = array_keys($points); $cursor = -1; $length = count($values); @@ -84,88 +282,19 @@ final class PhabricatorChartStackedAreaDataset $y = $ymin + (($ymax - $ymin) * $distance); } else { $xmin = $values[$cursor]; - $y = $function_points[$function_idx][$xmin]['y']; + $y = $points[$xmin]['y']; } - $function_points[$function_idx][$x] = array( + $point_lists[$idx][$x] = array( 'x' => $x, 'y' => $y, ); } - ksort($function_points[$function_idx]); + ksort($point_lists[$idx]); } - $range_min = null; - $range_max = null; - - $series = array(); - $baseline = array(); - foreach ($function_points as $function_idx => $points) { - $below = idx($function_points, $function_idx - 1); - - $bounds = array(); - foreach ($points as $x => $point) { - if (!isset($baseline[$x])) { - $baseline[$x] = 0; - } - - $y0 = $baseline[$x]; - $baseline[$x] += $point['y']; - $y1 = $baseline[$x]; - - $bounds[] = array( - 'x' => $x, - 'y0' => $y0, - 'y1' => $y1, - ); - - if (isset($raw_points[$function_idx][$x])) { - $raw_points[$function_idx][$x]['y1'] = $y1; - } - - if ($range_min === null) { - $range_min = $y0; - } - $range_min = min($range_min, $y0, $y1); - - if ($range_max === null) { - $range_max = $y1; - } - $range_max = max($range_max, $y0, $y1); - } - - $series[] = $bounds; - } - - $series = array_reverse($series); - - $events = array(); - foreach ($raw_points as $function_idx => $points) { - $event_list = array(); - foreach ($points as $point) { - $event_list[] = $point; - } - $events[] = $event_list; - } - - $wire_labels = array(); - foreach ($functions as $function_key => $function) { - $label = $function->getFunctionLabel(); - $wire_labels[] = $label->toWireFormat(); - } - - $result = array( - 'type' => $this->getDatasetTypeKey(), - 'data' => $series, - 'events' => $events, - 'labels' => $wire_labels, - ); - - return id(new PhabricatorChartDisplayData()) - ->setWireData($result) - ->setRange(new PhabricatorChartInterval($range_min, $range_max)); + return $point_lists; } - } diff --git a/src/applications/fact/chart/PhabricatorComposeChartFunction.php b/src/applications/fact/chart/PhabricatorComposeChartFunction.php index f6148ceae9..e69455b3ff 100644 --- a/src/applications/fact/chart/PhabricatorComposeChartFunction.php +++ b/src/applications/fact/chart/PhabricatorComposeChartFunction.php @@ -70,4 +70,22 @@ final class PhabricatorComposeChartFunction return $yv; } + public function getDataRefs(array $xv) { + // TODO: This is not entirely correct. The correct implementation would + // map "x -> y" at each stage of composition and pull and aggregate all + // the datapoint refs. In practice, we currently never compose functions + // with a data function somewhere in the middle, so just grabbing the first + // result is close enough. + + // In the future, we may: notably, "x -> shift(-1 month) -> ..." to + // generate a month-over-month overlay is a sensible operation which will + // source data from the middle of a function composition. + + foreach ($this->getFunctionArguments() as $function) { + return $function->getDataRefs($xv); + } + + return array(); + } + } diff --git a/src/applications/fact/chart/PhabricatorConstantChartFunction.php b/src/applications/fact/chart/PhabricatorConstantChartFunction.php index cdc6c9494a..e65c577827 100644 --- a/src/applications/fact/chart/PhabricatorConstantChartFunction.php +++ b/src/applications/fact/chart/PhabricatorConstantChartFunction.php @@ -1,7 +1,7 @@ newArgument() @@ -51,13 +52,15 @@ final class PhabricatorFactChartFunction $data = queryfx_all( $conn, - 'SELECT value, epoch FROM %T WHERE %LA ORDER BY epoch ASC', + 'SELECT id, value, epoch FROM %T WHERE %LA ORDER BY epoch ASC', $table_name, $where); $map = array(); + $refs = array(); if ($data) { foreach ($data as $row) { + $ref = (string)$row['id']; $value = (int)$row['value']; $epoch = (int)$row['epoch']; @@ -66,10 +69,17 @@ final class PhabricatorFactChartFunction } $map[$epoch] += $value; + + if (!isset($refs[$epoch])) { + $refs[$epoch] = array(); + } + + $refs[$epoch][] = $ref; } } $this->map = $map; + $this->refs = $refs; } public function getDomain() { @@ -99,4 +109,60 @@ final class PhabricatorFactChartFunction return $yv; } + public function getDataRefs(array $xv) { + return array_select_keys($this->refs, $xv); + } + + public function loadRefs(array $refs) { + $fact = $this->fact; + + $datapoint_table = $fact->newDatapoint(); + $conn = $datapoint_table->establishConnection('r'); + + $dimension_table = new PhabricatorFactObjectDimension(); + + $where = array(); + + $where[] = qsprintf( + $conn, + 'p.id IN (%Ld)', + $refs); + + + $rows = queryfx_all( + $conn, + 'SELECT + p.id id, + p.value, + od.objectPHID objectPHID, + dd.objectPHID dimensionPHID + FROM %R p + LEFT JOIN %R od ON od.id = p.objectID + LEFT JOIN %R dd ON dd.id = p.dimensionID + WHERE %LA', + $datapoint_table, + $dimension_table, + $dimension_table, + $where); + $rows = ipull($rows, null, 'id'); + + $results = array(); + + foreach ($refs as $ref) { + if (!isset($rows[$ref])) { + continue; + } + + $row = $rows[$ref]; + + $results[$ref] = array( + 'objectPHID' => $row['objectPHID'], + 'dimensionPHID' => $row['dimensionPHID'], + 'value' => (float)$row['value'], + ); + } + + return $results; + } + } diff --git a/src/applications/fact/chart/PhabricatorHigherOrderChartFunction.php b/src/applications/fact/chart/PhabricatorHigherOrderChartFunction.php index ab160bd10f..7124603388 100644 --- a/src/applications/fact/chart/PhabricatorHigherOrderChartFunction.php +++ b/src/applications/fact/chart/PhabricatorHigherOrderChartFunction.php @@ -32,4 +32,38 @@ abstract class PhabricatorHigherOrderChartFunction return array_keys($map); } + public function getDataRefs(array $xv) { + $refs = array(); + + foreach ($this->getFunctionArguments() as $function) { + $function_refs = $function->getDataRefs($xv); + + $function_refs = array_select_keys($function_refs, $xv); + if (!$function_refs) { + continue; + } + + foreach ($function_refs as $x => $ref_list) { + if (!isset($refs[$x])) { + $refs[$x] = array(); + } + foreach ($ref_list as $ref) { + $refs[$x][] = $ref; + } + } + } + + return $refs; + } + + public function loadRefs(array $refs) { + $results = array(); + + foreach ($this->getFunctionArguments() as $function) { + $results += $function->loadRefs($refs); + } + + return $results; + } + } diff --git a/src/applications/fact/chart/PhabricatorMaxChartFunction.php b/src/applications/fact/chart/PhabricatorMaxChartFunction.php index c874cef8e8..accf217328 100644 --- a/src/applications/fact/chart/PhabricatorMaxChartFunction.php +++ b/src/applications/fact/chart/PhabricatorMaxChartFunction.php @@ -1,36 +1,27 @@ newArgument() - ->setName('x') - ->setType('function'), $this->newArgument() ->setName('max') ->setType('number'), ); } - public function getDomain() { - return $this->getArgument('x')->getDomain(); - } - - public function newInputValues(PhabricatorChartDataQuery $query) { - return $this->getArgument('x')->newInputValues($query); - } - public function evaluateFunction(array $xv) { - $yv = $this->getArgument('x')->evaluateFunction($xv); $max = $this->getArgument('max'); - foreach ($yv as $k => $y) { - if ($y > $max) { - $yv[$k] = null; + $yv = array(); + foreach ($xv as $x) { + if ($x > $max) { + $yv[] = null; + } else { + $yv[] = $x; } } diff --git a/src/applications/fact/chart/PhabricatorMinChartFunction.php b/src/applications/fact/chart/PhabricatorMinChartFunction.php index db1a003811..e6dcec06a4 100644 --- a/src/applications/fact/chart/PhabricatorMinChartFunction.php +++ b/src/applications/fact/chart/PhabricatorMinChartFunction.php @@ -1,36 +1,27 @@ newArgument() - ->setName('x') - ->setType('function'), $this->newArgument() ->setName('min') ->setType('number'), ); } - public function getDomain() { - return $this->getArgument('x')->getDomain(); - } - - public function newInputValues(PhabricatorChartDataQuery $query) { - return $this->getArgument('x')->newInputValues($query); - } - public function evaluateFunction(array $xv) { - $yv = $this->getArgument('x')->evaluateFunction($xv); $min = $this->getArgument('min'); - foreach ($yv as $k => $y) { - if ($y < $min) { - $yv[$k] = null; + $yv = array(); + foreach ($xv as $x) { + if ($x < $min) { + $yv[] = null; + } else { + $yv[] = $x; } } diff --git a/src/applications/fact/chart/PhabricatorPureChartFunction.php b/src/applications/fact/chart/PhabricatorPureChartFunction.php new file mode 100644 index 0000000000..74c748c274 --- /dev/null +++ b/src/applications/fact/chart/PhabricatorPureChartFunction.php @@ -0,0 +1,14 @@ +newChartView(); - return $this->newChartResponse($chart_view); + $tabular_view = $engine->newTabularView(); + + return $this->newChartResponse($chart_view, $tabular_view); } - private function newChartResponse($chart_view) { + private function newChartResponse($chart_view, $tabular_view) { $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Chart')) ->appendChild($chart_view); @@ -50,56 +52,19 @@ final class PhabricatorFactChartController extends PhabricatorFactController { return $this->newPage() ->setTitle($title) ->setCrumbs($crumbs) - ->appendChild($box); + ->appendChild( + array( + $box, + $tabular_view, + )); } private function newDemoChart() { $viewer = $this->getViewer(); - $argvs = array(); - - $argvs[] = array('fact', 'tasks.count.create'); - - $argvs[] = array('constant', 360); - - $argvs[] = array('fact', 'tasks.open-count.create'); - - $argvs[] = array( - 'sum', - array( - 'accumulate', - array('fact', 'tasks.count.create'), - ), - array( - 'accumulate', - array('fact', 'tasks.open-count.create'), - ), - ); - - $argvs[] = array( - 'compose', - array('scale', 0.001), - array('cos'), - array('scale', 100), - array('shift', 800), - ); - - $datasets = array(); - foreach ($argvs as $argv) { - $datasets[] = PhabricatorChartDataset::newFromDictionary( - array( - 'function' => $argv, - )); - } - - $chart = id(new PhabricatorFactChart()) - ->setDatasets($datasets); - - $engine = id(new PhabricatorChartRenderingEngine()) + $chart = id(new PhabricatorDemoChartEngine()) ->setViewer($viewer) - ->setChart($chart); - - $chart = $engine->getStoredChart(); + ->newStoredChart(); return id(new AphrontRedirectResponse())->setURI($chart->getURI()); } diff --git a/src/applications/fact/engine/PhabricatorChartEngine.php b/src/applications/fact/engine/PhabricatorChartEngine.php index f723633d6a..918817d475 100644 --- a/src/applications/fact/engine/PhabricatorChartEngine.php +++ b/src/applications/fact/engine/PhabricatorChartEngine.php @@ -63,7 +63,7 @@ abstract class PhabricatorChartEngine abstract protected function newChart(PhabricatorFactChart $chart, array $map); - final public function buildChartPanel() { + final public function newStoredChart() { $viewer = $this->getViewer(); $parameters = $this->getEngineParameters(); @@ -76,7 +76,11 @@ abstract class PhabricatorChartEngine ->setViewer($viewer) ->setChart($chart); - $chart = $rendering_engine->getStoredChart(); + return $rendering_engine->getStoredChart(); + } + + final public function buildChartPanel() { + $chart = $this->newStoredChart(); $panel_type = id(new PhabricatorDashboardChartPanelType()) ->getPanelTypeKey(); @@ -91,7 +95,7 @@ abstract class PhabricatorChartEngine final protected function newFunction($name /* , ... */) { $argv = func_get_args(); return id(new PhabricatorComposeChartFunction()) - ->setArguments(array($argv)); + ->setArguments($argv); } } diff --git a/src/applications/fact/engine/PhabricatorChartRenderingEngine.php b/src/applications/fact/engine/PhabricatorChartRenderingEngine.php index b328241ea6..bf487f0052 100644 --- a/src/applications/fact/engine/PhabricatorChartRenderingEngine.php +++ b/src/applications/fact/engine/PhabricatorChartRenderingEngine.php @@ -109,7 +109,146 @@ final class PhabricatorChartRenderingEngine return $chart_view; } + public function newTabularView() { + $viewer = $this->getViewer(); + $tabular_data = $this->newTabularData(); + + $ref_keys = array(); + foreach ($tabular_data['datasets'] as $tabular_dataset) { + foreach ($tabular_dataset as $function) { + foreach ($function['data'] as $point) { + foreach ($point['refs'] as $ref) { + $ref_keys[$ref] = $ref; + } + } + } + } + + $chart = $this->getStoredChart(); + + $ref_map = array(); + foreach ($chart->getDatasets() as $dataset) { + foreach ($dataset->getFunctions() as $function) { + // If we aren't looking for anything else, bail out. + if (!$ref_keys) { + break 2; + } + + $function_refs = $function->loadRefs($ref_keys); + + $ref_map += $function_refs; + + // Remove the ref keys that we found data for from the list of keys + // we are looking for. If any function gives us data for a given ref, + // that's satisfactory. + foreach ($function_refs as $ref_key => $ref_data) { + unset($ref_keys[$ref_key]); + } + } + } + + $phids = array(); + foreach ($ref_map as $ref => $ref_data) { + if (isset($ref_data['objectPHID'])) { + $phids[] = $ref_data['objectPHID']; + } + } + + $handles = $viewer->loadHandles($phids); + + $tabular_view = array(); + foreach ($tabular_data['datasets'] as $tabular_data) { + foreach ($tabular_data as $function) { + $rows = array(); + foreach ($function['data'] as $point) { + $ref_views = array(); + + $xv = date('Y-m-d h:i:s', $point['x']); + $yv = $point['y']; + + $point_refs = array(); + foreach ($point['refs'] as $ref) { + if (!isset($ref_map[$ref])) { + continue; + } + $point_refs[$ref] = $ref_map[$ref]; + } + + if (!$point_refs) { + $rows[] = array( + $xv, + $yv, + null, + null, + null, + ); + } else { + foreach ($point_refs as $ref => $ref_data) { + $ref_value = $ref_data['value']; + $ref_link = $handles[$ref_data['objectPHID']] + ->renderLink(); + + $view_uri = urisprintf( + '/fact/object/%s/', + $ref_data['objectPHID']); + + $ref_button = id(new PHUIButtonView()) + ->setIcon('fa-table') + ->setTag('a') + ->setColor('grey') + ->setHref($view_uri) + ->setText(pht('View Data')); + + $rows[] = array( + $xv, + $yv, + $ref_value, + $ref_link, + $ref_button, + ); + + $xv = null; + $yv = null; + } + } + } + + $table = id(new AphrontTableView($rows)) + ->setHeaders( + array( + pht('X'), + pht('Y'), + pht('Raw'), + pht('Refs'), + null, + )) + ->setColumnClasses( + array( + 'n', + 'n', + 'n', + 'wide', + null, + )); + + $tabular_view[] = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Function')) + ->setTable($table); + } + } + + return $tabular_view; + } + public function newChartData() { + return $this->newWireData(false); + } + + public function newTabularData() { + return $this->newWireData(true); + } + + private function newWireData($is_tabular) { $chart = $this->getStoredChart(); $chart_key = $chart->getChartKey(); @@ -151,7 +290,11 @@ final class PhabricatorChartRenderingEngine $wire_datasets = array(); $ranges = array(); foreach ($datasets as $dataset) { - $display_data = $dataset->getChartDisplayData($data_query); + if ($is_tabular) { + $display_data = $dataset->getTabularDisplayData($data_query); + } else { + $display_data = $dataset->getChartDisplayData($data_query); + } $ranges[] = $display_data->getRange(); $wire_datasets[] = $display_data->getWireData(); diff --git a/src/applications/fact/engine/PhabricatorDemoChartEngine.php b/src/applications/fact/engine/PhabricatorDemoChartEngine.php new file mode 100644 index 0000000000..71fec03309 --- /dev/null +++ b/src/applications/fact/engine/PhabricatorDemoChartEngine.php @@ -0,0 +1,44 @@ +getViewer(); + + $functions = array(); + + $function = $this->newFunction( + array('scale', 0.0001), + array('cos'), + array('scale', 128), + array('shift', 256)); + + $function->getFunctionLabel() + ->setName(pht('cos(x)')) + ->setColor('rgba(0, 200, 0, 1)') + ->setFillColor('rgba(0, 200, 0, 0.15)'); + + $functions[] = $function; + + $function = $this->newFunction( + array('constant', 345)); + + $function->getFunctionLabel() + ->setName(pht('constant(345)')) + ->setColor('rgba(0, 0, 200, 1)') + ->setFillColor('rgba(0, 0, 200, 0.15)'); + + $functions[] = $function; + + $datasets = array(); + + $datasets[] = id(new PhabricatorChartStackedAreaDataset()) + ->setFunctions($functions); + + $chart->attachDatasets($datasets); + } + +} diff --git a/src/applications/flag/herald/PhabricatorFlagAddFlagHeraldAction.php b/src/applications/flag/herald/PhabricatorFlagAddFlagHeraldAction.php index e4ae03915f..57772ecb5f 100644 --- a/src/applications/flag/herald/PhabricatorFlagAddFlagHeraldAction.php +++ b/src/applications/flag/herald/PhabricatorFlagAddFlagHeraldAction.php @@ -1,6 +1,7 @@ getAdapter()->getPHID(); $rule = $effect->getRule(); diff --git a/src/applications/flag/herald/PhabricatorFlagHeraldAction.php b/src/applications/flag/herald/PhabricatorFlagHeraldAction.php new file mode 100644 index 0000000000..6f87a4ee6c --- /dev/null +++ b/src/applications/flag/herald/PhabricatorFlagHeraldAction.php @@ -0,0 +1,18 @@ +getAdapter()->getPHID(); + $rule = $effect->getRule(); + $author = $rule->getAuthor(); + + $flag = PhabricatorFlagQuery::loadUserFlag($author, $phid); + if (!$flag) { + $this->logEffect(self::DO_IGNORE_UNFLAG, null); + return; + } + + if ($flag->getColor() !== $effect->getTarget()) { + $this->logEffect(self::DO_IGNORE_UNFLAG, $flag->getColor()); + return; + } + + $flag->delete(); + + $this->logEffect(self::DO_UNFLAG, $flag->getColor()); + } + + public function getHeraldActionValueType() { + return id(new HeraldSelectFieldValue()) + ->setKey('flag.color') + ->setOptions(PhabricatorFlagColor::getColorNameMap()) + ->setDefault(PhabricatorFlagColor::COLOR_BLUE); + } + + protected function getActionEffectMap() { + return array( + self::DO_IGNORE_UNFLAG => array( + 'icon' => 'fa-times', + 'color' => 'grey', + 'name' => pht('Did Not Remove Flag'), + ), + self::DO_UNFLAG => array( + 'icon' => 'fa-flag', + 'name' => pht('Removed Flag'), + ), + ); + } + + public function renderActionDescription($value) { + $color = PhabricatorFlagColor::getColorName($value); + return pht('Remove %s flag.', $color); + } + + protected function renderActionEffectDescription($type, $data) { + switch ($type) { + case self::DO_IGNORE_UNFLAG: + if (!$data) { + return pht('Not marked with any flag.'); + } else { + return pht( + 'Marked with flag of the wrong color ("%s").', + PhabricatorFlagColor::getColorName($data)); + } + case self::DO_UNFLAG: + return pht( + 'Removed "%s" flag.', + PhabricatorFlagColor::getColorName($data)); + } + } + +} diff --git a/src/applications/fund/storage/FundInitiative.php b/src/applications/fund/storage/FundInitiative.php index 5e4dd48026..1ebbb35ef1 100644 --- a/src/applications/fund/storage/FundInitiative.php +++ b/src/applications/fund/storage/FundInitiative.php @@ -136,12 +136,12 @@ final class FundInitiative extends FundDAO } if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { - foreach ($viewer->getAuthorities() as $authority) { - if ($authority instanceof PhortuneMerchant) { - if ($authority->getPHID() == $this->getMerchantPHID()) { - return true; - } - } + $can_merchant = PhortuneMerchantQuery::canViewersEditMerchants( + array($viewer->getPHID()), + array($this->getMerchantPHID())); + + if ($can_merchant) { + return true; } } diff --git a/src/applications/harbormaster/herald/HarbormasterRunBuildPlansHeraldAction.php b/src/applications/harbormaster/herald/HarbormasterRunBuildPlansHeraldAction.php index 9fc053e8ae..6934a2e3d9 100644 --- a/src/applications/harbormaster/herald/HarbormasterRunBuildPlansHeraldAction.php +++ b/src/applications/harbormaster/herald/HarbormasterRunBuildPlansHeraldAction.php @@ -96,4 +96,8 @@ final class HarbormasterRunBuildPlansHeraldAction return $record->getTarget(); } + public function isActionAvailable() { + return id(new PhabricatorHarbormasterApplication())->isInstalled(); + } + } diff --git a/src/applications/herald/action/HeraldAction.php b/src/applications/herald/action/HeraldAction.php index a9740d1736..d914c97a1c 100644 --- a/src/applications/herald/action/HeraldAction.php +++ b/src/applications/herald/action/HeraldAction.php @@ -405,4 +405,8 @@ abstract class HeraldAction extends Phobject { return array(); } + public function isActionAvailable() { + return true; + } + } diff --git a/src/applications/herald/adapter/HeraldAdapter.php b/src/applications/herald/adapter/HeraldAdapter.php index cb8545b71d..2832c6d9f4 100644 --- a/src/applications/herald/adapter/HeraldAdapter.php +++ b/src/applications/herald/adapter/HeraldAdapter.php @@ -373,6 +373,16 @@ abstract class HeraldAdapter extends Phobject { return $field->getFieldGroupKey(); } + public function isFieldAvailable($field_key) { + $field = $this->getFieldImplementation($field_key); + + if (!$field) { + return null; + } + + return $field->isFieldAvailable(); + } + /* -( Conditions )--------------------------------------------------------- */ @@ -765,6 +775,16 @@ abstract class HeraldAdapter extends Phobject { return $action->getActionGroupKey(); } + public function isActionAvailable($action_key) { + $action = $this->getActionImplementation($action_key); + + if (!$action) { + return null; + } + + return $action->isActionAvailable(); + } + public function getActions($rule_type) { $actions = array(); foreach ($this->getActionsForRuleType($rule_type) as $key => $action) { diff --git a/src/applications/herald/controller/HeraldRuleController.php b/src/applications/herald/controller/HeraldRuleController.php index d05ed2d525..5e13522b6e 100644 --- a/src/applications/herald/controller/HeraldRuleController.php +++ b/src/applications/herald/controller/HeraldRuleController.php @@ -404,8 +404,8 @@ final class HeraldRuleController extends HeraldController { HeraldAdapter $adapter) { $all_rules = $this->loadRulesThisRuleMayDependUpon($rule); - $all_rules = mpull($all_rules, 'getName', 'getPHID'); - asort($all_rules); + $all_rules = msortv($all_rules, 'getEditorSortVector'); + $all_rules = mpull($all_rules, 'getEditorDisplayName', 'getPHID'); $all_fields = $adapter->getFieldNameMap(); $all_conditions = $adapter->getConditionNameMap(); @@ -674,15 +674,6 @@ final class HeraldRuleController extends HeraldController { ->execute(); } - // mark disabled rules as disabled since they are not useful as such; - // don't filter though to keep edit cases sane / expected - foreach ($all_rules as $current_rule) { - if ($current_rule->getIsDisabled()) { - $current_rule->makeEphemeral(); - $current_rule->setName($rule->getName().' '.pht('(Disabled)')); - } - } - // A rule can not depend upon itself. unset($all_rules[$rule->getID()]); @@ -693,7 +684,10 @@ final class HeraldRuleController extends HeraldController { $group_map = array(); foreach ($field_map as $field_key => $field_name) { $group_key = $adapter->getFieldGroupKey($field_key); - $group_map[$group_key][$field_key] = $field_name; + $group_map[$group_key][$field_key] = array( + 'name' => $field_name, + 'available' => $adapter->isFieldAvailable($field_key), + ); } return $this->getGroups( @@ -705,7 +699,10 @@ final class HeraldRuleController extends HeraldController { $group_map = array(); foreach ($action_map as $action_key => $action_name) { $group_key = $adapter->getActionGroupKey($action_key); - $group_map[$group_key][$action_key] = $action_name; + $group_map[$group_key][$action_key] = array( + 'name' => $action_name, + 'available' => $adapter->isActionAvailable($action_key), + ); } return $this->getGroups( diff --git a/src/applications/herald/engineextension/HeraldRuleIndexEngineExtension.php b/src/applications/herald/engineextension/HeraldRuleIndexEngineExtension.php index 5df229ccd6..ca28116d4c 100644 --- a/src/applications/herald/engineextension/HeraldRuleIndexEngineExtension.php +++ b/src/applications/herald/engineextension/HeraldRuleIndexEngineExtension.php @@ -37,6 +37,20 @@ final class HeraldRuleIndexEngineExtension $phids = array(); + $fields = HeraldField::getAllFields(); + foreach ($rule->getConditions() as $condition_record) { + $field = idx($fields, $condition_record->getFieldName()); + + if (!$field) { + continue; + } + + $affected_phids = $field->getPHIDsAffectedByCondition($condition_record); + foreach ($affected_phids as $phid) { + $phids[] = $phid; + } + } + $actions = HeraldAction::getAllActions(); foreach ($rule->getActions() as $action_record) { $action = idx($actions, $action_record->getAction()); diff --git a/src/applications/herald/field/HeraldField.php b/src/applications/herald/field/HeraldField.php index 611ea2cdb3..2a1e89d558 100644 --- a/src/applications/herald/field/HeraldField.php +++ b/src/applications/herald/field/HeraldField.php @@ -176,6 +176,29 @@ abstract class HeraldField extends Phobject { return $value_type->renderEditorValue($value); } + public function getPHIDsAffectedByCondition(HeraldCondition $condition) { + try { + $standard_type = $this->getHeraldFieldStandardType(); + } catch (PhutilMethodNotImplementedException $ex) { + $standard_type = null; + } + + switch ($standard_type) { + case self::STANDARD_PHID: + case self::STANDARD_PHID_NULLABLE: + case self::STANDARD_PHID_LIST: + $phids = $condition->getValue(); + + if (!is_array($phids)) { + $phids = array(); + } + + return $phids; + } + + return array(); + } + final public function setAdapter(HeraldAdapter $adapter) { $this->adapter = $adapter; return $this; @@ -218,4 +241,8 @@ abstract class HeraldField extends Phobject { return false; } + public function isFieldAvailable() { + return true; + } + } diff --git a/src/applications/herald/storage/HeraldRule.php b/src/applications/herald/storage/HeraldRule.php index a9c131e717..07be991bda 100644 --- a/src/applications/herald/storage/HeraldRule.php +++ b/src/applications/herald/storage/HeraldRule.php @@ -259,6 +259,22 @@ final class HeraldRule extends HeraldDAO return '/'.$this->getMonogram(); } + public function getEditorSortVector() { + return id(new PhutilSortVector()) + ->addInt($this->getIsDisabled() ? 1 : 0) + ->addString($this->getName()); + } + + public function getEditorDisplayName() { + $name = pht('%s %s', $this->getMonogram(), $this->getName()); + + if ($this->getIsDisabled()) { + $name = pht('%s (Disabled)', $name); + } + + return $name; + } + /* -( Repetition Policies )------------------------------------------------ */ diff --git a/src/applications/legalpad/herald/LegalpadRequireSignatureHeraldAction.php b/src/applications/legalpad/herald/LegalpadRequireSignatureHeraldAction.php index 7ff69d37d5..b477c2ce35 100644 --- a/src/applications/legalpad/herald/LegalpadRequireSignatureHeraldAction.php +++ b/src/applications/legalpad/herald/LegalpadRequireSignatureHeraldAction.php @@ -130,4 +130,9 @@ final class LegalpadRequireSignatureHeraldAction 'Require document signatures: %s.', $this->renderHandleList($value)); } + + public function isActionAvailable() { + return id(new PhabricatorLegalpadApplication())->isInstalled(); + } + } diff --git a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php index 077fc511ce..60a41a26ca 100644 --- a/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php +++ b/src/applications/maniphest/config/PhabricatorManiphestConfigOptions.php @@ -344,6 +344,8 @@ dictionary with these keys: - `children` //Optional map.// Configure options shown to the user when they "Create Subtask". See below. - `fields` //Optional map.// Configure field behaviors. See below. + - `mutations` //Optional list.// Configure which subtypes this subtype + can easily be converted to by using the "Change Subtype" action. See below. Each subtype must have a unique key, and you must define a subtype with the key "%s", which is used as a default subtype. @@ -404,15 +406,15 @@ The `fields` key can configure the behavior of custom fields on specific task subtypes. For example: ``` -{ - ... - "fields": { - "custom.some-field": { - "disabled": true + { + ... + "fields": { + "custom.some-field": { + "disabled": true + } } + ... } - ... -} ``` Each field supports these options: @@ -421,6 +423,31 @@ Each field supports these options: subtypes. - `name` //Optional string.// Custom name of this field for the subtype. + +The `mutations` key allows you to control the behavior of the "Change Subtype" +action above the comment area. By default, this action allows users to change +the task subtype into any other subtype. + +If you'd prefer to make it more difficult to change subtypes or offer only a +subset of subtypes, you can specify the list of subtypes that "Change Subtypes" +offers. For example, if you have several similar subtypes and want to allow +tasks to be converted between them but not easily converted to other types, +you can make the "Change Subtypes" control show only these options like this: + +``` + { + ... + "mutations": ["bug", "issue", "defect"] + ... + } +``` + +If you specify an empty list, the "Change Subtypes" action will be completely +hidden. + +This mutation list is advisory and only configures the UI. Tasks may still be +converted across subtypes freely by using the Bulk Editor or API. + EOTEXT , $subtype_default_key)); diff --git a/src/applications/maniphest/storage/ManiphestTask.php b/src/applications/maniphest/storage/ManiphestTask.php index d2700895ce..c56d8fe57a 100644 --- a/src/applications/maniphest/storage/ManiphestTask.php +++ b/src/applications/maniphest/storage/ManiphestTask.php @@ -564,7 +564,8 @@ final class ManiphestTask extends ManiphestDAO public function newEditEngineSubtypeMap() { $config = PhabricatorEnv::getEnvConfig('maniphest.subtypes'); - return PhabricatorEditEngineSubtype::newSubtypeMap($config); + return PhabricatorEditEngineSubtype::newSubtypeMap($config) + ->setDatasource(new ManiphestTaskSubtypeDatasource()); } diff --git a/src/applications/meta/engineextension/PhabricatorDatasourceURIEngineExtension.php b/src/applications/meta/engineextension/PhabricatorDatasourceURIEngineExtension.php index 147aa9635f..16d8dd9315 100644 --- a/src/applications/meta/engineextension/PhabricatorDatasourceURIEngineExtension.php +++ b/src/applications/meta/engineextension/PhabricatorDatasourceURIEngineExtension.php @@ -26,7 +26,17 @@ final class PhabricatorDatasourceURIEngineExtension ->setProtocol(null) ->setPort(null); - return phutil_string_cast($uri); + $uri = phutil_string_cast($uri); + + // See T13412. If the URI was in the form "http://dev.example.com" with + // no trailing slash, there may be no path. Redirecting to the empty + // string is considered an error by safety checks during redirection, + // so treat this like the user entered the URI with a trailing slash. + if (!strlen($uri)) { + $uri = '/'; + } + + return $uri; } return null; diff --git a/src/applications/meta/xactions/PhabricatorApplicationPolicyChangeTransaction.php b/src/applications/meta/xactions/PhabricatorApplicationPolicyChangeTransaction.php index 5364a3d4fa..94e1cc495d 100644 --- a/src/applications/meta/xactions/PhabricatorApplicationPolicyChangeTransaction.php +++ b/src/applications/meta/xactions/PhabricatorApplicationPolicyChangeTransaction.php @@ -52,28 +52,22 @@ final class PhabricatorApplicationPolicyChangeTransaction } public function getTitle() { - $old = $this->renderApplicationPolicy($this->getOldValue()); - $new = $this->renderApplicationPolicy($this->getNewValue()); - return pht( - '%s changed the "%s" policy from "%s" to "%s".', + '%s changed the %s policy from %s to %s.', $this->renderAuthor(), $this->renderCapability(), - $old, - $new); + $this->renderOldPolicy(), + $this->renderNewPolicy()); } public function getTitleForFeed() { - $old = $this->renderApplicationPolicy($this->getOldValue()); - $new = $this->renderApplicationPolicy($this->getNewValue()); - return pht( - '%s changed the "%s" policy for application %s from "%s" to "%s".', + '%s changed the %s policy for application %s from %s to %s.', $this->renderAuthor(), $this->renderCapability(), $this->renderObject(), - $old, - $new); + $this->renderOldPolicy(), + $this->renderNewPolicy()); } public function validateTransactions($object, array $xactions) { @@ -165,38 +159,11 @@ final class PhabricatorApplicationPolicyChangeTransaction return $errors; } - private function renderApplicationPolicy($name) { - $policies = $this->getAllPolicies(); - if (empty($policies[$name])) { - // Not a standard policy, check for a custom policy. - $policy = id(new PhabricatorPolicyQuery()) - ->setViewer($this->getViewer()) - ->withPHIDs(array($name)) - ->executeOne(); - $policies[$name] = $policy; - } - - $policy = idx($policies, $name); - return $this->renderValue($policy->getFullName()); - } - - private function getAllPolicies() { - if (!$this->policies) { - $viewer = $this->getViewer(); - $application = $this->getObject(); - $this->policies = id(new PhabricatorPolicyQuery()) - ->setViewer($viewer) - ->setObject($application) - ->execute(); - } - - return $this->policies; - } - private function renderCapability() { $application = $this->getObject(); $capability = $this->getCapabilityName(); - return $application->getCapabilityLabel($capability); + $label = $application->getCapabilityLabel($capability); + return $this->renderValue($label); } private function getCapabilityName() { diff --git a/src/applications/passphrase/controller/PassphraseCredentialViewController.php b/src/applications/passphrase/controller/PassphraseCredentialViewController.php index aabb3821e0..6688bef285 100644 --- a/src/applications/passphrase/controller/PassphraseCredentialViewController.php +++ b/src/applications/passphrase/controller/PassphraseCredentialViewController.php @@ -190,14 +190,6 @@ final class PassphraseCredentialViewController extends PassphraseController { pht('Credential Type'), $type->getCredentialTypeName()); - $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( - $viewer, - $credential); - - $properties->addProperty( - pht('Editable By'), - $descriptions[PhabricatorPolicyCapability::CAN_EDIT]); - if ($type->shouldRequireUsername()) { $properties->addProperty( pht('Username'), diff --git a/src/applications/phame/controller/blog/PhameBlogManageController.php b/src/applications/phame/controller/blog/PhameBlogManageController.php index 2bdcbf7635..65378d91cb 100644 --- a/src/applications/phame/controller/blog/PhameBlogManageController.php +++ b/src/applications/phame/controller/blog/PhameBlogManageController.php @@ -143,14 +143,6 @@ final class PhameBlogManageController extends PhameBlogController { ), $feed_uri)); - $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( - $viewer, - $blog); - - $properties->addProperty( - pht('Editable By'), - $descriptions[PhabricatorPolicyCapability::CAN_EDIT]); - $engine = id(new PhabricatorMarkupEngine()) ->setViewer($viewer) ->addObject($blog, PhameBlog::MARKUP_FIELD_DESCRIPTION) diff --git a/src/applications/phriction/controller/PhrictionDocumentController.php b/src/applications/phriction/controller/PhrictionDocumentController.php index bf7282b35a..5d30b5a90b 100644 --- a/src/applications/phriction/controller/PhrictionDocumentController.php +++ b/src/applications/phriction/controller/PhrictionDocumentController.php @@ -293,37 +293,6 @@ final class PhrictionDocumentController } else { throw new Exception(pht("Unknown document status '%s'!", $doc_status)); } - - $move_notice = null; - if ($current_status == PhrictionChangeType::CHANGE_MOVE_HERE) { - $from_doc_id = $content->getChangeRef(); - - $slug_uri = null; - - // If the old document exists and is visible, provide a link to it. - $from_docs = id(new PhrictionDocumentQuery()) - ->setViewer($viewer) - ->withIDs(array($from_doc_id)) - ->execute(); - if ($from_docs) { - $from_doc = head($from_docs); - $slug_uri = PhrictionDocument::getSlugURI($from_doc->getSlug()); - } - - $move_notice = id(new PHUIInfoView()) - ->setSeverity(PHUIInfoView::SEVERITY_NOTICE); - - if ($slug_uri) { - $move_notice->appendChild( - pht( - 'This document was moved from %s.', - phutil_tag('a', array('href' => $slug_uri), $slug_uri))); - } else { - // Render this for consistency, even though it's a bit silly. - $move_notice->appendChild( - pht('This document was moved from elsewhere.')); - } - } } $children = $this->renderDocumentChildren($slug); diff --git a/src/applications/phriction/xaction/PhrictionDocumentMoveToTransaction.php b/src/applications/phriction/xaction/PhrictionDocumentMoveToTransaction.php index d18c436cd0..078e9a0130 100644 --- a/src/applications/phriction/xaction/PhrictionDocumentMoveToTransaction.php +++ b/src/applications/phriction/xaction/PhrictionDocumentMoveToTransaction.php @@ -49,7 +49,7 @@ final class PhrictionDocumentMoveToTransaction $new = $this->getNewValue(); return pht( - '%s moved this document from %s', + '%s moved this document from %s.', $this->renderAuthor(), $this->renderHandle($new['phid'])); } diff --git a/src/applications/policy/controller/PhabricatorPolicyExplainController.php b/src/applications/policy/controller/PhabricatorPolicyExplainController.php index a4ba355b16..cb9bd20088 100644 --- a/src/applications/policy/controller/PhabricatorPolicyExplainController.php +++ b/src/applications/policy/controller/PhabricatorPolicyExplainController.php @@ -318,7 +318,7 @@ final class PhabricatorPolicyExplainController ->setViewer($viewer) ->setIcon($handle->getIcon().' bluegrey') ->setHeader(pht('Object Policy')) - ->appendList( + ->appendParagraph( array( array( phutil_tag('strong', array(), pht('%s:', $capability_name)), @@ -333,10 +333,17 @@ final class PhabricatorPolicyExplainController ->appendList( array( PhabricatorPolicy::getPolicyExplanation( - $viewer, - $policy->getPHID()), + $viewer, + $policy->getPHID()), )); + if ($policy->isCustomPolicy()) { + $rules_view = id(new PhabricatorPolicyRulesView()) + ->setViewer($viewer) + ->setPolicy($policy); + $object_section->appendRulesView($rules_view); + } + $strength = $this->getStrengthInformation($object, $policy, $capability); if ($strength) { $object_section->appendHint($strength); diff --git a/src/applications/policy/filter/PhabricatorPolicyFilter.php b/src/applications/policy/filter/PhabricatorPolicyFilter.php index 4ea7ce1549..d8f239e51a 100644 --- a/src/applications/policy/filter/PhabricatorPolicyFilter.php +++ b/src/applications/policy/filter/PhabricatorPolicyFilter.php @@ -602,12 +602,13 @@ final class PhabricatorPolicyFilter extends Phobject { PhabricatorPolicyInterface $object, $policy, $capability) { + $viewer = $this->viewer; if (!$this->raisePolicyExceptions) { return; } - if ($this->viewer->isOmnipotent()) { + if ($viewer->isOmnipotent()) { // Never raise policy exceptions for the omnipotent viewer. Although we // will never normally issue a policy rejection for the omnipotent // viewer, we can end up here when queries blanket reject objects that @@ -634,9 +635,60 @@ final class PhabricatorPolicyFilter extends Phobject { $capability); } - $more = PhabricatorPolicy::getPolicyExplanation($this->viewer, $policy); - $more = (array)$more; - $more = array_filter($more); + // See T13411. If you receive a policy exception because you can't view + // an object, we also want to avoid disclosing too many details about the + // actual policy (for example, the names of projects in the policy). + + // If you failed a "CAN_VIEW" check, or failed some other check and don't + // have "CAN_VIEW" on the object, we give you an "opaque" explanation. + // Otherwise, we give you a more detailed explanation. + + $view_capability = PhabricatorPolicyCapability::CAN_VIEW; + if ($capability === $view_capability) { + $show_details = false; + } else { + $show_details = self::hasCapability( + $viewer, + $object, + $view_capability); + } + + // TODO: This is a bit clumsy. We're producing HTML and text versions of + // this message, but can't render the full policy rules in text today. + // Users almost never get a text-only version of this exception anyway. + + $head = null; + $more = null; + + if ($show_details) { + $head = PhabricatorPolicy::getPolicyExplanation($viewer, $policy); + + $policy_type = PhabricatorPolicyPHIDTypePolicy::TYPECONST; + $is_custom = (phid_get_type($policy) === $policy_type); + if ($is_custom) { + $policy_map = PhabricatorPolicyQuery::loadPolicies( + $viewer, + $object); + if (isset($policy_map[$capability])) { + require_celerity_resource('phui-policy-section-view-css'); + + $more = id(new PhabricatorPolicyRulesView()) + ->setViewer($viewer) + ->setPolicy($policy_map[$capability]); + + $more = phutil_tag( + 'div', + array( + 'class' => 'phui-policy-section-view-rules', + ), + $more); + } + } + } else { + $head = PhabricatorPolicy::getOpaquePolicyExplanation($viewer, $policy); + } + + $head = (array)$head; $exceptions = PhabricatorPolicy::getSpecialRules( $object, @@ -644,7 +696,10 @@ final class PhabricatorPolicyFilter extends Phobject { $capability, true); - $details = array_filter(array_merge($more, $exceptions)); + $text_details = array_filter(array_merge($head, $exceptions)); + $text_details = implode(' ', $text_details); + + $html_details = array($head, $more, $exceptions); $access_denied = $this->renderAccessDenied($object); @@ -653,7 +708,7 @@ final class PhabricatorPolicyFilter extends Phobject { $access_denied, $capability_name, $rejection, - implode(' ', $details)); + $text_details); $exception = id(new PhabricatorPolicyException($full_message)) ->setTitle($access_denied) @@ -661,7 +716,7 @@ final class PhabricatorPolicyFilter extends Phobject { ->setRejection($rejection) ->setCapability($capability) ->setCapabilityName($capability_name) - ->setMoreInfo($details); + ->setMoreInfo($html_details); throw $exception; } diff --git a/src/applications/policy/management/PhabricatorPolicyManagementShowWorkflow.php b/src/applications/policy/management/PhabricatorPolicyManagementShowWorkflow.php index 208f1ae964..a378ecc076 100644 --- a/src/applications/policy/management/PhabricatorPolicyManagementShowWorkflow.php +++ b/src/applications/policy/management/PhabricatorPolicyManagementShowWorkflow.php @@ -60,8 +60,10 @@ final class PhabricatorPolicyManagementShowWorkflow $console->writeOut("__%s__\n\n", pht('CAPABILITIES')); foreach ($policies as $capability => $policy) { + $ref = $policy->newRef($viewer); + $console->writeOut(" **%s**\n", $capability); - $console->writeOut(" %s\n", $policy->renderDescription()); + $console->writeOut(" %s\n", $ref->getPolicyDisplayName()); $console->writeOut(" %s\n", PhabricatorPolicy::getPolicyExplanation($viewer, $policy->getPHID())); $console->writeOut("\n"); diff --git a/src/applications/policy/query/PhabricatorPolicyQuery.php b/src/applications/policy/query/PhabricatorPolicyQuery.php index e51b2ca401..018007db28 100644 --- a/src/applications/policy/query/PhabricatorPolicyQuery.php +++ b/src/applications/policy/query/PhabricatorPolicyQuery.php @@ -43,13 +43,13 @@ final class PhabricatorPolicyQuery public static function renderPolicyDescriptions( PhabricatorUser $viewer, - PhabricatorPolicyInterface $object, - $icon = false) { + PhabricatorPolicyInterface $object) { $policies = self::loadPolicies($viewer, $object); foreach ($policies as $capability => $policy) { - $policies[$capability] = $policy->renderDescription($icon); + $policies[$capability] = $policy->newRef($viewer) + ->newCapabilityLink($object, $capability); } return $policies; diff --git a/src/applications/policy/storage/PhabricatorPolicy.php b/src/applications/policy/storage/PhabricatorPolicy.php index 2df8fdf6a0..66a7d9e3be 100644 --- a/src/applications/policy/storage/PhabricatorPolicy.php +++ b/src/applications/policy/storage/PhabricatorPolicy.php @@ -85,8 +85,10 @@ final class PhabricatorPolicy $phid_type = phid_get_type($policy_identifier); switch ($phid_type) { case PhabricatorProjectProjectPHIDType::TYPECONST: - $policy->setType(PhabricatorPolicyType::TYPE_PROJECT); - $policy->setName($handle->getName()); + $policy + ->setType(PhabricatorPolicyType::TYPE_PROJECT) + ->setName($handle->getName()) + ->setIcon($handle->getIcon()); break; case PhabricatorPeopleUserPHIDType::TYPECONST: $policy->setType(PhabricatorPolicyType::TYPE_USER); @@ -218,6 +220,25 @@ final class PhabricatorPolicy PhabricatorUser $viewer, $policy) { + $type = phid_get_type($policy); + if ($type === PhabricatorProjectProjectPHIDType::TYPECONST) { + $handle = id(new PhabricatorHandleQuery()) + ->setViewer($viewer) + ->withPHIDs(array($policy)) + ->executeOne(); + + return pht( + 'Members of the project "%s" can take this action.', + $handle->getFullName()); + } + + return self::getOpaquePolicyExplanation($viewer, $policy); + } + + public static function getOpaquePolicyExplanation( + PhabricatorUser $viewer, + $policy) { + $rule = PhabricatorPolicyQuery::getObjectPolicyRule($policy); if ($rule) { return $rule->getPolicyExplanation(); @@ -243,7 +264,9 @@ final class PhabricatorPolicy $type = phid_get_type($policy); if ($type == PhabricatorProjectProjectPHIDType::TYPECONST) { return pht( - 'Members of the project "%s" can take this action.', + 'Members of a particular project can take this action. (You '. + 'can not see this object, so the name of this project is '. + 'restricted.)', $handle->getFullName()); } else if ($type == PhabricatorPeopleUserPHIDType::TYPECONST) { return pht( @@ -274,45 +297,22 @@ final class PhabricatorPolicy } } - public function renderDescription($icon = false) { - $img = null; - if ($icon) { - $img = id(new PHUIIconView()) - ->setIcon($this->getIcon()); - } + public function newRef(PhabricatorUser $viewer) { + return id(new PhabricatorPolicyRef()) + ->setViewer($viewer) + ->setPolicy($this); + } - if ($this->getHref()) { - $desc = javelin_tag( - 'a', - array( - 'href' => $this->getHref(), - 'class' => 'policy-link', - 'sigil' => $this->getWorkflow() ? 'workflow' : null, - ), - array( - $img, - $this->getName(), - )); - } else { - if ($img) { - $desc = array($img, $this->getName()); - } else { - $desc = $this->getName(); - } - } + public function isProjectPolicy() { + return ($this->getType() === PhabricatorPolicyType::TYPE_PROJECT); + } - switch ($this->getType()) { - case PhabricatorPolicyType::TYPE_PROJECT: - return pht('%s (Project)', $desc); - case PhabricatorPolicyType::TYPE_CUSTOM: - return $desc; - case PhabricatorPolicyType::TYPE_MASKED: - return pht( - '%s (You do not have permission to view policy details.)', - $desc); - default: - return $desc; - } + public function isCustomPolicy() { + return ($this->getType() === PhabricatorPolicyType::TYPE_CUSTOM); + } + + public function isMaskedPolicy() { + return ($this->getType() === PhabricatorPolicyType::TYPE_MASKED); } /** diff --git a/src/applications/policy/view/PHUIPolicySectionView.php b/src/applications/policy/view/PHUIPolicySectionView.php index 471e5035fb..14d97fb17e 100644 --- a/src/applications/policy/view/PHUIPolicySectionView.php +++ b/src/applications/policy/view/PHUIPolicySectionView.php @@ -93,6 +93,16 @@ final class PHUIPolicySectionView return $this->appendChild(phutil_tag('p', array(), $content)); } + public function appendRulesView(PhabricatorPolicyRulesView $rules_view) { + return $this->appendChild( + phutil_tag( + 'div', + array( + 'class' => 'phui-policy-section-view-rules', + ), + $rules_view)); + } + protected function getTagAttributes() { return array( 'class' => 'phui-policy-section-view', @@ -100,7 +110,7 @@ final class PHUIPolicySectionView } protected function getTagContent() { - require_celerity_resource('phui-header-view-css'); + require_celerity_resource('phui-policy-section-view-css'); $icon_view = null; $icon = $this->getIcon(); diff --git a/src/applications/policy/view/PhabricatorPolicyRef.php b/src/applications/policy/view/PhabricatorPolicyRef.php new file mode 100644 index 0000000000..605d59ee45 --- /dev/null +++ b/src/applications/policy/view/PhabricatorPolicyRef.php @@ -0,0 +1,99 @@ +viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + public function setPolicy(PhabricatorPolicy $policy) { + $this->policy = $policy; + return $this; + } + + public function getPolicy() { + return $this->policy; + } + + public function getPolicyDisplayName() { + $policy = $this->getPolicy(); + return $policy->getFullName(); + } + + public function newTransactionLink( + $mode, + PhabricatorApplicationTransaction $xaction) { + + $policy = $this->getPolicy(); + + if ($policy->isCustomPolicy()) { + $uri = urisprintf( + '/transactions/%s/%s/', + $mode, + $xaction->getPHID()); + $workflow = true; + } else { + $uri = $policy->getHref(); + $workflow = false; + } + + return $this->newLink($uri, $workflow); + } + + public function newCapabilityLink($object, $capability) { + $policy = $this->getPolicy(); + + $uri = urisprintf( + '/policy/explain/%s/%s/', + $object->getPHID(), + $capability); + + return $this->newLink($uri, true); + } + + private function newLink($uri, $workflow) { + $policy = $this->getPolicy(); + $name = $policy->getName(); + + if ($uri !== null) { + $name = javelin_tag( + 'a', + array( + 'href' => $uri, + 'sigil' => ($workflow ? 'workflow' : null), + ), + $name); + } + + $hint = $this->getPolicyTypeHint(); + if ($hint !== null) { + $name = pht('%s (%s)', $name, $hint); + } + + return $name; + } + + private function getPolicyTypeHint() { + $policy = $this->getPolicy(); + + if ($policy->isProjectPolicy()) { + return pht('Project'); + } + + if ($policy->isMaskedPolicy()) { + return pht('You do not have permission to view policy details.'); + } + + return null; + } + +} diff --git a/src/applications/policy/view/PhabricatorPolicyRulesView.php b/src/applications/policy/view/PhabricatorPolicyRulesView.php new file mode 100644 index 0000000000..657b612f16 --- /dev/null +++ b/src/applications/policy/view/PhabricatorPolicyRulesView.php @@ -0,0 +1,84 @@ +policy = $policy; + return $this; + } + + public function getPolicy() { + return $this->policy; + } + + public function render() { + $policy = $this->getPolicy(); + + require_celerity_resource('policy-transaction-detail-css'); + + $rule_objects = array(); + foreach ($policy->getCustomRuleClasses() as $class) { + $rule_objects[$class] = newv($class, array()); + } + + $policy = clone $policy; + $policy->attachRuleObjects($rule_objects); + + $details = array(); + $details[] = phutil_tag( + 'p', + array( + 'class' => 'policy-transaction-detail-intro', + ), + pht('These rules are processed in order:')); + + foreach ($policy->getRules() as $index => $rule) { + $rule_object = $rule_objects[$rule['rule']]; + if ($rule['action'] == 'allow') { + $icon = 'fa-check-circle green'; + } else { + $icon = 'fa-minus-circle red'; + } + $icon = id(new PHUIIconView()) + ->setIcon($icon) + ->setText( + ucfirst($rule['action']).' '.$rule_object->getRuleDescription()); + + $handle_phids = $rule_object->getRequiredHandlePHIDsForSummary( + $rule['value']); + if ($handle_phids) { + $value = $this->getViewer() + ->renderHandleList($handle_phids) + ->setAsInline(true); + } else { + $value = $rule['value']; + } + + $details[] = phutil_tag('div', + array( + 'class' => 'policy-transaction-detail-row', + ), + array( + $icon, + $value, + )); + } + + $details[] = phutil_tag( + 'p', + array( + 'class' => 'policy-transaction-detail-end', + ), + pht( + 'If no rules match, %s all other users.', + phutil_tag('b', + array(), + $policy->getDefaultAction()))); + + return $details; + } + +} diff --git a/src/applications/project/chart/PhabricatorProjectActivityChartEngine.php b/src/applications/project/chart/PhabricatorProjectActivityChartEngine.php new file mode 100644 index 0000000000..7fc599317f --- /dev/null +++ b/src/applications/project/chart/PhabricatorProjectActivityChartEngine.php @@ -0,0 +1,135 @@ +setEngineParameter('projectPHIDs', $project_phids); + } + + protected function newChart(PhabricatorFactChart $chart, array $map) { + $viewer = $this->getViewer(); + + $map = $map + array( + 'projectPHIDs' => array(), + ); + + if ($map['projectPHIDs']) { + $projects = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withPHIDs($map['projectPHIDs']) + ->execute(); + $project_phids = mpull($projects, 'getPHID'); + } else { + $project_phids = array(); + } + + $project_phid = head($project_phids); + + $functions = array(); + $stacks = array(); + + $function = $this->newFunction( + array( + 'accumulate', + array( + 'compose', + array('fact', 'tasks.open-count.assign.project', $project_phid), + array('min', 0), + ), + )); + + $function->getFunctionLabel() + ->setKey('moved-in') + ->setName(pht('Tasks Moved Into Project')) + ->setColor('rgba(128, 128, 200, 1)') + ->setFillColor('rgba(128, 128, 200, 0.15)'); + + $functions[] = $function; + + $function = $this->newFunction( + array( + 'accumulate', + array( + 'compose', + array('fact', 'tasks.open-count.status.project', $project_phid), + array('min', 0), + ), + )); + + $function->getFunctionLabel() + ->setKey('reopened') + ->setName(pht('Tasks Reopened')) + ->setColor('rgba(128, 128, 200, 1)') + ->setFillColor('rgba(128, 128, 200, 0.15)'); + + $functions[] = $function; + + $function = $this->newFunction( + array( + 'accumulate', + array('fact', 'tasks.open-count.create.project', $project_phid), + )); + + $function->getFunctionLabel() + ->setKey('created') + ->setName(pht('Tasks Created')) + ->setColor('rgba(0, 0, 200, 1)') + ->setFillColor('rgba(0, 0, 200, 0.15)'); + + $functions[] = $function; + + $function = $this->newFunction( + array( + 'accumulate', + array( + 'compose', + array('fact', 'tasks.open-count.status.project', $project_phid), + array('max', 0), + ), + )); + + $function->getFunctionLabel() + ->setKey('closed') + ->setName(pht('Tasks Closed')) + ->setColor('rgba(0, 200, 0, 1)') + ->setFillColor('rgba(0, 200, 0, 0.15)'); + + $functions[] = $function; + + $function = $this->newFunction( + array( + 'accumulate', + array( + 'compose', + array('fact', 'tasks.open-count.assign.project', $project_phid), + array('max', 0), + ), + )); + + $function->getFunctionLabel() + ->setKey('moved-out') + ->setName(pht('Tasks Moved Out of Project')) + ->setColor('rgba(128, 200, 128, 1)') + ->setFillColor('rgba(128, 200, 128, 0.15)'); + + $functions[] = $function; + + $stacks[] = array('created', 'reopened', 'moved-in'); + $stacks[] = array('closed', 'moved-out'); + + $datasets = array(); + + $dataset = id(new PhabricatorChartStackedAreaDataset()) + ->setFunctions($functions) + ->setStacks($stacks); + + $datasets[] = $dataset; + $chart->attachDatasets($datasets); + } + +} diff --git a/src/applications/project/chart/PhabricatorProjectBurndownChartEngine.php b/src/applications/project/chart/PhabricatorProjectBurndownChartEngine.php index 092e921c5a..1296f2eec8 100644 --- a/src/applications/project/chart/PhabricatorProjectBurndownChartEngine.php +++ b/src/applications/project/chart/PhabricatorProjectBurndownChartEngine.php @@ -30,97 +30,78 @@ final class PhabricatorProjectBurndownChartEngine $functions = array(); if ($project_phids) { - foreach ($project_phids as $project_phid) { - $function = $this->newFunction( - 'min', + $open_function = $this->newFunction( + array( + 'accumulate', array( - 'accumulate', - array('fact', 'tasks.open-count.assign.project', $project_phid), + 'sum', + $this->newFactSum( + 'tasks.open-count.create.project', $project_phids), + $this->newFactSum( + 'tasks.open-count.status.project', $project_phids), + $this->newFactSum( + 'tasks.open-count.assign.project', $project_phids), ), - 0); + )); - $function->getFunctionLabel() - ->setName(pht('Tasks Moved Into Project')) - ->setColor('rgba(0, 200, 200, 1)') - ->setFillColor('rgba(0, 200, 200, 0.15)'); - - $functions[] = $function; - - $function = $this->newFunction( - 'min', - array( - 'accumulate', - array('fact', 'tasks.open-count.status.project', $project_phid), - ), - 0); - - $function->getFunctionLabel() - ->setName(pht('Tasks Reopened')) - ->setColor('rgba(200, 0, 200, 1)') - ->setFillColor('rgba(200, 0, 200, 0.15)'); - - $functions[] = $function; - - $function = $this->newFunction( - 'sum', - array( - 'accumulate', - array('fact', 'tasks.open-count.create.project', $project_phid), - ), - array( - 'max', - array( - 'accumulate', - array('fact', 'tasks.open-count.status.project', $project_phid), - ), - 0, - ), - array( - 'max', - array( - 'accumulate', - array('fact', 'tasks.open-count.assign.project', $project_phid), - ), - 0, - )); - - $function->getFunctionLabel() - ->setName(pht('Tasks Created')) - ->setColor('rgba(0, 0, 200, 1)') - ->setFillColor('rgba(0, 0, 200, 0.15)'); - - $functions[] = $function; - } + $closed_function = $this->newFunction( + array( + 'accumulate', + $this->newFactSum('tasks.open-count.status.project', $project_phids), + )); } else { - $function = $this->newFunction( - 'accumulate', - array('fact', 'tasks.open-count.create')); + $open_function = $this->newFunction( + array( + 'accumulate', + array('fact', 'tasks.open-count.create'), + )); - $function->getFunctionLabel() - ->setName(pht('Tasks Created')) - ->setColor('rgba(0, 200, 200, 1)') - ->setFillColor('rgba(0, 200, 200, 0.15)'); - - $functions[] = $function; - - $function = $this->newFunction( - 'accumulate', - array('fact', 'tasks.open-count.status')); - - $function->getFunctionLabel() - ->setName(pht('Tasks Closed / Reopened')) - ->setColor('rgba(200, 0, 200, 1)') - ->setFillColor('rgba(200, 0, 200, 0.15)'); - - $functions[] = $function; + $closed_function = $this->newFunction( + array( + 'accumulate', + array('fact', 'tasks.open-count.status'), + )); } + $open_function->getFunctionLabel() + ->setKey('open') + ->setName(pht('Open Tasks')) + ->setColor('rgba(0, 0, 200, 1)') + ->setFillColor('rgba(0, 0, 200, 0.15)'); + + $closed_function->getFunctionLabel() + ->setKey('closed') + ->setName(pht('Closed Tasks')) + ->setColor('rgba(0, 200, 0, 1)') + ->setFillColor('rgba(0, 200, 0, 0.15)'); + $datasets = array(); - $datasets[] = id(new PhabricatorChartStackedAreaDataset()) - ->setFunctions($functions); + $dataset = id(new PhabricatorChartStackedAreaDataset()) + ->setFunctions( + array( + $open_function, + $closed_function, + )) + ->setStacks( + array( + array('open'), + array('closed'), + )); + $datasets[] = $dataset; $chart->attachDatasets($datasets); } + private function newFactSum($fact_key, array $phids) { + $result = array(); + $result[] = 'sum'; + + foreach ($phids as $phid) { + $result[] = array('fact', $fact_key, $phid); + } + + return $result; + } + } diff --git a/src/applications/project/controller/PhabricatorProjectReportsController.php b/src/applications/project/controller/PhabricatorProjectReportsController.php index bee114917b..cfd9ee3253 100644 --- a/src/applications/project/controller/PhabricatorProjectReportsController.php +++ b/src/applications/project/controller/PhabricatorProjectReportsController.php @@ -44,10 +44,24 @@ final class PhabricatorProjectReportsController ->setParentPanelPHIDs(array()) ->renderPanel(); + $activity_panel = id(new PhabricatorProjectActivityChartEngine()) + ->setViewer($viewer) + ->setProjects(array($project)) + ->buildChartPanel(); + + $activity_panel->setName(pht('%s: Activity', $project->getName())); + + $activity_view = id(new PhabricatorDashboardPanelRenderingEngine()) + ->setViewer($viewer) + ->setPanel($activity_panel) + ->setParentPanelPHIDs(array()) + ->renderPanel(); + $view = id(new PHUITwoColumnView()) ->setFooter( array( $chart_view, + $activity_view, )); return $this->newPage() diff --git a/src/applications/project/storage/PhabricatorProject.php b/src/applications/project/storage/PhabricatorProject.php index 54267829d3..1d234d4285 100644 --- a/src/applications/project/storage/PhabricatorProject.php +++ b/src/applications/project/storage/PhabricatorProject.php @@ -904,7 +904,8 @@ final class PhabricatorProject extends PhabricatorProjectDAO public function newEditEngineSubtypeMap() { $config = PhabricatorEnv::getEnvConfig('projects.subtypes'); - return PhabricatorEditEngineSubtype::newSubtypeMap($config); + return PhabricatorEditEngineSubtype::newSubtypeMap($config) + ->setDatasource(new PhabricatorProjectSubtypeDatasource()); } public function newSubtypeObject() { diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php index bd15e3e8de..fdc9a695c4 100644 --- a/src/applications/repository/storage/PhabricatorRepository.php +++ b/src/applications/repository/storage/PhabricatorRepository.php @@ -1842,6 +1842,20 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO PhabricatorUser $viewer, array $options) { + $refs = $this->getAlmanacServiceRefs($viewer, $options); + + if (!$refs) { + return null; + } + + $ref = head($refs); + return $ref->getURI(); + } + + public function getAlmanacServiceRefs( + PhabricatorUser $viewer, + array $options) { + PhutilTypeSpec::checkMap( $options, array( @@ -1856,7 +1870,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO $cache_key = $this->getAlmanacServiceCacheKey(); if (!$cache_key) { - return null; + return array(); } $cache = PhabricatorCaches::getMutableStructureCache(); @@ -1869,7 +1883,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO } if ($uris === null) { - return null; + return array(); } $local_device = AlmanacKeys::getDeviceID(); @@ -1893,7 +1907,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO if ($local_device && $never_proxy) { if ($uri['device'] == $local_device) { - return null; + return array(); } } @@ -1954,15 +1968,20 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO } } + $refs = array(); + foreach ($results as $result) { + $refs[] = DiffusionServiceRef::newFromDictionary($result); + } + // If we require a writable device, remove URIs which aren't writable. if ($writable) { - foreach ($results as $key => $uri) { - if (!$uri['writable']) { + foreach ($refs as $key => $ref) { + if (!$ref->isWritable()) { unset($results[$key]); } } - if (!$results) { + if (!$refs) { throw new Exception( pht( 'This repository ("%s") is not writable with the given '. @@ -1974,23 +1993,30 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO } if ($writable) { - $results = $this->sortWritableAlmanacServiceURIs($results); + $refs = $this->sortWritableAlmanacServiceRefs($refs); } else { - shuffle($results); + $refs = $this->sortReadableAlmanacServiceRefs($refs); } - $result = head($results); - return $result['uri']; + return array_values($refs); } - private function sortWritableAlmanacServiceURIs(array $results) { + private function sortReadableAlmanacServiceRefs(array $refs) { + assert_instances_of($refs, 'DiffusionServiceRef'); + shuffle($refs); + return $refs; + } + + private function sortWritableAlmanacServiceRefs(array $refs) { + assert_instances_of($refs, 'DiffusionServiceRef'); + // See T13109 for discussion of how this method routes requests. // In the absence of other rules, we'll send traffic to devices randomly. // We also want to select randomly among nodes which are equally good // candidates to receive the write, and accomplish that by shuffling the // list up front. - shuffle($results); + shuffle($refs); $order = array(); @@ -2002,8 +2028,8 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO $this->getPHID()); if ($writer) { $device_phid = $writer->getWriteProperty('devicePHID'); - foreach ($results as $key => $result) { - if ($result['devicePHID'] === $device_phid) { + foreach ($refs as $key => $ref) { + if ($ref->getDevicePHID() === $device_phid) { $order[] = $key; } } @@ -2025,8 +2051,8 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO } $max_devices = array_fuse($max_devices); - foreach ($results as $key => $result) { - if (isset($max_devices[$result['devicePHID']])) { + foreach ($refs as $key => $ref) { + if (isset($max_devices[$ref->getDevicePHID()])) { $order[] = $key; } } @@ -2034,9 +2060,9 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO // Reorder the results, putting any we've selected as preferred targets for // the write at the head of the list. - $results = array_select_keys($results, $order) + $results; + $refs = array_select_keys($refs, $order) + $refs; - return $results; + return $refs; } public function supportsSynchronization() { diff --git a/src/applications/settings/panel/PhabricatorSearchSettingsPanel.php b/src/applications/settings/panel/PhabricatorSearchSettingsPanel.php new file mode 100644 index 0000000000..37b0ea919d --- /dev/null +++ b/src/applications/settings/panel/PhabricatorSearchSettingsPanel.php @@ -0,0 +1,28 @@ +getViewer(), + new PhabricatorSettingsApplication(), + $only_global = true); + + $scope_map = array(); + foreach ($scopes as $scope) { + if (!isset($scope['value'])) { + continue; + } + $scope_map[$scope['value']] = $scope['name']; + } + + return $scope_map; + } + } diff --git a/src/applications/spaces/controller/PhabricatorSpacesViewController.php b/src/applications/spaces/controller/PhabricatorSpacesViewController.php index 5fa1c01143..ba55ba90a7 100644 --- a/src/applications/spaces/controller/PhabricatorSpacesViewController.php +++ b/src/applications/spaces/controller/PhabricatorSpacesViewController.php @@ -80,14 +80,6 @@ final class PhabricatorSpacesViewController ? pht('Yes') : pht('No')); - $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( - $viewer, - $space); - - $list->addProperty( - pht('Editable By'), - $descriptions[PhabricatorPolicyCapability::CAN_EDIT]); - $description = $space->getDescription(); if (strlen($description)) { $description = new PHUIRemarkupView($viewer, $description); diff --git a/src/applications/system/engine/PhabricatorSystemActionEngine.php b/src/applications/system/engine/PhabricatorSystemActionEngine.php index c097fa04a4..6d6f9eacfd 100644 --- a/src/applications/system/engine/PhabricatorSystemActionEngine.php +++ b/src/applications/system/engine/PhabricatorSystemActionEngine.php @@ -100,32 +100,34 @@ final class PhabricatorSystemActionEngine extends Phobject { $actor_hashes = array(); foreach ($actors as $actor) { - $actor_hashes[] = PhabricatorHash::digestForIndex($actor); + $digest = PhabricatorHash::digestForIndex($actor); + $actor_hashes[$digest] = $actor; } $log = new PhabricatorSystemActionLog(); $window = self::getWindow(); - $conn_r = $log->establishConnection('r'); - $scores = queryfx_all( - $conn_r, - 'SELECT actorIdentity, SUM(score) totalScore FROM %T + $conn = $log->establishConnection('r'); + + $rows = queryfx_all( + $conn, + 'SELECT actorHash, SUM(score) totalScore FROM %T WHERE action = %s AND actorHash IN (%Ls) AND epoch >= %d GROUP BY actorHash', $log->getTableName(), $action->getActionConstant(), - $actor_hashes, - (time() - $window)); + array_keys($actor_hashes), + (PhabricatorTime::getNow() - $window)); - $scores = ipull($scores, 'totalScore', 'actorIdentity'); + $rows = ipull($rows, 'totalScore', 'actorHash'); - foreach ($scores as $key => $score) { - $scores[$key] = $score / $window; + $scores = array(); + foreach ($actor_hashes as $digest => $actor) { + $score = idx($rows, $digest, 0); + $scores[$actor] = ($score / $window); } - $scores = $scores + array_fill_keys($actors, 0); - return $scores; } diff --git a/src/applications/transactions/controller/PhabricatorApplicationTransactionValueController.php b/src/applications/transactions/controller/PhabricatorApplicationTransactionValueController.php index bef6fef5a8..ef5e168898 100644 --- a/src/applications/transactions/controller/PhabricatorApplicationTransactionValueController.php +++ b/src/applications/transactions/controller/PhabricatorApplicationTransactionValueController.php @@ -33,6 +33,7 @@ final class PhabricatorApplicationTransactionValueController case PhabricatorTransactions::TYPE_EDIT_POLICY: case PhabricatorTransactions::TYPE_JOIN_POLICY: case PhabricatorRepositoryPushPolicyTransaction::TRANSACTIONTYPE: + case PhabricatorApplicationPolicyChangeTransaction::TRANSACTIONTYPE: break; default: return new Aphront404Response(); @@ -57,89 +58,16 @@ final class PhabricatorApplicationTransactionValueController return new Aphront404Response(); } - $rule_objects = array(); - foreach ($policy->getCustomRuleClasses() as $class) { - $rule_objects[$class] = newv($class, array()); - } - $policy->attachRuleObjects($rule_objects); + $rules_view = id(new PhabricatorPolicyRulesView()) + ->setViewer($viewer) + ->setPolicy($policy); - $this->requireResource('policy-transaction-detail-css'); $cancel_uri = $this->guessCancelURI($viewer, $xaction); return $this->newDialog() ->setTitle($policy->getFullName()) ->setWidth(AphrontDialogView::WIDTH_FORM) - ->appendChild($this->renderPolicyDetails($policy, $rule_objects)) + ->appendChild($rules_view) ->addCancelButton($cancel_uri, pht('Close')); } - - private function extractPHIDs( - PhabricatorPolicy $policy, - array $rule_objects) { - - $phids = array(); - foreach ($policy->getRules() as $rule) { - $rule_object = $rule_objects[$rule['rule']]; - $phids[] = - $rule_object->getRequiredHandlePHIDsForSummary($rule['value']); - } - return array_filter(array_mergev($phids)); - } - - private function renderPolicyDetails( - PhabricatorPolicy $policy, - array $rule_objects) { - $details = array(); - $details[] = phutil_tag( - 'p', - array( - 'class' => 'policy-transaction-detail-intro', - ), - pht('These rules are processed in order:')); - - foreach ($policy->getRules() as $index => $rule) { - $rule_object = $rule_objects[$rule['rule']]; - if ($rule['action'] == 'allow') { - $icon = 'fa-check-circle green'; - } else { - $icon = 'fa-minus-circle red'; - } - $icon = id(new PHUIIconView()) - ->setIcon($icon) - ->setText( - ucfirst($rule['action']).' '.$rule_object->getRuleDescription()); - - $handle_phids = $rule_object->getRequiredHandlePHIDsForSummary( - $rule['value']); - if ($handle_phids) { - $value = $this->getViewer() - ->renderHandleList($handle_phids) - ->setAsInline(true); - } else { - $value = $rule['value']; - } - - $details[] = phutil_tag('div', - array( - 'class' => 'policy-transaction-detail-row', - ), - array( - $icon, - $value, - )); - } - - $details[] = phutil_tag( - 'p', - array( - 'class' => 'policy-transaction-detail-end', - ), - pht( - 'If no rules match, %s all other users.', - phutil_tag('b', - array(), - $policy->getDefaultAction()))); - return $details; - } - } diff --git a/src/applications/transactions/editengine/PhabricatorEditEngineSubtype.php b/src/applications/transactions/editengine/PhabricatorEditEngineSubtype.php index 6e1d1de115..f471fcd92f 100644 --- a/src/applications/transactions/editengine/PhabricatorEditEngineSubtype.php +++ b/src/applications/transactions/editengine/PhabricatorEditEngineSubtype.php @@ -14,6 +14,7 @@ final class PhabricatorEditEngineSubtype private $childSubtypes = array(); private $childIdentifiers = array(); private $fieldConfiguration = array(); + private $mutations; public function setKey($key) { $this->key = $key; @@ -78,6 +79,15 @@ final class PhabricatorEditEngineSubtype return $this->childIdentifiers; } + public function setMutations($mutations) { + $this->mutations = $mutations; + return $this; + } + + public function getMutations() { + return $this->mutations; + } + public function hasTagView() { return (bool)strlen($this->getTagText()); } @@ -152,6 +162,7 @@ final class PhabricatorEditEngineSubtype 'icon' => 'optional string', 'children' => 'optional map', 'fields' => 'optional map', + 'mutations' => 'optional list', )); $key = $value['key']; @@ -217,6 +228,28 @@ final class PhabricatorEditEngineSubtype 'with key "%s". This subtype is required and must be defined.', self::SUBTYPE_DEFAULT)); } + + foreach ($config as $value) { + $key = idx($value, 'key'); + + $mutations = idx($value, 'mutations'); + if (!$mutations) { + continue; + } + + foreach ($mutations as $mutation) { + if (!isset($map[$mutation])) { + throw new Exception( + pht( + 'Subtype configuration is invalid: subtype with key "%s" '. + 'specifies that it can mutate into subtype "%s", but that is '. + 'not a valid subtype.', + $key, + $mutation)); + } + } + } + } public static function newSubtypeMap(array $config) { @@ -267,6 +300,8 @@ final class PhabricatorEditEngineSubtype } } + $subtype->setMutations(idx($entry, 'mutations')); + $map[$key] = $subtype; } diff --git a/src/applications/transactions/editengine/PhabricatorEditEngineSubtypeMap.php b/src/applications/transactions/editengine/PhabricatorEditEngineSubtypeMap.php index 638b665184..edf2d2045a 100644 --- a/src/applications/transactions/editengine/PhabricatorEditEngineSubtypeMap.php +++ b/src/applications/transactions/editengine/PhabricatorEditEngineSubtypeMap.php @@ -5,6 +5,7 @@ final class PhabricatorEditEngineSubtypeMap extends Phobject { private $subtypes; + private $datasource; public function __construct(array $subtypes) { assert_instances_of($subtypes, 'PhabricatorEditEngineSubtype'); @@ -39,6 +40,57 @@ final class PhabricatorEditEngineSubtypeMap return $this->subtypes[$subtype_key]; } + public function setDatasource(PhabricatorTypeaheadDatasource $datasource) { + $this->datasource = $datasource; + return $this; + } + + public function newDatasource() { + if (!$this->datasource) { + throw new PhutilInvalidStateException('setDatasource'); + } + + return clone($this->datasource); + } + + public function getMutationMap($source_key) { + return mpull($this->getMutations($source_key), 'getName'); + } + + public function getMutations($source_key) { + $mutations = $this->subtypes; + + $subtype = idx($this->subtypes, $source_key); + if ($subtype) { + $map = $subtype->getMutations(); + if ($map !== null) { + $map = array_fuse($map); + foreach ($mutations as $key => $mutation) { + if ($key === $source_key) { + // This is the current subtype, so we always want to show it. + continue; + } + + if (isset($map[$key])) { + // This is an allowed mutation, so keep it. + continue; + } + + // Discard other subtypes as mutation options. + unset($mutations[$key]); + } + } + } + + // If the only available mutation is the current subtype, treat this like + // no mutations are available. + if (array_keys($mutations) === array($source_key)) { + $mutations = array(); + } + + return $mutations; + } + public function getCreateFormsForSubtype( PhabricatorEditEngine $edit_engine, PhabricatorEditEngineSubtypeInterface $object) { diff --git a/src/applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php b/src/applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php index d0b6d017f3..e73d476d74 100644 --- a/src/applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php +++ b/src/applications/transactions/engineextension/PhabricatorSubtypeEditEngineExtension.php @@ -29,9 +29,17 @@ final class PhabricatorSubtypeEditEngineExtension PhabricatorApplicationTransactionInterface $object) { $subtype_type = PhabricatorTransactions::TYPE_SUBTYPE; + $subtype_value = $object->getEditEngineSubtype(); $map = $object->newEditEngineSubtypeMap(); - $options = $map->getDisplayMap(); + + if ($object->getID()) { + $options = $map->getMutationMap($subtype_value); + } else { + // NOTE: This is a crude proxy for "are we in the bulk edit workflow". + // We want to allow any mutation. + $options = $map->getDisplayMap(); + } $subtype_field = id(new PhabricatorSelectEditField()) ->setKey(self::EDITKEY) @@ -40,12 +48,12 @@ final class PhabricatorSubtypeEditEngineExtension ->setTransactionType($subtype_type) ->setConduitDescription(pht('Change the object subtype.')) ->setConduitTypeDescription(pht('New object subtype key.')) - ->setValue($object->getEditEngineSubtype()) + ->setValue($subtype_value) ->setOptions($options); - // If subtypes are configured, enable changing them from the bulk editor - // and comment action stack. - if ($map->getCount() > 1) { + // If subtypes are configured, enable changing them from the bulk editor. + // Bulk editor + if ($options) { $subtype_field ->setBulkEditLabel(pht('Change subtype to')) ->setCommentActionLabel(pht('Change Subtype')) diff --git a/src/applications/transactions/herald/PhabricatorEditEngineSubtypeHeraldField.php b/src/applications/transactions/herald/PhabricatorEditEngineSubtypeHeraldField.php new file mode 100644 index 0000000000..be15540fc9 --- /dev/null +++ b/src/applications/transactions/herald/PhabricatorEditEngineSubtypeHeraldField.php @@ -0,0 +1,52 @@ +getEditEngineSubtype(); + } + + protected function getHeraldFieldStandardType() { + return self::STANDARD_PHID; + } + + protected function getDatasource() { + $object = $this->getAdapter()->getObject(); + $map = $object->newEditEngineSubtypeMap(); + return $map->newDatasource(); + } + + protected function getDatasourceValueMap() { + $object = $this->getAdapter()->getObject(); + $map = $object->newEditEngineSubtypeMap(); + + $result = array(); + foreach ($map->getSubtypes() as $subtype) { + $result[$subtype->getKey()] = $subtype->getName(); + } + + return $result; + } + + public function isFieldAvailable() { + $object = $this->getAdapter()->getObject(); + $map = $object->newEditEngineSubtypeMap(); + return ($map->getCount() > 1); + } + +} diff --git a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php index 7c327393f8..7e65ac0a09 100644 --- a/src/applications/transactions/storage/PhabricatorApplicationTransaction.php +++ b/src/applications/transactions/storage/PhabricatorApplicationTransaction.php @@ -445,19 +445,15 @@ abstract class PhabricatorApplicationTransaction $policy = PhabricatorPolicy::newFromPolicyAndHandle( $phid, $this->getHandleIfExists($phid)); + + $ref = $policy->newRef($this->getViewer()); + if ($this->renderingTarget == self::TARGET_HTML) { - switch ($policy->getType()) { - case PhabricatorPolicyType::TYPE_CUSTOM: - $policy->setHref('/transactions/'.$state.'/'.$this->getPHID().'/'); - $policy->setWorkflow(true); - break; - default: - break; - } - $output = $policy->renderDescription(); + $output = $ref->newTransactionLink($state, $this); } else { - $output = hsprintf('%s', $policy->getFullName()); + $output = $ref->getPolicyDisplayName(); } + return $output; } @@ -775,6 +771,13 @@ abstract class PhabricatorApplicationTransaction case PhabricatorTransactions::TYPE_TOKEN: case PhabricatorTransactions::TYPE_MFA: return true; + case PhabricatorTransactions::TYPE_SUBSCRIBERS: + // See T8952. When an application (usually Herald) modifies + // subscribers, this tends to be very uninteresting. + if ($this->isApplicationAuthor()) { + return true; + } + break; case PhabricatorTransactions::TYPE_EDGE: $edge_type = $this->getMetadataValue('edge:type'); switch ($edge_type) { @@ -1387,12 +1390,6 @@ abstract class PhabricatorApplicationTransaction return 25; } - if ($this->isApplicationAuthor()) { - // When applications (most often: Herald) change subscriptions it - // is very uninteresting. - return 1; - } - // In other cases, subscriptions are more interesting than comments // (which are shown anyway) but less interesting than any other type of // transaction. diff --git a/src/applications/transactions/storage/PhabricatorModularTransactionType.php b/src/applications/transactions/storage/PhabricatorModularTransactionType.php index 2d0cb8e7c1..7d5e3c533e 100644 --- a/src/applications/transactions/storage/PhabricatorModularTransactionType.php +++ b/src/applications/transactions/storage/PhabricatorModularTransactionType.php @@ -215,17 +215,16 @@ abstract class PhabricatorModularTransactionType $phid, $handles[$phid]); + $ref = $policy->newRef($viewer); + if ($this->isTextMode()) { - return $this->renderValue($policy->getFullName()); + $name = $ref->getPolicyDisplayName(); + } else { + $storage = $this->getStorage(); + $name = $ref->newTransactionLink($mode, $storage); } - $storage = $this->getStorage(); - if ($policy->getType() == PhabricatorPolicyType::TYPE_CUSTOM) { - $policy->setHref('/transactions/'.$mode.'/'.$storage->getPHID().'/'); - $policy->setWorkflow(true); - } - - return $this->renderValue($policy->renderDescription()); + return $this->renderValue($name); } final protected function renderHandleList(array $phids) { diff --git a/src/docs/user/configuration/configuring_preamble.diviner b/src/docs/user/configuration/configuring_preamble.diviner index fc804e9072..6b6b9da149 100644 --- a/src/docs/user/configuration/configuring_preamble.diviner +++ b/src/docs/user/configuration/configuring_preamble.diviner @@ -15,10 +15,9 @@ You can use a special preamble script to make arbitrary adjustments to the environment and some parts of Phabricator's configuration in order to fix these problems and set up the environment which Phabricator expects. -NOTE: This is an advanced feature. Most installs should not need to configure -a preamble script. -= Creating a Preamble Script = +Creating a Preamble Script +========================== To create a preamble script, write a file to: @@ -37,6 +36,7 @@ If present, this script will be executed at the very beginning of each web request, allowing you to adjust the environment. For common adjustments and examples, see the next sections. + Adjusting Client IPs ==================== @@ -44,9 +44,15 @@ If your install is behind a load balancer, Phabricator may incorrectly detect all requests as originating from the load balancer, rather than from the correct client IPs. -If this is the case and some other header (like `X-Forwarded-For`) is known to -be trustworthy, you can read the header and overwrite the `REMOTE_ADDR` value -so Phabricator can figure out the client IP correctly. +In common cases where networks are configured like this, the `X-Forwarded-For` +header will have trustworthy information about the real client IP. You +can use the function `preamble_trust_x_forwarded_for_header()` in your +preamble to tell Phabricator that you expect to receive requests from a +load balancer or proxy which modifies this header: + +```name="Trust X-Forwarded-For Header", lang=php +preamble_trust_x_forwarded_for_header(); +``` You should do this //only// if the `X-Forwarded-For` header is known to be trustworthy. In particular, if users can make requests to the web server @@ -54,30 +60,29 @@ directly, they can provide an arbitrary `X-Forwarded-For` header, and thereby spoof an arbitrary client IP. The `X-Forwarded-For` header may also contain a list of addresses if a request -has been forwarded through multiple loadbalancers. Using a snippet like this -will usually handle most situations correctly: +has been forwarded through multiple load balancers. If you know that requests +on your network are routed through `N` trustworthy devices, you can specify +that `N` to tell the function how many layers of `X-Forwarded-For` to discard: +```name="Trust X-Forwarded-For Header, Multiple Layers", lang=php +preamble_trust_x_forwarded_for_header(3); ``` -name=Overwrite REMOTE_ADDR with X-Forwarded-For - +``` + +To identify the object you want to examine, you can specify an object +name (like `T123`) or a PHID as the `` parameter. + +If examining the policy isn't helpful, or no user account has access to an +object, you can then directly change object policies from the CLI: ``` $ ./bin/policy unlock [--view ...] [--edit ...] [--owner ...] ``` -To identify the object you want to unlock, you can specify an object name (like -`T123`) or a PHID as the `` parameter. - Use the `--view` and `--edit` flags (and, for some objects, the `--owner` flag) to specify new policies for the object. diff --git a/src/infrastructure/cluster/PhabricatorDatabaseRef.php b/src/infrastructure/cluster/PhabricatorDatabaseRef.php index 89435b5869..478f95750b 100644 --- a/src/infrastructure/cluster/PhabricatorDatabaseRef.php +++ b/src/infrastructure/cluster/PhabricatorDatabaseRef.php @@ -221,6 +221,9 @@ final class PhabricatorDatabaseRef return $this->replicaRefs; } + public function getDisplayName() { + return $this->getRefKey(); + } public function getRefKey() { $host = $this->getHost(); diff --git a/src/infrastructure/env/PhabricatorEnv.php b/src/infrastructure/env/PhabricatorEnv.php index 24fb940c9a..4806289f52 100644 --- a/src/infrastructure/env/PhabricatorEnv.php +++ b/src/infrastructure/env/PhabricatorEnv.php @@ -135,6 +135,11 @@ final class PhabricatorEnv extends Phobject { // TODO: Add a "locale.default" config option once we have some reasonable // defaults which aren't silly nonsense. self::setLocaleCode('en_US'); + + // Load the preamble utility library if we haven't already. On web + // requests this loaded earlier, but we want to load it for non-web + // requests so that unit tests can call these functions. + require_once $phabricator_path.'/support/startup/preamble-utils.php'; } public static function beginScopedLocale($locale_code) { @@ -249,9 +254,17 @@ final class PhabricatorEnv extends Phobject { } try { - $stack->pushSource( - id(new PhabricatorConfigDatabaseSource('default')) - ->setName(pht('Database'))); + // See T13403. If we're starting up in "config optional" mode, suppress + // messages about connection retries. + if ($config_optional) { + $database_source = @new PhabricatorConfigDatabaseSource('default'); + } else { + $database_source = new PhabricatorConfigDatabaseSource('default'); + } + + $database_source->setName(pht('Database')); + + $stack->pushSource($database_source); } catch (AphrontSchemaQueryException $exception) { // If the database is not available, just skip this configuration // source. This happens during `bin/storage upgrade`, `bin/conf` before diff --git a/src/infrastructure/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php b/src/infrastructure/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php index 0f9201b02d..6faf10e2c6 100644 --- a/src/infrastructure/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php +++ b/src/infrastructure/storage/connection/mysql/AphrontBaseMySQLDatabaseConnection.php @@ -10,6 +10,9 @@ abstract class AphrontBaseMySQLDatabaseConnection private $nextError; + const CALLERROR_QUERY = 777777; + const CALLERROR_CONNECT = 777778; + abstract protected function connect(); abstract protected function rawQuery($raw_query); abstract protected function rawQueries(array $raw_queries); @@ -123,7 +126,14 @@ abstract class AphrontBaseMySQLDatabaseConnection $code, $ex->getMessage()); - phlog($message); + // See T13403. If we're silenced with the "@" operator, don't log + // this connection attempt. This keeps things quiet if we're + // running a setup workflow like "bin/config" and expect that the + // database credentials will often be incorrect. + + if (error_reporting()) { + phlog($message); + } } else { $profiler->endServiceCall($call_id, array()); throw $ex; diff --git a/src/infrastructure/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php b/src/infrastructure/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php index 7a4b5193d5..6a0bc759a7 100644 --- a/src/infrastructure/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php +++ b/src/infrastructure/storage/connection/mysql/AphrontMySQLiDatabaseConnection.php @@ -68,19 +68,47 @@ final class AphrontMySQLiDatabaseConnection $host = 'p:'.$host; } - @$conn->real_connect( + $trap = new PhutilErrorTrap(); + + $ok = @$conn->real_connect( $host, $user, $pass, $database, $port); + $call_error = $trap->getErrorsAsString(); + $trap->destroy(); + $errno = $conn->connect_errno; if ($errno) { $error = $conn->connect_error; $this->throwConnectionException($errno, $error, $user, $host); } + // See T13403. If the parameters to "real_connect()" are wrong, it may + // fail without setting an error code. In this case, raise a generic + // exception. (One way to reproduce this is to pass a string to the + // "port" parameter.) + + if (!$ok) { + if (strlen($call_error)) { + $message = pht( + 'mysqli->real_connect() failed: %s', + $call_error); + } else { + $message = pht( + 'mysqli->real_connect() failed, but did not set an error code '. + 'or emit a message.'); + } + + $this->throwConnectionException( + self::CALLERROR_CONNECT, + $message, + $user, + $host); + } + // See T13238. Attempt to prevent "LOAD DATA LOCAL INFILE", which allows a // malicious server to ask the client for any file. At time of writing, // this option MUST be set after "real_connect()" on all PHP versions. @@ -152,7 +180,7 @@ final class AphrontMySQLiDatabaseConnection 'Call to "mysqli->query()" failed, but did not set an error '. 'code or emit an error message.'); } - $this->throwQueryCodeException(777777, $message); + $this->throwQueryCodeException(self::CALLERROR_QUERY, $message); } } diff --git a/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php b/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php index b838c8a5d9..a6a0d74593 100644 --- a/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php +++ b/src/infrastructure/storage/management/PhabricatorStorageManagementAPI.php @@ -89,6 +89,10 @@ final class PhabricatorStorageManagementAPI extends Phobject { return $this->namespace.'_'.$fragment; } + public function getDisplayName() { + return $this->getRef()->getDisplayName(); + } + public function getDatabaseList(array $patches, $only_living = false) { assert_instances_of($patches, 'PhabricatorStoragePatch'); diff --git a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php index 7d0946c8c3..9b718e231d 100644 --- a/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php +++ b/src/infrastructure/storage/management/workflow/PhabricatorStorageManagementDestroyWorkflow.php @@ -21,86 +21,112 @@ final class PhabricatorStorageManagementDestroyWorkflow } public function didExecute(PhutilArgumentParser $args) { - $console = PhutilConsole::getConsole(); + $api = $this->getSingleAPI(); + + $host_display = $api->getDisplayName(); if (!$this->isDryRun() && !$this->isForce()) { if ($args->getArg('unittest-fixtures')) { - $console->writeOut( - phutil_console_wrap( - pht( - 'Are you completely sure you really want to destroy all unit '. - 'test fixure data? This operation can not be undone.'))); + $warning = pht( + 'Are you completely sure you really want to destroy all unit '. + 'test fixure data on host "%s"? This operation can not be undone.', + $host_display); + + echo tsprintf( + '%B', + id(new PhutilConsoleBlock()) + ->addParagraph($warning) + ->drawConsoleString()); + if (!phutil_console_confirm(pht('Destroy all unit test data?'))) { - $console->writeOut("%s\n", pht('Cancelled.')); + $this->logFail( + pht('CANCELLED'), + pht('User cancelled operation.')); exit(1); } } else { - $console->writeOut( - phutil_console_wrap( - pht( - 'Are you completely sure you really want to permanently destroy '. - 'all storage for Phabricator data? This operation can not be '. - 'undone and your data will not be recoverable if you proceed.'))); + $warning = pht( + 'Are you completely sure you really want to permanently destroy '. + 'all storage for Phabricator data on host "%s"? This operation '. + 'can not be undone and your data will not be recoverable if '. + 'you proceed.', + $host_display); + + echo tsprintf( + '%B', + id(new PhutilConsoleBlock()) + ->addParagraph($warning) + ->drawConsoleString()); if (!phutil_console_confirm(pht('Permanently destroy all data?'))) { - $console->writeOut("%s\n", pht('Cancelled.')); + $this->logFail( + pht('CANCELLED'), + pht('User cancelled operation.')); exit(1); } if (!phutil_console_confirm(pht('Really destroy all data forever?'))) { - $console->writeOut("%s\n", pht('Cancelled.')); + $this->logFail( + pht('CANCELLED'), + pht('User cancelled operation.')); exit(1); } } } - $apis = $this->getMasterAPIs(); - foreach ($apis as $api) { - $patches = $this->getPatches(); + $patches = $this->getPatches(); - if ($args->getArg('unittest-fixtures')) { - $conn = $api->getConn(null); - $databases = queryfx_all( - $conn, - 'SELECT DISTINCT(TABLE_SCHEMA) AS db '. - 'FROM INFORMATION_SCHEMA.TABLES '. - 'WHERE TABLE_SCHEMA LIKE %>', - PhabricatorTestCase::NAMESPACE_PREFIX); - $databases = ipull($databases, 'db'); - } else { - $databases = $api->getDatabaseList($patches); - $databases[] = $api->getDatabaseName('meta_data'); + if ($args->getArg('unittest-fixtures')) { + $conn = $api->getConn(null); + $databases = queryfx_all( + $conn, + 'SELECT DISTINCT(TABLE_SCHEMA) AS db '. + 'FROM INFORMATION_SCHEMA.TABLES '. + 'WHERE TABLE_SCHEMA LIKE %>', + PhabricatorTestCase::NAMESPACE_PREFIX); + $databases = ipull($databases, 'db'); + } else { + $databases = $api->getDatabaseList($patches); + $databases[] = $api->getDatabaseName('meta_data'); - // These are legacy databases that were dropped long ago. See T2237. - $databases[] = $api->getDatabaseName('phid'); - $databases[] = $api->getDatabaseName('directory'); - } + // These are legacy databases that were dropped long ago. See T2237. + $databases[] = $api->getDatabaseName('phid'); + $databases[] = $api->getDatabaseName('directory'); + } - foreach ($databases as $database) { - if ($this->isDryRun()) { - $console->writeOut( - "%s\n", - pht("DRYRUN: Would drop database '%s'.", $database)); - } else { - $console->writeOut( - "%s\n", - pht("Dropping database '%s'...", $database)); - queryfx( - $api->getConn(null), - 'DROP DATABASE IF EXISTS %T', - $database); - } - } + asort($databases); - if (!$this->isDryRun()) { - $console->writeOut( - "%s\n", + foreach ($databases as $database) { + if ($this->isDryRun()) { + $this->logInfo( + pht('DRY RUN'), pht( - 'Storage on "%s" was destroyed.', - $api->getRef()->getRefKey())); + 'Would drop database "%s" on host "%s".', + $database, + $host_display)); + } else { + $this->logWarn( + pht('DESTROY'), + pht( + 'Dropping database "%s" on host "%s"...', + $database, + $host_display)); + + queryfx( + $api->getConn(null), + 'DROP DATABASE IF EXISTS %T', + $database); } } + if (!$this->isDryRun()) { + $this->logOkay( + pht('DONE'), + pht( + 'Storage on "%s" was destroyed.', + $host_display)); + } + return 0; } diff --git a/src/infrastructure/util/__tests__/PhabricatorPreambleTestCase.php b/src/infrastructure/util/__tests__/PhabricatorPreambleTestCase.php new file mode 100644 index 0000000000..c47e8c0c9a --- /dev/null +++ b/src/infrastructure/util/__tests__/PhabricatorPreambleTestCase.php @@ -0,0 +1,74 @@ + '1.2.3.4', + 'layers' => 1, + 'expect' => '1.2.3.4', + ), + + // In this case, the LB received a request which already had an + // "X-Forwarded-For" header. This might be legitimate (in the case of + // a CDN request) or illegitimate (in the case of a client making + // things up). We don't want to trust it. + array( + 'header' => '9.9.9.9, 1.2.3.4', + 'layers' => 1, + 'expect' => '1.2.3.4', + ), + + // Multiple layers of load balancers. + array( + 'header' => '9.9.9.9, 1.2.3.4', + 'layers' => 2, + 'expect' => '9.9.9.9', + ), + + // Multiple layers of load balancers, plus a client-supplied value. + array( + 'header' => '8.8.8.8, 9.9.9.9, 1.2.3.4', + 'layers' => 2, + 'expect' => '9.9.9.9', + ), + + // Multiple layers of load balancers, but this request came from + // somewhere inside the network. + array( + 'header' => '1.2.3.4', + 'layers' => 2, + 'expect' => '1.2.3.4', + ), + + array( + 'header' => 'A, B, C, D, E, F, G, H, I', + 'layers' => 7, + 'expect' => 'C', + ), + ); + + foreach ($tests as $test) { + $header = $test['header']; + $layers = $test['layers']; + $expect = $test['expect']; + + $actual = preamble_get_x_forwarded_for_address($header, $layers); + + $this->assertEqual( + $expect, + $actual, + pht( + 'Address after stripping %d layers from: %s', + $layers, + $header)); + } + } + +} diff --git a/src/view/AphrontDialogView.php b/src/view/AphrontDialogView.php index 09fc8e7a16..b8b00a6b3e 100644 --- a/src/view/AphrontDialogView.php +++ b/src/view/AphrontDialogView.php @@ -160,6 +160,20 @@ final class AphrontDialogView return $this->appendChild($box); } + public function appendRemarkup($remarkup) { + $viewer = $this->getViewer(); + $view = new PHUIRemarkupView($viewer, $remarkup); + + $view_tag = phutil_tag( + 'div', + array( + 'class' => 'aphront-dialog-view-paragraph', + ), + $view); + + return $this->appendChild($view_tag); + } + public function appendParagraph($paragraph) { return $this->appendParagraphTag($paragraph); } diff --git a/src/view/page/menu/PhabricatorMainMenuSearchView.php b/src/view/page/menu/PhabricatorMainMenuSearchView.php index 15319a357e..e038eec6c8 100644 --- a/src/view/page/menu/PhabricatorMainMenuSearchView.php +++ b/src/view/page/menu/PhabricatorMainMenuSearchView.php @@ -116,8 +116,10 @@ final class PhabricatorMainMenuSearchView extends AphrontView { return $form; } - private function buildModeSelector($selector_id, $application_id) { - $viewer = $this->getViewer(); + public static function getGlobalSearchScopeItems( + PhabricatorUser $viewer, + PhabricatorApplication $application = null, + $global_only = false) { $items = array(); $items[] = array( @@ -132,7 +134,6 @@ final class PhabricatorMainMenuSearchView extends AphrontView { $application_value = null; $application_icon = self::DEFAULT_APPLICATION_ICON; - $application = $this->getApplication(); if ($application) { $application_value = get_class($application); if ($application->getApplicationSearchDocumentTypes()) { @@ -154,14 +155,24 @@ final class PhabricatorMainMenuSearchView extends AphrontView { $engine = id(new PhabricatorSearchApplicationSearchEngine()) ->setViewer($viewer); $engine_queries = $engine->loadEnabledNamedQueries(); - $query_map = mpull($engine_queries, 'getQueryName', 'getQueryKey'); - foreach ($query_map as $query_key => $query_name) { + foreach ($engine_queries as $query) { + $query_key = $query->getQueryKey(); if ($query_key == 'all') { // Skip the builtin "All" query since it's redundant with the default // setting. continue; } + // In the global "Settings" panel, we don't want to offer personal + // queries the viewer may have saved. + if ($global_only) { + if (!$query->isGlobal()) { + continue; + } + } + + $query_name = $query->getQueryName(); + $items[] = array( 'icon' => 'fa-certificate', 'name' => $query_name, @@ -185,6 +196,14 @@ final class PhabricatorMainMenuSearchView extends AphrontView { 'href' => PhabricatorEnv::getDoclink('Search User Guide'), ); + return $items; + } + + private function buildModeSelector($selector_id, $application_id) { + $viewer = $this->getViewer(); + + $items = self::getGlobalSearchScopeItems($viewer, $this->getApplication()); + $scope_key = PhabricatorSearchScopeSetting::SETTINGKEY; $current_value = $viewer->getUserSetting($scope_key); @@ -196,6 +215,13 @@ final class PhabricatorMainMenuSearchView extends AphrontView { } } + $application = $this->getApplication(); + + $application_value = null; + if ($application) { + $application_value = get_class($application); + } + $selector = id(new PHUIButtonView()) ->setID($selector_id) ->addClass('phabricator-main-menu-search-dropdown') diff --git a/support/startup/preamble-utils.php b/support/startup/preamble-utils.php new file mode 100644 index 0000000000..8dd3b502d6 --- /dev/null +++ b/support/startup/preamble-utils.php @@ -0,0 +1,77 @@ +): '. + '"layers" parameter must an integer larger than 0.'."\n"; + echo "\n"; + exit(1); + } + + if (!isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { + return; + } + + $forwarded_for = $_SERVER['HTTP_X_FORWARDED_FOR']; + if (!strlen($forwarded_for)) { + return; + } + + $address = preamble_get_x_forwarded_for_address($forwarded_for, $layers); + + $_SERVER['REMOTE_ADDR'] = $address; +} + +function preamble_get_x_forwarded_for_address($raw_header, $layers) { + // The raw header may be a list of IPs, like "1.2.3.4, 4.5.6.7", if the + // request the load balancer received also had this header. In particular, + // this happens routinely with requests received through a CDN, but can also + // happen illegitimately if the client just makes up an "X-Forwarded-For" + // header full of lies. + + // We can only trust the N elements at the end of the list which correspond + // to network-adjacent devices we control. Usually, we're behind a single + // load balancer and "N" is 1, so we want to take the last element in the + // list. + + // In some cases, "N" may be more than 1, if the network is configured so + // that that requests are routed through multiple layers of load balancers + // and proxies. In this case, we want to take the Nth-to-last element of + // the list. + + $addresses = explode(',', $raw_header); + + // If we have more than one trustworthy device on the network path, discard + // corresponding elements from the list. For example, if we have 7 devices, + // we want to discard the last 6 elements of the list. + + // The final device address does not appear in the list, since devices do + // not append their own addresses to "X-Forwarded-For". + + $discard_addresses = ($layers - 1); + + // However, we don't want to throw away all of the addresses. Some requests + // may originate from within the network, and may thus not have as many + // addresses as we expect. If we have fewer addresses than trustworthy + // devices, discard all but one address. + + $max_discard = (count($addresses) - 1); + + $discard_count = min($discard_addresses, $max_discard); + if ($discard_count) { + $addresses = array_slice($addresses, 0, -$discard_count); + } + + $original_address = end($addresses); + $original_address = trim($original_address); + + return $original_address; +} diff --git a/webroot/index.php b/webroot/index.php index 0014edfa2c..38c5c77809 100644 --- a/webroot/index.php +++ b/webroot/index.php @@ -85,6 +85,7 @@ function phabricator_startup() { require_once $root.'/support/startup/PhabricatorClientLimit.php'; require_once $root.'/support/startup/PhabricatorClientRateLimit.php'; require_once $root.'/support/startup/PhabricatorClientConnectionLimit.php'; + require_once $root.'/support/startup/preamble-utils.php'; // If the preamble script exists, load it. $t_preamble = microtime(true); diff --git a/webroot/rsrc/css/phui/phui-chart.css b/webroot/rsrc/css/phui/phui-chart.css index 350d86014a..646ed52581 100644 --- a/webroot/rsrc/css/phui/phui-chart.css +++ b/webroot/rsrc/css/phui/phui-chart.css @@ -36,16 +36,17 @@ } .chart .point { - fill: {$lightblue}; + fill: #ffffff; stroke: {$blue}; - stroke-width: 1px; + stroke-width: 2px; + position: relative; + cursor: pointer; } .chart-tooltip { position: absolute; text-align: center; width: 120px; - height: 16px; overflow: hidden; padding: 2px; background: {$lightbluebackground}; diff --git a/webroot/rsrc/css/phui/phui-header-view.css b/webroot/rsrc/css/phui/phui-header-view.css index 6a096af76d..1d851f04ee 100644 --- a/webroot/rsrc/css/phui/phui-header-view.css +++ b/webroot/rsrc/css/phui/phui-header-view.css @@ -354,45 +354,3 @@ body .phui-header-shell.phui-bleed-header .phui-header-view .phui-tag-indigo a { color: {$sh-indigotext}; } - -.phui-policy-section-view { - margin-bottom: 24px; -} - -.phui-policy-section-view-header { - background: {$bluebackground}; - border-bottom: 1px solid {$lightblueborder}; - padding: 4px 8px; - color: {$darkbluetext}; - margin-bottom: 8px; -} - -.phui-policy-section-view-header-text { - font-weight: bold; -} - -.phui-policy-section-view-header .phui-icon-view { - margin-right: 8px; -} - -.phui-policy-section-view-link { - float: right; -} - -.phui-policy-section-view-link .phui-icon-view { - color: {$bluetext}; -} - -.phui-policy-section-view-hint { - color: {$greytext}; - background: {$lightbluebackground}; - padding: 8px; -} - -.phui-policy-section-view-body { - padding: 0 12px; -} - -.phui-policy-section-view-inactive-rule { - color: {$greytext}; -} diff --git a/webroot/rsrc/css/phui/phui-policy-section-view.css b/webroot/rsrc/css/phui/phui-policy-section-view.css new file mode 100644 index 0000000000..4325b34867 --- /dev/null +++ b/webroot/rsrc/css/phui/phui-policy-section-view.css @@ -0,0 +1,62 @@ +/** + * @provides phui-policy-section-view-css + */ + +.phui-policy-section-view { + margin-bottom: 24px; +} + +.phui-policy-section-view-header { + background: {$bluebackground}; + border-bottom: 1px solid {$lightblueborder}; + padding: 4px 8px; + color: {$darkbluetext}; + margin-bottom: 8px; +} + +.phui-policy-section-view-header-text { + font-weight: bold; +} + +.phui-policy-section-view-header .phui-icon-view { + margin-right: 8px; +} + +.phui-policy-section-view-link { + float: right; +} + +.phui-policy-section-view-link .phui-icon-view { + color: {$bluetext}; +} + +.phui-policy-section-view-hint { + color: {$greytext}; + background: {$lightbluebackground}; + padding: 8px; +} + +.phui-policy-section-view-body { + padding: 0 12px; +} + +.phui-policy-section-view-inactive-rule { + color: {$greytext}; +} + +.phui-policy-section-view-rules { + margin: 8px 0; + padding: 8px; + background: {$lightbluebackground}; + border: 1px solid {$lightblueborder}; +} + +.phui-policy-section-view .phui-policy-section-view-body ul { + margin: 8px 0; + padding: 0 16px 0 24px; + list-style: disc; +} + +.phui-policy-section-view .phui-policy-section-view-body p + p { + margin-top: 8px; +} diff --git a/webroot/rsrc/css/phui/workboards/phui-workcard.css b/webroot/rsrc/css/phui/workboards/phui-workcard.css index 3c6a798fc8..d9acef12ef 100644 --- a/webroot/rsrc/css/phui/workboards/phui-workcard.css +++ b/webroot/rsrc/css/phui/workboards/phui-workcard.css @@ -36,6 +36,10 @@ .phui-workcard .phui-oi-link { white-space: normal; + + /* See T13413. This works around a Chrome 77 rendering engine freeze. */ + word-wrap: normal; + font-weight: normal; color: {$blacktext}; margin-left: 2px; diff --git a/webroot/rsrc/js/application/fact/Chart.js b/webroot/rsrc/js/application/fact/Chart.js index 9ce50822ee..473feedcea 100644 --- a/webroot/rsrc/js/application/fact/Chart.js +++ b/webroot/rsrc/js/application/fact/Chart.js @@ -133,18 +133,33 @@ JX.install('Chart', { }, _newStackedArea: function(g, dataset, x, y, div, curtain) { + var ii; + var to_date = JX.bind(this, this._newDate); var area = d3.area() .x(function(d) { return x(to_date(d.x)); }) - .y0(function(d) { return y(d.y0); }) + .y0(function(d) { + // When the area is positive, draw it above the X axis. When the area + // is negative, draw it below the X axis. We currently avoid having + // functions which cross the X axis by clever construction. + if (d.y0 >= 0 && d.y1 >= 0) { + return y(d.y0); + } + + if (d.y0 <= 0 && d.y1 <= 0) { + return y(d.y0); + } + + return y(0); + }) .y1(function(d) { return y(d.y1); }); var line = d3.line() .x(function(d) { return x(to_date(d.x)); }) .y(function(d) { return y(d.y1); }); - for (var ii = 0; ii < dataset.data.length; ii++) { + for (ii = 0; ii < dataset.data.length; ii++) { var label = new JX.ChartFunctionLabel(dataset.labels[ii]); var fill_color = label.getFillColor() || label.getColor(); @@ -160,6 +175,11 @@ JX.install('Chart', { .style('stroke', stroke_color) .attr('d', line(dataset.data[ii])); + curtain.addFunctionLabel(label); + } + + // Now that we've drawn all the areas and lines, draw the dots. + for (ii = 0; ii < dataset.data.length; ii++) { g.selectAll('dot') .data(dataset.events[ii]) .enter() @@ -178,8 +198,16 @@ JX.install('Chart', { var d_d = dd.getDate(); + var y = parseInt(d.y1); + + var label = d.n + ' Points'; + + var view = + d_y + '-' + d_m + '-' + d_d + ': ' + y + '
' + + label; + div - .html(d_y + '-' + d_m + '-' + d_d + ': ' + d.y1) + .html(view) .style('opacity', 0.9) .style('left', (d3.event.pageX - 60) + 'px') .style('top', (d3.event.pageY - 38) + 'px'); @@ -187,9 +215,8 @@ JX.install('Chart', { .on('mouseout', function() { div.style('opacity', 0); }); - - curtain.addFunctionLabel(label); } + }, _newDate: function(epoch) { diff --git a/webroot/rsrc/js/application/herald/HeraldRuleEditor.js b/webroot/rsrc/js/application/herald/HeraldRuleEditor.js index 443fe9811e..254061533a 100644 --- a/webroot/rsrc/js/application/herald/HeraldRuleEditor.js +++ b/webroot/rsrc/js/application/herald/HeraldRuleEditor.js @@ -350,8 +350,10 @@ JX.install('HeraldRuleEditor', { sigil: 'field-select' }; - var field_select = this._renderGroupSelect(groups, attrs); - field_select.value = this._config.conditions[row_id][0]; + var field_select = this._renderGroupSelect( + groups, + attrs, + this._config.conditions[row_id][0]); var field_cell = JX.$N('td', {sigil: 'field-cell'}, field_select); @@ -367,18 +369,38 @@ JX.install('HeraldRuleEditor', { } }, - _renderGroupSelect: function(groups, attrs) { + _renderGroupSelect: function(groups, attrs, value) { var optgroups = []; for (var ii = 0; ii < groups.length; ii++) { var group = groups[ii]; var options = []; for (var k in group.options) { - options.push(JX.$N('option', {value: k}, group.options[k])); + var option = group.options[k]; + + var name = option.name; + var available = option.available; + + // See T7961. If the option is not marked as "available", we only + // include it in the dropdown if the dropdown already has it as a + // value. We want to hide options provided by applications which are + // not installed, but do not want to break existing rules. + + if (available || (k === value)) { + options.push(JX.$N('option', {value: k}, name)); + } + } + if (options.length) { + optgroups.push(JX.$N('optgroup', {label: group.label}, options)); } - optgroups.push(JX.$N('optgroup', {label: group.label}, options)); } - return JX.$N('select', attrs, optgroups); + var select = JX.$N('select', attrs, optgroups); + + if (value !== undefined) { + select.value = value; + } + + return select; }, _newAction : function(data) { @@ -402,8 +424,10 @@ JX.install('HeraldRuleEditor', { sigil: 'action-select' }; - var action_select = this._renderGroupSelect(groups, attrs); - action_select.value = action[0]; + var action_select = this._renderGroupSelect( + groups, + attrs, + action[0]); var action_cell = JX.$N('td', {sigil: 'action-cell'}, action_select);