diff --git a/src/applications/phame/skins/PhameBasicTemplateBlogSkin.php b/src/applications/phame/skins/PhameBasicTemplateBlogSkin.php index 7c49d2ae1e..25d0b66449 100644 --- a/src/applications/phame/skins/PhameBasicTemplateBlogSkin.php +++ b/src/applications/phame/skins/PhameBasicTemplateBlogSkin.php @@ -1,138 +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(); - $symbol_info = $map->lookupSymbolInformation('syntax-highlighting-css'); + $resource_symbol = 'syntax-highlighting-css'; + $resource_uri = $map->getFullyQualifiedURIForSymbol($resource_symbol); $this->cssResources[] = phutil_tag( 'link', array( 'rel' => 'stylesheet', 'type' => 'text/css', - 'href' => PhabricatorEnv::getCDNURI($symbol_info['uri']), + 'href' => $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 de6dce674d..ddcf5e038b 100644 --- a/src/infrastructure/celerity/CelerityResourceMap.php +++ b/src/infrastructure/celerity/CelerityResourceMap.php @@ -1,154 +1,204 @@ resourceMap = $resource_map; return $this; } public 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) { $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; } public function resolvePackage($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; } - public function lookupSymbolInformation($symbol) { + 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']); + } + return 0; + } + + + /** + * Return the fully-qualified, 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) { + $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. * * @param string Resource name to lookup. - * @return string Fully-qualified resource URI. + * @return string|null Fully-qualified resource URI, or null if the name + * is unknown. */ public function getFullyQualifiedURIForName($name) { $info = $this->lookupFileInformation($name); if ($info) { return idx($info, 'uri'); } return null; } /** * Return the resource symbols required by a named resource. * * @param string Resource name to lookup. - * @return list List of required symbols. + * @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/CelerityStaticResourceResponse.php b/src/infrastructure/celerity/CelerityStaticResourceResponse.php index 069be5cd85..aee24e4790 100644 --- a/src/infrastructure/celerity/CelerityStaticResourceResponse.php +++ b/src/infrastructure/celerity/CelerityStaticResourceResponse.php @@ -1,263 +1,262 @@ 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 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']; // 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'])); + $mtime = max($mtime, $map->getModifiedTimeForSymbol($symbol)); } } $uri = preg_replace('@^/res/@', '/res/'.$mtime.'T/', $uri); } return PhabricatorEnv::getCDNURI($uri); } } diff --git a/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php b/src/infrastructure/lint/linter/PhabricatorJavelinLinter.php index f02a019231..24003939f0 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(); $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 => $symbol_name) { - $symbol_info = $celerity->lookupSymbolInformation($symbol_name); - if (!$symbol_info) { + 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 '{$symbol_name}', but it does not ". - "exist. You may need to rebuild the Celerity map."); + "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$/', $symbol_info['disk'])) { + if (preg_match('/\\.css$/', $requires_name)) { // If JS requires CSS, just assume everything is fine. unset($requires[$key]); } else { - $symbol_path = 'webroot'.$symbol_info['disk']; + $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); } }