Generate file attachment transactions for explicit Remarkup attachments on common...
[phabricator.git] / src / applications / transactions / editengine / PhabricatorEditEngine.php
blobb0e3ff406275d788138c50bc8c282bcd48741eba
1 <?php
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
15 abstract class PhabricatorEditEngine
16 extends Phobject
17 implements PhabricatorPolicyInterface {
19 const EDITENGINECONFIG_DEFAULT = 'default';
21 const SUBTYPE_DEFAULT = 'default';
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;
33 final public function setViewer(PhabricatorUser $viewer) {
34 $this->viewer = $viewer;
35 return $this;
38 final public function getViewer() {
39 return $this->viewer;
42 final public function setController(PhabricatorController $controller) {
43 $this->controller = $controller;
44 $this->setViewer($controller->getViewer());
45 return $this;
48 final public function getController() {
49 return $this->controller;
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)));
60 return $key;
63 final public function getApplication() {
64 $app_class = $this->getEngineApplicationClass();
65 return PhabricatorApplication::getByClass($app_class);
68 final public function addContextParameter($key) {
69 $this->contextParameters[] = $key;
70 return $this;
73 public function isEngineConfigurable() {
74 return true;
77 public function isEngineExtensible() {
78 return true;
81 public function isDefaultQuickCreateEngine() {
82 return false;
85 public function getDefaultQuickCreateFormKeys() {
86 $keys = array();
88 if ($this->isDefaultQuickCreateEngine()) {
89 $keys[] = self::EDITENGINECONFIG_DEFAULT;
92 foreach ($keys as $idx => $key) {
93 $keys[$idx] = $this->getEngineKey().'/'.$key;
96 return $keys;
99 public static function splitFullKey($full_key) {
100 return explode('/', $full_key, 2);
103 public function getQuickCreateOrderVector() {
104 return id(new PhutilSortVector())
105 ->addString($this->getObjectCreateShortText());
109 * Force the engine to edit a particular object.
111 public function setTargetObject($target_object) {
112 $this->targetObject = $target_object;
113 return $this;
116 public function getTargetObject() {
117 return $this->targetObject;
120 public function setNavigation(AphrontSideNavFilterView $navigation) {
121 $this->navigation = $navigation;
122 return $this;
125 public function getNavigation() {
126 return $this->navigation;
130 /* -( Managing Fields )---------------------------------------------------- */
133 abstract public function getEngineApplicationClass();
134 abstract protected function buildCustomEditFields($object);
136 public function getFieldsForConfig(
137 PhabricatorEditEngineConfiguration $config) {
139 $object = $this->newEditableObject();
141 $this->editEngineConfiguration = $config;
143 // This is mostly making sure that we fill in default values.
144 $this->setIsCreate(true);
146 return $this->buildEditFields($object);
149 final protected function buildEditFields($object) {
150 $viewer = $this->getViewer();
152 $fields = $this->buildCustomEditFields($object);
154 foreach ($fields as $field) {
155 $field
156 ->setViewer($viewer)
157 ->setObject($object);
160 $fields = mpull($fields, null, 'getKey');
162 if ($this->isEngineExtensible()) {
163 $extensions = PhabricatorEditEngineExtension::getAllEnabledExtensions();
164 } else {
165 $extensions = array();
168 // See T13248. Create a template object to provide to extensions. We
169 // adjust the template to have the intended subtype, so that extensions
170 // may change behavior based on the form subtype.
172 $template_object = clone $object;
173 if ($this->getIsCreate()) {
174 if ($this->supportsSubtypes()) {
175 $config = $this->getEditEngineConfiguration();
176 $subtype = $config->getSubtype();
177 $template_object->setSubtype($subtype);
181 foreach ($extensions as $extension) {
182 $extension->setViewer($viewer);
184 if (!$extension->supportsObject($this, $template_object)) {
185 continue;
188 $extension_fields = $extension->buildCustomEditFields(
189 $this,
190 $template_object);
192 // TODO: Validate this in more detail with a more tailored error.
193 assert_instances_of($extension_fields, 'PhabricatorEditField');
195 foreach ($extension_fields as $field) {
196 $field
197 ->setViewer($viewer)
198 ->setObject($object);
200 $group_key = $field->getBulkEditGroupKey();
201 if ($group_key === null) {
202 $field->setBulkEditGroupKey('extension');
206 $extension_fields = mpull($extension_fields, null, 'getKey');
208 foreach ($extension_fields as $key => $field) {
209 $fields[$key] = $field;
213 $config = $this->getEditEngineConfiguration();
214 $fields = $this->willConfigureFields($object, $fields);
215 $fields = $config->applyConfigurationToFields($this, $object, $fields);
217 $fields = $this->applyPageToFields($object, $fields);
219 return $fields;
222 protected function willConfigureFields($object, array $fields) {
223 return $fields;
226 final public function supportsSubtypes() {
227 try {
228 $object = $this->newEditableObject();
229 } catch (Exception $ex) {
230 return false;
233 return ($object instanceof PhabricatorEditEngineSubtypeInterface);
236 final public function newSubtypeMap() {
237 return $this->newEditableObject()->newEditEngineSubtypeMap();
241 /* -( Display Text )------------------------------------------------------- */
245 * @task text
247 abstract public function getEngineName();
251 * @task text
253 abstract protected function getObjectCreateTitleText($object);
256 * @task text
258 protected function getFormHeaderText($object) {
259 $config = $this->getEditEngineConfiguration();
260 return $config->getName();
264 * @task text
266 abstract protected function getObjectEditTitleText($object);
270 * @task text
272 abstract protected function getObjectCreateShortText();
276 * @task text
278 abstract protected function getObjectName();
282 * @task text
284 abstract protected function getObjectEditShortText($object);
288 * @task text
290 protected function getObjectCreateButtonText($object) {
291 return $this->getObjectCreateTitleText($object);
296 * @task text
298 protected function getObjectEditButtonText($object) {
299 return pht('Save Changes');
304 * @task text
306 protected function getCommentViewSeriousHeaderText($object) {
307 return pht('Take Action');
312 * @task text
314 protected function getCommentViewSeriousButtonText($object) {
315 return pht('Submit');
320 * @task text
322 protected function getCommentViewHeaderText($object) {
323 return $this->getCommentViewSeriousHeaderText($object);
328 * @task text
330 protected function getCommentViewButtonText($object) {
331 return $this->getCommentViewSeriousButtonText($object);
336 * @task text
338 protected function getPageHeader($object) {
339 return null;
345 * Return a human-readable header describing what this engine is used to do,
346 * like "Configure Maniphest Task Forms".
348 * @return string Human-readable description of the engine.
349 * @task text
351 abstract public function getSummaryHeader();
355 * Return a human-readable summary of what this engine is used to do.
357 * @return string Human-readable description of the engine.
358 * @task text
360 abstract public function getSummaryText();
365 /* -( Edit Engine Configuration )------------------------------------------ */
368 protected function supportsEditEngineConfiguration() {
369 return true;
372 final protected function getEditEngineConfiguration() {
373 return $this->editEngineConfiguration;
376 public function newConfigurationQuery() {
377 return id(new PhabricatorEditEngineConfigurationQuery())
378 ->setViewer($this->getViewer())
379 ->withEngineKeys(array($this->getEngineKey()));
382 private function loadEditEngineConfigurationWithQuery(
383 PhabricatorEditEngineConfigurationQuery $query,
384 $sort_method) {
386 if ($sort_method) {
387 $results = $query->execute();
388 $results = msort($results, $sort_method);
389 $result = head($results);
390 } else {
391 $result = $query->executeOne();
394 if (!$result) {
395 return null;
398 $this->editEngineConfiguration = $result;
399 return $result;
402 private function loadEditEngineConfigurationWithIdentifier($identifier) {
403 $query = $this->newConfigurationQuery()
404 ->withIdentifiers(array($identifier));
406 return $this->loadEditEngineConfigurationWithQuery($query, null);
409 private function loadDefaultConfiguration() {
410 $query = $this->newConfigurationQuery()
411 ->withIdentifiers(
412 array(
413 self::EDITENGINECONFIG_DEFAULT,
415 ->withIgnoreDatabaseConfigurations(true);
417 return $this->loadEditEngineConfigurationWithQuery($query, null);
420 private function loadDefaultCreateConfiguration() {
421 $query = $this->newConfigurationQuery()
422 ->withIsDefault(true)
423 ->withIsDisabled(false);
425 return $this->loadEditEngineConfigurationWithQuery(
426 $query,
427 'getCreateSortKey');
430 public function loadDefaultEditConfiguration($object) {
431 $query = $this->newConfigurationQuery()
432 ->withIsEdit(true)
433 ->withIsDisabled(false);
435 // If this object supports subtyping, we edit it with a form of the same
436 // subtype: so "bug" tasks get edited with "bug" forms.
437 if ($object instanceof PhabricatorEditEngineSubtypeInterface) {
438 $query->withSubtypes(
439 array(
440 $object->getEditEngineSubtype(),
444 return $this->loadEditEngineConfigurationWithQuery(
445 $query,
446 'getEditSortKey');
449 final public function getBuiltinEngineConfigurations() {
450 $configurations = $this->newBuiltinEngineConfigurations();
452 if (!$configurations) {
453 throw new Exception(
454 pht(
455 'EditEngine ("%s") returned no builtin engine configurations, but '.
456 'an edit engine must have at least one configuration.',
457 get_class($this)));
460 assert_instances_of($configurations, 'PhabricatorEditEngineConfiguration');
462 $has_default = false;
463 foreach ($configurations as $config) {
464 if ($config->getBuiltinKey() == self::EDITENGINECONFIG_DEFAULT) {
465 $has_default = true;
469 if (!$has_default) {
470 $first = head($configurations);
471 if (!$first->getBuiltinKey()) {
472 $first
473 ->setBuiltinKey(self::EDITENGINECONFIG_DEFAULT)
474 ->setIsDefault(true)
475 ->setIsEdit(true);
477 $first_name = $first->getName();
479 if ($first_name === null || $first_name === '') {
480 $first->setName($this->getObjectCreateShortText());
482 } else {
483 throw new Exception(
484 pht(
485 'EditEngine ("%s") returned builtin engine configurations, '.
486 'but none are marked as default and the first configuration has '.
487 'a different builtin key already. Mark a builtin as default or '.
488 'omit the key from the first configuration',
489 get_class($this)));
493 $builtins = array();
494 foreach ($configurations as $key => $config) {
495 $builtin_key = $config->getBuiltinKey();
497 if ($builtin_key === null) {
498 throw new Exception(
499 pht(
500 'EditEngine ("%s") returned builtin engine configurations, '.
501 'but one (with key "%s") is missing a builtin key. Provide a '.
502 'builtin key for each configuration (you can omit it from the '.
503 'first configuration in the list to automatically assign the '.
504 'default key).',
505 get_class($this),
506 $key));
509 if (isset($builtins[$builtin_key])) {
510 throw new Exception(
511 pht(
512 'EditEngine ("%s") returned builtin engine configurations, '.
513 'but at least two specify the same builtin key ("%s"). Engines '.
514 'must have unique builtin keys.',
515 get_class($this),
516 $builtin_key));
519 $builtins[$builtin_key] = $config;
523 return $builtins;
526 protected function newBuiltinEngineConfigurations() {
527 return array(
528 $this->newConfiguration(),
532 final protected function newConfiguration() {
533 return PhabricatorEditEngineConfiguration::initializeNewConfiguration(
534 $this->getViewer(),
535 $this);
539 /* -( Managing URIs )------------------------------------------------------ */
543 * @task uri
545 abstract protected function getObjectViewURI($object);
549 * @task uri
551 protected function getObjectCreateCancelURI($object) {
552 return $this->getApplication()->getApplicationURI();
557 * @task uri
559 protected function getEditorURI() {
560 return $this->getApplication()->getApplicationURI('edit/');
565 * @task uri
567 protected function getObjectEditCancelURI($object) {
568 return $this->getObjectViewURI($object);
572 * @task uri
574 public function getCreateURI($form_key) {
575 try {
576 $create_uri = $this->getEditURI(null, "form/{$form_key}/");
577 } catch (Exception $ex) {
578 $create_uri = null;
581 return $create_uri;
585 * @task uri
587 public function getEditURI($object = null, $path = null) {
588 $parts = array();
590 $parts[] = $this->getEditorURI();
592 if ($object && $object->getID()) {
593 $parts[] = $object->getID().'/';
596 if ($path !== null) {
597 $parts[] = $path;
600 return implode('', $parts);
603 public function getEffectiveObjectViewURI($object) {
604 if ($this->getIsCreate()) {
605 return $this->getObjectViewURI($object);
608 $page = $this->getSelectedPage();
609 if ($page) {
610 $view_uri = $page->getViewURI();
611 if ($view_uri !== null) {
612 return $view_uri;
616 return $this->getObjectViewURI($object);
619 public function getEffectiveObjectEditDoneURI($object) {
620 return $this->getEffectiveObjectViewURI($object);
623 public function getEffectiveObjectEditCancelURI($object) {
624 $page = $this->getSelectedPage();
625 if ($page) {
626 $view_uri = $page->getViewURI();
627 if ($view_uri !== null) {
628 return $view_uri;
632 return $this->getObjectEditCancelURI($object);
636 /* -( Creating and Loading Objects )--------------------------------------- */
640 * Initialize a new object for creation.
642 * @return object Newly initialized object.
643 * @task load
645 abstract protected function newEditableObject();
649 * Build an empty query for objects.
651 * @return PhabricatorPolicyAwareQuery Query.
652 * @task load
654 abstract protected function newObjectQuery();
658 * Test if this workflow is creating a new object or editing an existing one.
660 * @return bool True if a new object is being created.
661 * @task load
663 final public function getIsCreate() {
664 return $this->isCreate;
668 * Initialize a new object for object creation via Conduit.
670 * @return object Newly initialized object.
671 * @param list<wild> Raw transactions.
672 * @task load
674 protected function newEditableObjectFromConduit(array $raw_xactions) {
675 return $this->newEditableObject();
679 * Initialize a new object for documentation creation.
681 * @return object Newly initialized object.
682 * @task load
684 protected function newEditableObjectForDocumentation() {
685 return $this->newEditableObject();
689 * Flag this workflow as a create or edit.
691 * @param bool True if this is a create workflow.
692 * @return this
693 * @task load
695 private function setIsCreate($is_create) {
696 $this->isCreate = $is_create;
697 return $this;
702 * Try to load an object by ID, PHID, or monogram. This is done primarily
703 * to make Conduit a little easier to use.
705 * @param wild ID, PHID, or monogram.
706 * @param list<const> List of required capability constants, or omit for
707 * defaults.
708 * @return object Corresponding editable object.
709 * @task load
711 private function newObjectFromIdentifier(
712 $identifier,
713 array $capabilities = array()) {
714 if (is_int($identifier) || ctype_digit($identifier)) {
715 $object = $this->newObjectFromID($identifier, $capabilities);
717 if (!$object) {
718 throw new Exception(
719 pht(
720 'No object exists with ID "%s".',
721 $identifier));
724 return $object;
727 $type_unknown = PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN;
728 if (phid_get_type($identifier) != $type_unknown) {
729 $object = $this->newObjectFromPHID($identifier, $capabilities);
731 if (!$object) {
732 throw new Exception(
733 pht(
734 'No object exists with PHID "%s".',
735 $identifier));
738 return $object;
741 $target = id(new PhabricatorObjectQuery())
742 ->setViewer($this->getViewer())
743 ->withNames(array($identifier))
744 ->executeOne();
745 if (!$target) {
746 throw new Exception(
747 pht(
748 'Monogram "%s" does not identify a valid object.',
749 $identifier));
752 $expect = $this->newEditableObject();
753 $expect_class = get_class($expect);
754 $target_class = get_class($target);
755 if ($expect_class !== $target_class) {
756 throw new Exception(
757 pht(
758 'Monogram "%s" identifies an object of the wrong type. Loaded '.
759 'object has class "%s", but this editor operates on objects of '.
760 'type "%s".',
761 $identifier,
762 $target_class,
763 $expect_class));
766 // Load the object by PHID using this engine's standard query. This makes
767 // sure it's really valid, goes through standard policy check logic, and
768 // picks up any `need...()` clauses we want it to load with.
770 $object = $this->newObjectFromPHID($target->getPHID(), $capabilities);
771 if (!$object) {
772 throw new Exception(
773 pht(
774 'Failed to reload object identified by monogram "%s" when '.
775 'querying by PHID.',
776 $identifier));
779 return $object;
783 * Load an object by ID.
785 * @param int Object ID.
786 * @param list<const> List of required capability constants, or omit for
787 * defaults.
788 * @return object|null Object, or null if no such object exists.
789 * @task load
791 private function newObjectFromID($id, array $capabilities = array()) {
792 $query = $this->newObjectQuery()
793 ->withIDs(array($id));
795 return $this->newObjectFromQuery($query, $capabilities);
800 * Load an object by PHID.
802 * @param phid Object PHID.
803 * @param list<const> List of required capability constants, or omit for
804 * defaults.
805 * @return object|null Object, or null if no such object exists.
806 * @task load
808 private function newObjectFromPHID($phid, array $capabilities = array()) {
809 $query = $this->newObjectQuery()
810 ->withPHIDs(array($phid));
812 return $this->newObjectFromQuery($query, $capabilities);
817 * Load an object given a configured query.
819 * @param PhabricatorPolicyAwareQuery Configured query.
820 * @param list<const> List of required capability constants, or omit for
821 * defaults.
822 * @return object|null Object, or null if no such object exists.
823 * @task load
825 private function newObjectFromQuery(
826 PhabricatorPolicyAwareQuery $query,
827 array $capabilities = array()) {
829 $viewer = $this->getViewer();
831 if (!$capabilities) {
832 $capabilities = array(
833 PhabricatorPolicyCapability::CAN_VIEW,
834 PhabricatorPolicyCapability::CAN_EDIT,
838 $object = $query
839 ->setViewer($viewer)
840 ->requireCapabilities($capabilities)
841 ->executeOne();
842 if (!$object) {
843 return null;
846 return $object;
851 * Verify that an object is appropriate for editing.
853 * @param wild Loaded value.
854 * @return void
855 * @task load
857 private function validateObject($object) {
858 if (!$object || !is_object($object)) {
859 throw new Exception(
860 pht(
861 'EditEngine "%s" created or loaded an invalid object: object must '.
862 'actually be an object, but is of some other type ("%s").',
863 get_class($this),
864 gettype($object)));
867 if (!($object instanceof PhabricatorApplicationTransactionInterface)) {
868 throw new Exception(
869 pht(
870 'EditEngine "%s" created or loaded an invalid object: object (of '.
871 'class "%s") must implement "%s", but does not.',
872 get_class($this),
873 get_class($object),
874 'PhabricatorApplicationTransactionInterface'));
879 /* -( Responding to Web Requests )----------------------------------------- */
882 final public function buildResponse() {
883 $viewer = $this->getViewer();
884 $controller = $this->getController();
885 $request = $controller->getRequest();
887 $action = $this->getEditAction();
889 $capabilities = array();
890 $use_default = false;
891 $require_create = true;
892 switch ($action) {
893 case 'comment':
894 $capabilities = array(
895 PhabricatorPolicyCapability::CAN_VIEW,
897 $use_default = true;
898 break;
899 case 'parameters':
900 $use_default = true;
901 break;
902 case 'nodefault':
903 case 'nocreate':
904 case 'nomanage':
905 $require_create = false;
906 break;
907 default:
908 break;
911 $object = $this->getTargetObject();
912 if (!$object) {
913 $id = $request->getURIData('id');
915 if ($id) {
916 $this->setIsCreate(false);
917 $object = $this->newObjectFromID($id, $capabilities);
918 if (!$object) {
919 return new Aphront404Response();
921 } else {
922 // Make sure the viewer has permission to create new objects of
923 // this type if we're going to create a new object.
924 if ($require_create) {
925 $this->requireCreateCapability();
928 $this->setIsCreate(true);
929 $object = $this->newEditableObject();
931 } else {
932 $id = $object->getID();
935 $this->validateObject($object);
937 if ($use_default) {
938 $config = $this->loadDefaultConfiguration();
939 if (!$config) {
940 return new Aphront404Response();
942 } else {
943 $form_key = $request->getURIData('formKey');
944 if (strlen($form_key)) {
945 $config = $this->loadEditEngineConfigurationWithIdentifier($form_key);
947 if (!$config) {
948 return new Aphront404Response();
951 if ($id && !$config->getIsEdit()) {
952 return $this->buildNotEditFormRespose($object, $config);
954 } else {
955 if ($id) {
956 $config = $this->loadDefaultEditConfiguration($object);
957 if (!$config) {
958 return $this->buildNoEditResponse($object);
960 } else {
961 $config = $this->loadDefaultCreateConfiguration();
962 if (!$config) {
963 return $this->buildNoCreateResponse($object);
969 if ($config->getIsDisabled()) {
970 return $this->buildDisabledFormResponse($object, $config);
973 $page_key = $request->getURIData('pageKey');
974 if (!strlen($page_key)) {
975 $pages = $this->getPages($object);
976 if ($pages) {
977 $page_key = head_key($pages);
981 if (strlen($page_key)) {
982 $page = $this->selectPage($object, $page_key);
983 if (!$page) {
984 return new Aphront404Response();
988 switch ($action) {
989 case 'parameters':
990 return $this->buildParametersResponse($object);
991 case 'nodefault':
992 return $this->buildNoDefaultResponse($object);
993 case 'nocreate':
994 return $this->buildNoCreateResponse($object);
995 case 'nomanage':
996 return $this->buildNoManageResponse($object);
997 case 'comment':
998 return $this->buildCommentResponse($object);
999 default:
1000 return $this->buildEditResponse($object);
1004 private function buildCrumbs($object, $final = false) {
1005 $controller = $this->getController();
1007 $crumbs = $controller->buildApplicationCrumbsForEditEngine();
1008 if ($this->getIsCreate()) {
1009 $create_text = $this->getObjectCreateShortText();
1010 if ($final) {
1011 $crumbs->addTextCrumb($create_text);
1012 } else {
1013 $edit_uri = $this->getEditURI($object);
1014 $crumbs->addTextCrumb($create_text, $edit_uri);
1016 } else {
1017 $crumbs->addTextCrumb(
1018 $this->getObjectEditShortText($object),
1019 $this->getEffectiveObjectViewURI($object));
1021 $edit_text = pht('Edit');
1022 if ($final) {
1023 $crumbs->addTextCrumb($edit_text);
1024 } else {
1025 $edit_uri = $this->getEditURI($object);
1026 $crumbs->addTextCrumb($edit_text, $edit_uri);
1030 return $crumbs;
1033 private function buildEditResponse($object) {
1034 $viewer = $this->getViewer();
1035 $controller = $this->getController();
1036 $request = $controller->getRequest();
1038 $fields = $this->buildEditFields($object);
1039 $template = $object->getApplicationTransactionTemplate();
1041 $page_state = new PhabricatorEditEnginePageState();
1043 if ($this->getIsCreate()) {
1044 $cancel_uri = $this->getObjectCreateCancelURI($object);
1045 $submit_button = $this->getObjectCreateButtonText($object);
1047 $page_state->setIsCreate(true);
1048 } else {
1049 $cancel_uri = $this->getEffectiveObjectEditCancelURI($object);
1050 $submit_button = $this->getObjectEditButtonText($object);
1053 $config = $this->getEditEngineConfiguration()
1054 ->attachEngine($this);
1056 // NOTE: Don't prompt users to override locks when creating objects,
1057 // even if the default settings would create a locked object.
1059 $can_interact = PhabricatorPolicyFilter::canInteract($viewer, $object);
1060 if (!$can_interact &&
1061 !$this->getIsCreate() &&
1062 !$request->getBool('editEngine') &&
1063 !$request->getBool('overrideLock')) {
1065 $lock = PhabricatorEditEngineLock::newForObject($viewer, $object);
1067 $dialog = $this->getController()
1068 ->newDialog()
1069 ->addHiddenInput('overrideLock', true)
1070 ->setDisableWorkflowOnSubmit(true)
1071 ->addCancelButton($cancel_uri);
1073 return $lock->willPromptUserForLockOverrideWithDialog($dialog);
1076 $validation_exception = null;
1077 if ($request->isFormOrHisecPost() && $request->getBool('editEngine')) {
1078 $page_state->setIsSubmit(true);
1080 $submit_fields = $fields;
1082 foreach ($submit_fields as $key => $field) {
1083 if (!$field->shouldGenerateTransactionsFromSubmit()) {
1084 unset($submit_fields[$key]);
1085 continue;
1089 // Before we read the submitted values, store a copy of what we would
1090 // use if the form was empty so we can figure out which transactions are
1091 // just setting things to their default values for the current form.
1092 $defaults = array();
1093 foreach ($submit_fields as $key => $field) {
1094 $defaults[$key] = $field->getValueForTransaction();
1097 foreach ($submit_fields as $key => $field) {
1098 $field->setIsSubmittedForm(true);
1100 if (!$field->shouldReadValueFromSubmit()) {
1101 continue;
1104 $field->readValueFromSubmit($request);
1107 $xactions = array();
1109 if ($this->getIsCreate()) {
1110 $xactions[] = id(clone $template)
1111 ->setTransactionType(PhabricatorTransactions::TYPE_CREATE);
1113 if ($this->supportsSubtypes()) {
1114 $xactions[] = id(clone $template)
1115 ->setTransactionType(PhabricatorTransactions::TYPE_SUBTYPE)
1116 ->setNewValue($config->getSubtype());
1120 foreach ($submit_fields as $key => $field) {
1121 $field_value = $field->getValueForTransaction();
1123 $type_xactions = $field->generateTransactions(
1124 clone $template,
1125 array(
1126 'value' => $field_value,
1129 foreach ($type_xactions as $type_xaction) {
1130 $default = $defaults[$key];
1132 if ($default === $field->getValueForTransaction()) {
1133 $type_xaction->setIsDefaultTransaction(true);
1136 $xactions[] = $type_xaction;
1140 $editor = $object->getApplicationTransactionEditor()
1141 ->setActor($viewer)
1142 ->setContentSourceFromRequest($request)
1143 ->setCancelURI($cancel_uri)
1144 ->setContinueOnNoEffect(true);
1146 try {
1147 $xactions = $this->willApplyTransactions($object, $xactions);
1149 $editor->applyTransactions($object, $xactions);
1151 $this->didApplyTransactions($object, $xactions);
1153 return $this->newEditResponse($request, $object, $xactions);
1154 } catch (PhabricatorApplicationTransactionValidationException $ex) {
1155 $validation_exception = $ex;
1157 foreach ($fields as $field) {
1158 $message = $this->getValidationExceptionShortMessage($ex, $field);
1159 if ($message === null) {
1160 continue;
1163 $field->setControlError($message);
1166 $page_state->setIsError(true);
1168 } else {
1169 if ($this->getIsCreate()) {
1170 $template = $request->getStr('template');
1172 if (strlen($template)) {
1173 $template_object = $this->newObjectFromIdentifier(
1174 $template,
1175 array(
1176 PhabricatorPolicyCapability::CAN_VIEW,
1178 if (!$template_object) {
1179 return new Aphront404Response();
1181 } else {
1182 $template_object = null;
1185 if ($template_object) {
1186 $copy_fields = $this->buildEditFields($template_object);
1187 $copy_fields = mpull($copy_fields, null, 'getKey');
1188 foreach ($copy_fields as $copy_key => $copy_field) {
1189 if (!$copy_field->getIsCopyable()) {
1190 unset($copy_fields[$copy_key]);
1193 } else {
1194 $copy_fields = array();
1197 foreach ($fields as $field) {
1198 if (!$field->shouldReadValueFromRequest()) {
1199 continue;
1202 $field_key = $field->getKey();
1203 if (isset($copy_fields[$field_key])) {
1204 $field->readValueFromField($copy_fields[$field_key]);
1207 $field->readValueFromRequest($request);
1212 $action_button = $this->buildEditFormActionButton($object);
1214 if ($this->getIsCreate()) {
1215 $header_text = $this->getFormHeaderText($object);
1216 } else {
1217 $header_text = $this->getObjectEditTitleText($object);
1220 $show_preview = !$request->isAjax();
1222 if ($show_preview) {
1223 $previews = array();
1224 foreach ($fields as $field) {
1225 $preview = $field->getPreviewPanel();
1226 if (!$preview) {
1227 continue;
1230 $control_id = $field->getControlID();
1232 $preview
1233 ->setControlID($control_id)
1234 ->setPreviewURI('/transactions/remarkuppreview/');
1236 $previews[] = $preview;
1238 } else {
1239 $previews = array();
1242 $form = $this->buildEditForm($object, $fields);
1244 $crumbs = $this->buildCrumbs($object, $final = true);
1245 $crumbs->setBorder(true);
1247 if ($request->isAjax()) {
1248 return $this->getController()
1249 ->newDialog()
1250 ->setWidth(AphrontDialogView::WIDTH_FULL)
1251 ->setTitle($header_text)
1252 ->setValidationException($validation_exception)
1253 ->appendForm($form)
1254 ->addCancelButton($cancel_uri)
1255 ->addSubmitButton($submit_button);
1258 $box_header = id(new PHUIHeaderView())
1259 ->setHeader($header_text);
1261 if ($action_button) {
1262 $box_header->addActionLink($action_button);
1265 $request_submit_key = $request->getSubmitKey();
1266 $engine_submit_key = $this->getEditEngineSubmitKey();
1268 if ($request_submit_key === $engine_submit_key) {
1269 $page_state->setIsSubmit(true);
1270 $page_state->setIsSave(true);
1273 $head = $this->newEditFormHeadContent($page_state);
1274 $tail = $this->newEditFormTailContent($page_state);
1276 $box = id(new PHUIObjectBoxView())
1277 ->setUser($viewer)
1278 ->setHeader($box_header)
1279 ->setValidationException($validation_exception)
1280 ->setBackground(PHUIObjectBoxView::WHITE_CONFIG)
1281 ->appendChild($form);
1283 $content = array(
1284 $head,
1285 $box,
1286 $previews,
1287 $tail,
1290 $view = new PHUITwoColumnView();
1292 $page_header = $this->getPageHeader($object);
1293 if ($page_header) {
1294 $view->setHeader($page_header);
1297 $view->setFooter($content);
1299 $page = $controller->newPage()
1300 ->setTitle($header_text)
1301 ->setCrumbs($crumbs)
1302 ->appendChild($view);
1304 $navigation = $this->getNavigation();
1305 if ($navigation) {
1306 $page->setNavigation($navigation);
1309 return $page;
1312 protected function newEditFormHeadContent(
1313 PhabricatorEditEnginePageState $state) {
1314 return null;
1317 protected function newEditFormTailContent(
1318 PhabricatorEditEnginePageState $state) {
1319 return null;
1322 protected function newEditResponse(
1323 AphrontRequest $request,
1324 $object,
1325 array $xactions) {
1327 $submit_cookie = PhabricatorCookies::COOKIE_SUBMIT;
1328 $submit_key = $this->getEditEngineSubmitKey();
1330 $request->setTemporaryCookie($submit_cookie, $submit_key);
1332 return id(new AphrontRedirectResponse())
1333 ->setURI($this->getEffectiveObjectEditDoneURI($object));
1336 private function getEditEngineSubmitKey() {
1337 return 'edit-engine/'.$this->getEngineKey();
1340 private function buildEditForm($object, array $fields) {
1341 $viewer = $this->getViewer();
1342 $controller = $this->getController();
1343 $request = $controller->getRequest();
1345 $fields = $this->willBuildEditForm($object, $fields);
1347 $request_path = $request->getPath();
1349 $form = id(new AphrontFormView())
1350 ->setUser($viewer)
1351 ->setAction($request_path)
1352 ->addHiddenInput('editEngine', 'true');
1354 foreach ($this->contextParameters as $param) {
1355 $form->addHiddenInput($param, $request->getStr($param));
1358 $requires_mfa = false;
1359 if ($object instanceof PhabricatorEditEngineMFAInterface) {
1360 $mfa_engine = PhabricatorEditEngineMFAEngine::newEngineForObject($object)
1361 ->setViewer($viewer);
1362 $requires_mfa = $mfa_engine->shouldRequireMFA();
1365 if ($requires_mfa) {
1366 $message = pht(
1367 'You will be required to provide multi-factor credentials to make '.
1368 'changes.');
1369 $form->appendChild(
1370 id(new PHUIInfoView())
1371 ->setSeverity(PHUIInfoView::SEVERITY_MFA)
1372 ->setErrors(array($message)));
1374 // TODO: This should also set workflow on the form, so the user doesn't
1375 // lose any form data if they "Cancel". However, Maniphest currently
1376 // overrides "newEditResponse()" if the request is Ajax and returns a
1377 // bag of view data. This can reasonably be cleaned up when workboards
1378 // get their next iteration.
1381 foreach ($fields as $field) {
1382 if (!$field->getIsFormField()) {
1383 continue;
1386 $field->appendToForm($form);
1389 if ($this->getIsCreate()) {
1390 $cancel_uri = $this->getObjectCreateCancelURI($object);
1391 $submit_button = $this->getObjectCreateButtonText($object);
1392 } else {
1393 $cancel_uri = $this->getEffectiveObjectEditCancelURI($object);
1394 $submit_button = $this->getObjectEditButtonText($object);
1397 if (!$request->isAjax()) {
1398 $buttons = id(new AphrontFormSubmitControl())
1399 ->setValue($submit_button);
1401 if ($cancel_uri) {
1402 $buttons->addCancelButton($cancel_uri);
1405 $form->appendControl($buttons);
1408 return $form;
1411 protected function willBuildEditForm($object, array $fields) {
1412 return $fields;
1415 private function buildEditFormActionButton($object) {
1416 if (!$this->isEngineConfigurable()) {
1417 return null;
1420 $viewer = $this->getViewer();
1422 $action_view = id(new PhabricatorActionListView())
1423 ->setUser($viewer);
1425 foreach ($this->buildEditFormActions($object) as $action) {
1426 $action_view->addAction($action);
1429 $action_button = id(new PHUIButtonView())
1430 ->setTag('a')
1431 ->setText(pht('Configure Form'))
1432 ->setHref('#')
1433 ->setIcon('fa-gear')
1434 ->setDropdownMenu($action_view);
1436 return $action_button;
1439 private function buildEditFormActions($object) {
1440 $actions = array();
1442 if ($this->supportsEditEngineConfiguration()) {
1443 $engine_key = $this->getEngineKey();
1444 $config = $this->getEditEngineConfiguration();
1446 $can_manage = PhabricatorPolicyFilter::hasCapability(
1447 $this->getViewer(),
1448 $config,
1449 PhabricatorPolicyCapability::CAN_EDIT);
1451 if ($can_manage) {
1452 $manage_uri = $config->getURI();
1453 } else {
1454 $manage_uri = $this->getEditURI(null, 'nomanage/');
1457 $view_uri = "/transactions/editengine/{$engine_key}/";
1459 $actions[] = id(new PhabricatorActionView())
1460 ->setLabel(true)
1461 ->setName(pht('Configuration'));
1463 $actions[] = id(new PhabricatorActionView())
1464 ->setName(pht('View Form Configurations'))
1465 ->setIcon('fa-list-ul')
1466 ->setHref($view_uri);
1468 $actions[] = id(new PhabricatorActionView())
1469 ->setName(pht('Edit Form Configuration'))
1470 ->setIcon('fa-pencil')
1471 ->setHref($manage_uri)
1472 ->setDisabled(!$can_manage)
1473 ->setWorkflow(!$can_manage);
1476 $actions[] = id(new PhabricatorActionView())
1477 ->setLabel(true)
1478 ->setName(pht('Documentation'));
1480 $actions[] = id(new PhabricatorActionView())
1481 ->setName(pht('Using HTTP Parameters'))
1482 ->setIcon('fa-book')
1483 ->setHref($this->getEditURI($object, 'parameters/'));
1485 $doc_href = PhabricatorEnv::getDoclink('User Guide: Customizing Forms');
1486 $actions[] = id(new PhabricatorActionView())
1487 ->setName(pht('User Guide: Customizing Forms'))
1488 ->setIcon('fa-book')
1489 ->setHref($doc_href);
1491 return $actions;
1495 public function newNUXButton($text) {
1496 $specs = $this->newCreateActionSpecifications(array());
1497 $head = head($specs);
1499 return id(new PHUIButtonView())
1500 ->setTag('a')
1501 ->setText($text)
1502 ->setHref($head['uri'])
1503 ->setDisabled($head['disabled'])
1504 ->setWorkflow($head['workflow'])
1505 ->setColor(PHUIButtonView::GREEN);
1509 final public function addActionToCrumbs(
1510 PHUICrumbsView $crumbs,
1511 array $parameters = array()) {
1512 $viewer = $this->getViewer();
1514 $specs = $this->newCreateActionSpecifications($parameters);
1516 $head = head($specs);
1517 $menu_uri = $head['uri'];
1519 $dropdown = null;
1520 if (count($specs) > 1) {
1521 $menu_icon = 'fa-caret-square-o-down';
1522 $menu_name = $this->getObjectCreateShortText();
1523 $workflow = false;
1524 $disabled = false;
1526 $dropdown = id(new PhabricatorActionListView())
1527 ->setUser($viewer);
1529 foreach ($specs as $spec) {
1530 $dropdown->addAction(
1531 id(new PhabricatorActionView())
1532 ->setName($spec['name'])
1533 ->setIcon($spec['icon'])
1534 ->setHref($spec['uri'])
1535 ->setDisabled($head['disabled'])
1536 ->setWorkflow($head['workflow']));
1539 } else {
1540 $menu_icon = $head['icon'];
1541 $menu_name = $head['name'];
1543 $workflow = $head['workflow'];
1544 $disabled = $head['disabled'];
1547 $action = id(new PHUIListItemView())
1548 ->setName($menu_name)
1549 ->setHref($menu_uri)
1550 ->setIcon($menu_icon)
1551 ->setWorkflow($workflow)
1552 ->setDisabled($disabled);
1554 if ($dropdown) {
1555 $action->setDropdownMenu($dropdown);
1558 $crumbs->addAction($action);
1563 * Build a raw description of available "Create New Object" UI options so
1564 * other methods can build menus or buttons.
1566 public function newCreateActionSpecifications(array $parameters) {
1567 $viewer = $this->getViewer();
1569 $can_create = $this->hasCreateCapability();
1570 if ($can_create) {
1571 $configs = $this->loadUsableConfigurationsForCreate();
1572 } else {
1573 $configs = array();
1576 $disabled = false;
1577 $workflow = false;
1579 $menu_icon = 'fa-plus-square';
1580 $specs = array();
1581 if (!$configs) {
1582 if ($viewer->isLoggedIn()) {
1583 $disabled = true;
1584 } else {
1585 // If the viewer isn't logged in, assume they'll get hit with a login
1586 // dialog and are likely able to create objects after they log in.
1587 $disabled = false;
1589 $workflow = true;
1591 if ($can_create) {
1592 $create_uri = $this->getEditURI(null, 'nodefault/');
1593 } else {
1594 $create_uri = $this->getEditURI(null, 'nocreate/');
1597 $specs[] = array(
1598 'name' => $this->getObjectCreateShortText(),
1599 'uri' => $create_uri,
1600 'icon' => $menu_icon,
1601 'disabled' => $disabled,
1602 'workflow' => $workflow,
1604 } else {
1605 foreach ($configs as $config) {
1606 $config_uri = $config->getCreateURI();
1608 if ($parameters) {
1609 $config_uri = (string)new PhutilURI($config_uri, $parameters);
1612 $specs[] = array(
1613 'name' => $config->getDisplayName(),
1614 'uri' => $config_uri,
1615 'icon' => 'fa-plus',
1616 'disabled' => false,
1617 'workflow' => false,
1622 return $specs;
1625 final public function buildEditEngineCommentView($object) {
1626 $config = $this->loadDefaultEditConfiguration($object);
1628 if (!$config) {
1629 // TODO: This just nukes the entire comment form if you don't have access
1630 // to any edit forms. We might want to tailor this UX a bit.
1631 return id(new PhabricatorApplicationTransactionCommentView())
1632 ->setNoPermission(true);
1635 $viewer = $this->getViewer();
1637 $can_interact = PhabricatorPolicyFilter::canInteract($viewer, $object);
1638 if (!$can_interact) {
1639 $lock = PhabricatorEditEngineLock::newForObject($viewer, $object);
1641 return id(new PhabricatorApplicationTransactionCommentView())
1642 ->setEditEngineLock($lock);
1645 $object_phid = $object->getPHID();
1646 $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
1648 if ($is_serious) {
1649 $header_text = $this->getCommentViewSeriousHeaderText($object);
1650 $button_text = $this->getCommentViewSeriousButtonText($object);
1651 } else {
1652 $header_text = $this->getCommentViewHeaderText($object);
1653 $button_text = $this->getCommentViewButtonText($object);
1656 $comment_uri = $this->getEditURI($object, 'comment/');
1658 $requires_mfa = false;
1659 if ($object instanceof PhabricatorEditEngineMFAInterface) {
1660 $mfa_engine = PhabricatorEditEngineMFAEngine::newEngineForObject($object)
1661 ->setViewer($viewer);
1662 $requires_mfa = $mfa_engine->shouldRequireMFA();
1665 $view = id(new PhabricatorApplicationTransactionCommentView())
1666 ->setUser($viewer)
1667 ->setObjectPHID($object_phid)
1668 ->setHeaderText($header_text)
1669 ->setAction($comment_uri)
1670 ->setRequiresMFA($requires_mfa)
1671 ->setSubmitButtonName($button_text);
1673 $draft = PhabricatorVersionedDraft::loadDraft(
1674 $object_phid,
1675 $viewer->getPHID());
1676 if ($draft) {
1677 $view->setVersionedDraft($draft);
1680 $view->setCurrentVersion($this->loadDraftVersion($object));
1682 $fields = $this->buildEditFields($object);
1684 $can_edit = PhabricatorPolicyFilter::hasCapability(
1685 $viewer,
1686 $object,
1687 PhabricatorPolicyCapability::CAN_EDIT);
1689 $comment_actions = array();
1690 foreach ($fields as $field) {
1691 if (!$field->shouldGenerateTransactionsFromComment()) {
1692 continue;
1695 if (!$can_edit) {
1696 if (!$field->getCanApplyWithoutEditCapability()) {
1697 continue;
1701 $comment_action = $field->getCommentAction();
1702 if (!$comment_action) {
1703 continue;
1706 $key = $comment_action->getKey();
1708 // TODO: Validate these better.
1710 $comment_actions[$key] = $comment_action;
1713 $comment_actions = msortv($comment_actions, 'getSortVector');
1715 $view->setCommentActions($comment_actions);
1717 $comment_groups = $this->newCommentActionGroups();
1718 $view->setCommentActionGroups($comment_groups);
1720 return $view;
1723 protected function loadDraftVersion($object) {
1724 $viewer = $this->getViewer();
1726 if (!$viewer->isLoggedIn()) {
1727 return null;
1730 $template = $object->getApplicationTransactionTemplate();
1731 $conn_r = $template->establishConnection('r');
1733 // Find the most recent transaction the user has written. We'll use this
1734 // as a version number to make sure that out-of-date drafts get discarded.
1735 $result = queryfx_one(
1736 $conn_r,
1737 'SELECT id AS version FROM %T
1738 WHERE objectPHID = %s AND authorPHID = %s
1739 ORDER BY id DESC LIMIT 1',
1740 $template->getTableName(),
1741 $object->getPHID(),
1742 $viewer->getPHID());
1744 if ($result) {
1745 return (int)$result['version'];
1746 } else {
1747 return null;
1752 /* -( Responding to HTTP Parameter Requests )------------------------------ */
1756 * Respond to a request for documentation on HTTP parameters.
1758 * @param object Editable object.
1759 * @return AphrontResponse Response object.
1760 * @task http
1762 private function buildParametersResponse($object) {
1763 $controller = $this->getController();
1764 $viewer = $this->getViewer();
1765 $request = $controller->getRequest();
1766 $fields = $this->buildEditFields($object);
1768 $crumbs = $this->buildCrumbs($object);
1769 $crumbs->addTextCrumb(pht('HTTP Parameters'));
1770 $crumbs->setBorder(true);
1772 $header_text = pht(
1773 'HTTP Parameters: %s',
1774 $this->getObjectCreateShortText());
1776 $header = id(new PHUIHeaderView())
1777 ->setHeader($header_text);
1779 $help_view = id(new PhabricatorApplicationEditHTTPParameterHelpView())
1780 ->setUser($viewer)
1781 ->setFields($fields);
1783 $document = id(new PHUIDocumentView())
1784 ->setUser($viewer)
1785 ->setHeader($header)
1786 ->appendChild($help_view);
1788 return $controller->newPage()
1789 ->setTitle(pht('HTTP Parameters'))
1790 ->setCrumbs($crumbs)
1791 ->appendChild($document);
1795 private function buildError($object, $title, $body) {
1796 $cancel_uri = $this->getObjectCreateCancelURI($object);
1798 $dialog = $this->getController()
1799 ->newDialog()
1800 ->addCancelButton($cancel_uri);
1802 if ($title !== null) {
1803 $dialog->setTitle($title);
1806 if ($body !== null) {
1807 $dialog->appendParagraph($body);
1810 return $dialog;
1814 private function buildNoDefaultResponse($object) {
1815 return $this->buildError(
1816 $object,
1817 pht('No Default Create Forms'),
1818 pht(
1819 'This application is not configured with any forms for creating '.
1820 'objects that are visible to you and enabled.'));
1823 private function buildNoCreateResponse($object) {
1824 return $this->buildError(
1825 $object,
1826 pht('No Create Permission'),
1827 pht('You do not have permission to create these objects.'));
1830 private function buildNoManageResponse($object) {
1831 return $this->buildError(
1832 $object,
1833 pht('No Manage Permission'),
1834 pht(
1835 'You do not have permission to configure forms for this '.
1836 'application.'));
1839 private function buildNoEditResponse($object) {
1840 return $this->buildError(
1841 $object,
1842 pht('No Edit Forms'),
1843 pht(
1844 'You do not have access to any forms which are enabled and marked '.
1845 'as edit forms.'));
1848 private function buildNotEditFormRespose($object, $config) {
1849 return $this->buildError(
1850 $object,
1851 pht('Not an Edit Form'),
1852 pht(
1853 'This form ("%s") is not marked as an edit form, so '.
1854 'it can not be used to edit objects.',
1855 $config->getName()));
1858 private function buildDisabledFormResponse($object, $config) {
1859 return $this->buildError(
1860 $object,
1861 pht('Form Disabled'),
1862 pht(
1863 'This form ("%s") has been disabled, so it can not be used.',
1864 $config->getName()));
1867 private function buildLockedObjectResponse($object) {
1868 $dialog = $this->buildError($object, null, null);
1869 $viewer = $this->getViewer();
1871 $lock = PhabricatorEditEngineLock::newForObject($viewer, $object);
1872 return $lock->willBlockUserInteractionWithDialog($dialog);
1875 private function buildCommentResponse($object) {
1876 $viewer = $this->getViewer();
1878 if ($this->getIsCreate()) {
1879 return new Aphront404Response();
1882 $controller = $this->getController();
1883 $request = $controller->getRequest();
1885 // NOTE: We handle hisec inside the transaction editor with "Sign With MFA"
1886 // comment actions.
1887 if (!$request->isFormOrHisecPost()) {
1888 return new Aphront400Response();
1891 $can_interact = PhabricatorPolicyFilter::canInteract($viewer, $object);
1892 if (!$can_interact) {
1893 return $this->buildLockedObjectResponse($object);
1896 $config = $this->loadDefaultEditConfiguration($object);
1897 if (!$config) {
1898 return new Aphront404Response();
1901 $fields = $this->buildEditFields($object);
1903 $is_preview = $request->isPreviewRequest();
1904 $view_uri = $this->getEffectiveObjectViewURI($object);
1906 $template = $object->getApplicationTransactionTemplate();
1907 $comment_template = $template->getApplicationTransactionCommentObject();
1909 $comment_text = $request->getStr('comment');
1911 $comment_metadata = $request->getStr('comment_metadata');
1912 if (strlen($comment_metadata)) {
1913 $comment_metadata = phutil_json_decode($comment_metadata);
1916 $actions = $request->getStr('editengine.actions');
1917 if ($actions) {
1918 $actions = phutil_json_decode($actions);
1921 if ($is_preview) {
1922 $version_key = PhabricatorVersionedDraft::KEY_VERSION;
1923 $request_version = $request->getInt($version_key);
1924 $current_version = $this->loadDraftVersion($object);
1925 if ($request_version >= $current_version) {
1926 $draft = PhabricatorVersionedDraft::loadOrCreateDraft(
1927 $object->getPHID(),
1928 $viewer->getPHID(),
1929 $current_version);
1931 $draft
1932 ->setProperty('comment', $comment_text)
1933 ->setProperty('metadata', $comment_metadata)
1934 ->setProperty('actions', $actions)
1935 ->save();
1937 $draft_engine = $this->newDraftEngine($object);
1938 if ($draft_engine) {
1939 $draft_engine
1940 ->setVersionedDraft($draft)
1941 ->synchronize();
1946 $xactions = array();
1948 $can_edit = PhabricatorPolicyFilter::hasCapability(
1949 $viewer,
1950 $object,
1951 PhabricatorPolicyCapability::CAN_EDIT);
1953 if ($actions) {
1954 $action_map = array();
1955 foreach ($actions as $action) {
1956 $type = idx($action, 'type');
1957 if (!$type) {
1958 continue;
1961 if (empty($fields[$type])) {
1962 continue;
1965 $action_map[$type] = $action;
1968 foreach ($action_map as $type => $action) {
1969 $field = $fields[$type];
1971 if (!$field->shouldGenerateTransactionsFromComment()) {
1972 continue;
1975 // If you don't have edit permission on the object, you're limited in
1976 // which actions you can take via the comment form. Most actions
1977 // need edit permission, but some actions (like "Accept Revision")
1978 // can be applied by anyone with view permission.
1979 if (!$can_edit) {
1980 if (!$field->getCanApplyWithoutEditCapability()) {
1981 // We know the user doesn't have the capability, so this will
1982 // raise a policy exception.
1983 PhabricatorPolicyFilter::requireCapability(
1984 $viewer,
1985 $object,
1986 PhabricatorPolicyCapability::CAN_EDIT);
1990 if (array_key_exists('initialValue', $action)) {
1991 $field->setInitialValue($action['initialValue']);
1994 $field->readValueFromComment(idx($action, 'value'));
1996 $type_xactions = $field->generateTransactions(
1997 clone $template,
1998 array(
1999 'value' => $field->getValueForTransaction(),
2001 foreach ($type_xactions as $type_xaction) {
2002 $xactions[] = $type_xaction;
2007 $auto_xactions = $this->newAutomaticCommentTransactions($object);
2008 foreach ($auto_xactions as $xaction) {
2009 $xactions[] = $xaction;
2012 if (strlen($comment_text) || !$xactions) {
2013 $xactions[] = id(clone $template)
2014 ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
2015 ->setMetadataValue('remarkup.control', $comment_metadata)
2016 ->attachComment(
2017 id(clone $comment_template)
2018 ->setContent($comment_text));
2021 $editor = $object->getApplicationTransactionEditor()
2022 ->setActor($viewer)
2023 ->setContinueOnNoEffect($request->isContinueRequest())
2024 ->setContinueOnMissingFields(true)
2025 ->setContentSourceFromRequest($request)
2026 ->setCancelURI($view_uri)
2027 ->setRaiseWarnings(!$request->getBool('editEngine.warnings'))
2028 ->setIsPreview($is_preview);
2030 try {
2031 $xactions = $editor->applyTransactions($object, $xactions);
2032 } catch (PhabricatorApplicationTransactionValidationException $ex) {
2033 return id(new PhabricatorApplicationTransactionValidationResponse())
2034 ->setCancelURI($view_uri)
2035 ->setException($ex);
2036 } catch (PhabricatorApplicationTransactionNoEffectException $ex) {
2037 return id(new PhabricatorApplicationTransactionNoEffectResponse())
2038 ->setCancelURI($view_uri)
2039 ->setException($ex);
2040 } catch (PhabricatorApplicationTransactionWarningException $ex) {
2041 return id(new PhabricatorApplicationTransactionWarningResponse())
2042 ->setObject($object)
2043 ->setCancelURI($view_uri)
2044 ->setException($ex);
2047 if (!$is_preview) {
2048 PhabricatorVersionedDraft::purgeDrafts(
2049 $object->getPHID(),
2050 $viewer->getPHID());
2052 $draft_engine = $this->newDraftEngine($object);
2053 if ($draft_engine) {
2054 $draft_engine
2055 ->setVersionedDraft(null)
2056 ->synchronize();
2060 if ($request->isAjax() && $is_preview) {
2061 $preview_content = $this->newCommentPreviewContent($object, $xactions);
2063 $raw_view_data = $request->getStr('viewData');
2064 try {
2065 $view_data = phutil_json_decode($raw_view_data);
2066 } catch (Exception $ex) {
2067 $view_data = array();
2070 return id(new PhabricatorApplicationTransactionResponse())
2071 ->setObject($object)
2072 ->setViewer($viewer)
2073 ->setTransactions($xactions)
2074 ->setIsPreview($is_preview)
2075 ->setViewData($view_data)
2076 ->setPreviewContent($preview_content);
2077 } else {
2078 return id(new AphrontRedirectResponse())
2079 ->setURI($view_uri);
2083 public static function newTransactionsFromRemarkupMetadata(
2084 PhabricatorApplicationTransaction $template,
2085 array $metadata) {
2087 $xactions = array();
2089 $attached_phids = idx($metadata, 'attachedFilePHIDs');
2090 if (is_array($attached_phids) && $attached_phids) {
2091 $attachment_map = array_fill_keys(
2092 $attached_phids,
2093 PhabricatorFileAttachment::MODE_ATTACH);
2095 $xactions[] = id(clone $template)
2096 ->setTransactionType(PhabricatorTransactions::TYPE_FILE)
2097 ->setNewValue($attachment_map);
2100 return $xactions;
2103 protected function newDraftEngine($object) {
2104 $viewer = $this->getViewer();
2106 if ($object instanceof PhabricatorDraftInterface) {
2107 $engine = $object->newDraftEngine();
2108 } else {
2109 $engine = new PhabricatorBuiltinDraftEngine();
2112 return $engine
2113 ->setObject($object)
2114 ->setViewer($viewer);
2118 /* -( Conduit )------------------------------------------------------------ */
2122 * Respond to a Conduit edit request.
2124 * This method accepts a list of transactions to apply to an object, and
2125 * either edits an existing object or creates a new one.
2127 * @task conduit
2129 final public function buildConduitResponse(ConduitAPIRequest $request) {
2130 $viewer = $this->getViewer();
2132 $config = $this->loadDefaultConfiguration();
2133 if (!$config) {
2134 throw new Exception(
2135 pht(
2136 'Unable to load configuration for this EditEngine ("%s").',
2137 get_class($this)));
2140 $raw_xactions = $this->getRawConduitTransactions($request);
2142 $identifier = $request->getValue('objectIdentifier');
2143 if ($identifier) {
2144 $this->setIsCreate(false);
2146 // After T13186, each transaction can individually weaken or replace the
2147 // capabilities required to apply it, so we no longer need CAN_EDIT to
2148 // attempt to apply transactions to objects. In practice, almost all
2149 // transactions require CAN_EDIT so we won't get very far if we don't
2150 // have it.
2151 $capabilities = array(
2152 PhabricatorPolicyCapability::CAN_VIEW,
2155 $object = $this->newObjectFromIdentifier(
2156 $identifier,
2157 $capabilities);
2158 } else {
2159 $this->requireCreateCapability();
2161 $this->setIsCreate(true);
2162 $object = $this->newEditableObjectFromConduit($raw_xactions);
2165 $this->validateObject($object);
2167 $fields = $this->buildEditFields($object);
2169 $types = $this->getConduitEditTypesFromFields($fields);
2170 $template = $object->getApplicationTransactionTemplate();
2172 $xactions = $this->getConduitTransactions(
2173 $request,
2174 $raw_xactions,
2175 $types,
2176 $template);
2178 $editor = $object->getApplicationTransactionEditor()
2179 ->setActor($viewer)
2180 ->setContentSource($request->newContentSource())
2181 ->setContinueOnNoEffect(true);
2183 if (!$this->getIsCreate()) {
2184 $editor->setContinueOnMissingFields(true);
2187 $xactions = $editor->applyTransactions($object, $xactions);
2189 $xactions_struct = array();
2190 foreach ($xactions as $xaction) {
2191 $xactions_struct[] = array(
2192 'phid' => $xaction->getPHID(),
2196 return array(
2197 'object' => array(
2198 'id' => (int)$object->getID(),
2199 'phid' => $object->getPHID(),
2201 'transactions' => $xactions_struct,
2205 private function getRawConduitTransactions(ConduitAPIRequest $request) {
2206 $transactions_key = 'transactions';
2208 $xactions = $request->getValue($transactions_key);
2209 if (!is_array($xactions)) {
2210 throw new Exception(
2211 pht(
2212 'Parameter "%s" is not a list of transactions.',
2213 $transactions_key));
2216 foreach ($xactions as $key => $xaction) {
2217 if (!is_array($xaction)) {
2218 throw new Exception(
2219 pht(
2220 'Parameter "%s" must contain a list of transaction descriptions, '.
2221 'but item with key "%s" is not a dictionary.',
2222 $transactions_key,
2223 $key));
2226 if (!array_key_exists('type', $xaction)) {
2227 throw new Exception(
2228 pht(
2229 'Parameter "%s" must contain a list of transaction descriptions, '.
2230 'but item with key "%s" is missing a "type" field. Each '.
2231 'transaction must have a type field.',
2232 $transactions_key,
2233 $key));
2236 if (!array_key_exists('value', $xaction)) {
2237 throw new Exception(
2238 pht(
2239 'Parameter "%s" must contain a list of transaction descriptions, '.
2240 'but item with key "%s" is missing a "value" field. Each '.
2241 'transaction must have a value field.',
2242 $transactions_key,
2243 $key));
2247 return $xactions;
2252 * Generate transactions which can be applied from edit actions in a Conduit
2253 * request.
2255 * @param ConduitAPIRequest The request.
2256 * @param list<wild> Raw conduit transactions.
2257 * @param list<PhabricatorEditType> Supported edit types.
2258 * @param PhabricatorApplicationTransaction Template transaction.
2259 * @return list<PhabricatorApplicationTransaction> Generated transactions.
2260 * @task conduit
2262 private function getConduitTransactions(
2263 ConduitAPIRequest $request,
2264 array $xactions,
2265 array $types,
2266 PhabricatorApplicationTransaction $template) {
2268 $viewer = $request->getUser();
2269 $results = array();
2271 foreach ($xactions as $key => $xaction) {
2272 $type = $xaction['type'];
2273 if (empty($types[$type])) {
2274 throw new Exception(
2275 pht(
2276 'Transaction with key "%s" has invalid type "%s". This type is '.
2277 'not recognized. Valid types are: %s.',
2278 $key,
2279 $type,
2280 implode(', ', array_keys($types))));
2284 if ($this->getIsCreate()) {
2285 $results[] = id(clone $template)
2286 ->setTransactionType(PhabricatorTransactions::TYPE_CREATE);
2289 $is_strict = $request->getIsStrictlyTyped();
2291 foreach ($xactions as $xaction) {
2292 $type = $types[$xaction['type']];
2294 // Let the parameter type interpret the value. This allows you to
2295 // use usernames in list<user> fields, for example.
2296 $parameter_type = $type->getConduitParameterType();
2298 $parameter_type->setViewer($viewer);
2300 try {
2301 $value = $xaction['value'];
2302 $value = $parameter_type->getValue($xaction, 'value', $is_strict);
2303 $value = $type->getTransactionValueFromConduit($value);
2304 $xaction['value'] = $value;
2305 } catch (Exception $ex) {
2306 throw new PhutilProxyException(
2307 pht(
2308 'Exception when processing transaction of type "%s": %s',
2309 $xaction['type'],
2310 $ex->getMessage()),
2311 $ex);
2314 $type_xactions = $type->generateTransactions(
2315 clone $template,
2316 $xaction);
2318 foreach ($type_xactions as $type_xaction) {
2319 $results[] = $type_xaction;
2323 return $results;
2328 * @return map<string, PhabricatorEditType>
2329 * @task conduit
2331 private function getConduitEditTypesFromFields(array $fields) {
2332 $types = array();
2333 foreach ($fields as $field) {
2334 $field_types = $field->getConduitEditTypes();
2336 if ($field_types === null) {
2337 continue;
2340 foreach ($field_types as $field_type) {
2341 $types[$field_type->getEditType()] = $field_type;
2344 return $types;
2347 public function getConduitEditTypes() {
2348 $config = $this->loadDefaultConfiguration();
2349 if (!$config) {
2350 return array();
2353 $object = $this->newEditableObjectForDocumentation();
2354 $fields = $this->buildEditFields($object);
2355 return $this->getConduitEditTypesFromFields($fields);
2358 final public static function getAllEditEngines() {
2359 return id(new PhutilClassMapQuery())
2360 ->setAncestorClass(__CLASS__)
2361 ->setUniqueMethod('getEngineKey')
2362 ->execute();
2365 final public static function getByKey(PhabricatorUser $viewer, $key) {
2366 return id(new PhabricatorEditEngineQuery())
2367 ->setViewer($viewer)
2368 ->withEngineKeys(array($key))
2369 ->executeOne();
2372 public function getIcon() {
2373 $application = $this->getApplication();
2374 return $application->getIcon();
2377 private function loadUsableConfigurationsForCreate() {
2378 $viewer = $this->getViewer();
2380 $configs = id(new PhabricatorEditEngineConfigurationQuery())
2381 ->setViewer($viewer)
2382 ->withEngineKeys(array($this->getEngineKey()))
2383 ->withIsDefault(true)
2384 ->withIsDisabled(false)
2385 ->execute();
2387 $configs = msort($configs, 'getCreateSortKey');
2389 // Attach this specific engine to configurations we load so they can access
2390 // any runtime configuration. For example, this allows us to generate the
2391 // correct "Create Form" buttons when editing forms, see T12301.
2392 foreach ($configs as $config) {
2393 $config->attachEngine($this);
2396 return $configs;
2399 protected function getValidationExceptionShortMessage(
2400 PhabricatorApplicationTransactionValidationException $ex,
2401 PhabricatorEditField $field) {
2403 $xaction_type = $field->getTransactionType();
2404 if ($xaction_type === null) {
2405 return null;
2408 return $ex->getShortMessage($xaction_type);
2411 protected function getCreateNewObjectPolicy() {
2412 return PhabricatorPolicies::POLICY_USER;
2415 private function requireCreateCapability() {
2416 PhabricatorPolicyFilter::requireCapability(
2417 $this->getViewer(),
2418 $this,
2419 PhabricatorPolicyCapability::CAN_EDIT);
2422 private function hasCreateCapability() {
2423 return PhabricatorPolicyFilter::hasCapability(
2424 $this->getViewer(),
2425 $this,
2426 PhabricatorPolicyCapability::CAN_EDIT);
2429 public function isCommentAction() {
2430 return ($this->getEditAction() == 'comment');
2433 public function getEditAction() {
2434 $controller = $this->getController();
2435 $request = $controller->getRequest();
2436 return $request->getURIData('editAction');
2439 protected function newCommentActionGroups() {
2440 return array();
2443 protected function newAutomaticCommentTransactions($object) {
2444 return array();
2447 protected function newCommentPreviewContent($object, array $xactions) {
2448 return null;
2452 /* -( Form Pages )--------------------------------------------------------- */
2455 public function getSelectedPage() {
2456 return $this->page;
2460 private function selectPage($object, $page_key) {
2461 $pages = $this->getPages($object);
2463 if (empty($pages[$page_key])) {
2464 return null;
2467 $this->page = $pages[$page_key];
2468 return $this->page;
2472 protected function newPages($object) {
2473 return array();
2477 protected function getPages($object) {
2478 if ($this->pages === null) {
2479 $pages = $this->newPages($object);
2481 assert_instances_of($pages, 'PhabricatorEditPage');
2482 $pages = mpull($pages, null, 'getKey');
2484 $this->pages = $pages;
2487 return $this->pages;
2490 private function applyPageToFields($object, array $fields) {
2491 $pages = $this->getPages($object);
2492 if (!$pages) {
2493 return $fields;
2496 if (!$this->getSelectedPage()) {
2497 return $fields;
2500 $page_picks = array();
2501 $default_key = head($pages)->getKey();
2502 foreach ($pages as $page_key => $page) {
2503 foreach ($page->getFieldKeys() as $field_key) {
2504 $page_picks[$field_key] = $page_key;
2506 if ($page->getIsDefault()) {
2507 $default_key = $page_key;
2511 $page_map = array_fill_keys(array_keys($pages), array());
2512 foreach ($fields as $field_key => $field) {
2513 if (isset($page_picks[$field_key])) {
2514 $page_map[$page_picks[$field_key]][$field_key] = $field;
2515 continue;
2518 // TODO: Maybe let the field pick a page to associate itself with so
2519 // extensions can force themselves onto a particular page?
2521 $page_map[$default_key][$field_key] = $field;
2524 $page = $this->getSelectedPage();
2525 if (!$page) {
2526 $page = head($pages);
2529 $selected_key = $page->getKey();
2530 return $page_map[$selected_key];
2533 protected function willApplyTransactions($object, array $xactions) {
2534 return $xactions;
2537 protected function didApplyTransactions($object, array $xactions) {
2538 return;
2542 /* -( Bulk Edits )--------------------------------------------------------- */
2544 final public function newBulkEditGroupMap() {
2545 $groups = $this->newBulkEditGroups();
2547 $map = array();
2548 foreach ($groups as $group) {
2549 $key = $group->getKey();
2551 if (isset($map[$key])) {
2552 throw new Exception(
2553 pht(
2554 'Two bulk edit groups have the same key ("%s"). Each bulk edit '.
2555 'group must have a unique key.',
2556 $key));
2559 $map[$key] = $group;
2562 if ($this->isEngineExtensible()) {
2563 $extensions = PhabricatorEditEngineExtension::getAllEnabledExtensions();
2564 } else {
2565 $extensions = array();
2568 foreach ($extensions as $extension) {
2569 $extension_groups = $extension->newBulkEditGroups($this);
2570 foreach ($extension_groups as $group) {
2571 $key = $group->getKey();
2573 if (isset($map[$key])) {
2574 throw new Exception(
2575 pht(
2576 'Extension "%s" defines a bulk edit group with the same key '.
2577 '("%s") as the main editor or another extension. Each bulk '.
2578 'edit group must have a unique key.',
2579 get_class($extension),
2580 $key));
2583 $map[$key] = $group;
2587 return $map;
2590 protected function newBulkEditGroups() {
2591 return array(
2592 id(new PhabricatorBulkEditGroup())
2593 ->setKey('default')
2594 ->setLabel(pht('Primary Fields')),
2595 id(new PhabricatorBulkEditGroup())
2596 ->setKey('extension')
2597 ->setLabel(pht('Support Applications')),
2601 final public function newBulkEditMap() {
2602 $viewer = $this->getViewer();
2604 $config = $this->loadDefaultConfiguration();
2605 if (!$config) {
2606 throw new Exception(
2607 pht('No default edit engine configuration for bulk edit.'));
2610 $object = $this->newEditableObject();
2611 $fields = $this->buildEditFields($object);
2612 $groups = $this->newBulkEditGroupMap();
2614 $edit_types = $this->getBulkEditTypesFromFields($fields);
2616 $map = array();
2617 foreach ($edit_types as $key => $type) {
2618 $bulk_type = $type->getBulkParameterType();
2619 if ($bulk_type === null) {
2620 continue;
2623 $bulk_type->setViewer($viewer);
2625 $bulk_label = $type->getBulkEditLabel();
2626 if ($bulk_label === null) {
2627 continue;
2630 $group_key = $type->getBulkEditGroupKey();
2631 if (!$group_key) {
2632 $group_key = 'default';
2635 if (!isset($groups[$group_key])) {
2636 throw new Exception(
2637 pht(
2638 'Field "%s" has a bulk edit group key ("%s") with no '.
2639 'corresponding bulk edit group.',
2640 $key,
2641 $group_key));
2644 $map[] = array(
2645 'label' => $bulk_label,
2646 'xaction' => $key,
2647 'group' => $group_key,
2648 'control' => array(
2649 'type' => $bulk_type->getPHUIXControlType(),
2650 'spec' => (object)$bulk_type->getPHUIXControlSpecification(),
2655 return $map;
2659 final public function newRawBulkTransactions(array $xactions) {
2660 $config = $this->loadDefaultConfiguration();
2661 if (!$config) {
2662 throw new Exception(
2663 pht('No default edit engine configuration for bulk edit.'));
2666 $object = $this->newEditableObject();
2667 $fields = $this->buildEditFields($object);
2669 $edit_types = $this->getBulkEditTypesFromFields($fields);
2670 $template = $object->getApplicationTransactionTemplate();
2672 $raw_xactions = array();
2673 foreach ($xactions as $key => $xaction) {
2674 PhutilTypeSpec::checkMap(
2675 $xaction,
2676 array(
2677 'type' => 'string',
2678 'value' => 'optional wild',
2681 $type = $xaction['type'];
2682 if (!isset($edit_types[$type])) {
2683 throw new Exception(
2684 pht(
2685 'Unsupported bulk edit type "%s".',
2686 $type));
2689 $edit_type = $edit_types[$type];
2691 // Replace the edit type with the underlying transaction type. Usually
2692 // these are 1:1 and the transaction type just has more internal noise,
2693 // but it's possible that this isn't the case.
2694 $xaction['type'] = $edit_type->getTransactionType();
2696 $value = $xaction['value'];
2697 $value = $edit_type->getTransactionValueFromBulkEdit($value);
2698 $xaction['value'] = $value;
2700 $xaction_objects = $edit_type->generateTransactions(
2701 clone $template,
2702 $xaction);
2704 foreach ($xaction_objects as $xaction_object) {
2705 $raw_xaction = array(
2706 'type' => $xaction_object->getTransactionType(),
2707 'metadata' => $xaction_object->getMetadata(),
2708 'new' => $xaction_object->getNewValue(),
2711 if ($xaction_object->hasOldValue()) {
2712 $raw_xaction['old'] = $xaction_object->getOldValue();
2715 if ($xaction_object->hasComment()) {
2716 $comment = $xaction_object->getComment();
2717 $raw_xaction['comment'] = $comment->getContent();
2720 $raw_xactions[] = $raw_xaction;
2724 return $raw_xactions;
2727 private function getBulkEditTypesFromFields(array $fields) {
2728 $types = array();
2730 foreach ($fields as $field) {
2731 $field_types = $field->getBulkEditTypes();
2733 if ($field_types === null) {
2734 continue;
2737 foreach ($field_types as $field_type) {
2738 $types[$field_type->getEditType()] = $field_type;
2742 return $types;
2746 /* -( PhabricatorPolicyInterface )----------------------------------------- */
2749 public function getPHID() {
2750 return get_class($this);
2753 public function getCapabilities() {
2754 return array(
2755 PhabricatorPolicyCapability::CAN_VIEW,
2756 PhabricatorPolicyCapability::CAN_EDIT,
2760 public function getPolicy($capability) {
2761 switch ($capability) {
2762 case PhabricatorPolicyCapability::CAN_VIEW:
2763 return PhabricatorPolicies::getMostOpenPolicy();
2764 case PhabricatorPolicyCapability::CAN_EDIT:
2765 return $this->getCreateNewObjectPolicy();
2769 public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
2770 return false;