Path: blob/master/src/applications/celerity/CelerityStaticResourceResponse.php
12250 views
<?php12/**3* Tracks and resolves dependencies the page declares with4* @{function:require_celerity_resource}, and then builds appropriate HTML or5* Ajax responses.6*/7final class CelerityStaticResourceResponse extends Phobject {89private $symbols = array();10private $needsResolve = true;11private $resolved;12private $packaged;13private $metadata = array();14private $metadataBlock = 0;15private $metadataLocked;16private $behaviors = array();17private $hasRendered = array();18private $postprocessorKey;19private $contentSecurityPolicyURIs = array();2021public function __construct() {22if (isset($_REQUEST['__metablock__'])) {23$this->metadataBlock = (int)$_REQUEST['__metablock__'];24}25}2627public function addMetadata($metadata) {28if ($this->metadataLocked) {29throw new Exception(30pht(31'Attempting to add more metadata after metadata has been '.32'locked.'));33}3435$id = count($this->metadata);36$this->metadata[$id] = $metadata;37return $this->metadataBlock.'_'.$id;38}3940public function addContentSecurityPolicyURI($kind, $uri) {41$this->contentSecurityPolicyURIs[$kind][] = $uri;42return $this;43}4445public function getContentSecurityPolicyURIMap() {46return $this->contentSecurityPolicyURIs;47}4849public function getMetadataBlock() {50return $this->metadataBlock;51}5253public function setPostprocessorKey($postprocessor_key) {54$this->postprocessorKey = $postprocessor_key;55return $this;56}5758public function getPostprocessorKey() {59return $this->postprocessorKey;60}6162/**63* Register a behavior for initialization.64*65* NOTE: If `$config` is empty, a behavior will execute only once even if it66* is initialized multiple times. If `$config` is nonempty, the behavior will67* be invoked once for each configuration.68*/69public function initBehavior(70$behavior,71array $config = array(),72$source_name = null) {7374$this->requireResource('javelin-behavior-'.$behavior, $source_name);7576if (empty($this->behaviors[$behavior])) {77$this->behaviors[$behavior] = array();78}7980if ($config) {81$this->behaviors[$behavior][] = $config;82}8384return $this;85}8687public function requireResource($symbol, $source_name) {88if (isset($this->symbols[$source_name][$symbol])) {89return $this;90}9192// Verify that the resource exists.93$map = CelerityResourceMap::getNamedInstance($source_name);94$name = $map->getResourceNameForSymbol($symbol);95if ($name === null) {96throw new Exception(97pht(98'No resource with symbol "%s" exists in source "%s"!',99$symbol,100$source_name));101}102103$this->symbols[$source_name][$symbol] = true;104$this->needsResolve = true;105106return $this;107}108109private function resolveResources() {110if ($this->needsResolve) {111$this->packaged = array();112foreach ($this->symbols as $source_name => $symbols_map) {113$symbols = array_keys($symbols_map);114115$map = CelerityResourceMap::getNamedInstance($source_name);116$packaged = $map->getPackagedNamesForSymbols($symbols);117118$this->packaged[$source_name] = $packaged;119}120$this->needsResolve = false;121}122return $this;123}124125public function renderSingleResource($symbol, $source_name) {126$map = CelerityResourceMap::getNamedInstance($source_name);127$packaged = $map->getPackagedNamesForSymbols(array($symbol));128return $this->renderPackagedResources($map, $packaged);129}130131public function renderResourcesOfType($type) {132$this->resolveResources();133134$result = array();135foreach ($this->packaged as $source_name => $resource_names) {136$map = CelerityResourceMap::getNamedInstance($source_name);137138$resources_of_type = array();139foreach ($resource_names as $resource_name) {140$resource_type = $map->getResourceTypeForName($resource_name);141if ($resource_type == $type) {142$resources_of_type[] = $resource_name;143}144}145146$result[] = $this->renderPackagedResources($map, $resources_of_type);147}148149return phutil_implode_html('', $result);150}151152private function renderPackagedResources(153CelerityResourceMap $map,154array $resources) {155156$output = array();157foreach ($resources as $name) {158if (isset($this->hasRendered[$name])) {159continue;160}161$this->hasRendered[$name] = true;162163$output[] = $this->renderResource($map, $name);164}165166return $output;167}168169private function renderResource(170CelerityResourceMap $map,171$name) {172173$uri = $this->getURI($map, $name);174$type = $map->getResourceTypeForName($name);175176$multimeter = MultimeterControl::getInstance();177if ($multimeter) {178$event_type = MultimeterEvent::TYPE_STATIC_RESOURCE;179$multimeter->newEvent($event_type, 'rsrc.'.$name, 1);180}181182switch ($type) {183case 'css':184return phutil_tag(185'link',186array(187'rel' => 'stylesheet',188'type' => 'text/css',189'href' => $uri,190));191case 'js':192return phutil_tag(193'script',194array(195'type' => 'text/javascript',196'src' => $uri,197),198'');199}200201throw new Exception(202pht(203'Unable to render resource "%s", which has unknown type "%s".',204$name,205$type));206}207208public function renderHTMLFooter($is_frameable) {209$this->metadataLocked = true;210211$merge_data = array(212'block' => $this->metadataBlock,213'data' => $this->metadata,214);215$this->metadata = array();216217$behavior_lists = array();218if ($this->behaviors) {219$behaviors = $this->behaviors;220$this->behaviors = array();221222$higher_priority_names = array(223'refresh-csrf',224'aphront-basic-tokenizer',225'dark-console',226'history-install',227);228229$higher_priority_behaviors = array_select_keys(230$behaviors,231$higher_priority_names);232233foreach ($higher_priority_names as $name) {234unset($behaviors[$name]);235}236237$behavior_groups = array(238$higher_priority_behaviors,239$behaviors,240);241242foreach ($behavior_groups as $group) {243if (!$group) {244continue;245}246$behavior_lists[] = $group;247}248}249250$initializers = array();251252// Even if there is no metadata on the page, Javelin uses the mergeData()253// call to start dispatching the event queue, so we always want to include254// this initializer.255$initializers[] = array(256'kind' => 'merge',257'data' => $merge_data,258);259260foreach ($behavior_lists as $behavior_list) {261$initializers[] = array(262'kind' => 'behaviors',263'data' => $behavior_list,264);265}266267if ($is_frameable) {268$initializers[] = array(269'data' => 'frameable',270'kind' => (bool)$is_frameable,271);272}273274$tags = array();275foreach ($initializers as $initializer) {276$data = $initializer['data'];277if (is_array($data)) {278$json_data = AphrontResponse::encodeJSONForHTTPResponse($data);279} else {280$json_data = json_encode($data);281}282283$tags[] = phutil_tag(284'data',285array(286'data-javelin-init-kind' => $initializer['kind'],287'data-javelin-init-data' => $json_data,288));289}290291return $tags;292}293294public static function renderInlineScript($data) {295if (stripos($data, '</script>') !== false) {296throw new Exception(297pht(298'Literal %s is not allowed inside inline script.',299'</script>'));300}301if (strpos($data, '<!') !== false) {302throw new Exception(303pht(304'Literal %s is not allowed inside inline script.',305'<!'));306}307// We don't use <![CDATA[ ]]> because it is ignored by HTML parsers. We308// would need to send the document with XHTML content type.309return phutil_tag(310'script',311array('type' => 'text/javascript'),312phutil_safe_html($data));313}314315public function buildAjaxResponse($payload, $error = null) {316$response = array(317'error' => $error,318'payload' => $payload,319);320321if ($this->metadata) {322$response['javelin_metadata'] = $this->metadata;323$this->metadata = array();324}325326if ($this->behaviors) {327$response['javelin_behaviors'] = $this->behaviors;328$this->behaviors = array();329}330331$this->resolveResources();332$resources = array();333foreach ($this->packaged as $source_name => $resource_names) {334$map = CelerityResourceMap::getNamedInstance($source_name);335foreach ($resource_names as $resource_name) {336$resources[] = $this->getURI($map, $resource_name);337}338}339if ($resources) {340$response['javelin_resources'] = $resources;341}342343return $response;344}345346public function getURI(347CelerityResourceMap $map,348$name,349$use_primary_domain = false) {350351$uri = $map->getURIForName($name);352353// If we have a postprocessor selected, add it to the URI.354$postprocessor_key = $this->getPostprocessorKey();355if ($postprocessor_key) {356$uri = preg_replace('@^/res/@', '/res/'.$postprocessor_key.'X/', $uri);357}358359// In developer mode, we dump file modification times into the URI. When a360// page is reloaded in the browser, any resources brought in by Ajax calls361// do not trigger revalidation, so without this it's very difficult to get362// changes to Ajaxed-in CSS to work (you must clear your cache or rerun363// the map script). In production, we can assume the map script gets run364// after changes, and safely skip this.365if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {366$mtime = $map->getModifiedTimeForName($name);367$uri = preg_replace('@^/res/@', '/res/'.$mtime.'T/', $uri);368}369370if ($use_primary_domain) {371return PhabricatorEnv::getURI($uri);372} else {373return PhabricatorEnv::getCDNURI($uri);374}375}376377}378379380