@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
at recaptime-dev/main 507 lines 12 kB view raw
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}