diff --git a/src/applications/policy/controller/PhabricatorPolicyEditController.php b/src/applications/policy/controller/PhabricatorPolicyEditController.php index be380aa0b1..3dd8924bb6 100644 --- a/src/applications/policy/controller/PhabricatorPolicyEditController.php +++ b/src/applications/policy/controller/PhabricatorPolicyEditController.php @@ -1,256 +1,340 @@ getViewer(); - $object_phid = $request->getURIData('objectPHID'); if ($object_phid) { $object = id(new PhabricatorObjectQuery()) ->setViewer($viewer) ->withPHIDs(array($object_phid)) ->executeOne(); if (!$object) { return new Aphront404Response(); } } else { $object_type = $request->getURIData('objectType'); if (!$object_type) { $object_type = $request->getURIData('templateType'); } $phid_types = PhabricatorPHIDType::getAllInstalledTypes($viewer); if (empty($phid_types[$object_type])) { return new Aphront404Response(); } $object = $phid_types[$object_type]->newObject(); if (!$object) { return new Aphront404Response(); } } + $phid = $request->getURIData('phid'); + switch ($phid) { + case AphrontFormPolicyControl::getSelectProjectKey(): + return $this->handleProjectRequest($request); + case AphrontFormPolicyControl::getSelectCustomKey(): + $phid = null; + break; + default: + break; + } + $action_options = array( PhabricatorPolicy::ACTION_ALLOW => pht('Allow'), PhabricatorPolicy::ACTION_DENY => pht('Deny'), ); $rules = id(new PhutilClassMapQuery()) ->setAncestorClass('PhabricatorPolicyRule') ->execute(); foreach ($rules as $key => $rule) { if (!$rule->canApplyToObject($object)) { unset($rules[$key]); } } $rules = msort($rules, 'getRuleOrder'); $default_rule = array( 'action' => head_key($action_options), 'rule' => head_key($rules), 'value' => null, ); - $phid = $request->getURIData('phid'); if ($phid) { $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->withPHIDs(array($phid)) ->execute(); if (!$policies) { return new Aphront404Response(); } $policy = head($policies); } else { $policy = id(new PhabricatorPolicy()) ->setRules(array($default_rule)) ->setDefaultAction(PhabricatorPolicy::ACTION_DENY); } $root_id = celerity_generate_unique_node_id(); $default_action = $policy->getDefaultAction(); $rule_data = $policy->getRules(); $errors = array(); if ($request->isFormPost()) { $data = $request->getStr('rules'); try { $data = phutil_json_decode($data); } catch (PhutilJSONParserException $ex) { throw new PhutilProxyException( pht('Failed to JSON decode rule data!'), $ex); } $rule_data = array(); foreach ($data as $rule) { $action = idx($rule, 'action'); switch ($action) { case 'allow': case 'deny': break; default: throw new Exception(pht("Invalid action '%s'!", $action)); } $rule_class = idx($rule, 'rule'); if (empty($rules[$rule_class])) { throw new Exception(pht("Invalid rule class '%s'!", $rule_class)); } $rule_obj = $rules[$rule_class]; $value = $rule_obj->getValueForStorage(idx($rule, 'value')); $rule_data[] = array( 'action' => $action, 'rule' => $rule_class, 'value' => $value, ); } // Filter out nonsense rules, like a "users" rule without any users // actually specified. $valid_rules = array(); foreach ($rule_data as $rule) { $rule_class = $rule['rule']; if ($rules[$rule_class]->ruleHasEffect($rule['value'])) { $valid_rules[] = $rule; } } if (!$valid_rules) { $errors[] = pht('None of these policy rules have any effect.'); } // NOTE: Policies are immutable once created, and we always create a new // policy here. If we didn't, we would need to lock this endpoint down, // as users could otherwise just go edit the policies of objects with // custom policies. if (!$errors) { $new_policy = new PhabricatorPolicy(); $new_policy->setRules($valid_rules); $new_policy->setDefaultAction($request->getStr('default')); $new_policy->save(); $data = array( 'phid' => $new_policy->getPHID(), 'info' => array( 'name' => $new_policy->getName(), 'full' => $new_policy->getName(), 'icon' => $new_policy->getIcon(), ), ); return id(new AphrontAjaxResponse())->setContent($data); } } // Convert rule values to display format (for example, expanding PHIDs // into tokens). foreach ($rule_data as $key => $rule) { $rule_data[$key]['value'] = $rules[$rule['rule']]->getValueForDisplay( $viewer, $rule['value']); } $default_select = AphrontFormSelectControl::renderSelectTag( $default_action, $action_options, array( 'name' => 'default', )); if ($errors) { $errors = id(new PHUIInfoView()) ->setErrors($errors); } $form = id(new PHUIFormLayoutView()) ->appendChild($errors) ->appendChild( javelin_tag( 'input', array( 'type' => 'hidden', 'name' => 'rules', 'sigil' => 'rules', ))) ->appendChild( id(new PHUIFormInsetView()) ->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, )); $title = pht('Custom Policy'); $key = $request->getStr('capability'); if ($key) { $capability = PhabricatorPolicyCapability::getCapabilityByKey($key); $title = pht('Custom "%s" Policy', $capability->getCapabilityName()); } $dialog = id(new AphrontDialogView()) ->setWidth(AphrontDialogView::WIDTH_FULL) ->setUser($viewer) ->setTitle($title) ->appendChild($form) ->addSubmitButton(pht('Save Policy')) ->addCancelButton('#'); return id(new AphrontDialogResponse())->setDialog($dialog); } + private function handleProjectRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); + + $errors = array(); + $e_project = true; + + if ($request->isFormPost()) { + $project_phids = $request->getArr('projectPHIDs'); + $project_phid = head($project_phids); + + $project = id(new PhabricatorObjectQuery()) + ->setViewer($viewer) + ->withPHIDs(array($project_phid)) + ->executeOne(); + + if ($project) { + // Save this project as one of the user's most recently used projects, + // so we'll show it by default in future menus. + + $pref_key = PhabricatorUserPreferences::PREFERENCE_FAVORITE_POLICIES; + + $preferences = $viewer->loadPreferences(); + $favorites = $preferences->getPreference($pref_key); + if (!is_array($favorites)) { + $favorites = array(); + } + + // Add this, or move it to the end of the list. + unset($favorites[$project_phid]); + $favorites[$project_phid] = true; + + $preferences->setPreference($pref_key, $favorites); + $preferences->save(); + + $data = array( + 'phid' => $project->getPHID(), + 'info' => array( + 'name' => $project->getName(), + 'full' => $project->getName(), + 'icon' => $project->getDisplayIconIcon(), + ), + ); + + return id(new AphrontAjaxResponse())->setContent($data); + } else { + $errors[] = pht('You must choose a project.'); + $e_project = pht('Required'); + } + } + + $project_datasource = id(new PhabricatorProjectDatasource()) + ->setParameters( + array( + 'policy' => 1, + )); + + $form = id(new AphrontFormView()) + ->setUser($viewer) + ->appendControl( + id(new AphrontFormTokenizerControl()) + ->setLabel(pht('Members Of')) + ->setName('projectPHIDs') + ->setLimit(1) + ->setError($e_project) + ->setDatasource($project_datasource)); + + return $this->newDialog() + ->setWidth(AphrontDialogView::WIDTH_FORM) + ->setErrors($errors) + ->setTitle(pht('Select Project')) + ->appendForm($form) + ->addSubmitButton(pht('Done')) + ->addCancelButton('#'); + } + } diff --git a/src/applications/policy/query/PhabricatorPolicyQuery.php b/src/applications/policy/query/PhabricatorPolicyQuery.php index 7ad2630503..df1d6fb0b8 100644 --- a/src/applications/policy/query/PhabricatorPolicyQuery.php +++ b/src/applications/policy/query/PhabricatorPolicyQuery.php @@ -1,387 +1,425 @@ object = $object; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public static function loadPolicies( PhabricatorUser $viewer, PhabricatorPolicyInterface $object) { $results = array(); $map = array(); foreach ($object->getCapabilities() as $capability) { $map[$capability] = $object->getPolicy($capability); } $policies = id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->withPHIDs($map) ->execute(); foreach ($map as $capability => $phid) { $results[$capability] = $policies[$phid]; } return $results; } public static function renderPolicyDescriptions( PhabricatorUser $viewer, PhabricatorPolicyInterface $object, $icon = false) { $policies = self::loadPolicies($viewer, $object); foreach ($policies as $capability => $policy) { $policies[$capability] = $policy->renderDescription($icon); } return $policies; } protected function loadPage() { if ($this->object && $this->phids) { throw new Exception( pht( 'You can not issue a policy query with both %s and %s.', 'setObject()', 'setPHIDs()')); } else if ($this->object) { $phids = $this->loadObjectPolicyPHIDs(); } else { $phids = $this->phids; } $phids = array_fuse($phids); $results = array(); // First, load global policies. foreach (self::getGlobalPolicies() as $phid => $policy) { if (isset($phids[$phid])) { $results[$phid] = $policy; unset($phids[$phid]); } } // Now, load object policies. foreach (self::getObjectPolicies($this->object) as $phid => $policy) { if (isset($phids[$phid])) { $results[$phid] = $policy; unset($phids[$phid]); } } // If we still need policies, we're going to have to fetch data. Bucket // the remaining policies into rule-based policies and handle-based // policies. if ($phids) { $rule_policies = array(); $handle_policies = array(); foreach ($phids as $phid) { $phid_type = phid_get_type($phid); if ($phid_type == PhabricatorPolicyPHIDTypePolicy::TYPECONST) { $rule_policies[$phid] = $phid; } else { $handle_policies[$phid] = $phid; } } if ($handle_policies) { $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->getViewer()) ->withPHIDs($handle_policies) ->execute(); foreach ($handle_policies as $phid) { $results[$phid] = PhabricatorPolicy::newFromPolicyAndHandle( $phid, $handles[$phid]); } } if ($rule_policies) { $rules = id(new PhabricatorPolicy())->loadAllWhere( 'phid IN (%Ls)', $rule_policies); $results += mpull($rules, null, 'getPHID'); } } $results = msort($results, 'getSortKey'); return $results; } public static function isGlobalPolicy($policy) { $global_policies = self::getGlobalPolicies(); if (isset($global_policies[$policy])) { return true; } return false; } public static function getGlobalPolicy($policy) { if (!self::isGlobalPolicy($policy)) { throw new Exception(pht("Policy '%s' is not a global policy!", $policy)); } return idx(self::getGlobalPolicies(), $policy); } private static function getGlobalPolicies() { static $constants = array( PhabricatorPolicies::POLICY_PUBLIC, PhabricatorPolicies::POLICY_USER, PhabricatorPolicies::POLICY_ADMIN, PhabricatorPolicies::POLICY_NOONE, ); $results = array(); foreach ($constants as $constant) { $results[$constant] = id(new PhabricatorPolicy()) ->setType(PhabricatorPolicyType::TYPE_GLOBAL) ->setPHID($constant) ->setName(self::getGlobalPolicyName($constant)) ->setShortName(self::getGlobalPolicyShortName($constant)) ->makeEphemeral(); } return $results; } private static function getGlobalPolicyName($policy) { switch ($policy) { case PhabricatorPolicies::POLICY_PUBLIC: return pht('Public (No Login Required)'); case PhabricatorPolicies::POLICY_USER: return pht('All Users'); case PhabricatorPolicies::POLICY_ADMIN: return pht('Administrators'); case PhabricatorPolicies::POLICY_NOONE: return pht('No One'); default: return pht('Unknown Policy'); } } private static function getGlobalPolicyShortName($policy) { switch ($policy) { case PhabricatorPolicies::POLICY_PUBLIC: return pht('Public'); default: return null; } } private function loadObjectPolicyPHIDs() { $phids = array(); $viewer = $this->getViewer(); if ($viewer->getPHID()) { - $projects = id(new PhabricatorProjectQuery()) - ->setViewer($viewer) - ->withMemberPHIDs(array($viewer->getPHID())) - ->execute(); + $pref_key = PhabricatorUserPreferences::PREFERENCE_FAVORITE_POLICIES; + + $favorite_limit = 10; + $default_limit = 5; + + // If possible, show the user's 10 most recently used projects. + $preferences = $viewer->loadPreferences(); + $favorites = $preferences->getPreference($pref_key); + if (!is_array($favorites)) { + $favorites = array(); + } + $favorite_phids = array_keys($favorites); + $favorite_phids = array_slice($favorite_phids, -$favorite_limit); + + if ($favorite_phids) { + $projects = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withPHIDs($favorite_phids) + ->withIsMilestone(false) + ->setLimit($favorite_limit) + ->execute(); + $projects = mpull($projects, null, 'getPHID'); + } else { + $projects = array(); + } + + // If we didn't find enough favorites, add some default projects. These + // are just arbitrary projects that the viewer is a member of, but may + // be useful on smaller installs and for new users until they can use + // the control enough time to establish useful favorites. + if (count($projects) < $default_limit) { + $default_projects = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withMemberPHIDs(array($viewer->getPHID())) + ->withIsMilestone(false) + ->setLimit($default_limit) + ->execute(); + $default_projects = mpull($default_projects, null, 'getPHID'); + $projects = $projects + $default_projects; + $projects = array_slice($projects, 0, $default_limit); + } + foreach ($projects as $project) { $phids[] = $project->getPHID(); } // Include the "current viewer" policy. This improves consistency, but // is also useful for creating private instances of normally-shared object // types, like repositories. $phids[] = $viewer->getPHID(); } $capabilities = $this->object->getCapabilities(); foreach ($capabilities as $capability) { $policy = $this->object->getPolicy($capability); if (!$policy) { continue; } $phids[] = $policy; } // If this install doesn't have "Public" enabled, don't include it as an // option unless the object already has a "Public" policy. In this case we // retain the policy but enforce it as though it was "All Users". $show_public = PhabricatorEnv::getEnvConfig('policy.allow-public'); foreach (self::getGlobalPolicies() as $phid => $policy) { if ($phid == PhabricatorPolicies::POLICY_PUBLIC) { if (!$show_public) { continue; } } $phids[] = $phid; } foreach (self::getObjectPolicies($this->object) as $phid => $policy) { $phids[] = $phid; } return $phids; } protected function shouldDisablePolicyFiltering() { // Policy filtering of policies is currently perilous and not required by // the application. return true; } public function getQueryApplicationClass() { return 'PhabricatorPolicyApplication'; } public static function isSpecialPolicy($identifier) { if (self::isObjectPolicy($identifier)) { return true; } if (self::isGlobalPolicy($identifier)) { return true; } return false; } /* -( Object Policies )---------------------------------------------------- */ public static function isObjectPolicy($identifier) { $prefix = self::OBJECT_POLICY_PREFIX; return !strncmp($identifier, $prefix, strlen($prefix)); } public static function getObjectPolicy($identifier) { if (!self::isObjectPolicy($identifier)) { return null; } $policies = self::getObjectPolicies(null); return idx($policies, $identifier); } public static function getObjectPolicyRule($identifier) { if (!self::isObjectPolicy($identifier)) { return null; } $rules = self::getObjectPolicyRules(null); return idx($rules, $identifier); } public static function getObjectPolicies($object) { $rule_map = self::getObjectPolicyRules($object); $results = array(); foreach ($rule_map as $key => $rule) { $results[$key] = id(new PhabricatorPolicy()) ->setType(PhabricatorPolicyType::TYPE_OBJECT) ->setPHID($key) ->setIcon($rule->getObjectPolicyIcon()) ->setName($rule->getObjectPolicyName()) ->setShortName($rule->getObjectPolicyShortName()) ->makeEphemeral(); } return $results; } public static function getObjectPolicyRules($object) { $rules = id(new PhutilClassMapQuery()) ->setAncestorClass('PhabricatorPolicyRule') ->execute(); $results = array(); foreach ($rules as $rule) { $key = $rule->getObjectPolicyKey(); if (!$key) { continue; } $full_key = $rule->getObjectPolicyFullKey(); if (isset($results[$full_key])) { throw new Exception( pht( 'Two policy rules (of classes "%s" and "%s") define the same '. 'object policy key ("%s"), but each object policy rule must use '. 'a unique key.', get_class($rule), get_class($results[$full_key]), $key)); } $results[$full_key] = $rule; } if ($object !== null) { foreach ($results as $key => $rule) { if (!$rule->canApplyToObject($object)) { unset($results[$key]); } } } return $results; } public static function getDefaultPolicyForObject( PhabricatorUser $viewer, PhabricatorPolicyInterface $object, $capability) { $phid = $object->getPHID(); if (!$phid) { return null; } $type = phid_get_type($phid); $map = self::getDefaultObjectTypePolicyMap(); if (empty($map[$type][$capability])) { return null; } $policy_phid = $map[$type][$capability]; return id(new PhabricatorPolicyQuery()) ->setViewer($viewer) ->withPHIDs(array($policy_phid)) ->executeOne(); } private static function getDefaultObjectTypePolicyMap() { static $map; if ($map === null) { $map = array(); $apps = PhabricatorApplication::getAllApplications(); foreach ($apps as $app) { $map += $app->getDefaultObjectTypePolicyMap(); } } return $map; } } diff --git a/src/applications/project/policyrule/PhabricatorProjectsPolicyRule.php b/src/applications/project/policyrule/PhabricatorProjectsPolicyRule.php index 1782b62a9d..3977b542c1 100644 --- a/src/applications/project/policyrule/PhabricatorProjectsPolicyRule.php +++ b/src/applications/project/policyrule/PhabricatorProjectsPolicyRule.php @@ -1,76 +1,82 @@ 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, PhabricatorPolicyInterface $object) { 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 $this->getDatasourceTemplate(new PhabricatorProjectDatasource()); + $datasource = id(new PhabricatorProjectDatasource()) + ->setParameters( + array( + 'policy' => 1, + )); + + return $this->getDatasourceTemplate($datasource); } 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'); } public function ruleHasEffect($value) { return (bool)$value; } } diff --git a/src/applications/project/typeahead/PhabricatorProjectDatasource.php b/src/applications/project/typeahead/PhabricatorProjectDatasource.php index 9a18006e5c..cc0140878f 100644 --- a/src/applications/project/typeahead/PhabricatorProjectDatasource.php +++ b/src/applications/project/typeahead/PhabricatorProjectDatasource.php @@ -1,88 +1,94 @@ getViewer(); $raw_query = $this->getRawQuery(); // Allow users to type "#qa" or "qa" to find "Quality Assurance". $raw_query = ltrim($raw_query, '#'); $tokens = self::tokenizeString($raw_query); $query = id(new PhabricatorProjectQuery()) ->needImages(true) ->needSlugs(true); if ($tokens) { $query->withNameTokens($tokens); } + // If this is for policy selection, prevent users from using milestones. + $for_policy = $this->getParameter('policy'); + if ($for_policy) { + $query->withIsMilestone(false); + } + $projs = $this->executeQuery($query); $projs = mpull($projs, null, 'getPHID'); $must_have_cols = $this->getParameter('mustHaveColumns', false); if ($must_have_cols) { $columns = id(new PhabricatorProjectColumnQuery()) ->setViewer($viewer) ->withProjectPHIDs(array_keys($projs)) ->execute(); $has_cols = mgroup($columns, 'getProjectPHID'); } else { $has_cols = array_fill_keys(array_keys($projs), true); } $results = array(); foreach ($projs as $proj) { if (!isset($has_cols[$proj->getPHID()])) { continue; } $closed = null; if ($proj->isArchived()) { $closed = pht('Archived'); } $all_strings = mpull($proj->getSlugs(), 'getSlug'); $all_strings[] = $proj->getName(); $all_strings = implode(' ', $all_strings); $proj_result = id(new PhabricatorTypeaheadResult()) ->setName($all_strings) ->setDisplayName($proj->getName()) ->setDisplayType(pht('Project')) ->setURI($proj->getURI()) ->setPHID($proj->getPHID()) ->setIcon($proj->getDisplayIconIcon()) ->setColor($proj->getColor()) ->setPriorityType('proj') ->setClosed($closed); $slug = $proj->getPrimarySlug(); if (strlen($slug)) { $proj_result->setAutocomplete('#'.$slug); } $proj_result->setImageURI($proj->getProfileImageURI()); $results[] = $proj_result; } return $results; } } diff --git a/src/applications/settings/storage/PhabricatorUserPreferences.php b/src/applications/settings/storage/PhabricatorUserPreferences.php index 53c080ec88..271fd1afb4 100644 --- a/src/applications/settings/storage/PhabricatorUserPreferences.php +++ b/src/applications/settings/storage/PhabricatorUserPreferences.php @@ -1,118 +1,119 @@ array( 'preferences' => self::SERIALIZATION_JSON, ), self::CONFIG_TIMESTAMPS => false, self::CONFIG_KEY_SCHEMA => array( 'userPHID' => array( 'columns' => array('userPHID'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function getPreference($key, $default = null) { return idx($this->preferences, $key, $default); } public function setPreference($key, $value) { $this->preferences[$key] = $value; return $this; } public function unsetPreference($key) { unset($this->preferences[$key]); return $this; } public function getPinnedApplications(array $apps, PhabricatorUser $viewer) { $pref_pinned = self::PREFERENCE_APP_PINNED; $pinned = $this->getPreference($pref_pinned); if ($pinned) { return $pinned; } $pref_tiles = self::PREFERENCE_APP_TILES; $tiles = $this->getPreference($pref_tiles, array()); $full_tile = 'full'; $large = array(); foreach ($apps as $app) { $show = $app->isPinnedByDefault($viewer); // TODO: This is legacy stuff, clean it up eventually. This approximately // retains the old "tiles" preference. if (isset($tiles[get_class($app)])) { $show = ($tiles[get_class($app)] == $full_tile); } if ($show) { $large[] = get_class($app); } } return $large; } public static function filterMonospacedCSSRule($monospaced) { // Prevent the user from doing dangerous things. return preg_replace('/[^a-z0-9 ,".]+/i', '', $monospaced); } } diff --git a/src/view/form/control/AphrontFormPolicyControl.php b/src/view/form/control/AphrontFormPolicyControl.php index 71087bfe07..32f5318a18 100644 --- a/src/view/form/control/AphrontFormPolicyControl.php +++ b/src/view/form/control/AphrontFormPolicyControl.php @@ -1,362 +1,399 @@ object = $object; return $this; } public function setPolicies(array $policies) { assert_instances_of($policies, 'PhabricatorPolicy'); $this->policies = $policies; return $this; } public function setSpacePHID($space_phid) { $this->spacePHID = $space_phid; return $this; } public function getSpacePHID() { return $this->spacePHID; } public function setTemplatePHIDType($type) { $this->templatePHIDType = $type; return $this; } public function setTemplateObject($object) { $this->templateObject = $object; return $this; } public function getSerializedValue() { return json_encode(array( $this->getValue(), $this->getSpacePHID(), )); } public function readSerializedValue($value) { $decoded = phutil_json_decode($value); $policy_value = $decoded[0]; $space_phid = $decoded[1]; $this->setValue($policy_value); $this->setSpacePHID($space_phid); return $this; } public function readValueFromDictionary(array $dictionary) { // TODO: This is a little hacky but will only get us into trouble if we // have multiple view policy controls in multiple paged form views on the // same page, which seems unlikely. $this->setSpacePHID(idx($dictionary, 'spacePHID')); return parent::readValueFromDictionary($dictionary); } public function readValueFromRequest(AphrontRequest $request) { // See note in readValueFromDictionary(). $this->setSpacePHID($request->getStr('spacePHID')); return parent::readValueFromRequest($request); } public function setCapability($capability) { $this->capability = $capability; $labels = array( PhabricatorPolicyCapability::CAN_VIEW => pht('Visible To'), PhabricatorPolicyCapability::CAN_EDIT => pht('Editable By'), PhabricatorPolicyCapability::CAN_JOIN => pht('Joinable By'), ); if (isset($labels[$capability])) { $label = $labels[$capability]; } else { $capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability); if ($capobj) { $label = $capobj->getCapabilityName(); } else { $label = pht('Capability "%s"', $capability); } } $this->setLabel($label); return $this; } protected function getCustomControlClass() { return 'aphront-form-control-policy'; } protected function getOptions() { $capability = $this->capability; $policies = $this->policies; + $viewer = $this->getUser(); + + // Check if we're missing the policy for the current control value. This + // is unusual, but can occur if the user is submitting a form and selected + // an unusual project as a policy but the change has not been saved yet. + $policy_map = mpull($policies, null, 'getPHID'); + $value = $this->getValue(); + if ($value && empty($policy_map[$value])) { + $handle = id(new PhabricatorHandleQuery()) + ->setViewer($viewer) + ->withPHIDs(array($value)) + ->executeOne(); + if ($handle->isComplete()) { + $policies[] = PhabricatorPolicy::newFromPolicyAndHandle( + $value, + $handle); + } + } // Exclude object policies which don't make sense here. This primarily // filters object policies associated from template capabilities (like // "Default Task View Policy" being set to "Task Author") so they aren't // made available on non-template capabilities (like "Can Bulk Edit"). foreach ($policies as $key => $policy) { if ($policy->getType() != PhabricatorPolicyType::TYPE_OBJECT) { continue; } $rule = PhabricatorPolicyQuery::getObjectPolicyRule($policy->getPHID()); if (!$rule) { continue; } $target = nonempty($this->templateObject, $this->object); if (!$rule->canApplyToObject($target)) { unset($policies[$key]); continue; } } $options = array(); foreach ($policies as $policy) { if ($policy->getPHID() == PhabricatorPolicies::POLICY_PUBLIC) { // Never expose "Public" for capabilities which don't support it. $capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability); if (!$capobj || !$capobj->shouldAllowPublicPolicySetting()) { continue; } } $policy_short_name = id(new PhutilUTF8StringTruncator()) ->setMaximumGlyphs(28) ->truncateString($policy->getName()); $options[$policy->getType()][$policy->getPHID()] = array( 'name' => $policy_short_name, 'full' => $policy->getName(), 'icon' => $policy->getIcon(), + 'sort' => phutil_utf8_strtolower($policy->getName()), ); } + $type_project = PhabricatorPolicyType::TYPE_PROJECT; + + $placeholder = id(new PhabricatorPolicy()) + ->setName(pht('Other Project...')) + ->setIcon('fa-search'); + + $options[$type_project] = isort($options[$type_project], 'sort'); + + $options[$type_project][$this->getSelectProjectKey()] = array( + 'name' => $placeholder->getName(), + 'full' => $placeholder->getName(), + 'icon' => $placeholder->getIcon(), + ); + // If we were passed several custom policy options, throw away the ones // which aren't the value for this capability. For example, an object might - // have a custom view pollicy and a custom edit policy. When we render + // have a custom view policy and a custom edit policy. When we render // the selector for "Can View", we don't want to show the "Can Edit" // custom policy -- if we did, the menu would look like this: // // Custom // Custom Policy // Custom Policy // // ...where one is the "view" custom policy, and one is the "edit" custom // policy. $type_custom = PhabricatorPolicyType::TYPE_CUSTOM; if (!empty($options[$type_custom])) { $options[$type_custom] = array_select_keys( $options[$type_custom], array($this->getValue())); } // If there aren't any custom policies, add a placeholder policy so we // render a menu item. This allows the user to switch to a custom policy. if (empty($options[$type_custom])) { $placeholder = new PhabricatorPolicy(); $placeholder->setName(pht('Custom Policy...')); - $options[$type_custom][$this->getCustomPolicyPlaceholder()] = array( + $options[$type_custom][$this->getSelectCustomKey()] = array( 'name' => $placeholder->getName(), 'full' => $placeholder->getName(), 'icon' => $placeholder->getIcon(), ); } $options = array_select_keys( $options, array( PhabricatorPolicyType::TYPE_GLOBAL, PhabricatorPolicyType::TYPE_OBJECT, PhabricatorPolicyType::TYPE_USER, PhabricatorPolicyType::TYPE_CUSTOM, PhabricatorPolicyType::TYPE_PROJECT, )); return $options; } protected function renderInput() { if (!$this->object) { throw new PhutilInvalidStateException('setPolicyObject'); } if (!$this->capability) { throw new PhutilInvalidStateException('setCapability'); } $policy = $this->object->getPolicy($this->capability); if (!$policy) { // TODO: Make this configurable. $policy = PhabricatorPolicies::POLICY_USER; } if (!$this->getValue()) { $this->setValue($policy); } $control_id = celerity_generate_unique_node_id(); $input_id = celerity_generate_unique_node_id(); $caret = phutil_tag( 'span', array( 'class' => 'caret', )); $input = phutil_tag( 'input', array( 'type' => 'hidden', 'id' => $input_id, 'name' => $this->getName(), 'value' => $this->getValue(), )); $options = $this->getOptions(); $order = array(); $labels = array(); foreach ($options as $key => $values) { $order[$key] = array_keys($values); $labels[$key] = PhabricatorPolicyType::getPolicyTypeName($key); } $flat_options = array_mergev($options); $icons = array(); foreach (igroup($flat_options, 'icon') as $icon => $ignored) { $icons[$icon] = id(new PHUIIconView()) ->setIcon($icon); } if ($this->templatePHIDType) { $context_path = 'template/'.$this->templatePHIDType.'/'; } else { $object_phid = $this->object->getPHID(); if ($object_phid) { $context_path = 'object/'.$object_phid.'/'; } else { $object_type = phid_get_type($this->object->generatePHID()); $context_path = 'type/'.$object_type.'/'; } } Javelin::initBehavior( 'policy-control', array( 'controlID' => $control_id, 'inputID' => $input_id, 'options' => $flat_options, 'groups' => array_keys($options), 'order' => $order, - 'icons' => $icons, 'labels' => $labels, 'value' => $this->getValue(), 'capability' => $this->capability, 'editURI' => '/policy/edit/'.$context_path, - 'customPlaceholder' => $this->getCustomPolicyPlaceholder(), + 'customKey' => $this->getSelectCustomKey(), + 'projectKey' => $this->getSelectProjectKey(), 'disabled' => $this->getDisabled(), )); $selected = idx($flat_options, $this->getValue(), array()); $selected_icon = idx($selected, 'icon'); $selected_name = idx($selected, 'name'); $spaces_control = $this->buildSpacesControl(); return phutil_tag( 'div', array( ), array( $spaces_control, javelin_tag( 'a', array( 'class' => 'grey button dropdown has-icon policy-control', 'href' => '#', 'mustcapture' => true, 'sigil' => 'policy-control', 'id' => $control_id, ), array( $caret, javelin_tag( 'span', array( 'sigil' => 'policy-label', 'class' => 'phui-button-text', ), array( idx($icons, $selected_icon), $selected_name, )), )), $input, )); return AphrontFormSelectControl::renderSelectTag( $this->getValue(), $this->getOptions(), array( 'name' => $this->getName(), 'disabled' => $this->getDisabled() ? 'disabled' : null, 'id' => $this->getID(), )); } - private function getCustomPolicyPlaceholder() { - return 'custom:placeholder'; + public static function getSelectCustomKey() { + return 'select:custom'; + } + + public static function getSelectProjectKey() { + return 'select:project'; } private function buildSpacesControl() { if ($this->capability != PhabricatorPolicyCapability::CAN_VIEW) { return null; } if (!($this->object instanceof PhabricatorSpacesInterface)) { return null; } $viewer = $this->getUser(); if (!PhabricatorSpacesNamespaceQuery::getViewerSpacesExist($viewer)) { return null; } $space_phid = $this->getSpacePHID(); if ($space_phid === null) { $space_phid = $viewer->getDefaultSpacePHID(); } $select = AphrontFormSelectControl::renderSelectTag( $space_phid, PhabricatorSpacesNamespaceQuery::getSpaceOptionsForViewer( $viewer, $space_phid), array( 'disabled' => ($this->getDisabled() ? 'disabled' : null), 'name' => 'spacePHID', 'class' => 'aphront-space-select-control-knob', )); return $select; } } diff --git a/webroot/rsrc/js/application/policy/behavior-policy-control.js b/webroot/rsrc/js/application/policy/behavior-policy-control.js index 9696a09dc8..044b909352 100644 --- a/webroot/rsrc/js/application/policy/behavior-policy-control.js +++ b/webroot/rsrc/js/application/policy/behavior-policy-control.js @@ -1,138 +1,168 @@ /** * @provides javelin-behavior-policy-control * @requires javelin-behavior * javelin-dom * javelin-util * phuix-dropdown-menu * phuix-action-list-view * phuix-action-view * javelin-workflow + * phuix-icon-view * @javelin */ JX.behavior('policy-control', function(config) { var control = JX.$(config.controlID); var input = JX.$(config.inputID); var value = config.value; if (config.disabled) { JX.DOM.alterClass(control, 'disabled-control', true); JX.DOM.listen(control, 'click', null, function(e) { e.kill(); }); return; } var menu = new JX.PHUIXDropdownMenu(control) .setWidth(260) .setAlign('left'); menu.listen('open', function() { var list = new JX.PHUIXActionListView(); for (var ii = 0; ii < config.groups.length; ii++) { var group = config.groups[ii]; list.addItem( new JX.PHUIXActionView() .setName(config.labels[group]) .setLabel(true)); for (var jj = 0; jj < config.order[group].length; jj++) { var phid = config.order[group][jj]; var onselect; if (group == 'custom') { onselect = JX.bind(null, function(phid) { var uri = get_custom_uri(phid, config.capability); new JX.Workflow(uri) .setHandler(function(response) { if (!response.phid) { return; } replace_policy(phid, response.phid, response.info); select_policy(response.phid); }) .start(); }, phid); + } else if (phid == config.projectKey) { + onselect = JX.bind(null, function(phid) { + var uri = get_custom_uri(phid, config.capability); + + new JX.Workflow(uri) + .setHandler(function(response) { + if (!response.phid) { + return; + } + + add_policy(phid, response.phid, response.info); + select_policy(response.phid); + }) + .start(); + }, phid); } else { onselect = JX.bind(null, select_policy, phid); } var option = config.options[phid]; var item = new JX.PHUIXActionView() .setName(option.name) .setIcon(option.icon + ' darkgreytext') .setHandler(JX.bind(null, function(fn, e) { e.prevent(); menu.close(); fn(); }, onselect)); if (phid == value) { item.setSelected(true); } list.addItem(item); } } menu.setContent(list.getNode()); }); var select_policy = function(phid) { JX.DOM.setContent( JX.DOM.find(control, 'span', 'policy-label'), render_option(phid)); input.value = phid; value = phid; }; var render_option = function(phid, with_title) { var option = config.options[phid]; var name = option.name; if (with_title && (option.full != option.name)) { name = JX.$N('span', {title: option.full}, name); } - return [JX.$H(config.icons[option.icon]), name]; + return [render_icon(option.icon), name]; }; + var render_icon = function(icon) { + return new JX.PHUIXIconView() + .setIcon(icon) + .getNode(); + }; /** * Get the workflow URI to create or edit a policy with a given PHID. */ var get_custom_uri = function(phid, capability) { - var uri = config.editURI; - if (phid != config.customPlaceholder) { - uri += phid + '/'; - } - uri += '?capability=' + capability; - return uri; + return JX.$U(config.editURI + phid + '/') + .setQueryParam('capability', capability) + .toString(); }; /** * Replace an existing policy option with a new one. Used to swap out custom * policies after the user edits them. */ var replace_policy = function(old_phid, new_phid, info) { + return add_policy(old_phid, new_phid, info, true); + }; + + + /** + * Add a new policy above an existing one, optionally replacing it. + */ + var add_policy = function(old_phid, new_phid, info, replace) { + if (config.options[new_phid]) { + return; + } + config.options[new_phid] = info; + for (var k in config.order) { for (var ii = 0; ii < config.order[k].length; ii++) { if (config.order[k][ii] == old_phid) { - config.order[k][ii] = new_phid; + config.order[k].splice(ii, (replace ? 1 : 0), new_phid); return; } } } }; - });