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