diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index 896b9fbf61..ded8d2387b 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -2265,6 +2265,24 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/js/application/pholio/behavior-pholio-mock-view.js', ), + 'javelin-behavior-policy-rule-editor' => + array( + 'uri' => '/res/4ae4249d/rsrc/js/application/policy/behavior-policy-rule-editor.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-behavior', + 1 => 'multirow-row-manager', + 2 => 'javelin-dom', + 3 => 'javelin-util', + 4 => 'phabricator-prefab', + 5 => 'javelin-tokenizer', + 6 => 'javelin-typeahead', + 7 => 'javelin-typeahead-preloaded-source', + 8 => 'javelin-json', + ), + 'disk' => '/rsrc/js/application/policy/behavior-policy-rule-editor.js', + ), 'javelin-behavior-ponder-votebox' => array( 'uri' => '/res/c28daa12/rsrc/js/application/ponder/behavior-votebox.js', @@ -3886,13 +3904,22 @@ celerity_register_resource_map(array( ), 'policy-css' => array( - 'uri' => '/res/ebb12aa0/rsrc/css/application/policy/policy.css', + 'uri' => '/res/51325bff/rsrc/css/application/policy/policy.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/application/policy/policy.css', ), + 'policy-edit-css' => + array( + 'uri' => '/res/1e2a2b5e/rsrc/css/application/policy/policy-edit.css', + 'type' => 'css', + 'requires' => + array( + ), + 'disk' => '/rsrc/css/application/policy/policy-edit.css', + ), 'ponder-comment-table-css' => array( 'uri' => '/res/4aa4b865/rsrc/css/application/ponder/comments.css', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index f4c439d217..5ca6db4847 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1483,6 +1483,7 @@ phutil_register_library_map(array( 'PhabricatorPolicyConstants' => 'applications/policy/constants/PhabricatorPolicyConstants.php', 'PhabricatorPolicyController' => 'applications/policy/controller/PhabricatorPolicyController.php', 'PhabricatorPolicyDataTestCase' => 'applications/policy/__tests__/PhabricatorPolicyDataTestCase.php', + 'PhabricatorPolicyEditController' => 'applications/policy/controller/PhabricatorPolicyEditController.php', 'PhabricatorPolicyException' => 'applications/policy/exception/PhabricatorPolicyException.php', 'PhabricatorPolicyExplainController' => 'applications/policy/controller/PhabricatorPolicyExplainController.php', 'PhabricatorPolicyFilter' => 'applications/policy/filter/PhabricatorPolicyFilter.php', @@ -1491,6 +1492,11 @@ phutil_register_library_map(array( 'PhabricatorPolicyManagementUnlockWorkflow' => 'applications/policy/management/PhabricatorPolicyManagementUnlockWorkflow.php', 'PhabricatorPolicyManagementWorkflow' => 'applications/policy/management/PhabricatorPolicyManagementWorkflow.php', 'PhabricatorPolicyQuery' => 'applications/policy/query/PhabricatorPolicyQuery.php', + 'PhabricatorPolicyRule' => 'applications/policy/rule/PhabricatorPolicyRule.php', + 'PhabricatorPolicyRuleAdministrators' => 'applications/policy/rule/PhabricatorPolicyRuleAdministrators.php', + 'PhabricatorPolicyRuleLunarPhase' => 'applications/policy/rule/PhabricatorPolicyRuleLunarPhase.php', + 'PhabricatorPolicyRuleProjects' => 'applications/policy/rule/PhabricatorPolicyRuleProjects.php', + 'PhabricatorPolicyRuleUsers' => 'applications/policy/rule/PhabricatorPolicyRuleUsers.php', 'PhabricatorPolicyTestCase' => 'applications/policy/__tests__/PhabricatorPolicyTestCase.php', 'PhabricatorPolicyTestObject' => 'applications/policy/__tests__/PhabricatorPolicyTestObject.php', 'PhabricatorPolicyType' => 'applications/policy/constants/PhabricatorPolicyType.php', @@ -3672,12 +3678,17 @@ phutil_register_library_map(array( 'PhabricatorPolicyConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPolicyController' => 'PhabricatorController', 'PhabricatorPolicyDataTestCase' => 'PhabricatorTestCase', + 'PhabricatorPolicyEditController' => 'PhabricatorPolicyController', 'PhabricatorPolicyException' => 'Exception', 'PhabricatorPolicyExplainController' => 'PhabricatorPolicyController', 'PhabricatorPolicyManagementShowWorkflow' => 'PhabricatorPolicyManagementWorkflow', 'PhabricatorPolicyManagementUnlockWorkflow' => 'PhabricatorPolicyManagementWorkflow', 'PhabricatorPolicyManagementWorkflow' => 'PhutilArgumentWorkflow', 'PhabricatorPolicyQuery' => 'PhabricatorQuery', + 'PhabricatorPolicyRuleAdministrators' => 'PhabricatorPolicyRule', + 'PhabricatorPolicyRuleLunarPhase' => 'PhabricatorPolicyRule', + 'PhabricatorPolicyRuleProjects' => 'PhabricatorPolicyRule', + 'PhabricatorPolicyRuleUsers' => 'PhabricatorPolicyRule', 'PhabricatorPolicyTestCase' => 'PhabricatorTestCase', 'PhabricatorPolicyTestObject' => 'PhabricatorPolicyInterface', 'PhabricatorPolicyType' => 'PhabricatorPolicyConstants', diff --git a/src/applications/policy/application/PhabricatorApplicationPolicy.php b/src/applications/policy/application/PhabricatorApplicationPolicy.php index c9eec4bcef..54c55aefaa 100644 --- a/src/applications/policy/application/PhabricatorApplicationPolicy.php +++ b/src/applications/policy/application/PhabricatorApplicationPolicy.php @@ -15,6 +15,7 @@ final class PhabricatorApplicationPolicy extends PhabricatorApplication { '/policy/' => array( 'explain/(?P[^/]+)/(?P[^/]+)/' => 'PhabricatorPolicyExplainController', + 'edit/' => 'PhabricatorPolicyEditController', ), ); } diff --git a/src/applications/policy/controller/PhabricatorPolicyEditController.php b/src/applications/policy/controller/PhabricatorPolicyEditController.php new file mode 100644 index 0000000000..a5d7d462d2 --- /dev/null +++ b/src/applications/policy/controller/PhabricatorPolicyEditController.php @@ -0,0 +1,153 @@ +getRequest(); + $viewer = $request->getUser(); + + $root_id = celerity_generate_unique_node_id(); + + $action_options = array( + 'allow' => pht('Allow'), + 'deny' => pht('Deny'), + ); + + $rules = id(new PhutilSymbolLoader()) + ->setAncestorClass('PhabricatorPolicyRule') + ->loadObjects(); + + $rules = msort($rules, 'getRuleOrder'); + + $default_value = 'deny'; + $default_rule = array( + 'action' => head_key($action_options), + 'rule' => head_key($rules), + 'value' => null, + ); + + if ($request->isFormPost()) { + $data = $request->getStr('rules'); + $data = @json_decode($data, true); + if (!is_array($data)) { + throw new Exception("Failed to JSON decode rule data!"); + } + + $rule_data = array(); + foreach ($data as $rule) { + $action = idx($rule, 'action'); + switch ($action) { + case 'allow': + case 'deny': + break; + default: + throw new Exception("Invalid action '{$action}'!"); + } + + $rule_class = idx($rule, 'rule'); + if (empty($rules[$rule_class])) { + throw new Exception("Invalid rule class '{$rule_class}'!"); + } + + $rule_obj = $rules[$rule_class]; + + $value = $rule_obj->getValueForStorage(idx($rule, 'value')); + $value = $rule_obj->getValueForDisplay($viewer, $value); + + $rule_data[] = array( + 'action' => $action, + 'rule' => $rule_class, + 'value' => $value, + ); + } + + $default_value = $request->getStr('default'); + } else { + $rule_data = array( + $default_rule, + ); + } + + $default_select = AphrontFormSelectControl::renderSelectTag( + $default_value, + $action_options, + array( + 'name' => 'default', + )); + + + $form = id(new PHUIFormLayoutView()) + ->appendChild( + javelin_tag( + 'input', + array( + 'type' => 'hidden', + 'name' => 'rules', + 'sigil' => 'rules', + ))) + ->appendChild( + id(new AphrontFormInsetView()) + ->setTitle(pht('Rules')) + ->setRightButton( + javelin_tag( + 'a', + array( + 'href' => '#', + 'class' => 'button green', + 'sigil' => 'create-rule', + 'mustcapture' => true + ), + pht('New Rule'))) + ->setDescription( + pht('These rules are processed in order.')) + ->setContent(javelin_tag( + 'table', + array( + 'sigil' => 'rules', + 'class' => 'policy-rules-table' + ), + ''))) + ->appendChild( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('If No Rules Match')) + ->setValue(pht( + "%s all other users.", + $default_select))); + + $form = phutil_tag( + 'div', + array( + 'id' => $root_id, + ), + $form); + + $rule_options = mpull($rules, 'getRuleDescription'); + $type_map = mpull($rules, 'getValueControlType'); + $templates = mpull($rules, 'getValueControlTemplate'); + + require_celerity_resource('policy-edit-css'); + Javelin::initBehavior( + 'policy-rule-editor', + array( + 'rootID' => $root_id, + 'actions' => $action_options, + 'rules' => $rule_options, + 'types' => $type_map, + 'templates' => $templates, + 'data' => $rule_data, + 'defaultRule' => $default_rule, + )); + + $dialog = id(new AphrontDialogView()) + ->setWidth(AphrontDialogView::WIDTH_FULL) + ->setUser($viewer) + ->setTitle(pht('Edit Policy')) + ->appendChild($form) + ->addSubmitButton(pht('Save Policy')) + ->addCancelButton('#'); + + return id(new AphrontDialogResponse())->setDialog($dialog); + } + +} diff --git a/src/applications/policy/rule/PhabricatorPolicyRule.php b/src/applications/policy/rule/PhabricatorPolicyRule.php new file mode 100644 index 0000000000..39fc614af2 --- /dev/null +++ b/src/applications/policy/rule/PhabricatorPolicyRule.php @@ -0,0 +1,37 @@ +getIsAdmin(); + } + + public function getValueControlType() { + return self::CONTROL_TYPE_NONE; + } + +} diff --git a/src/applications/policy/rule/PhabricatorPolicyRuleLunarPhase.php b/src/applications/policy/rule/PhabricatorPolicyRuleLunarPhase.php new file mode 100644 index 0000000000..df2fa79674 --- /dev/null +++ b/src/applications/policy/rule/PhabricatorPolicyRuleLunarPhase.php @@ -0,0 +1,51 @@ +isFull(); + case 'new': + return $moon->isNew(); + case 'waxing': + return $moon->isWaxing(); + case 'waning': + return $moon->isWaning(); + } + + return false; + } + + public function getValueControlType() { + return self::CONTROL_TYPE_SELECT; + } + + public function getValueControlTemplate() { + return array( + 'options' => array( + self::PHASE_FULL => pht('is full'), + self::PHASE_NEW => pht('is new'), + self::PHASE_WAXING => pht('is waxing'), + self::PHASE_WANING => pht('is waning'), + ), + ); + } + + public function getRuleOrder() { + return 1000; + } + +} diff --git a/src/applications/policy/rule/PhabricatorPolicyRuleProjects.php b/src/applications/policy/rule/PhabricatorPolicyRuleProjects.php new file mode 100644 index 0000000000..11a08cf878 --- /dev/null +++ b/src/applications/policy/rule/PhabricatorPolicyRuleProjects.php @@ -0,0 +1,67 @@ +setViewer(PhabricatorUser::getOmnipotentUser()) + ->withMemberPHIDs(array($viewer->getPHID())) + ->withPHIDs($values) + ->execute(); + foreach ($projects as $project) { + $this->memberships[$viewer->getPHID()][$project->getPHID()] = true; + } + } + + public function applyRule(PhabricatorUser $viewer, $value) { + foreach ($value as $project_phid) { + if (isset($this->memberships[$viewer->getPHID()][$project_phid])) { + return true; + } + } + return false; + } + + public function getValueControlType() { + return self::CONTROL_TYPE_TOKENIZER; + } + + public function getValueControlTemplate() { + return array( + 'markup' => new AphrontTokenizerTemplateView(), + 'uri' => '/typeahead/common/projects/', + 'placeholder' => pht('Type a project name...'), + ); + } + + public function getRuleOrder() { + return 200; + } + + public function getValueForStorage($value) { + PhutilTypeSpec::newFromString('list')->check($value); + return array_values($value); + } + + public function getValueForDisplay(PhabricatorUser $viewer, $value) { + $handles = id(new PhabricatorHandleQuery()) + ->setViewer($viewer) + ->withPHIDs($value) + ->execute(); + + return mpull($handles, 'getFullName', 'getPHID'); + } + +} diff --git a/src/applications/policy/rule/PhabricatorPolicyRuleUsers.php b/src/applications/policy/rule/PhabricatorPolicyRuleUsers.php new file mode 100644 index 0000000000..a4f28aa5dc --- /dev/null +++ b/src/applications/policy/rule/PhabricatorPolicyRuleUsers.php @@ -0,0 +1,44 @@ +getPHID()]); + } + + public function getValueControlType() { + return self::CONTROL_TYPE_TOKENIZER; + } + + public function getValueControlTemplate() { + return array( + 'markup' => new AphrontTokenizerTemplateView(), + 'uri' => '/typeahead/common/accounts/', + 'placeholder' => pht('Type a user name...'), + ); + } + + public function getRuleOrder() { + return 100; + } + + public function getValueForStorage($value) { + PhutilTypeSpec::newFromString('list')->check($value); + return array_values($value); + } + + public function getValueForDisplay(PhabricatorUser $viewer, $value) { + $handles = id(new PhabricatorHandleQuery()) + ->setViewer($viewer) + ->withPHIDs($value) + ->execute(); + + return mpull($handles, 'getFullName', 'getPHID'); + } + +} diff --git a/webroot/rsrc/css/application/policy/policy-edit.css b/webroot/rsrc/css/application/policy/policy-edit.css new file mode 100644 index 0000000000..08446d96ae --- /dev/null +++ b/webroot/rsrc/css/application/policy/policy-edit.css @@ -0,0 +1,32 @@ +/** + * @provides policy-edit-css + */ + +.policy-rules-table { + width: 100%; +} + +.policy-rules-table td { + padding: 4px; + width: 32px; + vertical-align: middle; +} + +.policy-rules-table td.action-cell { + width: 120px; +} + +.policy-rules-table td.rule-cell { + width: 180px; +} + +.policy-rules-table td.value-cell { + width: auto; + padding-right: 12px; +} + +.policy-rules-table td.action-cell select, +.policy-rules-table td.rule-cell select, +.policy-rules-table td input { + width: 100%; +} diff --git a/webroot/rsrc/js/application/policy/behavior-policy-rule-editor.js b/webroot/rsrc/js/application/policy/behavior-policy-rule-editor.js new file mode 100644 index 0000000000..a7b77376e8 --- /dev/null +++ b/webroot/rsrc/js/application/policy/behavior-policy-rule-editor.js @@ -0,0 +1,179 @@ +/** + * @provides javelin-behavior-policy-rule-editor + * @requires javelin-behavior + * multirow-row-manager + * javelin-dom + * javelin-util + * phabricator-prefab + * javelin-tokenizer + * javelin-typeahead + * javelin-typeahead-preloaded-source + * javelin-json + */ +JX.behavior('policy-rule-editor', function(config) { + var root = JX.$(config.rootID); + var rows = []; + var data = {}; + + JX.DOM.listen( + root, + 'click', + 'create-rule', + function(e) { + e.kill(); + new_rule(config.defaultRule); + }); + + JX.DOM.listen( + root, + 'change', + 'rule-select', + function(e) { + e.kill(); + + var row = e.getNode(JX.MultirowRowManager.getRowSigil()); + var row_id = rules_manager.getRowID(row); + + data[row_id].rule = data[row_id].ruleNode.value; + data[row_id].value = null; + + redraw(row_id); + }); + + JX.DOM.listen( + JX.DOM.findAbove(root, 'form'), + 'submit', + null, + function(e) { + var rules = JX.DOM.find(e.getNode('tag:form'), 'input', 'rules'); + + var value = []; + for (var ii = 0; ii < rows.length; ii++) { + var row_data = data[rows[ii]]; + + var row_dict = { + action: row_data.actionNode.value, + rule: row_data.rule, + value: row_data.getValue() + }; + + value.push(row_dict); + } + + rules.value = JX.JSON.stringify(value); + }); + + + var rules_table = JX.DOM.find(root, 'table', 'rules'); + var rules_manager = new JX.MultirowRowManager(rules_table); + rules_manager.listen( + 'row-removed', + function(row_id) { + delete data[row_id]; + for (var ii = 0; ii < rows.length; ii++) { + if (rows[ii] == row_id) { + rows.splice(ii, 1); + break; + } + } + }); + + + function new_rule(spec) { + var row = rules_manager.addRow([]); + var row_id = rules_manager.getRowID(row); + + rows.push(row_id); + data[row_id] = JX.copy({}, spec); + + redraw(row_id); + } + + function redraw(row_id) { + var action_content = JX.Prefab.renderSelect( + config.actions, + data[row_id].action); + data[row_id].actionNode = action_content; + var action_cell = JX.$N('td', {className: "action-cell"}, action_content); + + var rule_content = JX.Prefab.renderSelect( + config.rules, + data[row_id].rule, + {sigil: 'rule-select'}); + data[row_id].ruleNode = rule_content; + var rule_cell = JX.$N('td', {className: "rule-cell"}, rule_content); + + var input = render_input(data[row_id].rule, null); + + var value_content = input.node; + data[row_id].getValue = input.get; + input.set(data[row_id].value); + + var value_cell = JX.$N('td', {className: "value-cell"}, value_content); + + rules_manager.updateRow(row_id, [action_cell, rule_cell, value_cell]); + } + + function render_input(rule, value) { + var node, get_fn, set_fn; + var type = config.types[rule]; + var template = config.templates[rule]; + + switch (type) { + case 'tokenizer': + node = JX.$H(template.markup).getNode(); + node.id = ''; + + var datasource = new JX.TypeaheadPreloadedSource(template.uri); + + var typeahead = new JX.Typeahead(node); + typeahead.setDatasource(datasource); + + var tokenizer = new JX.Tokenizer(node); + tokenizer.setLimit(template.limit); + tokenizer.setTypeahead(typeahead); + tokenizer.setPlaceholder(template.placeholder); + tokenizer.start(); + + get_fn = function() { return JX.keys(tokenizer.getTokens()); }; + set_fn = function(map) { + if (!map) { + return; + } + for (var k in map) { + tokenizer.addToken(k, map[k]); + } + }; + break; + case 'none': + node = null; + get_fn = JX.bag; + set_fn = JX.bag; + break; + case 'select': + node = JX.Prefab.renderSelect( + config.templates[rule].options, + value); + get_fn = function() { return node.value; }; + set_fn = function(v) { node.value = v; }; + break; + default: + case 'text': + node = JX.$N('input', {type: 'text'}); + get_fn = function() { return node.value; }; + set_fn = function(v) { node.value = v; }; + break; + } + + return { + node: node, + get: get_fn, + set: set_fn + }; + } + + for (var ii = 0; ii < config.data.length; ii++) { + new_rule(config.data[ii]); + } + +});