Merge branch 'master' into blender-tweaks

This commit is contained in:
2019-09-18 10:42:01 +02:00
95 changed files with 2595 additions and 938 deletions

View File

@@ -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',

View File

@@ -0,0 +1,3 @@
<?php
PhabricatorRebuildIndexesWorker::rebuildObjectsWithQuery('HeraldRuleQuery');

View File

@@ -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',

View File

@@ -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('/');

View File

@@ -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'));

View File

@@ -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()) {

View File

@@ -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,

View 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;
}
}

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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();

View File

@@ -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) {

View File

@@ -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.

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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(),

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -1,7 +1,7 @@
<?php
final class PhabricatorConstantChartFunction
extends PhabricatorChartFunction {
extends PhabricatorPureChartFunction {
const FUNCTIONKEY = 'constant';

View File

@@ -1,7 +1,7 @@
<?php
final class PhabricatorCosChartFunction
extends PhabricatorChartFunction {
extends PhabricatorPureChartFunction {
const FUNCTIONKEY = 'cos';

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View 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();
}
}

View File

@@ -1,7 +1,7 @@
<?php
final class PhabricatorScaleChartFunction
extends PhabricatorChartFunction {
extends PhabricatorPureChartFunction {
const FUNCTIONKEY = 'scale';

View File

@@ -1,7 +1,7 @@
<?php
final class PhabricatorShiftChartFunction
extends PhabricatorChartFunction {
extends PhabricatorPureChartFunction {
const FUNCTIONKEY = 'shift';

View File

@@ -1,7 +1,7 @@
<?php
final class PhabricatorSinChartFunction
extends PhabricatorChartFunction {
extends PhabricatorPureChartFunction {
const FUNCTIONKEY = 'sin';

View File

@@ -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());
}

View File

@@ -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);
}
}

View File

@@ -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();

View 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);
}
}

View File

@@ -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();

View 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);
}
}

View File

@@ -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));
}
}
}

View File

@@ -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;
}
}

View File

@@ -96,4 +96,8 @@ final class HarbormasterRunBuildPlansHeraldAction
return $record->getTarget();
}
public function isActionAvailable() {
return id(new PhabricatorHarbormasterApplication())->isInstalled();
}
}

View File

@@ -405,4 +405,8 @@ abstract class HeraldAction extends Phobject {
return array();
}
public function isActionAvailable() {
return true;
}
}

View File

@@ -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) {

View File

@@ -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(

View File

@@ -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());

View File

@@ -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;
}
}

View File

@@ -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 )------------------------------------------------ */

View File

@@ -130,4 +130,9 @@ final class LegalpadRequireSignatureHeraldAction
'Require document signatures: %s.',
$this->renderHandleList($value));
}
public function isActionAvailable() {
return id(new PhabricatorLegalpadApplication())->isInstalled();
}
}

View File

@@ -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));

View File

@@ -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());
}

View File

@@ -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;

View File

@@ -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() {

View File

@@ -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'),

View File

@@ -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)

View File

@@ -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);

View File

@@ -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']));
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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");

View File

@@ -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;

View File

@@ -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);
}
/**

View File

@@ -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();

View 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;
}
}

View 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;
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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()

View File

@@ -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() {

View File

@@ -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() {

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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) {

View File

@@ -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'))

View File

@@ -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);
}
}

View File

@@ -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.

View File

@@ -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) {

View File

@@ -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;
}
```

View File

@@ -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.

View File

@@ -221,6 +221,9 @@ final class PhabricatorDatabaseRef
return $this->replicaRefs;
}
public function getDisplayName() {
return $this->getRefKey();
}
public function getRefKey() {
$host = $this->getHost();

View File

@@ -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

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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');

View File

@@ -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;
}

View File

@@ -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));
}
}
}

View File

@@ -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);
}

View File

@@ -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')

View 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;
}

View File

@@ -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);

View File

@@ -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};

View File

@@ -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};
}

View 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;
}

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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);