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

Configure Feed

Select the types of activity you want to include in your feed.

at upstream/main 2788 lines 76 kB view raw
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}