Path: blob/master/src/view/page/PhabricatorStandardPageView.php
12241 views
<?php12/**3* This is a standard Phabricator page with menus, Javelin, DarkConsole, and4* basic styles.5*/6final class PhabricatorStandardPageView extends PhabricatorBarePageView7implements AphrontResponseProducerInterface {89private $baseURI;10private $applicationName;11private $glyph;12private $menuContent;13private $showChrome = true;14private $classes = array();15private $disableConsole;16private $pageObjects = array();17private $applicationMenu;18private $showFooter = true;19private $showDurableColumn = true;20private $quicksandConfig = array();21private $tabs;22private $crumbs;23private $navigation;24private $footer;2526public function setShowFooter($show_footer) {27$this->showFooter = $show_footer;28return $this;29}3031public function getShowFooter() {32return $this->showFooter;33}3435public function setApplicationName($application_name) {36$this->applicationName = $application_name;37return $this;38}3940public function setDisableConsole($disable) {41$this->disableConsole = $disable;42return $this;43}4445public function getApplicationName() {46return $this->applicationName;47}4849public function setBaseURI($base_uri) {50$this->baseURI = $base_uri;51return $this;52}5354public function getBaseURI() {55return $this->baseURI;56}5758public function setShowChrome($show_chrome) {59$this->showChrome = $show_chrome;60return $this;61}6263public function getShowChrome() {64return $this->showChrome;65}6667public function addClass($class) {68$this->classes[] = $class;69return $this;70}7172public function setPageObjectPHIDs(array $phids) {73$this->pageObjects = $phids;74return $this;75}7677public function setShowDurableColumn($show) {78$this->showDurableColumn = $show;79return $this;80}8182public function getShowDurableColumn() {83$request = $this->getRequest();84if (!$request) {85return false;86}8788$viewer = $request->getUser();89if (!$viewer->isLoggedIn()) {90return false;91}9293$conpherence_installed = PhabricatorApplication::isClassInstalledForViewer(94'PhabricatorConpherenceApplication',95$viewer);96if (!$conpherence_installed) {97return false;98}99100if ($this->isQuicksandBlacklistURI()) {101return false;102}103104return true;105}106107private function isQuicksandBlacklistURI() {108$request = $this->getRequest();109if (!$request) {110return false;111}112113$patterns = $this->getQuicksandURIPatternBlacklist();114$path = $request->getRequestURI()->getPath();115foreach ($patterns as $pattern) {116if (preg_match('(^'.$pattern.'$)', $path)) {117return true;118}119}120return false;121}122123public function getDurableColumnVisible() {124$column_key = PhabricatorConpherenceColumnVisibleSetting::SETTINGKEY;125return (bool)$this->getUserPreference($column_key, false);126}127128public function getDurableColumnMinimize() {129$column_key = PhabricatorConpherenceColumnMinimizeSetting::SETTINGKEY;130return (bool)$this->getUserPreference($column_key, false);131}132133public function addQuicksandConfig(array $config) {134$this->quicksandConfig = $config + $this->quicksandConfig;135return $this;136}137138public function getQuicksandConfig() {139return $this->quicksandConfig;140}141142public function setCrumbs(PHUICrumbsView $crumbs) {143$this->crumbs = $crumbs;144return $this;145}146147public function getCrumbs() {148return $this->crumbs;149}150151public function setTabs(PHUIListView $tabs) {152$tabs->setType(PHUIListView::TABBAR_LIST);153$tabs->addClass('phabricator-standard-page-tabs');154$this->tabs = $tabs;155return $this;156}157158public function getTabs() {159return $this->tabs;160}161162public function setNavigation(AphrontSideNavFilterView $navigation) {163$this->navigation = $navigation;164return $this;165}166167public function getNavigation() {168return $this->navigation;169}170171public function getTitle() {172$glyph_key = PhabricatorTitleGlyphsSetting::SETTINGKEY;173$glyph_on = PhabricatorTitleGlyphsSetting::VALUE_TITLE_GLYPHS;174$glyph_setting = $this->getUserPreference($glyph_key, $glyph_on);175176$use_glyph = ($glyph_setting == $glyph_on);177178$title = parent::getTitle();179180$prefix = null;181if ($use_glyph) {182$prefix = $this->getGlyph();183} else {184$application_name = $this->getApplicationName();185if (strlen($application_name)) {186$prefix = '['.$application_name.']';187}188}189190if ($prefix !== null && strlen($prefix)) {191$title = $prefix.' '.$title;192}193194return $title;195}196197198protected function willRenderPage() {199$footer = $this->renderFooter();200201// NOTE: A cleaner solution would be to let body layout elements implement202// some kind of "LayoutInterface" so content can be embedded inside frames,203// but there's only really one use case for this for now.204$children = $this->renderChildren();205if ($children) {206$layout = head($children);207if ($layout instanceof PHUIFormationView) {208$layout->setFooter($footer);209$footer = null;210}211}212213$this->footer = $footer;214215parent::willRenderPage();216217if (!$this->getRequest()) {218throw new Exception(219pht(220'You must set the %s to render a %s.',221'Request',222__CLASS__));223}224225$console = $this->getConsole();226227require_celerity_resource('phabricator-core-css');228require_celerity_resource('phabricator-zindex-css');229require_celerity_resource('phui-button-css');230require_celerity_resource('phui-spacing-css');231require_celerity_resource('phui-form-css');232require_celerity_resource('phabricator-standard-page-view');233require_celerity_resource('conpherence-durable-column-view');234require_celerity_resource('font-lato');235236Javelin::initBehavior('workflow', array());237238$request = $this->getRequest();239$user = null;240if ($request) {241$user = $request->getUser();242}243244if ($user) {245if ($user->isUserActivated()) {246$offset = $user->getTimeZoneOffset();247248$ignore_key = PhabricatorTimezoneIgnoreOffsetSetting::SETTINGKEY;249$ignore = $user->getUserSetting($ignore_key);250251Javelin::initBehavior(252'detect-timezone',253array(254'offset' => $offset,255'uri' => '/settings/timezone/',256'message' => pht(257'Your browser timezone setting differs from the timezone '.258'setting in your profile, click to reconcile.'),259'ignoreKey' => $ignore_key,260'ignore' => $ignore,261));262263if ($user->getIsAdmin()) {264$server_https = $request->isHTTPS();265$server_protocol = $server_https ? 'HTTPS' : 'HTTP';266$client_protocol = $server_https ? 'HTTP' : 'HTTPS';267268$doc_name = 'Configuring a Preamble Script';269$doc_href = PhabricatorEnv::getDoclink($doc_name);270271Javelin::initBehavior(272'setup-check-https',273array(274'server_https' => $server_https,275'doc_name' => pht('See Documentation'),276'doc_href' => $doc_href,277'message' => pht(278'This server thinks you are using %s, but your '.279'client is convinced that it is using %s. This is a serious '.280'misconfiguration with subtle, but significant, consequences.',281$server_protocol, $client_protocol),282));283}284}285286Javelin::initBehavior('lightbox-attachments');287}288289Javelin::initBehavior('aphront-form-disable-on-submit');290Javelin::initBehavior('toggle-class', array());291Javelin::initBehavior('history-install');292Javelin::initBehavior('phabricator-gesture');293294$current_token = null;295if ($user) {296$current_token = $user->getCSRFToken();297}298299Javelin::initBehavior(300'refresh-csrf',301array(302'tokenName' => AphrontRequest::getCSRFTokenName(),303'header' => AphrontRequest::getCSRFHeaderName(),304'viaHeader' => AphrontRequest::getViaHeaderName(),305'current' => $current_token,306));307308Javelin::initBehavior('device');309310Javelin::initBehavior(311'high-security-warning',312$this->getHighSecurityWarningConfig());313314if (PhabricatorEnv::isReadOnly()) {315Javelin::initBehavior(316'read-only-warning',317array(318'message' => PhabricatorEnv::getReadOnlyMessage(),319'uri' => PhabricatorEnv::getReadOnlyURI(),320));321}322323// If we aren't showing the page chrome, skip rendering DarkConsole and the324// main menu, since they won't be visible on the page.325if (!$this->getShowChrome()) {326return;327}328329if ($console) {330require_celerity_resource('aphront-dark-console-css');331332$headers = array();333if (DarkConsoleXHProfPluginAPI::isProfilerStarted()) {334$headers[DarkConsoleXHProfPluginAPI::getProfilerHeader()] = 'page';335}336if (DarkConsoleServicesPlugin::isQueryAnalyzerRequested()) {337$headers[DarkConsoleServicesPlugin::getQueryAnalyzerHeader()] = true;338}339340Javelin::initBehavior(341'dark-console',342$this->getConsoleConfig());343}344345if ($user) {346$viewer = $user;347} else {348$viewer = new PhabricatorUser();349}350351$menu = id(new PhabricatorMainMenuView())352->setUser($viewer);353354if ($this->getController()) {355$menu->setController($this->getController());356}357358$application_menu = $this->applicationMenu;359if ($application_menu) {360if ($application_menu instanceof PHUIApplicationMenuView) {361$crumbs = $this->getCrumbs();362if ($crumbs) {363$application_menu->setCrumbs($crumbs);364}365366$application_menu = $application_menu->buildListView();367}368369$menu->setApplicationMenu($application_menu);370}371372373$this->menuContent = $menu->render();374}375376377protected function getHead() {378$monospaced = null;379380$request = $this->getRequest();381if ($request) {382$user = $request->getUser();383if ($user) {384$monospaced = $user->getUserSetting(385PhabricatorMonospacedFontSetting::SETTINGKEY);386}387}388389$response = CelerityAPI::getStaticResourceResponse();390391$font_css = null;392if (!empty($monospaced)) {393// We can't print this normally because escaping quotation marks will394// break the CSS. Instead, filter it strictly and then mark it as safe.395$monospaced = new PhutilSafeHTML(396PhabricatorMonospacedFontSetting::filterMonospacedCSSRule(397$monospaced));398399$font_css = hsprintf(400'<style type="text/css">'.401'.PhabricatorMonospaced, '.402'.phabricator-remarkup .remarkup-code-block '.403'.remarkup-code { font: %s !important; } '.404'</style>',405$monospaced);406}407408return hsprintf(409'%s%s%s',410parent::getHead(),411$font_css,412$response->renderSingleResource('javelin-magical-init', 'phabricator'));413}414415public function setGlyph($glyph) {416$this->glyph = $glyph;417return $this;418}419420public function getGlyph() {421return $this->glyph;422}423424protected function willSendResponse($response) {425$request = $this->getRequest();426$response = parent::willSendResponse($response);427428$console = $request->getApplicationConfiguration()->getConsole();429430if ($console) {431$response = PhutilSafeHTML::applyFunction(432'str_replace',433hsprintf('<darkconsole />'),434$console->render($request),435$response);436}437438return $response;439}440441protected function getBody() {442$user = null;443$request = $this->getRequest();444if ($request) {445$user = $request->getUser();446}447448$header_chrome = null;449if ($this->getShowChrome()) {450$header_chrome = $this->menuContent;451}452453$classes = array();454$classes[] = 'main-page-frame';455$developer_warning = null;456if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode') &&457DarkConsoleErrorLogPluginAPI::getErrors()) {458$developer_warning = phutil_tag_div(459'aphront-developer-error-callout',460pht(461'This page raised PHP errors. Find them in DarkConsole '.462'or the error log.'));463}464465$main_page = phutil_tag(466'div',467array(468'id' => 'phabricator-standard-page',469'class' => 'phabricator-standard-page',470),471array(472$developer_warning,473$header_chrome,474phutil_tag(475'div',476array(477'id' => 'phabricator-standard-page-body',478'class' => 'phabricator-standard-page-body',479),480$this->renderPageBodyContent()),481));482483$durable_column = null;484if ($this->getShowDurableColumn()) {485$is_visible = $this->getDurableColumnVisible();486$is_minimize = $this->getDurableColumnMinimize();487$durable_column = id(new ConpherenceDurableColumnView())488->setSelectedConpherence(null)489->setUser($user)490->setQuicksandConfig($this->buildQuicksandConfig())491->setVisible($is_visible)492->setMinimize($is_minimize)493->setInitialLoad(true);494if ($is_minimize) {495$this->classes[] = 'minimize-column';496}497}498499Javelin::initBehavior('quicksand-blacklist', array(500'patterns' => $this->getQuicksandURIPatternBlacklist(),501));502503return phutil_tag(504'div',505array(506'class' => implode(' ', $classes),507'id' => 'main-page-frame',508),509array(510$main_page,511$durable_column,512));513}514515private function renderPageBodyContent() {516$console = $this->getConsole();517518$body = parent::getBody();519520$nav = $this->getNavigation();521$tabs = $this->getTabs();522if ($nav) {523$crumbs = $this->getCrumbs();524if ($crumbs) {525$nav->setCrumbs($crumbs);526}527$nav->appendChild($body);528$nav->appendFooter($this->footer);529$content = phutil_implode_html('', array($nav->render()));530} else {531$content = array();532533$crumbs = $this->getCrumbs();534if ($crumbs) {535if ($this->getTabs()) {536$crumbs->setBorder(true);537}538$content[] = $crumbs;539}540541$tabs = $this->getTabs();542if ($tabs) {543$content[] = $tabs;544}545546$content[] = $body;547$content[] = $this->footer;548549$content = phutil_implode_html('', $content);550}551552return array(553($console ? hsprintf('<darkconsole />') : null),554$content,555);556}557558protected function getTail() {559$request = $this->getRequest();560$user = $request->getUser();561562$tail = array(563parent::getTail(),564);565566$response = CelerityAPI::getStaticResourceResponse();567568if ($request->isHTTPS()) {569$with_protocol = 'https';570} else {571$with_protocol = 'http';572}573574$servers = PhabricatorNotificationServerRef::getEnabledClientServers(575$with_protocol);576577if ($servers) {578if ($user && $user->isLoggedIn()) {579// TODO: We could tell the browser about all the servers and let it580// do random reconnects to improve reliability.581shuffle($servers);582$server = head($servers);583584$client_uri = $server->getWebsocketURI();585586Javelin::initBehavior(587'aphlict-listen',588array(589'websocketURI' => (string)$client_uri,590) + $this->buildAphlictListenConfigData());591592CelerityAPI::getStaticResourceResponse()593->addContentSecurityPolicyURI('connect-src', $client_uri);594}595}596597$tail[] = $response->renderHTMLFooter($this->getFrameable());598599return $tail;600}601602protected function getBodyClasses() {603$classes = array();604605if (!$this->getShowChrome()) {606$classes[] = 'phabricator-chromeless-page';607}608609$agent = AphrontRequest::getHTTPHeader('User-Agent');610611// Try to guess the device resolution based on UA strings to avoid a flash612// of incorrectly-styled content.613$device_guess = 'device-desktop';614if (preg_match('@iPhone|iPod|(Android.*Chrome/[.0-9]* Mobile)@', $agent)) {615$device_guess = 'device-phone device';616} else if (preg_match('@iPad|(Android.*Chrome/)@', $agent)) {617$device_guess = 'device-tablet device';618}619620$classes[] = $device_guess;621622if (preg_match('@Windows@', $agent)) {623$classes[] = 'platform-windows';624} else if (preg_match('@Macintosh@', $agent)) {625$classes[] = 'platform-mac';626} else if (preg_match('@X11@', $agent)) {627$classes[] = 'platform-linux';628}629630if ($this->getRequest()->getStr('__print__')) {631$classes[] = 'printable';632}633634if ($this->getRequest()->getStr('__aural__')) {635$classes[] = 'audible';636}637638$classes[] = 'phui-theme-'.PhabricatorEnv::getEnvConfig('ui.header-color');639foreach ($this->classes as $class) {640$classes[] = $class;641}642643return implode(' ', $classes);644}645646private function getConsole() {647if ($this->disableConsole) {648return null;649}650return $this->getRequest()->getApplicationConfiguration()->getConsole();651}652653private function getConsoleConfig() {654$user = $this->getRequest()->getUser();655656$headers = array();657if (DarkConsoleXHProfPluginAPI::isProfilerStarted()) {658$headers[DarkConsoleXHProfPluginAPI::getProfilerHeader()] = 'page';659}660if (DarkConsoleServicesPlugin::isQueryAnalyzerRequested()) {661$headers[DarkConsoleServicesPlugin::getQueryAnalyzerHeader()] = true;662}663664if ($user) {665$setting_tab = PhabricatorDarkConsoleTabSetting::SETTINGKEY;666$setting_visible = PhabricatorDarkConsoleVisibleSetting::SETTINGKEY;667$tab = $user->getUserSetting($setting_tab);668$visible = $user->getUserSetting($setting_visible);669} else {670$tab = null;671$visible = true;672}673674return array(675// NOTE: We use a generic label here to prevent input reflection676// and mitigate compression attacks like BREACH. See discussion in677// T3684.678'uri' => pht('Main Request'),679'selected' => $tab,680'visible' => $visible,681'headers' => $headers,682);683}684685private function getHighSecurityWarningConfig() {686$user = $this->getRequest()->getUser();687688$show = false;689if ($user->hasSession()) {690$hisec = ($user->getSession()->getHighSecurityUntil() - time());691if ($hisec > 0) {692$show = true;693}694}695696return array(697'show' => $show,698'uri' => '/auth/session/downgrade/',699'message' => pht(700'Your session is in high security mode. When you '.701'finish using it, click here to leave.'),702);703}704705private function renderFooter() {706if (!$this->getShowChrome()) {707return null;708}709710if (!$this->getShowFooter()) {711return null;712}713714$items = PhabricatorEnv::getEnvConfig('ui.footer-items');715if (!$items) {716return null;717}718719$foot = array();720foreach ($items as $item) {721$name = idx($item, 'name', pht('Unnamed Footer Item'));722723$href = idx($item, 'href');724if (!PhabricatorEnv::isValidURIForLink($href)) {725$href = null;726}727728if ($href !== null) {729$tag = 'a';730} else {731$tag = 'span';732}733734$foot[] = phutil_tag(735$tag,736array(737'href' => $href,738),739$name);740}741$foot = phutil_implode_html(" \xC2\xB7 ", $foot);742743return phutil_tag(744'div',745array(746'class' => 'phabricator-standard-page-footer grouped',747),748$foot);749}750751public function renderForQuicksand() {752parent::willRenderPage();753$response = $this->renderPageBodyContent();754$response = $this->willSendResponse($response);755756$extra_config = $this->getQuicksandConfig();757758return array(759'content' => hsprintf('%s', $response),760) + $this->buildQuicksandConfig()761+ $extra_config;762}763764private function buildQuicksandConfig() {765$viewer = $this->getRequest()->getUser();766$controller = $this->getController();767768$dropdown_query = id(new AphlictDropdownDataQuery())769->setViewer($viewer);770$dropdown_query->execute();771772$hisec_warning_config = $this->getHighSecurityWarningConfig();773774$console_config = null;775$console = $this->getConsole();776if ($console) {777$console_config = $this->getConsoleConfig();778}779780$upload_enabled = false;781if ($controller) {782$upload_enabled = $controller->isGlobalDragAndDropUploadEnabled();783}784785$application_class = null;786$application_search_icon = null;787$application_help = null;788$controller = $this->getController();789if ($controller) {790$application = $controller->getCurrentApplication();791if ($application) {792$application_class = get_class($application);793if ($application->getApplicationSearchDocumentTypes()) {794$application_search_icon = $application->getIcon();795}796797$help_items = $application->getHelpMenuItems($viewer);798if ($help_items) {799$help_list = id(new PhabricatorActionListView())800->setViewer($viewer);801foreach ($help_items as $help_item) {802$help_list->addAction($help_item);803}804$application_help = $help_list->getDropdownMenuMetadata();805}806}807}808809return array(810'title' => $this->getTitle(),811'bodyClasses' => $this->getBodyClasses(),812'aphlictDropdownData' => array(813$dropdown_query->getNotificationData(),814$dropdown_query->getConpherenceData(),815),816'globalDragAndDrop' => $upload_enabled,817'hisecWarningConfig' => $hisec_warning_config,818'consoleConfig' => $console_config,819'applicationClass' => $application_class,820'applicationSearchIcon' => $application_search_icon,821'helpItems' => $application_help,822) + $this->buildAphlictListenConfigData();823}824825private function buildAphlictListenConfigData() {826$user = $this->getRequest()->getUser();827$subscriptions = $this->pageObjects;828$subscriptions[] = $user->getPHID();829830return array(831'pageObjects' => array_fill_keys($this->pageObjects, true),832'subscriptions' => $subscriptions,833);834}835836private function getQuicksandURIPatternBlacklist() {837$applications = PhabricatorApplication::getAllApplications();838839$blacklist = array();840foreach ($applications as $application) {841$blacklist[] = $application->getQuicksandURIPatternBlacklist();842}843844// See T4340. Currently, Phortune and Auth both require pulling in external845// Javascript (for Stripe card management and Recaptcha, respectively).846// This can put us in a position where the user loads a page with a847// restrictive Content-Security-Policy, then uses Quicksand to navigate to848// a page which needs to load external scripts. For now, just blacklist849// these entire applications since we aren't giving up anything850// significant by doing so.851852$blacklist[] = array(853'/phortune/.*',854'/auth/.*',855);856857return array_mergev($blacklist);858}859860private function getUserPreference($key, $default = null) {861$request = $this->getRequest();862if (!$request) {863return $default;864}865866$user = $request->getUser();867if (!$user) {868return $default;869}870871return $user->getUserSetting($key);872}873874public function produceAphrontResponse() {875$controller = $this->getController();876877$viewer = $this->getUser();878if ($viewer && $viewer->getPHID()) {879$object_phids = $this->pageObjects;880foreach ($object_phids as $object_phid) {881PhabricatorFeedStoryNotification::updateObjectNotificationViews(882$viewer,883$object_phid);884}885}886887if ($this->getRequest()->isQuicksand()) {888$content = $this->renderForQuicksand();889$response = id(new AphrontAjaxResponse())890->setContent($content);891} else {892// See T13247. Try to find some navigational menu items to create a893// mobile navigation menu from.894$application_menu = $controller->buildApplicationMenu();895if (!$application_menu) {896$navigation = $this->getNavigation();897if ($navigation) {898$application_menu = $navigation->getMenu();899}900}901$this->applicationMenu = $application_menu;902903$content = $this->render();904905$response = id(new AphrontWebpageResponse())906->setContent($content)907->setFrameable($this->getFrameable());908}909910return $response;911}912913}914915916