4 * This is a standard Phabricator page with menus, Javelin, DarkConsole, and
7 final class PhabricatorStandardPageView
extends PhabricatorBarePageView
8 implements AphrontResponseProducerInterface
{
11 private $applicationName;
14 private $showChrome = true;
15 private $classes = array();
16 private $disableConsole;
17 private $pageObjects = array();
18 private $applicationMenu;
19 private $showFooter = true;
20 private $showDurableColumn = true;
21 private $quicksandConfig = array();
27 public function setShowFooter($show_footer) {
28 $this->showFooter
= $show_footer;
32 public function getShowFooter() {
33 return $this->showFooter
;
36 public function setApplicationName($application_name) {
37 $this->applicationName
= $application_name;
41 public function setDisableConsole($disable) {
42 $this->disableConsole
= $disable;
46 public function getApplicationName() {
47 return $this->applicationName
;
50 public function setBaseURI($base_uri) {
51 $this->baseURI
= $base_uri;
55 public function getBaseURI() {
56 return $this->baseURI
;
59 public function setShowChrome($show_chrome) {
60 $this->showChrome
= $show_chrome;
64 public function getShowChrome() {
65 return $this->showChrome
;
68 public function addClass($class) {
69 $this->classes
[] = $class;
73 public function setPageObjectPHIDs(array $phids) {
74 $this->pageObjects
= $phids;
78 public function setShowDurableColumn($show) {
79 $this->showDurableColumn
= $show;
83 public function getShowDurableColumn() {
84 $request = $this->getRequest();
89 $viewer = $request->getUser();
90 if (!$viewer->isLoggedIn()) {
94 $conpherence_installed = PhabricatorApplication
::isClassInstalledForViewer(
95 'PhabricatorConpherenceApplication',
97 if (!$conpherence_installed) {
101 if ($this->isQuicksandBlacklistURI()) {
108 private function isQuicksandBlacklistURI() {
109 $request = $this->getRequest();
114 $patterns = $this->getQuicksandURIPatternBlacklist();
115 $path = $request->getRequestURI()->getPath();
116 foreach ($patterns as $pattern) {
117 if (preg_match('(^'.$pattern.'$)', $path)) {
124 public function getDurableColumnVisible() {
125 $column_key = PhabricatorConpherenceColumnVisibleSetting
::SETTINGKEY
;
126 return (bool)$this->getUserPreference($column_key, false);
129 public function getDurableColumnMinimize() {
130 $column_key = PhabricatorConpherenceColumnMinimizeSetting
::SETTINGKEY
;
131 return (bool)$this->getUserPreference($column_key, false);
134 public function addQuicksandConfig(array $config) {
135 $this->quicksandConfig
= $config +
$this->quicksandConfig
;
139 public function getQuicksandConfig() {
140 return $this->quicksandConfig
;
143 public function setCrumbs(PHUICrumbsView
$crumbs) {
144 $this->crumbs
= $crumbs;
148 public function getCrumbs() {
149 return $this->crumbs
;
152 public function setTabs(PHUIListView
$tabs) {
153 $tabs->setType(PHUIListView
::TABBAR_LIST
);
154 $tabs->addClass('phabricator-standard-page-tabs');
159 public function getTabs() {
163 public function setNavigation(AphrontSideNavFilterView
$navigation) {
164 $this->navigation
= $navigation;
168 public function getNavigation() {
169 return $this->navigation
;
172 public function getTitle() {
173 $glyph_key = PhabricatorTitleGlyphsSetting
::SETTINGKEY
;
174 $glyph_on = PhabricatorTitleGlyphsSetting
::VALUE_TITLE_GLYPHS
;
175 $glyph_setting = $this->getUserPreference($glyph_key, $glyph_on);
177 $use_glyph = ($glyph_setting == $glyph_on);
179 $title = parent
::getTitle();
183 $prefix = $this->getGlyph();
185 $application_name = $this->getApplicationName();
186 if (strlen($application_name)) {
187 $prefix = '['.$application_name.']';
191 if ($prefix !== null && strlen($prefix)) {
192 $title = $prefix.' '.$title;
199 protected function willRenderPage() {
200 $footer = $this->renderFooter();
202 // NOTE: A cleaner solution would be to let body layout elements implement
203 // some kind of "LayoutInterface" so content can be embedded inside frames,
204 // but there's only really one use case for this for now.
205 $children = $this->renderChildren();
207 $layout = head($children);
208 if ($layout instanceof PHUIFormationView
) {
209 $layout->setFooter($footer);
214 $this->footer
= $footer;
216 parent
::willRenderPage();
218 if (!$this->getRequest()) {
221 'You must set the %s to render a %s.',
226 $console = $this->getConsole();
228 require_celerity_resource('phabricator-core-css');
229 require_celerity_resource('phabricator-zindex-css');
230 require_celerity_resource('phui-button-css');
231 require_celerity_resource('phui-spacing-css');
232 require_celerity_resource('phui-form-css');
233 require_celerity_resource('phabricator-standard-page-view');
234 require_celerity_resource('conpherence-durable-column-view');
235 require_celerity_resource('font-lato');
237 Javelin
::initBehavior('workflow', array());
239 $request = $this->getRequest();
242 $user = $request->getUser();
246 if ($user->isUserActivated()) {
247 $offset = $user->getTimeZoneOffset();
249 $ignore_key = PhabricatorTimezoneIgnoreOffsetSetting
::SETTINGKEY
;
250 $ignore = $user->getUserSetting($ignore_key);
252 Javelin
::initBehavior(
256 'uri' => '/settings/timezone/',
258 'Your browser timezone setting differs from the timezone '.
259 'setting in your profile, click to reconcile.'),
260 'ignoreKey' => $ignore_key,
264 if ($user->getIsAdmin()) {
265 $server_https = $request->isHTTPS();
266 $server_protocol = $server_https ?
'HTTPS' : 'HTTP';
267 $client_protocol = $server_https ?
'HTTP' : 'HTTPS';
269 $doc_name = 'Configuring a Preamble Script';
270 $doc_href = PhabricatorEnv
::getDoclink($doc_name);
272 Javelin
::initBehavior(
275 'server_https' => $server_https,
276 'doc_name' => pht('See Documentation'),
277 'doc_href' => $doc_href,
279 'This server thinks you are using %s, but your '.
280 'client is convinced that it is using %s. This is a serious '.
281 'misconfiguration with subtle, but significant, consequences.',
282 $server_protocol, $client_protocol),
287 Javelin
::initBehavior('lightbox-attachments');
290 Javelin
::initBehavior('aphront-form-disable-on-submit');
291 Javelin
::initBehavior('toggle-class', array());
292 Javelin
::initBehavior('history-install');
293 Javelin
::initBehavior('phabricator-gesture');
295 $current_token = null;
297 $current_token = $user->getCSRFToken();
300 Javelin
::initBehavior(
303 'tokenName' => AphrontRequest
::getCSRFTokenName(),
304 'header' => AphrontRequest
::getCSRFHeaderName(),
305 'viaHeader' => AphrontRequest
::getViaHeaderName(),
306 'current' => $current_token,
309 Javelin
::initBehavior('device');
311 Javelin
::initBehavior(
312 'high-security-warning',
313 $this->getHighSecurityWarningConfig());
315 if (PhabricatorEnv
::isReadOnly()) {
316 Javelin
::initBehavior(
319 'message' => PhabricatorEnv
::getReadOnlyMessage(),
320 'uri' => PhabricatorEnv
::getReadOnlyURI(),
324 // If we aren't showing the page chrome, skip rendering DarkConsole and the
325 // main menu, since they won't be visible on the page.
326 if (!$this->getShowChrome()) {
331 require_celerity_resource('aphront-dark-console-css');
334 if (DarkConsoleXHProfPluginAPI
::isProfilerStarted()) {
335 $headers[DarkConsoleXHProfPluginAPI
::getProfilerHeader()] = 'page';
337 if (DarkConsoleServicesPlugin
::isQueryAnalyzerRequested()) {
338 $headers[DarkConsoleServicesPlugin
::getQueryAnalyzerHeader()] = true;
341 Javelin
::initBehavior(
343 $this->getConsoleConfig());
349 $viewer = new PhabricatorUser();
352 $menu = id(new PhabricatorMainMenuView())
355 if ($this->getController()) {
356 $menu->setController($this->getController());
359 $application_menu = $this->applicationMenu
;
360 if ($application_menu) {
361 if ($application_menu instanceof PHUIApplicationMenuView
) {
362 $crumbs = $this->getCrumbs();
364 $application_menu->setCrumbs($crumbs);
367 $application_menu = $application_menu->buildListView();
370 $menu->setApplicationMenu($application_menu);
374 $this->menuContent
= $menu->render();
378 protected function getHead() {
381 $request = $this->getRequest();
383 $user = $request->getUser();
385 $monospaced = $user->getUserSetting(
386 PhabricatorMonospacedFontSetting
::SETTINGKEY
);
390 $response = CelerityAPI
::getStaticResourceResponse();
393 if (!empty($monospaced)) {
394 // We can't print this normally because escaping quotation marks will
395 // break the CSS. Instead, filter it strictly and then mark it as safe.
396 $monospaced = new PhutilSafeHTML(
397 PhabricatorMonospacedFontSetting
::filterMonospacedCSSRule(
400 $font_css = hsprintf(
401 '<style type="text/css">'.
402 '.PhabricatorMonospaced, '.
403 '.phabricator-remarkup .remarkup-code-block '.
404 '.remarkup-code { font: %s !important; } '.
413 $response->renderSingleResource('javelin-magical-init', 'phabricator'));
416 public function setGlyph($glyph) {
417 $this->glyph
= $glyph;
421 public function getGlyph() {
425 protected function willSendResponse($response) {
426 $request = $this->getRequest();
427 $response = parent
::willSendResponse($response);
429 $console = $request->getApplicationConfiguration()->getConsole();
432 $response = PhutilSafeHTML
::applyFunction(
434 hsprintf('<darkconsole />'),
435 $console->render($request),
442 protected function getBody() {
444 $request = $this->getRequest();
446 $user = $request->getUser();
449 $header_chrome = null;
450 if ($this->getShowChrome()) {
451 $header_chrome = $this->menuContent
;
455 $classes[] = 'main-page-frame';
456 $developer_warning = null;
457 if (PhabricatorEnv
::getEnvConfig('phabricator.developer-mode') &&
458 DarkConsoleErrorLogPluginAPI
::getErrors()) {
459 $developer_warning = phutil_tag_div(
460 'aphront-developer-error-callout',
462 'This page raised PHP errors. Find them in DarkConsole '.
463 'or the error log.'));
466 $main_page = phutil_tag(
469 'id' => 'phabricator-standard-page',
470 'class' => 'phabricator-standard-page',
478 'id' => 'phabricator-standard-page-body',
479 'class' => 'phabricator-standard-page-body',
481 $this->renderPageBodyContent()),
484 $durable_column = null;
485 if ($this->getShowDurableColumn()) {
486 $is_visible = $this->getDurableColumnVisible();
487 $is_minimize = $this->getDurableColumnMinimize();
488 $durable_column = id(new ConpherenceDurableColumnView())
489 ->setSelectedConpherence(null)
491 ->setQuicksandConfig($this->buildQuicksandConfig())
492 ->setVisible($is_visible)
493 ->setMinimize($is_minimize)
494 ->setInitialLoad(true);
496 $this->classes
[] = 'minimize-column';
500 Javelin
::initBehavior('quicksand-blacklist', array(
501 'patterns' => $this->getQuicksandURIPatternBlacklist(),
507 'class' => implode(' ', $classes),
508 'id' => 'main-page-frame',
516 private function renderPageBodyContent() {
517 $console = $this->getConsole();
519 $body = parent
::getBody();
521 $nav = $this->getNavigation();
522 $tabs = $this->getTabs();
524 $crumbs = $this->getCrumbs();
526 $nav->setCrumbs($crumbs);
528 $nav->appendChild($body);
529 $nav->appendFooter($this->footer
);
530 $content = phutil_implode_html('', array($nav->render()));
534 $crumbs = $this->getCrumbs();
536 if ($this->getTabs()) {
537 $crumbs->setBorder(true);
539 $content[] = $crumbs;
542 $tabs = $this->getTabs();
548 $content[] = $this->footer
;
550 $content = phutil_implode_html('', $content);
554 ($console ?
hsprintf('<darkconsole />') : null),
559 protected function getTail() {
560 $request = $this->getRequest();
561 $user = $request->getUser();
567 $response = CelerityAPI
::getStaticResourceResponse();
569 if ($request->isHTTPS()) {
570 $with_protocol = 'https';
572 $with_protocol = 'http';
575 $servers = PhabricatorNotificationServerRef
::getEnabledClientServers(
579 if ($user && $user->isLoggedIn()) {
580 // TODO: We could tell the browser about all the servers and let it
581 // do random reconnects to improve reliability.
583 $server = head($servers);
585 $client_uri = $server->getWebsocketURI();
587 Javelin
::initBehavior(
590 'websocketURI' => (string)$client_uri,
591 ) +
$this->buildAphlictListenConfigData());
593 CelerityAPI
::getStaticResourceResponse()
594 ->addContentSecurityPolicyURI('connect-src', $client_uri);
598 $tail[] = $response->renderHTMLFooter($this->getFrameable());
603 protected function getBodyClasses() {
606 if (!$this->getShowChrome()) {
607 $classes[] = 'phabricator-chromeless-page';
610 $agent = AphrontRequest
::getHTTPHeader('User-Agent');
612 // Try to guess the device resolution based on UA strings to avoid a flash
613 // of incorrectly-styled content.
614 $device_guess = 'device-desktop';
615 if (preg_match('@iPhone|iPod|(Android.*Chrome/[.0-9]* Mobile)@', $agent)) {
616 $device_guess = 'device-phone device';
617 } else if (preg_match('@iPad|(Android.*Chrome/)@', $agent)) {
618 $device_guess = 'device-tablet device';
621 $classes[] = $device_guess;
623 if (preg_match('@Windows@', $agent)) {
624 $classes[] = 'platform-windows';
625 } else if (preg_match('@Macintosh@', $agent)) {
626 $classes[] = 'platform-mac';
627 } else if (preg_match('@X11@', $agent)) {
628 $classes[] = 'platform-linux';
631 if ($this->getRequest()->getStr('__print__')) {
632 $classes[] = 'printable';
635 if ($this->getRequest()->getStr('__aural__')) {
636 $classes[] = 'audible';
639 $classes[] = 'phui-theme-'.PhabricatorEnv
::getEnvConfig('ui.header-color');
640 foreach ($this->classes
as $class) {
644 return implode(' ', $classes);
647 private function getConsole() {
648 if ($this->disableConsole
) {
651 return $this->getRequest()->getApplicationConfiguration()->getConsole();
654 private function getConsoleConfig() {
655 $user = $this->getRequest()->getUser();
658 if (DarkConsoleXHProfPluginAPI
::isProfilerStarted()) {
659 $headers[DarkConsoleXHProfPluginAPI
::getProfilerHeader()] = 'page';
661 if (DarkConsoleServicesPlugin
::isQueryAnalyzerRequested()) {
662 $headers[DarkConsoleServicesPlugin
::getQueryAnalyzerHeader()] = true;
666 $setting_tab = PhabricatorDarkConsoleTabSetting
::SETTINGKEY
;
667 $setting_visible = PhabricatorDarkConsoleVisibleSetting
::SETTINGKEY
;
668 $tab = $user->getUserSetting($setting_tab);
669 $visible = $user->getUserSetting($setting_visible);
676 // NOTE: We use a generic label here to prevent input reflection
677 // and mitigate compression attacks like BREACH. See discussion in
679 'uri' => pht('Main Request'),
681 'visible' => $visible,
682 'headers' => $headers,
686 private function getHighSecurityWarningConfig() {
687 $user = $this->getRequest()->getUser();
690 if ($user->hasSession()) {
691 $hisec = ($user->getSession()->getHighSecurityUntil() - time());
699 'uri' => '/auth/session/downgrade/',
701 'Your session is in high security mode. When you '.
702 'finish using it, click here to leave.'),
706 private function renderFooter() {
707 if (!$this->getShowChrome()) {
711 if (!$this->getShowFooter()) {
715 $items = PhabricatorEnv
::getEnvConfig('ui.footer-items');
721 foreach ($items as $item) {
722 $name = idx($item, 'name', pht('Unnamed Footer Item'));
724 $href = idx($item, 'href');
725 if (!PhabricatorEnv
::isValidURIForLink($href)) {
729 if ($href !== null) {
735 $foot[] = phutil_tag(
742 $foot = phutil_implode_html(" \xC2\xB7 ", $foot);
747 'class' => 'phabricator-standard-page-footer grouped',
752 public function renderForQuicksand() {
753 parent
::willRenderPage();
754 $response = $this->renderPageBodyContent();
755 $response = $this->willSendResponse($response);
757 $extra_config = $this->getQuicksandConfig();
760 'content' => hsprintf('%s', $response),
761 ) +
$this->buildQuicksandConfig()
765 private function buildQuicksandConfig() {
766 $viewer = $this->getRequest()->getUser();
767 $controller = $this->getController();
769 $dropdown_query = id(new AphlictDropdownDataQuery())
770 ->setViewer($viewer);
771 $dropdown_query->execute();
773 $hisec_warning_config = $this->getHighSecurityWarningConfig();
775 $console_config = null;
776 $console = $this->getConsole();
778 $console_config = $this->getConsoleConfig();
781 $upload_enabled = false;
783 $upload_enabled = $controller->isGlobalDragAndDropUploadEnabled();
786 $application_class = null;
787 $application_search_icon = null;
788 $application_help = null;
789 $controller = $this->getController();
791 $application = $controller->getCurrentApplication();
793 $application_class = get_class($application);
794 if ($application->getApplicationSearchDocumentTypes()) {
795 $application_search_icon = $application->getIcon();
798 $help_items = $application->getHelpMenuItems($viewer);
800 $help_list = id(new PhabricatorActionListView())
801 ->setViewer($viewer);
802 foreach ($help_items as $help_item) {
803 $help_list->addAction($help_item);
805 $application_help = $help_list->getDropdownMenuMetadata();
811 'title' => $this->getTitle(),
812 'bodyClasses' => $this->getBodyClasses(),
813 'aphlictDropdownData' => array(
814 $dropdown_query->getNotificationData(),
815 $dropdown_query->getConpherenceData(),
817 'globalDragAndDrop' => $upload_enabled,
818 'hisecWarningConfig' => $hisec_warning_config,
819 'consoleConfig' => $console_config,
820 'applicationClass' => $application_class,
821 'applicationSearchIcon' => $application_search_icon,
822 'helpItems' => $application_help,
823 ) +
$this->buildAphlictListenConfigData();
826 private function buildAphlictListenConfigData() {
827 $user = $this->getRequest()->getUser();
828 $subscriptions = $this->pageObjects
;
829 $subscriptions[] = $user->getPHID();
832 'pageObjects' => array_fill_keys($this->pageObjects
, true),
833 'subscriptions' => $subscriptions,
837 private function getQuicksandURIPatternBlacklist() {
838 $applications = PhabricatorApplication
::getAllApplications();
840 $blacklist = array();
841 foreach ($applications as $application) {
842 $blacklist[] = $application->getQuicksandURIPatternBlacklist();
845 // See T4340. Currently, Phortune and Auth both require pulling in external
846 // Javascript (for Stripe card management and Recaptcha, respectively).
847 // This can put us in a position where the user loads a page with a
848 // restrictive Content-Security-Policy, then uses Quicksand to navigate to
849 // a page which needs to load external scripts. For now, just blacklist
850 // these entire applications since we aren't giving up anything
851 // significant by doing so.
853 $blacklist[] = array(
858 return array_mergev($blacklist);
861 private function getUserPreference($key, $default = null) {
862 $request = $this->getRequest();
867 $user = $request->getUser();
872 return $user->getUserSetting($key);
875 public function produceAphrontResponse() {
876 $controller = $this->getController();
878 $viewer = $this->getUser();
879 if ($viewer && $viewer->getPHID()) {
880 $object_phids = $this->pageObjects
;
881 foreach ($object_phids as $object_phid) {
882 PhabricatorFeedStoryNotification
::updateObjectNotificationViews(
888 if ($this->getRequest()->isQuicksand()) {
889 $content = $this->renderForQuicksand();
890 $response = id(new AphrontAjaxResponse())
891 ->setContent($content);
893 // See T13247. Try to find some navigational menu items to create a
894 // mobile navigation menu from.
895 $application_menu = $controller->buildApplicationMenu();
896 if (!$application_menu) {
897 $navigation = $this->getNavigation();
899 $application_menu = $navigation->getMenu();
902 $this->applicationMenu
= $application_menu;
904 $content = $this->render();
906 $response = id(new AphrontWebpageResponse())
907 ->setContent($content)
908 ->setFrameable($this->getFrameable());