Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/applications/celerity/CelerityStaticResourceResponse.php
12250 views
1
<?php
2
3
/**
4
* Tracks and resolves dependencies the page declares with
5
* @{function:require_celerity_resource}, and then builds appropriate HTML or
6
* Ajax responses.
7
*/
8
final class CelerityStaticResourceResponse extends Phobject {
9
10
private $symbols = array();
11
private $needsResolve = true;
12
private $resolved;
13
private $packaged;
14
private $metadata = array();
15
private $metadataBlock = 0;
16
private $metadataLocked;
17
private $behaviors = array();
18
private $hasRendered = array();
19
private $postprocessorKey;
20
private $contentSecurityPolicyURIs = array();
21
22
public function __construct() {
23
if (isset($_REQUEST['__metablock__'])) {
24
$this->metadataBlock = (int)$_REQUEST['__metablock__'];
25
}
26
}
27
28
public function addMetadata($metadata) {
29
if ($this->metadataLocked) {
30
throw new Exception(
31
pht(
32
'Attempting to add more metadata after metadata has been '.
33
'locked.'));
34
}
35
36
$id = count($this->metadata);
37
$this->metadata[$id] = $metadata;
38
return $this->metadataBlock.'_'.$id;
39
}
40
41
public function addContentSecurityPolicyURI($kind, $uri) {
42
$this->contentSecurityPolicyURIs[$kind][] = $uri;
43
return $this;
44
}
45
46
public function getContentSecurityPolicyURIMap() {
47
return $this->contentSecurityPolicyURIs;
48
}
49
50
public function getMetadataBlock() {
51
return $this->metadataBlock;
52
}
53
54
public function setPostprocessorKey($postprocessor_key) {
55
$this->postprocessorKey = $postprocessor_key;
56
return $this;
57
}
58
59
public function getPostprocessorKey() {
60
return $this->postprocessorKey;
61
}
62
63
/**
64
* Register a behavior for initialization.
65
*
66
* NOTE: If `$config` is empty, a behavior will execute only once even if it
67
* is initialized multiple times. If `$config` is nonempty, the behavior will
68
* be invoked once for each configuration.
69
*/
70
public function initBehavior(
71
$behavior,
72
array $config = array(),
73
$source_name = null) {
74
75
$this->requireResource('javelin-behavior-'.$behavior, $source_name);
76
77
if (empty($this->behaviors[$behavior])) {
78
$this->behaviors[$behavior] = array();
79
}
80
81
if ($config) {
82
$this->behaviors[$behavior][] = $config;
83
}
84
85
return $this;
86
}
87
88
public function requireResource($symbol, $source_name) {
89
if (isset($this->symbols[$source_name][$symbol])) {
90
return $this;
91
}
92
93
// Verify that the resource exists.
94
$map = CelerityResourceMap::getNamedInstance($source_name);
95
$name = $map->getResourceNameForSymbol($symbol);
96
if ($name === null) {
97
throw new Exception(
98
pht(
99
'No resource with symbol "%s" exists in source "%s"!',
100
$symbol,
101
$source_name));
102
}
103
104
$this->symbols[$source_name][$symbol] = true;
105
$this->needsResolve = true;
106
107
return $this;
108
}
109
110
private function resolveResources() {
111
if ($this->needsResolve) {
112
$this->packaged = array();
113
foreach ($this->symbols as $source_name => $symbols_map) {
114
$symbols = array_keys($symbols_map);
115
116
$map = CelerityResourceMap::getNamedInstance($source_name);
117
$packaged = $map->getPackagedNamesForSymbols($symbols);
118
119
$this->packaged[$source_name] = $packaged;
120
}
121
$this->needsResolve = false;
122
}
123
return $this;
124
}
125
126
public function renderSingleResource($symbol, $source_name) {
127
$map = CelerityResourceMap::getNamedInstance($source_name);
128
$packaged = $map->getPackagedNamesForSymbols(array($symbol));
129
return $this->renderPackagedResources($map, $packaged);
130
}
131
132
public function renderResourcesOfType($type) {
133
$this->resolveResources();
134
135
$result = array();
136
foreach ($this->packaged as $source_name => $resource_names) {
137
$map = CelerityResourceMap::getNamedInstance($source_name);
138
139
$resources_of_type = array();
140
foreach ($resource_names as $resource_name) {
141
$resource_type = $map->getResourceTypeForName($resource_name);
142
if ($resource_type == $type) {
143
$resources_of_type[] = $resource_name;
144
}
145
}
146
147
$result[] = $this->renderPackagedResources($map, $resources_of_type);
148
}
149
150
return phutil_implode_html('', $result);
151
}
152
153
private function renderPackagedResources(
154
CelerityResourceMap $map,
155
array $resources) {
156
157
$output = array();
158
foreach ($resources as $name) {
159
if (isset($this->hasRendered[$name])) {
160
continue;
161
}
162
$this->hasRendered[$name] = true;
163
164
$output[] = $this->renderResource($map, $name);
165
}
166
167
return $output;
168
}
169
170
private function renderResource(
171
CelerityResourceMap $map,
172
$name) {
173
174
$uri = $this->getURI($map, $name);
175
$type = $map->getResourceTypeForName($name);
176
177
$multimeter = MultimeterControl::getInstance();
178
if ($multimeter) {
179
$event_type = MultimeterEvent::TYPE_STATIC_RESOURCE;
180
$multimeter->newEvent($event_type, 'rsrc.'.$name, 1);
181
}
182
183
switch ($type) {
184
case 'css':
185
return phutil_tag(
186
'link',
187
array(
188
'rel' => 'stylesheet',
189
'type' => 'text/css',
190
'href' => $uri,
191
));
192
case 'js':
193
return phutil_tag(
194
'script',
195
array(
196
'type' => 'text/javascript',
197
'src' => $uri,
198
),
199
'');
200
}
201
202
throw new Exception(
203
pht(
204
'Unable to render resource "%s", which has unknown type "%s".',
205
$name,
206
$type));
207
}
208
209
public function renderHTMLFooter($is_frameable) {
210
$this->metadataLocked = true;
211
212
$merge_data = array(
213
'block' => $this->metadataBlock,
214
'data' => $this->metadata,
215
);
216
$this->metadata = array();
217
218
$behavior_lists = array();
219
if ($this->behaviors) {
220
$behaviors = $this->behaviors;
221
$this->behaviors = array();
222
223
$higher_priority_names = array(
224
'refresh-csrf',
225
'aphront-basic-tokenizer',
226
'dark-console',
227
'history-install',
228
);
229
230
$higher_priority_behaviors = array_select_keys(
231
$behaviors,
232
$higher_priority_names);
233
234
foreach ($higher_priority_names as $name) {
235
unset($behaviors[$name]);
236
}
237
238
$behavior_groups = array(
239
$higher_priority_behaviors,
240
$behaviors,
241
);
242
243
foreach ($behavior_groups as $group) {
244
if (!$group) {
245
continue;
246
}
247
$behavior_lists[] = $group;
248
}
249
}
250
251
$initializers = array();
252
253
// Even if there is no metadata on the page, Javelin uses the mergeData()
254
// call to start dispatching the event queue, so we always want to include
255
// this initializer.
256
$initializers[] = array(
257
'kind' => 'merge',
258
'data' => $merge_data,
259
);
260
261
foreach ($behavior_lists as $behavior_list) {
262
$initializers[] = array(
263
'kind' => 'behaviors',
264
'data' => $behavior_list,
265
);
266
}
267
268
if ($is_frameable) {
269
$initializers[] = array(
270
'data' => 'frameable',
271
'kind' => (bool)$is_frameable,
272
);
273
}
274
275
$tags = array();
276
foreach ($initializers as $initializer) {
277
$data = $initializer['data'];
278
if (is_array($data)) {
279
$json_data = AphrontResponse::encodeJSONForHTTPResponse($data);
280
} else {
281
$json_data = json_encode($data);
282
}
283
284
$tags[] = phutil_tag(
285
'data',
286
array(
287
'data-javelin-init-kind' => $initializer['kind'],
288
'data-javelin-init-data' => $json_data,
289
));
290
}
291
292
return $tags;
293
}
294
295
public static function renderInlineScript($data) {
296
if (stripos($data, '</script>') !== false) {
297
throw new Exception(
298
pht(
299
'Literal %s is not allowed inside inline script.',
300
'</script>'));
301
}
302
if (strpos($data, '<!') !== false) {
303
throw new Exception(
304
pht(
305
'Literal %s is not allowed inside inline script.',
306
'<!'));
307
}
308
// We don't use <![CDATA[ ]]> because it is ignored by HTML parsers. We
309
// would need to send the document with XHTML content type.
310
return phutil_tag(
311
'script',
312
array('type' => 'text/javascript'),
313
phutil_safe_html($data));
314
}
315
316
public function buildAjaxResponse($payload, $error = null) {
317
$response = array(
318
'error' => $error,
319
'payload' => $payload,
320
);
321
322
if ($this->metadata) {
323
$response['javelin_metadata'] = $this->metadata;
324
$this->metadata = array();
325
}
326
327
if ($this->behaviors) {
328
$response['javelin_behaviors'] = $this->behaviors;
329
$this->behaviors = array();
330
}
331
332
$this->resolveResources();
333
$resources = array();
334
foreach ($this->packaged as $source_name => $resource_names) {
335
$map = CelerityResourceMap::getNamedInstance($source_name);
336
foreach ($resource_names as $resource_name) {
337
$resources[] = $this->getURI($map, $resource_name);
338
}
339
}
340
if ($resources) {
341
$response['javelin_resources'] = $resources;
342
}
343
344
return $response;
345
}
346
347
public function getURI(
348
CelerityResourceMap $map,
349
$name,
350
$use_primary_domain = false) {
351
352
$uri = $map->getURIForName($name);
353
354
// If we have a postprocessor selected, add it to the URI.
355
$postprocessor_key = $this->getPostprocessorKey();
356
if ($postprocessor_key) {
357
$uri = preg_replace('@^/res/@', '/res/'.$postprocessor_key.'X/', $uri);
358
}
359
360
// In developer mode, we dump file modification times into the URI. When a
361
// page is reloaded in the browser, any resources brought in by Ajax calls
362
// do not trigger revalidation, so without this it's very difficult to get
363
// changes to Ajaxed-in CSS to work (you must clear your cache or rerun
364
// the map script). In production, we can assume the map script gets run
365
// after changes, and safely skip this.
366
if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) {
367
$mtime = $map->getModifiedTimeForName($name);
368
$uri = preg_replace('@^/res/@', '/res/'.$mtime.'T/', $uri);
369
}
370
371
if ($use_primary_domain) {
372
return PhabricatorEnv::getURI($uri);
373
} else {
374
return PhabricatorEnv::getCDNURI($uri);
375
}
376
}
377
378
}
379
380