@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
at recaptime-dev/main 398 lines 12 kB view raw
1<?php 2 3final class AphrontFormPolicyControl extends AphrontFormControl { 4 5 private $object; 6 private $capability; 7 private $policies; 8 private $spacePHID; 9 private $templatePHIDType; 10 private $templateObject; 11 12 public function setPolicyObject(PhabricatorPolicyInterface $object) { 13 $this->object = $object; 14 return $this; 15 } 16 17 /** 18 * @param array<PhabricatorPolicy> $policies 19 */ 20 public function setPolicies(array $policies) { 21 assert_instances_of($policies, PhabricatorPolicy::class); 22 $this->policies = $policies; 23 return $this; 24 } 25 26 public function setSpacePHID($space_phid) { 27 $this->spacePHID = $space_phid; 28 return $this; 29 } 30 31 public function getSpacePHID() { 32 return $this->spacePHID; 33 } 34 35 public function setTemplatePHIDType($type) { 36 $this->templatePHIDType = $type; 37 return $this; 38 } 39 40 public function setTemplateObject($object) { 41 $this->templateObject = $object; 42 return $this; 43 } 44 45 public function getSerializedValue() { 46 return json_encode(array( 47 $this->getValue(), 48 $this->getSpacePHID(), 49 )); 50 } 51 52 public function readSerializedValue($value) { 53 $decoded = phutil_json_decode($value); 54 $policy_value = $decoded[0]; 55 $space_phid = $decoded[1]; 56 $this->setValue($policy_value); 57 $this->setSpacePHID($space_phid); 58 return $this; 59 } 60 61 public function readValueFromDictionary(array $dictionary) { 62 // TODO: This is a little hacky but will only get us into trouble if we 63 // have multiple view policy controls in multiple paged form views on the 64 // same page, which seems unlikely. 65 $this->setSpacePHID(idx($dictionary, 'spacePHID')); 66 67 return parent::readValueFromDictionary($dictionary); 68 } 69 70 public function readValueFromRequest(AphrontRequest $request) { 71 // See note in readValueFromDictionary(). 72 $this->setSpacePHID($request->getStr('spacePHID')); 73 74 return parent::readValueFromRequest($request); 75 } 76 77 public function setCapability($capability) { 78 $this->capability = $capability; 79 80 $labels = array( 81 PhabricatorPolicyCapability::CAN_VIEW => pht('Visible To'), 82 PhabricatorPolicyCapability::CAN_EDIT => pht('Editable By'), 83 PhabricatorPolicyCapability::CAN_JOIN => pht('Joinable By'), 84 ); 85 86 if (isset($labels[$capability])) { 87 $label = $labels[$capability]; 88 } else { 89 $capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability); 90 if ($capobj) { 91 $label = $capobj->getCapabilityName(); 92 } else { 93 $label = pht('Capability "%s"', $capability); 94 } 95 } 96 97 $this->setLabel($label); 98 99 return $this; 100 } 101 102 protected function getCustomControlClass() { 103 return 'aphront-form-control-policy'; 104 } 105 106 protected function getOptions() { 107 $capability = $this->capability; 108 $policies = $this->policies; 109 $viewer = $this->getUser(); 110 111 // Check if we're missing the policy for the current control value. This 112 // is unusual, but can occur if the user is submitting a form and selected 113 // an unusual project as a policy but the change has not been saved yet. 114 $policy_map = mpull($policies, null, 'getPHID'); 115 $value = $this->getValue(); 116 if ($value && empty($policy_map[$value])) { 117 $handle = id(new PhabricatorHandleQuery()) 118 ->setViewer($viewer) 119 ->withPHIDs(array($value)) 120 ->executeOne(); 121 if ($handle->isComplete()) { 122 $policies[] = PhabricatorPolicy::newFromPolicyAndHandle( 123 $value, 124 $handle); 125 } 126 } 127 128 // Exclude object policies which don't make sense here. This primarily 129 // filters object policies associated from template capabilities (like 130 // "Default Task View Policy" being set to "Task Author") so they aren't 131 // made available on non-template capabilities (like "Can Bulk Edit"). 132 foreach ($policies as $key => $policy) { 133 if ($policy->getType() != PhabricatorPolicyType::TYPE_OBJECT) { 134 continue; 135 } 136 137 $rule = PhabricatorPolicyQuery::getObjectPolicyRule($policy->getPHID()); 138 if (!$rule) { 139 continue; 140 } 141 142 $target = nonempty($this->templateObject, $this->object); 143 if (!$rule->canApplyToObject($target)) { 144 unset($policies[$key]); 145 continue; 146 } 147 } 148 149 $options = array(); 150 foreach ($policies as $policy) { 151 if ($policy->getPHID() == PhabricatorPolicies::POLICY_PUBLIC) { 152 // Never expose "Public" for capabilities which don't support it. 153 $capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability); 154 if (!$capobj || !$capobj->shouldAllowPublicPolicySetting()) { 155 continue; 156 } 157 } 158 159 $options[$policy->getType()][$policy->getPHID()] = array( 160 'name' => $policy->getName(), 161 'full' => $policy->getName(), 162 'icon' => $policy->getIcon(), 163 'sort' => phutil_utf8_strtolower($policy->getName()), 164 ); 165 } 166 167 $type_project = PhabricatorPolicyType::TYPE_PROJECT; 168 169 // Make sure we have a "Projects" group before we adjust it. 170 if (empty($options[$type_project])) { 171 $options[$type_project] = array(); 172 } 173 174 $options[$type_project] = isort($options[$type_project], 'sort'); 175 176 $placeholder = id(new PhabricatorPolicy()) 177 ->setName(pht('Other Project...')) 178 ->setIcon('fa-search'); 179 180 $options[$type_project][$this->getSelectProjectKey()] = array( 181 'name' => $placeholder->getName(), 182 'full' => $placeholder->getName(), 183 'icon' => $placeholder->getIcon(), 184 ); 185 186 // If we were passed several custom policy options, throw away the ones 187 // which aren't the value for this capability. For example, an object might 188 // have a custom view policy and a custom edit policy. When we render 189 // the selector for "Can View", we don't want to show the "Can Edit" 190 // custom policy -- if we did, the menu would look like this: 191 // 192 // Custom 193 // Custom Policy 194 // Custom Policy 195 // 196 // ...where one is the "view" custom policy, and one is the "edit" custom 197 // policy. 198 199 $type_custom = PhabricatorPolicyType::TYPE_CUSTOM; 200 if (!empty($options[$type_custom])) { 201 $options[$type_custom] = array_select_keys( 202 $options[$type_custom], 203 array($this->getValue())); 204 } 205 206 // If there aren't any custom policies, add a placeholder policy so we 207 // render a menu item. This allows the user to switch to a custom policy. 208 209 if (empty($options[$type_custom])) { 210 $placeholder = new PhabricatorPolicy(); 211 $placeholder->setName(pht('Custom Policy...')); 212 $options[$type_custom][$this->getSelectCustomKey()] = array( 213 'name' => $placeholder->getName(), 214 'full' => $placeholder->getName(), 215 'icon' => $placeholder->getIcon(), 216 ); 217 } 218 219 $options = array_select_keys( 220 $options, 221 array( 222 PhabricatorPolicyType::TYPE_GLOBAL, 223 PhabricatorPolicyType::TYPE_OBJECT, 224 PhabricatorPolicyType::TYPE_USER, 225 PhabricatorPolicyType::TYPE_CUSTOM, 226 PhabricatorPolicyType::TYPE_PROJECT, 227 )); 228 229 return $options; 230 } 231 232 protected function renderInput() { 233 if (!$this->object) { 234 throw new PhutilInvalidStateException('setPolicyObject'); 235 } 236 if (!$this->capability) { 237 throw new PhutilInvalidStateException('setCapability'); 238 } 239 240 $policy = $this->object->getPolicy($this->capability); 241 if (!$policy) { 242 // TODO: Make this configurable. 243 $policy = PhabricatorPolicies::POLICY_USER; 244 } 245 246 if (!$this->getValue()) { 247 $this->setValue($policy); 248 } 249 250 if ($this->getID()) { 251 $control_id = $this->getID(); 252 } else { 253 $control_id = celerity_generate_unique_node_id(); 254 } 255 $input_id = celerity_generate_unique_node_id(); 256 257 $caret = phutil_tag( 258 'span', 259 array( 260 'class' => 'caret', 261 )); 262 263 $input = phutil_tag( 264 'input', 265 array( 266 'type' => 'hidden', 267 'id' => $input_id, 268 'name' => $this->getName(), 269 'value' => $this->getValue(), 270 )); 271 272 $options = $this->getOptions(); 273 274 $order = array(); 275 $labels = array(); 276 foreach ($options as $key => $values) { 277 $order[$key] = array_keys($values); 278 $labels[$key] = PhabricatorPolicyType::getPolicyTypeName($key); 279 } 280 281 $flat_options = array_mergev($options); 282 283 $icons = array(); 284 foreach (igroup($flat_options, 'icon') as $icon => $ignored) { 285 $icons[$icon] = id(new PHUIIconView()) 286 ->setIcon($icon); 287 } 288 289 if ($this->templatePHIDType) { 290 $context_path = 'template/'.$this->templatePHIDType.'/'; 291 } else { 292 $object_phid = $this->object->getPHID(); 293 if ($object_phid) { 294 $context_path = 'object/'.$object_phid.'/'; 295 } else { 296 $object_type = phid_get_type($this->object->generatePHID()); 297 $context_path = 'type/'.$object_type.'/'; 298 } 299 } 300 301 Javelin::initBehavior( 302 'policy-control', 303 array( 304 'controlID' => $control_id, 305 'inputID' => $input_id, 306 'options' => $flat_options, 307 'groups' => array_keys($options), 308 'order' => $order, 309 'labels' => $labels, 310 'value' => $this->getValue(), 311 'capability' => $this->capability, 312 'editURI' => '/policy/edit/'.$context_path, 313 'customKey' => $this->getSelectCustomKey(), 314 'projectKey' => $this->getSelectProjectKey(), 315 'disabled' => $this->getDisabled(), 316 )); 317 318 $selected = idx($flat_options, $this->getValue(), array()); 319 $selected_icon = idx($selected, 'icon'); 320 $selected_name = idx($selected, 'name'); 321 322 $spaces_control = $this->buildSpacesControl(); 323 324 return phutil_tag( 325 'div', 326 array( 327 ), 328 array( 329 $spaces_control, 330 javelin_tag( 331 'a', 332 array( 333 'class' => 'button button-grey dropdown has-icon has-text '. 334 'policy-control', 335 'href' => '#', 336 'mustcapture' => true, 337 'sigil' => 'policy-control', 338 'id' => $control_id, 339 ), 340 array( 341 $caret, 342 javelin_tag( 343 'span', 344 array( 345 'sigil' => 'policy-label', 346 'class' => 'phui-button-text', 347 ), 348 array( 349 idx($icons, $selected_icon), 350 $selected_name, 351 )), 352 )), 353 $input, 354 )); 355 } 356 357 public static function getSelectCustomKey() { 358 return 'select:custom'; 359 } 360 361 public static function getSelectProjectKey() { 362 return 'select:project'; 363 } 364 365 private function buildSpacesControl() { 366 if ($this->capability != PhabricatorPolicyCapability::CAN_VIEW) { 367 return null; 368 } 369 370 if (!($this->object instanceof PhabricatorSpacesInterface)) { 371 return null; 372 } 373 374 $viewer = $this->getUser(); 375 if (!PhabricatorSpacesNamespaceQuery::getViewerSpacesExist($viewer)) { 376 return null; 377 } 378 379 $space_phid = $this->getSpacePHID(); 380 if ($space_phid === null) { 381 $space_phid = $viewer->getDefaultSpacePHID(); 382 } 383 384 $select = AphrontFormSelectControl::renderSelectTag( 385 $space_phid, 386 PhabricatorSpacesNamespaceQuery::getSpaceOptionsForViewer( 387 $viewer, 388 $space_phid), 389 array( 390 'disabled' => ($this->getDisabled() ? 'disabled' : null), 391 'name' => 'spacePHID', 392 'class' => 'aphront-space-select-control-knob', 393 )); 394 395 return $select; 396 } 397 398}