@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
3
4/**
5 * @task fields Managing Fields
6 * @task text Display Text
7 * @task config Edit Engine Configuration
8 * @task uri Managing URIs
9 * @task load Creating and Loading Objects
10 * @task web Responding to Web Requests
11 * @task edit Responding to Edit Requests
12 * @task http Responding to HTTP Parameter Requests
13 * @task conduit Responding to Conduit Requests
14 */
15abstract class PhabricatorEditEngine
16 extends Phobject
17 implements PhabricatorPolicyInterface {
18
19 const EDITENGINECONFIG_DEFAULT = 'default';
20
21 const SUBTYPE_DEFAULT = 'default';
22
23 private $viewer;
24 private $controller;
25 private $isCreate;
26 private $editEngineConfiguration;
27 private $contextParameters = array();
28 private $targetObject;
29 private $page;
30 private $pages;
31 private $navigation;
32
33 final public function setViewer(PhabricatorUser $viewer) {
34 $this->viewer = $viewer;
35 return $this;
36 }
37
38 final public function getViewer() {
39 return $this->viewer;
40 }
41
42 final public function setController(PhabricatorController $controller) {
43 $this->controller = $controller;
44 $this->setViewer($controller->getViewer());
45 return $this;
46 }
47
48 final public function getController() {
49 return $this->controller;
50 }
51
52 final public function getEngineKey() {
53 $key = $this->getPhobjectClassConstant('ENGINECONST', 64);
54 if (strpos($key, '/') !== false) {
55 throw new Exception(
56 pht(
57 'EditEngine ("%s") contains an invalid key character "/".',
58 get_class($this)));
59 }
60 return $key;
61 }
62
63 final public function getApplication() {
64 $app_class = $this->getEngineApplicationClass();
65 return PhabricatorApplication::getByClass($app_class);
66 }
67
68 final public function addContextParameter($key) {
69 $this->contextParameters[] = $key;
70 return $this;
71 }
72
73 public function isEngineConfigurable() {
74 return true;
75 }
76
77 public function isEngineExtensible() {
78 return true;
79 }
80
81 /**
82 * Whether this EditEngine creates by default an entry in the Favorites
83 * dropdown in the top bar to use the Default Form for this EditEngine
84 *
85 * @return bool
86 */
87 public function isDefaultQuickCreateEngine() {
88 return false;
89 }
90
91 public function getDefaultQuickCreateFormKeys() {
92 $keys = array();
93
94 if ($this->isDefaultQuickCreateEngine()) {
95 $keys[] = self::EDITENGINECONFIG_DEFAULT;
96 }
97
98 foreach ($keys as $idx => $key) {
99 $keys[$idx] = $this->getEngineKey().'/'.$key;
100 }
101
102 return $keys;
103 }
104
105 /**
106 * Split the Full Key into its Edit Engine Key and its Form Key
107 *
108 * @param string $full_key 'Edit Engine Key/Form Key' string, e.g.
109 * 'macro.image/default' or 'maniphest.task/5'
110 * @return array<string,string> Edit Engine Key and Form Key
111 */
112 public static function splitFullKey($full_key) {
113 return explode('/', $full_key, 2);
114 }
115
116 public function getQuickCreateOrderVector() {
117 return id(new PhutilSortVector())
118 ->addString($this->getObjectCreateShortText());
119 }
120
121 /**
122 * Force the engine to edit a particular object.
123 */
124 public function setTargetObject($target_object) {
125 $this->targetObject = $target_object;
126 return $this;
127 }
128
129 public function getTargetObject() {
130 return $this->targetObject;
131 }
132
133 public function setNavigation(AphrontSideNavFilterView $navigation) {
134 $this->navigation = $navigation;
135 return $this;
136 }
137
138 public function getNavigation() {
139 return $this->navigation;
140 }
141
142
143/* -( Managing Fields )---------------------------------------------------- */
144
145
146 abstract public function getEngineApplicationClass();
147 abstract protected function buildCustomEditFields($object);
148
149 public function getFieldsForConfig(
150 PhabricatorEditEngineConfiguration $config) {
151
152 $object = $this->newEditableObject();
153
154 $this->editEngineConfiguration = $config;
155
156 // This is mostly making sure that we fill in default values.
157 $this->setIsCreate(true);
158
159 return $this->buildEditFields($object);
160 }
161
162 final protected function buildEditFields($object) {
163 $viewer = $this->getViewer();
164
165 $fields = $this->buildCustomEditFields($object);
166
167 foreach ($fields as $field) {
168 $field
169 ->setViewer($viewer)
170 ->setObject($object);
171 }
172
173 $fields = mpull($fields, null, 'getKey');
174
175 if ($this->isEngineExtensible()) {
176 $extensions = PhabricatorEditEngineExtension::getAllEnabledExtensions();
177 } else {
178 $extensions = array();
179 }
180
181 // See T13248. Create a template object to provide to extensions. We
182 // adjust the template to have the intended subtype, so that extensions
183 // may change behavior based on the form subtype.
184
185 $template_object = clone $object;
186 if ($this->getIsCreate()) {
187 if ($this->supportsSubtypes()) {
188 $config = $this->getEditEngineConfiguration();
189 $subtype = $config->getSubtype();
190 $template_object->setSubtype($subtype);
191 }
192 }
193
194 foreach ($extensions as $extension) {
195 $extension->setViewer($viewer);
196
197 if (!$extension->supportsObject($this, $template_object)) {
198 continue;
199 }
200
201 $extension_fields = $extension->buildCustomEditFields(
202 $this,
203 $template_object);
204
205 // TODO: Validate this in more detail with a more tailored error.
206 assert_instances_of($extension_fields, PhabricatorEditField::class);
207
208 foreach ($extension_fields as $field) {
209 $field
210 ->setViewer($viewer)
211 ->setObject($object);
212
213 $group_key = $field->getBulkEditGroupKey();
214 if ($group_key === null) {
215 $field->setBulkEditGroupKey('extension');
216 }
217 }
218
219 $extension_fields = mpull($extension_fields, null, 'getKey');
220
221 foreach ($extension_fields as $key => $field) {
222 $fields[$key] = $field;
223 }
224 }
225
226 $config = $this->getEditEngineConfiguration();
227 $fields = $this->willConfigureFields($object, $fields);
228 $fields = $config->applyConfigurationToFields($this, $object, $fields);
229
230 $fields = $this->applyPageToFields($object, $fields);
231
232 return $fields;
233 }
234
235 protected function willConfigureFields($object, array $fields) {
236 return $fields;
237 }
238
239 final public function supportsSubtypes() {
240 try {
241 $object = $this->newEditableObject();
242 } catch (Exception $ex) {
243 return false;
244 }
245
246 return ($object instanceof PhabricatorEditEngineSubtypeInterface);
247 }
248
249 final public function newSubtypeMap() {
250 return $this->newEditableObject()->newEditEngineSubtypeMap();
251 }
252
253
254/* -( Display Text )------------------------------------------------------- */
255
256
257 /**
258 * @task text
259 */
260 abstract public function getEngineName();
261
262
263 /**
264 * @task text
265 */
266 abstract protected function getObjectCreateTitleText($object);
267
268 /**
269 * @task text
270 */
271 protected function getFormHeaderText($object) {
272 $config = $this->getEditEngineConfiguration();
273 return $config->getName();
274 }
275
276 /**
277 * @task text
278 */
279 abstract protected function getObjectEditTitleText($object);
280
281
282 /**
283 * @task text
284 */
285 abstract protected function getObjectCreateShortText();
286
287
288 /**
289 * @task text
290 */
291 abstract protected function getObjectName();
292
293
294 /**
295 * @task text
296 */
297 abstract protected function getObjectEditShortText($object);
298
299
300 /**
301 * @task text
302 */
303 protected function getObjectCreateButtonText($object) {
304 return $this->getObjectCreateTitleText($object);
305 }
306
307
308 /**
309 * @task text
310 */
311 protected function getObjectEditButtonText($object) {
312 return pht('Save Changes');
313 }
314
315
316 /**
317 * @task text
318 */
319 protected function getCommentViewSeriousHeaderText($object) {
320 return pht('Take Action');
321 }
322
323
324 /**
325 * @task text
326 */
327 protected function getCommentViewSeriousButtonText($object) {
328 return pht('Submit');
329 }
330
331
332 /**
333 * @task text
334 */
335 protected function getCommentViewHeaderText($object) {
336 return $this->getCommentViewSeriousHeaderText($object);
337 }
338
339
340 /**
341 * @task text
342 */
343 protected function getCommentViewButtonText($object) {
344 return $this->getCommentViewSeriousButtonText($object);
345 }
346
347
348 /**
349 * @task text
350 */
351 protected function getPageHeader($object) {
352 return null;
353 }
354
355 /**
356 * Set default placeholder plain text in the comment textarea of the engine.
357 * To be overwritten by conditions defined in the child EditEngine class.
358 *
359 * @param object $object Object in which the comment textarea is displayed.
360 * @return string Placeholder text to display in the comment textarea.
361 * @task text
362 */
363 public function getCommentFieldPlaceholderText($object) {
364 return '';
365 }
366
367 /**
368 * Return a human-readable header describing what this engine is used to do,
369 * like "Configure Maniphest Task Forms".
370 *
371 * @return string Human-readable description of the engine.
372 * @task text
373 */
374 abstract public function getSummaryHeader();
375
376
377 /**
378 * Return a human-readable summary of what this engine is used to do.
379 *
380 * @return string Human-readable description of the engine.
381 * @task text
382 */
383 abstract public function getSummaryText();
384
385
386
387
388/* -( Edit Engine Configuration )------------------------------------------ */
389
390
391 protected function supportsEditEngineConfiguration() {
392 return true;
393 }
394
395 final protected function getEditEngineConfiguration() {
396 return $this->editEngineConfiguration;
397 }
398
399 public function newConfigurationQuery() {
400 return id(new PhabricatorEditEngineConfigurationQuery())
401 ->setViewer($this->getViewer())
402 ->withEngineKeys(array($this->getEngineKey()));
403 }
404
405 private function loadEditEngineConfigurationWithQuery(
406 PhabricatorEditEngineConfigurationQuery $query,
407 $sort_method) {
408
409 if ($sort_method) {
410 $results = $query->execute();
411 $results = msort($results, $sort_method);
412 $result = head($results);
413 } else {
414 $result = $query->executeOne();
415 }
416
417 if (!$result) {
418 return null;
419 }
420
421 $this->editEngineConfiguration = $result;
422 return $result;
423 }
424
425 private function loadEditEngineConfigurationWithIdentifier($identifier) {
426 $query = $this->newConfigurationQuery()
427 ->withIdentifiers(array($identifier));
428
429 return $this->loadEditEngineConfigurationWithQuery($query, null);
430 }
431
432 private function loadDefaultConfiguration() {
433 $query = $this->newConfigurationQuery()
434 ->withIdentifiers(
435 array(
436 self::EDITENGINECONFIG_DEFAULT,
437 ))
438 ->withIgnoreDatabaseConfigurations(true);
439
440 return $this->loadEditEngineConfigurationWithQuery($query, null);
441 }
442
443 private function loadDefaultCreateConfiguration() {
444 $query = $this->newConfigurationQuery()
445 ->withIsDefault(true)
446 ->withIsDisabled(false);
447
448 return $this->loadEditEngineConfigurationWithQuery(
449 $query,
450 'getCreateSortKey');
451 }
452
453 public function loadDefaultEditConfiguration($object) {
454 $query = $this->newConfigurationQuery()
455 ->withIsEdit(true)
456 ->withIsDisabled(false);
457
458 // If this object supports subtyping, we edit it with a form of the same
459 // subtype: so "bug" tasks get edited with "bug" forms.
460 if ($object instanceof PhabricatorEditEngineSubtypeInterface) {
461 $query->withSubtypes(
462 array(
463 $object->getEditEngineSubtype(),
464 ));
465 }
466
467 return $this->loadEditEngineConfigurationWithQuery(
468 $query,
469 'getEditSortKey');
470 }
471
472 final public function getBuiltinEngineConfigurations() {
473 $configurations = $this->newBuiltinEngineConfigurations();
474
475 if (!$configurations) {
476 throw new Exception(
477 pht(
478 'EditEngine ("%s") returned no builtin engine configurations, but '.
479 'an edit engine must have at least one configuration.',
480 get_class($this)));
481 }
482
483 assert_instances_of(
484 $configurations,
485 PhabricatorEditEngineConfiguration::class);
486
487 $has_default = false;
488 foreach ($configurations as $config) {
489 if ($config->getBuiltinKey() == self::EDITENGINECONFIG_DEFAULT) {
490 $has_default = true;
491 }
492 }
493
494 if (!$has_default) {
495 $first = head($configurations);
496 if (!$first->getBuiltinKey()) {
497 $first
498 ->setBuiltinKey(self::EDITENGINECONFIG_DEFAULT)
499 ->setIsDefault(true)
500 ->setIsEdit(true);
501
502 $first_name = $first->getName();
503
504 if ($first_name === null || $first_name === '') {
505 $first->setName($this->getObjectCreateShortText());
506 }
507 } else {
508 throw new Exception(
509 pht(
510 'EditEngine ("%s") returned builtin engine configurations, '.
511 'but none are marked as default and the first configuration has '.
512 'a different builtin key already. Mark a builtin as default or '.
513 'omit the key from the first configuration',
514 get_class($this)));
515 }
516 }
517
518 $builtins = array();
519 foreach ($configurations as $key => $config) {
520 $builtin_key = $config->getBuiltinKey();
521
522 if ($builtin_key === null) {
523 throw new Exception(
524 pht(
525 'EditEngine ("%s") returned builtin engine configurations, '.
526 'but one (with key "%s") is missing a builtin key. Provide a '.
527 'builtin key for each configuration (you can omit it from the '.
528 'first configuration in the list to automatically assign the '.
529 'default key).',
530 get_class($this),
531 $key));
532 }
533
534 if (isset($builtins[$builtin_key])) {
535 throw new Exception(
536 pht(
537 'EditEngine ("%s") returned builtin engine configurations, '.
538 'but at least two specify the same builtin key ("%s"). Engines '.
539 'must have unique builtin keys.',
540 get_class($this),
541 $builtin_key));
542 }
543
544 $builtins[$builtin_key] = $config;
545 }
546
547
548 return $builtins;
549 }
550
551 protected function newBuiltinEngineConfigurations() {
552 return array(
553 $this->newConfiguration(),
554 );
555 }
556
557 final protected function newConfiguration() {
558 return PhabricatorEditEngineConfiguration::initializeNewConfiguration(
559 $this->getViewer(),
560 $this);
561 }
562
563
564/* -( Managing URIs )------------------------------------------------------ */
565
566
567 /**
568 * @task uri
569 */
570 abstract protected function getObjectViewURI($object);
571
572
573 /**
574 * @task uri
575 */
576 protected function getObjectCreateCancelURI($object) {
577 return $this->getApplication()->getApplicationURI();
578 }
579
580
581 /**
582 * @task uri
583 */
584 protected function getEditorURI() {
585 return $this->getApplication()->getApplicationURI('edit/');
586 }
587
588
589 /**
590 * @task uri
591 */
592 protected function getObjectEditCancelURI($object) {
593 return $this->getObjectViewURI($object);
594 }
595
596 /**
597 * @task uri
598 */
599 public function getCreateURI($form_key) {
600 try {
601 $create_uri = $this->getEditURI(null, "form/{$form_key}/");
602 } catch (Exception $ex) {
603 $create_uri = null;
604 }
605
606 return $create_uri;
607 }
608
609 /**
610 * @task uri
611 */
612 public function getEditURI($object = null, $path = null) {
613 $parts = array();
614
615 $parts[] = $this->getEditorURI();
616
617 if ($object && $object->getID()) {
618 $parts[] = $object->getID().'/';
619 }
620
621 if ($path !== null) {
622 $parts[] = $path;
623 }
624
625 return implode('', $parts);
626 }
627
628 public function getEffectiveObjectViewURI($object) {
629 if ($this->getIsCreate()) {
630 return $this->getObjectViewURI($object);
631 }
632
633 $page = $this->getSelectedPage();
634 if ($page) {
635 $view_uri = $page->getViewURI();
636 if ($view_uri !== null) {
637 return $view_uri;
638 }
639 }
640
641 return $this->getObjectViewURI($object);
642 }
643
644 public function getEffectiveObjectEditDoneURI($object) {
645 return $this->getEffectiveObjectViewURI($object);
646 }
647
648 public function getEffectiveObjectEditCancelURI($object) {
649 $page = $this->getSelectedPage();
650 if ($page) {
651 $view_uri = $page->getViewURI();
652 if ($view_uri !== null) {
653 return $view_uri;
654 }
655 }
656
657 return $this->getObjectEditCancelURI($object);
658 }
659
660
661/* -( Creating and Loading Objects )--------------------------------------- */
662
663
664 /**
665 * Initialize a new object for creation.
666 *
667 * @return object Newly initialized object.
668 * @task load
669 */
670 abstract protected function newEditableObject();
671
672
673 /**
674 * Build an empty query for objects.
675 *
676 * @return PhabricatorPolicyAwareQuery Query.
677 * @task load
678 */
679 abstract protected function newObjectQuery();
680
681
682 /**
683 * Test if this workflow is creating a new object or editing an existing one.
684 *
685 * @return bool True if a new object is being created.
686 * @task load
687 */
688 final public function getIsCreate() {
689 return $this->isCreate;
690 }
691
692 /**
693 * Initialize a new object for object creation via Conduit.
694 *
695 * @return object Newly initialized object.
696 * @param array<mixed> $raw_xactions Raw transactions.
697 * @task load
698 */
699 protected function newEditableObjectFromConduit(array $raw_xactions) {
700 return $this->newEditableObject();
701 }
702
703 /**
704 * Initialize a new object for documentation creation.
705 *
706 * @return object Newly initialized object.
707 * @task load
708 */
709 protected function newEditableObjectForDocumentation() {
710 return $this->newEditableObject();
711 }
712
713 /**
714 * Flag this workflow as a create or edit.
715 *
716 * @param bool $is_create True if this is a create workflow.
717 * @return $this
718 * @task load
719 */
720 private function setIsCreate($is_create) {
721 $this->isCreate = $is_create;
722 return $this;
723 }
724
725
726 /**
727 * Try to load an object by ID, PHID, or monogram. This is done primarily
728 * to make Conduit a little easier to use.
729 *
730 * @param int|string $identifier ID, PHID, or monogram.
731 * @param list<string> $capabilities (optional) List of required capability
732 * constants, or omit for defaults.
733 * @return object Corresponding editable object.
734 * @task load
735 */
736 private function newObjectFromIdentifier(
737 $identifier,
738 array $capabilities = array()) {
739 if (is_int($identifier) || ctype_digit($identifier)) {
740 $object = $this->newObjectFromID($identifier, $capabilities);
741
742 if (!$object) {
743 throw new Exception(
744 pht(
745 'No object exists with ID "%s".',
746 $identifier));
747 }
748
749 return $object;
750 }
751
752 $type_unknown = PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN;
753 if (phid_get_type($identifier) != $type_unknown) {
754 $object = $this->newObjectFromPHID($identifier, $capabilities);
755
756 if (!$object) {
757 throw new Exception(
758 pht(
759 'No object exists with PHID "%s".',
760 $identifier));
761 }
762
763 return $object;
764 }
765
766 $target = id(new PhabricatorObjectQuery())
767 ->setViewer($this->getViewer())
768 ->withNames(array($identifier))
769 ->executeOne();
770 if (!$target) {
771 throw new Exception(
772 pht(
773 'Monogram "%s" does not identify a valid object.',
774 $identifier));
775 }
776
777 $expect = $this->newEditableObject();
778 $expect_class = get_class($expect);
779 $target_class = get_class($target);
780 if ($expect_class !== $target_class) {
781 throw new Exception(
782 pht(
783 'Monogram "%s" identifies an object of the wrong type. Loaded '.
784 'object has class "%s", but this editor operates on objects of '.
785 'type "%s".',
786 $identifier,
787 $target_class,
788 $expect_class));
789 }
790
791 // Load the object by PHID using this engine's standard query. This makes
792 // sure it's really valid, goes through standard policy check logic, and
793 // picks up any `need...()` clauses we want it to load with.
794
795 $object = $this->newObjectFromPHID($target->getPHID(), $capabilities);
796 if (!$object) {
797 throw new Exception(
798 pht(
799 'Failed to reload object identified by monogram "%s" when '.
800 'querying by PHID.',
801 $identifier));
802 }
803
804 return $object;
805 }
806
807 /**
808 * Load an object by ID.
809 *
810 * @param int $id Object ID.
811 * @param list<string> $capabilities (optional) List of required capability
812 * constants, or omit for defaults.
813 * @return object|null Object, or null if no such object exists.
814 * @task load
815 */
816 private function newObjectFromID($id, array $capabilities = array()) {
817 $query = $this->newObjectQuery()
818 ->withIDs(array($id));
819
820 return $this->newObjectFromQuery($query, $capabilities);
821 }
822
823
824 /**
825 * Load an object by PHID.
826 *
827 * @param string $phid Object PHID.
828 * @param list<string> $capabilities (optional) List of required capability
829 * constants, or omit for defaults.
830 * @return object|null Object, or null if no such object exists.
831 * @task load
832 */
833 private function newObjectFromPHID($phid, array $capabilities = array()) {
834 $query = $this->newObjectQuery()
835 ->withPHIDs(array($phid));
836
837 return $this->newObjectFromQuery($query, $capabilities);
838 }
839
840
841 /**
842 * Load an object given a configured query.
843 *
844 * @param PhabricatorPolicyAwareQuery $query Configured query.
845 * @param list<string> $capabilities (optional) List of required capability
846 * constants, or omit for defaults.
847 * @return object|null Object, or null if no such object exists.
848 * @task load
849 */
850 private function newObjectFromQuery(
851 PhabricatorPolicyAwareQuery $query,
852 array $capabilities = array()) {
853
854 $viewer = $this->getViewer();
855
856 if (!$capabilities) {
857 $capabilities = array(
858 PhabricatorPolicyCapability::CAN_VIEW,
859 PhabricatorPolicyCapability::CAN_EDIT,
860 );
861 }
862
863 $object = $query
864 ->setViewer($viewer)
865 ->requireCapabilities($capabilities)
866 ->executeOne();
867 if (!$object) {
868 return null;
869 }
870
871 return $object;
872 }
873
874
875 /**
876 * Verify that an object is appropriate for editing.
877 *
878 * @param PhabricatorApplicationTransactionInterface $object Loaded value.
879 * @return void
880 * @task load
881 */
882 private function validateObject($object) {
883 if (!$object || !is_object($object)) {
884 throw new Exception(
885 pht(
886 'EditEngine "%s" created or loaded an invalid object: object must '.
887 'actually be an object, but is of some other type ("%s").',
888 get_class($this),
889 gettype($object)));
890 }
891
892 if (!($object instanceof PhabricatorApplicationTransactionInterface)) {
893 throw new Exception(
894 pht(
895 'EditEngine "%s" created or loaded an invalid object: object (of '.
896 'class "%s") must implement "%s", but does not.',
897 get_class($this),
898 get_class($object),
899 'PhabricatorApplicationTransactionInterface'));
900 }
901 }
902
903
904/* -( Responding to Web Requests )----------------------------------------- */
905
906
907 final public function buildResponse() {
908 $viewer = $this->getViewer();
909 $controller = $this->getController();
910 $request = $controller->getRequest();
911
912 $action = $this->getEditAction();
913
914 $capabilities = array();
915 $use_default = false;
916 $require_create = true;
917 switch ($action) {
918 case 'comment':
919 $capabilities = array(
920 PhabricatorPolicyCapability::CAN_VIEW,
921 );
922 $use_default = true;
923 break;
924 case 'parameters':
925 $use_default = true;
926 break;
927 case 'nodefault':
928 case 'nocreate':
929 case 'nomanage':
930 $require_create = false;
931 break;
932 default:
933 break;
934 }
935
936 $object = $this->getTargetObject();
937 if (!$object) {
938 $id = $request->getURIData('id');
939
940 if ($id) {
941 $this->setIsCreate(false);
942 $object = $this->newObjectFromID($id, $capabilities);
943 if (!$object) {
944 return new Aphront404Response();
945 }
946 } else {
947 // Make sure the viewer has permission to create new objects of
948 // this type if we're going to create a new object.
949 if ($require_create) {
950 $this->requireCreateCapability();
951 }
952
953 $this->setIsCreate(true);
954 $object = $this->newEditableObject();
955 }
956 } else {
957 $id = $object->getID();
958 }
959
960 $this->validateObject($object);
961
962 if ($use_default) {
963 $config = $this->loadDefaultConfiguration();
964 if (!$config) {
965 return new Aphront404Response();
966 }
967 } else {
968 $form_key = $request->getURIData('formKey');
969 if (phutil_nonempty_string($form_key)) {
970 $config = $this->loadEditEngineConfigurationWithIdentifier($form_key);
971
972 if (!$config) {
973 return new Aphront404Response();
974 }
975
976 if ($id && !$config->getIsEdit()) {
977 return $this->buildNotEditFormRespose($object, $config);
978 }
979 } else {
980 if ($id) {
981 $config = $this->loadDefaultEditConfiguration($object);
982 if (!$config) {
983 return $this->buildNoEditResponse($object);
984 }
985 } else {
986 $config = $this->loadDefaultCreateConfiguration();
987 if (!$config) {
988 return $this->buildNoCreateResponse($object);
989 }
990 }
991 }
992 }
993
994 if ($config->getIsDisabled()) {
995 return $this->buildDisabledFormResponse($object, $config);
996 }
997
998 $page_key = $request->getURIData('pageKey');
999 if (!phutil_nonempty_string($page_key)) {
1000 $pages = $this->getPages($object);
1001 if ($pages) {
1002 $page_key = head_key($pages);
1003 }
1004 }
1005
1006 if (phutil_nonempty_string($page_key)) {
1007 $page = $this->selectPage($object, $page_key);
1008 if (!$page) {
1009 return new Aphront404Response();
1010 }
1011 }
1012
1013 switch ($action) {
1014 case 'parameters':
1015 return $this->buildParametersResponse($object);
1016 case 'nodefault':
1017 return $this->buildNoDefaultResponse($object);
1018 case 'nocreate':
1019 return $this->buildNoCreateResponse($object);
1020 case 'nomanage':
1021 return $this->buildNoManageResponse($object);
1022 case 'comment':
1023 return $this->buildCommentResponse($object);
1024 default:
1025 return $this->buildEditResponse($object);
1026 }
1027 }
1028
1029 private function buildCrumbs($object, $final = false) {
1030 $controller = $this->getController();
1031
1032 $crumbs = $controller->buildApplicationCrumbsForEditEngine();
1033 if ($this->getIsCreate()) {
1034 $create_text = $this->getObjectCreateShortText();
1035 if ($final) {
1036 $crumbs->addTextCrumb($create_text);
1037 } else {
1038 $edit_uri = $this->getEditURI($object);
1039 $crumbs->addTextCrumb($create_text, $edit_uri);
1040 }
1041 } else {
1042 $crumbs->addTextCrumb(
1043 $this->getObjectEditShortText($object),
1044 $this->getEffectiveObjectViewURI($object));
1045
1046 $edit_text = pht('Edit');
1047 if ($final) {
1048 $crumbs->addTextCrumb($edit_text);
1049 } else {
1050 $edit_uri = $this->getEditURI($object);
1051 $crumbs->addTextCrumb($edit_text, $edit_uri);
1052 }
1053 }
1054
1055 return $crumbs;
1056 }
1057
1058 private function buildEditResponse($object) {
1059 $viewer = $this->getViewer();
1060 $controller = $this->getController();
1061 $request = $controller->getRequest();
1062
1063 $fields = $this->buildEditFields($object);
1064 $template = $object->getApplicationTransactionTemplate();
1065
1066 $page_state = new PhabricatorEditEnginePageState();
1067
1068 if ($this->getIsCreate()) {
1069 $cancel_uri = $this->getObjectCreateCancelURI($object);
1070 $submit_button = $this->getObjectCreateButtonText($object);
1071
1072 $page_state->setIsCreate(true);
1073 } else {
1074 $cancel_uri = $this->getEffectiveObjectEditCancelURI($object);
1075 $submit_button = $this->getObjectEditButtonText($object);
1076 }
1077
1078 $config = $this->getEditEngineConfiguration()
1079 ->attachEngine($this);
1080
1081 // NOTE: Don't prompt users to override locks when creating objects,
1082 // even if the default settings would create a locked object.
1083
1084 $can_interact = PhabricatorPolicyFilter::canInteract($viewer, $object);
1085 if (!$can_interact &&
1086 !$this->getIsCreate() &&
1087 !$request->getBool('editEngine') &&
1088 !$request->getBool('overrideLock')) {
1089
1090 $lock = PhabricatorEditEngineLock::newForObject($viewer, $object);
1091
1092 $dialog = $this->getController()
1093 ->newDialog()
1094 ->addHiddenInput('overrideLock', true)
1095 ->setDisableWorkflowOnSubmit(true)
1096 ->addCancelButton($cancel_uri);
1097
1098 return $lock->willPromptUserForLockOverrideWithDialog($dialog);
1099 }
1100
1101 $validation_exception = null;
1102 if ($request->isFormOrHisecPost() && $request->getBool('editEngine')) {
1103 $page_state->setIsSubmit(true);
1104
1105 $submit_fields = $fields;
1106
1107 foreach ($submit_fields as $key => $field) {
1108 if (!$field->shouldGenerateTransactionsFromSubmit()) {
1109 unset($submit_fields[$key]);
1110 continue;
1111 }
1112 }
1113
1114 // Before we read the submitted values, store a copy of what we would
1115 // use if the form was empty so we can figure out which transactions are
1116 // just setting things to their default values for the current form.
1117 $defaults = array();
1118 foreach ($submit_fields as $key => $field) {
1119 $defaults[$key] = $field->getValueForTransaction();
1120 }
1121
1122 foreach ($submit_fields as $key => $field) {
1123 $field->setIsSubmittedForm(true);
1124
1125 if (!$field->shouldReadValueFromSubmit()) {
1126 continue;
1127 }
1128
1129 $field->readValueFromSubmit($request);
1130 }
1131
1132 $xactions = array();
1133
1134 if ($this->getIsCreate()) {
1135 $xactions[] = id(clone $template)
1136 ->setTransactionType(PhabricatorTransactions::TYPE_CREATE);
1137
1138 if ($this->supportsSubtypes()) {
1139 $xactions[] = id(clone $template)
1140 ->setTransactionType(PhabricatorTransactions::TYPE_SUBTYPE)
1141 ->setNewValue($config->getSubtype());
1142 }
1143 }
1144
1145 foreach ($submit_fields as $key => $field) {
1146 $field_value = $field->getValueForTransaction();
1147
1148 $type_xactions = $field->generateTransactions(
1149 clone $template,
1150 array(
1151 'value' => $field_value,
1152 ));
1153
1154 foreach ($type_xactions as $type_xaction) {
1155 $default = $defaults[$key];
1156
1157 if ($default === $field->getValueForTransaction()) {
1158 $type_xaction->setIsDefaultTransaction(true);
1159 }
1160
1161 $xactions[] = $type_xaction;
1162 }
1163 }
1164
1165 $editor = $object->getApplicationTransactionEditor()
1166 ->setActor($viewer)
1167 ->setContentSourceFromRequest($request)
1168 ->setCancelURI($cancel_uri)
1169 ->setContinueOnNoEffect(true);
1170
1171 try {
1172 $xactions = $this->willApplyTransactions($object, $xactions);
1173
1174 $editor->applyTransactions($object, $xactions);
1175
1176 $this->didApplyTransactions($object, $xactions);
1177
1178 return $this->newEditResponse($request, $object, $xactions);
1179 } catch (PhabricatorApplicationTransactionValidationException $ex) {
1180 $validation_exception = $ex;
1181
1182 foreach ($fields as $field) {
1183 $message = $this->getValidationExceptionShortMessage($ex, $field);
1184 if ($message === null) {
1185 continue;
1186 }
1187
1188 $field->setControlError($message);
1189 }
1190
1191 $page_state->setIsError(true);
1192 }
1193 } else {
1194 if ($this->getIsCreate()) {
1195 $template = $request->getStr('template');
1196
1197 if (phutil_nonempty_string($template)) {
1198 $template_object = $this->newObjectFromIdentifier(
1199 $template,
1200 array(
1201 PhabricatorPolicyCapability::CAN_VIEW,
1202 ));
1203 if (!$template_object) {
1204 return new Aphront404Response();
1205 }
1206 } else {
1207 $template_object = null;
1208 }
1209
1210 if ($template_object) {
1211 $copy_fields = $this->buildEditFields($template_object);
1212 $copy_fields = mpull($copy_fields, null, 'getKey');
1213 foreach ($copy_fields as $copy_key => $copy_field) {
1214 if (!$copy_field->getIsCopyable()) {
1215 unset($copy_fields[$copy_key]);
1216 }
1217 }
1218 } else {
1219 $copy_fields = array();
1220 }
1221
1222 foreach ($fields as $field) {
1223 if (!$field->shouldReadValueFromRequest()) {
1224 continue;
1225 }
1226
1227 $field_key = $field->getKey();
1228 if ($field_key && isset($copy_fields[$field_key])) {
1229 $field->readValueFromField($copy_fields[$field_key]);
1230 }
1231
1232 $field->readValueFromRequest($request);
1233 }
1234 }
1235 }
1236
1237 $action_button = $this->buildEditFormActionButton($object);
1238
1239 if ($this->getIsCreate()) {
1240 $header_text = $this->getFormHeaderText($object);
1241 } else {
1242 $header_text = $this->getObjectEditTitleText($object);
1243 }
1244
1245 $show_preview = !$request->isAjax();
1246
1247 if ($show_preview) {
1248 $previews = array();
1249 foreach ($fields as $field) {
1250 $preview = $field->getPreviewPanel();
1251 if (!$preview) {
1252 continue;
1253 }
1254
1255 $control_id = $field->getControlID();
1256
1257 $preview
1258 ->setControlID($control_id)
1259 ->setPreviewURI('/transactions/remarkuppreview/');
1260
1261 $previews[] = $preview;
1262 }
1263 } else {
1264 $previews = array();
1265 }
1266
1267 $form = $this->buildEditForm($object, $fields);
1268
1269 $crumbs = $this->buildCrumbs($object, $final = true);
1270 $crumbs->setBorder(true);
1271
1272 if ($request->isAjax()) {
1273 return $this->getController()
1274 ->newDialog()
1275 ->setWidth(AphrontDialogView::WIDTH_FULL)
1276 ->setTitle($header_text)
1277 ->setValidationException($validation_exception)
1278 ->appendForm($form)
1279 ->addCancelButton($cancel_uri)
1280 ->addSubmitButton($submit_button);
1281 }
1282
1283 $box_header = id(new PHUIHeaderView())
1284 ->setHeader($header_text);
1285
1286 if ($action_button) {
1287 $box_header->addActionLink($action_button);
1288 }
1289
1290 $request_submit_key = $request->getSubmitKey();
1291 $engine_submit_key = $this->getEditEngineSubmitKey();
1292
1293 if ($request_submit_key === $engine_submit_key) {
1294 $page_state->setIsSubmit(true);
1295 $page_state->setIsSave(true);
1296 }
1297
1298 $head = $this->newEditFormHeadContent($page_state);
1299 $tail = $this->newEditFormTailContent($page_state);
1300
1301 $box = id(new PHUIObjectBoxView())
1302 ->setViewer($viewer)
1303 ->setHeader($box_header)
1304 ->setValidationException($validation_exception)
1305 ->setBackground(PHUIObjectBoxView::WHITE_CONFIG)
1306 ->appendChild($form);
1307
1308 $content = array(
1309 $head,
1310 $box,
1311 $previews,
1312 $tail,
1313 );
1314
1315 $view = new PHUITwoColumnView();
1316
1317 $page_header = $this->getPageHeader($object);
1318 if ($page_header) {
1319 $view->setHeader($page_header);
1320 }
1321
1322 $view->setFooter($content);
1323
1324 $page = $controller->newPage()
1325 ->setTitle($header_text)
1326 ->setCrumbs($crumbs)
1327 ->appendChild($view);
1328
1329 $navigation = $this->getNavigation();
1330 if ($navigation) {
1331 $page->setNavigation($navigation);
1332 }
1333
1334 return $page;
1335 }
1336
1337 protected function newEditFormHeadContent(
1338 PhabricatorEditEnginePageState $state) {
1339 return null;
1340 }
1341
1342 protected function newEditFormTailContent(
1343 PhabricatorEditEnginePageState $state) {
1344 return null;
1345 }
1346
1347 protected function newEditResponse(
1348 AphrontRequest $request,
1349 $object,
1350 array $xactions) {
1351
1352 $submit_cookie = PhabricatorCookies::COOKIE_SUBMIT;
1353 $submit_key = $this->getEditEngineSubmitKey();
1354
1355 $request->setTemporaryCookie($submit_cookie, $submit_key);
1356
1357 return id(new AphrontRedirectResponse())
1358 ->setURI($this->getEffectiveObjectEditDoneURI($object));
1359 }
1360
1361 private function getEditEngineSubmitKey() {
1362 return 'edit-engine/'.$this->getEngineKey();
1363 }
1364
1365 private function buildEditForm($object, array $fields) {
1366 $viewer = $this->getViewer();
1367 $controller = $this->getController();
1368 $request = $controller->getRequest();
1369
1370 $fields = $this->willBuildEditForm($object, $fields);
1371
1372 $request_path = $request->getPath();
1373
1374 $form = id(new AphrontFormView())
1375 ->setViewer($viewer)
1376 ->setAction($request_path)
1377 ->addHiddenInput('editEngine', 'true');
1378
1379 foreach ($this->contextParameters as $param) {
1380 $form->addHiddenInput($param, $request->getStr($param));
1381 }
1382
1383 $requires_mfa = false;
1384 if ($object instanceof PhabricatorEditEngineMFAInterface) {
1385 $mfa_engine = PhabricatorEditEngineMFAEngine::newEngineForObject($object)
1386 ->setViewer($viewer);
1387 $requires_mfa = $mfa_engine->shouldRequireMFA();
1388 }
1389
1390 if ($requires_mfa) {
1391 $message = pht(
1392 'You will be required to provide multi-factor credentials to make '.
1393 'changes.');
1394 $form->appendChild(
1395 id(new PHUIInfoView())
1396 ->setSeverity(PHUIInfoView::SEVERITY_MFA)
1397 ->setErrors(array($message)));
1398
1399 // TODO: This should also set workflow on the form, so the user doesn't
1400 // lose any form data if they "Cancel". However, Maniphest currently
1401 // overrides "newEditResponse()" if the request is Ajax and returns a
1402 // bag of view data. This can reasonably be cleaned up when workboards
1403 // get their next iteration.
1404 }
1405
1406 foreach ($fields as $field) {
1407 if (!$field->getIsFormField()) {
1408 continue;
1409 }
1410
1411 $field->appendToForm($form);
1412 }
1413
1414 if ($this->getIsCreate()) {
1415 $cancel_uri = $this->getObjectCreateCancelURI($object);
1416 $submit_button = $this->getObjectCreateButtonText($object);
1417 } else {
1418 $cancel_uri = $this->getEffectiveObjectEditCancelURI($object);
1419 $submit_button = $this->getObjectEditButtonText($object);
1420 }
1421
1422 if (!$request->isAjax()) {
1423 $buttons = id(new AphrontFormSubmitControl())
1424 ->setValue($submit_button);
1425
1426 if ($cancel_uri) {
1427 $buttons->addCancelButton($cancel_uri);
1428 }
1429
1430 $form->appendControl($buttons);
1431 }
1432
1433 return $form;
1434 }
1435
1436 protected function willBuildEditForm($object, array $fields) {
1437 return $fields;
1438 }
1439
1440 /**
1441 * @return PHUIButtonView|null
1442 */
1443 private function buildEditFormActionButton($object) {
1444 if (!$this->isEngineConfigurable()) {
1445 return null;
1446 }
1447
1448 $viewer = $this->getViewer();
1449
1450 $action_view = id(new PhabricatorActionListView())
1451 ->setViewer($viewer);
1452
1453 foreach ($this->buildEditFormActions($object) as $action) {
1454 $action_view->addAction($action);
1455 }
1456
1457 $action_button = id(new PHUIButtonView())
1458 ->setTag('a')
1459 ->setText(pht('Configure Form'))
1460 ->setHref('#')
1461 ->setIcon('fa-gear')
1462 ->setDropdownMenu($action_view);
1463
1464 return $action_button;
1465 }
1466
1467 /**
1468 * @return array<PhabricatorActionView>
1469 */
1470 private function buildEditFormActions($object) {
1471 $actions = array();
1472
1473 if ($this->supportsEditEngineConfiguration()) {
1474 $engine_key = $this->getEngineKey();
1475 $config = $this->getEditEngineConfiguration();
1476
1477 $can_manage = PhabricatorPolicyFilter::hasCapability(
1478 $this->getViewer(),
1479 $config,
1480 PhabricatorPolicyCapability::CAN_EDIT);
1481
1482 if ($can_manage) {
1483 $manage_uri = $config->getURI();
1484 } else {
1485 $manage_uri = $this->getEditURI(null, 'nomanage/');
1486 }
1487
1488 $view_uri = "/transactions/editengine/{$engine_key}/";
1489
1490 $actions[] = id(new PhabricatorActionView())
1491 ->setLabel(true)
1492 ->setName(pht('Configuration'));
1493
1494 $actions[] = id(new PhabricatorActionView())
1495 ->setName(pht('View Form Configurations'))
1496 ->setIcon('fa-list-ul')
1497 ->setHref($view_uri);
1498
1499 $actions[] = id(new PhabricatorActionView())
1500 ->setName(pht('Edit Form Configuration'))
1501 ->setIcon('fa-pencil')
1502 ->setHref($manage_uri)
1503 ->setDisabled(!$can_manage)
1504 ->setWorkflow(!$can_manage);
1505 }
1506
1507 $actions[] = id(new PhabricatorActionView())
1508 ->setLabel(true)
1509 ->setName(pht('Documentation'));
1510
1511 $actions[] = id(new PhabricatorActionView())
1512 ->setName(pht('Using HTTP Parameters'))
1513 ->setIcon('fa-book')
1514 ->setHref($this->getEditURI($object, 'parameters/'));
1515
1516 $doc_href = PhabricatorEnv::getDoclink('User Guide: Customizing Forms');
1517 $actions[] = id(new PhabricatorActionView())
1518 ->setName(pht('User Guide: Customizing Forms'))
1519 ->setIcon('fa-book')
1520 ->setHref($doc_href);
1521
1522 return $actions;
1523 }
1524
1525
1526 public function newNUXButton($text) {
1527 $specs = $this->newCreateActionSpecifications(array());
1528 $head = head($specs);
1529
1530 return id(new PHUIButtonView())
1531 ->setTag('a')
1532 ->setText($text)
1533 ->setHref($head['uri'])
1534 ->setDisabled($head['disabled'])
1535 ->setWorkflow($head['workflow'])
1536 ->setColor(PHUIButtonView::GREEN);
1537 }
1538
1539
1540 final public function addActionToCrumbs(
1541 PHUICrumbsView $crumbs,
1542 array $parameters = array()) {
1543 $viewer = $this->getViewer();
1544
1545 $specs = $this->newCreateActionSpecifications($parameters);
1546
1547 $head = head($specs);
1548 $menu_uri = $head['uri'];
1549
1550 $dropdown = null;
1551 if (count($specs) > 1) {
1552 $menu_icon = 'fa-caret-square-o-down';
1553 $menu_name = $this->getObjectCreateShortText();
1554 $workflow = false;
1555 $disabled = false;
1556
1557 $dropdown = id(new PhabricatorActionListView())
1558 ->setUser($viewer);
1559
1560 foreach ($specs as $spec) {
1561 $dropdown->addAction(
1562 id(new PhabricatorActionView())
1563 ->setName($spec['name'])
1564 ->setIcon($spec['icon'])
1565 ->setHref($spec['uri'])
1566 ->setDisabled($head['disabled'])
1567 ->setWorkflow($head['workflow']));
1568 }
1569
1570 } else {
1571 $menu_icon = $head['icon'];
1572 $menu_name = $head['name'];
1573
1574 $workflow = $head['workflow'];
1575 $disabled = $head['disabled'];
1576 }
1577
1578 $action = id(new PHUIListItemView())
1579 ->setName($menu_name)
1580 ->setHref($menu_uri)
1581 ->setIcon($menu_icon)
1582 ->setWorkflow($workflow)
1583 ->setDisabled($disabled);
1584
1585 if ($dropdown) {
1586 $action->setDropdownMenu($dropdown);
1587 }
1588
1589 $crumbs->addAction($action);
1590 }
1591
1592
1593 /**
1594 * Build a raw description of available "Create New Object" UI options so
1595 * other methods can build menus or buttons.
1596 */
1597 public function newCreateActionSpecifications(array $parameters) {
1598 $viewer = $this->getViewer();
1599
1600 $can_create = $this->hasCreateCapability();
1601 if ($can_create) {
1602 $configs = $this->loadUsableConfigurationsForCreate();
1603 } else {
1604 $configs = array();
1605 }
1606
1607 $disabled = false;
1608 $workflow = false;
1609
1610 $menu_icon = 'fa-plus-square';
1611 $specs = array();
1612 if (!$configs) {
1613 if ($viewer->isLoggedIn()) {
1614 $disabled = true;
1615 } else {
1616 // If the viewer isn't logged in, assume they'll get hit with a login
1617 // dialog and are likely able to create objects after they log in.
1618 $disabled = false;
1619 }
1620 $workflow = true;
1621
1622 if ($can_create) {
1623 $create_uri = $this->getEditURI(null, 'nodefault/');
1624 } else {
1625 $create_uri = $this->getEditURI(null, 'nocreate/');
1626 }
1627
1628 $specs[] = array(
1629 'name' => $this->getObjectCreateShortText(),
1630 'uri' => $create_uri,
1631 'icon' => $menu_icon,
1632 'disabled' => $disabled,
1633 'workflow' => $workflow,
1634 );
1635 } else {
1636 foreach ($configs as $config) {
1637 $config_uri = $config->getCreateURI();
1638
1639 if ($parameters) {
1640 $config_uri = (string)new PhutilURI($config_uri, $parameters);
1641 }
1642
1643 $specs[] = array(
1644 'name' => $config->getDisplayName(),
1645 'uri' => $config_uri,
1646 'icon' => 'fa-plus',
1647 'disabled' => false,
1648 'workflow' => false,
1649 );
1650 }
1651 }
1652
1653 return $specs;
1654 }
1655
1656 final public function buildEditEngineCommentView($object) {
1657 $config = $this->loadDefaultEditConfiguration($object);
1658
1659 if (!$config) {
1660 // TODO: This just nukes the entire comment form if you don't have access
1661 // to any edit forms. We might want to tailor this UX a bit.
1662 return id(new PhabricatorApplicationTransactionCommentView())
1663 ->setNoPermission(true);
1664 }
1665
1666 $viewer = $this->getViewer();
1667
1668 $can_interact = PhabricatorPolicyFilter::canInteract($viewer, $object);
1669 if (!$can_interact) {
1670 $lock = PhabricatorEditEngineLock::newForObject($viewer, $object);
1671
1672 return id(new PhabricatorApplicationTransactionCommentView())
1673 ->setEditEngineLock($lock);
1674 }
1675
1676 $object_phid = $object->getPHID();
1677 $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
1678
1679 if ($is_serious) {
1680 $header_text = $this->getCommentViewSeriousHeaderText($object);
1681 $button_text = $this->getCommentViewSeriousButtonText($object);
1682 } else {
1683 $header_text = $this->getCommentViewHeaderText($object);
1684 $button_text = $this->getCommentViewButtonText($object);
1685 }
1686
1687 $comment_uri = $this->getEditURI($object, 'comment/');
1688
1689 $requires_mfa = false;
1690 if ($object instanceof PhabricatorEditEngineMFAInterface) {
1691 $mfa_engine = PhabricatorEditEngineMFAEngine::newEngineForObject($object)
1692 ->setViewer($viewer);
1693 $requires_mfa = $mfa_engine->shouldRequireMFA();
1694 }
1695
1696 $view = id(new PhabricatorApplicationTransactionCommentView())
1697 ->setViewer($viewer)
1698 ->setHeaderText($header_text)
1699 ->setAction($comment_uri)
1700 ->setRequestURI(new PhutilURI($this->getObjectViewURI($object)))
1701 ->setRequiresMFA($requires_mfa)
1702 ->setObject($object)
1703 ->setEditEngine($this)
1704 ->setSubmitButtonName($button_text);
1705
1706 $draft = PhabricatorVersionedDraft::loadDraft(
1707 $object_phid,
1708 $viewer->getPHID());
1709 if ($draft) {
1710 $view->setVersionedDraft($draft);
1711 }
1712
1713 $view->setCurrentVersion($this->loadDraftVersion($object));
1714
1715 $fields = $this->buildEditFields($object);
1716
1717 $can_edit = PhabricatorPolicyFilter::hasCapability(
1718 $viewer,
1719 $object,
1720 PhabricatorPolicyCapability::CAN_EDIT);
1721
1722 $comment_actions = array();
1723 foreach ($fields as $field) {
1724 if (!$field->shouldGenerateTransactionsFromComment()) {
1725 continue;
1726 }
1727
1728 if (!$can_edit) {
1729 if (!$field->getCanApplyWithoutEditCapability()) {
1730 continue;
1731 }
1732 }
1733
1734 $comment_action = $field->getCommentAction();
1735 if (!$comment_action) {
1736 continue;
1737 }
1738
1739 $key = $comment_action->getKey();
1740
1741 // TODO: Validate these better.
1742
1743 $comment_actions[$key] = $comment_action;
1744 }
1745
1746 $comment_actions = msortv($comment_actions, 'getSortVector');
1747
1748 $view->setCommentActions($comment_actions);
1749
1750 $comment_groups = $this->newCommentActionGroups();
1751 $view->setCommentActionGroups($comment_groups);
1752
1753 return $view;
1754 }
1755
1756 protected function loadDraftVersion($object) {
1757 $viewer = $this->getViewer();
1758
1759 if (!$viewer->isLoggedIn()) {
1760 return null;
1761 }
1762
1763 $template = $object->getApplicationTransactionTemplate();
1764 $conn_r = $template->establishConnection('r');
1765
1766 // Find the most recent transaction the user has written. We'll use this
1767 // as a version number to make sure that out-of-date drafts get discarded.
1768 $result = queryfx_one(
1769 $conn_r,
1770 'SELECT id AS version FROM %T
1771 WHERE objectPHID = %s AND authorPHID = %s
1772 ORDER BY id DESC LIMIT 1',
1773 $template->getTableName(),
1774 $object->getPHID(),
1775 $viewer->getPHID());
1776
1777 if ($result) {
1778 return (int)$result['version'];
1779 } else {
1780 return null;
1781 }
1782 }
1783
1784
1785/* -( Responding to HTTP Parameter Requests )------------------------------ */
1786
1787
1788 /**
1789 * Respond to a request for documentation on HTTP parameters.
1790 *
1791 * @param object $object Editable object.
1792 * @return AphrontResponse Response object.
1793 * @task http
1794 */
1795 private function buildParametersResponse($object) {
1796 $controller = $this->getController();
1797 $viewer = $this->getViewer();
1798 $request = $controller->getRequest();
1799 $fields = $this->buildEditFields($object);
1800
1801 $crumbs = $this->buildCrumbs($object);
1802 $crumbs->addTextCrumb(pht('HTTP Parameters'));
1803 $crumbs->setBorder(true);
1804
1805 $header_text = pht(
1806 'HTTP Parameters: %s',
1807 $this->getObjectCreateShortText());
1808
1809 $header = id(new PHUIHeaderView())
1810 ->setHeader($header_text);
1811
1812 $help_view = id(new PhabricatorApplicationEditHTTPParameterHelpView())
1813 ->setUser($viewer)
1814 ->setFields($fields);
1815
1816 $document = id(new PHUIDocumentView())
1817 ->setUser($viewer)
1818 ->setHeader($header)
1819 ->appendChild($help_view);
1820
1821 return $controller->newPage()
1822 ->setTitle(pht('HTTP Parameters'))
1823 ->setCrumbs($crumbs)
1824 ->appendChild($document);
1825 }
1826
1827
1828 private function buildError($object, $title, $body) {
1829 $cancel_uri = $this->getObjectCreateCancelURI($object);
1830
1831 $dialog = $this->getController()
1832 ->newDialog()
1833 ->addCancelButton($cancel_uri);
1834
1835 if ($title !== null) {
1836 $dialog->setTitle($title);
1837 }
1838
1839 if ($body !== null) {
1840 $dialog->appendParagraph($body);
1841 }
1842
1843 return $dialog;
1844 }
1845
1846
1847 private function buildNoDefaultResponse($object) {
1848 return $this->buildError(
1849 $object,
1850 pht('No Default Create Forms'),
1851 pht(
1852 'This application is not configured with any forms for creating '.
1853 'objects that are visible to you and enabled.'));
1854 }
1855
1856 private function buildNoCreateResponse($object) {
1857 return $this->buildError(
1858 $object,
1859 pht('No Create Permission'),
1860 pht('You do not have permission to create these objects.'));
1861 }
1862
1863 private function buildNoManageResponse($object) {
1864 return $this->buildError(
1865 $object,
1866 pht('No Manage Permission'),
1867 pht(
1868 'You do not have permission to configure forms for this '.
1869 'application.'));
1870 }
1871
1872 private function buildNoEditResponse($object) {
1873 return $this->buildError(
1874 $object,
1875 pht('No Edit Forms'),
1876 pht(
1877 'You do not have access to any forms which are enabled and marked '.
1878 'as edit forms.'));
1879 }
1880
1881 private function buildNotEditFormRespose($object, $config) {
1882 return $this->buildError(
1883 $object,
1884 pht('Not an Edit Form'),
1885 pht(
1886 'This form ("%s") is not marked as an edit form, so '.
1887 'it can not be used to edit objects.',
1888 $config->getName()));
1889 }
1890
1891 private function buildDisabledFormResponse($object, $config) {
1892 return $this->buildError(
1893 $object,
1894 pht('Form Disabled'),
1895 pht(
1896 'This form ("%s") has been disabled, so it can not be used.',
1897 $config->getName()));
1898 }
1899
1900 private function buildLockedObjectResponse($object) {
1901 $dialog = $this->buildError($object, null, null);
1902 $viewer = $this->getViewer();
1903
1904 $lock = PhabricatorEditEngineLock::newForObject($viewer, $object);
1905 return $lock->willBlockUserInteractionWithDialog($dialog);
1906 }
1907
1908 private function buildCommentResponse($object) {
1909 $viewer = $this->getViewer();
1910
1911 if ($this->getIsCreate()) {
1912 return new Aphront404Response();
1913 }
1914
1915 $controller = $this->getController();
1916 $request = $controller->getRequest();
1917
1918 // NOTE: We handle hisec inside the transaction editor with "Sign With MFA"
1919 // comment actions.
1920 if (!$request->isFormOrHisecPost()) {
1921 return new Aphront400Response();
1922 }
1923
1924 $can_interact = PhabricatorPolicyFilter::canInteract($viewer, $object);
1925 if (!$can_interact) {
1926 return $this->buildLockedObjectResponse($object);
1927 }
1928
1929 $config = $this->loadDefaultEditConfiguration($object);
1930 if (!$config) {
1931 return new Aphront404Response();
1932 }
1933
1934 $fields = $this->buildEditFields($object);
1935
1936 $is_preview = $request->isPreviewRequest();
1937 $view_uri = $this->getEffectiveObjectViewURI($object);
1938
1939 $template = $object->getApplicationTransactionTemplate();
1940 $comment_template = $template->getApplicationTransactionCommentObject();
1941
1942 $comment_text = $request->getStr('comment');
1943
1944 $comment_metadata = $request->getStr('comment_metadata');
1945 if (phutil_nonempty_string($comment_metadata)) {
1946 $comment_metadata = phutil_json_decode($comment_metadata);
1947 }
1948
1949 $actions = $request->getStr('editengine.actions');
1950 if ($actions) {
1951 $actions = phutil_json_decode($actions);
1952 }
1953
1954 if ($is_preview) {
1955 $version_key = PhabricatorVersionedDraft::KEY_VERSION;
1956 $request_version = $request->getInt($version_key);
1957 $current_version = $this->loadDraftVersion($object);
1958 if ($request_version >= $current_version) {
1959 $draft = PhabricatorVersionedDraft::loadOrCreateDraft(
1960 $object->getPHID(),
1961 $viewer->getPHID(),
1962 $current_version);
1963
1964 $draft
1965 ->setProperty('comment', $comment_text)
1966 ->setProperty('metadata', $comment_metadata)
1967 ->setProperty('actions', $actions)
1968 ->save();
1969
1970 $draft_engine = $this->newDraftEngine($object);
1971 if ($draft_engine) {
1972 $draft_engine
1973 ->setVersionedDraft($draft)
1974 ->synchronize();
1975 }
1976 }
1977 }
1978
1979 $xactions = array();
1980
1981 $can_edit = PhabricatorPolicyFilter::hasCapability(
1982 $viewer,
1983 $object,
1984 PhabricatorPolicyCapability::CAN_EDIT);
1985
1986 if ($actions) {
1987 $action_map = array();
1988 foreach ($actions as $action) {
1989 $type = idx($action, 'type');
1990 if (!$type) {
1991 continue;
1992 }
1993
1994 if (empty($fields[$type])) {
1995 continue;
1996 }
1997
1998 $action_map[$type] = $action;
1999 }
2000
2001 foreach ($action_map as $type => $action) {
2002 $field = $fields[$type];
2003
2004 if (!$field->shouldGenerateTransactionsFromComment()) {
2005 continue;
2006 }
2007
2008 // If you don't have edit permission on the object, you're limited in
2009 // which actions you can take via the comment form. Most actions
2010 // need edit permission, but some actions (like "Accept Revision")
2011 // can be applied by anyone with view permission.
2012 if (!$can_edit) {
2013 if (!$field->getCanApplyWithoutEditCapability()) {
2014 // We know the user doesn't have the capability, so this will
2015 // raise a policy exception.
2016 PhabricatorPolicyFilter::requireCapability(
2017 $viewer,
2018 $object,
2019 PhabricatorPolicyCapability::CAN_EDIT);
2020 }
2021 }
2022
2023 if (array_key_exists('initialValue', $action)) {
2024 $field->setInitialValue($action['initialValue']);
2025 }
2026
2027 $field->readValueFromComment(idx($action, 'value'));
2028
2029 $type_xactions = $field->generateTransactions(
2030 clone $template,
2031 array(
2032 'value' => $field->getValueForTransaction(),
2033 ));
2034 foreach ($type_xactions as $type_xaction) {
2035 $xactions[] = $type_xaction;
2036 }
2037 }
2038 }
2039
2040 $auto_xactions = $this->newAutomaticCommentTransactions($object);
2041 foreach ($auto_xactions as $xaction) {
2042 $xactions[] = $xaction;
2043 }
2044
2045 if (phutil_nonempty_string($comment_text) || !$xactions) {
2046 $xactions[] = id(clone $template)
2047 ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
2048 ->setMetadataValue('remarkup.control', $comment_metadata)
2049 ->attachComment(
2050 id(clone $comment_template)
2051 ->setContent($comment_text));
2052 }
2053
2054 $editor = $object->getApplicationTransactionEditor()
2055 ->setActor($viewer)
2056 ->setContinueOnNoEffect($request->isContinueRequest())
2057 ->setContinueOnMissingFields(true)
2058 ->setContentSourceFromRequest($request)
2059 ->setCancelURI($view_uri)
2060 ->setRaiseWarnings(!$request->getBool('editEngine.warnings'))
2061 ->setIsPreview($is_preview);
2062
2063 try {
2064 $xactions = $editor->applyTransactions($object, $xactions);
2065 } catch (PhabricatorApplicationTransactionValidationException $ex) {
2066 return id(new PhabricatorApplicationTransactionValidationResponse())
2067 ->setCancelURI($view_uri)
2068 ->setException($ex);
2069 } catch (PhabricatorApplicationTransactionNoEffectException $ex) {
2070 return id(new PhabricatorApplicationTransactionNoEffectResponse())
2071 ->setCancelURI($view_uri)
2072 ->setException($ex);
2073 } catch (PhabricatorApplicationTransactionWarningException $ex) {
2074 return id(new PhabricatorApplicationTransactionWarningResponse())
2075 ->setObject($object)
2076 ->setCancelURI($view_uri)
2077 ->setException($ex);
2078 }
2079
2080 if (!$is_preview) {
2081 PhabricatorVersionedDraft::purgeDrafts(
2082 $object->getPHID(),
2083 $viewer->getPHID());
2084
2085 $draft_engine = $this->newDraftEngine($object);
2086 if ($draft_engine) {
2087 $draft_engine
2088 ->setVersionedDraft(null)
2089 ->synchronize();
2090 }
2091 }
2092
2093 if ($request->isAjax() && $is_preview) {
2094 $preview_content = $this->newCommentPreviewContent($object, $xactions);
2095
2096 $raw_view_data = $request->getStr('viewData');
2097 try {
2098 $view_data = phutil_json_decode($raw_view_data);
2099 } catch (Exception $ex) {
2100 $view_data = array();
2101 }
2102
2103 return id(new PhabricatorApplicationTransactionResponse())
2104 ->setObject($object)
2105 ->setViewer($viewer)
2106 ->setTransactions($xactions)
2107 ->setIsPreview($is_preview)
2108 ->setViewData($view_data)
2109 ->setPreviewContent($preview_content);
2110 } else {
2111 return id(new AphrontRedirectResponse())
2112 ->setURI($view_uri);
2113 }
2114 }
2115
2116 protected function newDraftEngine($object) {
2117 $viewer = $this->getViewer();
2118
2119 if ($object instanceof PhabricatorDraftInterface) {
2120 $engine = $object->newDraftEngine();
2121 } else {
2122 $engine = new PhabricatorBuiltinDraftEngine();
2123 }
2124
2125 return $engine
2126 ->setObject($object)
2127 ->setViewer($viewer);
2128 }
2129
2130
2131/* -( Conduit )------------------------------------------------------------ */
2132
2133
2134 /**
2135 * Respond to a Conduit edit request.
2136 *
2137 * This method accepts a list of transactions to apply to an object, and
2138 * either edits an existing object or creates a new one.
2139 *
2140 * @task conduit
2141 */
2142 final public function buildConduitResponse(ConduitAPIRequest $request) {
2143 $viewer = $this->getViewer();
2144
2145 $config = $this->loadDefaultConfiguration();
2146 if (!$config) {
2147 throw new Exception(
2148 pht(
2149 'Unable to load configuration for this EditEngine ("%s").',
2150 get_class($this)));
2151 }
2152
2153 $raw_xactions = $this->getRawConduitTransactions($request);
2154
2155 $identifier = $request->getValue('objectIdentifier');
2156 if ($identifier) {
2157 $this->setIsCreate(false);
2158
2159 // After T13186, each transaction can individually weaken or replace the
2160 // capabilities required to apply it, so we no longer need CAN_EDIT to
2161 // attempt to apply transactions to objects. In practice, almost all
2162 // transactions require CAN_EDIT so we won't get very far if we don't
2163 // have it.
2164 $capabilities = array(
2165 PhabricatorPolicyCapability::CAN_VIEW,
2166 );
2167
2168 $object = $this->newObjectFromIdentifier(
2169 $identifier,
2170 $capabilities);
2171 } else {
2172 $this->requireCreateCapability();
2173
2174 $this->setIsCreate(true);
2175 $object = $this->newEditableObjectFromConduit($raw_xactions);
2176 }
2177
2178 $this->validateObject($object);
2179
2180 $fields = $this->buildEditFields($object);
2181
2182 $types = $this->getConduitEditTypesFromFields($fields);
2183 $template = $object->getApplicationTransactionTemplate();
2184
2185 $xactions = $this->getConduitTransactions(
2186 $request,
2187 $raw_xactions,
2188 $types,
2189 $template);
2190
2191 $editor = $object->getApplicationTransactionEditor()
2192 ->setActor($viewer)
2193 ->setContentSource($request->newContentSource())
2194 ->setContinueOnNoEffect(true);
2195
2196 if (!$this->getIsCreate()) {
2197 $editor->setContinueOnMissingFields(true);
2198 }
2199
2200 $xactions = $editor->applyTransactions($object, $xactions);
2201
2202 $xactions_struct = array();
2203 foreach ($xactions as $xaction) {
2204 $xactions_struct[] = array(
2205 'phid' => $xaction->getPHID(),
2206 );
2207 }
2208
2209 return array(
2210 'object' => array(
2211 'id' => (int)$object->getID(),
2212 'phid' => $object->getPHID(),
2213 ),
2214 'transactions' => $xactions_struct,
2215 );
2216 }
2217
2218 private function getRawConduitTransactions(ConduitAPIRequest $request) {
2219 $transactions_key = 'transactions';
2220
2221 $xactions = $request->getValue($transactions_key);
2222 if (!is_array($xactions)) {
2223 throw new Exception(
2224 pht(
2225 'Parameter "%s" is not a list of transactions.',
2226 $transactions_key));
2227 }
2228
2229 foreach ($xactions as $key => $xaction) {
2230 if (!is_array($xaction)) {
2231 throw new Exception(
2232 pht(
2233 'Parameter "%s" must contain a list of transaction descriptions, '.
2234 'but item with key "%s" is not a dictionary.',
2235 $transactions_key,
2236 $key));
2237 }
2238
2239 if (!array_key_exists('type', $xaction)) {
2240 throw new Exception(
2241 pht(
2242 'Parameter "%s" must contain a list of transaction descriptions, '.
2243 'but item with key "%s" is missing a "type" field. Each '.
2244 'transaction must have a type field.',
2245 $transactions_key,
2246 $key));
2247 }
2248
2249 if (!array_key_exists('value', $xaction)) {
2250 throw new Exception(
2251 pht(
2252 'Parameter "%s" must contain a list of transaction descriptions, '.
2253 'but item with key "%s" is missing a "value" field. Each '.
2254 'transaction must have a value field.',
2255 $transactions_key,
2256 $key));
2257 }
2258 }
2259
2260 return $xactions;
2261 }
2262
2263
2264 /**
2265 * Generate transactions which can be applied from edit actions in a Conduit
2266 * request.
2267 *
2268 * @param ConduitAPIRequest $request The request.
2269 * @param array<array{type:string,value:mixed}> $xactions Raw conduit
2270 * transactions.
2271 * @param list<PhabricatorEditType> $types Supported edit types.
2272 * @param PhabricatorApplicationTransaction $template Template transaction.
2273 * @return list<PhabricatorApplicationTransaction> Generated transactions.
2274 * @task conduit
2275 */
2276 private function getConduitTransactions(
2277 ConduitAPIRequest $request,
2278 array $xactions,
2279 array $types,
2280 PhabricatorApplicationTransaction $template) {
2281
2282 $viewer = $request->getUser();
2283 $results = array();
2284
2285 foreach ($xactions as $key => $xaction) {
2286 $type = $xaction['type'];
2287 if (empty($types[$type])) {
2288 throw new Exception(
2289 pht(
2290 'Transaction with key "%s" has invalid type "%s". This type is '.
2291 'not recognized. Valid types are: %s.',
2292 $key,
2293 $type,
2294 implode(', ', array_keys($types))));
2295 }
2296 }
2297
2298 if ($this->getIsCreate()) {
2299 $results[] = id(clone $template)
2300 ->setTransactionType(PhabricatorTransactions::TYPE_CREATE);
2301 }
2302
2303 $is_strict = $request->getIsStrictlyTyped();
2304
2305 foreach ($xactions as $xaction) {
2306 $type = $types[$xaction['type']];
2307
2308 // Let the parameter type interpret the value. This allows you to
2309 // use usernames in list<user> fields, for example.
2310 $parameter_type = $type->getConduitParameterType();
2311
2312 $parameter_type->setViewer($viewer);
2313
2314 try {
2315 $value = $xaction['value'];
2316 $value = $parameter_type->getValue($xaction, 'value', $is_strict);
2317 $value = $type->getTransactionValueFromConduit($value);
2318 $xaction['value'] = $value;
2319 } catch (Exception $ex) {
2320 throw new Exception(
2321 pht(
2322 'Exception when processing transaction of type "%s": %s',
2323 $xaction['type'],
2324 $ex->getMessage()),
2325 0,
2326 $ex);
2327 }
2328
2329 $type_xactions = $type->generateTransactions(
2330 clone $template,
2331 $xaction);
2332
2333 foreach ($type_xactions as $type_xaction) {
2334 $results[] = $type_xaction;
2335 }
2336 }
2337
2338 return $results;
2339 }
2340
2341
2342 /**
2343 * @return map<string, PhabricatorEditType>
2344 * @task conduit
2345 */
2346 private function getConduitEditTypesFromFields(array $fields) {
2347 $types = array();
2348 foreach ($fields as $field) {
2349 $field_types = $field->getConduitEditTypes();
2350
2351 if ($field_types === null) {
2352 continue;
2353 }
2354
2355 foreach ($field_types as $field_type) {
2356 $types[$field_type->getEditType()] = $field_type;
2357 }
2358 }
2359 return $types;
2360 }
2361
2362 public function getConduitEditTypes() {
2363 $config = $this->loadDefaultConfiguration();
2364 if (!$config) {
2365 return array();
2366 }
2367
2368 $object = $this->newEditableObjectForDocumentation();
2369 $fields = $this->buildEditFields($object);
2370 return $this->getConduitEditTypesFromFields($fields);
2371 }
2372
2373 final public static function getAllEditEngines() {
2374 return id(new PhutilClassMapQuery())
2375 ->setAncestorClass(self::class)
2376 ->setUniqueMethod('getEngineKey')
2377 ->execute();
2378 }
2379
2380 final public static function getByKey(PhabricatorUser $viewer, $key) {
2381 return id(new PhabricatorEditEngineQuery())
2382 ->setViewer($viewer)
2383 ->withEngineKeys(array($key))
2384 ->executeOne();
2385 }
2386
2387 public function getIcon() {
2388 $application = $this->getApplication();
2389 return $application->getIcon();
2390 }
2391
2392 private function loadUsableConfigurationsForCreate() {
2393 $viewer = $this->getViewer();
2394
2395 $configs = id(new PhabricatorEditEngineConfigurationQuery())
2396 ->setViewer($viewer)
2397 ->withEngineKeys(array($this->getEngineKey()))
2398 ->withIsDefault(true)
2399 ->withIsDisabled(false)
2400 ->execute();
2401
2402 $configs = msort($configs, 'getCreateSortKey');
2403
2404 // Attach this specific engine to configurations we load so they can access
2405 // any runtime configuration. For example, this allows us to generate the
2406 // correct "Create Form" buttons when editing forms, see T12301.
2407 foreach ($configs as $config) {
2408 $config->attachEngine($this);
2409 }
2410
2411 return $configs;
2412 }
2413
2414 protected function getValidationExceptionShortMessage(
2415 PhabricatorApplicationTransactionValidationException $ex,
2416 PhabricatorEditField $field) {
2417
2418 $xaction_type = $field->getTransactionType();
2419 if ($xaction_type === null) {
2420 return null;
2421 }
2422
2423 return $ex->getShortMessage($xaction_type);
2424 }
2425
2426 protected function getCreateNewObjectPolicy() {
2427 return PhabricatorPolicies::POLICY_USER;
2428 }
2429
2430 private function requireCreateCapability() {
2431 PhabricatorPolicyFilter::requireCapability(
2432 $this->getViewer(),
2433 $this,
2434 PhabricatorPolicyCapability::CAN_EDIT);
2435 }
2436
2437 private function hasCreateCapability() {
2438 return PhabricatorPolicyFilter::hasCapability(
2439 $this->getViewer(),
2440 $this,
2441 PhabricatorPolicyCapability::CAN_EDIT);
2442 }
2443
2444 public function isCommentAction() {
2445 return ($this->getEditAction() == 'comment');
2446 }
2447
2448 public function getEditAction() {
2449 $controller = $this->getController();
2450 $request = $controller->getRequest();
2451 return $request->getURIData('editAction');
2452 }
2453
2454 protected function newCommentActionGroups() {
2455 return array();
2456 }
2457
2458 protected function newAutomaticCommentTransactions($object) {
2459 return array();
2460 }
2461
2462 protected function newCommentPreviewContent($object, array $xactions) {
2463 return null;
2464 }
2465
2466
2467/* -( Form Pages )--------------------------------------------------------- */
2468
2469
2470 public function getSelectedPage() {
2471 return $this->page;
2472 }
2473
2474
2475 private function selectPage($object, $page_key) {
2476 $pages = $this->getPages($object);
2477
2478 if (empty($pages[$page_key])) {
2479 return null;
2480 }
2481
2482 $this->page = $pages[$page_key];
2483 return $this->page;
2484 }
2485
2486
2487 protected function newPages($object) {
2488 return array();
2489 }
2490
2491
2492 protected function getPages($object) {
2493 if ($this->pages === null) {
2494 $pages = $this->newPages($object);
2495
2496 assert_instances_of($pages, PhabricatorEditPage::class);
2497 $pages = mpull($pages, null, 'getKey');
2498
2499 $this->pages = $pages;
2500 }
2501
2502 return $this->pages;
2503 }
2504
2505 private function applyPageToFields($object, array $fields) {
2506 $pages = $this->getPages($object);
2507 if (!$pages) {
2508 return $fields;
2509 }
2510
2511 if (!$this->getSelectedPage()) {
2512 return $fields;
2513 }
2514
2515 $page_picks = array();
2516 $default_key = head($pages)->getKey();
2517 foreach ($pages as $page_key => $page) {
2518 foreach ($page->getFieldKeys() as $field_key) {
2519 $page_picks[$field_key] = $page_key;
2520 }
2521 if ($page->getIsDefault()) {
2522 $default_key = $page_key;
2523 }
2524 }
2525
2526 $page_map = array_fill_keys(array_keys($pages), array());
2527 foreach ($fields as $field_key => $field) {
2528 if (isset($page_picks[$field_key])) {
2529 $page_map[$page_picks[$field_key]][$field_key] = $field;
2530 continue;
2531 }
2532
2533 // TODO: Maybe let the field pick a page to associate itself with so
2534 // extensions can force themselves onto a particular page?
2535
2536 $page_map[$default_key][$field_key] = $field;
2537 }
2538
2539 $page = $this->getSelectedPage();
2540 if (!$page) {
2541 $page = head($pages);
2542 }
2543
2544 $selected_key = $page->getKey();
2545 return $page_map[$selected_key];
2546 }
2547
2548 protected function willApplyTransactions($object, array $xactions) {
2549 return $xactions;
2550 }
2551
2552 protected function didApplyTransactions($object, array $xactions) {
2553 return;
2554 }
2555
2556
2557/* -( Bulk Edits )--------------------------------------------------------- */
2558
2559 final public function newBulkEditGroupMap() {
2560 $groups = $this->newBulkEditGroups();
2561
2562 $map = array();
2563 foreach ($groups as $group) {
2564 $key = $group->getKey();
2565
2566 if (isset($map[$key])) {
2567 throw new Exception(
2568 pht(
2569 'Two bulk edit groups have the same key ("%s"). Each bulk edit '.
2570 'group must have a unique key.',
2571 $key));
2572 }
2573
2574 $map[$key] = $group;
2575 }
2576
2577 if ($this->isEngineExtensible()) {
2578 $extensions = PhabricatorEditEngineExtension::getAllEnabledExtensions();
2579 } else {
2580 $extensions = array();
2581 }
2582
2583 foreach ($extensions as $extension) {
2584 $extension_groups = $extension->newBulkEditGroups($this);
2585 foreach ($extension_groups as $group) {
2586 $key = $group->getKey();
2587
2588 if (isset($map[$key])) {
2589 throw new Exception(
2590 pht(
2591 'Extension "%s" defines a bulk edit group with the same key '.
2592 '("%s") as the main editor or another extension. Each bulk '.
2593 'edit group must have a unique key.',
2594 get_class($extension),
2595 $key));
2596 }
2597
2598 $map[$key] = $group;
2599 }
2600 }
2601
2602 return $map;
2603 }
2604
2605 protected function newBulkEditGroups() {
2606 return array(
2607 id(new PhabricatorBulkEditGroup())
2608 ->setKey('default')
2609 ->setLabel(pht('Primary Fields')),
2610 id(new PhabricatorBulkEditGroup())
2611 ->setKey('extension')
2612 ->setLabel(pht('Support Applications')),
2613 );
2614 }
2615
2616 final public function newBulkEditMap() {
2617 $viewer = $this->getViewer();
2618
2619 $config = $this->loadDefaultConfiguration();
2620 if (!$config) {
2621 throw new Exception(
2622 pht('No default edit engine configuration for bulk edit.'));
2623 }
2624
2625 $object = $this->newEditableObject();
2626 $fields = $this->buildEditFields($object);
2627 $groups = $this->newBulkEditGroupMap();
2628
2629 $edit_types = $this->getBulkEditTypesFromFields($fields);
2630
2631 $map = array();
2632 foreach ($edit_types as $key => $type) {
2633 $bulk_type = $type->getBulkParameterType();
2634 if ($bulk_type === null) {
2635 continue;
2636 }
2637
2638 $bulk_type->setViewer($viewer);
2639
2640 $bulk_label = $type->getBulkEditLabel();
2641 if ($bulk_label === null) {
2642 continue;
2643 }
2644
2645 $group_key = $type->getBulkEditGroupKey();
2646 if (!$group_key) {
2647 $group_key = 'default';
2648 }
2649
2650 if (!isset($groups[$group_key])) {
2651 throw new Exception(
2652 pht(
2653 'Field "%s" has a bulk edit group key ("%s") with no '.
2654 'corresponding bulk edit group.',
2655 $key,
2656 $group_key));
2657 }
2658
2659 $map[] = array(
2660 'label' => $bulk_label,
2661 'xaction' => $key,
2662 'group' => $group_key,
2663 'control' => array(
2664 'type' => $bulk_type->getPHUIXControlType(),
2665 'spec' => (object)$bulk_type->getPHUIXControlSpecification(),
2666 ),
2667 );
2668 }
2669
2670 return $map;
2671 }
2672
2673
2674 final public function newRawBulkTransactions(array $xactions) {
2675 $config = $this->loadDefaultConfiguration();
2676 if (!$config) {
2677 throw new Exception(
2678 pht('No default edit engine configuration for bulk edit.'));
2679 }
2680
2681 $object = $this->newEditableObject();
2682 $fields = $this->buildEditFields($object);
2683
2684 $edit_types = $this->getBulkEditTypesFromFields($fields);
2685 $template = $object->getApplicationTransactionTemplate();
2686
2687 $raw_xactions = array();
2688 foreach ($xactions as $key => $xaction) {
2689 PhutilTypeSpec::checkMap(
2690 $xaction,
2691 array(
2692 'type' => 'string',
2693 'value' => 'optional wild',
2694 ));
2695
2696 $type = $xaction['type'];
2697 if (!isset($edit_types[$type])) {
2698 throw new Exception(
2699 pht(
2700 'Unsupported bulk edit type "%s".',
2701 $type));
2702 }
2703
2704 $edit_type = $edit_types[$type];
2705
2706 // Replace the edit type with the underlying transaction type. Usually
2707 // these are 1:1 and the transaction type just has more internal noise,
2708 // but it's possible that this isn't the case.
2709 $xaction['type'] = $edit_type->getTransactionType();
2710
2711 $value = $xaction['value'];
2712 $value = $edit_type->getTransactionValueFromBulkEdit($value);
2713 $xaction['value'] = $value;
2714
2715 $xaction_objects = $edit_type->generateTransactions(
2716 clone $template,
2717 $xaction);
2718
2719 foreach ($xaction_objects as $xaction_object) {
2720 $raw_xaction = array(
2721 'type' => $xaction_object->getTransactionType(),
2722 'metadata' => $xaction_object->getMetadata(),
2723 'new' => $xaction_object->getNewValue(),
2724 );
2725
2726 if ($xaction_object->hasOldValue()) {
2727 $raw_xaction['old'] = $xaction_object->getOldValue();
2728 }
2729
2730 if ($xaction_object->hasComment()) {
2731 $comment = $xaction_object->getComment();
2732 $raw_xaction['comment'] = $comment->getContent();
2733 }
2734
2735 $raw_xactions[] = $raw_xaction;
2736 }
2737 }
2738
2739 return $raw_xactions;
2740 }
2741
2742 private function getBulkEditTypesFromFields(array $fields) {
2743 $types = array();
2744
2745 foreach ($fields as $field) {
2746 $field_types = $field->getBulkEditTypes();
2747
2748 if ($field_types === null) {
2749 continue;
2750 }
2751
2752 foreach ($field_types as $field_type) {
2753 $types[$field_type->getEditType()] = $field_type;
2754 }
2755 }
2756
2757 return $types;
2758 }
2759
2760
2761/* -( PhabricatorPolicyInterface )----------------------------------------- */
2762
2763
2764 public function getPHID() {
2765 return get_class($this);
2766 }
2767
2768 public function getCapabilities() {
2769 return array(
2770 PhabricatorPolicyCapability::CAN_VIEW,
2771 PhabricatorPolicyCapability::CAN_EDIT,
2772 );
2773 }
2774
2775 public function getPolicy($capability) {
2776 switch ($capability) {
2777 case PhabricatorPolicyCapability::CAN_VIEW:
2778 return PhabricatorPolicies::getMostOpenPolicy();
2779 case PhabricatorPolicyCapability::CAN_EDIT:
2780 return $this->getCreateNewObjectPolicy();
2781 }
2782 }
2783
2784 public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
2785 return false;
2786 }
2787
2788}