@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 685 lines 18 kB view raw
1<?php 2 3/** 4 * @task info Application Information 5 * @task ui UI Integration 6 * @task uri URI Routing 7 * @task mail Email integration 8 * @task fact Fact Integration 9 * @task meta Application Management 10 */ 11abstract class PhabricatorApplication 12 extends PhabricatorLiskDAO 13 implements 14 PhabricatorPolicyInterface, 15 PhabricatorApplicationTransactionInterface { 16 17 const GROUP_CORE = 'core'; 18 const GROUP_UTILITIES = 'util'; 19 const GROUP_ADMIN = 'admin'; 20 const GROUP_DEVELOPER = 'developer'; 21 22 final public static function getApplicationGroups() { 23 return array( 24 self::GROUP_CORE => pht('Core Applications'), 25 self::GROUP_UTILITIES => pht('Utilities'), 26 self::GROUP_ADMIN => pht('Administration'), 27 self::GROUP_DEVELOPER => pht('Developer Tools'), 28 ); 29 } 30 31 final public function getApplicationName() { 32 return 'application'; 33 } 34 35 final public function getTableName() { 36 return 'application_application'; 37 } 38 39 final protected function getConfiguration() { 40 return array( 41 self::CONFIG_AUX_PHID => true, 42 ) + parent::getConfiguration(); 43 } 44 45 final public function generatePHID() { 46 return $this->getPHID(); 47 } 48 49 final public function save() { 50 // When "save()" is called on applications, we just return without 51 // actually writing anything to the database. 52 return $this; 53 } 54 55 56/* -( Application Information )-------------------------------------------- */ 57 58 abstract public function getName(); 59 60 public function getShortDescription() { 61 return pht('%s Application', $this->getName()); 62 } 63 64 /** 65 * Extensions are allowed to register multi-character monograms. 66 * The name "Monogram" is actually a bit of a misnomer, 67 * but we're keeping it due to the history. 68 * 69 * @return array 70 */ 71 public function getMonograms() { 72 return array(); 73 } 74 75 public function isDeprecated() { 76 return false; 77 } 78 79 final public function isInstalled() { 80 if (!$this->canUninstall()) { 81 return true; 82 } 83 84 $prototypes = PhabricatorEnv::getEnvConfig('phabricator.show-prototypes'); 85 if (!$prototypes && $this->isPrototype()) { 86 return false; 87 } 88 89 $uninstalled = PhabricatorEnv::getEnvConfig( 90 'phabricator.uninstalled-applications'); 91 92 return empty($uninstalled[get_class($this)]); 93 } 94 95 96 public function isPrototype() { 97 return false; 98 } 99 100 101 /** 102 * Return `true` if this application should never appear in application lists 103 * in the UI. Primarily intended for unit test applications or other 104 * pseudo-applications. 105 * 106 * Few applications should be unlisted. For most applications, use 107 * @{method:isLaunchable} to hide them from main launch views instead. 108 * 109 * @return bool True to remove application from UI lists. 110 */ 111 public function isUnlisted() { 112 return false; 113 } 114 115 116 /** 117 * Return `true` if this application is a normal application with a base 118 * URI and a web interface. 119 * 120 * Launchable applications can be pinned to the home page, and show up in the 121 * "Launcher" view of the Applications application. Making an application 122 * unlaunchable prevents pinning and hides it from this view. 123 * 124 * Usually, an application should be marked unlaunchable if: 125 * 126 * - it is available on every page anyway (like search); or 127 * - it does not have a web interface (like subscriptions); or 128 * - it is still pre-release and being intentionally buried. 129 * 130 * To hide applications more completely, use @{method:isUnlisted}. 131 * 132 * @return bool True if the application is launchable. 133 */ 134 public function isLaunchable() { 135 return true; 136 } 137 138 139 /** 140 * Return `true` if this application should be pinned by default. 141 * 142 * Users who have not yet set preferences see a default list of applications. 143 * 144 * @param PhabricatorUser $viewer User viewing the pinned application list. 145 * @return bool True if this application should be pinned by default. 146 */ 147 public function isPinnedByDefault(PhabricatorUser $viewer) { 148 return false; 149 } 150 151 152 /** 153 * Returns true if an application is first-party and false otherwise. 154 * 155 * @return bool True if this application is first-party. 156 */ 157 final public function isFirstParty() { 158 $where = id(new ReflectionClass($this))->getFileName(); 159 $root = phutil_get_library_root('phabricator'); 160 161 if (!Filesystem::isDescendant($where, $root)) { 162 return false; 163 } 164 165 if (Filesystem::isDescendant($where, $root.'/extensions')) { 166 return false; 167 } 168 169 return true; 170 } 171 172 public function canUninstall() { 173 return true; 174 } 175 176 final public function getPHID() { 177 return 'PHID-APPS-'.get_class($this); 178 } 179 180 public function getTypeaheadURI() { 181 return $this->isLaunchable() ? $this->getBaseURI() : null; 182 } 183 184 public function getBaseURI() { 185 return null; 186 } 187 188 final public function getApplicationURI($path = '') { 189 return $this->getBaseURI().ltrim($path, '/'); 190 } 191 192 public function getIcon() { 193 return 'fa-puzzle-piece'; 194 } 195 196 public function getApplicationOrder() { 197 return PHP_INT_MAX; 198 } 199 200 public function getApplicationGroup() { 201 return self::GROUP_CORE; 202 } 203 204 public function getTitleGlyph() { 205 return null; 206 } 207 208 final public function getHelpMenuItems(PhabricatorUser $viewer) { 209 $items = array(); 210 211 $articles = $this->getHelpDocumentationArticles($viewer); 212 if ($articles) { 213 foreach ($articles as $article) { 214 $item = id(new PhabricatorActionView()) 215 ->setName($article['name']) 216 ->setHref($article['href']) 217 ->addSigil('help-item') 218 ->setOpenInNewWindow(true); 219 $items[] = $item; 220 } 221 } 222 223 if (PhabricatorEnv::getEnvConfig('metamta.reply-handler-domain')) { 224 $command_specs = $this->getMailCommandObjects(); 225 if ($command_specs) { 226 foreach ($command_specs as $key => $spec) { 227 $object = $spec['object']; 228 229 $class = get_class($this); 230 $href = '/applications/mailcommands/'.$class.'/'.$key.'/'; 231 $item = id(new PhabricatorActionView()) 232 ->setName($spec['name']) 233 ->setHref($href) 234 ->addSigil('help-item') 235 ->setOpenInNewWindow(true); 236 $items[] = $item; 237 } 238 } 239 } 240 241 if ($items) { 242 $divider = id(new PhabricatorActionView()) 243 ->addSigil('help-item') 244 ->setType(PhabricatorActionView::TYPE_DIVIDER); 245 array_unshift($items, $divider); 246 } 247 248 return array_values($items); 249 } 250 251 public function getHelpDocumentationArticles(PhabricatorUser $viewer) { 252 return array(); 253 } 254 255 /** 256 * Get the Application Overview in raw Remarkup 257 * 258 * @return string|null 259 */ 260 public function getOverview() { 261 return null; 262 } 263 264 public function getEventListeners() { 265 return array(); 266 } 267 268 public function getRemarkupRules() { 269 return array(); 270 } 271 272 public function getQuicksandURIPatternBlacklist() { 273 return array(); 274 } 275 276 public function getMailCommandObjects() { 277 return array(); 278 } 279 280 281/* -( URI Routing )-------------------------------------------------------- */ 282 283 284 public function getRoutes() { 285 return array(); 286 } 287 288 public function getResourceRoutes() { 289 return array(); 290 } 291 292 293/* -( Email Integration )-------------------------------------------------- */ 294 295 296 /** 297 * Whether the application supports inbound email 298 * 299 * @return bool 300 */ 301 public function supportsEmailIntegration() { 302 return false; 303 } 304 305 final protected function getInboundEmailSupportLink() { 306 return PhabricatorEnv::getDoclink('Configuring Inbound Email'); 307 } 308 309 public function getAppEmailBlurb() { 310 throw new PhutilMethodNotImplementedException(); 311 } 312 313/* -( Fact Integration )--------------------------------------------------- */ 314 315 316 public function getFactObjectsForAnalysis() { 317 return array(); 318 } 319 320 321/* -( UI Integration )----------------------------------------------------- */ 322 323 324 /** 325 * You can provide an optional piece of flavor text for the application. This 326 * is currently rendered in application launch views if the application has no 327 * status elements. 328 * 329 * @return string|null Flavor text. 330 * @task ui 331 */ 332 public function getFlavorText() { 333 return null; 334 } 335 336 337 /** 338 * Build items for the main menu. 339 * 340 * @param PhabricatorUser $user The viewing user. 341 * @param PhabricatorController|null $controller (optional) The current 342 * controller. Null for special pages like 404, exception handlers, etc. 343 * @return list<PHUIListItemView> List of menu items. 344 * @task ui 345 */ 346 public function buildMainMenuItems( 347 PhabricatorUser $user, 348 ?PhabricatorController $controller = null) { 349 return array(); 350 } 351 352 353/* -( Application Management )--------------------------------------------- */ 354 355 356 final public static function getByClass($class_name) { 357 $selected = null; 358 $applications = self::getAllApplications(); 359 360 foreach ($applications as $application) { 361 if (get_class($application) == $class_name) { 362 $selected = $application; 363 break; 364 } 365 } 366 367 if (!$selected) { 368 throw new Exception(pht("No application '%s'!", $class_name)); 369 } 370 371 return $selected; 372 } 373 374 final public static function getAllApplications() { 375 static $applications; 376 377 if ($applications === null) { 378 $apps = id(new PhutilClassMapQuery()) 379 ->setAncestorClass(self::class) 380 ->setSortMethod('getApplicationOrder') 381 ->execute(); 382 383 // Reorder the applications into "application order". Notably, this 384 // ensures their event handlers register in application order. 385 $apps = mgroup($apps, 'getApplicationGroup'); 386 387 $group_order = array_keys(self::getApplicationGroups()); 388 $apps = array_select_keys($apps, $group_order) + $apps; 389 390 $apps = array_mergev($apps); 391 392 $applications = $apps; 393 } 394 395 return $applications; 396 } 397 398 final public static function getAllInstalledApplications() { 399 $all_applications = self::getAllApplications(); 400 $apps = array(); 401 foreach ($all_applications as $app) { 402 if (!$app->isInstalled()) { 403 continue; 404 } 405 406 $apps[] = $app; 407 } 408 409 return $apps; 410 } 411 412 413 /** 414 * Determine if an application is enabled, by application class name. 415 * 416 * To check if an application is enabled //and// available to a particular 417 * viewer, user @{method:isClassInstalledForViewer}. 418 * 419 * @param class-string<PhabricatorApplication> $class Application class name. 420 * @return bool True if the application is enabled. 421 * @task meta 422 */ 423 final public static function isClassInstalled($class) { 424 return self::getByClass($class)->isInstalled(); 425 } 426 427 428 /** 429 * Determine if an application is enabled and available to a viewer, by 430 * application class name. 431 * 432 * To check if an application is enabled at all, use 433 * @{method:isClassInstalled}. 434 * 435 * @param class-string<PhabricatorApplication> $class Application class name. 436 * @param PhabricatorUser $viewer Viewing user. 437 * @return bool True if the application is enabled for the viewer. 438 * @task meta 439 */ 440 final public static function isClassInstalledForViewer( 441 $class, 442 PhabricatorUser $viewer) { 443 444 if ($viewer->isOmnipotent()) { 445 return true; 446 } 447 448 $cache = PhabricatorCaches::getRequestCache(); 449 $viewer_fragment = $viewer->getCacheFragment(); 450 $key = 'app.'.$class.'.installed.'.$viewer_fragment; 451 452 $result = $cache->getKey($key); 453 if ($result === null) { 454 if (!self::isClassInstalled($class)) { 455 $result = false; 456 } else { 457 $application = self::getByClass($class); 458 if (!$application->canUninstall()) { 459 // If the application can not be disabled, always allow viewers 460 // to see it. In particular, this allows logged-out viewers to see 461 // Settings and load global default settings even if the install 462 // does not allow public viewers. 463 $result = true; 464 } else { 465 $result = PhabricatorPolicyFilter::hasCapability( 466 $viewer, 467 self::getByClass($class), 468 PhabricatorPolicyCapability::CAN_VIEW); 469 } 470 } 471 472 $cache->setKey($key, $result); 473 } 474 475 return $result; 476 } 477 478/* -( PhabricatorPolicyInterface )----------------------------------------- */ 479 480 481 public function getCapabilities() { 482 return array_merge( 483 array( 484 PhabricatorPolicyCapability::CAN_VIEW, 485 PhabricatorPolicyCapability::CAN_EDIT, 486 ), 487 array_keys($this->getCustomCapabilities())); 488 } 489 490 public function getPolicy($capability) { 491 $default = $this->getCustomPolicySetting($capability); 492 if ($default) { 493 return $default; 494 } 495 496 switch ($capability) { 497 case PhabricatorPolicyCapability::CAN_VIEW: 498 return PhabricatorPolicies::getMostOpenPolicy(); 499 case PhabricatorPolicyCapability::CAN_EDIT: 500 return PhabricatorPolicies::POLICY_ADMIN; 501 default: 502 $spec = $this->getCustomCapabilitySpecification($capability); 503 return idx($spec, 'default', PhabricatorPolicies::POLICY_USER); 504 } 505 } 506 507 public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { 508 return false; 509 } 510 511 512/* -( Policies )----------------------------------------------------------- */ 513 514 protected function getCustomCapabilities() { 515 return array(); 516 } 517 518 private function getCustomPolicySetting($capability) { 519 if (!$this->isCapabilityEditable($capability)) { 520 return null; 521 } 522 523 $policy_locked = PhabricatorEnv::getEnvConfig('policy.locked'); 524 if (isset($policy_locked[$capability])) { 525 return $policy_locked[$capability]; 526 } 527 528 $config = PhabricatorEnv::getEnvConfig('phabricator.application-settings'); 529 530 $app = idx($config, $this->getPHID()); 531 if (!$app) { 532 return null; 533 } 534 535 $policy = idx($app, 'policy'); 536 if (!$policy) { 537 return null; 538 } 539 540 return idx($policy, $capability); 541 } 542 543 544 private function getCustomCapabilitySpecification($capability) { 545 $custom = $this->getCustomCapabilities(); 546 if (!isset($custom[$capability])) { 547 throw new Exception(pht("Unknown capability '%s'!", $capability)); 548 } 549 return $custom[$capability]; 550 } 551 552 final public function getCapabilityLabel($capability) { 553 switch ($capability) { 554 case PhabricatorPolicyCapability::CAN_VIEW: 555 return pht('Can Use Application'); 556 case PhabricatorPolicyCapability::CAN_EDIT: 557 return pht('Can Configure Application'); 558 } 559 560 $capobj = PhabricatorPolicyCapability::getCapabilityByKey($capability); 561 if ($capobj) { 562 return $capobj->getCapabilityName(); 563 } 564 565 return null; 566 } 567 568 final public function isCapabilityEditable($capability) { 569 switch ($capability) { 570 case PhabricatorPolicyCapability::CAN_VIEW: 571 return $this->canUninstall(); 572 case PhabricatorPolicyCapability::CAN_EDIT: 573 return true; 574 default: 575 $spec = $this->getCustomCapabilitySpecification($capability); 576 return idx($spec, 'edit', true); 577 } 578 } 579 580 final public function getCapabilityCaption($capability) { 581 switch ($capability) { 582 case PhabricatorPolicyCapability::CAN_VIEW: 583 if (!$this->canUninstall()) { 584 return pht( 585 'This application is required, so all '. 586 'users must have access to it.'); 587 } else { 588 return null; 589 } 590 case PhabricatorPolicyCapability::CAN_EDIT: 591 return null; 592 default: 593 $spec = $this->getCustomCapabilitySpecification($capability); 594 return idx($spec, 'caption'); 595 } 596 } 597 598 final public function getCapabilityTemplatePHIDType($capability) { 599 switch ($capability) { 600 case PhabricatorPolicyCapability::CAN_VIEW: 601 case PhabricatorPolicyCapability::CAN_EDIT: 602 return null; 603 } 604 605 $spec = $this->getCustomCapabilitySpecification($capability); 606 return idx($spec, 'template'); 607 } 608 609 final public function getDefaultObjectTypePolicyMap() { 610 $map = array(); 611 612 foreach ($this->getCustomCapabilities() as $capability => $spec) { 613 if (empty($spec['template'])) { 614 continue; 615 } 616 if (empty($spec['capability'])) { 617 continue; 618 } 619 $default = $this->getPolicy($capability); 620 $map[$spec['template']][$spec['capability']] = $default; 621 } 622 623 return $map; 624 } 625 626 /** 627 * @return array<string|null> Type constants of supported document types, 628 * e.g. 'DREV' for Differential Revisions or 'TASK' for Maniphest Tasks 629 */ 630 public function getApplicationSearchDocumentTypes() { 631 return array(); 632 } 633 634 protected function getEditRoutePattern($base = null) { 635 return $base.'(?:'. 636 '(?P<id>[0-9]\d*)/)?'. 637 '(?:'. 638 '(?:'. 639 '(?P<editAction>parameters|nodefault|nocreate|nomanage|comment)/'. 640 '|'. 641 '(?:form/(?P<formKey>[^/]+)/)?(?:page/(?P<pageKey>[^/]+)/)?'. 642 ')'. 643 ')?'; 644 } 645 646 protected function getBulkRoutePattern($base = null) { 647 return $base.'(?:query/(?P<queryKey>[^/]+)/)?'; 648 } 649 650 protected function getQueryRoutePattern($base = null) { 651 return $base.'(?:query/(?P<queryKey>[^/]+)/(?:(?P<queryAction>[^/]+)/)?)?'; 652 } 653 654 protected function getProfileMenuRouting($controller) { 655 $edit_route = $this->getEditRoutePattern(); 656 657 $mode_route = '(?P<itemEditMode>global|custom)/'; 658 659 return array( 660 '(?P<itemAction>view)/(?P<itemID>[^/]+)/' => $controller, 661 '(?P<itemAction>hide)/(?P<itemID>[^/]+)/' => $controller, 662 '(?P<itemAction>default)/(?P<itemID>[^/]+)/' => $controller, 663 '(?P<itemAction>configure)/' => $controller, 664 '(?P<itemAction>configure)/'.$mode_route => $controller, 665 '(?P<itemAction>reorder)/'.$mode_route => $controller, 666 '(?P<itemAction>edit)/'.$edit_route => $controller, 667 '(?P<itemAction>new)/'.$mode_route.'(?<itemKey>[^/]+)/'.$edit_route 668 => $controller, 669 '(?P<itemAction>builtin)/(?<itemID>[^/]+)/'.$edit_route 670 => $controller, 671 ); 672 } 673 674/* -( PhabricatorApplicationTransactionInterface )------------------------- */ 675 676 677 public function getApplicationTransactionEditor() { 678 return new PhabricatorApplicationEditor(); 679 } 680 681 public function getApplicationTransactionTemplate() { 682 return new PhabricatorApplicationApplicationTransaction(); 683 } 684 685}