diff --git a/src/applications/phame/skins/PhameBasicTemplateBlogSkin.php b/src/applications/phame/skins/PhameBasicTemplateBlogSkin.php index 25d0b66449..8bee67fe8f 100644 --- a/src/applications/phame/skins/PhameBasicTemplateBlogSkin.php +++ b/src/applications/phame/skins/PhameBasicTemplateBlogSkin.php @@ -1,139 +1,139 @@ cssResources = array(); $css = $this->getPath('css/'); if (Filesystem::pathExists($css)) { foreach (Filesystem::listDirectory($css) as $path) { if (!preg_match('/.css$/', $path)) { continue; } $this->cssResources[] = phutil_tag( 'link', array( 'rel' => 'stylesheet', 'type' => 'text/css', 'href' => $this->getResourceURI('css/'.$path), )); } } $map = CelerityResourceMap::getInstance(); $resource_symbol = 'syntax-highlighting-css'; - $resource_uri = $map->getFullyQualifiedURIForSymbol($resource_symbol); + $resource_uri = $map->getURIForSymbol($resource_symbol); $this->cssResources[] = phutil_tag( 'link', array( 'rel' => 'stylesheet', 'type' => 'text/css', - 'href' => $resource_uri, + 'href' => PhabricatorEnv::getCDNURI($resource_uri), )); $this->cssResources = phutil_implode_html("\n", $this->cssResources); $request = $this->getRequest(); $content = $this->renderContent($request); if (!$content) { $content = $this->render404Page(); } $content = array( $this->renderHeader(), $content, $this->renderFooter(), ); $response = new AphrontWebpageResponse(); $response->setContent(phutil_implode_html("\n", $content)); return $response; } public function getCSSResources() { return $this->cssResources; } public function getName() { return $this->getSpecification()->getName(); } public function getPath($to_file = null) { $path = $this->getSpecification()->getRootDirectory(); if ($to_file) { $path = $path.DIRECTORY_SEPARATOR.$to_file; } return $path; } private function renderTemplate($__template__, array $__scope__) { chdir($this->getPath()); ob_start(); if (Filesystem::pathExists($this->getPath($__template__))) { // Fool lint. $__evil__ = 'extract'; $__evil__($__scope__ + $this->getDefaultScope()); require $this->getPath($__template__); } return phutil_safe_html(ob_get_clean()); } private function getDefaultScope() { return array( 'skin' => $this, 'blog' => $this->getBlog(), 'uri' => $this->getURI($this->getURIPath()), 'home_uri' => $this->getURI(''), 'title' => $this->getTitle(), 'description' => $this->getDescription(), 'og_type' => $this->getOGType(), ); } protected function renderHeader() { return $this->renderTemplate( 'header.php', array()); } protected function renderFooter() { return $this->renderTemplate('footer.php', array()); } protected function render404Page() { return $this->renderTemplate('404.php', array()); } protected function renderPostDetail(PhamePostView $post) { return $this->renderTemplate( 'post-detail.php', array( 'post' => $post, )); } protected function renderPostList(array $posts) { return $this->renderTemplate( 'post-list.php', array( 'posts' => $posts, 'older' => $this->renderOlderPageLink(), 'newer' => $this->renderNewerPageLink(), )); } } diff --git a/src/infrastructure/celerity/CelerityResourceMap.php b/src/infrastructure/celerity/CelerityResourceMap.php index 3e52e58de6..623fca2bdd 100644 --- a/src/infrastructure/celerity/CelerityResourceMap.php +++ b/src/infrastructure/celerity/CelerityResourceMap.php @@ -1,210 +1,255 @@ resourceMap = $resource_map; return $this; } - public function resolveResources(array $symbols) { + public function getPackagedNamesForSymbols(array $symbols) { + $resolved = $this->resolveResources($symbols); + return $this->packageResources($resolved); + } + + private function resolveResources(array $symbols) { $map = array(); foreach ($symbols as $symbol) { if (!empty($map[$symbol])) { continue; } $this->resolveResource($map, $symbol); } return $map; } private function resolveResource(array &$map, $symbol) { if (empty($this->resourceMap[$symbol])) { throw new Exception( "Attempting to resolve unknown Celerity resource, '{$symbol}'."); } $info = $this->resourceMap[$symbol]; foreach ($info['requires'] as $requires) { if (!empty($map[$requires])) { continue; } $this->resolveResource($map, $requires); } $map[$symbol] = $info; } public function setPackageMap($package_map) { $this->packageMap = $package_map; return $this; } - public function packageResources(array $resolved_map) { + private function packageResources(array $resolved_map) { $packaged = array(); $handled = array(); foreach ($resolved_map as $symbol => $info) { if (isset($handled[$symbol])) { continue; } if (empty($this->packageMap['reverse'][$symbol])) { $packaged[$symbol] = $info; } else { $package = $this->packageMap['reverse'][$symbol]; $package_info = $this->packageMap['packages'][$package]; $packaged[$package_info['name']] = $package_info; foreach ($package_info['symbols'] as $packaged_symbol) { $handled[$packaged_symbol] = true; } } } - return $packaged; + + $names = array(); + foreach ($packaged as $key => $resource) { + if (isset($resource['disk'])) { + $names[] = $resource['disk']; + } else { + $names[] = $key; + } + } + + return $names; } public function getResourceDataForName($resource_name) { $root = phutil_get_library_root('phabricator'); $root = dirname($root).'/webroot/'; return Filesystem::readFile($root.$resource_name); } public function getResourceNamesForPackageHash($package_hash) { $package = idx($this->packageMap['packages'], $package_hash); if (!$package) { return null; } $paths = array(); foreach ($package['symbols'] as $symbol) { $paths[] = $this->resourceMap[$symbol]['disk']; } return $paths; } private function lookupSymbolInformation($symbol) { return idx($this->resourceMap, $symbol); } private function lookupFileInformation($path) { if (empty($this->reverseMap)) { $this->reverseMap = array(); foreach ($this->resourceMap as $symbol => $data) { $data['provides'] = $symbol; $this->reverseMap[$data['disk']] = $data; } } return idx($this->reverseMap, $path); } /** * Get the epoch timestamp of the last modification time of a symbol. * * @param string Resource symbol to lookup. * @return int Epoch timestamp of last resource modification. */ - public function getModifiedTimeForSymbol($symbol) { - $info = $this->lookupSymbolInformation($symbol); - if ($info) { - $root = dirname(phutil_get_library_root('phabricator')).'/webroot'; - return (int)filemtime($root.$info['disk']); + public function getModifiedTimeForName($name) { + $package_hash = null; + foreach ($this->packageMap['packages'] as $hash => $package) { + if ($package['name'] == $name) { + $package_hash = $hash; + break; + } + } + + $root = dirname(phutil_get_library_root('phabricator')).'/webroot'; + + $mtime = 0; + + if ($package_hash) { + $names = $this->getResourceNamesForPackageHash($package_hash); + foreach ($names as $component_name) { + $info = $this->lookupFileInformation($component_name); + if ($info) { + $mtime = max($mtime, (int)filemtime($root.$info['disk'])); + } + } + } else { + $info = $this->lookupFileInformation($name); + if ($info) { + $root = dirname(phutil_get_library_root('phabricator')).'/webroot'; + $mtime = (int)filemtime($root.$info['disk']); + } } - return 0; + + return $mtime; } /** - * Return the fully-qualified, absolute URI for the resource associated with - * a symbol. This method is fairly low-level and ignores packaging. + * Return the absolute URI for the resource associated with a symbol. This + * method is fairly low-level and ignores packaging. * * @param string Resource symbol to lookup. * @return string|null Fully-qualified resource URI, or null if the symbol * is unknown. */ - public function getFullyQualifiedURIForSymbol($symbol) { + public function getURIForSymbol($symbol) { $info = $this->lookupSymbolInformation($symbol); if ($info) { return idx($info, 'uri'); } return null; } /** - * Return the fully-qualified, absolute URI for the resource associated with - * a resource name. This method is fairly low-level and ignores packaging. + * Return the absolute URI for the resource associated with a resource name. + * This method is fairly low-level and ignores packaging. * * @param string Resource name to lookup. * @return string|null Fully-qualified resource URI, or null if the name * is unknown. */ - public function getFullyQualifiedURIForName($name) { + public function getURIForName($name) { $info = $this->lookupFileInformation($name); if ($info) { return idx($info, 'uri'); } + + foreach ($this->packageMap['packages'] as $hash => $package) { + if ($package['name'] == $name) { + return $package['uri']; + } + } + return null; } /** * Return the resource symbols required by a named resource. * * @param string Resource name to lookup. * @return list|null List of required symbols, or null if the name * is unknown. */ public function getRequiredSymbolsForName($name) { $info = $this->lookupFileInformation($name); if ($info) { return idx($info, 'requires', array()); } return null; } /** * Return the resource name for a given symbol. * * @param string Resource symbol to lookup. * @return string|null Resource name, or null if the symbol is unknown. */ public function getResourceNameForSymbol($symbol) { $info = $this->lookupSymbolInformation($symbol); if ($info) { return idx($info, 'disk'); } return null; } } diff --git a/src/infrastructure/celerity/CelerityResourceTransformer.php b/src/infrastructure/celerity/CelerityResourceTransformer.php index d6b74926c7..846d8b5e89 100644 --- a/src/infrastructure/celerity/CelerityResourceTransformer.php +++ b/src/infrastructure/celerity/CelerityResourceTransformer.php @@ -1,226 +1,226 @@ translateURICallback = $translate_uricallback; return $this; } public function setMinify($minify) { $this->minify = $minify; return $this; } public function setRawResourceMap(array $raw_resource_map) { $this->rawResourceMap = $raw_resource_map; return $this; } public function setCelerityMap(CelerityResourceMap $celerity_map) { $this->celerityMap = $celerity_map; return $this; } public function setRawURIMap(array $raw_urimap) { $this->rawURIMap = $raw_urimap; return $this; } public function getRawURIMap() { return $this->rawURIMap; } /** * @phutil-external-symbol function jsShrink */ public function transformResource($path, $data) { $type = self::getResourceType($path); switch ($type) { case 'css': $data = $this->replaceCSSPrintRules($path, $data); $data = $this->replaceCSSVariables($path, $data); $data = preg_replace_callback( '@url\s*\((\s*[\'"]?.*?)\)@s', nonempty( $this->translateURICallback, array($this, 'translateResourceURI')), $data); break; } if (!$this->minify) { return $data; } // Some resources won't survive minification (like Raphael.js), and are // marked so as not to be minified. if (strpos($data, '@'.'do-not-minify') !== false) { return $data; } switch ($type) { case 'css': // Remove comments. $data = preg_replace('@/\*.*?\*/@s', '', $data); // Remove whitespace around symbols. $data = preg_replace('@\s*([{}:;,])\s*@', '\1', $data); // Remove unnecessary semicolons. $data = preg_replace('@;}@', '}', $data); // Replace #rrggbb with #rgb when possible. $data = preg_replace( '@#([a-f0-9])\1([a-f0-9])\2([a-f0-9])\3@i', '#\1\2\3', $data); $data = trim($data); break; case 'js': // If `jsxmin` is available, use it. jsxmin is the Javelin minifier and // produces the smallest output, but is complicated to build. if (Filesystem::binaryExists('jsxmin')) { $future = new ExecFuture('jsxmin __DEV__:0'); $future->write($data); list($err, $result) = $future->resolve(); if (!$err) { $data = $result; break; } } // If `jsxmin` is not available, use `JsShrink`, which doesn't compress // quite as well but is always available. $root = dirname(phutil_get_library_root('phabricator')); require_once $root.'/externals/JsShrink/jsShrink.php'; $data = jsShrink($data); break; } return $data; } public static function getResourceType($path) { return last(explode('.', $path)); } public function translateResourceURI(array $matches) { $uri = trim($matches[1], "'\" \r\t\n"); if ($this->rawURIMap !== null) { if (isset($this->rawURIMap[$uri])) { $uri = $this->rawURIMap[$uri]; } } else if ($this->rawResourceMap) { if (isset($this->rawResourceMap[$uri]['uri'])) { $uri = $this->rawResourceMap[$uri]['uri']; } } else if ($this->celerityMap) { - $resource_uri = $this->celerityMap->getFullyQualifiedURIForName($uri); + $resource_uri = $this->celerityMap->getURIForName($uri); if ($resource_uri) { $uri = $resource_uri; } } return 'url('.$uri.')'; } private function replaceCSSVariables($path, $data) { $this->currentPath = $path; return preg_replace_callback( '/{\$([^}]+)}/', array($this, 'replaceCSSVariable'), $data); } private function replaceCSSPrintRules($path, $data) { $this->currentPath = $path; return preg_replace_callback( '/!print\s+(.+?{.+?})/s', array($this, 'replaceCSSPrintRule'), $data); } public static function getCSSVariableMap() { return array( // Base Colors 'red' => '#c0392b', 'lightred' => '#f4dddb', 'orange' => '#e67e22', 'lightorange' => '#f7e2d4', 'yellow' => '#f1c40f', 'lightyellow' => '#fdf5d4', 'green' => '#139543', 'lightgreen' => '#d7eddf', 'blue' => '#2980b9', 'lightblue' => '#daeaf3', 'sky' => '#3498db', 'lightsky' => '#ddeef9', 'indigo' => '#c6539d', 'lightindigo' => '#f5e2ef', 'violet' => '#8e44ad', 'lightviolet' => '#ecdff1', 'charcoal' => '#4b4d51', 'backdrop' => '#c4cde0', // Base Greys 'lightgreyborder' => '#C7CCD9', 'greyborder' => '#A1A6B0', 'darkgreyborder' => '#676A70', 'lightgreytext' => '#92969D', 'greytext' => '#74777D', 'darkgreytext' => '#4B4D51', 'lightgreybackground' => '#F7F7F7', 'greybackground' => '#EBECEE', // Base Blues 'thinblueborder' => '#DDE8EF', 'lightblueborder' => '#BFCFDA', 'blueborder' => '#8C98B8', 'darkblueborder' => '#626E82', 'lightbluebackground' => '#F8F9FC', 'bluebackground' => '#DAE7FF', 'lightbluetext' => '#8C98B8', 'bluetext' => '#6B748C', 'darkbluetext' => '#464C5C', ); } public function replaceCSSVariable($matches) { static $map; if (!$map) { $map = self::getCSSVariableMap(); } $var_name = $matches[1]; if (empty($map[$var_name])) { $path = $this->currentPath; throw new Exception( "CSS file '{$path}' has unknown variable '{$var_name}'."); } return $map[$var_name]; } public function replaceCSSPrintRule($matches) { $rule = $matches[1]; $rules = array(); $rules[] = '.printable '.$rule; $rules[] = "@media print {\n ".str_replace("\n", "\n ", $rule)."\n}\n"; return implode("\n\n", $rules); } } diff --git a/src/infrastructure/celerity/CelerityStaticResourceResponse.php b/src/infrastructure/celerity/CelerityStaticResourceResponse.php index aee24e4790..ee55074ac4 100644 --- a/src/infrastructure/celerity/CelerityStaticResourceResponse.php +++ b/src/infrastructure/celerity/CelerityStaticResourceResponse.php @@ -1,262 +1,256 @@ 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); + + $symbols = array_keys($this->symbols); + $this->packaged = $map->getPackagedNamesForSymbols($symbols); + $this->needsResolve = false; } return $this; } public function renderSingleResource($symbol) { $map = CelerityResourceMap::getInstance(); - $resolved = $map->resolveResources(array($symbol)); - $packaged = $map->packageResources($resolved); + $packaged = $map->getPackagedNamesForSymbols(array($symbol)); return $this->renderPackagedResources($packaged); } public function renderResourcesOfType($type) { $this->resolveResources(); $resources = array(); - foreach ($this->packaged as $resource) { - if ($resource['type'] == $type) { - $resources[] = $resource; + foreach ($this->packaged as $name) { + $resource_type = CelerityResourceTransformer::getResourceType($name); + if ($resource_type == $type) { + $resources[] = $name; } } return $this->renderPackagedResources($resources); } private function renderPackagedResources(array $resources) { $output = array(); - foreach ($resources as $resource) { - if (isset($this->hasRendered[$resource['uri']])) { + foreach ($resources as $name) { + if (isset($this->hasRendered[$name])) { continue; } - $this->hasRendered[$resource['uri']] = true; + $this->hasRendered[$name] = true; - $output[] = $this->renderResource($resource); + $output[] = $this->renderResource($name); $output[] = "\n"; } return phutil_implode_html('', $output); } - private function renderResource(array $resource) { - $uri = $this->getURI($resource); - switch ($resource['type']) { + private function renderResource($name) { + $uri = $this->getURI($name); + $type = CelerityResourceTransformer::getResourceType($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("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 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 $resource) { $resources[] = $this->getURI($resource); } if ($resources) { $response['javelin_resources'] = $resources; } return $response; } - private function getURI($resource) { - $uri = $resource['uri']; + private function getURI($name) { + $map = CelerityResourceMap::getInstance(); + $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')) { - $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(); - $mtime = max($mtime, $map->getModifiedTimeForSymbol($symbol)); - } - } - + $mtime = $map->getModifiedTimeForName($name); $uri = preg_replace('@^/res/@', '/res/'.$mtime.'T/', $uri); } return PhabricatorEnv::getCDNURI($uri); } } diff --git a/src/infrastructure/celerity/api.php b/src/infrastructure/celerity/api.php index d12f3c3f16..1f5346df90 100644 --- a/src/infrastructure/celerity/api.php +++ b/src/infrastructure/celerity/api.php @@ -1,61 +1,61 @@ requireResource($symbol); } /** * Generate a node ID which is guaranteed to be unique for the current page, * even across Ajax requests. You should use this method to generate IDs for * nodes which require a uniqueness guarantee. * * @return string A string appropriate for use as an 'id' attribute on a DOM * node. It is guaranteed to be unique for the current page, even * if the current request is a subsequent Ajax request. * * @group celerity */ function celerity_generate_unique_node_id() { static $uniq = 0; $response = CelerityAPI::getStaticResourceResponse(); $block = $response->getMetadataBlock(); return 'UQ'.$block.'_'.($uniq++); } /** * Get the versioned URI for a raw resource, like an image. * * @param string Path to the raw image. * @return string Versioned path to the image, if one is available. * * @group celerity */ function celerity_get_resource_uri($resource) { $map = CelerityResourceMap::getInstance(); - $uri = $map->getFullyQualifiedURIForName($resource); + $uri = $map->getURIForName($resource); if ($uri) { return $uri; } return $resource; }