@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1<?php
2
3/**
4 * @task validate Configuration Validation
5 */
6final class ManiphestTaskStatus extends ManiphestConstants {
7
8 const STATUS_OPEN = 'open';
9 const STATUS_CLOSED_RESOLVED = 'resolved';
10 const STATUS_CLOSED_WONTFIX = 'wontfix';
11 const STATUS_CLOSED_INVALID = 'invalid';
12 const STATUS_CLOSED_DUPLICATE = 'duplicate';
13 const STATUS_CLOSED_SPITE = 'spite';
14
15 const SPECIAL_DEFAULT = 'default';
16 const SPECIAL_CLOSED = 'closed';
17 const SPECIAL_DUPLICATE = 'duplicate';
18
19 const LOCKED_COMMENTS = 'comments';
20 const LOCKED_EDITS = 'edits';
21
22 private static function getStatusConfig() {
23 return PhabricatorEnv::getEnvConfig('maniphest.statuses');
24 }
25
26 private static function getEnabledStatusMap() {
27 $spec = self::getStatusConfig();
28
29 $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
30 foreach ($spec as $const => $status) {
31 if ($is_serious && !empty($status['silly'])) {
32 unset($spec[$const]);
33 continue;
34 }
35 }
36
37 return $spec;
38 }
39
40 public static function getTaskStatusMap() {
41 return ipull(self::getEnabledStatusMap(), 'name');
42 }
43
44
45 /**
46 * Get the statuses and their command keywords.
47 *
48 * @return map Statuses to lists of command keywords.
49 */
50 public static function getTaskStatusKeywordsMap() {
51 $map = self::getEnabledStatusMap();
52 foreach ($map as $key => $spec) {
53 $words = idx($spec, 'keywords', array());
54 if (!is_array($words)) {
55 $words = array($words);
56 }
57
58 // For statuses, we include the status name because it's usually
59 // at least somewhat meaningful.
60 $words[] = $key;
61
62 foreach ($words as $word_key => $word) {
63 $words[$word_key] = phutil_utf8_strtolower($word);
64 }
65
66 $words = array_unique($words);
67
68 $map[$key] = $words;
69 }
70
71 return $map;
72 }
73
74
75 public static function getTaskStatusName($status) {
76 return self::getStatusAttribute($status, 'name', pht('Unknown Status'));
77 }
78
79 public static function getTaskStatusFullName($status) {
80 $name = self::getStatusAttribute($status, 'name.full');
81 if ($name !== null) {
82 return $name;
83 }
84
85 return self::getStatusAttribute($status, 'name', pht('Unknown Status'));
86 }
87
88 public static function renderFullDescription($status, $priority) {
89 if (self::isOpenStatus($status)) {
90 $name = pht('%s, %s', self::getTaskStatusFullName($status), $priority);
91 $color = 'grey';
92 $icon = 'fa-square-o';
93 } else {
94 $name = self::getTaskStatusFullName($status);
95 $color = 'indigo';
96 $icon = 'fa-check-square-o';
97 }
98
99 $tag = id(new PHUITagView())
100 ->setName($name)
101 ->setIcon($icon)
102 ->setType(PHUITagView::TYPE_SHADE)
103 ->setColor($color);
104
105 return $tag;
106 }
107
108 private static function getSpecialStatus($special) {
109 foreach (self::getStatusConfig() as $const => $status) {
110 if (idx($status, 'special') == $special) {
111 return $const;
112 }
113 }
114 return null;
115 }
116
117 public static function getDefaultStatus() {
118 return self::getSpecialStatus(self::SPECIAL_DEFAULT);
119 }
120
121 public static function getDefaultClosedStatus() {
122 return self::getSpecialStatus(self::SPECIAL_CLOSED);
123 }
124
125 public static function getDuplicateStatus() {
126 return self::getSpecialStatus(self::SPECIAL_DUPLICATE);
127 }
128
129 public static function getOpenStatusConstants() {
130 $result = array();
131 foreach (self::getEnabledStatusMap() as $const => $status) {
132 if (empty($status['closed'])) {
133 $result[] = $const;
134 }
135 }
136 return $result;
137 }
138
139 public static function getClosedStatusConstants() {
140 $all = array_keys(self::getTaskStatusMap());
141 $open = self::getOpenStatusConstants();
142 return array_diff($all, $open);
143 }
144
145 public static function isOpenStatus($status) {
146 foreach (self::getOpenStatusConstants() as $constant) {
147 if ($status == $constant) {
148 return true;
149 }
150 }
151 return false;
152 }
153
154 public static function isClaimStatus($status) {
155 return self::getStatusAttribute($status, 'claim', true);
156 }
157
158 public static function isClosedStatus($status) {
159 return !self::isOpenStatus($status);
160 }
161
162 public static function areCommentsLockedInStatus($status) {
163 return (bool)self::getStatusAttribute($status, 'locked', false);
164 }
165
166 public static function areEditsLockedInStatus($status) {
167 $locked = self::getStatusAttribute($status, 'locked');
168 return ($locked === self::LOCKED_EDITS);
169 }
170
171 public static function isMFAStatus($status) {
172 return self::getStatusAttribute($status, 'mfa', false);
173 }
174
175 public static function getStatusActionName($status) {
176 return self::getStatusAttribute($status, 'name.action');
177 }
178
179 public static function getStatusColor($status) {
180 return self::getStatusAttribute($status, 'transaction.color');
181 }
182
183 public static function isDisabledStatus($status) {
184 return self::getStatusAttribute($status, 'disabled');
185 }
186
187 public static function getStatusIcon($status) {
188 $icon = self::getStatusAttribute($status, 'transaction.icon');
189 if ($icon) {
190 return $icon;
191 }
192
193 if (self::isOpenStatus($status)) {
194 return 'fa-exclamation-circle';
195 } else {
196 return 'fa-check-square-o';
197 }
198 }
199
200 public static function getStatusPrefixMap() {
201 $map = array();
202 foreach (self::getEnabledStatusMap() as $const => $status) {
203 foreach (idx($status, 'prefixes', array()) as $prefix) {
204 $map[$prefix] = $const;
205 }
206 }
207
208 $map += array(
209 'ref' => null,
210 'refs' => null,
211 'references' => null,
212 'cf.' => null,
213 );
214
215 return $map;
216 }
217
218 public static function getStatusSuffixMap() {
219 $map = array();
220 foreach (self::getEnabledStatusMap() as $const => $status) {
221 foreach (idx($status, 'suffixes', array()) as $prefix) {
222 $map[$prefix] = $const;
223 }
224 }
225 return $map;
226 }
227
228 private static function getStatusAttribute($status, $key, $default = null) {
229 $config = self::getStatusConfig();
230
231 $spec = idx($config, $status);
232 if ($spec) {
233 return idx($spec, $key, $default);
234 }
235
236 return $default;
237 }
238
239
240/* -( Configuration Validation )------------------------------------------- */
241
242
243 /**
244 * @task validate
245 */
246 public static function isValidStatusConstant($constant) {
247 if (!strlen($constant) || strlen($constant) > 64) {
248 return false;
249 }
250
251 // Alphanumeric, but not exclusively numeric
252 if (!preg_match('/^(?![0-9]*$)[a-zA-Z0-9]+$/', $constant)) {
253 return false;
254 }
255 return true;
256 }
257
258 /**
259 * @task validate
260 */
261 public static function validateConfiguration(array $config) {
262 foreach ($config as $key => $value) {
263 if (!self::isValidStatusConstant($key)) {
264 throw new Exception(
265 pht(
266 'Key "%s" is not a valid status constant. Status constants '.
267 'must be 1-64 alphanumeric characters and cannot be exclusively '.
268 'digits. For example, "%s" or "%s" are reasonable choices.',
269 $key,
270 'open',
271 'closed'));
272 }
273 if (!is_array($value)) {
274 throw new Exception(
275 pht(
276 'Value for key "%s" should be a dictionary.',
277 $key));
278 }
279
280 PhutilTypeSpec::checkMap(
281 $value,
282 array(
283 'name' => 'string',
284 'name.full' => 'optional string',
285 'name.action' => 'optional string',
286 'closed' => 'optional bool',
287 'special' => 'optional string',
288 'transaction.icon' => 'optional string',
289 'transaction.color' => 'optional string',
290 'silly' => 'optional bool',
291 'prefixes' => 'optional list<string>',
292 'suffixes' => 'optional list<string>',
293 'keywords' => 'optional list<string>',
294 'disabled' => 'optional bool',
295 'claim' => 'optional bool',
296 'locked' => 'optional bool|string',
297 'mfa' => 'optional bool',
298 ));
299 }
300
301 // Supported values are "comments" or "edits". For backward compatibility,
302 // "true" is an alias of "comments".
303
304 foreach ($config as $key => $value) {
305 $locked = idx($value, 'locked', false);
306 if ($locked === true || $locked === false) {
307 continue;
308 }
309
310 if ($locked === self::LOCKED_EDITS ||
311 $locked === self::LOCKED_COMMENTS) {
312 continue;
313 }
314
315 throw new Exception(
316 pht(
317 'Task status ("%s") has unrecognized value for "locked" '.
318 'configuration ("%s"). Supported values are: "%s", "%s".',
319 $key,
320 $locked,
321 self::LOCKED_COMMENTS,
322 self::LOCKED_EDITS));
323 }
324
325 $special_map = array();
326 foreach ($config as $key => $value) {
327 $special = idx($value, 'special');
328 if (!$special) {
329 continue;
330 }
331
332 if (isset($special_map[$special])) {
333 throw new Exception(
334 pht(
335 'Configuration has two statuses both marked with the special '.
336 'attribute "%s" ("%s" and "%s"). There should be only one.',
337 $special,
338 $special_map[$special],
339 $key));
340 }
341
342 switch ($special) {
343 case self::SPECIAL_DEFAULT:
344 if (!empty($value['closed'])) {
345 throw new Exception(
346 pht(
347 'Status "%s" is marked as default, but it is a closed '.
348 'status. The default status should be an open status.',
349 $key));
350 }
351 break;
352 case self::SPECIAL_CLOSED:
353 if (empty($value['closed'])) {
354 throw new Exception(
355 pht(
356 'Status "%s" is marked as the default status for closing '.
357 'tasks, but is not a closed status. It should be a closed '.
358 'status.',
359 $key));
360 }
361 break;
362 case self::SPECIAL_DUPLICATE:
363 if (empty($value['closed'])) {
364 throw new Exception(
365 pht(
366 'Status "%s" is marked as the status for closing tasks as '.
367 'duplicates, but it is not a closed status. It should '.
368 'be a closed status.',
369 $key));
370 }
371 break;
372 }
373
374 $special_map[$special] = $key;
375 }
376
377 // NOTE: We're not explicitly validating that we have at least one open
378 // and one closed status, because the DEFAULT and CLOSED specials imply
379 // that to be true. If those change in the future, that might become a
380 // reasonable thing to validate.
381
382 $required = array(
383 self::SPECIAL_DEFAULT,
384 self::SPECIAL_CLOSED,
385 self::SPECIAL_DUPLICATE,
386 );
387
388 foreach ($required as $required_special) {
389 if (!isset($special_map[$required_special])) {
390 throw new Exception(
391 pht(
392 'Configuration defines no task status with special attribute '.
393 '"%s", but you must specify a status which fills this special '.
394 'role.',
395 $required_special));
396 }
397 }
398 }
399
400}