diff --git a/src/applications/phame/view/PhamePostView.php b/src/applications/phame/view/PhamePostView.php index 7d6ac9297..4f3b59fad 100644 --- a/src/applications/phame/view/PhamePostView.php +++ b/src/applications/phame/view/PhamePostView.php @@ -1,244 +1,242 @@ skin = $skin; return $this; } public function getSkin() { return $this->skin; } public function setAuthor(PhabricatorObjectHandle $author) { $this->author = $author; return $this; } public function getAuthor() { return $this->author; } public function setPost(PhamePost $post) { $this->post = $post; return $this; } public function getPost() { return $this->post; } public function setBody($body) { $this->body = $body; return $this; } public function getBody() { return $this->body; } public function setSummary($summary) { $this->summary = $summary; return $this; } public function getSummary() { return $this->summary; } public function renderTitle() { $href = $this->getSkin()->getURI('post/'.$this->getPost()->getPhameTitle()); return phutil_tag( 'h2', array( 'class' => 'phame-post-title', ), phutil_tag( 'a', array( 'href' => $href, ), $this->getPost()->getTitle())); } public function renderDatePublished() { return phutil_tag( 'div', array( 'class' => 'phame-post-date', ), pht( 'Published on %s by %s', phabricator_datetime( $this->getPost()->getDatePublished(), $this->getUser()), $this->getAuthor()->getName())); } public function renderBody() { return phutil_tag( 'div', array( 'class' => 'phame-post-body', ), $this->getBody()); } public function renderSummary() { return phutil_tag( 'div', array( 'class' => 'phame-post-body', ), $this->getSummary()); } public function renderComments() { $post = $this->getPost(); switch ($post->getCommentsWidget()) { case 'facebook': $comments = $this->renderFacebookComments(); break; case 'disqus': $comments = $this->renderDisqusComments(); break; case 'none': default: $comments = null; break; } return $comments; } public function render() { return phutil_tag( 'div', array( 'class' => 'phame-post', ), array( $this->renderTitle(), $this->renderDatePublished(), $this->renderBody(), $this->renderComments(), )); } public function renderWithSummary() { return phutil_tag( 'div', array( 'class' => 'phame-post', ), array( $this->renderTitle(), $this->renderDatePublished(), $this->renderSummary(), )); } private function renderFacebookComments() { $fb_id = PhabricatorEnv::getEnvConfig('facebook.application-id'); if (!$fb_id) { return null; } $fb_root = phutil_tag('div', array( 'id' => 'fb-root', ), ''); $c_uri = '//connect.facebook.net/en_US/all.js#xfbml=1&appId='.$fb_id; - $fb_js = hsprintf( - '', + $fb_js = CelerityStaticResourceResponse::renderInlineScript( jsprintf( '(function(d, s, id) {'. ' var js, fjs = d.getElementsByTagName(s)[0];'. ' if (d.getElementById(id)) return;'. ' js = d.createElement(s); js.id = id;'. ' js.src = %s;'. ' fjs.parentNode.insertBefore(js, fjs);'. '}(document, \'script\', \'facebook-jssdk\'));', $c_uri)); $uri = $this->getSkin()->getURI('post/'.$this->getPost()->getPhameTitle()); $fb_comments = phutil_tag('div', array( 'class' => 'fb-comments', 'data-href' => $uri, 'data-num-posts' => 5, ), ''); return phutil_tag( 'div', array( 'class' => 'phame-comments-facebook', ), array( $fb_root, $fb_js, $fb_comments, )); } private function renderDisqusComments() { $disqus_shortname = PhabricatorEnv::getEnvConfig('disqus.shortname'); if (!$disqus_shortname) { return null; } $post = $this->getPost(); $disqus_thread = phutil_tag('div', array( 'id' => 'disqus_thread' )); // protip - try some var disqus_developer = 1; action to test locally - $disqus_js = hsprintf( - '', + $disqus_js = CelerityStaticResourceResponse::renderInlineScript( jsprintf( ' var disqus_shortname = "phabricator";'. ' var disqus_identifier = %s;'. ' var disqus_url = %s;'. ' var disqus_title = %s;'. '(function() {'. ' var dsq = document.createElement("script");'. ' dsq.type = "text/javascript";'. ' dsq.async = true;'. ' dsq.src = "http://" + disqus_shortname + ".disqus.com/embed.js";'. '(document.getElementsByTagName("head")[0] ||'. ' document.getElementsByTagName("body")[0]).appendChild(dsq);'. '})();', $post->getPHID(), $this->getSkin()->getURI('post/'.$this->getPost()->getPhameTitle()), $post->getTitle())); return phutil_tag( 'div', array( 'class' => 'phame-comments-disqus', ), array( $disqus_thread, $disqus_js, )); } } diff --git a/src/infrastructure/celerity/CelerityStaticResourceResponse.php b/src/infrastructure/celerity/CelerityStaticResourceResponse.php index 287f6644c..08e6da5a4 100644 --- a/src/infrastructure/celerity/CelerityStaticResourceResponse.php +++ b/src/infrastructure/celerity/CelerityStaticResourceResponse.php @@ -1,249 +1,259 @@ 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()) { $this->requireResource('javelin-behavior-'.$behavior); if (empty($this->behaviors[$behavior])) { $this->behaviors[$behavior] = array(); } if ($config) { $this->behaviors[$behavior][] = $config; } return $this; } public function requireResource($symbol) { $this->symbols[$symbol] = true; $this->needsResolve = true; return $this; } private function resolveResources() { if ($this->needsResolve) { $map = CelerityResourceMap::getInstance(); $this->resolved = $map->resolveResources(array_keys($this->symbols)); $this->packaged = $map->packageResources($this->resolved); $this->needsResolve = false; } return $this; } public function renderSingleResource($symbol) { $map = CelerityResourceMap::getInstance(); $resolved = $map->resolveResources(array($symbol)); $packaged = $map->packageResources($resolved); return $this->renderPackagedResources($packaged); } public function renderResourcesOfType($type) { $this->resolveResources(); $resources = array(); foreach ($this->packaged as $resource) { if ($resource['type'] == $type) { $resources[] = $resource; } } return $this->renderPackagedResources($resources); } private function renderPackagedResources(array $resources) { $output = array(); foreach ($resources as $resource) { if (isset($this->hasRendered[$resource['uri']])) { continue; } $this->hasRendered[$resource['uri']] = true; $output[] = $this->renderResource($resource); $output[] = "\n"; } return phutil_implode_html('', $output); } private function renderResource(array $resource) { $uri = $this->getURI($resource); switch ($resource['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("Unable to render resource."); } 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 hsprintf( - '', - phutil_safe_html($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.'); + } + return hsprintf( + // We don't use because it is ignored by HTML parsers. We + // would need to send the document with XHTML content type. + '', + 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 $resource) { $resources[] = $this->getURI($resource); } if ($resources) { $response['javelin_resources'] = $resources; } return $response; } private function getURI($resource) { $uri = $resource['uri']; // 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')) { $root = dirname(phutil_get_library_root('phabricator')).'/webroot'; if (isset($resource['disk'])) { $mtime = (int)filemtime($root.$resource['disk']); } else { $mtime = 0; foreach ($resource['symbols'] as $symbol) { $map = CelerityResourceMap::getInstance(); $symbol_info = $map->lookupSymbolInformation($symbol); $mtime = max($mtime, (int)filemtime($root.$symbol_info['disk'])); } } $uri = preg_replace('@^/res/@', '/res/'.$mtime.'T/', $uri); } return PhabricatorEnv::getCDNURI($uri); } } diff --git a/src/view/page/PhabricatorBarePageView.php b/src/view/page/PhabricatorBarePageView.php index a5c112bb2..2f0e23eb7 100644 --- a/src/view/page/PhabricatorBarePageView.php +++ b/src/view/page/PhabricatorBarePageView.php @@ -1,113 +1,114 @@ controller = $controller; return $this; } public function getController() { return $this->controller; } public function setRequest(AphrontRequest $request) { $this->request = $request; return $this; } public function getRequest() { return $this->request; } public function setFrameable($frameable) { $this->frameable = $frameable; return $this; } public function getFrameable() { return $this->frameable; } public function setDeviceReady($device_ready) { $this->deviceReady = $device_ready; return $this; } public function getDeviceReady() { return $this->deviceReady; } protected function willRenderPage() { // We render this now to resolve static resources so they can appear in the // document head. $this->bodyContent = phutil_implode_html('', $this->renderChildren()); } protected function getHead() { $framebust = null; if (!$this->getFrameable()) { $framebust = '(top == self) || top.location.replace(self.location.href);'; } $viewport_tag = null; if ($this->getDeviceReady()) { $viewport_tag = phutil_tag( 'meta', array( 'name' => 'viewport', 'content' => 'width=device-width, '. 'initial-scale=1, '. 'maximum-scale=1', )); } $icon_tag = phutil_tag( 'link', array( 'rel' => 'apple-touch-icon', 'href' => celerity_get_resource_uri('/rsrc/image/apple-touch-icon.png') )); $apple_tag = phutil_tag( 'meta', array( 'name' => 'apple-mobile-web-app-status-bar-style', 'content' => 'black-translucent' )); $response = CelerityAPI::getStaticResourceResponse(); + $developer = PhabricatorEnv::getEnvConfig('phabricator.developer-mode'); return hsprintf( - '%s%s%s%s', + '%s%s%s%s%s', $viewport_tag, $icon_tag, $apple_tag, - $framebust, - (PhabricatorEnv::getEnvConfig('phabricator.developer-mode') ? '1' : '0'), + CelerityStaticResourceResponse::renderInlineScript( + $framebust.jsprintf('window.__DEV__=%d;', ($developer ? 1 : 0))), $response->renderResourcesOfType('css')); } protected function getBody() { return $this->bodyContent; } protected function getTail() { $response = CelerityAPI::getStaticResourceResponse(); return $response->renderResourcesOfType('js'); } }