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

Validate menu item fields (links, projects, dashboards, applications, forms, etc)

Summary:
Ref T12128. This adds validation to menu items.

This feels a touch flimsy-ish (kind of copy/paste heavy?) but maybe it can be cleaned up a bit once some similar lightweight modular item types (build steps in Harbormaster, blueprints in Drydock) convert.

Test Plan:
- Tried to create each item with errors (no dashboard, no project, etc). Got appropriate form errors.
- Created valid items of each type.

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T12128

Differential Revision: https://secure.phabricator.com/D17235

+396 -7
+43
src/applications/search/editor/PhabricatorProfileMenuEditEngine.php
··· 149 149 return $fields; 150 150 } 151 151 152 + protected function getValidationExceptionShortMessage( 153 + PhabricatorApplicationTransactionValidationException $ex, 154 + PhabricatorEditField $field) { 155 + 156 + // Menu item properties all have the same transaction type, so we need 157 + // to make sure errors about a specific property (like the URI for a 158 + // link) are only applied to the field for that particular property. If 159 + // we don't do this, the red error text like "Required" will display 160 + // next to every field. 161 + 162 + $property_type = 163 + PhabricatorProfileMenuItemConfigurationTransaction::TYPE_PROPERTY; 164 + 165 + $xaction_type = $field->getTransactionType(); 166 + if ($xaction_type == $property_type) { 167 + $field_key = $field->getKey(); 168 + foreach ($ex->getErrors() as $error) { 169 + if ($error->getType() !== $xaction_type) { 170 + continue; 171 + } 172 + 173 + $xaction = $error->getTransaction(); 174 + if (!$xaction) { 175 + continue; 176 + } 177 + 178 + $xaction_setting = $xaction->getMetadataValue('property.key'); 179 + if ($xaction_setting != $field_key) { 180 + continue; 181 + } 182 + 183 + $short_message = $error->getShortMessage(); 184 + if ($short_message !== null) { 185 + return $short_message; 186 + } 187 + } 188 + 189 + return null; 190 + } 191 + 192 + return parent::getValidationExceptionShortMessage($ex, $field); 193 + } 194 + 152 195 }
+35
src/applications/search/editor/PhabricatorProfileMenuEditor.php
··· 87 87 return parent::applyCustomExternalTransaction($object, $xaction); 88 88 } 89 89 90 + protected function validateTransaction( 91 + PhabricatorLiskDAO $object, 92 + $type, 93 + array $xactions) { 94 + 95 + $errors = parent::validateTransaction($object, $type, $xactions); 96 + 97 + $actor = $this->getActor(); 98 + $menu_item = $object->getMenuItem(); 99 + $menu_item->setViewer($actor); 100 + 101 + switch ($type) { 102 + case PhabricatorProfileMenuItemConfigurationTransaction::TYPE_PROPERTY: 103 + $key_map = array(); 104 + foreach ($xactions as $xaction) { 105 + $xaction_key = $xaction->getMetadataValue('property.key'); 106 + $old = $this->getCustomTransactionOldValue($object, $xaction); 107 + $new = $xaction->getNewValue(); 108 + $key_map[$xaction_key][] = array( 109 + 'xaction' => $xaction, 110 + 'old' => $old, 111 + 'new' => $new, 112 + ); 113 + } 114 + 115 + foreach ($object->validateTransactions($key_map) as $error) { 116 + $errors[] = $error; 117 + } 118 + break; 119 + } 120 + 121 + return $errors; 122 + } 123 + 124 + 90 125 }
+50 -1
src/applications/search/menuitem/PhabricatorApplicationProfileMenuItem.php
··· 5 5 6 6 const MENUITEMKEY = 'application'; 7 7 8 + const FIELD_APPLICATION = 'application'; 9 + 8 10 public function getMenuItemTypeIcon() { 9 11 return 'fa-globe'; 10 12 } ··· 32 34 PhabricatorProfileMenuItemConfiguration $config) { 33 35 return array( 34 36 id(new PhabricatorDatasourceEditField()) 35 - ->setKey('application') 37 + ->setKey(self::FIELD_APPLICATION) 36 38 ->setLabel(pht('Application')) 37 39 ->setIsRequired(true) 38 40 ->setDatasource(new PhabricatorApplicationDatasource()) 41 + ->setIsRequired(true) 39 42 ->setSingleValue($config->getMenuItemProperty('application')), 40 43 ); 41 44 } ··· 44 47 PhabricatorProfileMenuItemConfiguration $config) { 45 48 $viewer = $this->getViewer(); 46 49 $phid = $config->getMenuItemProperty('application'); 50 + 47 51 $app = id(new PhabricatorApplicationQuery()) 48 52 ->setViewer($viewer) 49 53 ->withPHIDs(array($phid)) ··· 75 79 return array( 76 80 $item, 77 81 ); 82 + } 83 + 84 + public function validateTransactions( 85 + PhabricatorProfileMenuItemConfiguration $config, 86 + $field_key, 87 + $value, 88 + array $xactions) { 89 + 90 + $viewer = $this->getViewer(); 91 + $errors = array(); 92 + 93 + if ($field_key == self::FIELD_APPLICATION) { 94 + if ($this->isEmptyTransaction($value, $xactions)) { 95 + $errors[] = $this->newRequiredError( 96 + pht('You must choose an application.'), 97 + $field_key); 98 + } 99 + 100 + foreach ($xactions as $xaction) { 101 + $new = $xaction['new']; 102 + 103 + if (!$new) { 104 + continue; 105 + } 106 + 107 + if ($new === $value) { 108 + continue; 109 + } 110 + 111 + $applications = id(new PhabricatorApplicationQuery()) 112 + ->setViewer($viewer) 113 + ->withPHIDs(array($new)) 114 + ->execute(); 115 + if (!$applications) { 116 + $errors[] = $this->newInvalidError( 117 + pht( 118 + 'Application "%s" is not a valid application which you have '. 119 + 'permission to see.', 120 + $new), 121 + $xaction['xaction']); 122 + } 123 + } 124 + } 125 + 126 + return $errors; 78 127 } 79 128 80 129 }
+48 -1
src/applications/search/menuitem/PhabricatorDashboardProfileMenuItem.php
··· 5 5 6 6 const MENUITEMKEY = 'dashboard'; 7 7 8 + const FIELD_DASHBOARD = 'dashboardPHID'; 9 + 8 10 private $dashboard; 9 11 10 12 public function getMenuItemTypeIcon() { ··· 75 77 ->setLabel(pht('Name')) 76 78 ->setValue($this->getName($config)), 77 79 id(new PhabricatorDatasourceEditField()) 78 - ->setKey('dashboardPHID') 80 + ->setKey(self::FIELD_DASHBOARD) 79 81 ->setLabel(pht('Dashboard')) 80 82 ->setIsRequired(true) 81 83 ->setDatasource(new PhabricatorDashboardDatasource()) ··· 108 110 return array( 109 111 $item, 110 112 ); 113 + } 114 + 115 + public function validateTransactions( 116 + PhabricatorProfileMenuItemConfiguration $config, 117 + $field_key, 118 + $value, 119 + array $xactions) { 120 + 121 + $viewer = $this->getViewer(); 122 + $errors = array(); 123 + 124 + if ($field_key == self::FIELD_DASHBOARD) { 125 + if ($this->isEmptyTransaction($value, $xactions)) { 126 + $errors[] = $this->newRequiredError( 127 + pht('You must choose a dashboard.'), 128 + $field_key); 129 + } 130 + 131 + foreach ($xactions as $xaction) { 132 + $new = $xaction['new']; 133 + 134 + if (!$new) { 135 + continue; 136 + } 137 + 138 + if ($new === $value) { 139 + continue; 140 + } 141 + 142 + $dashboards = id(new PhabricatorDashboardQuery()) 143 + ->setViewer($viewer) 144 + ->withPHIDs(array($new)) 145 + ->execute(); 146 + if (!$dashboards) { 147 + $errors[] = $this->newInvalidError( 148 + pht( 149 + 'Dashboard "%s" is not a valid dashboard which you have '. 150 + 'permission to see.', 151 + $new), 152 + $xaction['xaction']); 153 + } 154 + } 155 + } 156 + 157 + return $errors; 111 158 } 112 159 113 160 }
+54 -2
src/applications/search/menuitem/PhabricatorEditEngineProfileMenuItem.php
··· 5 5 6 6 const MENUITEMKEY = 'editengine'; 7 7 8 + const FIELD_FORM = 'formKey'; 9 + 8 10 private $form; 9 11 10 12 public function getMenuItemTypeIcon() { ··· 51 53 52 54 foreach ($items as $item) { 53 55 $key = $item->getMenuItemProperty('formKey'); 54 - list($engine_key, $form_key) = explode('/', $key); 56 + list($engine_key, $form_key) = PhabricatorEditEngine::splitFullKey($key); 57 + 55 58 if (is_numeric($form_key)) { 56 59 $form = idx($form_ids, $form_key, null); 57 60 $item->getMenuItem()->attachForm($form); ··· 83 86 ->setLabel(pht('Name')) 84 87 ->setValue($this->getName($config)), 85 88 id(new PhabricatorDatasourceEditField()) 86 - ->setKey('formKey') 89 + ->setKey(self::FIELD_FORM) 87 90 ->setLabel(pht('Form')) 88 91 ->setIsRequired(true) 89 92 ->setDatasource(new PhabricatorEditEngineDatasource()) ··· 118 121 return array( 119 122 $item, 120 123 ); 124 + } 125 + 126 + public function validateTransactions( 127 + PhabricatorProfileMenuItemConfiguration $config, 128 + $field_key, 129 + $value, 130 + array $xactions) { 131 + 132 + $viewer = $this->getViewer(); 133 + $errors = array(); 134 + 135 + if ($field_key == self::FIELD_FORM) { 136 + if ($this->isEmptyTransaction($value, $xactions)) { 137 + $errors[] = $this->newRequiredError( 138 + pht('You must choose a form.'), 139 + $field_key); 140 + } 141 + 142 + foreach ($xactions as $xaction) { 143 + $new = $xaction['new']; 144 + 145 + if (!$new) { 146 + continue; 147 + } 148 + 149 + if ($new === $value) { 150 + continue; 151 + } 152 + 153 + list($engine_key, $form_key) = PhabricatorEditEngine::splitFullKey( 154 + $new); 155 + 156 + $forms = id(new PhabricatorEditEngineConfigurationQuery()) 157 + ->setViewer($viewer) 158 + ->withEngineKeys(array($engine_key)) 159 + ->withIdentifiers(array($form_key)) 160 + ->execute(); 161 + if (!$forms) { 162 + $errors[] = $this->newInvalidError( 163 + pht( 164 + 'Form "%s" is not a valid form which you have permission to '. 165 + 'see.', 166 + $new), 167 + $xaction['xaction']); 168 + } 169 + } 170 + } 171 + 172 + return $errors; 121 173 } 122 174 123 175 }
+54 -2
src/applications/search/menuitem/PhabricatorLinkProfileMenuItem.php
··· 5 5 6 6 const MENUITEMKEY = 'link'; 7 7 8 + const FIELD_URI = 'uri'; 9 + const FIELD_NAME = 'name'; 10 + 8 11 public function getMenuItemTypeIcon() { 9 12 return 'fa-link'; 10 13 } ··· 26 29 PhabricatorProfileMenuItemConfiguration $config) { 27 30 return array( 28 31 id(new PhabricatorTextEditField()) 29 - ->setKey('name') 32 + ->setKey(self::FIELD_NAME) 30 33 ->setLabel(pht('Name')) 31 34 ->setIsRequired(true) 32 35 ->setValue($this->getLinkName($config)), 33 36 id(new PhabricatorTextEditField()) 34 - ->setKey('uri') 37 + ->setKey(self::FIELD_URI) 35 38 ->setLabel(pht('URI')) 36 39 ->setIsRequired(true) 37 40 ->setValue($this->getLinkURI($config)), ··· 91 94 ); 92 95 } 93 96 97 + public function validateTransactions( 98 + PhabricatorProfileMenuItemConfiguration $config, 99 + $field_key, 100 + $value, 101 + array $xactions) { 102 + 103 + $viewer = $this->getViewer(); 104 + $errors = array(); 105 + 106 + if ($field_key == self::FIELD_NAME) { 107 + if ($this->isEmptyTransaction($value, $xactions)) { 108 + $errors[] = $this->newRequiredError( 109 + pht('You must choose a link name.'), 110 + $field_key); 111 + } 112 + } 113 + 114 + if ($field_key == self::FIELD_URI) { 115 + if ($this->isEmptyTransaction($value, $xactions)) { 116 + $errors[] = $this->newRequiredError( 117 + pht('You must choose a URI to link to.'), 118 + $field_key); 119 + } 120 + 121 + foreach ($xactions as $xaction) { 122 + $new = $xaction['new']; 123 + 124 + if (!$new) { 125 + continue; 126 + } 127 + 128 + if ($new === $value) { 129 + continue; 130 + } 131 + 132 + if (!$this->isValidLinkURI($new)) { 133 + $errors[] = $this->newInvalidError( 134 + pht( 135 + 'URI "%s" is not a valid link URI. It should be a full, valid '. 136 + 'URI beginning with a protocol like "%s".', 137 + $new, 138 + 'https://'), 139 + $xaction['xaction']); 140 + } 141 + } 142 + } 143 + 144 + return $errors; 145 + } 94 146 }
+37
src/applications/search/menuitem/PhabricatorProfileMenuItem.php
··· 70 70 return new PHUIListItemView(); 71 71 } 72 72 73 + public function valdateTransactions( 74 + PhabricatorProfileMenuItemConfiguration $config, 75 + $field_key, 76 + $value, 77 + array $xactions) { 78 + return array(); 79 + } 80 + 81 + final protected function isEmptyTransaction($value, array $xactions) { 82 + $result = $value; 83 + foreach ($xactions as $xaction) { 84 + $result = $xaction['new']; 85 + } 86 + 87 + return !strlen($result); 88 + } 89 + 90 + final protected function newError($title, $message, $xaction = null) { 91 + return new PhabricatorApplicationTransactionValidationError( 92 + PhabricatorProfileMenuItemConfigurationTransaction::TYPE_PROPERTY, 93 + $title, 94 + $message, 95 + $xaction); 96 + } 97 + 98 + final protected function newRequiredError($message, $type) { 99 + $xaction = id(new PhabricatorProfileMenuItemConfigurationTransaction()) 100 + ->setMetadataValue('property.key', $type); 101 + 102 + return $this->newError(pht('Required'), $message, $xaction) 103 + ->setIsMissingFieldError(true); 104 + } 105 + 106 + final protected function newInvalidError($message, $xaction = null) { 107 + return $this->newError(pht('Invalid'), $message, $xaction); 108 + } 109 + 73 110 }
+47 -1
src/applications/search/menuitem/PhabricatorProjectProfileMenuItem.php
··· 4 4 extends PhabricatorProfileMenuItem { 5 5 6 6 const MENUITEMKEY = 'project'; 7 + const FIELD_PROJECT = 'project'; 7 8 8 9 private $project; 9 10 ··· 76 77 ->setLabel(pht('Name')) 77 78 ->setValue($this->getName($config)), 78 79 id(new PhabricatorDatasourceEditField()) 79 - ->setKey('project') 80 + ->setKey(self::FIELD_PROJECT) 80 81 ->setLabel(pht('Project')) 81 82 ->setIsRequired(true) 82 83 ->setDatasource(new PhabricatorProjectDatasource()) ··· 109 110 return array( 110 111 $item, 111 112 ); 113 + } 114 + 115 + public function validateTransactions( 116 + PhabricatorProfileMenuItemConfiguration $config, 117 + $field_key, 118 + $value, 119 + array $xactions) { 120 + 121 + $viewer = $this->getViewer(); 122 + $errors = array(); 123 + 124 + if ($field_key == self::FIELD_PROJECT) { 125 + if ($this->isEmptyTransaction($value, $xactions)) { 126 + $errors[] = $this->newRequiredError( 127 + pht('You must choose a project.'), 128 + $field_key); 129 + } 130 + 131 + foreach ($xactions as $xaction) { 132 + $new = $xaction['new']; 133 + 134 + if (!$new) { 135 + continue; 136 + } 137 + 138 + if ($new === $value) { 139 + continue; 140 + } 141 + 142 + $projects = id(new PhabricatorProjectQuery()) 143 + ->setViewer($viewer) 144 + ->withPHIDs(array($new)) 145 + ->execute(); 146 + if (!$projects) { 147 + $errors[] = $this->newInvalidError( 148 + pht( 149 + 'Project "%s" is not a valid project which you have '. 150 + 'permission to see.', 151 + $new), 152 + $xaction['xaction']); 153 + } 154 + } 155 + } 156 + 157 + return $errors; 112 158 } 113 159 114 160 }
+24
src/applications/search/storage/PhabricatorProfileMenuItemConfiguration.php
··· 126 126 return $this->getMenuItem()->willBuildNavigationItems($items); 127 127 } 128 128 129 + public function validateTransactions(array $map) { 130 + $item = $this->getMenuItem(); 131 + 132 + $fields = $item->buildEditEngineFields($this); 133 + $errors = array(); 134 + foreach ($fields as $field) { 135 + $field_key = $field->getKey(); 136 + 137 + $xactions = idx($map, $field_key, array()); 138 + $value = $this->getMenuItemProperty($field_key); 139 + 140 + $field_errors = $item->validateTransactions( 141 + $this, 142 + $field_key, 143 + $value, 144 + $xactions); 145 + foreach ($field_errors as $error) { 146 + $errors[] = $error; 147 + } 148 + } 149 + 150 + return $errors; 151 + } 152 + 129 153 public function getSortVector() { 130 154 // Sort custom items above global items. 131 155 if ($this->getCustomPHID()) {
+4
src/applications/transactions/editengine/PhabricatorEditEngine.php
··· 95 95 return $keys; 96 96 } 97 97 98 + public static function splitFullKey($full_key) { 99 + return explode('/', $full_key, 2); 100 + } 101 + 98 102 public function getQuickCreateOrderVector() { 99 103 return id(new PhutilSortVector()) 100 104 ->addString($this->getObjectCreateShortText());