diff --git a/src/applications/phame/skins/PhameBasicTemplateBlogSkin.php b/src/applications/phame/skins/PhameBasicTemplateBlogSkin.php index 8bee67fe8f..a6851b2f9a 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(); + $map = CelerityResourceMap::getNamedInstance('phabricator'); $resource_symbol = 'syntax-highlighting-css'; $resource_uri = $map->getURIForSymbol($resource_symbol); $this->cssResources[] = phutil_tag( 'link', array( 'rel' => 'stylesheet', 'type' => 'text/css', '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/CelerityPhabricatorResourceController.php b/src/infrastructure/celerity/CelerityPhabricatorResourceController.php index ab8aa49d1a..1f258cea8c 100644 --- a/src/infrastructure/celerity/CelerityPhabricatorResourceController.php +++ b/src/infrastructure/celerity/CelerityPhabricatorResourceController.php @@ -1,39 +1,40 @@ path = $data['path']; $this->hash = $data['hash']; } public function processRequest() { return $this->serveResource($this->path); } protected function buildResourceTransformer() { - $xformer = new CelerityResourceTransformer(); - $xformer->setMinify( - !PhabricatorEnv::getEnvConfig('phabricator.developer-mode') && - PhabricatorEnv::getEnvConfig('celerity.minify')); - $xformer->setCelerityMap(CelerityResourceMap::getInstance()); - return $xformer; + $minify_on = PhabricatorEnv::getEnvConfig('celerity.minify'); + $developer_on = PhabricatorEnv::getEnvConfig('phabricator.developer-mode'); + + $should_minify = ($minify_on && !$developer_on); + + return id(new CelerityResourceTransformer()) + ->setMinify($should_minify) + ->setCelerityMap($this->getCelerityResourceMap()); } } diff --git a/src/infrastructure/celerity/CelerityResourceController.php b/src/infrastructure/celerity/CelerityResourceController.php index f192c049b5..17b8403276 100644 --- a/src/infrastructure/celerity/CelerityResourceController.php +++ b/src/infrastructure/celerity/CelerityResourceController.php @@ -1,96 +1,94 @@ getSupportedResourceTypes(); if (empty($type_map[$type])) { throw new Exception("Only static resources may be served."); } if (AphrontRequest::getHTTPHeader('If-Modified-Since') && !PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) { // Return a "304 Not Modified". We don't care about the value of this // field since we never change what resource is served by a given URI. return $this->makeResponseCacheable(new Aphront304Response()); } $map = $this->getCelerityResourceMap(); if ($map->isPackageResource($path)) { $resource_names = $map->getResourceNamesForPackageName($path); if (!$resource_names) { return new Aphront404Response(); } try { $data = array(); foreach ($resource_names as $resource_name) { $data[] = $map->getResourceDataForName($resource_name); } $data = implode("\n\n", $data); } catch (Exception $ex) { return new Aphront404Response(); } } else { try { $data = $map->getResourceDataForName($path); } catch (Exception $ex) { return new Aphront404Response(); } } $xformer = $this->buildResourceTransformer(); if ($xformer) { $data = $xformer->transformResource($path, $data); } $response = new AphrontFileResponse(); $response->setContent($data); $response->setMimeType($type_map[$type]); return $this->makeResponseCacheable($response); } protected function getSupportedResourceTypes() { return array( 'css' => 'text/css; charset=utf-8', 'js' => 'text/javascript; charset=utf-8', 'png' => 'image/png', 'gif' => 'image/gif', 'jpg' => 'image/jpg', 'swf' => 'application/x-shockwave-flash', ); } private function makeResponseCacheable(AphrontResponse $response) { $response->setCacheDurationInSeconds(60 * 60 * 24 * 30); $response->setLastModified(time()); return $response; } } diff --git a/src/infrastructure/celerity/CelerityResourceMap.php b/src/infrastructure/celerity/CelerityResourceMap.php index 6d307b2eaa..ebaa4fb9a6 100644 --- a/src/infrastructure/celerity/CelerityResourceMap.php +++ b/src/infrastructure/celerity/CelerityResourceMap.php @@ -1,231 +1,239 @@ resources = $resources; $map = $resources->loadMap(); $this->symbolMap = idx($map, 'symbols', array()); $this->requiresMap = idx($map, 'requires', array()); $this->packageMap = idx($map, 'packages', array()); $this->nameMap = idx($map, 'names', array()); // We derive these reverse maps at runtime. $this->hashMap = array_flip($this->nameMap); $this->componentMap = array(); foreach ($this->packageMap as $package_name => $symbols) { foreach ($symbols as $symbol) { $this->componentMap[$symbol] = $package_name; } } } - public static function getInstance() { - if (empty(self::$instance)) { - $resources = new CelerityPhabricatorResources(); - self::$instance = new CelerityResourceMap($resources); + public static function getNamedInstance($name) { + if (empty(self::$instances[$name])) { + $resources_list = CelerityPhysicalResources::getAll(); + if (empty($resources_list[$name])) { + throw new Exception( + pht( + 'No resource source exists with name "%s"!', $name)); + } + + $instance = new CelerityResourceMap($resources_list[$name]); + self::$instances[$name] = $instance; } - return self::$instance; + + return self::$instances[$name]; } 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->symbolMap[$symbol])) { throw new Exception( pht( 'Attempting to resolve unknown resource, "%s".', $symbol)); } $hash = $this->symbolMap[$symbol]; $map[$symbol] = $hash; if (isset($this->requiresMap[$hash])) { $requires = $this->requiresMap[$hash]; } else { $requires = array(); } foreach ($requires as $required_symbol) { if (!empty($map[$required_symbol])) { continue; } $this->resolveResource($map, $required_symbol); } } private function packageResources(array $resolved_map) { $packaged = array(); $handled = array(); foreach ($resolved_map as $symbol => $hash) { if (isset($handled[$symbol])) { continue; } if (empty($this->componentMap[$symbol])) { $packaged[] = $this->hashMap[$hash]; } else { $package_name = $this->componentMap[$symbol]; $packaged[] = $package_name; $package_symbols = $this->packageMap[$package_name]; foreach ($package_symbols as $package_symbol) { $handled[$package_symbol] = true; } } } return $packaged; } public function getResourceDataForName($resource_name) { return $this->resources->getResourceData($resource_name); } public function getResourceNamesForPackageName($package_name) { $package_symbols = idx($this->packageMap, $package_name); if (!$package_symbols) { return null; } $resource_names = array(); foreach ($package_symbols as $symbol) { $resource_names[] = $this->hashMap[$this->symbolMap[$symbol]]; } return $resource_names; } /** * 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 getModifiedTimeForName($name) { if ($this->isPackageResource($name)) { $names = array(); foreach ($this->packageMap[$name] as $symbol) { $names[] = $this->getResourceNameForSymbol($symbol); } } else { $names = array($name); } $mtime = 0; foreach ($names as $name) { $mtime = max($mtime, $this->resources->getResourceModifiedTime($name)); } return $mtime; } /** * 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 Resource URI, or null if the symbol is unknown. */ public function getURIForSymbol($symbol) { $hash = idx($this->symbolMap, $symbol); return $this->getURIForHash($hash); } /** * 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 Resource URI, or null if the name is unknown. */ public function getURIForName($name) { $hash = idx($this->nameMap, $name); return $this->getURIForHash($hash); } /** * Return the absolute URI for a resource, identified by hash. * This method is fairly low-level and ignores packaging. * * @param string Resource hash to lookup. * @return string|null Resource URI, or null if the hash is unknown. */ private function getURIForHash($hash) { if ($hash === null) { return null; } return $this->resources->getResourceURI($hash, $this->hashMap[$hash]); } /** * 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) { $hash = idx($this->symbolMap, $name); if ($hash === null) { return null; } return idx($this->requiresMap, $hash, array()); } /** * 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) { $hash = idx($this->symbolMap, $symbol); return idx($this->hashMap, $hash); } public function isPackageResource($name) { return isset($this->packageMap[$name]); } } diff --git a/src/infrastructure/celerity/CelerityResourceTransformer.php b/src/infrastructure/celerity/CelerityResourceTransformer.php index 846d8b5e89..52b9083b82 100644 --- a/src/infrastructure/celerity/CelerityResourceTransformer.php +++ b/src/infrastructure/celerity/CelerityResourceTransformer.php @@ -1,226 +1,216 @@ 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->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 ee55074ac4..b52274f323 100644 --- a/src/infrastructure/celerity/CelerityStaticResourceResponse.php +++ b/src/infrastructure/celerity/CelerityStaticResourceResponse.php @@ -1,256 +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(); + $map = CelerityResourceMap::getNamedInstance('phabricator'); $symbols = array_keys($this->symbols); $this->packaged = $map->getPackagedNamesForSymbols($symbols); $this->needsResolve = false; } return $this; } - public function renderSingleResource($symbol) { - $map = CelerityResourceMap::getInstance(); + public function renderSingleResource($symbol, $source_name) { + $map = CelerityResourceMap::getNamedInstance($source_name); $packaged = $map->getPackagedNamesForSymbols(array($symbol)); return $this->renderPackagedResources($packaged); } public function renderResourcesOfType($type) { $this->resolveResources(); $resources = array(); 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 $name) { if (isset($this->hasRendered[$name])) { continue; } $this->hasRendered[$name] = true; $output[] = $this->renderResource($name); $output[] = "\n"; } return phutil_implode_html('', $output); } 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($name) { - $map = CelerityResourceMap::getInstance(); + $map = CelerityResourceMap::getNamedInstance('phabricator'); $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/infrastructure/celerity/__tests__/CelerityResourceTransformerTestCase.php b/src/infrastructure/celerity/__tests__/CelerityResourceTransformerTestCase.php index 883e415540..f23862b8d0 100644 --- a/src/infrastructure/celerity/__tests__/CelerityResourceTransformerTestCase.php +++ b/src/infrastructure/celerity/__tests__/CelerityResourceTransformerTestCase.php @@ -1,39 +1,37 @@ parse($options) + array( 'minify' => false, 'name' => $name, ); $xformer = new CelerityResourceTransformer(); - $xformer->setRawResourceMap( + $xformer->setRawURIMap( array( - '/rsrc/example.png' => array( - 'uri' => '/res/hash/example.png', - ), + '/rsrc/example.png' => '/res/hash/example.png', )); $xformer->setMinify($options['minify']); $result = $xformer->transformResource($options['name'], $in); $this->assertEqual(rtrim($expect), rtrim($result), $file); } } } diff --git a/src/infrastructure/celerity/api.php b/src/infrastructure/celerity/api.php index 1f5346df90..8b04b49fd3 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(); +function celerity_get_resource_uri($resource, $source = 'phabricator') { + $map = CelerityResourceMap::getNamedInstance($source); $uri = $map->getURIForName($resource); if ($uri) { return $uri; } return $resource; } diff --git a/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php b/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php index 24003939f0..b979c9e87b 100644 --- a/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php +++ b/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php @@ -1,265 +1,265 @@ symbolsBinary === null) { list($err, $stdout) = exec_manual('which javelinsymbols'); $this->symbolsBinary = ($err ? false : rtrim($stdout)); } return $this->symbolsBinary; } public function willLintPaths(array $paths) { if (!$this->getBinaryPath()) { return; } $root = dirname(phutil_get_library_root('phabricator')); require_once $root.'/scripts/__init_script__.php'; $futures = array(); foreach ($paths as $path) { if ($this->shouldIgnorePath($path)) { continue; } $future = $this->newSymbolsFuture($path); $futures[$path] = $future; } foreach (Futures($futures)->limit(8) as $path => $future) { $this->symbols[$path] = $future->resolvex(); } } public function getLinterName() { return 'JAVELIN'; } public function getLintSeverityMap() { return array( self::LINT_MISSING_BINARY => ArcanistLintSeverity::SEVERITY_WARNING, ); } public function getLintNameMap() { return array( self::LINT_PRIVATE_ACCESS => 'Private Method/Member Access', self::LINT_MISSING_DEPENDENCY => 'Missing Javelin Dependency', self::LINT_UNNECESSARY_DEPENDENCY => 'Unnecessary Javelin Dependency', self::LINT_UNKNOWN_DEPENDENCY => 'Unknown Javelin Dependency', self::LINT_MISSING_BINARY => '`javelinsymbols` Not In Path', ); } public function getCacheGranularity() { return ArcanistLinter::GRANULARITY_REPOSITORY; } public function getCacheVersion() { $version = '0'; $binary_path = $this->getBinaryPath(); if ($binary_path) { $version .= '-'.md5_file($binary_path); } return $version; } private function shouldIgnorePath($path) { return preg_match('@/__tests__/|externals/javelin/docs/@', $path); } public function lintPath($path) { if ($this->shouldIgnorePath($path)) { return; } if (!$this->symbolsBinary) { if (!$this->haveWarnedAboutBinary) { $this->haveWarnedAboutBinary = true; // TODO: Write build documentation for the Javelin binaries and point // the user at it. $this->raiseLintAtLine( 1, 0, self::LINT_MISSING_BINARY, "The 'javelinsymbols' binary in the Javelin project is not ". "available in \$PATH, so the Javelin linter can't run. This ". "isn't a big concern, but means some Javelin problems can't be ". "automatically detected."); } return; } list($uses, $installs) = $this->getUsedAndInstalledSymbolsForPath($path); foreach ($uses as $symbol => $line) { $parts = explode('.', $symbol); foreach ($parts as $part) { if ($part[0] == '_' && $part[1] != '_') { $base = implode('.', array_slice($parts, 0, 2)); if (!array_key_exists($base, $installs)) { $this->raiseLintAtLine( $line, 0, self::LINT_PRIVATE_ACCESS, "This file accesses private symbol '{$symbol}' across file ". "boundaries. You may only access private members and methods ". "from the file where they are defined."); } break; } } } if ($this->getEngine()->getCommitHookMode()) { // Don't do the dependency checks in commit-hook mode because we won't // have an available working copy. return; } $external_classes = array(); foreach ($uses as $symbol => $line) { $parts = explode('.', $symbol); $class = implode('.', array_slice($parts, 0, 2)); if (!array_key_exists($class, $external_classes) && !array_key_exists($class, $installs)) { $external_classes[$class] = $line; } } - $celerity = CelerityResourceMap::getInstance(); + $celerity = CelerityResourceMap::getNamedInstance('phabricator'); $path = preg_replace( '@^externals/javelinjs/src/@', 'webroot/rsrc/js/javelin/', $path); $need = $external_classes; $resource_name = substr($path, strlen('webroot')); $requires = $celerity->getRequiredSymbolsForName($resource_name); if (!$requires) { $requires = array(); } foreach ($requires as $key => $requires_symbol) { $requires_name = $celerity->getResourceNameForSymbol($requires_symbol); if ($requires_name === null) { $this->raiseLintAtLine( 0, 0, self::LINT_UNKNOWN_DEPENDENCY, "This file @requires component '{$requires_symbol}', but it does ". "not exist. You may need to rebuild the Celerity map."); unset($requires[$key]); continue; } if (preg_match('/\\.css$/', $requires_name)) { // If JS requires CSS, just assume everything is fine. unset($requires[$key]); } else { $symbol_path = 'webroot'.$requires_name; list($ignored, $req_install) = $this->getUsedAndInstalledSymbolsForPath( $symbol_path); if (array_intersect_key($req_install, $external_classes)) { $need = array_diff_key($need, $req_install); unset($requires[$key]); } } } foreach ($need as $class => $line) { $this->raiseLintAtLine( $line, 0, self::LINT_MISSING_DEPENDENCY, "This file uses '{$class}' but does not @requires the component ". "which installs it. You may need to rebuild the Celerity map."); } foreach ($requires as $component) { $this->raiseLintAtLine( 0, 0, self::LINT_UNNECESSARY_DEPENDENCY, "This file @requires component '{$component}' but does not use ". "anything it provides."); } } private function loadSymbols($path) { if (empty($this->symbols[$path])) { $this->symbols[$path] = $this->newSymbolsFuture($path)->resolvex(); } return $this->symbols[$path]; } private function newSymbolsFuture($path) { $future = new ExecFuture('javelinsymbols # %s', $path); $future->write($this->getData($path)); return $future; } private function getUsedAndInstalledSymbolsForPath($path) { list($symbols) = $this->loadSymbols($path); $symbols = trim($symbols); $uses = array(); $installs = array(); if (empty($symbols)) { // This file has no symbols. return array($uses, $installs); } $symbols = explode("\n", trim($symbols)); foreach ($symbols as $line) { $matches = null; if (!preg_match('/^([?+\*])([^:]*):(\d+)$/', $line, $matches)) { throw new Exception( "Received malformed output from `javelinsymbols`."); } $type = $matches[1]; $symbol = $matches[2]; $line = $matches[3]; switch ($type) { case '?': $uses[$symbol] = $line; break; case '+': $installs['JX.'.$symbol] = $line; break; } } $contents = $this->getData($path); $matches = null; $count = preg_match_all( '/@javelin-installs\W+(\S+)/', $contents, $matches, PREG_PATTERN_ORDER); if ($count) { foreach ($matches[1] as $symbol) { $installs[$symbol] = 0; } } return array($uses, $installs); } } diff --git a/src/view/page/PhabricatorStandardPageView.php b/src/view/page/PhabricatorStandardPageView.php index ee639564b2..d31c3567b8 100644 --- a/src/view/page/PhabricatorStandardPageView.php +++ b/src/view/page/PhabricatorStandardPageView.php @@ -1,438 +1,438 @@ 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 setSearchDefaultScope($search_default_scope) { $this->searchDefaultScope = $search_default_scope; return $this; } public function getSearchDefaultScope() { return $this->searchDefaultScope; } 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 ($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) ->setDefaultSearchScope($this->getSearchDefaultScope()); 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')); + $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(); $container = null; 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()); } $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, 'pageObjects' => array_fill_keys($this->pageObjects, true), )); $container = phutil_tag( 'div', array( 'id' => $aphlict_container_id, 'style' => 'position: absolute; width: 0; height: 0; overflow: hidden;', ), ''); } $response = CelerityAPI::getStaticResourceResponse(); $tail = array( parent::getTail(), $container, $response->renderHTMLFooter(), ); return phutil_implode_html("\n", $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'; } return implode(' ', $classes); } private function getConsole() { if ($this->disableConsole) { return null; } return $this->getRequest()->getApplicationConfiguration()->getConsole(); } }