@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3final class DifferentialJIRAIssuesField
4 extends DifferentialStoredCustomField {
5
6 private $error;
7
8 public function getFieldKey() {
9 return 'phabricator:jira-issues';
10 }
11
12 public function getFieldKeyForConduit() {
13 return 'jira.issues';
14 }
15
16 public function isFieldEnabled() {
17 return (bool)PhabricatorJIRAAuthProvider::getJIRAProvider();
18 }
19
20 public function canDisableField() {
21 return false;
22 }
23
24 public function getValueForStorage() {
25 return json_encode($this->getValue());
26 }
27
28 public function setValueFromStorage($value) {
29 try {
30 $this->setValue(phutil_json_decode($value));
31 } catch (PhutilJSONParserException $ex) {
32 $this->setValue(array());
33 }
34 return $this;
35 }
36
37 public function getFieldName() {
38 return pht('JIRA Issues');
39 }
40
41 public function getFieldDescription() {
42 return pht('Lists associated JIRA issues.');
43 }
44
45 public function shouldAppearInPropertyView() {
46 return true;
47 }
48
49 public function renderPropertyViewLabel() {
50 return $this->getFieldName();
51 }
52
53 public function renderPropertyViewValue(array $handles) {
54 $xobjs = $this->loadDoorkeeperExternalObjects($this->getValue());
55 if (!$xobjs) {
56 return null;
57 }
58
59 $links = array();
60 foreach ($xobjs as $xobj) {
61 $links[] = id(new DoorkeeperTagView())
62 ->setExternalObject($xobj);
63 }
64
65 return phutil_implode_html(phutil_tag('br'), $links);
66 }
67
68 private function buildDoorkeeperRefs($value) {
69 $provider = PhabricatorJIRAAuthProvider::getJIRAProvider();
70
71 $refs = array();
72 if ($value) {
73 foreach ($value as $jira_key) {
74 $refs[] = id(new DoorkeeperObjectRef())
75 ->setApplicationType(DoorkeeperBridgeJIRA::APPTYPE_JIRA)
76 ->setApplicationDomain($provider->getProviderDomain())
77 ->setObjectType(DoorkeeperBridgeJIRA::OBJTYPE_ISSUE)
78 ->setObjectID($jira_key);
79 }
80 }
81
82 return $refs;
83 }
84
85 private function loadDoorkeeperExternalObjects($value) {
86 $refs = $this->buildDoorkeeperRefs($value);
87 if (!$refs) {
88 return array();
89 }
90
91 $xobjs = id(new DoorkeeperExternalObjectQuery())
92 ->setViewer($this->getViewer())
93 ->withObjectKeys(mpull($refs, 'getObjectKey'))
94 ->execute();
95
96 return $xobjs;
97 }
98
99 public function shouldAppearInEditView() {
100 return PhabricatorJIRAAuthProvider::getJIRAProvider();
101 }
102
103 public function shouldAppearInApplicationTransactions() {
104 return PhabricatorJIRAAuthProvider::getJIRAProvider();
105 }
106
107 public function readValueFromRequest(AphrontRequest $request) {
108 $this->setValue($request->getStrList($this->getFieldKey()));
109 return $this;
110 }
111
112 public function renderEditControl(array $handles) {
113 return id(new AphrontFormTextControl())
114 ->setLabel(pht('JIRA Issues'))
115 ->setCaption(
116 pht('Example: %s', phutil_tag('tt', array(), 'JIS-3, JIS-9')))
117 ->setName($this->getFieldKey())
118 ->setValue(implode(', ', nonempty($this->getValue(), array())))
119 ->setError($this->error);
120 }
121
122 public function getOldValueForApplicationTransactions() {
123 return array_unique(nonempty($this->getValue(), array()));
124 }
125
126 public function getNewValueForApplicationTransactions() {
127 return array_unique(nonempty($this->getValue(), array()));
128 }
129
130 public function validateApplicationTransactions(
131 PhabricatorApplicationTransactionEditor $editor,
132 $type,
133 array $xactions) {
134
135 $this->error = null;
136
137 $errors = parent::validateApplicationTransactions(
138 $editor,
139 $type,
140 $xactions);
141
142 $transaction = null;
143 foreach ($xactions as $xaction) {
144 $old = $xaction->getOldValue();
145 $new = $xaction->getNewValue();
146
147 $add = array_diff($new, $old);
148 if (!$add) {
149 continue;
150 }
151
152 // Only check that the actor can see newly added JIRA refs. You're
153 // allowed to remove refs or make no-op changes even if you aren't
154 // linked to JIRA.
155
156 try {
157 $refs = id(new DoorkeeperImportEngine())
158 ->setViewer($this->getViewer())
159 ->setRefs($this->buildDoorkeeperRefs($add))
160 ->setThrowOnMissingLink(true)
161 ->execute();
162 } catch (DoorkeeperMissingLinkException $ex) {
163 $this->error = pht('Not Linked');
164 $errors[] = new PhabricatorApplicationTransactionValidationError(
165 $type,
166 pht('Not Linked'),
167 pht(
168 'You can not add JIRA issues (%s) to this revision because your '.
169 '%s account is not linked to a JIRA account.',
170 implode(', ', $add),
171 PlatformSymbols::getPlatformServerName()),
172 $xaction);
173 continue;
174 }
175
176 $bad = array();
177 foreach ($refs as $ref) {
178 if (!$ref->getIsVisible()) {
179 $bad[] = $ref->getObjectID();
180 }
181 }
182
183 if ($bad) {
184 $bad = implode(', ', $bad);
185 $this->error = pht('Invalid');
186
187 $errors[] = new PhabricatorApplicationTransactionValidationError(
188 $type,
189 pht('Invalid'),
190 pht(
191 'Some JIRA issues could not be loaded. They may not exist, or '.
192 'you may not have permission to view them: %s',
193 $bad),
194 $xaction);
195 }
196 }
197
198 return $errors;
199 }
200
201 public function getApplicationTransactionTitle(
202 PhabricatorApplicationTransaction $xaction) {
203
204 $old = $xaction->getOldValue();
205 if (!is_array($old)) {
206 $old = array();
207 }
208
209 $new = $xaction->getNewValue();
210 if (!is_array($new)) {
211 $new = array();
212 }
213
214 $add = array_diff($new, $old);
215 $rem = array_diff($old, $new);
216
217 $author_phid = $xaction->getAuthorPHID();
218 if ($add && $rem) {
219 return pht(
220 '%s updated JIRA issue(s): added %s: %s; removed %s: %s.',
221 $xaction->renderHandleLink($author_phid),
222 phutil_count($add),
223 implode(', ', $add),
224 phutil_count($rem),
225 implode(', ', $rem));
226 } else if ($add) {
227 return pht(
228 '%s added %s JIRA issue(s): %s.',
229 $xaction->renderHandleLink($author_phid),
230 phutil_count($add),
231 implode(', ', $add));
232 } else if ($rem) {
233 return pht(
234 '%s removed %s JIRA issue(s): %s.',
235 $xaction->renderHandleLink($author_phid),
236 phutil_count($rem),
237 implode(', ', $rem));
238 }
239
240 return parent::getApplicationTransactionTitle($xaction);
241 }
242
243 public function applyApplicationTransactionExternalEffects(
244 PhabricatorApplicationTransaction $xaction) {
245
246 // Update the CustomField storage.
247 parent::applyApplicationTransactionExternalEffects($xaction);
248
249 // Now, synchronize the Doorkeeper edges.
250 $revision = $this->getObject();
251 $revision_phid = $revision->getPHID();
252
253 $edge_type = PhabricatorJiraIssueHasObjectEdgeType::EDGECONST;
254 $xobjs = $this->loadDoorkeeperExternalObjects($xaction->getNewValue());
255 $edge_dsts = mpull($xobjs, 'getPHID');
256
257 $edges = PhabricatorEdgeQuery::loadDestinationPHIDs(
258 $revision_phid,
259 $edge_type);
260
261 $editor = new PhabricatorEdgeEditor();
262
263 foreach (array_diff($edges, $edge_dsts) as $rem_edge) {
264 $editor->removeEdge($revision_phid, $edge_type, $rem_edge);
265 }
266
267 foreach (array_diff($edge_dsts, $edges) as $add_edge) {
268 $editor->addEdge($revision_phid, $edge_type, $add_edge);
269 }
270
271 $editor->save();
272 }
273
274 public function shouldAppearInConduitDictionary() {
275 return true;
276 }
277
278 public function shouldAppearInConduitTransactions() {
279 return true;
280 }
281
282 protected function newConduitEditParameterType() {
283 return new ConduitStringListParameterType();
284 }
285
286}