Merge branch 'master' into blender-tweaks
This commit is contained in:
		| @@ -9,7 +9,7 @@ return array( | |||||||
|   'names' => array( |   'names' => array( | ||||||
|     'conpherence.pkg.css' => '3c8a0668', |     'conpherence.pkg.css' => '3c8a0668', | ||||||
|     'conpherence.pkg.js' => '020aebcf', |     'conpherence.pkg.js' => '020aebcf', | ||||||
|     'core.pkg.css' => 'eef4903d', |     'core.pkg.css' => '242f9ce6', | ||||||
|     'core.pkg.js' => '73a06a9f', |     'core.pkg.js' => '73a06a9f', | ||||||
|     'differential.pkg.css' => '8d8360fb', |     'differential.pkg.css' => '8d8360fb', | ||||||
|     'differential.pkg.js' => '0b037a4f', |     'differential.pkg.js' => '0b037a4f', | ||||||
| @@ -141,7 +141,7 @@ return array( | |||||||
|     'rsrc/css/phui/phui-big-info-view.css' => '362ad37b', |     'rsrc/css/phui/phui-big-info-view.css' => '362ad37b', | ||||||
|     'rsrc/css/phui/phui-box.css' => '5ed3b8cb', |     'rsrc/css/phui/phui-box.css' => '5ed3b8cb', | ||||||
|     'rsrc/css/phui/phui-bulk-editor.css' => '374d5e30', |     '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-cms.css' => '8c05c41e', | ||||||
|     'rsrc/css/phui/phui-comment-form.css' => '68a2d99a', |     'rsrc/css/phui/phui-comment-form.css' => '68a2d99a', | ||||||
|     'rsrc/css/phui/phui-comment-panel.css' => 'ec4e31c0', |     '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-view.css' => 'a8e0a1ab', | ||||||
|     'rsrc/css/phui/phui-form.css' => '159e2d9c', |     'rsrc/css/phui/phui-form.css' => '159e2d9c', | ||||||
|     'rsrc/css/phui/phui-head-thing.css' => 'd7f293df', |     '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-hovercard.css' => '6ca90fa0', | ||||||
|     'rsrc/css/phui/phui-icon-set-selector.css' => '7aa5f3ec', |     'rsrc/css/phui/phui-icon-set-selector.css' => '7aa5f3ec', | ||||||
|     'rsrc/css/phui/phui-icon.css' => '4cbc684a', |     '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-object-box.css' => 'f434b6be', | ||||||
|     'rsrc/css/phui/phui-pager.css' => 'd022c7ad', |     'rsrc/css/phui/phui-pager.css' => 'd022c7ad', | ||||||
|     'rsrc/css/phui/phui-pinboard-view.css' => '1f08f5d8', |     '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-property-list-view.css' => 'cad62236', | ||||||
|     'rsrc/css/phui/phui-remarkup-preview.css' => '91767007', |     'rsrc/css/phui/phui-remarkup-preview.css' => '91767007', | ||||||
|     'rsrc/css/phui/phui-segment-bar-view.css' => '5166b370', |     '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/phui-two-column-view.css' => '01e6991e', | ||||||
|     'rsrc/css/phui/workboards/phui-workboard-color.css' => 'e86de308', |     'rsrc/css/phui/workboards/phui-workboard-color.css' => 'e86de308', | ||||||
|     'rsrc/css/phui/workboards/phui-workboard.css' => '74fc9d98', |     '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/phui/workboards/phui-workpanel.css' => '3ae89b20', | ||||||
|     'rsrc/css/sprite-login.css' => '18b368a6', |     'rsrc/css/sprite-login.css' => '18b368a6', | ||||||
|     'rsrc/css/sprite-tokens.css' => 'f1896dc5', |     'rsrc/css/sprite-tokens.css' => 'f1896dc5', | ||||||
| @@ -391,14 +392,14 @@ return array( | |||||||
|     'rsrc/js/application/diffusion/behavior-pull-lastmodified.js' => 'c715c123', |     'rsrc/js/application/diffusion/behavior-pull-lastmodified.js' => 'c715c123', | ||||||
|     'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => '6a85bc5a', |     'rsrc/js/application/doorkeeper/behavior-doorkeeper-tag.js' => '6a85bc5a', | ||||||
|     'rsrc/js/application/drydock/drydock-live-operation-status.js' => '47a0728b', |     '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/ChartCurtainView.js' => '86954222', | ||||||
|     'rsrc/js/application/fact/ChartFunctionLabel.js' => '81de1dab', |     'rsrc/js/application/fact/ChartFunctionLabel.js' => '81de1dab', | ||||||
|     'rsrc/js/application/files/behavior-document-engine.js' => '243d6c22', |     'rsrc/js/application/files/behavior-document-engine.js' => '243d6c22', | ||||||
|     'rsrc/js/application/files/behavior-icon-composer.js' => '38a6cedb', |     'rsrc/js/application/files/behavior-icon-composer.js' => '38a6cedb', | ||||||
|     'rsrc/js/application/files/behavior-launch-icon-composer.js' => 'a17b84f1', |     'rsrc/js/application/files/behavior-launch-icon-composer.js' => 'a17b84f1', | ||||||
|     'rsrc/js/application/harbormaster/behavior-harbormaster-log.js' => 'b347a301', |     '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/PathTypeahead.js' => 'ad486db3', | ||||||
|     'rsrc/js/application/herald/herald-rule-editor.js' => '0922e81d', |     'rsrc/js/application/herald/herald-rule-editor.js' => '0922e81d', | ||||||
|     'rsrc/js/application/maniphest/behavior-batch-selector.js' => '139ef688', |     'rsrc/js/application/maniphest/behavior-batch-selector.js' => '139ef688', | ||||||
| @@ -572,7 +573,7 @@ return array( | |||||||
|     'global-drag-and-drop-css' => '1d2713a4', |     'global-drag-and-drop-css' => '1d2713a4', | ||||||
|     'harbormaster-css' => '8dfe16b2', |     'harbormaster-css' => '8dfe16b2', | ||||||
|     'herald-css' => '648d39e2', |     'herald-css' => '648d39e2', | ||||||
|     'herald-rule-editor' => '27daef73', |     'herald-rule-editor' => '2633bef7', | ||||||
|     'herald-test-css' => 'e004176f', |     'herald-test-css' => 'e004176f', | ||||||
|     'inline-comment-summary-css' => '81eb368d', |     'inline-comment-summary-css' => '81eb368d', | ||||||
|     'javelin-aphlict' => '022516b4', |     'javelin-aphlict' => '022516b4', | ||||||
| @@ -700,7 +701,7 @@ return array( | |||||||
|     'javelin-behavior-user-menu' => '60cd9241', |     'javelin-behavior-user-menu' => '60cd9241', | ||||||
|     'javelin-behavior-view-placeholder' => 'a9942052', |     'javelin-behavior-view-placeholder' => 'a9942052', | ||||||
|     'javelin-behavior-workflow' => '9623adc1', |     'javelin-behavior-workflow' => '9623adc1', | ||||||
|     'javelin-chart' => 'eec96de0', |     'javelin-chart' => '52e3ff03', | ||||||
|     'javelin-chart-curtain-view' => '86954222', |     'javelin-chart-curtain-view' => '86954222', | ||||||
|     'javelin-chart-function-label' => '81de1dab', |     'javelin-chart-function-label' => '81de1dab', | ||||||
|     'javelin-color' => '78f811c9', |     'javelin-color' => '78f811c9', | ||||||
| @@ -830,7 +831,7 @@ return array( | |||||||
|     'phui-calendar-day-css' => '9597d706', |     'phui-calendar-day-css' => '9597d706', | ||||||
|     'phui-calendar-list-css' => 'ccd7e4e2', |     'phui-calendar-list-css' => 'ccd7e4e2', | ||||||
|     'phui-calendar-month-css' => 'cb758c42', |     'phui-calendar-month-css' => 'cb758c42', | ||||||
|     'phui-chart-css' => '10135a9d', |     'phui-chart-css' => '14df9ae3', | ||||||
|     'phui-cms-css' => '8c05c41e', |     'phui-cms-css' => '8c05c41e', | ||||||
|     'phui-comment-form-css' => '68a2d99a', |     'phui-comment-form-css' => '68a2d99a', | ||||||
|     'phui-comment-panel-css' => 'ec4e31c0', |     'phui-comment-panel-css' => 'ec4e31c0', | ||||||
| @@ -845,7 +846,7 @@ return array( | |||||||
|     'phui-form-css' => '159e2d9c', |     'phui-form-css' => '159e2d9c', | ||||||
|     'phui-form-view-css' => 'a8e0a1ab', |     'phui-form-view-css' => 'a8e0a1ab', | ||||||
|     'phui-head-thing-view-css' => 'd7f293df', |     'phui-head-thing-view-css' => 'd7f293df', | ||||||
|     'phui-header-view-css' => '285c9139', |     'phui-header-view-css' => 'b500eeea', | ||||||
|     'phui-hovercard' => '074f0783', |     'phui-hovercard' => '074f0783', | ||||||
|     'phui-hovercard-view-css' => '6ca90fa0', |     'phui-hovercard-view-css' => '6ca90fa0', | ||||||
|     'phui-icon-set-selector-css' => '7aa5f3ec', |     'phui-icon-set-selector-css' => '7aa5f3ec', | ||||||
| @@ -866,6 +867,7 @@ return array( | |||||||
|     'phui-oi-simple-ui-css' => '6a30fa46', |     'phui-oi-simple-ui-css' => '6a30fa46', | ||||||
|     'phui-pager-css' => 'd022c7ad', |     'phui-pager-css' => 'd022c7ad', | ||||||
|     'phui-pinboard-view-css' => '1f08f5d8', |     'phui-pinboard-view-css' => '1f08f5d8', | ||||||
|  |     'phui-policy-section-view-css' => '139fdc64', | ||||||
|     'phui-property-list-view-css' => 'cad62236', |     'phui-property-list-view-css' => 'cad62236', | ||||||
|     'phui-remarkup-preview-css' => '91767007', |     'phui-remarkup-preview-css' => '91767007', | ||||||
|     'phui-segment-bar-view-css' => '5166b370', |     'phui-segment-bar-view-css' => '5166b370', | ||||||
| @@ -877,7 +879,7 @@ return array( | |||||||
|     'phui-two-column-view-css' => '01e6991e', |     'phui-two-column-view-css' => '01e6991e', | ||||||
|     'phui-workboard-color-css' => 'e86de308', |     'phui-workboard-color-css' => 'e86de308', | ||||||
|     'phui-workboard-view-css' => '74fc9d98', |     'phui-workboard-view-css' => '74fc9d98', | ||||||
|     'phui-workcard-view-css' => '9e9eb0df', |     'phui-workcard-view-css' => '913441b6', | ||||||
|     'phui-workpanel-view-css' => '3ae89b20', |     'phui-workpanel-view-css' => '3ae89b20', | ||||||
|     'phuix-action-list-view' => 'c68f183f', |     'phuix-action-list-view' => 'c68f183f', | ||||||
|     'phuix-action-view' => 'aaa08f3b', |     'phuix-action-view' => 'aaa08f3b', | ||||||
| @@ -1115,7 +1117,7 @@ return array( | |||||||
|       'javelin-json', |       'javelin-json', | ||||||
|       'phabricator-draggable-list', |       'phabricator-draggable-list', | ||||||
|     ), |     ), | ||||||
|     '27daef73' => array( |     '2633bef7' => array( | ||||||
|       'multirow-row-manager', |       'multirow-row-manager', | ||||||
|       'javelin-install', |       'javelin-install', | ||||||
|       'javelin-util', |       'javelin-util', | ||||||
| @@ -1367,6 +1369,12 @@ return array( | |||||||
|       'javelin-dom', |       'javelin-dom', | ||||||
|       'javelin-fx', |       'javelin-fx', | ||||||
|     ), |     ), | ||||||
|  |     '52e3ff03' => array( | ||||||
|  |       'phui-chart-css', | ||||||
|  |       'd3', | ||||||
|  |       'javelin-chart-curtain-view', | ||||||
|  |       'javelin-chart-function-label', | ||||||
|  |     ), | ||||||
|     '541f81c3' => array( |     '541f81c3' => array( | ||||||
|       'javelin-install', |       'javelin-install', | ||||||
|     ), |     ), | ||||||
| @@ -2128,12 +2136,6 @@ return array( | |||||||
|       'phabricator-keyboard-shortcut', |       'phabricator-keyboard-shortcut', | ||||||
|       'javelin-stratcom', |       'javelin-stratcom', | ||||||
|     ), |     ), | ||||||
|     'eec96de0' => array( |  | ||||||
|       'phui-chart-css', |  | ||||||
|       'd3', |  | ||||||
|       'javelin-chart-curtain-view', |  | ||||||
|       'javelin-chart-function-label', |  | ||||||
|     ), |  | ||||||
|     'ef836bf2' => array( |     'ef836bf2' => array( | ||||||
|       'javelin-behavior', |       'javelin-behavior', | ||||||
|       'javelin-dom', |       'javelin-dom', | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								resources/sql/autopatches/20190909.herald.01.rebuild.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								resources/sql/autopatches/20190909.herald.01.rebuild.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | PhabricatorRebuildIndexesWorker::rebuildObjectsWithQuery('HeraldRuleQuery'); | ||||||
| @@ -1021,6 +1021,7 @@ phutil_register_library_map(array( | |||||||
|     'DiffusionSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSSHWorkflow.php', |     'DiffusionSSHWorkflow' => 'applications/diffusion/ssh/DiffusionSSHWorkflow.php', | ||||||
|     'DiffusionSearchQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionSearchQueryConduitAPIMethod.php', |     'DiffusionSearchQueryConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionSearchQueryConduitAPIMethod.php', | ||||||
|     'DiffusionServeController' => 'applications/diffusion/controller/DiffusionServeController.php', |     'DiffusionServeController' => 'applications/diffusion/controller/DiffusionServeController.php', | ||||||
|  |     'DiffusionServiceRef' => 'applications/diffusion/ref/DiffusionServiceRef.php', | ||||||
|     'DiffusionSetPasswordSettingsPanel' => 'applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php', |     'DiffusionSetPasswordSettingsPanel' => 'applications/diffusion/panel/DiffusionSetPasswordSettingsPanel.php', | ||||||
|     'DiffusionSetupException' => 'applications/diffusion/exception/DiffusionSetupException.php', |     'DiffusionSetupException' => 'applications/diffusion/exception/DiffusionSetupException.php', | ||||||
|     'DiffusionSourceHyperlinkEngineExtension' => 'applications/diffusion/engineextension/DiffusionSourceHyperlinkEngineExtension.php', |     'DiffusionSourceHyperlinkEngineExtension' => 'applications/diffusion/engineextension/DiffusionSourceHyperlinkEngineExtension.php', | ||||||
| @@ -3107,6 +3108,7 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorDefaultRequestExceptionHandler' => 'aphront/handler/PhabricatorDefaultRequestExceptionHandler.php', |     'PhabricatorDefaultRequestExceptionHandler' => 'aphront/handler/PhabricatorDefaultRequestExceptionHandler.php', | ||||||
|     'PhabricatorDefaultSyntaxStyle' => 'infrastructure/syntax/PhabricatorDefaultSyntaxStyle.php', |     'PhabricatorDefaultSyntaxStyle' => 'infrastructure/syntax/PhabricatorDefaultSyntaxStyle.php', | ||||||
|     'PhabricatorDefaultUnlockEngine' => 'applications/system/engine/PhabricatorDefaultUnlockEngine.php', |     'PhabricatorDefaultUnlockEngine' => 'applications/system/engine/PhabricatorDefaultUnlockEngine.php', | ||||||
|  |     'PhabricatorDemoChartEngine' => 'applications/fact/engine/PhabricatorDemoChartEngine.php', | ||||||
|     'PhabricatorDestructibleCodex' => 'applications/system/codex/PhabricatorDestructibleCodex.php', |     'PhabricatorDestructibleCodex' => 'applications/system/codex/PhabricatorDestructibleCodex.php', | ||||||
|     'PhabricatorDestructibleCodexInterface' => 'applications/system/interface/PhabricatorDestructibleCodexInterface.php', |     'PhabricatorDestructibleCodexInterface' => 'applications/system/interface/PhabricatorDestructibleCodexInterface.php', | ||||||
|     'PhabricatorDestructibleInterface' => 'applications/system/interface/PhabricatorDestructibleInterface.php', |     'PhabricatorDestructibleInterface' => 'applications/system/interface/PhabricatorDestructibleInterface.php', | ||||||
| @@ -3220,6 +3222,7 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorEditEngineSettingsPanel' => 'applications/settings/panel/PhabricatorEditEngineSettingsPanel.php', |     'PhabricatorEditEngineSettingsPanel' => 'applications/settings/panel/PhabricatorEditEngineSettingsPanel.php', | ||||||
|     'PhabricatorEditEngineStaticCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineStaticCommentAction.php', |     'PhabricatorEditEngineStaticCommentAction' => 'applications/transactions/commentaction/PhabricatorEditEngineStaticCommentAction.php', | ||||||
|     'PhabricatorEditEngineSubtype' => 'applications/transactions/editengine/PhabricatorEditEngineSubtype.php', |     'PhabricatorEditEngineSubtype' => 'applications/transactions/editengine/PhabricatorEditEngineSubtype.php', | ||||||
|  |     'PhabricatorEditEngineSubtypeHeraldField' => 'applications/transactions/herald/PhabricatorEditEngineSubtypeHeraldField.php', | ||||||
|     'PhabricatorEditEngineSubtypeInterface' => 'applications/transactions/editengine/PhabricatorEditEngineSubtypeInterface.php', |     'PhabricatorEditEngineSubtypeInterface' => 'applications/transactions/editengine/PhabricatorEditEngineSubtypeInterface.php', | ||||||
|     'PhabricatorEditEngineSubtypeMap' => 'applications/transactions/editengine/PhabricatorEditEngineSubtypeMap.php', |     'PhabricatorEditEngineSubtypeMap' => 'applications/transactions/editengine/PhabricatorEditEngineSubtypeMap.php', | ||||||
|     'PhabricatorEditEngineSubtypeTestCase' => 'applications/transactions/editengine/__tests__/PhabricatorEditEngineSubtypeTestCase.php', |     'PhabricatorEditEngineSubtypeTestCase' => 'applications/transactions/editengine/__tests__/PhabricatorEditEngineSubtypeTestCase.php', | ||||||
| @@ -3441,8 +3444,10 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorFlagDeleteController' => 'applications/flag/controller/PhabricatorFlagDeleteController.php', |     'PhabricatorFlagDeleteController' => 'applications/flag/controller/PhabricatorFlagDeleteController.php', | ||||||
|     'PhabricatorFlagDestructionEngineExtension' => 'applications/flag/engineextension/PhabricatorFlagDestructionEngineExtension.php', |     'PhabricatorFlagDestructionEngineExtension' => 'applications/flag/engineextension/PhabricatorFlagDestructionEngineExtension.php', | ||||||
|     'PhabricatorFlagEditController' => 'applications/flag/controller/PhabricatorFlagEditController.php', |     'PhabricatorFlagEditController' => 'applications/flag/controller/PhabricatorFlagEditController.php', | ||||||
|  |     'PhabricatorFlagHeraldAction' => 'applications/flag/herald/PhabricatorFlagHeraldAction.php', | ||||||
|     'PhabricatorFlagListController' => 'applications/flag/controller/PhabricatorFlagListController.php', |     'PhabricatorFlagListController' => 'applications/flag/controller/PhabricatorFlagListController.php', | ||||||
|     'PhabricatorFlagQuery' => 'applications/flag/query/PhabricatorFlagQuery.php', |     'PhabricatorFlagQuery' => 'applications/flag/query/PhabricatorFlagQuery.php', | ||||||
|  |     'PhabricatorFlagRemoveFlagHeraldAction' => 'applications/flag/herald/PhabricatorFlagRemoveFlagHeraldAction.php', | ||||||
|     'PhabricatorFlagSearchEngine' => 'applications/flag/query/PhabricatorFlagSearchEngine.php', |     'PhabricatorFlagSearchEngine' => 'applications/flag/query/PhabricatorFlagSearchEngine.php', | ||||||
|     'PhabricatorFlagSelectControl' => 'applications/flag/view/PhabricatorFlagSelectControl.php', |     'PhabricatorFlagSelectControl' => 'applications/flag/view/PhabricatorFlagSelectControl.php', | ||||||
|     'PhabricatorFlaggableInterface' => 'applications/flag/interface/PhabricatorFlaggableInterface.php', |     'PhabricatorFlaggableInterface' => 'applications/flag/interface/PhabricatorFlaggableInterface.php', | ||||||
| @@ -4196,14 +4201,17 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorPolicyManagementWorkflow' => 'applications/policy/management/PhabricatorPolicyManagementWorkflow.php', |     'PhabricatorPolicyManagementWorkflow' => 'applications/policy/management/PhabricatorPolicyManagementWorkflow.php', | ||||||
|     'PhabricatorPolicyPHIDTypePolicy' => 'applications/policy/phid/PhabricatorPolicyPHIDTypePolicy.php', |     'PhabricatorPolicyPHIDTypePolicy' => 'applications/policy/phid/PhabricatorPolicyPHIDTypePolicy.php', | ||||||
|     'PhabricatorPolicyQuery' => 'applications/policy/query/PhabricatorPolicyQuery.php', |     'PhabricatorPolicyQuery' => 'applications/policy/query/PhabricatorPolicyQuery.php', | ||||||
|  |     'PhabricatorPolicyRef' => 'applications/policy/view/PhabricatorPolicyRef.php', | ||||||
|     'PhabricatorPolicyRequestExceptionHandler' => 'aphront/handler/PhabricatorPolicyRequestExceptionHandler.php', |     'PhabricatorPolicyRequestExceptionHandler' => 'aphront/handler/PhabricatorPolicyRequestExceptionHandler.php', | ||||||
|     'PhabricatorPolicyRule' => 'applications/policy/rule/PhabricatorPolicyRule.php', |     'PhabricatorPolicyRule' => 'applications/policy/rule/PhabricatorPolicyRule.php', | ||||||
|  |     'PhabricatorPolicyRulesView' => 'applications/policy/view/PhabricatorPolicyRulesView.php', | ||||||
|     'PhabricatorPolicySearchEngineExtension' => 'applications/policy/engineextension/PhabricatorPolicySearchEngineExtension.php', |     'PhabricatorPolicySearchEngineExtension' => 'applications/policy/engineextension/PhabricatorPolicySearchEngineExtension.php', | ||||||
|     'PhabricatorPolicyStrengthConstants' => 'applications/policy/constants/PhabricatorPolicyStrengthConstants.php', |     'PhabricatorPolicyStrengthConstants' => 'applications/policy/constants/PhabricatorPolicyStrengthConstants.php', | ||||||
|     'PhabricatorPolicyTestCase' => 'applications/policy/__tests__/PhabricatorPolicyTestCase.php', |     'PhabricatorPolicyTestCase' => 'applications/policy/__tests__/PhabricatorPolicyTestCase.php', | ||||||
|     'PhabricatorPolicyTestObject' => 'applications/policy/__tests__/PhabricatorPolicyTestObject.php', |     'PhabricatorPolicyTestObject' => 'applications/policy/__tests__/PhabricatorPolicyTestObject.php', | ||||||
|     'PhabricatorPolicyType' => 'applications/policy/constants/PhabricatorPolicyType.php', |     'PhabricatorPolicyType' => 'applications/policy/constants/PhabricatorPolicyType.php', | ||||||
|     'PhabricatorPonderApplication' => 'applications/ponder/application/PhabricatorPonderApplication.php', |     'PhabricatorPonderApplication' => 'applications/ponder/application/PhabricatorPonderApplication.php', | ||||||
|  |     'PhabricatorPreambleTestCase' => 'infrastructure/util/__tests__/PhabricatorPreambleTestCase.php', | ||||||
|     'PhabricatorPrimaryEmailUserLogType' => 'applications/people/userlog/PhabricatorPrimaryEmailUserLogType.php', |     'PhabricatorPrimaryEmailUserLogType' => 'applications/people/userlog/PhabricatorPrimaryEmailUserLogType.php', | ||||||
|     'PhabricatorProfileMenuEditEngine' => 'applications/search/editor/PhabricatorProfileMenuEditEngine.php', |     'PhabricatorProfileMenuEditEngine' => 'applications/search/editor/PhabricatorProfileMenuEditEngine.php', | ||||||
|     'PhabricatorProfileMenuEditor' => 'applications/search/editor/PhabricatorProfileMenuEditor.php', |     'PhabricatorProfileMenuEditor' => 'applications/search/editor/PhabricatorProfileMenuEditor.php', | ||||||
| @@ -4220,6 +4228,7 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorProfileMenuItemView' => 'applications/search/engine/PhabricatorProfileMenuItemView.php', |     'PhabricatorProfileMenuItemView' => 'applications/search/engine/PhabricatorProfileMenuItemView.php', | ||||||
|     'PhabricatorProfileMenuItemViewList' => 'applications/search/engine/PhabricatorProfileMenuItemViewList.php', |     'PhabricatorProfileMenuItemViewList' => 'applications/search/engine/PhabricatorProfileMenuItemViewList.php', | ||||||
|     'PhabricatorProject' => 'applications/project/storage/PhabricatorProject.php', |     'PhabricatorProject' => 'applications/project/storage/PhabricatorProject.php', | ||||||
|  |     'PhabricatorProjectActivityChartEngine' => 'applications/project/chart/PhabricatorProjectActivityChartEngine.php', | ||||||
|     'PhabricatorProjectAddHeraldAction' => 'applications/project/herald/PhabricatorProjectAddHeraldAction.php', |     'PhabricatorProjectAddHeraldAction' => 'applications/project/herald/PhabricatorProjectAddHeraldAction.php', | ||||||
|     'PhabricatorProjectApplication' => 'applications/project/application/PhabricatorProjectApplication.php', |     'PhabricatorProjectApplication' => 'applications/project/application/PhabricatorProjectApplication.php', | ||||||
|     'PhabricatorProjectArchiveController' => 'applications/project/controller/PhabricatorProjectArchiveController.php', |     'PhabricatorProjectArchiveController' => 'applications/project/controller/PhabricatorProjectArchiveController.php', | ||||||
| @@ -4419,6 +4428,7 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorProjectsWatchersSearchEngineAttachment' => 'applications/project/engineextension/PhabricatorProjectsWatchersSearchEngineAttachment.php', |     'PhabricatorProjectsWatchersSearchEngineAttachment' => 'applications/project/engineextension/PhabricatorProjectsWatchersSearchEngineAttachment.php', | ||||||
|     'PhabricatorPronounSetting' => 'applications/settings/setting/PhabricatorPronounSetting.php', |     'PhabricatorPronounSetting' => 'applications/settings/setting/PhabricatorPronounSetting.php', | ||||||
|     'PhabricatorProtocolLog' => 'infrastructure/log/PhabricatorProtocolLog.php', |     'PhabricatorProtocolLog' => 'infrastructure/log/PhabricatorProtocolLog.php', | ||||||
|  |     'PhabricatorPureChartFunction' => 'applications/fact/chart/PhabricatorPureChartFunction.php', | ||||||
|     'PhabricatorPygmentSetupCheck' => 'applications/config/check/PhabricatorPygmentSetupCheck.php', |     'PhabricatorPygmentSetupCheck' => 'applications/config/check/PhabricatorPygmentSetupCheck.php', | ||||||
|     'PhabricatorQuery' => 'infrastructure/query/PhabricatorQuery.php', |     'PhabricatorQuery' => 'infrastructure/query/PhabricatorQuery.php', | ||||||
|     'PhabricatorQueryConstraint' => 'infrastructure/query/constraint/PhabricatorQueryConstraint.php', |     'PhabricatorQueryConstraint' => 'infrastructure/query/constraint/PhabricatorQueryConstraint.php', | ||||||
| @@ -4657,6 +4667,7 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorSearchScopeSetting' => 'applications/settings/setting/PhabricatorSearchScopeSetting.php', |     'PhabricatorSearchScopeSetting' => 'applications/settings/setting/PhabricatorSearchScopeSetting.php', | ||||||
|     'PhabricatorSearchSelectField' => 'applications/search/field/PhabricatorSearchSelectField.php', |     'PhabricatorSearchSelectField' => 'applications/search/field/PhabricatorSearchSelectField.php', | ||||||
|     'PhabricatorSearchService' => 'infrastructure/cluster/search/PhabricatorSearchService.php', |     'PhabricatorSearchService' => 'infrastructure/cluster/search/PhabricatorSearchService.php', | ||||||
|  |     'PhabricatorSearchSettingsPanel' => 'applications/settings/panel/PhabricatorSearchSettingsPanel.php', | ||||||
|     'PhabricatorSearchStringListField' => 'applications/search/field/PhabricatorSearchStringListField.php', |     'PhabricatorSearchStringListField' => 'applications/search/field/PhabricatorSearchStringListField.php', | ||||||
|     'PhabricatorSearchSubscribersField' => 'applications/search/field/PhabricatorSearchSubscribersField.php', |     'PhabricatorSearchSubscribersField' => 'applications/search/field/PhabricatorSearchSubscribersField.php', | ||||||
|     'PhabricatorSearchTextField' => 'applications/search/field/PhabricatorSearchTextField.php', |     'PhabricatorSearchTextField' => 'applications/search/field/PhabricatorSearchTextField.php', | ||||||
| @@ -6974,6 +6985,7 @@ phutil_register_library_map(array( | |||||||
|     'DiffusionSSHWorkflow' => 'PhabricatorSSHWorkflow', |     'DiffusionSSHWorkflow' => 'PhabricatorSSHWorkflow', | ||||||
|     'DiffusionSearchQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', |     'DiffusionSearchQueryConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', | ||||||
|     'DiffusionServeController' => 'DiffusionController', |     'DiffusionServeController' => 'DiffusionController', | ||||||
|  |     'DiffusionServiceRef' => 'Phobject', | ||||||
|     'DiffusionSetPasswordSettingsPanel' => 'PhabricatorSettingsPanel', |     'DiffusionSetPasswordSettingsPanel' => 'PhabricatorSettingsPanel', | ||||||
|     'DiffusionSetupException' => 'Exception', |     'DiffusionSetupException' => 'Exception', | ||||||
|     'DiffusionSourceHyperlinkEngineExtension' => 'PhabricatorRemarkupHyperlinkEngineExtension', |     'DiffusionSourceHyperlinkEngineExtension' => 'PhabricatorRemarkupHyperlinkEngineExtension', | ||||||
| @@ -8299,7 +8311,7 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorAccessLog' => 'Phobject', |     'PhabricatorAccessLog' => 'Phobject', | ||||||
|     'PhabricatorAccessLogConfigOptions' => 'PhabricatorApplicationConfigOptions', |     'PhabricatorAccessLogConfigOptions' => 'PhabricatorApplicationConfigOptions', | ||||||
|     'PhabricatorAccessibilitySetting' => 'PhabricatorSelectSetting', |     'PhabricatorAccessibilitySetting' => 'PhabricatorSelectSetting', | ||||||
|     'PhabricatorAccumulateChartFunction' => 'PhabricatorChartFunction', |     'PhabricatorAccumulateChartFunction' => 'PhabricatorHigherOrderChartFunction', | ||||||
|     'PhabricatorActionListView' => 'AphrontTagView', |     'PhabricatorActionListView' => 'AphrontTagView', | ||||||
|     'PhabricatorActionView' => 'AphrontView', |     'PhabricatorActionView' => 'AphrontView', | ||||||
|     'PhabricatorActivitySettingsPanel' => 'PhabricatorSettingsPanel', |     'PhabricatorActivitySettingsPanel' => 'PhabricatorSettingsPanel', | ||||||
| @@ -9157,7 +9169,7 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorConpherenceWidgetVisibleSetting' => 'PhabricatorInternalSetting', |     'PhabricatorConpherenceWidgetVisibleSetting' => 'PhabricatorInternalSetting', | ||||||
|     'PhabricatorConsoleApplication' => 'PhabricatorApplication', |     'PhabricatorConsoleApplication' => 'PhabricatorApplication', | ||||||
|     'PhabricatorConsoleContentSource' => 'PhabricatorContentSource', |     'PhabricatorConsoleContentSource' => 'PhabricatorContentSource', | ||||||
|     'PhabricatorConstantChartFunction' => 'PhabricatorChartFunction', |     'PhabricatorConstantChartFunction' => 'PhabricatorPureChartFunction', | ||||||
|     'PhabricatorContactNumbersSettingsPanel' => 'PhabricatorSettingsPanel', |     'PhabricatorContactNumbersSettingsPanel' => 'PhabricatorSettingsPanel', | ||||||
|     'PhabricatorContentSource' => 'Phobject', |     'PhabricatorContentSource' => 'Phobject', | ||||||
|     'PhabricatorContentSourceModule' => 'PhabricatorConfigModule', |     'PhabricatorContentSourceModule' => 'PhabricatorConfigModule', | ||||||
| @@ -9169,7 +9181,7 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorCoreCreateTransaction' => 'PhabricatorCoreTransactionType', |     'PhabricatorCoreCreateTransaction' => 'PhabricatorCoreTransactionType', | ||||||
|     'PhabricatorCoreTransactionType' => 'PhabricatorModularTransactionType', |     'PhabricatorCoreTransactionType' => 'PhabricatorModularTransactionType', | ||||||
|     'PhabricatorCoreVoidTransaction' => 'PhabricatorModularTransactionType', |     'PhabricatorCoreVoidTransaction' => 'PhabricatorModularTransactionType', | ||||||
|     'PhabricatorCosChartFunction' => 'PhabricatorChartFunction', |     'PhabricatorCosChartFunction' => 'PhabricatorPureChartFunction', | ||||||
|     'PhabricatorCountFact' => 'PhabricatorFact', |     'PhabricatorCountFact' => 'PhabricatorFact', | ||||||
|     'PhabricatorCountdown' => array( |     'PhabricatorCountdown' => array( | ||||||
|       'PhabricatorCountdownDAO', |       'PhabricatorCountdownDAO', | ||||||
| @@ -9435,6 +9447,7 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorDefaultRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler', |     'PhabricatorDefaultRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler', | ||||||
|     'PhabricatorDefaultSyntaxStyle' => 'PhabricatorSyntaxStyle', |     'PhabricatorDefaultSyntaxStyle' => 'PhabricatorSyntaxStyle', | ||||||
|     'PhabricatorDefaultUnlockEngine' => 'PhabricatorUnlockEngine', |     'PhabricatorDefaultUnlockEngine' => 'PhabricatorUnlockEngine', | ||||||
|  |     'PhabricatorDemoChartEngine' => 'PhabricatorChartEngine', | ||||||
|     'PhabricatorDestructibleCodex' => 'Phobject', |     'PhabricatorDestructibleCodex' => 'Phobject', | ||||||
|     'PhabricatorDestructionEngine' => 'Phobject', |     'PhabricatorDestructionEngine' => 'Phobject', | ||||||
|     'PhabricatorDestructionEngineExtension' => 'Phobject', |     'PhabricatorDestructionEngineExtension' => 'Phobject', | ||||||
| @@ -9553,6 +9566,7 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorEditEngineSettingsPanel' => 'PhabricatorSettingsPanel', |     'PhabricatorEditEngineSettingsPanel' => 'PhabricatorSettingsPanel', | ||||||
|     'PhabricatorEditEngineStaticCommentAction' => 'PhabricatorEditEngineCommentAction', |     'PhabricatorEditEngineStaticCommentAction' => 'PhabricatorEditEngineCommentAction', | ||||||
|     'PhabricatorEditEngineSubtype' => 'Phobject', |     'PhabricatorEditEngineSubtype' => 'Phobject', | ||||||
|  |     'PhabricatorEditEngineSubtypeHeraldField' => 'HeraldField', | ||||||
|     'PhabricatorEditEngineSubtypeMap' => 'Phobject', |     'PhabricatorEditEngineSubtypeMap' => 'Phobject', | ||||||
|     'PhabricatorEditEngineSubtypeTestCase' => 'PhabricatorTestCase', |     'PhabricatorEditEngineSubtypeTestCase' => 'PhabricatorTestCase', | ||||||
|     'PhabricatorEditEngineSubtypeTransaction' => 'PhabricatorEditEngineTransactionType', |     'PhabricatorEditEngineSubtypeTransaction' => 'PhabricatorEditEngineTransactionType', | ||||||
| @@ -9806,7 +9820,7 @@ phutil_register_library_map(array( | |||||||
|       'PhabricatorFlagDAO', |       'PhabricatorFlagDAO', | ||||||
|       'PhabricatorPolicyInterface', |       'PhabricatorPolicyInterface', | ||||||
|     ), |     ), | ||||||
|     'PhabricatorFlagAddFlagHeraldAction' => 'HeraldAction', |     'PhabricatorFlagAddFlagHeraldAction' => 'PhabricatorFlagHeraldAction', | ||||||
|     'PhabricatorFlagColor' => 'PhabricatorFlagConstants', |     'PhabricatorFlagColor' => 'PhabricatorFlagConstants', | ||||||
|     'PhabricatorFlagConstants' => 'Phobject', |     'PhabricatorFlagConstants' => 'Phobject', | ||||||
|     'PhabricatorFlagController' => 'PhabricatorController', |     'PhabricatorFlagController' => 'PhabricatorController', | ||||||
| @@ -9814,8 +9828,10 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorFlagDeleteController' => 'PhabricatorFlagController', |     'PhabricatorFlagDeleteController' => 'PhabricatorFlagController', | ||||||
|     'PhabricatorFlagDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', |     'PhabricatorFlagDestructionEngineExtension' => 'PhabricatorDestructionEngineExtension', | ||||||
|     'PhabricatorFlagEditController' => 'PhabricatorFlagController', |     'PhabricatorFlagEditController' => 'PhabricatorFlagController', | ||||||
|  |     'PhabricatorFlagHeraldAction' => 'HeraldAction', | ||||||
|     'PhabricatorFlagListController' => 'PhabricatorFlagController', |     'PhabricatorFlagListController' => 'PhabricatorFlagController', | ||||||
|     'PhabricatorFlagQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', |     'PhabricatorFlagQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', | ||||||
|  |     'PhabricatorFlagRemoveFlagHeraldAction' => 'PhabricatorFlagHeraldAction', | ||||||
|     'PhabricatorFlagSearchEngine' => 'PhabricatorApplicationSearchEngine', |     'PhabricatorFlagSearchEngine' => 'PhabricatorApplicationSearchEngine', | ||||||
|     'PhabricatorFlagSelectControl' => 'AphrontFormControl', |     'PhabricatorFlagSelectControl' => 'AphrontFormControl', | ||||||
|     'PhabricatorFlaggableInterface' => 'PhabricatorPHIDInterface', |     'PhabricatorFlaggableInterface' => 'PhabricatorPHIDInterface', | ||||||
| @@ -10067,7 +10083,7 @@ phutil_register_library_map(array( | |||||||
|       'PhabricatorMarkupInterface', |       'PhabricatorMarkupInterface', | ||||||
|     ), |     ), | ||||||
|     'PhabricatorMarkupPreviewController' => 'PhabricatorController', |     'PhabricatorMarkupPreviewController' => 'PhabricatorController', | ||||||
|     'PhabricatorMaxChartFunction' => 'PhabricatorChartFunction', |     'PhabricatorMaxChartFunction' => 'PhabricatorPureChartFunction', | ||||||
|     'PhabricatorMemeEngine' => 'Phobject', |     'PhabricatorMemeEngine' => 'Phobject', | ||||||
|     'PhabricatorMemeRemarkupRule' => 'PhutilRemarkupRule', |     'PhabricatorMemeRemarkupRule' => 'PhutilRemarkupRule', | ||||||
|     'PhabricatorMentionRemarkupRule' => 'PhutilRemarkupRule', |     'PhabricatorMentionRemarkupRule' => 'PhutilRemarkupRule', | ||||||
| @@ -10134,7 +10150,7 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorMetronome' => 'Phobject', |     'PhabricatorMetronome' => 'Phobject', | ||||||
|     'PhabricatorMetronomeTestCase' => 'PhabricatorTestCase', |     'PhabricatorMetronomeTestCase' => 'PhabricatorTestCase', | ||||||
|     'PhabricatorMetronomicTriggerClock' => 'PhabricatorTriggerClock', |     'PhabricatorMetronomicTriggerClock' => 'PhabricatorTriggerClock', | ||||||
|     'PhabricatorMinChartFunction' => 'PhabricatorChartFunction', |     'PhabricatorMinChartFunction' => 'PhabricatorPureChartFunction', | ||||||
|     'PhabricatorModularTransaction' => 'PhabricatorApplicationTransaction', |     'PhabricatorModularTransaction' => 'PhabricatorApplicationTransaction', | ||||||
|     'PhabricatorModularTransactionType' => 'Phobject', |     'PhabricatorModularTransactionType' => 'Phobject', | ||||||
|     'PhabricatorMonogramDatasourceEngineExtension' => 'PhabricatorDatasourceEngineExtension', |     'PhabricatorMonogramDatasourceEngineExtension' => 'PhabricatorDatasourceEngineExtension', | ||||||
| @@ -10678,8 +10694,10 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorPolicyManagementWorkflow' => 'PhabricatorManagementWorkflow', |     'PhabricatorPolicyManagementWorkflow' => 'PhabricatorManagementWorkflow', | ||||||
|     'PhabricatorPolicyPHIDTypePolicy' => 'PhabricatorPHIDType', |     'PhabricatorPolicyPHIDTypePolicy' => 'PhabricatorPHIDType', | ||||||
|     'PhabricatorPolicyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', |     'PhabricatorPolicyQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', | ||||||
|  |     'PhabricatorPolicyRef' => 'Phobject', | ||||||
|     'PhabricatorPolicyRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler', |     'PhabricatorPolicyRequestExceptionHandler' => 'PhabricatorRequestExceptionHandler', | ||||||
|     'PhabricatorPolicyRule' => 'Phobject', |     'PhabricatorPolicyRule' => 'Phobject', | ||||||
|  |     'PhabricatorPolicyRulesView' => 'AphrontView', | ||||||
|     'PhabricatorPolicySearchEngineExtension' => 'PhabricatorSearchEngineExtension', |     'PhabricatorPolicySearchEngineExtension' => 'PhabricatorSearchEngineExtension', | ||||||
|     'PhabricatorPolicyStrengthConstants' => 'PhabricatorPolicyConstants', |     'PhabricatorPolicyStrengthConstants' => 'PhabricatorPolicyConstants', | ||||||
|     'PhabricatorPolicyTestCase' => 'PhabricatorTestCase', |     'PhabricatorPolicyTestCase' => 'PhabricatorTestCase', | ||||||
| @@ -10690,6 +10708,7 @@ phutil_register_library_map(array( | |||||||
|     ), |     ), | ||||||
|     'PhabricatorPolicyType' => 'PhabricatorPolicyConstants', |     'PhabricatorPolicyType' => 'PhabricatorPolicyConstants', | ||||||
|     'PhabricatorPonderApplication' => 'PhabricatorApplication', |     'PhabricatorPonderApplication' => 'PhabricatorApplication', | ||||||
|  |     'PhabricatorPreambleTestCase' => 'PhabricatorTestCase', | ||||||
|     'PhabricatorPrimaryEmailUserLogType' => 'PhabricatorUserLogType', |     'PhabricatorPrimaryEmailUserLogType' => 'PhabricatorUserLogType', | ||||||
|     'PhabricatorProfileMenuEditEngine' => 'PhabricatorEditEngine', |     'PhabricatorProfileMenuEditEngine' => 'PhabricatorEditEngine', | ||||||
|     'PhabricatorProfileMenuEditor' => 'PhabricatorApplicationTransactionEditor', |     'PhabricatorProfileMenuEditor' => 'PhabricatorApplicationTransactionEditor', | ||||||
| @@ -10726,6 +10745,7 @@ phutil_register_library_map(array( | |||||||
|       'PhabricatorSpacesInterface', |       'PhabricatorSpacesInterface', | ||||||
|       'PhabricatorEditEngineSubtypeInterface', |       'PhabricatorEditEngineSubtypeInterface', | ||||||
|     ), |     ), | ||||||
|  |     'PhabricatorProjectActivityChartEngine' => 'PhabricatorChartEngine', | ||||||
|     'PhabricatorProjectAddHeraldAction' => 'PhabricatorProjectHeraldAction', |     'PhabricatorProjectAddHeraldAction' => 'PhabricatorProjectHeraldAction', | ||||||
|     'PhabricatorProjectApplication' => 'PhabricatorApplication', |     'PhabricatorProjectApplication' => 'PhabricatorApplication', | ||||||
|     'PhabricatorProjectArchiveController' => 'PhabricatorProjectController', |     'PhabricatorProjectArchiveController' => 'PhabricatorProjectController', | ||||||
| @@ -10946,6 +10966,7 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorProjectsWatchersSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', |     'PhabricatorProjectsWatchersSearchEngineAttachment' => 'PhabricatorSearchEngineAttachment', | ||||||
|     'PhabricatorPronounSetting' => 'PhabricatorSelectSetting', |     'PhabricatorPronounSetting' => 'PhabricatorSelectSetting', | ||||||
|     'PhabricatorProtocolLog' => 'Phobject', |     'PhabricatorProtocolLog' => 'Phobject', | ||||||
|  |     'PhabricatorPureChartFunction' => 'PhabricatorChartFunction', | ||||||
|     'PhabricatorPygmentSetupCheck' => 'PhabricatorSetupCheck', |     'PhabricatorPygmentSetupCheck' => 'PhabricatorSetupCheck', | ||||||
|     'PhabricatorQuery' => 'Phobject', |     'PhabricatorQuery' => 'Phobject', | ||||||
|     'PhabricatorQueryConstraint' => 'Phobject', |     'PhabricatorQueryConstraint' => 'Phobject', | ||||||
| @@ -11204,7 +11225,7 @@ phutil_register_library_map(array( | |||||||
|       'PhabricatorPolicyInterface', |       'PhabricatorPolicyInterface', | ||||||
|     ), |     ), | ||||||
|     'PhabricatorSavedQueryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', |     'PhabricatorSavedQueryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', | ||||||
|     'PhabricatorScaleChartFunction' => 'PhabricatorChartFunction', |     'PhabricatorScaleChartFunction' => 'PhabricatorPureChartFunction', | ||||||
|     'PhabricatorScheduleTaskTriggerAction' => 'PhabricatorTriggerAction', |     'PhabricatorScheduleTaskTriggerAction' => 'PhabricatorTriggerAction', | ||||||
|     'PhabricatorScopedEnv' => 'Phobject', |     'PhabricatorScopedEnv' => 'Phobject', | ||||||
|     'PhabricatorSearchAbstractDocument' => 'Phobject', |     'PhabricatorSearchAbstractDocument' => 'Phobject', | ||||||
| @@ -11255,9 +11276,10 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorSearchResultBucketGroup' => 'Phobject', |     'PhabricatorSearchResultBucketGroup' => 'Phobject', | ||||||
|     'PhabricatorSearchResultView' => 'AphrontView', |     'PhabricatorSearchResultView' => 'AphrontView', | ||||||
|     'PhabricatorSearchSchemaSpec' => 'PhabricatorConfigSchemaSpec', |     'PhabricatorSearchSchemaSpec' => 'PhabricatorConfigSchemaSpec', | ||||||
|     'PhabricatorSearchScopeSetting' => 'PhabricatorInternalSetting', |     'PhabricatorSearchScopeSetting' => 'PhabricatorSelectSetting', | ||||||
|     'PhabricatorSearchSelectField' => 'PhabricatorSearchField', |     'PhabricatorSearchSelectField' => 'PhabricatorSearchField', | ||||||
|     'PhabricatorSearchService' => 'Phobject', |     'PhabricatorSearchService' => 'Phobject', | ||||||
|  |     'PhabricatorSearchSettingsPanel' => 'PhabricatorEditEngineSettingsPanel', | ||||||
|     'PhabricatorSearchStringListField' => 'PhabricatorSearchField', |     'PhabricatorSearchStringListField' => 'PhabricatorSearchField', | ||||||
|     'PhabricatorSearchSubscribersField' => 'PhabricatorSearchTokenizerField', |     'PhabricatorSearchSubscribersField' => 'PhabricatorSearchTokenizerField', | ||||||
|     'PhabricatorSearchTextField' => 'PhabricatorSearchField', |     'PhabricatorSearchTextField' => 'PhabricatorSearchField', | ||||||
| @@ -11295,12 +11317,12 @@ phutil_register_library_map(array( | |||||||
|     'PhabricatorSetupIssue' => 'Phobject', |     'PhabricatorSetupIssue' => 'Phobject', | ||||||
|     'PhabricatorSetupIssueUIExample' => 'PhabricatorUIExample', |     'PhabricatorSetupIssueUIExample' => 'PhabricatorUIExample', | ||||||
|     'PhabricatorSetupIssueView' => 'AphrontView', |     'PhabricatorSetupIssueView' => 'AphrontView', | ||||||
|     'PhabricatorShiftChartFunction' => 'PhabricatorChartFunction', |     'PhabricatorShiftChartFunction' => 'PhabricatorPureChartFunction', | ||||||
|     'PhabricatorShortSite' => 'PhabricatorSite', |     'PhabricatorShortSite' => 'PhabricatorSite', | ||||||
|     'PhabricatorShowFiletreeSetting' => 'PhabricatorSelectSetting', |     'PhabricatorShowFiletreeSetting' => 'PhabricatorSelectSetting', | ||||||
|     'PhabricatorSignDocumentsUserLogType' => 'PhabricatorUserLogType', |     'PhabricatorSignDocumentsUserLogType' => 'PhabricatorUserLogType', | ||||||
|     'PhabricatorSimpleEditType' => 'PhabricatorEditType', |     'PhabricatorSimpleEditType' => 'PhabricatorEditType', | ||||||
|     'PhabricatorSinChartFunction' => 'PhabricatorChartFunction', |     'PhabricatorSinChartFunction' => 'PhabricatorPureChartFunction', | ||||||
|     'PhabricatorSite' => 'AphrontSite', |     'PhabricatorSite' => 'AphrontSite', | ||||||
|     'PhabricatorSlackAuthProvider' => 'PhabricatorOAuth2AuthProvider', |     'PhabricatorSlackAuthProvider' => 'PhabricatorOAuth2AuthProvider', | ||||||
|     'PhabricatorSlowvoteApplication' => 'PhabricatorApplication', |     'PhabricatorSlowvoteApplication' => 'PhabricatorApplication', | ||||||
|   | |||||||
| @@ -68,12 +68,42 @@ final class PhabricatorLogoutController | |||||||
|         ->setURI('/auth/loggedout/'); |         ->setURI('/auth/loggedout/'); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |  | ||||||
|     if ($viewer->getPHID()) { |     if ($viewer->getPHID()) { | ||||||
|       return $this->newDialog() |       $dialog = $this->newDialog() | ||||||
|         ->setTitle(pht('Log Out?')) |         ->setTitle(pht('Log Out?')) | ||||||
|         ->appendChild(pht('Are you sure you want to log out?')) |         ->appendParagraph(pht('Are you sure you want to log out?')) | ||||||
|         ->addSubmitButton(pht('Log Out')) |  | ||||||
|         ->addCancelButton('/'); |         ->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('/'); |     return id(new AphrontRedirectResponse())->setURI('/'); | ||||||
|   | |||||||
| @@ -64,7 +64,7 @@ final class PhabricatorAuthListController | |||||||
|           array( |           array( | ||||||
|             'href' => $this->getApplicationURI('config/new/'), |             'href' => $this->getApplicationURI('config/new/'), | ||||||
|           ), |           ), | ||||||
|           pht('Add Authentication Provider')))); |           pht('Add Provider')))); | ||||||
|  |  | ||||||
|     $crumbs = $this->buildApplicationCrumbs(); |     $crumbs = $this->buildApplicationCrumbs(); | ||||||
|     $crumbs->addTextCrumb(pht('Login and Registration')); |     $crumbs->addTextCrumb(pht('Login and Registration')); | ||||||
|   | |||||||
| @@ -50,33 +50,50 @@ final class PhabricatorMySQLSetupCheck extends PhabricatorSetupCheck { | |||||||
|     if (!in_array('STRICT_ALL_TABLES', $modes)) { |     if (!in_array('STRICT_ALL_TABLES', $modes)) { | ||||||
|       $summary = pht( |       $summary = pht( | ||||||
|         'MySQL is not in strict mode (on host "%s"), but using strict mode '. |         'MySQL is not in strict mode (on host "%s"), but using strict mode '. | ||||||
|         'is strongly encouraged.', |         'is recommended.', | ||||||
|         $host_name); |         $host_name); | ||||||
|  |  | ||||||
|       $message = pht( |       $message = pht( | ||||||
|         "On database host \"%s\", the global %s is not set to %s. ". |         'On database host "%s", the global "sql_mode" setting does not '. | ||||||
|         "It is strongly encouraged that you enable this mode when running ". |         'include the "STRICT_ALL_TABLES" mode. Enabling this mode is '. | ||||||
|         "Phabricator.\n\n". |         'recommended to generally improve how MySQL handles certain errors.'. | ||||||
|         "By default MySQL will silently ignore some types of errors, which ". |         "\n\n". | ||||||
|         "can cause data loss and raise security concerns. Enabling strict ". |         'Without this mode enabled, MySQL will silently ignore some error '. | ||||||
|         "mode makes MySQL raise an explicit error instead, and prevents this ". |         'conditions, including inserts which attempt to store more data in '. | ||||||
|         "entire class of problems from doing any damage.\n\n". |         'a column than actually fits. This behavior is usually undesirable '. | ||||||
|         "You can find more information about this mode (and how to configure ". |         'and can lead to data corruption (by truncating multibyte characters '. | ||||||
|         "it) in the MySQL manual. Usually, it is sufficient to add this to ". |         'in the middle), data loss (by discarding the data which does not '. | ||||||
|         "your %s file (in the %s section) and then restart %s:\n\n". |         'fit into the column), or security concerns (for example, by '. | ||||||
|         "%s\n". |         'truncating keys or credentials).'. | ||||||
|         "(Note that if you run other applications against the same database, ". |         "\n\n". | ||||||
|         "they may not work in strict mode. Be careful about enabling it in ". |         'Phabricator is developed and tested in "STRICT_ALL_TABLES" mode so '. | ||||||
|         "these cases.)", |         '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, |         $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')); |         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')) |         ->setName(pht('MySQL %s Mode Not Set', 'STRICT_ALL_TABLES')) | ||||||
|         ->setSummary($summary) |         ->setSummary($summary) | ||||||
|         ->setMessage($message) |         ->setMessage($message) | ||||||
| @@ -84,49 +101,6 @@ final class PhabricatorMySQLSetupCheck extends PhabricatorSetupCheck { | |||||||
|         ->addMySQLConfig('sql_mode'); |         ->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_innodb_fulltext = false; | ||||||
|     $is_myisam_fulltext = false; |     $is_myisam_fulltext = false; | ||||||
|     if ($this->shouldUseMySQLSearchEngine()) { |     if ($this->shouldUseMySQLSearchEngine()) { | ||||||
|   | |||||||
| @@ -140,11 +140,20 @@ final class PhabricatorConfigManagementSetWorkflow | |||||||
|         'Wrote configuration key "%s" to database storage.', |         'Wrote configuration key "%s" to database storage.', | ||||||
|         $key); |         $key); | ||||||
|     } else { |     } else { | ||||||
|       $config_source = id(new PhabricatorConfigLocalSource()) |       $config_source = new PhabricatorConfigLocalSource(); | ||||||
|         ->setKeys(array($key => $value)); |  | ||||||
|  |  | ||||||
|       $local_path = $config_source->getReadablePath(); |       $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( |       $write_message = pht( | ||||||
|         'Wrote configuration key "%s" to local storage (in file "%s").', |         'Wrote configuration key "%s" to local storage (in file "%s").', | ||||||
|         $key, |         $key, | ||||||
|   | |||||||
							
								
								
									
										48
									
								
								src/applications/diffusion/ref/DiffusionServiceRef.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/applications/diffusion/ref/DiffusionServiceRef.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,48 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | final class DiffusionServiceRef | ||||||
|  |   extends Phobject { | ||||||
|  |  | ||||||
|  |   private $uri; | ||||||
|  |   private $protocol; | ||||||
|  |   private $isWritable; | ||||||
|  |   private $devicePHID; | ||||||
|  |   private $deviceName; | ||||||
|  |  | ||||||
|  |   private function __construct() { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public static function newFromDictionary(array $map) { | ||||||
|  |     $ref = new self(); | ||||||
|  |  | ||||||
|  |     $ref->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; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -14,42 +14,33 @@ final class DiffusionGitReceivePackSSHWorkflow extends DiffusionGitSSHWorkflow { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected function executeRepositoryOperations() { |   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); |     $host_wait_start = microtime(true); | ||||||
|  |  | ||||||
|     $repository = $this->getRepository(); |     $repository = $this->getRepository(); | ||||||
|     $viewer = $this->getSSHUser(); |     $viewer = $this->getSSHUser(); | ||||||
|     $device = AlmanacKeys::getLiveDevice(); |     $device = AlmanacKeys::getLiveDevice(); | ||||||
|  |  | ||||||
|     // This is a write, and must have write access. |  | ||||||
|     $this->requireWriteAccess(); |  | ||||||
|  |  | ||||||
|     $cluster_engine = id(new DiffusionRepositoryClusterEngine()) |     $cluster_engine = id(new DiffusionRepositoryClusterEngine()) | ||||||
|       ->setViewer($viewer) |       ->setViewer($viewer) | ||||||
|       ->setRepository($repository) |       ->setRepository($repository) | ||||||
|       ->setLog($this); |       ->setLog($this); | ||||||
|  |  | ||||||
|     $is_proxy = $this->shouldProxy(); |     $command = csprintf('git-receive-pack %s', $repository->getLocalPath()); | ||||||
|     if ($is_proxy) { |     $cluster_engine->synchronizeWorkingCopyBeforeWrite(); | ||||||
|       $command = $this->getProxyCommand(true); |  | ||||||
|       $did_write = false; |  | ||||||
|  |  | ||||||
|       if ($device) { |     if ($device) { | ||||||
|         $this->writeClusterEngineLogMessage( |       $this->writeClusterEngineLogMessage( | ||||||
|           pht( |         pht( | ||||||
|             "# Push received by \"%s\", forwarding to cluster host.\n", |           "# Ready to receive on cluster host \"%s\".\n", | ||||||
|             $device->getName())); |           $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())); |  | ||||||
|       } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     $log = $this->newProtocolLog($is_proxy); |     $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 |     // We've committed the write (or rejected it), so we can release the lock | ||||||
|     // without waiting for the client to receive the acknowledgement. |     // without waiting for the client to receive the acknowledgement. | ||||||
|     if ($did_write) { |     $cluster_engine->synchronizeWorkingCopyAfterWrite(); | ||||||
|       $cluster_engine->synchronizeWorkingCopyAfterWrite(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if ($caught) { |     if ($caught) { | ||||||
|       throw $caught; |       throw $caught; | ||||||
| @@ -85,18 +74,16 @@ final class DiffusionGitReceivePackSSHWorkflow extends DiffusionGitSSHWorkflow { | |||||||
|       // When a repository is clustered, we reach this cleanup code on both |       // 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 |       // the proxy and the actual final endpoint node. Don't do more cleanup | ||||||
|       // or logging than we need to. |       // or logging than we need to. | ||||||
|       if ($did_write) { |       $repository->writeStatusMessage( | ||||||
|         $repository->writeStatusMessage( |         PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE, | ||||||
|           PhabricatorRepositoryStatusMessage::TYPE_NEEDS_UPDATE, |         PhabricatorRepositoryStatusMessage::CODE_OKAY); | ||||||
|           PhabricatorRepositoryStatusMessage::CODE_OKAY); |  | ||||||
|  |  | ||||||
|         $host_wait_end = microtime(true); |       $host_wait_end = microtime(true); | ||||||
|  |  | ||||||
|         $this->updatePushLogWithTimingInformation( |       $this->updatePushLogWithTimingInformation( | ||||||
|           $this->getClusterEngineLogProperty('writeWait'), |         $this->getClusterEngineLogProperty('writeWait'), | ||||||
|           $this->getClusterEngineLogProperty('readWait'), |         $this->getClusterEngineLogProperty('readWait'), | ||||||
|           ($host_wait_end - $host_wait_start)); |         ($host_wait_end - $host_wait_start)); | ||||||
|       } |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return $err; |     return $err; | ||||||
|   | |||||||
| @@ -8,6 +8,10 @@ abstract class DiffusionGitSSHWorkflow | |||||||
|   private $protocolLog; |   private $protocolLog; | ||||||
|  |  | ||||||
|   private $wireProtocol; |   private $wireProtocol; | ||||||
|  |   private $ioBytesRead = 0; | ||||||
|  |   private $ioBytesWritten = 0; | ||||||
|  |   private $requestAttempts = 0; | ||||||
|  |   private $requestFailures = 0; | ||||||
|  |  | ||||||
|   protected function writeError($message) { |   protected function writeError($message) { | ||||||
|     // Git assumes we'll add our own newlines. |     // Git assumes we'll add our own newlines. | ||||||
| @@ -98,6 +102,8 @@ abstract class DiffusionGitSSHWorkflow | |||||||
|     PhabricatorSSHPassthruCommand $command, |     PhabricatorSSHPassthruCommand $command, | ||||||
|     $message) { |     $message) { | ||||||
|  |  | ||||||
|  |     $this->ioBytesWritten += strlen($message); | ||||||
|  |  | ||||||
|     $log = $this->getProtocolLog(); |     $log = $this->getProtocolLog(); | ||||||
|     if ($log) { |     if ($log) { | ||||||
|       $log->didWriteBytes($message); |       $log->didWriteBytes($message); | ||||||
| @@ -125,7 +131,131 @@ abstract class DiffusionGitSSHWorkflow | |||||||
|       $message = $protocol->willReadBytes($message); |       $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; |     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; | ||||||
|  |   } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| <?php | <?php | ||||||
|  |  | ||||||
| final class DiffusionGitUploadPackSSHWorkflow extends DiffusionGitSSHWorkflow { | final class DiffusionGitUploadPackSSHWorkflow | ||||||
|  |   extends DiffusionGitSSHWorkflow { | ||||||
|  |  | ||||||
|   protected function didConstruct() { |   protected function didConstruct() { | ||||||
|     $this->setName('git-upload-pack'); |     $this->setName('git-upload-pack'); | ||||||
| @@ -14,39 +15,33 @@ final class DiffusionGitUploadPackSSHWorkflow extends DiffusionGitSSHWorkflow { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected function executeRepositoryOperations() { |   protected function executeRepositoryOperations() { | ||||||
|     $repository = $this->getRepository(); |     $is_proxy = $this->shouldProxy(); | ||||||
|  |     if ($is_proxy) { | ||||||
|  |       return $this->executeRepositoryProxyOperations($for_write = false); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     $viewer = $this->getSSHUser(); |     $viewer = $this->getSSHUser(); | ||||||
|  |     $repository = $this->getRepository(); | ||||||
|     $device = AlmanacKeys::getLiveDevice(); |     $device = AlmanacKeys::getLiveDevice(); | ||||||
|  |  | ||||||
|     $skip_sync = $this->shouldSkipReadSynchronization(); |     $skip_sync = $this->shouldSkipReadSynchronization(); | ||||||
|     $is_proxy = $this->shouldProxy(); |  | ||||||
|  |  | ||||||
|     if ($is_proxy) { |     $command = csprintf('git-upload-pack -- %s', $repository->getLocalPath()); | ||||||
|       $command = $this->getProxyCommand(false); |     if (!$skip_sync) { | ||||||
|  |       $cluster_engine = id(new DiffusionRepositoryClusterEngine()) | ||||||
|  |         ->setViewer($viewer) | ||||||
|  |         ->setRepository($repository) | ||||||
|  |         ->setLog($this) | ||||||
|  |         ->synchronizeWorkingCopyBeforeRead(); | ||||||
|  |  | ||||||
|       if ($device) { |       if ($device) { | ||||||
|         $this->writeClusterEngineLogMessage( |         $this->writeClusterEngineLogMessage( | ||||||
|           pht( |           pht( | ||||||
|             "# Fetch received by \"%s\", forwarding to cluster host.\n", |             "# Cleared to fetch on cluster host \"%s\".\n", | ||||||
|             $device->getName())); |             $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); |     $command = PhabricatorDaemon::sudoCommandAsDaemonUser($command); | ||||||
|  |  | ||||||
|     $pull_event = $this->newPullEvent(); |     $pull_event = $this->newPullEvent(); | ||||||
| @@ -60,14 +55,12 @@ final class DiffusionGitUploadPackSSHWorkflow extends DiffusionGitSSHWorkflow { | |||||||
|       $log->didStartSession($command); |       $log->didStartSession($command); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (!$is_proxy) { |     if (PhabricatorEnv::getEnvConfig('phabricator.show-prototypes')) { | ||||||
|       if (PhabricatorEnv::getEnvConfig('phabricator.show-prototypes')) { |       $protocol = new DiffusionGitUploadPackWireProtocol(); | ||||||
|         $protocol = new DiffusionGitUploadPackWireProtocol(); |       if ($log) { | ||||||
|         if ($log) { |         $protocol->setProtocolLog($log); | ||||||
|           $protocol->setProtocolLog($log); |  | ||||||
|         } |  | ||||||
|         $this->setWireProtocol($protocol); |  | ||||||
|       } |       } | ||||||
|  |       $this->setWireProtocol($protocol); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     $err = $this->newPassthruCommand() |     $err = $this->newPassthruCommand() | ||||||
| @@ -89,15 +82,7 @@ final class DiffusionGitUploadPackSSHWorkflow extends DiffusionGitSSHWorkflow { | |||||||
|         ->setResultCode(0); |         ->setResultCode(0); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // TODO: Currently, when proxying, we do not write a log on the proxy. |     $pull_event->save(); | ||||||
|     // 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(); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (!$err) { |     if (!$err) { | ||||||
|       $this->waitForGitClient(); |       $this->waitForGitClient(); | ||||||
|   | |||||||
| @@ -73,13 +73,13 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow { | |||||||
|     return $this->shouldProxy; |     return $this->shouldProxy; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected function getProxyCommand($for_write) { |   final protected function getAlmanacServiceRefs($for_write) { | ||||||
|     $viewer = $this->getSSHUser(); |     $viewer = $this->getSSHUser(); | ||||||
|     $repository = $this->getRepository(); |     $repository = $this->getRepository(); | ||||||
|  |  | ||||||
|     $is_cluster_request = $this->getIsClusterRequest(); |     $is_cluster_request = $this->getIsClusterRequest(); | ||||||
|  |  | ||||||
|     $uri = $repository->getAlmanacServiceURI( |     $refs = $repository->getAlmanacServiceRefs( | ||||||
|       $viewer, |       $viewer, | ||||||
|       array( |       array( | ||||||
|         'neverProxy' => $is_cluster_request, |         'neverProxy' => $is_cluster_request, | ||||||
| @@ -89,14 +89,28 @@ abstract class DiffusionSSHWorkflow extends PhabricatorSSHWorkflow { | |||||||
|         'writable' => $for_write, |         'writable' => $for_write, | ||||||
|       )); |       )); | ||||||
|  |  | ||||||
|     if (!$uri) { |     if (!$refs) { | ||||||
|       throw new Exception( |       throw new Exception( | ||||||
|         pht( |         pht( | ||||||
|           'Failed to generate an intracluster proxy URI even though this '. |           'Failed to generate an intracluster proxy URI even though this '. | ||||||
|           'request was routed as a proxy request.')); |           '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(); |     $username = AlmanacKeys::getClusterSSHUser(); | ||||||
|     if ($username === null) { |     if ($username === null) { | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| <?php | <?php | ||||||
|  |  | ||||||
| final class PhabricatorAccumulateChartFunction | final class PhabricatorAccumulateChartFunction | ||||||
|   extends PhabricatorChartFunction { |   extends PhabricatorHigherOrderChartFunction { | ||||||
|  |  | ||||||
|   const FUNCTIONKEY = 'accumulate'; |   const FUNCTIONKEY = 'accumulate'; | ||||||
|  |  | ||||||
| @@ -13,14 +13,6 @@ final class PhabricatorAccumulateChartFunction | |||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public function getDomain() { |  | ||||||
|     return $this->getArgument('x')->getDomain(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public function newInputValues(PhabricatorChartDataQuery $query) { |  | ||||||
|     return $this->getArgument('x')->newInputValues($query); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public function evaluateFunction(array $xv) { |   public function evaluateFunction(array $xv) { | ||||||
|     // First, we're going to accumulate the underlying function. Then |     // First, we're going to accumulate the underlying function. Then | ||||||
|     // we'll map the inputs through the accumulation. |     // we'll map the inputs through the accumulation. | ||||||
|   | |||||||
| @@ -59,13 +59,6 @@ abstract class PhabricatorChartDataset | |||||||
|     return $dataset; |     return $dataset; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   final public function toDictionary() { |  | ||||||
|     return array( |  | ||||||
|       'type' => $this->getDatasetTypeKey(), |  | ||||||
|       'functions' => mpull($this->getFunctions(), 'toDictionary'), |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   final public function getChartDisplayData( |   final public function getChartDisplayData( | ||||||
|     PhabricatorChartDataQuery $data_query) { |     PhabricatorChartDataQuery $data_query) { | ||||||
|     return $this->newChartDisplayData($data_query); |     return $this->newChartDisplayData($data_query); | ||||||
| @@ -75,4 +68,35 @@ abstract class PhabricatorChartDataset | |||||||
|     PhabricatorChartDataQuery $data_query); |     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); | ||||||
|  |   } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -60,6 +60,10 @@ abstract class PhabricatorChartFunction | |||||||
|     return $this->functionLabel; |     return $this->functionLabel; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   final public function getKey() { | ||||||
|  |     return $this->getFunctionLabel()->getKey(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   final public static function newFromDictionary(array $map) { |   final public static function newFromDictionary(array $map) { | ||||||
|     PhutilTypeSpec::checkMap( |     PhutilTypeSpec::checkMap( | ||||||
|       $map, |       $map, | ||||||
| @@ -86,13 +90,6 @@ abstract class PhabricatorChartFunction | |||||||
|     return $function; |     return $function; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public function toDictionary() { |  | ||||||
|     return array( |  | ||||||
|       'function' => $this->getFunctionKey(), |  | ||||||
|       'arguments' => $this->getArgumentParser()->getRawArguments(), |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public function getSubfunctions() { |   public function getSubfunctions() { | ||||||
|     $result = array(); |     $result = array(); | ||||||
|     $result[] = $this; |     $result[] = $this; | ||||||
| @@ -180,6 +177,8 @@ abstract class PhabricatorChartFunction | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   abstract public function evaluateFunction(array $xv); |   abstract public function evaluateFunction(array $xv); | ||||||
|  |   abstract public function getDataRefs(array $xv); | ||||||
|  |   abstract public function loadRefs(array $refs); | ||||||
|  |  | ||||||
|   public function getDomain() { |   public function getDomain() { | ||||||
|     return null; |     return null; | ||||||
|   | |||||||
| @@ -3,11 +3,21 @@ | |||||||
| final class PhabricatorChartFunctionLabel | final class PhabricatorChartFunctionLabel | ||||||
|   extends Phobject { |   extends Phobject { | ||||||
|  |  | ||||||
|  |   private $key; | ||||||
|   private $name; |   private $name; | ||||||
|   private $color; |   private $color; | ||||||
|   private $icon; |   private $icon; | ||||||
|   private $fillColor; |   private $fillColor; | ||||||
|  |  | ||||||
|  |   public function setKey($key) { | ||||||
|  |     $this->key = $key; | ||||||
|  |     return $this; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public function getKey() { | ||||||
|  |     return $this->key; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   public function setName($name) { |   public function setName($name) { | ||||||
|     $this->name = $name; |     $this->name = $name; | ||||||
|     return $this; |     return $this; | ||||||
| @@ -46,6 +56,7 @@ final class PhabricatorChartFunctionLabel | |||||||
|  |  | ||||||
|   public function toWireFormat() { |   public function toWireFormat() { | ||||||
|     return array( |     return array( | ||||||
|  |       'key' => $this->getKey(), | ||||||
|       'name' => $this->getName(), |       'name' => $this->getName(), | ||||||
|       'color' => $this->getColor(), |       'color' => $this->getColor(), | ||||||
|       'icon' => $this->getIcon(), |       'icon' => $this->getIcon(), | ||||||
|   | |||||||
| @@ -5,24 +5,187 @@ final class PhabricatorChartStackedAreaDataset | |||||||
|  |  | ||||||
|   const DATASETKEY = 'stacked-area'; |   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( |   protected function newChartDisplayData( | ||||||
|     PhabricatorChartDataQuery $data_query) { |     PhabricatorChartDataQuery $data_query) { | ||||||
|  |  | ||||||
|     $functions = $this->getFunctions(); |     $functions = $this->getFunctions(); | ||||||
|  |     $functions = mpull($functions, null, 'getKey'); | ||||||
|  |  | ||||||
|     $reversed_functions = array_reverse($functions, true); |     $stacks = $this->getStacks(); | ||||||
|  |  | ||||||
|     $function_points = array(); |     if (!$stacks) { | ||||||
|     foreach ($reversed_functions as $function_idx => $function) { |       $stacks = array( | ||||||
|       $function_points[$function_idx] = array(); |         array_reverse(array_keys($functions), true), | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|       $datapoints = $function->newDatapoints($data_query); |     $series = array(); | ||||||
|       foreach ($datapoints as $point) { |     $raw_points = array(); | ||||||
|         $x = $point['x']; |  | ||||||
|         $function_points[$function_idx][$x] = $point; |     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 |     // 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 |     // 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. |     // stacking the functions on top of one another. | ||||||
|  |  | ||||||
|     $must_define = array(); |     $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; |         $must_define[$x] = $x; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     ksort($must_define); |     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(); |       $missing = array(); | ||||||
|       foreach ($must_define as $x) { |       foreach ($must_define as $x) { | ||||||
|         if (!isset($function_points[$function_idx][$x])) { |         if (!isset($points[$x])) { | ||||||
|           $missing[$x] = true; |           $missing[$x] = true; | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
| @@ -50,8 +250,6 @@ final class PhabricatorChartStackedAreaDataset | |||||||
|         continue; |         continue; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       $points = $function_points[$function_idx]; |  | ||||||
|  |  | ||||||
|       $values = array_keys($points); |       $values = array_keys($points); | ||||||
|       $cursor = -1; |       $cursor = -1; | ||||||
|       $length = count($values); |       $length = count($values); | ||||||
| @@ -84,88 +282,19 @@ final class PhabricatorChartStackedAreaDataset | |||||||
|           $y = $ymin + (($ymax - $ymin) * $distance); |           $y = $ymin + (($ymax - $ymin) * $distance); | ||||||
|         } else { |         } else { | ||||||
|           $xmin = $values[$cursor]; |           $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, |           'x' => $x, | ||||||
|           'y' => $y, |           'y' => $y, | ||||||
|         ); |         ); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       ksort($function_points[$function_idx]); |       ksort($point_lists[$idx]); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     $range_min = null; |     return $point_lists; | ||||||
|     $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)); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -70,4 +70,22 @@ final class PhabricatorComposeChartFunction | |||||||
|     return $yv; |     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(); | ||||||
|  |   } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| <?php | <?php | ||||||
|  |  | ||||||
| final class PhabricatorConstantChartFunction | final class PhabricatorConstantChartFunction | ||||||
|   extends PhabricatorChartFunction { |   extends PhabricatorPureChartFunction { | ||||||
|  |  | ||||||
|   const FUNCTIONKEY = 'constant'; |   const FUNCTIONKEY = 'constant'; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| <?php | <?php | ||||||
|  |  | ||||||
| final class PhabricatorCosChartFunction | final class PhabricatorCosChartFunction | ||||||
|   extends PhabricatorChartFunction { |   extends PhabricatorPureChartFunction { | ||||||
|  |  | ||||||
|   const FUNCTIONKEY = 'cos'; |   const FUNCTIONKEY = 'cos'; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ final class PhabricatorFactChartFunction | |||||||
|  |  | ||||||
|   private $fact; |   private $fact; | ||||||
|   private $map; |   private $map; | ||||||
|  |   private $refs; | ||||||
|  |  | ||||||
|   protected function newArguments() { |   protected function newArguments() { | ||||||
|     $key_argument = $this->newArgument() |     $key_argument = $this->newArgument() | ||||||
| @@ -51,13 +52,15 @@ final class PhabricatorFactChartFunction | |||||||
|  |  | ||||||
|     $data = queryfx_all( |     $data = queryfx_all( | ||||||
|       $conn, |       $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, |       $table_name, | ||||||
|       $where); |       $where); | ||||||
|  |  | ||||||
|     $map = array(); |     $map = array(); | ||||||
|  |     $refs = array(); | ||||||
|     if ($data) { |     if ($data) { | ||||||
|       foreach ($data as $row) { |       foreach ($data as $row) { | ||||||
|  |         $ref = (string)$row['id']; | ||||||
|         $value = (int)$row['value']; |         $value = (int)$row['value']; | ||||||
|         $epoch = (int)$row['epoch']; |         $epoch = (int)$row['epoch']; | ||||||
|  |  | ||||||
| @@ -66,10 +69,17 @@ final class PhabricatorFactChartFunction | |||||||
|         } |         } | ||||||
|  |  | ||||||
|         $map[$epoch] += $value; |         $map[$epoch] += $value; | ||||||
|  |  | ||||||
|  |         if (!isset($refs[$epoch])) { | ||||||
|  |           $refs[$epoch] = array(); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         $refs[$epoch][] = $ref; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     $this->map = $map; |     $this->map = $map; | ||||||
|  |     $this->refs = $refs; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public function getDomain() { |   public function getDomain() { | ||||||
| @@ -99,4 +109,60 @@ final class PhabricatorFactChartFunction | |||||||
|     return $yv; |     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; | ||||||
|  |   } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -32,4 +32,38 @@ abstract class PhabricatorHigherOrderChartFunction | |||||||
|     return array_keys($map); |     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; | ||||||
|  |   } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,36 +1,27 @@ | |||||||
| <?php | <?php | ||||||
|  |  | ||||||
| final class PhabricatorMaxChartFunction | final class PhabricatorMaxChartFunction | ||||||
|   extends PhabricatorChartFunction { |   extends PhabricatorPureChartFunction { | ||||||
|  |  | ||||||
|   const FUNCTIONKEY = 'max'; |   const FUNCTIONKEY = 'max'; | ||||||
|  |  | ||||||
|   protected function newArguments() { |   protected function newArguments() { | ||||||
|     return array( |     return array( | ||||||
|       $this->newArgument() |  | ||||||
|         ->setName('x') |  | ||||||
|         ->setType('function'), |  | ||||||
|       $this->newArgument() |       $this->newArgument() | ||||||
|         ->setName('max') |         ->setName('max') | ||||||
|         ->setType('number'), |         ->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) { |   public function evaluateFunction(array $xv) { | ||||||
|     $yv = $this->getArgument('x')->evaluateFunction($xv); |  | ||||||
|     $max = $this->getArgument('max'); |     $max = $this->getArgument('max'); | ||||||
|  |  | ||||||
|     foreach ($yv as $k => $y) { |     $yv = array(); | ||||||
|       if ($y > $max) { |     foreach ($xv as $x) { | ||||||
|         $yv[$k] = null; |       if ($x > $max) { | ||||||
|  |         $yv[] = null; | ||||||
|  |       } else { | ||||||
|  |         $yv[] = $x; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,36 +1,27 @@ | |||||||
| <?php | <?php | ||||||
|  |  | ||||||
| final class PhabricatorMinChartFunction | final class PhabricatorMinChartFunction | ||||||
|   extends PhabricatorChartFunction { |   extends PhabricatorPureChartFunction { | ||||||
|  |  | ||||||
|   const FUNCTIONKEY = 'min'; |   const FUNCTIONKEY = 'min'; | ||||||
|  |  | ||||||
|   protected function newArguments() { |   protected function newArguments() { | ||||||
|     return array( |     return array( | ||||||
|       $this->newArgument() |  | ||||||
|         ->setName('x') |  | ||||||
|         ->setType('function'), |  | ||||||
|       $this->newArgument() |       $this->newArgument() | ||||||
|         ->setName('min') |         ->setName('min') | ||||||
|         ->setType('number'), |         ->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) { |   public function evaluateFunction(array $xv) { | ||||||
|     $yv = $this->getArgument('x')->evaluateFunction($xv); |  | ||||||
|     $min = $this->getArgument('min'); |     $min = $this->getArgument('min'); | ||||||
|  |  | ||||||
|     foreach ($yv as $k => $y) { |     $yv = array(); | ||||||
|       if ($y < $min) { |     foreach ($xv as $x) { | ||||||
|         $yv[$k] = null; |       if ($x < $min) { | ||||||
|  |         $yv[] = null; | ||||||
|  |       } else { | ||||||
|  |         $yv[] = $x; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										14
									
								
								src/applications/fact/chart/PhabricatorPureChartFunction.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/applications/fact/chart/PhabricatorPureChartFunction.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | abstract class PhabricatorPureChartFunction | ||||||
|  |   extends PhabricatorChartFunction { | ||||||
|  |  | ||||||
|  |   public function getDataRefs(array $xv) { | ||||||
|  |     return array(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public function loadRefs(array $refs) { | ||||||
|  |     return array(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| <?php | <?php | ||||||
|  |  | ||||||
| final class PhabricatorScaleChartFunction | final class PhabricatorScaleChartFunction | ||||||
|   extends PhabricatorChartFunction { |   extends PhabricatorPureChartFunction { | ||||||
|  |  | ||||||
|   const FUNCTIONKEY = 'scale'; |   const FUNCTIONKEY = 'scale'; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| <?php | <?php | ||||||
|  |  | ||||||
| final class PhabricatorShiftChartFunction | final class PhabricatorShiftChartFunction | ||||||
|   extends PhabricatorChartFunction { |   extends PhabricatorPureChartFunction { | ||||||
|  |  | ||||||
|   const FUNCTIONKEY = 'shift'; |   const FUNCTIONKEY = 'shift'; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| <?php | <?php | ||||||
|  |  | ||||||
| final class PhabricatorSinChartFunction | final class PhabricatorSinChartFunction | ||||||
|   extends PhabricatorChartFunction { |   extends PhabricatorPureChartFunction { | ||||||
|  |  | ||||||
|   const FUNCTIONKEY = 'sin'; |   const FUNCTIONKEY = 'sin'; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -33,10 +33,12 @@ final class PhabricatorFactChartController extends PhabricatorFactController { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     $chart_view = $engine->newChartView(); |     $chart_view = $engine->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()) |     $box = id(new PHUIObjectBoxView()) | ||||||
|       ->setHeaderText(pht('Chart')) |       ->setHeaderText(pht('Chart')) | ||||||
|       ->appendChild($chart_view); |       ->appendChild($chart_view); | ||||||
| @@ -50,56 +52,19 @@ final class PhabricatorFactChartController extends PhabricatorFactController { | |||||||
|     return $this->newPage() |     return $this->newPage() | ||||||
|       ->setTitle($title) |       ->setTitle($title) | ||||||
|       ->setCrumbs($crumbs) |       ->setCrumbs($crumbs) | ||||||
|       ->appendChild($box); |       ->appendChild( | ||||||
|  |         array( | ||||||
|  |           $box, | ||||||
|  |           $tabular_view, | ||||||
|  |         )); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private function newDemoChart() { |   private function newDemoChart() { | ||||||
|     $viewer = $this->getViewer(); |     $viewer = $this->getViewer(); | ||||||
|  |  | ||||||
|     $argvs = array(); |     $chart = id(new PhabricatorDemoChartEngine()) | ||||||
|  |  | ||||||
|     $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()) |  | ||||||
|       ->setViewer($viewer) |       ->setViewer($viewer) | ||||||
|       ->setChart($chart); |       ->newStoredChart(); | ||||||
|  |  | ||||||
|     $chart = $engine->getStoredChart(); |  | ||||||
|  |  | ||||||
|     return id(new AphrontRedirectResponse())->setURI($chart->getURI()); |     return id(new AphrontRedirectResponse())->setURI($chart->getURI()); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -63,7 +63,7 @@ abstract class PhabricatorChartEngine | |||||||
|  |  | ||||||
|   abstract protected function newChart(PhabricatorFactChart $chart, array $map); |   abstract protected function newChart(PhabricatorFactChart $chart, array $map); | ||||||
|  |  | ||||||
|   final public function buildChartPanel() { |   final public function newStoredChart() { | ||||||
|     $viewer = $this->getViewer(); |     $viewer = $this->getViewer(); | ||||||
|  |  | ||||||
|     $parameters = $this->getEngineParameters(); |     $parameters = $this->getEngineParameters(); | ||||||
| @@ -76,7 +76,11 @@ abstract class PhabricatorChartEngine | |||||||
|       ->setViewer($viewer) |       ->setViewer($viewer) | ||||||
|       ->setChart($chart); |       ->setChart($chart); | ||||||
|  |  | ||||||
|     $chart = $rendering_engine->getStoredChart(); |     return $rendering_engine->getStoredChart(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   final public function buildChartPanel() { | ||||||
|  |     $chart = $this->newStoredChart(); | ||||||
|  |  | ||||||
|     $panel_type = id(new PhabricatorDashboardChartPanelType()) |     $panel_type = id(new PhabricatorDashboardChartPanelType()) | ||||||
|       ->getPanelTypeKey(); |       ->getPanelTypeKey(); | ||||||
| @@ -91,7 +95,7 @@ abstract class PhabricatorChartEngine | |||||||
|   final protected function newFunction($name /* , ... */) { |   final protected function newFunction($name /* , ... */) { | ||||||
|     $argv = func_get_args(); |     $argv = func_get_args(); | ||||||
|     return id(new PhabricatorComposeChartFunction()) |     return id(new PhabricatorComposeChartFunction()) | ||||||
|       ->setArguments(array($argv)); |       ->setArguments($argv); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -109,7 +109,146 @@ final class PhabricatorChartRenderingEngine | |||||||
|     return $chart_view; |     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() { |   public function newChartData() { | ||||||
|  |     return $this->newWireData(false); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public function newTabularData() { | ||||||
|  |     return $this->newWireData(true); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private function newWireData($is_tabular) { | ||||||
|     $chart = $this->getStoredChart(); |     $chart = $this->getStoredChart(); | ||||||
|     $chart_key = $chart->getChartKey(); |     $chart_key = $chart->getChartKey(); | ||||||
|  |  | ||||||
| @@ -151,7 +290,11 @@ final class PhabricatorChartRenderingEngine | |||||||
|     $wire_datasets = array(); |     $wire_datasets = array(); | ||||||
|     $ranges = array(); |     $ranges = array(); | ||||||
|     foreach ($datasets as $dataset) { |     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(); |       $ranges[] = $display_data->getRange(); | ||||||
|       $wire_datasets[] = $display_data->getWireData(); |       $wire_datasets[] = $display_data->getWireData(); | ||||||
|   | |||||||
							
								
								
									
										44
									
								
								src/applications/fact/engine/PhabricatorDemoChartEngine.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								src/applications/fact/engine/PhabricatorDemoChartEngine.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,44 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | final class PhabricatorDemoChartEngine | ||||||
|  |   extends PhabricatorChartEngine { | ||||||
|  |  | ||||||
|  |   const CHARTENGINEKEY = 'facts.demo'; | ||||||
|  |  | ||||||
|  |   protected function newChart(PhabricatorFactChart $chart, array $map) { | ||||||
|  |     $viewer = $this->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); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -1,6 +1,7 @@ | |||||||
| <?php | <?php | ||||||
|  |  | ||||||
| final class PhabricatorFlagAddFlagHeraldAction extends HeraldAction { | final class PhabricatorFlagAddFlagHeraldAction | ||||||
|  |   extends PhabricatorFlagHeraldAction { | ||||||
|  |  | ||||||
|   const ACTIONCONST = 'flag'; |   const ACTIONCONST = 'flag'; | ||||||
|  |  | ||||||
| @@ -11,18 +12,6 @@ final class PhabricatorFlagAddFlagHeraldAction extends HeraldAction { | |||||||
|     return pht('Mark with flag'); |     return pht('Mark with flag'); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public function getActionGroupKey() { |  | ||||||
|     return HeraldSupportActionGroup::ACTIONGROUPKEY; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public function supportsObject($object) { |  | ||||||
|     return ($object instanceof PhabricatorFlaggableInterface); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public function supportsRuleType($rule_type) { |  | ||||||
|     return ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public function applyEffect($object, HeraldEffect $effect) { |   public function applyEffect($object, HeraldEffect $effect) { | ||||||
|     $phid = $this->getAdapter()->getPHID(); |     $phid = $this->getAdapter()->getPHID(); | ||||||
|     $rule = $effect->getRule(); |     $rule = $effect->getRule(); | ||||||
|   | |||||||
							
								
								
									
										18
									
								
								src/applications/flag/herald/PhabricatorFlagHeraldAction.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/applications/flag/herald/PhabricatorFlagHeraldAction.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,18 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | abstract class PhabricatorFlagHeraldAction | ||||||
|  |   extends HeraldAction { | ||||||
|  |  | ||||||
|  |   public function getActionGroupKey() { | ||||||
|  |     return HeraldSupportActionGroup::ACTIONGROUPKEY; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public function supportsObject($object) { | ||||||
|  |     return ($object instanceof PhabricatorFlaggableInterface); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public function supportsRuleType($rule_type) { | ||||||
|  |     return ($rule_type === HeraldRuleTypeConfig::RULE_TYPE_PERSONAL); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -0,0 +1,79 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | final class PhabricatorFlagRemoveFlagHeraldAction | ||||||
|  |   extends PhabricatorFlagHeraldAction { | ||||||
|  |  | ||||||
|  |   const ACTIONCONST = 'unflag'; | ||||||
|  |  | ||||||
|  |   const DO_UNFLAG = 'do.unflag'; | ||||||
|  |   const DO_IGNORE_UNFLAG = 'do.ignore-unflag'; | ||||||
|  |  | ||||||
|  |   public function getHeraldActionName() { | ||||||
|  |     return pht('Remove flag'); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public function applyEffect($object, HeraldEffect $effect) { | ||||||
|  |     $phid = $this->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)); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -136,12 +136,12 @@ final class FundInitiative extends FundDAO | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { |     if ($capability == PhabricatorPolicyCapability::CAN_VIEW) { | ||||||
|       foreach ($viewer->getAuthorities() as $authority) { |       $can_merchant = PhortuneMerchantQuery::canViewersEditMerchants( | ||||||
|         if ($authority instanceof PhortuneMerchant) { |         array($viewer->getPHID()), | ||||||
|           if ($authority->getPHID() == $this->getMerchantPHID()) { |         array($this->getMerchantPHID())); | ||||||
|             return true; |  | ||||||
|           } |       if ($can_merchant) { | ||||||
|         } |         return true; | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -96,4 +96,8 @@ final class HarbormasterRunBuildPlansHeraldAction | |||||||
|     return $record->getTarget(); |     return $record->getTarget(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public function isActionAvailable() { | ||||||
|  |     return id(new PhabricatorHarbormasterApplication())->isInstalled(); | ||||||
|  |   } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -405,4 +405,8 @@ abstract class HeraldAction extends Phobject { | |||||||
|     return array(); |     return array(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public function isActionAvailable() { | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -373,6 +373,16 @@ abstract class HeraldAdapter extends Phobject { | |||||||
|     return $field->getFieldGroupKey(); |     return $field->getFieldGroupKey(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public function isFieldAvailable($field_key) { | ||||||
|  |     $field = $this->getFieldImplementation($field_key); | ||||||
|  |  | ||||||
|  |     if (!$field) { | ||||||
|  |       return null; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return $field->isFieldAvailable(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  | ||||||
| /* -(  Conditions  )--------------------------------------------------------- */ | /* -(  Conditions  )--------------------------------------------------------- */ | ||||||
|  |  | ||||||
| @@ -765,6 +775,16 @@ abstract class HeraldAdapter extends Phobject { | |||||||
|     return $action->getActionGroupKey(); |     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) { |   public function getActions($rule_type) { | ||||||
|     $actions = array(); |     $actions = array(); | ||||||
|     foreach ($this->getActionsForRuleType($rule_type) as $key => $action) { |     foreach ($this->getActionsForRuleType($rule_type) as $key => $action) { | ||||||
|   | |||||||
| @@ -404,8 +404,8 @@ final class HeraldRuleController extends HeraldController { | |||||||
|     HeraldAdapter $adapter) { |     HeraldAdapter $adapter) { | ||||||
|  |  | ||||||
|     $all_rules = $this->loadRulesThisRuleMayDependUpon($rule); |     $all_rules = $this->loadRulesThisRuleMayDependUpon($rule); | ||||||
|     $all_rules = mpull($all_rules, 'getName', 'getPHID'); |     $all_rules = msortv($all_rules, 'getEditorSortVector'); | ||||||
|     asort($all_rules); |     $all_rules = mpull($all_rules, 'getEditorDisplayName', 'getPHID'); | ||||||
|  |  | ||||||
|     $all_fields = $adapter->getFieldNameMap(); |     $all_fields = $adapter->getFieldNameMap(); | ||||||
|     $all_conditions = $adapter->getConditionNameMap(); |     $all_conditions = $adapter->getConditionNameMap(); | ||||||
| @@ -674,15 +674,6 @@ final class HeraldRuleController extends HeraldController { | |||||||
|         ->execute(); |         ->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. |     // A rule can not depend upon itself. | ||||||
|     unset($all_rules[$rule->getID()]); |     unset($all_rules[$rule->getID()]); | ||||||
|  |  | ||||||
| @@ -693,7 +684,10 @@ final class HeraldRuleController extends HeraldController { | |||||||
|     $group_map = array(); |     $group_map = array(); | ||||||
|     foreach ($field_map as $field_key => $field_name) { |     foreach ($field_map as $field_key => $field_name) { | ||||||
|       $group_key = $adapter->getFieldGroupKey($field_key); |       $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( |     return $this->getGroups( | ||||||
| @@ -705,7 +699,10 @@ final class HeraldRuleController extends HeraldController { | |||||||
|     $group_map = array(); |     $group_map = array(); | ||||||
|     foreach ($action_map as $action_key => $action_name) { |     foreach ($action_map as $action_key => $action_name) { | ||||||
|       $group_key = $adapter->getActionGroupKey($action_key); |       $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( |     return $this->getGroups( | ||||||
|   | |||||||
| @@ -37,6 +37,20 @@ final class HeraldRuleIndexEngineExtension | |||||||
|  |  | ||||||
|     $phids = array(); |     $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(); |     $actions = HeraldAction::getAllActions(); | ||||||
|     foreach ($rule->getActions() as $action_record) { |     foreach ($rule->getActions() as $action_record) { | ||||||
|       $action = idx($actions, $action_record->getAction()); |       $action = idx($actions, $action_record->getAction()); | ||||||
|   | |||||||
| @@ -176,6 +176,29 @@ abstract class HeraldField extends Phobject { | |||||||
|     return $value_type->renderEditorValue($value); |     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) { |   final public function setAdapter(HeraldAdapter $adapter) { | ||||||
|     $this->adapter = $adapter; |     $this->adapter = $adapter; | ||||||
|     return $this; |     return $this; | ||||||
| @@ -218,4 +241,8 @@ abstract class HeraldField extends Phobject { | |||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public function isFieldAvailable() { | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -259,6 +259,22 @@ final class HeraldRule extends HeraldDAO | |||||||
|     return '/'.$this->getMonogram(); |     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  )------------------------------------------------ */ | /* -(  Repetition Policies  )------------------------------------------------ */ | ||||||
|  |  | ||||||
|   | |||||||
| @@ -130,4 +130,9 @@ final class LegalpadRequireSignatureHeraldAction | |||||||
|       'Require document signatures: %s.', |       'Require document signatures: %s.', | ||||||
|       $this->renderHandleList($value)); |       $this->renderHandleList($value)); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public function isActionAvailable() { | ||||||
|  |     return id(new PhabricatorLegalpadApplication())->isInstalled(); | ||||||
|  |   } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -344,6 +344,8 @@ dictionary with these keys: | |||||||
|   - `children` //Optional map.// Configure options shown to the user when |   - `children` //Optional map.// Configure options shown to the user when | ||||||
|      they "Create Subtask". See below. |      they "Create Subtask". See below. | ||||||
|   - `fields` //Optional map.// Configure field behaviors. 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 | Each subtype must have a unique key, and you must define a subtype with | ||||||
| the key "%s", which is used as a default subtype. | 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: | task subtypes. For example: | ||||||
|  |  | ||||||
| ``` | ``` | ||||||
| { |   { | ||||||
|   ... |     ... | ||||||
|   "fields": { |     "fields": { | ||||||
|     "custom.some-field": { |       "custom.some-field": { | ||||||
|       "disabled": true |         "disabled": true | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|  |     ... | ||||||
|   } |   } | ||||||
|   ... |  | ||||||
| } |  | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| Each field supports these options: | Each field supports these options: | ||||||
| @@ -421,6 +423,31 @@ Each field supports these options: | |||||||
|     subtypes. |     subtypes. | ||||||
|   - `name` //Optional string.// Custom name of this field for the subtype. |   - `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 | EOTEXT | ||||||
|       , |       , | ||||||
|       $subtype_default_key)); |       $subtype_default_key)); | ||||||
|   | |||||||
| @@ -564,7 +564,8 @@ final class ManiphestTask extends ManiphestDAO | |||||||
|  |  | ||||||
|   public function newEditEngineSubtypeMap() { |   public function newEditEngineSubtypeMap() { | ||||||
|     $config = PhabricatorEnv::getEnvConfig('maniphest.subtypes'); |     $config = PhabricatorEnv::getEnvConfig('maniphest.subtypes'); | ||||||
|     return PhabricatorEditEngineSubtype::newSubtypeMap($config); |     return PhabricatorEditEngineSubtype::newSubtypeMap($config) | ||||||
|  |       ->setDatasource(new ManiphestTaskSubtypeDatasource()); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -26,7 +26,17 @@ final class PhabricatorDatasourceURIEngineExtension | |||||||
|         ->setProtocol(null) |         ->setProtocol(null) | ||||||
|         ->setPort(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; |     return null; | ||||||
|   | |||||||
| @@ -52,28 +52,22 @@ final class PhabricatorApplicationPolicyChangeTransaction | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   public function getTitle() { |   public function getTitle() { | ||||||
|     $old = $this->renderApplicationPolicy($this->getOldValue()); |  | ||||||
|     $new = $this->renderApplicationPolicy($this->getNewValue()); |  | ||||||
|  |  | ||||||
|     return pht( |     return pht( | ||||||
|       '%s changed the "%s" policy from "%s" to "%s".', |       '%s changed the %s policy from %s to %s.', | ||||||
|       $this->renderAuthor(), |       $this->renderAuthor(), | ||||||
|       $this->renderCapability(), |       $this->renderCapability(), | ||||||
|       $old, |       $this->renderOldPolicy(), | ||||||
|       $new); |       $this->renderNewPolicy()); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public function getTitleForFeed() { |   public function getTitleForFeed() { | ||||||
|     $old = $this->renderApplicationPolicy($this->getOldValue()); |  | ||||||
|     $new = $this->renderApplicationPolicy($this->getNewValue()); |  | ||||||
|  |  | ||||||
|     return pht( |     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->renderAuthor(), | ||||||
|       $this->renderCapability(), |       $this->renderCapability(), | ||||||
|       $this->renderObject(), |       $this->renderObject(), | ||||||
|       $old, |       $this->renderOldPolicy(), | ||||||
|       $new); |       $this->renderNewPolicy()); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public function validateTransactions($object, array $xactions) { |   public function validateTransactions($object, array $xactions) { | ||||||
| @@ -165,38 +159,11 @@ final class PhabricatorApplicationPolicyChangeTransaction | |||||||
|     return $errors; |     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() { |   private function renderCapability() { | ||||||
|     $application = $this->getObject(); |     $application = $this->getObject(); | ||||||
|     $capability = $this->getCapabilityName(); |     $capability = $this->getCapabilityName(); | ||||||
|     return $application->getCapabilityLabel($capability); |     $label = $application->getCapabilityLabel($capability); | ||||||
|  |     return $this->renderValue($label); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private function getCapabilityName() { |   private function getCapabilityName() { | ||||||
|   | |||||||
| @@ -190,14 +190,6 @@ final class PassphraseCredentialViewController extends PassphraseController { | |||||||
|       pht('Credential Type'), |       pht('Credential Type'), | ||||||
|       $type->getCredentialTypeName()); |       $type->getCredentialTypeName()); | ||||||
|  |  | ||||||
|     $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( |  | ||||||
|       $viewer, |  | ||||||
|       $credential); |  | ||||||
|  |  | ||||||
|     $properties->addProperty( |  | ||||||
|       pht('Editable By'), |  | ||||||
|       $descriptions[PhabricatorPolicyCapability::CAN_EDIT]); |  | ||||||
|  |  | ||||||
|     if ($type->shouldRequireUsername()) { |     if ($type->shouldRequireUsername()) { | ||||||
|       $properties->addProperty( |       $properties->addProperty( | ||||||
|         pht('Username'), |         pht('Username'), | ||||||
|   | |||||||
| @@ -143,14 +143,6 @@ final class PhameBlogManageController extends PhameBlogController { | |||||||
|         ), |         ), | ||||||
|         $feed_uri)); |         $feed_uri)); | ||||||
|  |  | ||||||
|     $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( |  | ||||||
|       $viewer, |  | ||||||
|       $blog); |  | ||||||
|  |  | ||||||
|     $properties->addProperty( |  | ||||||
|       pht('Editable By'), |  | ||||||
|       $descriptions[PhabricatorPolicyCapability::CAN_EDIT]); |  | ||||||
|  |  | ||||||
|     $engine = id(new PhabricatorMarkupEngine()) |     $engine = id(new PhabricatorMarkupEngine()) | ||||||
|       ->setViewer($viewer) |       ->setViewer($viewer) | ||||||
|       ->addObject($blog, PhameBlog::MARKUP_FIELD_DESCRIPTION) |       ->addObject($blog, PhameBlog::MARKUP_FIELD_DESCRIPTION) | ||||||
|   | |||||||
| @@ -293,37 +293,6 @@ final class PhrictionDocumentController | |||||||
|       } else { |       } else { | ||||||
|         throw new Exception(pht("Unknown document status '%s'!", $doc_status)); |         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); |     $children = $this->renderDocumentChildren($slug); | ||||||
|   | |||||||
| @@ -49,7 +49,7 @@ final class PhrictionDocumentMoveToTransaction | |||||||
|     $new = $this->getNewValue(); |     $new = $this->getNewValue(); | ||||||
|  |  | ||||||
|     return pht( |     return pht( | ||||||
|       '%s moved this document from %s', |       '%s moved this document from %s.', | ||||||
|       $this->renderAuthor(), |       $this->renderAuthor(), | ||||||
|       $this->renderHandle($new['phid'])); |       $this->renderHandle($new['phid'])); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -318,7 +318,7 @@ final class PhabricatorPolicyExplainController | |||||||
|       ->setViewer($viewer) |       ->setViewer($viewer) | ||||||
|       ->setIcon($handle->getIcon().' bluegrey') |       ->setIcon($handle->getIcon().' bluegrey') | ||||||
|       ->setHeader(pht('Object Policy')) |       ->setHeader(pht('Object Policy')) | ||||||
|       ->appendList( |       ->appendParagraph( | ||||||
|         array( |         array( | ||||||
|           array( |           array( | ||||||
|             phutil_tag('strong', array(), pht('%s:', $capability_name)), |             phutil_tag('strong', array(), pht('%s:', $capability_name)), | ||||||
| @@ -333,10 +333,17 @@ final class PhabricatorPolicyExplainController | |||||||
|       ->appendList( |       ->appendList( | ||||||
|         array( |         array( | ||||||
|           PhabricatorPolicy::getPolicyExplanation( |           PhabricatorPolicy::getPolicyExplanation( | ||||||
|           $viewer, |             $viewer, | ||||||
|           $policy->getPHID()), |             $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); |     $strength = $this->getStrengthInformation($object, $policy, $capability); | ||||||
|     if ($strength) { |     if ($strength) { | ||||||
|       $object_section->appendHint($strength); |       $object_section->appendHint($strength); | ||||||
|   | |||||||
| @@ -602,12 +602,13 @@ final class PhabricatorPolicyFilter extends Phobject { | |||||||
|     PhabricatorPolicyInterface $object, |     PhabricatorPolicyInterface $object, | ||||||
|     $policy, |     $policy, | ||||||
|     $capability) { |     $capability) { | ||||||
|  |     $viewer = $this->viewer; | ||||||
|  |  | ||||||
|     if (!$this->raisePolicyExceptions) { |     if (!$this->raisePolicyExceptions) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if ($this->viewer->isOmnipotent()) { |     if ($viewer->isOmnipotent()) { | ||||||
|       // Never raise policy exceptions for the omnipotent viewer. Although we |       // Never raise policy exceptions for the omnipotent viewer. Although we | ||||||
|       // will never normally issue a policy rejection for the omnipotent |       // will never normally issue a policy rejection for the omnipotent | ||||||
|       // viewer, we can end up here when queries blanket reject objects that |       // viewer, we can end up here when queries blanket reject objects that | ||||||
| @@ -634,9 +635,60 @@ final class PhabricatorPolicyFilter extends Phobject { | |||||||
|         $capability); |         $capability); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     $more = PhabricatorPolicy::getPolicyExplanation($this->viewer, $policy); |     // See T13411. If you receive a policy exception because you can't view | ||||||
|     $more = (array)$more; |     // an object, we also want to avoid disclosing too many details about the | ||||||
|     $more = array_filter($more); |     // 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( |     $exceptions = PhabricatorPolicy::getSpecialRules( | ||||||
|       $object, |       $object, | ||||||
| @@ -644,7 +696,10 @@ final class PhabricatorPolicyFilter extends Phobject { | |||||||
|       $capability, |       $capability, | ||||||
|       true); |       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); |     $access_denied = $this->renderAccessDenied($object); | ||||||
|  |  | ||||||
| @@ -653,7 +708,7 @@ final class PhabricatorPolicyFilter extends Phobject { | |||||||
|       $access_denied, |       $access_denied, | ||||||
|       $capability_name, |       $capability_name, | ||||||
|       $rejection, |       $rejection, | ||||||
|       implode(' ', $details)); |       $text_details); | ||||||
|  |  | ||||||
|     $exception = id(new PhabricatorPolicyException($full_message)) |     $exception = id(new PhabricatorPolicyException($full_message)) | ||||||
|       ->setTitle($access_denied) |       ->setTitle($access_denied) | ||||||
| @@ -661,7 +716,7 @@ final class PhabricatorPolicyFilter extends Phobject { | |||||||
|       ->setRejection($rejection) |       ->setRejection($rejection) | ||||||
|       ->setCapability($capability) |       ->setCapability($capability) | ||||||
|       ->setCapabilityName($capability_name) |       ->setCapabilityName($capability_name) | ||||||
|       ->setMoreInfo($details); |       ->setMoreInfo($html_details); | ||||||
|  |  | ||||||
|     throw $exception; |     throw $exception; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -60,8 +60,10 @@ final class PhabricatorPolicyManagementShowWorkflow | |||||||
|  |  | ||||||
|     $console->writeOut("__%s__\n\n", pht('CAPABILITIES')); |     $console->writeOut("__%s__\n\n", pht('CAPABILITIES')); | ||||||
|     foreach ($policies as $capability => $policy) { |     foreach ($policies as $capability => $policy) { | ||||||
|  |       $ref = $policy->newRef($viewer); | ||||||
|  |  | ||||||
|       $console->writeOut("  **%s**\n", $capability); |       $console->writeOut("  **%s**\n", $capability); | ||||||
|       $console->writeOut("    %s\n", $policy->renderDescription()); |       $console->writeOut("    %s\n", $ref->getPolicyDisplayName()); | ||||||
|       $console->writeOut("    %s\n", |       $console->writeOut("    %s\n", | ||||||
|         PhabricatorPolicy::getPolicyExplanation($viewer, $policy->getPHID())); |         PhabricatorPolicy::getPolicyExplanation($viewer, $policy->getPHID())); | ||||||
|       $console->writeOut("\n"); |       $console->writeOut("\n"); | ||||||
|   | |||||||
| @@ -43,13 +43,13 @@ final class PhabricatorPolicyQuery | |||||||
|  |  | ||||||
|   public static function renderPolicyDescriptions( |   public static function renderPolicyDescriptions( | ||||||
|     PhabricatorUser $viewer, |     PhabricatorUser $viewer, | ||||||
|     PhabricatorPolicyInterface $object, |     PhabricatorPolicyInterface $object) { | ||||||
|     $icon = false) { |  | ||||||
|  |  | ||||||
|     $policies = self::loadPolicies($viewer, $object); |     $policies = self::loadPolicies($viewer, $object); | ||||||
|  |  | ||||||
|     foreach ($policies as $capability => $policy) { |     foreach ($policies as $capability => $policy) { | ||||||
|       $policies[$capability] = $policy->renderDescription($icon); |       $policies[$capability] = $policy->newRef($viewer) | ||||||
|  |         ->newCapabilityLink($object, $capability); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return $policies; |     return $policies; | ||||||
|   | |||||||
| @@ -85,8 +85,10 @@ final class PhabricatorPolicy | |||||||
|     $phid_type = phid_get_type($policy_identifier); |     $phid_type = phid_get_type($policy_identifier); | ||||||
|     switch ($phid_type) { |     switch ($phid_type) { | ||||||
|       case PhabricatorProjectProjectPHIDType::TYPECONST: |       case PhabricatorProjectProjectPHIDType::TYPECONST: | ||||||
|         $policy->setType(PhabricatorPolicyType::TYPE_PROJECT); |         $policy | ||||||
|         $policy->setName($handle->getName()); |           ->setType(PhabricatorPolicyType::TYPE_PROJECT) | ||||||
|  |           ->setName($handle->getName()) | ||||||
|  |           ->setIcon($handle->getIcon()); | ||||||
|         break; |         break; | ||||||
|       case PhabricatorPeopleUserPHIDType::TYPECONST: |       case PhabricatorPeopleUserPHIDType::TYPECONST: | ||||||
|         $policy->setType(PhabricatorPolicyType::TYPE_USER); |         $policy->setType(PhabricatorPolicyType::TYPE_USER); | ||||||
| @@ -218,6 +220,25 @@ final class PhabricatorPolicy | |||||||
|     PhabricatorUser $viewer, |     PhabricatorUser $viewer, | ||||||
|     $policy) { |     $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); |     $rule = PhabricatorPolicyQuery::getObjectPolicyRule($policy); | ||||||
|     if ($rule) { |     if ($rule) { | ||||||
|       return $rule->getPolicyExplanation(); |       return $rule->getPolicyExplanation(); | ||||||
| @@ -243,7 +264,9 @@ final class PhabricatorPolicy | |||||||
|         $type = phid_get_type($policy); |         $type = phid_get_type($policy); | ||||||
|         if ($type == PhabricatorProjectProjectPHIDType::TYPECONST) { |         if ($type == PhabricatorProjectProjectPHIDType::TYPECONST) { | ||||||
|           return pht( |           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()); |             $handle->getFullName()); | ||||||
|         } else if ($type == PhabricatorPeopleUserPHIDType::TYPECONST) { |         } else if ($type == PhabricatorPeopleUserPHIDType::TYPECONST) { | ||||||
|           return pht( |           return pht( | ||||||
| @@ -274,45 +297,22 @@ final class PhabricatorPolicy | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public function renderDescription($icon = false) { |   public function newRef(PhabricatorUser $viewer) { | ||||||
|     $img = null; |     return id(new PhabricatorPolicyRef()) | ||||||
|     if ($icon) { |       ->setViewer($viewer) | ||||||
|       $img = id(new PHUIIconView()) |       ->setPolicy($this); | ||||||
|         ->setIcon($this->getIcon()); |   } | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if ($this->getHref()) { |   public function isProjectPolicy() { | ||||||
|       $desc = javelin_tag( |     return ($this->getType() === PhabricatorPolicyType::TYPE_PROJECT); | ||||||
|         '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(); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     switch ($this->getType()) { |   public function isCustomPolicy() { | ||||||
|       case PhabricatorPolicyType::TYPE_PROJECT: |     return ($this->getType() === PhabricatorPolicyType::TYPE_CUSTOM); | ||||||
|         return pht('%s (Project)', $desc); |   } | ||||||
|       case PhabricatorPolicyType::TYPE_CUSTOM: |  | ||||||
|         return $desc; |   public function isMaskedPolicy() { | ||||||
|       case PhabricatorPolicyType::TYPE_MASKED: |     return ($this->getType() === PhabricatorPolicyType::TYPE_MASKED); | ||||||
|         return pht( |  | ||||||
|           '%s (You do not have permission to view policy details.)', |  | ||||||
|           $desc); |  | ||||||
|       default: |  | ||||||
|         return $desc; |  | ||||||
|     } |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|   | |||||||
| @@ -93,6 +93,16 @@ final class PHUIPolicySectionView | |||||||
|     return $this->appendChild(phutil_tag('p', array(), $content)); |     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() { |   protected function getTagAttributes() { | ||||||
|     return array( |     return array( | ||||||
|       'class' => 'phui-policy-section-view', |       'class' => 'phui-policy-section-view', | ||||||
| @@ -100,7 +110,7 @@ final class PHUIPolicySectionView | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected function getTagContent() { |   protected function getTagContent() { | ||||||
|     require_celerity_resource('phui-header-view-css'); |     require_celerity_resource('phui-policy-section-view-css'); | ||||||
|  |  | ||||||
|     $icon_view = null; |     $icon_view = null; | ||||||
|     $icon = $this->getIcon(); |     $icon = $this->getIcon(); | ||||||
|   | |||||||
							
								
								
									
										99
									
								
								src/applications/policy/view/PhabricatorPolicyRef.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								src/applications/policy/view/PhabricatorPolicyRef.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,99 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | final class PhabricatorPolicyRef | ||||||
|  |   extends Phobject { | ||||||
|  |  | ||||||
|  |   private $viewer; | ||||||
|  |   private $policy; | ||||||
|  |  | ||||||
|  |   public function setViewer(PhabricatorUser $viewer) { | ||||||
|  |     $this->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; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
							
								
								
									
										84
									
								
								src/applications/policy/view/PhabricatorPolicyRulesView.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								src/applications/policy/view/PhabricatorPolicyRulesView.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | final class PhabricatorPolicyRulesView | ||||||
|  |   extends AphrontView { | ||||||
|  |  | ||||||
|  |   private $policy; | ||||||
|  |  | ||||||
|  |   public function setPolicy(PhabricatorPolicy $policy) { | ||||||
|  |     $this->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; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -0,0 +1,135 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | final class PhabricatorProjectActivityChartEngine | ||||||
|  |   extends PhabricatorChartEngine { | ||||||
|  |  | ||||||
|  |   const CHARTENGINEKEY = 'project.activity'; | ||||||
|  |  | ||||||
|  |   public function setProjects(array $projects) { | ||||||
|  |     assert_instances_of($projects, 'PhabricatorProject'); | ||||||
|  |     $project_phids = mpull($projects, 'getPHID'); | ||||||
|  |     return $this->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); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -30,97 +30,78 @@ final class PhabricatorProjectBurndownChartEngine | |||||||
|  |  | ||||||
|     $functions = array(); |     $functions = array(); | ||||||
|     if ($project_phids) { |     if ($project_phids) { | ||||||
|       foreach ($project_phids as $project_phid) { |       $open_function = $this->newFunction( | ||||||
|         $function = $this->newFunction( |         array( | ||||||
|           'min', |           'accumulate', | ||||||
|           array( |           array( | ||||||
|             'accumulate', |             'sum', | ||||||
|             array('fact', 'tasks.open-count.assign.project', $project_phid), |             $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() |       $closed_function = $this->newFunction( | ||||||
|           ->setName(pht('Tasks Moved Into Project')) |         array( | ||||||
|           ->setColor('rgba(0, 200, 200, 1)') |           'accumulate', | ||||||
|           ->setFillColor('rgba(0, 200, 200, 0.15)'); |           $this->newFactSum('tasks.open-count.status.project', $project_phids), | ||||||
|  |         )); | ||||||
|         $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; |  | ||||||
|       } |  | ||||||
|     } else { |     } else { | ||||||
|       $function = $this->newFunction( |       $open_function = $this->newFunction( | ||||||
|         'accumulate', |         array( | ||||||
|         array('fact', 'tasks.open-count.create')); |           'accumulate', | ||||||
|  |           array('fact', 'tasks.open-count.create'), | ||||||
|  |         )); | ||||||
|  |  | ||||||
|       $function->getFunctionLabel() |       $closed_function = $this->newFunction( | ||||||
|         ->setName(pht('Tasks Created')) |         array( | ||||||
|         ->setColor('rgba(0, 200, 200, 1)') |           'accumulate', | ||||||
|         ->setFillColor('rgba(0, 200, 200, 0.15)'); |           array('fact', 'tasks.open-count.status'), | ||||||
|  |         )); | ||||||
|       $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; |  | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     $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 = array(); | ||||||
|  |  | ||||||
|     $datasets[] = id(new PhabricatorChartStackedAreaDataset()) |     $dataset = id(new PhabricatorChartStackedAreaDataset()) | ||||||
|       ->setFunctions($functions); |       ->setFunctions( | ||||||
|  |         array( | ||||||
|  |           $open_function, | ||||||
|  |           $closed_function, | ||||||
|  |         )) | ||||||
|  |       ->setStacks( | ||||||
|  |         array( | ||||||
|  |           array('open'), | ||||||
|  |           array('closed'), | ||||||
|  |         )); | ||||||
|  |  | ||||||
|  |     $datasets[] = $dataset; | ||||||
|     $chart->attachDatasets($datasets); |     $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; | ||||||
|  |   } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -44,10 +44,24 @@ final class PhabricatorProjectReportsController | |||||||
|       ->setParentPanelPHIDs(array()) |       ->setParentPanelPHIDs(array()) | ||||||
|       ->renderPanel(); |       ->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()) |     $view = id(new PHUITwoColumnView()) | ||||||
|       ->setFooter( |       ->setFooter( | ||||||
|         array( |         array( | ||||||
|           $chart_view, |           $chart_view, | ||||||
|  |           $activity_view, | ||||||
|         )); |         )); | ||||||
|  |  | ||||||
|     return $this->newPage() |     return $this->newPage() | ||||||
|   | |||||||
| @@ -904,7 +904,8 @@ final class PhabricatorProject extends PhabricatorProjectDAO | |||||||
|  |  | ||||||
|   public function newEditEngineSubtypeMap() { |   public function newEditEngineSubtypeMap() { | ||||||
|     $config = PhabricatorEnv::getEnvConfig('projects.subtypes'); |     $config = PhabricatorEnv::getEnvConfig('projects.subtypes'); | ||||||
|     return PhabricatorEditEngineSubtype::newSubtypeMap($config); |     return PhabricatorEditEngineSubtype::newSubtypeMap($config) | ||||||
|  |       ->setDatasource(new PhabricatorProjectSubtypeDatasource()); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public function newSubtypeObject() { |   public function newSubtypeObject() { | ||||||
|   | |||||||
| @@ -1842,6 +1842,20 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO | |||||||
|     PhabricatorUser $viewer, |     PhabricatorUser $viewer, | ||||||
|     array $options) { |     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( |     PhutilTypeSpec::checkMap( | ||||||
|       $options, |       $options, | ||||||
|       array( |       array( | ||||||
| @@ -1856,7 +1870,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO | |||||||
|  |  | ||||||
|     $cache_key = $this->getAlmanacServiceCacheKey(); |     $cache_key = $this->getAlmanacServiceCacheKey(); | ||||||
|     if (!$cache_key) { |     if (!$cache_key) { | ||||||
|       return null; |       return array(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     $cache = PhabricatorCaches::getMutableStructureCache(); |     $cache = PhabricatorCaches::getMutableStructureCache(); | ||||||
| @@ -1869,7 +1883,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     if ($uris === null) { |     if ($uris === null) { | ||||||
|       return null; |       return array(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     $local_device = AlmanacKeys::getDeviceID(); |     $local_device = AlmanacKeys::getDeviceID(); | ||||||
| @@ -1893,7 +1907,7 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO | |||||||
|  |  | ||||||
|       if ($local_device && $never_proxy) { |       if ($local_device && $never_proxy) { | ||||||
|         if ($uri['device'] == $local_device) { |         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 we require a writable device, remove URIs which aren't writable. | ||||||
|     if ($writable) { |     if ($writable) { | ||||||
|       foreach ($results as $key => $uri) { |       foreach ($refs as $key => $ref) { | ||||||
|         if (!$uri['writable']) { |         if (!$ref->isWritable()) { | ||||||
|           unset($results[$key]); |           unset($results[$key]); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       if (!$results) { |       if (!$refs) { | ||||||
|         throw new Exception( |         throw new Exception( | ||||||
|           pht( |           pht( | ||||||
|             'This repository ("%s") is not writable with the given '. |             'This repository ("%s") is not writable with the given '. | ||||||
| @@ -1974,23 +1993,30 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     if ($writable) { |     if ($writable) { | ||||||
|       $results = $this->sortWritableAlmanacServiceURIs($results); |       $refs = $this->sortWritableAlmanacServiceRefs($refs); | ||||||
|     } else { |     } else { | ||||||
|       shuffle($results); |       $refs = $this->sortReadableAlmanacServiceRefs($refs); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     $result = head($results); |     return array_values($refs); | ||||||
|     return $result['uri']; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   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. |     // See T13109 for discussion of how this method routes requests. | ||||||
|  |  | ||||||
|     // In the absence of other rules, we'll send traffic to devices randomly. |     // 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 |     // We also want to select randomly among nodes which are equally good | ||||||
|     // candidates to receive the write, and accomplish that by shuffling the |     // candidates to receive the write, and accomplish that by shuffling the | ||||||
|     // list up front. |     // list up front. | ||||||
|     shuffle($results); |     shuffle($refs); | ||||||
|  |  | ||||||
|     $order = array(); |     $order = array(); | ||||||
|  |  | ||||||
| @@ -2002,8 +2028,8 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO | |||||||
|       $this->getPHID()); |       $this->getPHID()); | ||||||
|     if ($writer) { |     if ($writer) { | ||||||
|       $device_phid = $writer->getWriteProperty('devicePHID'); |       $device_phid = $writer->getWriteProperty('devicePHID'); | ||||||
|       foreach ($results as $key => $result) { |       foreach ($refs as $key => $ref) { | ||||||
|         if ($result['devicePHID'] === $device_phid) { |         if ($ref->getDevicePHID() === $device_phid) { | ||||||
|           $order[] = $key; |           $order[] = $key; | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
| @@ -2025,8 +2051,8 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO | |||||||
|       } |       } | ||||||
|       $max_devices = array_fuse($max_devices); |       $max_devices = array_fuse($max_devices); | ||||||
|  |  | ||||||
|       foreach ($results as $key => $result) { |       foreach ($refs as $key => $ref) { | ||||||
|         if (isset($max_devices[$result['devicePHID']])) { |         if (isset($max_devices[$ref->getDevicePHID()])) { | ||||||
|           $order[] = $key; |           $order[] = $key; | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
| @@ -2034,9 +2060,9 @@ final class PhabricatorRepository extends PhabricatorRepositoryDAO | |||||||
|  |  | ||||||
|     // Reorder the results, putting any we've selected as preferred targets for |     // Reorder the results, putting any we've selected as preferred targets for | ||||||
|     // the write at the head of the list. |     // 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() { |   public function supportsSynchronization() { | ||||||
|   | |||||||
| @@ -0,0 +1,28 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | final class PhabricatorSearchSettingsPanel | ||||||
|  |   extends PhabricatorEditEngineSettingsPanel { | ||||||
|  |  | ||||||
|  |   const PANELKEY = 'search'; | ||||||
|  |  | ||||||
|  |   public function getPanelName() { | ||||||
|  |     return pht('Search'); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public function getPanelMenuIcon() { | ||||||
|  |     return 'fa-search'; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public function getPanelGroupKey() { | ||||||
|  |     return PhabricatorSettingsApplicationsPanelGroup::PANELGROUPKEY; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public function isTemplatePanel() { | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public function isUserPanel() { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| <?php | <?php | ||||||
|  |  | ||||||
| final class PhabricatorSearchScopeSetting | final class PhabricatorSearchScopeSetting | ||||||
|   extends PhabricatorInternalSetting { |   extends PhabricatorSelectSetting { | ||||||
|  |  | ||||||
|   const SETTINGKEY = 'search-scope'; |   const SETTINGKEY = 'search-scope'; | ||||||
|  |  | ||||||
| @@ -9,8 +9,34 @@ final class PhabricatorSearchScopeSetting | |||||||
|     return pht('Search Scope'); |     return pht('Search Scope'); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public function getSettingPanelKey() { | ||||||
|  |     return PhabricatorSearchSettingsPanel::PANELKEY; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   public function getSettingDefaultValue() { |   public function getSettingDefaultValue() { | ||||||
|     return 'all'; |     return 'all'; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   protected function getControlInstructions() { | ||||||
|  |     return pht( | ||||||
|  |       'Choose the default behavior of the global search in the main menu.'); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   protected function getSelectOptions() { | ||||||
|  |     $scopes = PhabricatorMainMenuSearchView::getGlobalSearchScopeItems( | ||||||
|  |       $this->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; | ||||||
|  |   } | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -80,14 +80,6 @@ final class PhabricatorSpacesViewController | |||||||
|         ? pht('Yes') |         ? pht('Yes') | ||||||
|         : pht('No')); |         : pht('No')); | ||||||
|  |  | ||||||
|     $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( |  | ||||||
|       $viewer, |  | ||||||
|       $space); |  | ||||||
|  |  | ||||||
|     $list->addProperty( |  | ||||||
|       pht('Editable By'), |  | ||||||
|       $descriptions[PhabricatorPolicyCapability::CAN_EDIT]); |  | ||||||
|  |  | ||||||
|     $description = $space->getDescription(); |     $description = $space->getDescription(); | ||||||
|     if (strlen($description)) { |     if (strlen($description)) { | ||||||
|       $description = new PHUIRemarkupView($viewer, $description); |       $description = new PHUIRemarkupView($viewer, $description); | ||||||
|   | |||||||
| @@ -100,32 +100,34 @@ final class PhabricatorSystemActionEngine extends Phobject { | |||||||
|  |  | ||||||
|     $actor_hashes = array(); |     $actor_hashes = array(); | ||||||
|     foreach ($actors as $actor) { |     foreach ($actors as $actor) { | ||||||
|       $actor_hashes[] = PhabricatorHash::digestForIndex($actor); |       $digest = PhabricatorHash::digestForIndex($actor); | ||||||
|  |       $actor_hashes[$digest] = $actor; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     $log = new PhabricatorSystemActionLog(); |     $log = new PhabricatorSystemActionLog(); | ||||||
|  |  | ||||||
|     $window = self::getWindow(); |     $window = self::getWindow(); | ||||||
|  |  | ||||||
|     $conn_r = $log->establishConnection('r'); |     $conn = $log->establishConnection('r'); | ||||||
|     $scores = queryfx_all( |  | ||||||
|       $conn_r, |     $rows = queryfx_all( | ||||||
|       'SELECT actorIdentity, SUM(score) totalScore FROM %T |       $conn, | ||||||
|  |       'SELECT actorHash, SUM(score) totalScore FROM %T | ||||||
|         WHERE action = %s AND actorHash IN (%Ls) |         WHERE action = %s AND actorHash IN (%Ls) | ||||||
|           AND epoch >= %d GROUP BY actorHash', |           AND epoch >= %d GROUP BY actorHash', | ||||||
|       $log->getTableName(), |       $log->getTableName(), | ||||||
|       $action->getActionConstant(), |       $action->getActionConstant(), | ||||||
|       $actor_hashes, |       array_keys($actor_hashes), | ||||||
|       (time() - $window)); |       (PhabricatorTime::getNow() - $window)); | ||||||
|  |  | ||||||
|     $scores = ipull($scores, 'totalScore', 'actorIdentity'); |     $rows = ipull($rows, 'totalScore', 'actorHash'); | ||||||
|  |  | ||||||
|     foreach ($scores as $key => $score) { |     $scores = array(); | ||||||
|       $scores[$key] = $score / $window; |     foreach ($actor_hashes as $digest => $actor) { | ||||||
|  |       $score = idx($rows, $digest, 0); | ||||||
|  |       $scores[$actor] = ($score / $window); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     $scores = $scores + array_fill_keys($actors, 0); |  | ||||||
|  |  | ||||||
|     return $scores; |     return $scores; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -33,6 +33,7 @@ final class PhabricatorApplicationTransactionValueController | |||||||
|       case PhabricatorTransactions::TYPE_EDIT_POLICY: |       case PhabricatorTransactions::TYPE_EDIT_POLICY: | ||||||
|       case PhabricatorTransactions::TYPE_JOIN_POLICY: |       case PhabricatorTransactions::TYPE_JOIN_POLICY: | ||||||
|       case PhabricatorRepositoryPushPolicyTransaction::TRANSACTIONTYPE: |       case PhabricatorRepositoryPushPolicyTransaction::TRANSACTIONTYPE: | ||||||
|  |       case PhabricatorApplicationPolicyChangeTransaction::TRANSACTIONTYPE: | ||||||
|         break; |         break; | ||||||
|       default: |       default: | ||||||
|         return new Aphront404Response(); |         return new Aphront404Response(); | ||||||
| @@ -57,89 +58,16 @@ final class PhabricatorApplicationTransactionValueController | |||||||
|       return new Aphront404Response(); |       return new Aphront404Response(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     $rule_objects = array(); |     $rules_view = id(new PhabricatorPolicyRulesView()) | ||||||
|     foreach ($policy->getCustomRuleClasses() as $class) { |       ->setViewer($viewer) | ||||||
|       $rule_objects[$class] = newv($class, array()); |       ->setPolicy($policy); | ||||||
|     } |  | ||||||
|     $policy->attachRuleObjects($rule_objects); |  | ||||||
|  |  | ||||||
|     $this->requireResource('policy-transaction-detail-css'); |  | ||||||
|     $cancel_uri = $this->guessCancelURI($viewer, $xaction); |     $cancel_uri = $this->guessCancelURI($viewer, $xaction); | ||||||
|  |  | ||||||
|     return $this->newDialog() |     return $this->newDialog() | ||||||
|       ->setTitle($policy->getFullName()) |       ->setTitle($policy->getFullName()) | ||||||
|       ->setWidth(AphrontDialogView::WIDTH_FORM) |       ->setWidth(AphrontDialogView::WIDTH_FORM) | ||||||
|       ->appendChild($this->renderPolicyDetails($policy, $rule_objects)) |       ->appendChild($rules_view) | ||||||
|       ->addCancelButton($cancel_uri, pht('Close')); |       ->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; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -14,6 +14,7 @@ final class PhabricatorEditEngineSubtype | |||||||
|   private $childSubtypes = array(); |   private $childSubtypes = array(); | ||||||
|   private $childIdentifiers = array(); |   private $childIdentifiers = array(); | ||||||
|   private $fieldConfiguration = array(); |   private $fieldConfiguration = array(); | ||||||
|  |   private $mutations; | ||||||
|  |  | ||||||
|   public function setKey($key) { |   public function setKey($key) { | ||||||
|     $this->key = $key; |     $this->key = $key; | ||||||
| @@ -78,6 +79,15 @@ final class PhabricatorEditEngineSubtype | |||||||
|     return $this->childIdentifiers; |     return $this->childIdentifiers; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public function setMutations($mutations) { | ||||||
|  |     $this->mutations = $mutations; | ||||||
|  |     return $this; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public function getMutations() { | ||||||
|  |     return $this->mutations; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   public function hasTagView() { |   public function hasTagView() { | ||||||
|     return (bool)strlen($this->getTagText()); |     return (bool)strlen($this->getTagText()); | ||||||
|   } |   } | ||||||
| @@ -152,6 +162,7 @@ final class PhabricatorEditEngineSubtype | |||||||
|           'icon' => 'optional string', |           'icon' => 'optional string', | ||||||
|           'children' => 'optional map<string, wild>', |           'children' => 'optional map<string, wild>', | ||||||
|           'fields' => 'optional map<string, wild>', |           'fields' => 'optional map<string, wild>', | ||||||
|  |           'mutations' => 'optional list<string>', | ||||||
|         )); |         )); | ||||||
|  |  | ||||||
|       $key = $value['key']; |       $key = $value['key']; | ||||||
| @@ -217,6 +228,28 @@ final class PhabricatorEditEngineSubtype | |||||||
|           'with key "%s". This subtype is required and must be defined.', |           'with key "%s". This subtype is required and must be defined.', | ||||||
|           self::SUBTYPE_DEFAULT)); |           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) { |   public static function newSubtypeMap(array $config) { | ||||||
| @@ -267,6 +300,8 @@ final class PhabricatorEditEngineSubtype | |||||||
|         } |         } | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  |       $subtype->setMutations(idx($entry, 'mutations')); | ||||||
|  |  | ||||||
|       $map[$key] = $subtype; |       $map[$key] = $subtype; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ final class PhabricatorEditEngineSubtypeMap | |||||||
|   extends Phobject { |   extends Phobject { | ||||||
|  |  | ||||||
|   private $subtypes; |   private $subtypes; | ||||||
|  |   private $datasource; | ||||||
|  |  | ||||||
|   public function __construct(array $subtypes) { |   public function __construct(array $subtypes) { | ||||||
|     assert_instances_of($subtypes, 'PhabricatorEditEngineSubtype'); |     assert_instances_of($subtypes, 'PhabricatorEditEngineSubtype'); | ||||||
| @@ -39,6 +40,57 @@ final class PhabricatorEditEngineSubtypeMap | |||||||
|     return $this->subtypes[$subtype_key]; |     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( |   public function getCreateFormsForSubtype( | ||||||
|     PhabricatorEditEngine $edit_engine, |     PhabricatorEditEngine $edit_engine, | ||||||
|     PhabricatorEditEngineSubtypeInterface $object) { |     PhabricatorEditEngineSubtypeInterface $object) { | ||||||
|   | |||||||
| @@ -29,9 +29,17 @@ final class PhabricatorSubtypeEditEngineExtension | |||||||
|     PhabricatorApplicationTransactionInterface $object) { |     PhabricatorApplicationTransactionInterface $object) { | ||||||
|  |  | ||||||
|     $subtype_type = PhabricatorTransactions::TYPE_SUBTYPE; |     $subtype_type = PhabricatorTransactions::TYPE_SUBTYPE; | ||||||
|  |     $subtype_value = $object->getEditEngineSubtype(); | ||||||
|  |  | ||||||
|     $map = $object->newEditEngineSubtypeMap(); |     $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()) |     $subtype_field = id(new PhabricatorSelectEditField()) | ||||||
|       ->setKey(self::EDITKEY) |       ->setKey(self::EDITKEY) | ||||||
| @@ -40,12 +48,12 @@ final class PhabricatorSubtypeEditEngineExtension | |||||||
|       ->setTransactionType($subtype_type) |       ->setTransactionType($subtype_type) | ||||||
|       ->setConduitDescription(pht('Change the object subtype.')) |       ->setConduitDescription(pht('Change the object subtype.')) | ||||||
|       ->setConduitTypeDescription(pht('New object subtype key.')) |       ->setConduitTypeDescription(pht('New object subtype key.')) | ||||||
|       ->setValue($object->getEditEngineSubtype()) |       ->setValue($subtype_value) | ||||||
|       ->setOptions($options); |       ->setOptions($options); | ||||||
|  |  | ||||||
|     // If subtypes are configured, enable changing them from the bulk editor |     // If subtypes are configured, enable changing them from the bulk editor. | ||||||
|     // and comment action stack. |     // Bulk editor | ||||||
|     if ($map->getCount() > 1) { |     if ($options) { | ||||||
|       $subtype_field |       $subtype_field | ||||||
|         ->setBulkEditLabel(pht('Change subtype to')) |         ->setBulkEditLabel(pht('Change subtype to')) | ||||||
|         ->setCommentActionLabel(pht('Change Subtype')) |         ->setCommentActionLabel(pht('Change Subtype')) | ||||||
|   | |||||||
| @@ -0,0 +1,52 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | final class PhabricatorEditEngineSubtypeHeraldField | ||||||
|  |   extends HeraldField { | ||||||
|  |  | ||||||
|  |   const FIELDCONST = 'subtype'; | ||||||
|  |  | ||||||
|  |   public function getHeraldFieldName() { | ||||||
|  |     return pht('Subtype'); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public function getFieldGroupKey() { | ||||||
|  |     return HeraldSupportFieldGroup::FIELDGROUPKEY; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public function supportsObject($object) { | ||||||
|  |     return ($object instanceof PhabricatorEditEngineSubtypeInterface); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public function getHeraldFieldValue($object) { | ||||||
|  |     return $object->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); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -445,19 +445,15 @@ abstract class PhabricatorApplicationTransaction | |||||||
|     $policy = PhabricatorPolicy::newFromPolicyAndHandle( |     $policy = PhabricatorPolicy::newFromPolicyAndHandle( | ||||||
|       $phid, |       $phid, | ||||||
|       $this->getHandleIfExists($phid)); |       $this->getHandleIfExists($phid)); | ||||||
|  |  | ||||||
|  |     $ref = $policy->newRef($this->getViewer()); | ||||||
|  |  | ||||||
|     if ($this->renderingTarget == self::TARGET_HTML) { |     if ($this->renderingTarget == self::TARGET_HTML) { | ||||||
|       switch ($policy->getType()) { |       $output = $ref->newTransactionLink($state, $this); | ||||||
|         case PhabricatorPolicyType::TYPE_CUSTOM: |  | ||||||
|           $policy->setHref('/transactions/'.$state.'/'.$this->getPHID().'/'); |  | ||||||
|           $policy->setWorkflow(true); |  | ||||||
|           break; |  | ||||||
|         default: |  | ||||||
|           break; |  | ||||||
|       } |  | ||||||
|       $output = $policy->renderDescription(); |  | ||||||
|     } else { |     } else { | ||||||
|       $output = hsprintf('%s', $policy->getFullName()); |       $output = $ref->getPolicyDisplayName(); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return $output; |     return $output; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -775,6 +771,13 @@ abstract class PhabricatorApplicationTransaction | |||||||
|       case PhabricatorTransactions::TYPE_TOKEN: |       case PhabricatorTransactions::TYPE_TOKEN: | ||||||
|       case PhabricatorTransactions::TYPE_MFA: |       case PhabricatorTransactions::TYPE_MFA: | ||||||
|         return true; |         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: |       case PhabricatorTransactions::TYPE_EDGE: | ||||||
|         $edge_type = $this->getMetadataValue('edge:type'); |         $edge_type = $this->getMetadataValue('edge:type'); | ||||||
|         switch ($edge_type) { |         switch ($edge_type) { | ||||||
| @@ -1387,12 +1390,6 @@ abstract class PhabricatorApplicationTransaction | |||||||
|           return 25; |           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 |         // In other cases, subscriptions are more interesting than comments | ||||||
|         // (which are shown anyway) but less interesting than any other type of |         // (which are shown anyway) but less interesting than any other type of | ||||||
|         // transaction. |         // transaction. | ||||||
|   | |||||||
| @@ -215,17 +215,16 @@ abstract class PhabricatorModularTransactionType | |||||||
|       $phid, |       $phid, | ||||||
|       $handles[$phid]); |       $handles[$phid]); | ||||||
|  |  | ||||||
|  |     $ref = $policy->newRef($viewer); | ||||||
|  |  | ||||||
|     if ($this->isTextMode()) { |     if ($this->isTextMode()) { | ||||||
|       return $this->renderValue($policy->getFullName()); |       $name = $ref->getPolicyDisplayName(); | ||||||
|  |     } else { | ||||||
|  |       $storage = $this->getStorage(); | ||||||
|  |       $name = $ref->newTransactionLink($mode, $storage); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     $storage = $this->getStorage(); |     return $this->renderValue($name); | ||||||
|     if ($policy->getType() == PhabricatorPolicyType::TYPE_CUSTOM) { |  | ||||||
|       $policy->setHref('/transactions/'.$mode.'/'.$storage->getPHID().'/'); |  | ||||||
|       $policy->setWorkflow(true); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return $this->renderValue($policy->renderDescription()); |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   final protected function renderHandleList(array $phids) { |   final protected function renderHandleList(array $phids) { | ||||||
|   | |||||||
| @@ -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 | environment and some parts of Phabricator's configuration in order to fix these | ||||||
| problems and set up the environment which Phabricator expects. | 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: | 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 | request, allowing you to adjust the environment. For common adjustments and | ||||||
| examples, see the next sections. | examples, see the next sections. | ||||||
|  |  | ||||||
|  |  | ||||||
| Adjusting Client IPs | 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 | all requests as originating from the load balancer, rather than from the | ||||||
| correct client IPs. | correct client IPs. | ||||||
|  |  | ||||||
| If this is the case and some other header (like `X-Forwarded-For`) is known to | In common cases where networks are configured like this, the `X-Forwarded-For` | ||||||
| be trustworthy, you can read the header and overwrite the `REMOTE_ADDR` value | header will have trustworthy information about the real client IP. You | ||||||
| so Phabricator can figure out the client IP correctly. | 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 | 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 | 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. | spoof an arbitrary client IP. | ||||||
|  |  | ||||||
| The `X-Forwarded-For` header may also contain a list of addresses if a request | 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 | has been forwarded through multiple load balancers. If you know that requests | ||||||
| will usually handle most situations correctly: | 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 |  | ||||||
| <?php |  | ||||||
|  |  | ||||||
| // Overwrite REMOTE_ADDR with the value in the "X-Forwarded-For" HTTP header. | If you have an unusual network configuration (for example, the number of | ||||||
|  | trustworthy devices depends on the network path) you can also implement your | ||||||
|  | own logic. | ||||||
|  |  | ||||||
| // Only do this if you're certain the request is coming from a loadbalancer! | Note that this is very odd, advanced, and easy to get wrong. If you get it | ||||||
| // If the request came directly from a client, doing this will allow them to | wrong, users will most likely be able to spoof any client address. | ||||||
| // them spoof any remote address. |  | ||||||
|  |  | ||||||
| // The header may contain a list of IPs, like "1.2.3.4, 4.5.6.7", if the | ```name="Custom X-Forwarded-For Handling", lang=php | ||||||
| // request the load balancer received also had this header. |  | ||||||
|  |  | ||||||
| if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { | if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { | ||||||
|   $forwarded_for = $_SERVER['HTTP_X_FORWARDED_FOR']; |   $raw_header = $_SERVER['X_FORWARDED_FOR']; | ||||||
|   if ($forwarded_for) { |  | ||||||
|     $forwarded_for = explode(',', $forwarded_for); |   $real_address = your_custom_parsing_function($raw_header); | ||||||
|     $forwarded_for = end($forwarded_for); |  | ||||||
|     $forwarded_for = trim($forwarded_for); |   $_SERVER['REMOTE_ADDR'] = $real_address; | ||||||
|     $_SERVER['REMOTE_ADDR'] = $forwarded_for; |  | ||||||
|   } |  | ||||||
| } | } | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
|   | |||||||
| @@ -63,16 +63,23 @@ For detailed help on managing and stripping MFA, see the instructions in | |||||||
| Unlocking Objects | Unlocking Objects | ||||||
| ================= | ================= | ||||||
|  |  | ||||||
| If you aren't sure who owns an object, or no user account has access to an | If you aren't sure who owns an object, you can inspect the policies from the | ||||||
| object, you can directly change object policies from the CLI: | CLI: | ||||||
|  |  | ||||||
|  | ``` | ||||||
|  | $ ./bin/policy show <object> | ||||||
|  | ``` | ||||||
|  |  | ||||||
|  | To identify the object you want to examine, you can specify an object | ||||||
|  | name (like `T123`) or a PHID as the `<object>` 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 <object> [--view ...] [--edit ...] [--owner ...] | $ ./bin/policy unlock <object> [--view ...] [--edit ...] [--owner ...] | ||||||
| ``` | ``` | ||||||
|  |  | ||||||
| To identify the object you want to unlock, you can specify an object name (like |  | ||||||
| `T123`) or a PHID as the `<object>` parameter. |  | ||||||
|  |  | ||||||
| Use the `--view` and `--edit` flags (and, for some objects, the `--owner` | Use the `--view` and `--edit` flags (and, for some objects, the `--owner` | ||||||
| flag) to specify new policies for the object. | flag) to specify new policies for the object. | ||||||
|  |  | ||||||
|   | |||||||
| @@ -221,6 +221,9 @@ final class PhabricatorDatabaseRef | |||||||
|     return $this->replicaRefs; |     return $this->replicaRefs; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public function getDisplayName() { | ||||||
|  |     return $this->getRefKey(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   public function getRefKey() { |   public function getRefKey() { | ||||||
|     $host = $this->getHost(); |     $host = $this->getHost(); | ||||||
|   | |||||||
							
								
								
									
										19
									
								
								src/infrastructure/env/PhabricatorEnv.php
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										19
									
								
								src/infrastructure/env/PhabricatorEnv.php
									
									
									
									
										vendored
									
									
								
							| @@ -135,6 +135,11 @@ final class PhabricatorEnv extends Phobject { | |||||||
|     // TODO: Add a "locale.default" config option once we have some reasonable |     // TODO: Add a "locale.default" config option once we have some reasonable | ||||||
|     // defaults which aren't silly nonsense. |     // defaults which aren't silly nonsense. | ||||||
|     self::setLocaleCode('en_US'); |     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) { |   public static function beginScopedLocale($locale_code) { | ||||||
| @@ -249,9 +254,17 @@ final class PhabricatorEnv extends Phobject { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
|       $stack->pushSource( |       // See T13403. If we're starting up in "config optional" mode, suppress | ||||||
|         id(new PhabricatorConfigDatabaseSource('default')) |       // messages about connection retries. | ||||||
|           ->setName(pht('Database'))); |       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) { |     } catch (AphrontSchemaQueryException $exception) { | ||||||
|       // If the database is not available, just skip this configuration |       // If the database is not available, just skip this configuration | ||||||
|       // source. This happens during `bin/storage upgrade`, `bin/conf` before |       // source. This happens during `bin/storage upgrade`, `bin/conf` before | ||||||
|   | |||||||
| @@ -10,6 +10,9 @@ abstract class AphrontBaseMySQLDatabaseConnection | |||||||
|  |  | ||||||
|   private $nextError; |   private $nextError; | ||||||
|  |  | ||||||
|  |   const CALLERROR_QUERY = 777777; | ||||||
|  |   const CALLERROR_CONNECT = 777778; | ||||||
|  |  | ||||||
|   abstract protected function connect(); |   abstract protected function connect(); | ||||||
|   abstract protected function rawQuery($raw_query); |   abstract protected function rawQuery($raw_query); | ||||||
|   abstract protected function rawQueries(array $raw_queries); |   abstract protected function rawQueries(array $raw_queries); | ||||||
| @@ -123,7 +126,14 @@ abstract class AphrontBaseMySQLDatabaseConnection | |||||||
|             $code, |             $code, | ||||||
|             $ex->getMessage()); |             $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 { |         } else { | ||||||
|           $profiler->endServiceCall($call_id, array()); |           $profiler->endServiceCall($call_id, array()); | ||||||
|           throw $ex; |           throw $ex; | ||||||
|   | |||||||
| @@ -68,19 +68,47 @@ final class AphrontMySQLiDatabaseConnection | |||||||
|       $host = 'p:'.$host; |       $host = 'p:'.$host; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     @$conn->real_connect( |     $trap = new PhutilErrorTrap(); | ||||||
|  |  | ||||||
|  |     $ok = @$conn->real_connect( | ||||||
|       $host, |       $host, | ||||||
|       $user, |       $user, | ||||||
|       $pass, |       $pass, | ||||||
|       $database, |       $database, | ||||||
|       $port); |       $port); | ||||||
|  |  | ||||||
|  |     $call_error = $trap->getErrorsAsString(); | ||||||
|  |     $trap->destroy(); | ||||||
|  |  | ||||||
|     $errno = $conn->connect_errno; |     $errno = $conn->connect_errno; | ||||||
|     if ($errno) { |     if ($errno) { | ||||||
|       $error = $conn->connect_error; |       $error = $conn->connect_error; | ||||||
|       $this->throwConnectionException($errno, $error, $user, $host); |       $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 |     // 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, |     // 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. |     // 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 '. |             'Call to "mysqli->query()" failed, but did not set an error '. | ||||||
|             'code or emit an error message.'); |             'code or emit an error message.'); | ||||||
|         } |         } | ||||||
|         $this->throwQueryCodeException(777777, $message); |         $this->throwQueryCodeException(self::CALLERROR_QUERY, $message); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -89,6 +89,10 @@ final class PhabricatorStorageManagementAPI extends Phobject { | |||||||
|     return $this->namespace.'_'.$fragment; |     return $this->namespace.'_'.$fragment; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   public function getDisplayName() { | ||||||
|  |     return $this->getRef()->getDisplayName(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   public function getDatabaseList(array $patches, $only_living = false) { |   public function getDatabaseList(array $patches, $only_living = false) { | ||||||
|     assert_instances_of($patches, 'PhabricatorStoragePatch'); |     assert_instances_of($patches, 'PhabricatorStoragePatch'); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -21,86 +21,112 @@ final class PhabricatorStorageManagementDestroyWorkflow | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   public function didExecute(PhutilArgumentParser $args) { |   public function didExecute(PhutilArgumentParser $args) { | ||||||
|     $console = PhutilConsole::getConsole(); |     $api = $this->getSingleAPI(); | ||||||
|  |  | ||||||
|  |     $host_display = $api->getDisplayName(); | ||||||
|  |  | ||||||
|     if (!$this->isDryRun() && !$this->isForce()) { |     if (!$this->isDryRun() && !$this->isForce()) { | ||||||
|       if ($args->getArg('unittest-fixtures')) { |       if ($args->getArg('unittest-fixtures')) { | ||||||
|         $console->writeOut( |         $warning = pht( | ||||||
|           phutil_console_wrap( |           'Are you completely sure you really want to destroy all unit '. | ||||||
|             pht( |           'test fixure data on host "%s"? This operation can not be undone.', | ||||||
|               'Are you completely sure you really want to destroy all unit '. |           $host_display); | ||||||
|               'test fixure data? This operation can not be undone.'))); |  | ||||||
|  |         echo tsprintf( | ||||||
|  |           '%B', | ||||||
|  |           id(new PhutilConsoleBlock()) | ||||||
|  |             ->addParagraph($warning) | ||||||
|  |             ->drawConsoleString()); | ||||||
|  |  | ||||||
|         if (!phutil_console_confirm(pht('Destroy all unit test data?'))) { |         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); |           exit(1); | ||||||
|         } |         } | ||||||
|       } else { |       } else { | ||||||
|         $console->writeOut( |         $warning = pht( | ||||||
|           phutil_console_wrap( |           'Are you completely sure you really want to permanently destroy '. | ||||||
|             pht( |           'all storage for Phabricator data on host "%s"? This operation '. | ||||||
|               'Are you completely sure you really want to permanently destroy '. |           'can not be undone and your data will not be recoverable if '. | ||||||
|               'all storage for Phabricator data? This operation can not be '. |           'you proceed.', | ||||||
|               '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?'))) { |         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); |           exit(1); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         if (!phutil_console_confirm(pht('Really destroy all data forever?'))) { |         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); |           exit(1); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     $apis = $this->getMasterAPIs(); |     $patches = $this->getPatches(); | ||||||
|     foreach ($apis as $api) { |  | ||||||
|       $patches = $this->getPatches(); |  | ||||||
|  |  | ||||||
|       if ($args->getArg('unittest-fixtures')) { |     if ($args->getArg('unittest-fixtures')) { | ||||||
|         $conn = $api->getConn(null); |       $conn = $api->getConn(null); | ||||||
|         $databases = queryfx_all( |       $databases = queryfx_all( | ||||||
|           $conn, |         $conn, | ||||||
|           'SELECT DISTINCT(TABLE_SCHEMA) AS db '. |         'SELECT DISTINCT(TABLE_SCHEMA) AS db '. | ||||||
|           'FROM INFORMATION_SCHEMA.TABLES '. |         'FROM INFORMATION_SCHEMA.TABLES '. | ||||||
|           'WHERE TABLE_SCHEMA LIKE %>', |         'WHERE TABLE_SCHEMA LIKE %>', | ||||||
|           PhabricatorTestCase::NAMESPACE_PREFIX); |         PhabricatorTestCase::NAMESPACE_PREFIX); | ||||||
|         $databases = ipull($databases, 'db'); |       $databases = ipull($databases, 'db'); | ||||||
|       } else { |     } else { | ||||||
|         $databases   = $api->getDatabaseList($patches); |       $databases = $api->getDatabaseList($patches); | ||||||
|         $databases[] = $api->getDatabaseName('meta_data'); |       $databases[] = $api->getDatabaseName('meta_data'); | ||||||
|  |  | ||||||
|         // These are legacy databases that were dropped long ago. See T2237. |       // These are legacy databases that were dropped long ago. See T2237. | ||||||
|         $databases[] = $api->getDatabaseName('phid'); |       $databases[] = $api->getDatabaseName('phid'); | ||||||
|         $databases[] = $api->getDatabaseName('directory'); |       $databases[] = $api->getDatabaseName('directory'); | ||||||
|       } |     } | ||||||
|  |  | ||||||
|       foreach ($databases as $database) { |     asort($databases); | ||||||
|         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); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (!$this->isDryRun()) { |     foreach ($databases as $database) { | ||||||
|         $console->writeOut( |       if ($this->isDryRun()) { | ||||||
|           "%s\n", |         $this->logInfo( | ||||||
|  |           pht('DRY RUN'), | ||||||
|           pht( |           pht( | ||||||
|             'Storage on "%s" was destroyed.', |             'Would drop database "%s" on host "%s".', | ||||||
|             $api->getRef()->getRefKey())); |             $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; |     return 0; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -0,0 +1,74 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | final class PhabricatorPreambleTestCase | ||||||
|  |   extends PhabricatorTestCase { | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * @phutil-external-symbol function preamble_get_x_forwarded_for_address | ||||||
|  |    */ | ||||||
|  |   public function testXForwardedForLayers() { | ||||||
|  |     $tests = array( | ||||||
|  |       // This is normal behavior with one load balancer. | ||||||
|  |       array( | ||||||
|  |         'header' => '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)); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  | } | ||||||
| @@ -160,6 +160,20 @@ final class AphrontDialogView | |||||||
|     return $this->appendChild($box); |     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) { |   public function appendParagraph($paragraph) { | ||||||
|     return $this->appendParagraphTag($paragraph); |     return $this->appendParagraphTag($paragraph); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -116,8 +116,10 @@ final class PhabricatorMainMenuSearchView extends AphrontView { | |||||||
|     return $form; |     return $form; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private function buildModeSelector($selector_id, $application_id) { |   public static function getGlobalSearchScopeItems( | ||||||
|     $viewer = $this->getViewer(); |     PhabricatorUser $viewer, | ||||||
|  |     PhabricatorApplication $application = null, | ||||||
|  |     $global_only = false) { | ||||||
|  |  | ||||||
|     $items = array(); |     $items = array(); | ||||||
|     $items[] = array( |     $items[] = array( | ||||||
| @@ -132,7 +134,6 @@ final class PhabricatorMainMenuSearchView extends AphrontView { | |||||||
|  |  | ||||||
|     $application_value = null; |     $application_value = null; | ||||||
|     $application_icon = self::DEFAULT_APPLICATION_ICON; |     $application_icon = self::DEFAULT_APPLICATION_ICON; | ||||||
|     $application = $this->getApplication(); |  | ||||||
|     if ($application) { |     if ($application) { | ||||||
|       $application_value = get_class($application); |       $application_value = get_class($application); | ||||||
|       if ($application->getApplicationSearchDocumentTypes()) { |       if ($application->getApplicationSearchDocumentTypes()) { | ||||||
| @@ -154,14 +155,24 @@ final class PhabricatorMainMenuSearchView extends AphrontView { | |||||||
|     $engine = id(new PhabricatorSearchApplicationSearchEngine()) |     $engine = id(new PhabricatorSearchApplicationSearchEngine()) | ||||||
|       ->setViewer($viewer); |       ->setViewer($viewer); | ||||||
|     $engine_queries = $engine->loadEnabledNamedQueries(); |     $engine_queries = $engine->loadEnabledNamedQueries(); | ||||||
|     $query_map = mpull($engine_queries, 'getQueryName', 'getQueryKey'); |     foreach ($engine_queries as $query) { | ||||||
|     foreach ($query_map as $query_key => $query_name) { |       $query_key = $query->getQueryKey(); | ||||||
|       if ($query_key == 'all') { |       if ($query_key == 'all') { | ||||||
|         // Skip the builtin "All" query since it's redundant with the default |         // Skip the builtin "All" query since it's redundant with the default | ||||||
|         // setting. |         // setting. | ||||||
|         continue; |         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( |       $items[] = array( | ||||||
|         'icon' => 'fa-certificate', |         'icon' => 'fa-certificate', | ||||||
|         'name' => $query_name, |         'name' => $query_name, | ||||||
| @@ -185,6 +196,14 @@ final class PhabricatorMainMenuSearchView extends AphrontView { | |||||||
|       'href' => PhabricatorEnv::getDoclink('Search User Guide'), |       '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; |     $scope_key = PhabricatorSearchScopeSetting::SETTINGKEY; | ||||||
|     $current_value = $viewer->getUserSetting($scope_key); |     $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()) |     $selector = id(new PHUIButtonView()) | ||||||
|       ->setID($selector_id) |       ->setID($selector_id) | ||||||
|       ->addClass('phabricator-main-menu-search-dropdown') |       ->addClass('phabricator-main-menu-search-dropdown') | ||||||
|   | |||||||
							
								
								
									
										77
									
								
								support/startup/preamble-utils.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								support/startup/preamble-utils.php
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,77 @@ | |||||||
|  | <?php | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Parse the "X_FORWARDED_FOR" HTTP header to determine the original client | ||||||
|  |  * address. | ||||||
|  |  * | ||||||
|  |  * @param  int  Number of devices to trust. | ||||||
|  |  * @return void | ||||||
|  |  */ | ||||||
|  | function preamble_trust_x_forwarded_for_header($layers = 1) { | ||||||
|  |   if (!is_int($layers) || ($layers < 1)) { | ||||||
|  |     echo | ||||||
|  |       'preamble_trust_x_forwarded_for_header(<layers>): '. | ||||||
|  |       '"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; | ||||||
|  | } | ||||||
| @@ -85,6 +85,7 @@ function phabricator_startup() { | |||||||
|   require_once $root.'/support/startup/PhabricatorClientLimit.php'; |   require_once $root.'/support/startup/PhabricatorClientLimit.php'; | ||||||
|   require_once $root.'/support/startup/PhabricatorClientRateLimit.php'; |   require_once $root.'/support/startup/PhabricatorClientRateLimit.php'; | ||||||
|   require_once $root.'/support/startup/PhabricatorClientConnectionLimit.php'; |   require_once $root.'/support/startup/PhabricatorClientConnectionLimit.php'; | ||||||
|  |   require_once $root.'/support/startup/preamble-utils.php'; | ||||||
|  |  | ||||||
|   // If the preamble script exists, load it. |   // If the preamble script exists, load it. | ||||||
|   $t_preamble = microtime(true); |   $t_preamble = microtime(true); | ||||||
|   | |||||||
| @@ -36,16 +36,17 @@ | |||||||
| } | } | ||||||
|  |  | ||||||
| .chart .point { | .chart .point { | ||||||
|   fill: {$lightblue}; |   fill: #ffffff; | ||||||
|   stroke: {$blue}; |   stroke: {$blue}; | ||||||
|   stroke-width: 1px; |   stroke-width: 2px; | ||||||
|  |   position: relative; | ||||||
|  |   cursor: pointer; | ||||||
| } | } | ||||||
|  |  | ||||||
| .chart-tooltip { | .chart-tooltip { | ||||||
|   position: absolute; |   position: absolute; | ||||||
|   text-align: center; |   text-align: center; | ||||||
|   width: 120px; |   width: 120px; | ||||||
|   height: 16px; |  | ||||||
|   overflow: hidden; |   overflow: hidden; | ||||||
|   padding: 2px; |   padding: 2px; | ||||||
|   background: {$lightbluebackground}; |   background: {$lightbluebackground}; | ||||||
|   | |||||||
| @@ -354,45 +354,3 @@ body .phui-header-shell.phui-bleed-header | |||||||
| .phui-header-view .phui-tag-indigo a { | .phui-header-view .phui-tag-indigo a { | ||||||
|   color: {$sh-indigotext}; |   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}; |  | ||||||
| } |  | ||||||
|   | |||||||
							
								
								
									
										62
									
								
								webroot/rsrc/css/phui/phui-policy-section-view.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								webroot/rsrc/css/phui/phui-policy-section-view.css
									
									
									
									
									
										Normal file
									
								
							| @@ -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; | ||||||
|  | } | ||||||
| @@ -36,6 +36,10 @@ | |||||||
|  |  | ||||||
| .phui-workcard .phui-oi-link { | .phui-workcard .phui-oi-link { | ||||||
|   white-space: normal; |   white-space: normal; | ||||||
|  |  | ||||||
|  |   /* See T13413. This works around a Chrome 77 rendering engine freeze. */ | ||||||
|  |   word-wrap: normal; | ||||||
|  |  | ||||||
|   font-weight: normal; |   font-weight: normal; | ||||||
|   color: {$blacktext}; |   color: {$blacktext}; | ||||||
|   margin-left: 2px; |   margin-left: 2px; | ||||||
|   | |||||||
							
								
								
									
										37
									
								
								webroot/rsrc/js/application/fact/Chart.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										37
									
								
								webroot/rsrc/js/application/fact/Chart.js
									
									
									
									
										vendored
									
									
								
							| @@ -133,18 +133,33 @@ JX.install('Chart', { | |||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     _newStackedArea: function(g, dataset, x, y, div, curtain) { |     _newStackedArea: function(g, dataset, x, y, div, curtain) { | ||||||
|  |       var ii; | ||||||
|  |  | ||||||
|       var to_date = JX.bind(this, this._newDate); |       var to_date = JX.bind(this, this._newDate); | ||||||
|  |  | ||||||
|       var area = d3.area() |       var area = d3.area() | ||||||
|         .x(function(d) { return x(to_date(d.x)); }) |         .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); }); |         .y1(function(d) { return y(d.y1); }); | ||||||
|  |  | ||||||
|       var line = d3.line() |       var line = d3.line() | ||||||
|         .x(function(d) { return x(to_date(d.x)); }) |         .x(function(d) { return x(to_date(d.x)); }) | ||||||
|         .y(function(d) { return y(d.y1); }); |         .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 label = new JX.ChartFunctionLabel(dataset.labels[ii]); | ||||||
|  |  | ||||||
|         var fill_color = label.getFillColor() || label.getColor(); |         var fill_color = label.getFillColor() || label.getColor(); | ||||||
| @@ -160,6 +175,11 @@ JX.install('Chart', { | |||||||
|           .style('stroke', stroke_color) |           .style('stroke', stroke_color) | ||||||
|           .attr('d', line(dataset.data[ii])); |           .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') |         g.selectAll('dot') | ||||||
|           .data(dataset.events[ii]) |           .data(dataset.events[ii]) | ||||||
|           .enter() |           .enter() | ||||||
| @@ -178,8 +198,16 @@ JX.install('Chart', { | |||||||
|  |  | ||||||
|             var d_d = dd.getDate(); |             var d_d = dd.getDate(); | ||||||
|  |  | ||||||
|  |             var y = parseInt(d.y1); | ||||||
|  |  | ||||||
|  |             var label = d.n + ' Points'; | ||||||
|  |  | ||||||
|  |             var view = | ||||||
|  |               d_y + '-' + d_m + '-' + d_d + ': ' + y + '<br />' + | ||||||
|  |               label; | ||||||
|  |  | ||||||
|             div |             div | ||||||
|               .html(d_y + '-' + d_m + '-' + d_d + ': ' + d.y1) |               .html(view) | ||||||
|               .style('opacity', 0.9) |               .style('opacity', 0.9) | ||||||
|               .style('left', (d3.event.pageX - 60) + 'px') |               .style('left', (d3.event.pageX - 60) + 'px') | ||||||
|               .style('top', (d3.event.pageY - 38) + 'px'); |               .style('top', (d3.event.pageY - 38) + 'px'); | ||||||
| @@ -187,9 +215,8 @@ JX.install('Chart', { | |||||||
|           .on('mouseout', function() { |           .on('mouseout', function() { | ||||||
|             div.style('opacity', 0); |             div.style('opacity', 0); | ||||||
|           }); |           }); | ||||||
|  |  | ||||||
|         curtain.addFunctionLabel(label); |  | ||||||
|       } |       } | ||||||
|  |  | ||||||
|     }, |     }, | ||||||
|  |  | ||||||
|     _newDate: function(epoch) { |     _newDate: function(epoch) { | ||||||
|   | |||||||
| @@ -350,8 +350,10 @@ JX.install('HeraldRuleEditor', { | |||||||
|         sigil: 'field-select' |         sigil: 'field-select' | ||||||
|       }; |       }; | ||||||
|  |  | ||||||
|       var field_select = this._renderGroupSelect(groups, attrs); |       var field_select = this._renderGroupSelect( | ||||||
|       field_select.value = this._config.conditions[row_id][0]; |         groups, | ||||||
|  |         attrs, | ||||||
|  |         this._config.conditions[row_id][0]); | ||||||
|  |  | ||||||
|       var field_cell = JX.$N('td', {sigil: 'field-cell'}, field_select); |       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 = []; |       var optgroups = []; | ||||||
|       for (var ii = 0; ii < groups.length; ii++) { |       for (var ii = 0; ii < groups.length; ii++) { | ||||||
|         var group = groups[ii]; |         var group = groups[ii]; | ||||||
|         var options = []; |         var options = []; | ||||||
|         for (var k in group.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) { |     _newAction : function(data) { | ||||||
| @@ -402,8 +424,10 @@ JX.install('HeraldRuleEditor', { | |||||||
|         sigil: 'action-select' |         sigil: 'action-select' | ||||||
|       }; |       }; | ||||||
|  |  | ||||||
|       var action_select = this._renderGroupSelect(groups, attrs); |       var action_select = this._renderGroupSelect( | ||||||
|       action_select.value = action[0]; |         groups, | ||||||
|  |         attrs, | ||||||
|  |         action[0]); | ||||||
|  |  | ||||||
|       var action_cell = JX.$N('td', {sigil: 'action-cell'}, action_select); |       var action_cell = JX.$N('td', {sigil: 'action-cell'}, action_select); | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user