@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.)
hq.recaptime.dev/wiki/Phorge
phorge
phabricator
1@title Rendering HTML
2@group developer
3
4Rendering HTML in the Phorge environment.
5
6= Overview =
7
8Phorge attempts to prevent XSS by treating strings as default-unsafe when
9rendering. This means that if you try to build HTML through string
10concatenation, it won't work: the string will be escaped by the rendering
11pipeline, and the browser will treat it as plain text, not HTML.
12
13This document describes the right way to build HTML components so they are safe
14from XSS and render correctly. Broadly:
15
16 - Use @{function:phutil_tag} (and @{function:javelin_tag}) to build
17 tags.
18 - Use @{function:hsprintf} where @{function:phutil_tag}
19 is awkward.
20 - Combine elements with arrays, not string concatenation.
21 - @{class:AphrontView} subclasses should return a
22 @{class:PhutilSafeHTML} object from their `render()` method.
23 - @{class:AphrontView} subclasses act like tags when rendering.
24 - @{function:pht} has some special rules.
25 - There are some other things that you should be aware of.
26
27See below for discussion.
28
29= Building Tags: phutil_tag() =
30
31Build HTML tags with @{function:phutil_tag}. For example:
32
33 phutil_tag(
34 'div',
35 array(
36 'class' => 'some-class',
37 ),
38 $content);
39
40@{function:phutil_tag} will properly escape the content and all the
41attributes, and return a @{class:PhutilSafeHTML} object. The rendering
42pipeline knows that this object represents a properly escaped HTML tag. This
43allows @{function:phutil_tag} to render tags with other tags as
44content correctly (without double-escaping):
45
46 phutil_tag(
47 'div',
48 array(),
49 phutil_tag(
50 'strong',
51 array(),
52 $content));
53
54In Phorge, the @{function:javelin_tag} function is similar to
55@{function:phutil_tag}, but provides special handling for the
56`sigil` and `meta` attributes.
57
58= Building Blocks: hsprintf() =
59
60Sometimes, @{function:phutil_tag} can be particularly awkward to
61use. You can use @{function:hsprintf} to build larger and more
62complex blocks of HTML, when @{function:phutil_tag} is a poor fit.
63@{function:hsprintf} has `sprintf()` semantics, but `%s` escapes HTML:
64
65 // Safely build fragments or unwieldy blocks.
66 hsprintf(
67 '<div id="%s">',
68 $div_id);
69
70@{function:hsprintf} can be especially useful when:
71
72 - You need to build a block with a lot of tags, like a table with rows and
73 cells.
74 - You need to build part of a tag (usually you should avoid this, but if you
75 do need to, @{function:phutil_tag} can not do it).
76
77Note that it is unsafe to provide any user-controlled data to the first
78parameter of @{function:hsprintf} (the `sprintf()`-style pattern).
79
80Like @{function:phutil_tag}, this function returns a
81@{class:PhutilSafeHTML} object.
82
83= Composing Tags =
84
85When you are building a view which combines smaller components, like a section
86with a header and a body:
87
88 $header = phutil_tag('h1', ...);
89 $body = phutil_tag('p', ...);
90
91...you should NOT use string concatenation:
92
93 COUNTEREXAMPLE
94 // Not dangerous, but does the wrong thing.
95 phutil_tag('div', array(), $header.$body);
96
97Instead, use an array:
98
99 // Render a tag containing other tags safely.
100 phutil_tag('div', array(), array($header, $body));
101
102If you concatenate @{class:PhutilSafeHTML} objects, they revert to
103normal strings and are no longer marked as properly escaped tags.
104
105(In the future, these objects may stop converting to strings, but for now they
106must to maintain backward compatibility.)
107
108If you need to build a list of items with some element in between each of them
109(like a middot, comma, or vertical bar) you can use
110@{function:phutil_implode_html}:
111
112 // Render links with commas between them.
113 phutil_tag(
114 'div',
115 array(),
116 phutil_implode_html(', ', $list_of_links));
117
118= AphrontView Classes =
119
120Subclasses of @{class:AphrontView} in Phorge should return a
121@{class:PhutilSafeHTML} object. The easiest way to do this is to
122return `phutil_tag()` or `javelin_tag()`:
123
124 return phutil_tag('div', ...);
125
126You can use an @{class:AphrontView} subclass like you would a tag:
127
128 phutil_tag('div', array(), $view);
129
130= Internationalization: pht() =
131
132The @{function:pht} function has some special rules. If any input to
133@{function:pht} is a @{class:PhutilSafeHTML} object, @{function:pht}
134returns a @{class:PhutilSafeHTML} object itself. Otherwise, it returns
135normal text.
136
137This is generally safe because translations are not permitted to have more tags
138than the original text did (so if the original text had no tags, translations
139can not add any).
140
141Normally, this just means that @{function:pht} does the right thing and behaves
142like you would expect, but it is worth being aware of.
143
144= Special Cases =
145
146NOTE: This section describes dangerous methods which can bypass XSS protections.
147If possible, do not use them.
148
149You can build @{class:PhutilSafeHTML} out of a string explicitly by
150calling @{function:phutil_safe_html} on it. This is **dangerous**, because if
151you are wrong and the string is not actually safe, you have introduced an XSS
152vulnerability. Consequently, you should avoid calling this if possible.
153
154You can use @{function:phutil_escape_html_newlines} to escape HTML
155while converting newlines to `<br />`. You should not need to explicitly use
156@{function:phutil_escape_html} anywhere.
157
158If you need to apply a string function (such as `trim()`) to safe HTML, use
159@{method:PhutilSafeHTML::applyFunction}.
160
161If you need to extract the content of a @{class:PhutilSafeHTML}
162object, you should call `getHTMLContent()`, not cast it to a string. Eventually,
163we would like to remove the string cast entirely.
164
165Functions @{function:phutil_tag} and @{function:hsprintf}
166are not safe if you pass the user input for the tag or attribute name. All the
167following examples are dangerous:
168
169 counterexample
170 phutil_tag($evil);
171
172 phutil_tag('span', array($evil => $evil2));
173
174 phutil_tag('span', array('onmouseover' => $evil));
175
176 // Use PhutilURI to check if $evil is valid HTTP link.
177 hsprintf('<a href="%s">', $evil);
178
179 hsprintf('<%s>%s</%s>', $evil, $evil2, $evil);
180
181 // We have a lint rule disallowing this.
182 hsprintf($evil);