@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 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}