Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
freebsd
GitHub Repository: freebsd/phabricator
Path: blob/master/src/infrastructure/markup/render.php
12241 views
1
<?php
2
3
/**
4
* Render an HTML tag in a way that treats user content as unsafe by default.
5
*
6
* Tag rendering has some special logic which implements security features:
7
*
8
* - When rendering `<a>` tags, if the `rel` attribute is not specified, it
9
* is interpreted as `rel="noreferrer"`.
10
* - When rendering `<a>` tags, the `href` attribute may not begin with
11
* `javascript:`.
12
*
13
* These special cases can not be disabled.
14
*
15
* IMPORTANT: The `$tag` attribute and the keys of the `$attributes` array are
16
* trusted blindly, and not escaped. You should not pass user data in these
17
* parameters.
18
*
19
* @param string The name of the tag, like `a` or `div`.
20
* @param map<string, string> A map of tag attributes.
21
* @param wild Content to put in the tag.
22
* @return PhutilSafeHTML Tag object.
23
*/
24
function phutil_tag($tag, array $attributes = array(), $content = null) {
25
// If the `href` attribute is present, make sure it is not a "javascript:"
26
// URI. We never permit these.
27
if (!empty($attributes['href'])) {
28
// This might be a URI object, so cast it to a string.
29
$href = (string)$attributes['href'];
30
31
if (isset($href[0])) {
32
// Block 'javascript:' hrefs at the tag level: no well-designed
33
// application should ever use them, and they are a potent attack vector.
34
35
// This function is deep in the core and performance sensitive, so we're
36
// doing a cheap version of this test first to avoid calling preg_match()
37
// on URIs which begin with '/' or `#`. These cover essentially all URIs
38
// in Phabricator.
39
if (($href[0] !== '/') && ($href[0] !== '#')) {
40
// Chrome 33 and IE 11 both interpret "javascript\n:" as a Javascript
41
// URI, and all browsers interpret " javascript:" as a Javascript URI,
42
// so be aggressive about looking for "javascript:" in the initial
43
// section of the string.
44
45
$normalized_href = preg_replace('([^a-z0-9/:]+)i', '', $href);
46
if (preg_match('/^javascript:/i', $normalized_href)) {
47
throw new Exception(
48
pht(
49
"Attempting to render a tag with an '%s' attribute that begins ".
50
"with '%s'. This is either a serious security concern or a ".
51
"serious architecture concern. Seek urgent remedy.",
52
'href',
53
'javascript:'));
54
}
55
}
56
}
57
}
58
59
// For tags which can't self-close, treat null as the empty string -- for
60
// example, always render `<div></div>`, never `<div />`.
61
static $self_closing_tags = array(
62
'area' => true,
63
'base' => true,
64
'br' => true,
65
'col' => true,
66
'command' => true,
67
'embed' => true,
68
'frame' => true,
69
'hr' => true,
70
'img' => true,
71
'input' => true,
72
'keygen' => true,
73
'link' => true,
74
'meta' => true,
75
'param' => true,
76
'source' => true,
77
'track' => true,
78
'wbr' => true,
79
);
80
81
$attr_string = '';
82
foreach ($attributes as $k => $v) {
83
if ($v === null) {
84
continue;
85
}
86
$v = phutil_escape_html($v);
87
$attr_string .= ' '.$k.'="'.$v.'"';
88
}
89
90
if ($content === null) {
91
if (isset($self_closing_tags[$tag])) {
92
return new PhutilSafeHTML('<'.$tag.$attr_string.' />');
93
} else {
94
$content = '';
95
}
96
} else {
97
$content = phutil_escape_html($content);
98
}
99
100
return new PhutilSafeHTML('<'.$tag.$attr_string.'>'.$content.'</'.$tag.'>');
101
}
102
103
function phutil_tag_div($class, $content = null) {
104
return phutil_tag('div', array('class' => $class), $content);
105
}
106
107
function phutil_escape_html($string) {
108
if ($string === null) {
109
return '';
110
}
111
112
if ($string instanceof PhutilSafeHTML) {
113
return $string;
114
} else if ($string instanceof PhutilSafeHTMLProducerInterface) {
115
$result = $string->producePhutilSafeHTML();
116
if ($result instanceof PhutilSafeHTML) {
117
return phutil_escape_html($result);
118
} else if (is_array($result)) {
119
return phutil_escape_html($result);
120
} else if ($result instanceof PhutilSafeHTMLProducerInterface) {
121
return phutil_escape_html($result);
122
} else {
123
try {
124
assert_stringlike($result);
125
return phutil_escape_html((string)$result);
126
} catch (Exception $ex) {
127
throw new Exception(
128
pht(
129
"Object (of class '%s') implements %s but did not return anything ".
130
"renderable from %s.",
131
get_class($string),
132
'PhutilSafeHTMLProducerInterface',
133
'producePhutilSafeHTML()'));
134
}
135
}
136
} else if (is_array($string)) {
137
$result = '';
138
foreach ($string as $item) {
139
$result .= phutil_escape_html($item);
140
}
141
return $result;
142
}
143
144
return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
145
}
146
147
function phutil_escape_html_newlines($string) {
148
return PhutilSafeHTML::applyFunction('nl2br', $string);
149
}
150
151
/**
152
* Mark string as safe for use in HTML.
153
*/
154
function phutil_safe_html($string) {
155
if ($string == '') {
156
return $string;
157
} else if ($string instanceof PhutilSafeHTML) {
158
return $string;
159
} else {
160
return new PhutilSafeHTML($string);
161
}
162
}
163
164
/**
165
* HTML safe version of `implode()`.
166
*/
167
function phutil_implode_html($glue, array $pieces) {
168
$glue = phutil_escape_html($glue);
169
170
foreach ($pieces as $k => $piece) {
171
$pieces[$k] = phutil_escape_html($piece);
172
}
173
174
return phutil_safe_html(implode($glue, $pieces));
175
}
176
177
/**
178
* Format a HTML code. This function behaves like `sprintf()`, except that all
179
* the normal conversions (like %s) will be properly escaped.
180
*/
181
function hsprintf($html /* , ... */) {
182
$args = func_get_args();
183
array_shift($args);
184
return new PhutilSafeHTML(
185
vsprintf($html, array_map('phutil_escape_html', $args)));
186
}
187
188
189