diff --git a/src/infrastructure/celerity/CelerityStaticResourceResponse.php b/src/infrastructure/celerity/CelerityStaticResourceResponse.php index 21ad591e87..feba9ffac1 100644 --- a/src/infrastructure/celerity/CelerityStaticResourceResponse.php +++ b/src/infrastructure/celerity/CelerityStaticResourceResponse.php @@ -1,304 +1,304 @@ metadataBlock = (int)$_REQUEST['__metablock__']; } } public function addMetadata($metadata) { $id = count($this->metadata); $this->metadata[$id] = $metadata; return $this->metadataBlock.'_'.$id; } public function getMetadataBlock() { return $this->metadataBlock; } /** * Register a behavior for initialization. NOTE: if $config is empty, * a behavior will execute only once even if it is initialized multiple times. * If $config is nonempty, the behavior will be invoked once for each config. */ public function initBehavior( $behavior, array $config = array(), $source_name = 'phabricator') { $this->requireResource('javelin-behavior-'.$behavior, $source_name); if (empty($this->behaviors[$behavior])) { $this->behaviors[$behavior] = array(); } if ($config) { $this->behaviors[$behavior][] = $config; } return $this; } public function requireResource($symbol, $source_name) { if (isset($this->symbols[$source_name][$symbol])) { return $this; } // Verify that the resource exists. $map = CelerityResourceMap::getNamedInstance($source_name); $name = $map->getResourceNameForSymbol($symbol); if ($name === null) { throw new Exception( pht( 'No resource with symbol "%s" exists in source "%s"!', $symbol, $source_name)); } $this->symbols[$source_name][$symbol] = true; $this->needsResolve = true; return $this; } private function resolveResources() { if ($this->needsResolve) { $this->packaged = array(); foreach ($this->symbols as $source_name => $symbols_map) { $symbols = array_keys($symbols_map); $map = CelerityResourceMap::getNamedInstance($source_name); $packaged = $map->getPackagedNamesForSymbols($symbols); $this->packaged[$source_name] = $packaged; } $this->needsResolve = false; } return $this; } public function renderSingleResource($symbol, $source_name) { $map = CelerityResourceMap::getNamedInstance($source_name); $packaged = $map->getPackagedNamesForSymbols(array($symbol)); return $this->renderPackagedResources($map, $packaged); } public function renderResourcesOfType($type) { $this->resolveResources(); $result = array(); foreach ($this->packaged as $source_name => $resource_names) { $map = CelerityResourceMap::getNamedInstance($source_name); $resources_of_type = array(); foreach ($resource_names as $resource_name) { $resource_type = $map->getResourceTypeForName($resource_name); if ($resource_type == $type) { $resources_of_type[] = $resource_name; } } $result[] = $this->renderPackagedResources($map, $resources_of_type); } return phutil_implode_html('', $result); } private function renderPackagedResources( CelerityResourceMap $map, array $resources) { $output = array(); foreach ($resources as $name) { if (isset($this->hasRendered[$name])) { continue; } $this->hasRendered[$name] = true; $output[] = $this->renderResource($map, $name); } return $output; } private function renderResource( CelerityResourceMap $map, $name) { $uri = $this->getURI($map, $name); $type = $map->getResourceTypeForName($name); switch ($type) { case 'css': return phutil_tag( 'link', array( 'rel' => 'stylesheet', 'type' => 'text/css', 'href' => $uri, )); case 'js': return phutil_tag( 'script', array( 'type' => 'text/javascript', 'src' => $uri, ), ''); } throw new Exception( pht( 'Unable to render resource "%s", which has unknown type "%s".', $name, $type)); } public function renderHTMLFooter() { $data = array(); if ($this->metadata) { $json_metadata = AphrontResponse::encodeJSONForHTTPResponse( $this->metadata); $this->metadata = array(); } else { $json_metadata = '{}'; } // Even if there is no metadata on the page, Javelin uses the mergeData() // call to start dispatching the event queue. $data[] = 'JX.Stratcom.mergeData('.$this->metadataBlock.', '. $json_metadata.');'; $onload = array(); if ($this->behaviors) { $behaviors = $this->behaviors; $this->behaviors = array(); $higher_priority_names = array( 'refresh-csrf', 'aphront-basic-tokenizer', 'dark-console', 'history-install', ); $higher_priority_behaviors = array_select_keys( $behaviors, $higher_priority_names); foreach ($higher_priority_names as $name) { unset($behaviors[$name]); } $behavior_groups = array( $higher_priority_behaviors, $behaviors); foreach ($behavior_groups as $group) { if (!$group) { continue; } $group_json = AphrontResponse::encodeJSONForHTTPResponse( $group); $onload[] = 'JX.initBehaviors('.$group_json.')'; } } if ($onload) { foreach ($onload as $func) { $data[] = 'JX.onload(function(){'.$func.'});'; } } if ($data) { $data = implode("\n", $data); return self::renderInlineScript($data); } else { return ''; } } public static function renderInlineScript($data) { if (stripos($data, '') !== false) { throw new Exception( 'Literal is not allowed inside inline script.'); } if (strpos($data, ' because it is ignored by HTML parsers. We // would need to send the document with XHTML content type. return phutil_tag( 'script', array('type' => 'text/javascript'), phutil_safe_html($data)); } public function buildAjaxResponse($payload, $error = null) { $response = array( 'error' => $error, 'payload' => $payload, ); if ($this->metadata) { $response['javelin_metadata'] = $this->metadata; $this->metadata = array(); } if ($this->behaviors) { $response['javelin_behaviors'] = $this->behaviors; $this->behaviors = array(); } $this->resolveResources(); $resources = array(); foreach ($this->packaged as $source_name => $resource_names) { $map = CelerityResourceMap::getNamedInstance($source_name); foreach ($resource_names as $resource_name) { $resources[] = $this->getURI($map, $resource_name); } } if ($resources) { $response['javelin_resources'] = $resources; } return $response; } - private function getURI( + public function getURI( CelerityResourceMap $map, $name) { $uri = $map->getURIForName($name); // In developer mode, we dump file modification times into the URI. When a // page is reloaded in the browser, any resources brought in by Ajax calls // do not trigger revalidation, so without this it's very difficult to get // changes to Ajaxed-in CSS to work (you must clear your cache or rerun // the map script). In production, we can assume the map script gets run // after changes, and safely skip this. if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) { $mtime = $map->getModifiedTimeForName($name); $uri = preg_replace('@^/res/@', '/res/'.$mtime.'T/', $uri); } return PhabricatorEnv::getCDNURI($uri); } } diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php index 58f96579d0..093287eb6a 100644 --- a/src/view/page/PhabricatorStandardPageView.php +++ b/src/view/page/PhabricatorStandardPageView.php @@ -1,448 +1,453 @@ applicationMenu = $application_menu; return $this; } public function getApplicationMenu() { return $this->applicationMenu; } public function setApplicationName($application_name) { $this->applicationName = $application_name; return $this; } public function setDisableConsole($disable) { $this->disableConsole = $disable; return $this; } public function getApplicationName() { return $this->applicationName; } public function setBaseURI($base_uri) { $this->baseURI = $base_uri; return $this; } public function getBaseURI() { return $this->baseURI; } public function setShowChrome($show_chrome) { $this->showChrome = $show_chrome; return $this; } public function getShowChrome() { return $this->showChrome; } public function appendPageObjects(array $objs) { foreach ($objs as $obj) { $this->pageObjects[] = $obj; } } public function getTitle() { $use_glyph = true; $request = $this->getRequest(); if ($request) { $user = $request->getUser(); if ($user && $user->loadPreferences()->getPreference( PhabricatorUserPreferences::PREFERENCE_TITLES) !== 'glyph') { $use_glyph = false; } } $title = parent::getTitle(); $prefix = null; if ($use_glyph) { $prefix = $this->getGlyph(); } else { $application_name = $this->getApplicationName(); if (strlen($application_name)) { $prefix = '['.$application_name.']'; } } if (strlen($prefix)) { $title = $prefix.' '.$title; } return $title; } protected function willRenderPage() { parent::willRenderPage(); if (!$this->getRequest()) { throw new Exception( pht( "You must set the Request to render a PhabricatorStandardPageView.")); } $console = $this->getConsole(); require_celerity_resource('phabricator-core-css'); require_celerity_resource('phabricator-zindex-css'); require_celerity_resource('phui-button-css'); require_celerity_resource('phui-spacing-css'); require_celerity_resource('phui-form-css'); require_celerity_resource('sprite-gradient-css'); require_celerity_resource('phabricator-standard-page-view'); Javelin::initBehavior('workflow', array()); $request = $this->getRequest(); $user = null; if ($request) { $user = $request->getUser(); } if ($user) { $default_img_uri = PhabricatorEnv::getCDNURI( '/rsrc/image/icon/fatcow/document_black.png'); $download_form = phabricator_form( $user, array( 'action' => '#', 'method' => 'POST', 'class' => 'lightbox-download-form', 'sigil' => 'download', ), phutil_tag( 'button', array(), pht('Download'))); Javelin::initBehavior( 'lightbox-attachments', array( 'defaultImageUri' => $default_img_uri, 'downloadForm' => $download_form, )); } Javelin::initBehavior('aphront-form-disable-on-submit'); Javelin::initBehavior('toggle-class', array()); Javelin::initBehavior('konami', array()); Javelin::initBehavior('history-install'); Javelin::initBehavior('phabricator-gesture'); $current_token = null; if ($user) { $current_token = $user->getCSRFToken(); } Javelin::initBehavior( 'refresh-csrf', array( 'tokenName' => AphrontRequest::getCSRFTokenName(), 'header' => AphrontRequest::getCSRFHeaderName(), 'current' => $current_token, )); Javelin::initBehavior('device'); if ($user->hasSession()) { $hisec = ($user->getSession()->getHighSecurityUntil() - time()); if ($hisec > 0) { $remaining_time = phabricator_format_relative_time($hisec); Javelin::initBehavior( 'high-security-warning', array( 'uri' => '/auth/session/downgrade/', 'message' => pht( 'Your session is in high security mode. When you '. 'finish using it, click here to leave.', $remaining_time), )); } } if ($console) { require_celerity_resource('aphront-dark-console-css'); $headers = array(); if (DarkConsoleXHProfPluginAPI::isProfilerStarted()) { $headers[DarkConsoleXHProfPluginAPI::getProfilerHeader()] = 'page'; } if (DarkConsoleServicesPlugin::isQueryAnalyzerRequested()) { $headers[DarkConsoleServicesPlugin::getQueryAnalyzerHeader()] = true; } Javelin::initBehavior( 'dark-console', array( // NOTE: We use a generic label here to prevent input reflection // and mitigate compression attacks like BREACH. See discussion in // T3684. 'uri' => pht('Main Request'), 'selected' => $user ? $user->getConsoleTab() : null, 'visible' => $user ? (int)$user->getConsoleVisible() : true, 'headers' => $headers, )); // Change this to initBehavior when there is some behavior to initialize require_celerity_resource('javelin-behavior-error-log'); } if ($user) { $viewer = $user; } else { $viewer = new PhabricatorUser(); } $menu = id(new PhabricatorMainMenuView()) ->setUser($viewer); if ($this->getController()) { $menu->setController($this->getController()); } if ($this->getApplicationMenu()) { $menu->setApplicationMenu($this->getApplicationMenu()); } $this->menuContent = $menu->render(); } protected function getHead() { $monospaced = PhabricatorEnv::getEnvConfig('style.monospace'); $monospaced_win = PhabricatorEnv::getEnvConfig('style.monospace.windows'); $request = $this->getRequest(); if ($request) { $user = $request->getUser(); if ($user) { $pref = $user->loadPreferences()->getPreference( PhabricatorUserPreferences::PREFERENCE_MONOSPACED); $monospaced = nonempty($pref, $monospaced); $monospaced_win = nonempty($pref, $monospaced_win); } } $response = CelerityAPI::getStaticResourceResponse(); return hsprintf( '%s%s', parent::getHead(), phutil_safe_html($monospaced), phutil_safe_html($monospaced_win), $response->renderSingleResource('javelin-magical-init', 'phabricator')); } public function setGlyph($glyph) { $this->glyph = $glyph; return $this; } public function getGlyph() { return $this->glyph; } protected function willSendResponse($response) { $request = $this->getRequest(); $response = parent::willSendResponse($response); $console = $request->getApplicationConfiguration()->getConsole(); if ($console) { $response = PhutilSafeHTML::applyFunction( 'str_replace', hsprintf(''), $console->render($request), $response); } return $response; } protected function getBody() { $console = $this->getConsole(); $user = null; $request = $this->getRequest(); if ($request) { $user = $request->getUser(); } $header_chrome = null; if ($this->getShowChrome()) { $header_chrome = $this->menuContent; } $developer_warning = null; if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode') && DarkConsoleErrorLogPluginAPI::getErrors()) { $developer_warning = phutil_tag_div( 'aphront-developer-error-callout', pht( 'This page raised PHP errors. Find them in DarkConsole '. 'or the error log.')); } // Render the "you have unresolved setup issues..." warning. $setup_warning = null; if ($user && $user->getIsAdmin()) { $open = PhabricatorSetupCheck::getOpenSetupIssueCount(); if ($open) { $setup_warning = phutil_tag_div( 'setup-warning-callout', phutil_tag( 'a', array( 'href' => '/config/issue/', ), pht('You have %d unresolved setup issue(s)...', $open))); } } return phutil_tag( 'div', array( 'id' => 'base-page', 'class' => 'phabricator-standard-page', ), array( $developer_warning, $setup_warning, $header_chrome, phutil_tag_div('phabricator-standard-page-body', array( ($console ? hsprintf('') : null), parent::getBody(), phutil_tag('div', array('style' => 'clear: both;')), )), )); } protected function getTail() { $request = $this->getRequest(); $user = $request->getUser(); $tail = array( parent::getTail(), ); + $response = CelerityAPI::getStaticResourceResponse(); + if (PhabricatorEnv::getEnvConfig('notification.enabled')) { if ($user && $user->isLoggedIn()) { $aphlict_object_id = celerity_generate_unique_node_id(); $aphlict_container_id = celerity_generate_unique_node_id(); $client_uri = PhabricatorEnv::getEnvConfig('notification.client-uri'); $client_uri = new PhutilURI($client_uri); if ($client_uri->getDomain() == 'localhost') { $this_host = $this->getRequest()->getHost(); $this_host = new PhutilURI('http://'.$this_host.'/'); $client_uri->setDomain($this_host->getDomain()); } + $map = CelerityResourceMap::getNamedInstance('phabricator'); + $swf_uri = $response->getURI($map, 'rsrc/swf/aphlict.swf'); + $enable_debug = PhabricatorEnv::getEnvConfig('notification.debug'); Javelin::initBehavior( 'aphlict-listen', array( 'id' => $aphlict_object_id, 'containerID' => $aphlict_container_id, 'server' => $client_uri->getDomain(), 'port' => $client_uri->getPort(), 'debug' => $enable_debug, + 'swfURI' => $swf_uri, 'pageObjects' => array_fill_keys($this->pageObjects, true), )); $tail[] = phutil_tag( 'div', array( 'id' => $aphlict_container_id, 'style' => 'position: absolute; width: 0; height: 0; overflow: hidden;', ), ''); } } - $response = CelerityAPI::getStaticResourceResponse(); $tail[] = $response->renderHTMLFooter(); return $tail; } protected function getBodyClasses() { $classes = array(); if (!$this->getShowChrome()) { $classes[] = 'phabricator-chromeless-page'; } $agent = AphrontRequest::getHTTPHeader('User-Agent'); // Try to guess the device resolution based on UA strings to avoid a flash // of incorrectly-styled content. $device_guess = 'device-desktop'; if (preg_match('@iPhone|iPod|(Android.*Chrome/[.0-9]* Mobile)@', $agent)) { $device_guess = 'device-phone device'; } else if (preg_match('@iPad|(Android.*Chrome/)@', $agent)) { $device_guess = 'device-tablet device'; } $classes[] = $device_guess; if (preg_match('@Windows@', $agent)) { $classes[] = 'platform-windows'; } else if (preg_match('@Macintosh@', $agent)) { $classes[] = 'platform-mac'; } else if (preg_match('@X11@', $agent)) { $classes[] = 'platform-linux'; } if ($this->getRequest()->getStr('__print__')) { $classes[] = 'printable'; } if ($this->getRequest()->getStr('__aural__')) { $classes[] = 'audible'; } return implode(' ', $classes); } private function getConsole() { if ($this->disableConsole) { return null; } return $this->getRequest()->getApplicationConfiguration()->getConsole(); } } diff --git a/webroot/rsrc/js/application/aphlict/behavior-aphlict-listen.js b/webroot/rsrc/js/application/aphlict/behavior-aphlict-listen.js index 284534b262..84c221b6ac 100644 --- a/webroot/rsrc/js/application/aphlict/behavior-aphlict-listen.js +++ b/webroot/rsrc/js/application/aphlict/behavior-aphlict-listen.js @@ -1,108 +1,108 @@ /** * @provides javelin-behavior-aphlict-listen * @requires javelin-behavior * javelin-aphlict * javelin-stratcom * javelin-request * javelin-uri * javelin-dom * javelin-json * javelin-router * phabricator-notification */ JX.behavior('aphlict-listen', function(config) { var showing_reload = false; function onready() { var client = new JX.Aphlict(config.id, config.server, config.port) .setHandler(onaphlictmessage) .start(); } // Respond to a notification from the Aphlict notification server. We send // a request to Phabricator to get notification details. function onaphlictmessage(type, message) { switch (type) { case 'error': new JX.Notification() .setContent('(Aphlict) ' + message) .alterClassName('jx-notification-error', true) .show(); break; case 'receive': var request = new JX.Request( '/notification/individual/', onnotification); var routable = request .addData({key: message.key}) .getRoutable(); routable .setType('notification') .setPriority(250); JX.Router.getInstance().queue(routable); break; default: if (__DEV__ && config.debug) { var details = message ? JX.JSON.stringify(message) : ''; new JX.Notification() .setContent('(Aphlict) [' + type + '] ' + details) .alterClassName('jx-notification-debug', true) .setDuration(3000) .show(); } } } // Respond to a response from Phabricator about a specific notification. function onnotification(response) { if (!response.pertinent) { return; } JX.Stratcom.invoke('notification-panel-update', null, {}); // Show the notification itself. new JX.Notification() .setContent(JX.$H(response.content)) .show(); // If the notification affected an object on this page, show a // permanent reload notification if we aren't already. if ((response.primaryObjectPHID in config.pageObjects) && !showing_reload) { var reload = new JX.Notification() .setContent('Page updated, click to reload.') .alterClassName('jx-notification-alert', true) .setDuration(0); reload.listen('activate', function(e) { JX.$U().go(); }); reload.show(); showing_reload = true; } } // Wait for the element to load, and don't do anything if it never loads. // If we just go crazy and start making calls to it before it loads, its // interfaces won't be registered yet. JX.Stratcom.listen('aphlict-component-ready', null, onready); // Add Flash object to page JX.$(config.containerID).innerHTML = '' + - '' + + '' + '' + '' + - '' + ''; //Evan sanctioned });