@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 info Method Information
5 * @task status Method Status
6 * @task pager Paging Results
7 */
8abstract class ConduitAPIMethod
9 extends Phobject
10 implements PhabricatorPolicyInterface {
11
12 private $viewer;
13
14 const METHOD_STATUS_STABLE = 'stable';
15 const METHOD_STATUS_UNSTABLE = 'unstable';
16 const METHOD_STATUS_DEPRECATED = 'deprecated';
17 const METHOD_STATUS_FROZEN = 'frozen';
18
19 const SCOPE_NEVER = 'scope.never';
20 const SCOPE_ALWAYS = 'scope.always';
21
22 /**
23 * Get a short, human-readable text summary of the method.
24 *
25 * @return string Short summary of method.
26 * @task info
27 */
28 public function getMethodSummary() {
29 return $this->getMethodDescription();
30 }
31
32
33 /**
34 * Get a detailed description of the method.
35 *
36 * This method should return remarkup.
37 *
38 * @return string Detailed description of the method.
39 * @task info
40 */
41 abstract public function getMethodDescription();
42
43 final public function getDocumentationPages(PhabricatorUser $viewer) {
44 $pages = $this->newDocumentationPages($viewer);
45 return $pages;
46 }
47
48 protected function newDocumentationPages(PhabricatorUser $viewer) {
49 return array();
50 }
51
52 /**
53 * @return ConduitAPIDocumentationPage
54 */
55 final protected function newDocumentationPage(PhabricatorUser $viewer) {
56 return id(new ConduitAPIDocumentationPage())
57 ->setIconIcon('fa-chevron-right');
58 }
59
60 /**
61 * @return ConduitAPIDocumentationPage
62 */
63 final protected function newDocumentationBoxPage(
64 PhabricatorUser $viewer,
65 $title,
66 $content) {
67
68 $box_view = id(new PHUIObjectBoxView())
69 ->setHeaderText($title)
70 ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY)
71 ->setTable($content);
72
73 return $this->newDocumentationPage($viewer)
74 ->setName($title)
75 ->setContent($box_view);
76 }
77
78 /**
79 * @return array Pairs of parameter name and their type
80 */
81 abstract protected function defineParamTypes();
82
83 /**
84 * @return string Human-readable text description of the return format,
85 * for example 'array<string,mixed> | null'
86 */
87 abstract protected function defineReturnType();
88
89 /**
90 * @return array Pairs of error code and the corresponding error message
91 */
92 protected function defineErrorTypes() {
93 return array();
94 }
95
96 abstract protected function execute(ConduitAPIRequest $request);
97
98 public function isInternalAPI() {
99 return false;
100 }
101
102 public function getParamTypes() {
103 $types = $this->defineParamTypes();
104
105 $query = $this->newQueryObject();
106 if ($query) {
107 $types['order'] = 'optional order';
108 $types += $this->getPagerParamTypes();
109 }
110
111 return $types;
112 }
113
114 /**
115 * @return string Human-readable text description of the return format,
116 * for example 'array<string,mixed> | null'
117 */
118 public function getReturnType() {
119 return $this->defineReturnType();
120 }
121
122 /**
123 * @return array Pairs of error code and the corresponding error message
124 */
125 public function getErrorTypes() {
126 return $this->defineErrorTypes();
127 }
128
129 /**
130 * This is mostly for compatibility with
131 * @{class:PhabricatorCursorPagedPolicyAwareQuery}.
132 */
133 public function getID() {
134 return $this->getAPIMethodName();
135 }
136
137 /**
138 * Get the status for this method (e.g., stable, unstable or deprecated).
139 * Should return a METHOD_STATUS_* constant. By default, methods are
140 * "stable".
141 *
142 * @return string METHOD_STATUS_* constant.
143 * @task status
144 */
145 public function getMethodStatus() {
146 return self::METHOD_STATUS_STABLE;
147 }
148
149 /**
150 * Optional description to supplement the method status. In particular, if
151 * a method is deprecated, you can return a string here describing the reason
152 * for deprecation and stable alternatives.
153 *
154 * @return string|null Description of the method status, if available.
155 * @task status
156 */
157 public function getMethodStatusDescription() {
158 return null;
159 }
160
161 public function getErrorDescription($error_code) {
162 return idx($this->getErrorTypes(), $error_code, pht('Unknown Error'));
163 }
164
165 public function getRequiredScope() {
166 return self::SCOPE_NEVER;
167 }
168
169 public function executeMethod(ConduitAPIRequest $request) {
170 $this->setViewer($request->getUser());
171
172 $client = $this->newConduitCallProxyClient($request);
173 if ($client) {
174 // We're proxying, so just make an intracluster call.
175 return $client->callMethodSynchronous(
176 $this->getAPIMethodName(),
177 $request->getAllParameters());
178 }
179
180 return $this->execute($request);
181 }
182
183 protected function newConduitCallProxyClient(ConduitAPIRequest $request) {
184 return null;
185 }
186
187 abstract public function getAPIMethodName();
188
189 /**
190 * Return a key which sorts methods by application name, then method status,
191 * then method name.
192 *
193 * @return string For example 'almanac.0.namespace.edit' or 'user.2.enable'
194 */
195 public function getSortOrder() {
196 $name = $this->getAPIMethodName();
197
198 $map = array(
199 self::METHOD_STATUS_STABLE => 0,
200 self::METHOD_STATUS_UNSTABLE => 1,
201 self::METHOD_STATUS_DEPRECATED => 2,
202 );
203 $ord = idx($map, $this->getMethodStatus(), 0);
204
205 list($head, $tail) = explode('.', $name, 2);
206
207 return "{$head}.{$ord}.{$tail}";
208 }
209
210 public static function getMethodStatusMap() {
211 $map = array(
212 self::METHOD_STATUS_STABLE => pht('Stable'),
213 self::METHOD_STATUS_UNSTABLE => pht('Unstable'),
214 self::METHOD_STATUS_DEPRECATED => pht('Deprecated'),
215 );
216
217 return $map;
218 }
219
220 public function getApplicationName() {
221 return head(explode('.', $this->getAPIMethodName(), 2));
222 }
223
224 public static function loadAllConduitMethods() {
225 return self::newClassMapQuery()->execute();
226 }
227
228 private static function newClassMapQuery() {
229 return id(new PhutilClassMapQuery())
230 ->setAncestorClass(self::class)
231 ->setUniqueMethod('getAPIMethodName');
232 }
233
234 public static function getConduitMethod($method_name) {
235 return id(new PhabricatorCachedClassMapQuery())
236 ->setClassMapQuery(self::newClassMapQuery())
237 ->setMapKeyMethod('getAPIMethodName')
238 ->loadClass($method_name);
239 }
240
241 /**
242 * Whether to require a session key for calling the API method.
243 *
244 * @return bool Defaults to true
245 */
246 public function shouldRequireAuthentication() {
247 return true;
248 }
249
250 /**
251 * Whether to allow public access. Related to the `policy.allow-public`
252 * global setting and policies set for the corresponding application.
253 *
254 * @return bool Defaults to false
255 */
256 public function shouldAllowPublic() {
257 return false;
258 }
259
260 /**
261 * Whether not to guard writes against CSRF. See @{class:AphrontWriteGuard}.
262 *
263 * @return bool Defaults to false
264 */
265 public function shouldAllowUnguardedWrites() {
266 return false;
267 }
268
269
270 /**
271 * Optionally, return a @{class:PhabricatorApplication} which this call is
272 * part of. The call will be disabled when the application is disabled.
273 *
274 * @return PhabricatorApplication|null Related application.
275 */
276 public function getApplication() {
277 return null;
278 }
279
280 protected function formatStringConstants($constants) {
281 foreach ($constants as $key => $value) {
282 $constants[$key] = '"'.$value.'"';
283 }
284 $constants = implode(', ', $constants);
285 return 'string-constant<'.$constants.'>';
286 }
287
288 public static function getParameterMetadataKey($key) {
289 if (strncmp($key, 'api.', 4) === 0) {
290 // All keys passed beginning with "api." are always metadata keys.
291 return substr($key, 4);
292 } else {
293 switch ($key) {
294 // These are real keys which always belong to request metadata.
295 case 'access_token':
296 case 'scope':
297 case 'output':
298
299 // This is not a real metadata key; it is included here only to
300 // prevent Conduit methods from defining it.
301 case '__conduit__':
302
303 // This is prevented globally as a blanket defense against OAuth
304 // redirection attacks. It is included here to stop Conduit methods
305 // from defining it.
306 case 'code':
307
308 // This is not a real metadata key, but the presence of this
309 // parameter triggers an alternate request decoding pathway.
310 case 'params':
311 return $key;
312 }
313 }
314
315 return null;
316 }
317
318 final public function setViewer(PhabricatorUser $viewer) {
319 $this->viewer = $viewer;
320 return $this;
321 }
322
323 final public function getViewer() {
324 return $this->viewer;
325 }
326
327/* -( Paging Results )----------------------------------------------------- */
328
329
330 /**
331 * @task pager
332 */
333 protected function getPagerParamTypes() {
334 return array(
335 'before' => 'optional string',
336 'after' => 'optional string',
337 'limit' => 'optional int (default = 100)',
338 );
339 }
340
341
342 /**
343 * @return AphrontCursorPagerView
344 * @task pager
345 */
346 protected function newPager(ConduitAPIRequest $request) {
347 $limit = $request->getValue('limit', 100);
348 $limit = min(1000, $limit);
349 $limit = max(1, $limit);
350
351 $pager = id(new AphrontCursorPagerView())
352 ->setPageSize($limit);
353
354 $before_id = $request->getValue('before');
355 if ($before_id !== null) {
356 $pager->setBeforeID($before_id);
357 }
358
359 $after_id = $request->getValue('after');
360 if ($after_id !== null) {
361 $pager->setAfterID($after_id);
362 }
363
364 return $pager;
365 }
366
367
368 /**
369 * @task pager
370 */
371 protected function addPagerResults(
372 array $results,
373 AphrontCursorPagerView $pager) {
374
375 $results['cursor'] = array(
376 'limit' => $pager->getPageSize(),
377 'after' => $pager->getNextPageID(),
378 'before' => $pager->getPrevPageID(),
379 );
380
381 return $results;
382 }
383
384
385/* -( Implementing Query Methods )----------------------------------------- */
386
387
388 public function newQueryObject() {
389 return null;
390 }
391
392
393 protected function newQueryForRequest(ConduitAPIRequest $request) {
394 $query = $this->newQueryObject();
395
396 if (!$query) {
397 throw new Exception(
398 pht(
399 'You can not call newQueryFromRequest() in this method ("%s") '.
400 'because it does not implement newQueryObject().',
401 get_class($this)));
402 }
403
404 if (!($query instanceof PhabricatorCursorPagedPolicyAwareQuery)) {
405 throw new Exception(
406 pht(
407 'Call to method newQueryObject() did not return an object of class '.
408 '"%s".',
409 'PhabricatorCursorPagedPolicyAwareQuery'));
410 }
411
412 $query->setViewer($request->getUser());
413
414 $order = $request->getValue('order');
415 if ($order !== null) {
416 if (is_scalar($order)) {
417 $query->setOrder($order);
418 } else {
419 $query->setOrderVector($order);
420 }
421 }
422
423 return $query;
424 }
425
426
427/* -( PhabricatorPolicyInterface )----------------------------------------- */
428
429
430 public function getPHID() {
431 return null;
432 }
433
434 public function getCapabilities() {
435 return array(
436 PhabricatorPolicyCapability::CAN_VIEW,
437 );
438 }
439
440 public function getPolicy($capability) {
441 // Application methods get application visibility; other methods get open
442 // visibility.
443
444 $application = $this->getApplication();
445 if ($application) {
446 return $application->getPolicy($capability);
447 }
448
449 return PhabricatorPolicies::getMostOpenPolicy();
450 }
451
452 public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
453 if (!$this->shouldRequireAuthentication()) {
454 // Make unauthenticated methods universally visible.
455 return true;
456 }
457
458 return false;
459 }
460
461 protected function hasApplicationCapability(
462 $capability,
463 PhabricatorUser $viewer) {
464
465 $application = $this->getApplication();
466
467 if (!$application) {
468 return false;
469 }
470
471 return PhabricatorPolicyFilter::hasCapability(
472 $viewer,
473 $application,
474 $capability);
475 }
476
477 protected function requireApplicationCapability(
478 $capability,
479 PhabricatorUser $viewer) {
480
481 $application = $this->getApplication();
482 if (!$application) {
483 return;
484 }
485
486 PhabricatorPolicyFilter::requireCapability(
487 $viewer,
488 $this->getApplication(),
489 $capability);
490 }
491
492 final protected function newRemarkupDocumentationView($remarkup) {
493 $viewer = $this->getViewer();
494
495 $view = new PHUIRemarkupView($viewer, $remarkup);
496
497 $view->setRemarkupOptions(
498 array(
499 PHUIRemarkupView::OPTION_PRESERVE_LINEBREAKS => false,
500 ));
501
502 return id(new PHUIBoxView())
503 ->appendChild($view)
504 ->addPadding(PHUI::PADDING_LARGE);
505 }
506
507}