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