@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
3final class PhabricatorMainMenuView extends AphrontView {
4
5 private $controller;
6 private $applicationMenu;
7
8 public function setApplicationMenu(PHUIListView $application_menu) {
9 $this->applicationMenu = $application_menu;
10 return $this;
11 }
12
13 public function getApplicationMenu() {
14 return $this->applicationMenu;
15 }
16
17 public function setController(PhabricatorController $controller) {
18 $this->controller = $controller;
19 return $this;
20 }
21
22 public function getController() {
23 return $this->controller;
24 }
25
26 private static function getFavicons() {
27 $refs = array();
28
29 $refs['favicon'] = id(new PhabricatorFaviconRef())
30 ->setWidth(64)
31 ->setHeight(64);
32
33 $refs['message_favicon'] = id(new PhabricatorFaviconRef())
34 ->setWidth(64)
35 ->setHeight(64)
36 ->setEmblems(
37 array(
38 'dot-pink',
39 null,
40 null,
41 null,
42 ));
43
44 id(new PhabricatorFaviconRefQuery())
45 ->withRefs($refs)
46 ->execute();
47
48 return mpull($refs, 'getURI');
49 }
50
51 public function render() {
52 $viewer = $this->getViewer();
53
54 require_celerity_resource('phabricator-main-menu-view');
55
56 $header_id = celerity_generate_unique_node_id();
57 $menu_bar = array();
58 $alerts = array();
59 $search_button = '';
60 $app_button = '';
61 $aural = null;
62
63 $is_full = $this->isFullSession($viewer);
64
65 if ($is_full) {
66 list($menu, $dropdowns, $aural) = $this->renderNotificationMenu();
67 if (array_filter($menu)) {
68 $alerts[] = $menu;
69 }
70 $menu_bar = array_merge($menu_bar, $dropdowns);
71 $app_button = $this->renderApplicationMenuButton();
72 $search_button = $this->renderSearchMenuButton($header_id);
73 } else if (!$viewer->isLoggedIn()) {
74 $app_button = $this->renderApplicationMenuButton();
75 if (PhabricatorEnv::getEnvConfig('policy.allow-public')) {
76 $search_button = $this->renderSearchMenuButton($header_id);
77 }
78 }
79
80 if ($search_button) {
81 $search_menu = $this->renderPhabricatorSearchMenu();
82 } else {
83 $search_menu = null;
84 }
85
86 if ($alerts) {
87 $alerts = javelin_tag(
88 'div',
89 array(
90 'class' => 'phabricator-main-menu-alerts',
91 ),
92 $alerts);
93 }
94
95 if ($aural) {
96 $aural = javelin_tag(
97 'span',
98 array(
99 'aural' => true,
100 ),
101 phutil_implode_html(' ', $aural));
102 }
103
104 $extensions = PhabricatorMainMenuBarExtension::getAllEnabledExtensions();
105 foreach ($extensions as $extension) {
106 $extension
107 ->setViewer($viewer)
108 ->setIsFullSession($is_full);
109
110 $controller = $this->getController();
111 if ($controller) {
112 $extension->setController($controller);
113 $application = $controller->getCurrentApplication();
114 if ($application) {
115 $extension->setApplication($application);
116 }
117 }
118 }
119
120 if (!$is_full) {
121 foreach ($extensions as $key => $extension) {
122 if ($extension->shouldRequireFullSession()) {
123 unset($extensions[$key]);
124 }
125 }
126 }
127
128 foreach ($extensions as $key => $extension) {
129 if (!$extension->isExtensionEnabledForViewer($extension->getViewer())) {
130 unset($extensions[$key]);
131 }
132 }
133
134 $menus = array();
135 foreach ($extensions as $extension) {
136 foreach ($extension->buildMainMenus() as $menu) {
137 $menus[] = $menu;
138 }
139 }
140
141 // Because we display these with "float: right", reverse their order before
142 // rendering them into the document so that the extension order and display
143 // order are the same.
144 $menus = array_reverse($menus);
145
146 foreach ($menus as $menu) {
147 $menu_bar[] = $menu;
148 }
149
150 $classes = array();
151 $classes[] = 'phabricator-main-menu';
152 $classes[] = 'phabricator-main-menu-background';
153
154 return phutil_tag(
155 'div',
156 array(
157 'class' => implode(' ', $classes),
158 'id' => $header_id,
159 ),
160 array(
161 $app_button,
162 $search_button,
163 $this->renderPhabricatorLogo(),
164 $alerts,
165 $aural,
166 $search_menu,
167 $menu_bar,
168 ));
169 }
170
171 private function renderSearch() {
172 $viewer = $this->getViewer();
173
174 $result = null;
175
176 $keyboard_config = array(
177 'helpURI' => '/help/keyboardshortcut/',
178 );
179
180 if ($viewer->isLoggedIn()) {
181 $show_search = $viewer->isUserActivated();
182 } else {
183 $show_search = PhabricatorEnv::getEnvConfig('policy.allow-public');
184 }
185
186 if ($show_search) {
187 $search = new PhabricatorMainMenuSearchView();
188 $search->setViewer($viewer);
189
190 $application = null;
191 $controller = $this->getController();
192 if ($controller) {
193 $application = $controller->getCurrentApplication();
194 }
195 if ($application) {
196 $search->setApplication($application);
197 }
198
199 $result = $search;
200 $keyboard_config['searchID'] = $search->getID();
201 }
202
203 $keyboard_config['pht'] = array(
204 '/' => pht('Give keyboard focus to the search box.'),
205 '?' => pht('Show keyboard shortcut help for the current page.'),
206 );
207
208 Javelin::initBehavior(
209 'phabricator-keyboard-shortcuts',
210 $keyboard_config);
211
212 if ($result) {
213 $result = id(new PHUIListItemView())
214 ->addClass('phabricator-main-menu-search')
215 ->appendChild($result);
216 }
217
218 return $result;
219 }
220
221 public function renderApplicationMenuButton() {
222 $dropdown = $this->renderApplicationMenu();
223 if (!$dropdown) {
224 return null;
225 }
226
227 return id(new PHUIButtonView())
228 ->setTag('a')
229 ->setHref('#')
230 ->setIcon('fa-bars')
231 ->addClass('phabricator-core-user-menu')
232 ->addClass('phabricator-core-user-mobile-menu')
233 ->setNoCSS(true)
234 ->setDropdownMenu($dropdown)
235 ->setAuralLabel(pht('Page Menu'));
236 }
237
238 private function renderApplicationMenu() {
239 $viewer = $this->getViewer();
240 $view = $this->getApplicationMenu();
241 if ($view) {
242 $items = $view->getItems();
243 $view = id(new PhabricatorActionListView())
244 ->setViewer($viewer);
245 foreach ($items as $item) {
246 $view->addAction(
247 id(new PhabricatorActionView())
248 ->setName($item->getName())
249 ->setHref($item->getHref())
250 ->setType($item->getType()));
251 }
252 }
253 return $view;
254 }
255
256 public function renderSearchMenuButton($header_id) {
257 $button_id = celerity_generate_unique_node_id();
258 return javelin_tag(
259 'a',
260 array(
261 'class' => 'phabricator-main-menu-search-button '.
262 'phabricator-expand-application-menu',
263 'sigil' => 'jx-toggle-class',
264 'meta' => array(
265 'map' => array(
266 $header_id => 'phabricator-search-menu-expanded',
267 $button_id => 'menu-icon-selected',
268 ),
269 ),
270 ),
271 phutil_tag(
272 'span',
273 array(
274 'class' => 'phabricator-menu-button-icon phui-icon-view '.
275 'phui-font-fa fa-search',
276 'id' => $button_id,
277 ),
278 ''));
279 }
280
281 private function renderPhabricatorSearchMenu() {
282
283 $view = new PHUIListView();
284 $view->addClass('phabricator-search-menu');
285
286 $search = $this->renderSearch();
287 if ($search) {
288 $view->addMenuItem($search);
289 }
290
291 return $view;
292 }
293
294 private function renderPhabricatorLogo() {
295 $logo_style = array();
296 $custom_header = PhabricatorCustomLogoConfigType::getLogoImagePHID();
297 if ($custom_header) {
298 $viewer = $this->getViewer();
299 $logo_uri = PhabricatorCustomLogoConfigType::getLogoURI($viewer);
300 $logo_style[] = 'background-size: 40px 40px;';
301 $logo_style[] = 'background-position: 0 0;';
302 $logo_style[] = 'background-image: url('.$logo_uri.')';
303 }
304
305 $logo_node = phutil_tag(
306 'span',
307 array(
308 'class' => 'phabricator-main-menu-project-logo',
309 'style' => implode(' ', $logo_style),
310 ));
311
312 $wordmark_text = PhabricatorCustomLogoConfigType::getLogoWordmark();
313 if (!phutil_nonempty_string($wordmark_text)) {
314 $wordmark_text = PlatformSymbols::getPlatformServerName();
315 }
316
317 $wordmark_node = phutil_tag(
318 'span',
319 array(
320 'class' => 'phabricator-wordmark',
321 ),
322 $wordmark_text);
323
324 return phutil_tag(
325 'a',
326 array(
327 'class' => 'phabricator-main-menu-brand',
328 'href' => '/',
329 ),
330 array(
331 javelin_tag(
332 'span',
333 array(
334 'aural' => true,
335 ),
336 pht('Home')),
337 $logo_node,
338 $wordmark_node,
339 ));
340 }
341
342 private function renderNotificationMenu() {
343 $viewer = $this->getViewer();
344
345 require_celerity_resource('phabricator-notification-css');
346 require_celerity_resource('phabricator-notification-menu-css');
347
348 $container_classes = array('alert-notifications');
349 $aural = array();
350
351 $dropdown_query = id(new AphlictDropdownDataQuery())
352 ->setViewer($viewer);
353 $dropdown_data = $dropdown_query->execute();
354
355 $message_tag = '';
356 $message_notification_dropdown = '';
357 $conpherence_app = PhabricatorConpherenceApplication::class;
358 $conpherence_data = $dropdown_data[$conpherence_app];
359 if ($conpherence_data['isInstalled']) {
360 $message_id = celerity_generate_unique_node_id();
361 $message_count_id = celerity_generate_unique_node_id();
362 $message_dropdown_id = celerity_generate_unique_node_id();
363
364 $message_count_number = $conpherence_data['rawCount'];
365
366 if ($message_count_number) {
367 $aural[] = phutil_tag(
368 'a',
369 array(
370 'href' => '/conpherence/',
371 ),
372 pht(
373 '%s unread messages.',
374 new PhutilNumber($message_count_number)));
375 } else {
376 $aural[] = pht('No messages.');
377 }
378
379 $message_count_tag = phutil_tag(
380 'span',
381 array(
382 'id' => $message_count_id,
383 'class' => 'phabricator-main-menu-message-count',
384 ),
385 $conpherence_data['count']);
386
387 $message_icon_tag = javelin_tag(
388 'span',
389 array(
390 'class' => 'phabricator-main-menu-message-icon phui-icon-view '.
391 'phui-font-fa fa-comments',
392 'sigil' => 'menu-icon',
393 ),
394 '');
395
396 if ($message_count_number) {
397 $container_classes[] = 'message-unread';
398 }
399
400 $message_tag = phutil_tag(
401 'a',
402 array(
403 'href' => '/conpherence/',
404 'class' => implode(' ', $container_classes),
405 'id' => $message_id,
406 'aria-label' => pht('Chat Messages'),
407 ),
408 array(
409 $message_icon_tag,
410 $message_count_tag,
411 ));
412
413 Javelin::initBehavior(
414 'aphlict-dropdown',
415 array(
416 'bubbleID' => $message_id,
417 'countID' => $message_count_id,
418 'dropdownID' => $message_dropdown_id,
419 'loadingText' => pht('Loading...'),
420 'uri' => '/conpherence/panel/',
421 'countType' => $conpherence_data['countType'],
422 'countNumber' => $message_count_number,
423 'unreadClass' => 'message-unread',
424 ) + self::getFavicons());
425
426 $message_notification_dropdown = javelin_tag(
427 'div',
428 array(
429 'id' => $message_dropdown_id,
430 'class' => 'phabricator-notification-menu',
431 'sigil' => 'phabricator-notification-menu',
432 'style' => 'display: none;',
433 ),
434 '');
435 }
436
437 $bubble_tag = '';
438 $notification_dropdown = '';
439 $notification_app = PhabricatorNotificationsApplication::class;
440 $notification_data = $dropdown_data[$notification_app];
441 if ($notification_data['isInstalled']) {
442 $count_id = celerity_generate_unique_node_id();
443 $dropdown_id = celerity_generate_unique_node_id();
444 $bubble_id = celerity_generate_unique_node_id();
445
446 $count_number = $notification_data['rawCount'];
447
448 if ($count_number) {
449 $aural[] = phutil_tag(
450 'a',
451 array(
452 'href' => '/notification/',
453 ),
454 pht(
455 '%s unread notifications.',
456 new PhutilNumber($count_number)));
457 } else {
458 $aural[] = pht('No notifications.');
459 }
460
461 $count_tag = phutil_tag(
462 'span',
463 array(
464 'id' => $count_id,
465 'class' => 'phabricator-main-menu-alert-count',
466 ),
467 $notification_data['count']);
468
469 $icon_tag = javelin_tag(
470 'span',
471 array(
472 'class' => 'phabricator-main-menu-alert-icon phui-icon-view '.
473 'phui-font-fa fa-bell',
474 'sigil' => 'menu-icon',
475 ),
476 '');
477
478 if ($count_number) {
479 $container_classes[] = 'alert-unread';
480 }
481
482 $bubble_tag = phutil_tag(
483 'a',
484 array(
485 'href' => '/notification/',
486 'class' => implode(' ', $container_classes),
487 'id' => $bubble_id,
488 'aria-label' => pht('Notifications'),
489 ),
490 array($icon_tag, $count_tag));
491
492 Javelin::initBehavior(
493 'aphlict-dropdown',
494 array(
495 'bubbleID' => $bubble_id,
496 'countID' => $count_id,
497 'dropdownID' => $dropdown_id,
498 'loadingText' => pht('Loading...'),
499 'uri' => '/notification/panel/',
500 'countType' => $notification_data['countType'],
501 'countNumber' => $count_number,
502 'unreadClass' => 'alert-unread',
503 ) + self::getFavicons());
504
505 $notification_dropdown = javelin_tag(
506 'div',
507 array(
508 'id' => $dropdown_id,
509 'class' => 'phabricator-notification-menu',
510 'sigil' => 'phabricator-notification-menu',
511 'style' => 'display: none;',
512 ),
513 '');
514 }
515
516 // Admin Level Urgent Notification Channel
517 $setup_tag = '';
518 $setup_notification_dropdown = '';
519 if ($viewer && $viewer->getIsAdmin()) {
520 $open = PhabricatorSetupCheck::getOpenSetupIssueKeys();
521 if ($open) {
522 $setup_id = celerity_generate_unique_node_id();
523 $setup_count_id = celerity_generate_unique_node_id();
524 $setup_dropdown_id = celerity_generate_unique_node_id();
525
526 $setup_count_number = count($open);
527
528 if ($setup_count_number) {
529 $aural[] = phutil_tag(
530 'a',
531 array(
532 'href' => '/config/issue/',
533 ),
534 pht(
535 '%s unresolved issues.',
536 new PhutilNumber($setup_count_number)));
537 } else {
538 $aural[] = pht('No issues.');
539 }
540
541 $setup_count_tag = phutil_tag(
542 'span',
543 array(
544 'id' => $setup_count_id,
545 'class' => 'phabricator-main-menu-setup-count',
546 ),
547 $setup_count_number);
548
549 $setup_icon_tag = javelin_tag(
550 'span',
551 array(
552 'class' => 'phabricator-main-menu-setup-icon phui-icon-view '.
553 'phui-font-fa fa-exclamation-circle',
554 'sigil' => 'menu-icon',
555 ),
556 '');
557
558 if ($setup_count_number) {
559 $container_classes[] = 'setup-unread';
560 }
561
562 $setup_tag = phutil_tag(
563 'a',
564 array(
565 'href' => '/config/issue/',
566 'class' => implode(' ', $container_classes),
567 'id' => $setup_id,
568 'aria-label' => pht('Unresolved Setup Issues'),
569 ),
570 array(
571 $setup_icon_tag,
572 $setup_count_tag,
573 ));
574
575 Javelin::initBehavior(
576 'aphlict-dropdown',
577 array(
578 'bubbleID' => $setup_id,
579 'countID' => $setup_count_id,
580 'dropdownID' => $setup_dropdown_id,
581 'loadingText' => pht('Loading...'),
582 'uri' => '/config/issue/panel/',
583 'countType' => null,
584 'countNumber' => null,
585 'unreadClass' => 'setup-unread',
586 ) + self::getFavicons());
587
588 $setup_notification_dropdown = javelin_tag(
589 'div',
590 array(
591 'id' => $setup_dropdown_id,
592 'class' => 'phabricator-notification-menu',
593 'sigil' => 'phabricator-notification-menu',
594 'style' => 'display: none;',
595 ),
596 '');
597 }
598 }
599
600 $user_dropdown = null;
601 $user_tag = null;
602 if ($viewer->isLoggedIn()) {
603 if (!$viewer->getIsEmailVerified()) {
604 $bubble_id = celerity_generate_unique_node_id();
605 $count_id = celerity_generate_unique_node_id();
606 $dropdown_id = celerity_generate_unique_node_id();
607
608 $settings_uri = id(new PhabricatorEmailAddressesSettingsPanel())
609 ->setViewer($viewer)
610 ->setUser($viewer)
611 ->getPanelURI();
612
613 $user_icon = javelin_tag(
614 'span',
615 array(
616 'class' => 'phabricator-main-menu-setup-icon phui-icon-view '.
617 'phui-font-fa fa-user',
618 'sigil' => 'menu-icon',
619 ));
620
621 $user_count = javelin_tag(
622 'span',
623 array(
624 'class' => 'phabricator-main-menu-setup-count',
625 'id' => $count_id,
626 ),
627 1);
628
629 $user_tag = phutil_tag(
630 'a',
631 array(
632 'href' => $settings_uri,
633 'class' => 'setup-unread',
634 'id' => $bubble_id,
635 'aria-label' => pht('Account Setup Issues'),
636 ),
637 array(
638 $user_icon,
639 $user_count,
640 ));
641
642 Javelin::initBehavior(
643 'aphlict-dropdown',
644 array(
645 'bubbleID' => $bubble_id,
646 'countID' => $count_id,
647 'dropdownID' => $dropdown_id,
648 'loadingText' => pht('Loading...'),
649 'uri' => '/settings/issue/',
650 'unreadClass' => 'setup-unread',
651 ));
652
653 $user_dropdown = javelin_tag(
654 'div',
655 array(
656 'id' => $dropdown_id,
657 'class' => 'phabricator-notification-menu',
658 'sigil' => 'phabricator-notification-menu',
659 'style' => 'display: none;',
660 ));
661 }
662 }
663
664 $dropdowns = array(
665 $notification_dropdown,
666 $message_notification_dropdown,
667 $setup_notification_dropdown,
668 $user_dropdown,
669 );
670
671 return array(
672 array(
673 $bubble_tag,
674 $message_tag,
675 $setup_tag,
676 $user_tag,
677 ),
678 $dropdowns,
679 $aural,
680 );
681 }
682
683 private function isFullSession(PhabricatorUser $viewer) {
684 if (!$viewer->isLoggedIn()) {
685 return false;
686 }
687
688 if (!$viewer->isUserActivated()) {
689 return false;
690 }
691
692 if (!$viewer->hasSession()) {
693 return false;
694 }
695
696 $session = $viewer->getSession();
697 if ($session->getIsPartial()) {
698 return false;
699 }
700
701 if (!$session->getSignedLegalpadDocuments()) {
702 return false;
703 }
704
705 $mfa_key = 'security.require-multi-factor-auth';
706 $need_mfa = PhabricatorEnv::getEnvConfig($mfa_key);
707 if ($need_mfa) {
708 $have_mfa = $viewer->getIsEnrolledInMultiFactor();
709 if (!$have_mfa) {
710 return false;
711 }
712 }
713
714 return true;
715 }
716
717}