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
11 abstract class PhabricatorApplication
12 extends PhabricatorLiskDAO
14 PhabricatorPolicyInterface
,
15 PhabricatorApplicationTransactionInterface
{
17 const GROUP_CORE
= 'core';
18 const GROUP_UTILITIES
= 'util';
19 const GROUP_ADMIN
= 'admin';
20 const GROUP_DEVELOPER
= 'developer';
22 final public static function getApplicationGroups() {
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'),
31 final public function getApplicationName() {
35 final public function getTableName() {
36 return 'application_application';
39 final protected function getConfiguration() {
41 self
::CONFIG_AUX_PHID
=> true,
42 ) + parent
::getConfiguration();
45 final public function generatePHID() {
46 return $this->getPHID();
49 final public function save() {
50 // When "save()" is called on applications, we just return without
51 // actually writing anything to the database.
56 /* -( Application Information )-------------------------------------------- */
58 abstract public function getName();
60 public function getShortDescription() {
61 return pht('%s Application', $this->getName());
64 final public function isInstalled() {
65 if (!$this->canUninstall()) {
69 $prototypes = PhabricatorEnv
::getEnvConfig('phabricator.show-prototypes');
70 if (!$prototypes && $this->isPrototype()) {
74 $uninstalled = PhabricatorEnv
::getEnvConfig(
75 'phabricator.uninstalled-applications');
77 return empty($uninstalled[get_class($this)]);
81 public function isPrototype() {
87 * Return `true` if this application should never appear in application lists
88 * in the UI. Primarily intended for unit test applications or other
89 * pseudo-applications.
91 * Few applications should be unlisted. For most applications, use
92 * @{method:isLaunchable} to hide them from main launch views instead.
94 * @return bool True to remove application from UI lists.
96 public function isUnlisted() {
102 * Return `true` if this application is a normal application with a base
103 * URI and a web interface.
105 * Launchable applications can be pinned to the home page, and show up in the
106 * "Launcher" view of the Applications application. Making an application
107 * unlaunchable prevents pinning and hides it from this view.
109 * Usually, an application should be marked unlaunchable if:
111 * - it is available on every page anyway (like search); or
112 * - it does not have a web interface (like subscriptions); or
113 * - it is still pre-release and being intentionally buried.
115 * To hide applications more completely, use @{method:isUnlisted}.
117 * @return bool True if the application is launchable.
119 public function isLaunchable() {
125 * Return `true` if this application should be pinned by default.
127 * Users who have not yet set preferences see a default list of applications.
129 * @param PhabricatorUser User viewing the pinned application list.
130 * @return bool True if this application should be pinned by default.
132 public function isPinnedByDefault(PhabricatorUser
$viewer) {
138 * Returns true if an application is first-party and false otherwise.
140 * @return bool True if this application is first-party.
142 final public function isFirstParty() {
143 $where = id(new ReflectionClass($this))->getFileName();
144 $root = phutil_get_library_root('phabricator');
146 if (!Filesystem
::isDescendant($where, $root)) {
150 if (Filesystem
::isDescendant($where, $root.'/extensions')) {
157 public function canUninstall() {
161 final public function getPHID() {
162 return 'PHID-APPS-'.get_class($this);
165 public function getTypeaheadURI() {
166 return $this->isLaunchable() ?
$this->getBaseURI() : null;
169 public function getBaseURI() {
173 final public function getApplicationURI($path = '') {
174 return $this->getBaseURI().ltrim($path, '/');
177 public function getIcon() {
178 return 'fa-puzzle-piece';
181 public function getApplicationOrder() {
185 public function getApplicationGroup() {
186 return self
::GROUP_CORE
;
189 public function getTitleGlyph() {
193 final public function getHelpMenuItems(PhabricatorUser
$viewer) {
196 $articles = $this->getHelpDocumentationArticles($viewer);
198 foreach ($articles as $article) {
199 $item = id(new PhabricatorActionView())
200 ->setName($article['name'])
201 ->setHref($article['href'])
202 ->addSigil('help-item')
203 ->setOpenInNewWindow(true);
208 $command_specs = $this->getMailCommandObjects();
209 if ($command_specs) {
210 foreach ($command_specs as $key => $spec) {
211 $object = $spec['object'];
213 $class = get_class($this);
214 $href = '/applications/mailcommands/'.$class.'/'.$key.'/';
215 $item = id(new PhabricatorActionView())
216 ->setName($spec['name'])
218 ->addSigil('help-item')
219 ->setOpenInNewWindow(true);
225 $divider = id(new PhabricatorActionView())
226 ->addSigil('help-item')
227 ->setType(PhabricatorActionView
::TYPE_DIVIDER
);
228 array_unshift($items, $divider);
231 return array_values($items);
234 public function getHelpDocumentationArticles(PhabricatorUser
$viewer) {
238 public function getOverview() {
242 public function getEventListeners() {
246 public function getRemarkupRules() {
250 public function getQuicksandURIPatternBlacklist() {
254 public function getMailCommandObjects() {
259 /* -( URI Routing )-------------------------------------------------------- */
262 public function getRoutes() {
266 public function getResourceRoutes() {
271 /* -( Email Integration )-------------------------------------------------- */
274 public function supportsEmailIntegration() {
278 final protected function getInboundEmailSupportLink() {
279 return PhabricatorEnv
::getDoclink('Configuring Inbound Email');
282 public function getAppEmailBlurb() {
283 throw new PhutilMethodNotImplementedException();
286 /* -( Fact Integration )--------------------------------------------------- */
289 public function getFactObjectsForAnalysis() {
294 /* -( UI Integration )----------------------------------------------------- */
298 * You can provide an optional piece of flavor text for the application. This
299 * is currently rendered in application launch views if the application has no
302 * @return string|null Flavor text.
305 public function getFlavorText() {
311 * Build items for the main menu.
313 * @param PhabricatorUser The viewing user.
314 * @param AphrontController The current controller. May be null for special
315 * pages like 404, exception handlers, etc.
316 * @return list<PHUIListItemView> List of menu items.
319 public function buildMainMenuItems(
320 PhabricatorUser
$user,
321 PhabricatorController
$controller = null) {
326 /* -( Application Management )--------------------------------------------- */
329 final public static function getByClass($class_name) {
331 $applications = self
::getAllApplications();
333 foreach ($applications as $application) {
334 if (get_class($application) == $class_name) {
335 $selected = $application;
341 throw new Exception(pht("No application '%s'!", $class_name));
347 final public static function getAllApplications() {
348 static $applications;
350 if ($applications === null) {
351 $apps = id(new PhutilClassMapQuery())
352 ->setAncestorClass(__CLASS__
)
353 ->setSortMethod('getApplicationOrder')
356 // Reorder the applications into "application order". Notably, this
357 // ensures their event handlers register in application order.
358 $apps = mgroup($apps, 'getApplicationGroup');
360 $group_order = array_keys(self
::getApplicationGroups());
361 $apps = array_select_keys($apps, $group_order) +
$apps;
363 $apps = array_mergev($apps);
365 $applications = $apps;
368 return $applications;
371 final public static function getAllInstalledApplications() {
372 $all_applications = self
::getAllApplications();
374 foreach ($all_applications as $app) {
375 if (!$app->isInstalled()) {
387 * Determine if an application is installed, by application class name.
389 * To check if an application is installed //and// available to a particular
390 * viewer, user @{method:isClassInstalledForViewer}.
392 * @param string Application class name.
393 * @return bool True if the class is installed.
396 final public static function isClassInstalled($class) {
397 return self
::getByClass($class)->isInstalled();
402 * Determine if an application is installed and available to a viewer, by
403 * application class name.
405 * To check if an application is installed at all, use
406 * @{method:isClassInstalled}.
408 * @param string Application class name.
409 * @param PhabricatorUser Viewing user.
410 * @return bool True if the class is installed for the viewer.
413 final public static function isClassInstalledForViewer(
415 PhabricatorUser
$viewer) {
417 if ($viewer->isOmnipotent()) {
421 $cache = PhabricatorCaches
::getRequestCache();
422 $viewer_fragment = $viewer->getCacheFragment();
423 $key = 'app.'.$class.'.installed.'.$viewer_fragment;
425 $result = $cache->getKey($key);
426 if ($result === null) {
427 if (!self
::isClassInstalled($class)) {
430 $application = self
::getByClass($class);
431 if (!$application->canUninstall()) {
432 // If the application can not be uninstalled, always allow viewers
433 // to see it. In particular, this allows logged-out viewers to see
434 // Settings and load global default settings even if the install
435 // does not allow public viewers.
438 $result = PhabricatorPolicyFilter
::hasCapability(
440 self
::getByClass($class),
441 PhabricatorPolicyCapability
::CAN_VIEW
);
445 $cache->setKey($key, $result);
452 /* -( PhabricatorPolicyInterface )----------------------------------------- */
455 public function getCapabilities() {
458 PhabricatorPolicyCapability
::CAN_VIEW
,
459 PhabricatorPolicyCapability
::CAN_EDIT
,
461 array_keys($this->getCustomCapabilities()));
464 public function getPolicy($capability) {
465 $default = $this->getCustomPolicySetting($capability);
470 switch ($capability) {
471 case PhabricatorPolicyCapability
::CAN_VIEW
:
472 return PhabricatorPolicies
::getMostOpenPolicy();
473 case PhabricatorPolicyCapability
::CAN_EDIT
:
474 return PhabricatorPolicies
::POLICY_ADMIN
;
476 $spec = $this->getCustomCapabilitySpecification($capability);
477 return idx($spec, 'default', PhabricatorPolicies
::POLICY_USER
);
481 public function hasAutomaticCapability($capability, PhabricatorUser
$viewer) {
486 /* -( Policies )----------------------------------------------------------- */
488 protected function getCustomCapabilities() {
492 private function getCustomPolicySetting($capability) {
493 if (!$this->isCapabilityEditable($capability)) {
497 $policy_locked = PhabricatorEnv
::getEnvConfig('policy.locked');
498 if (isset($policy_locked[$capability])) {
499 return $policy_locked[$capability];
502 $config = PhabricatorEnv
::getEnvConfig('phabricator.application-settings');
504 $app = idx($config, $this->getPHID());
509 $policy = idx($app, 'policy');
514 return idx($policy, $capability);
518 private function getCustomCapabilitySpecification($capability) {
519 $custom = $this->getCustomCapabilities();
520 if (!isset($custom[$capability])) {
521 throw new Exception(pht("Unknown capability '%s'!", $capability));
523 return $custom[$capability];
526 final public function getCapabilityLabel($capability) {
527 switch ($capability) {
528 case PhabricatorPolicyCapability
::CAN_VIEW
:
529 return pht('Can Use Application');
530 case PhabricatorPolicyCapability
::CAN_EDIT
:
531 return pht('Can Configure Application');
534 $capobj = PhabricatorPolicyCapability
::getCapabilityByKey($capability);
536 return $capobj->getCapabilityName();
542 final public function isCapabilityEditable($capability) {
543 switch ($capability) {
544 case PhabricatorPolicyCapability
::CAN_VIEW
:
545 return $this->canUninstall();
546 case PhabricatorPolicyCapability
::CAN_EDIT
:
549 $spec = $this->getCustomCapabilitySpecification($capability);
550 return idx($spec, 'edit', true);
554 final public function getCapabilityCaption($capability) {
555 switch ($capability) {
556 case PhabricatorPolicyCapability
::CAN_VIEW
:
557 if (!$this->canUninstall()) {
559 'This application is required for Phabricator to operate, so all '.
560 'users must have access to it.');
564 case PhabricatorPolicyCapability
::CAN_EDIT
:
567 $spec = $this->getCustomCapabilitySpecification($capability);
568 return idx($spec, 'caption');
572 final public function getCapabilityTemplatePHIDType($capability) {
573 switch ($capability) {
574 case PhabricatorPolicyCapability
::CAN_VIEW
:
575 case PhabricatorPolicyCapability
::CAN_EDIT
:
579 $spec = $this->getCustomCapabilitySpecification($capability);
580 return idx($spec, 'template');
583 final public function getDefaultObjectTypePolicyMap() {
586 foreach ($this->getCustomCapabilities() as $capability => $spec) {
587 if (empty($spec['template'])) {
590 if (empty($spec['capability'])) {
593 $default = $this->getPolicy($capability);
594 $map[$spec['template']][$spec['capability']] = $default;
600 public function getApplicationSearchDocumentTypes() {
604 protected function getEditRoutePattern($base = null) {
606 '(?P<id>[0-9]\d*)/)?'.
609 '(?P<editAction>parameters|nodefault|nocreate|nomanage|comment)/'.
611 '(?:form/(?P<formKey>[^/]+)/)?(?:page/(?P<pageKey>[^/]+)/)?'.
616 protected function getBulkRoutePattern($base = null) {
617 return $base.'(?:query/(?P<queryKey>[^/]+)/)?';
620 protected function getQueryRoutePattern($base = null) {
621 return $base.'(?:query/(?P<queryKey>[^/]+)/(?:(?P<queryAction>[^/]+)/)?)?';
624 protected function getProfileMenuRouting($controller) {
625 $edit_route = $this->getEditRoutePattern();
627 $mode_route = '(?P<itemEditMode>global|custom)/';
630 '(?P<itemAction>view)/(?P<itemID>[^/]+)/' => $controller,
631 '(?P<itemAction>hide)/(?P<itemID>[^/]+)/' => $controller,
632 '(?P<itemAction>default)/(?P<itemID>[^/]+)/' => $controller,
633 '(?P<itemAction>configure)/' => $controller,
634 '(?P<itemAction>configure)/'.$mode_route => $controller,
635 '(?P<itemAction>reorder)/'.$mode_route => $controller,
636 '(?P<itemAction>edit)/'.$edit_route => $controller,
637 '(?P<itemAction>new)/'.$mode_route.'(?<itemKey>[^/]+)/'.$edit_route
639 '(?P<itemAction>builtin)/(?<itemID>[^/]+)/'.$edit_route
644 /* -( PhabricatorApplicationTransactionInterface )------------------------- */
647 public function getApplicationTransactionEditor() {
648 return new PhabricatorApplicationEditor();
651 public function getApplicationTransactionTemplate() {
652 return new PhabricatorApplicationApplicationTransaction();