Remove product literal strings in "pht()", part 18
[phabricator.git] / src / infrastructure / markup / render.php
blob84c3616fe88018c10f841f3f176ecf2df2a8df6e
1 <?php
3 /**
4 * Render an HTML tag in a way that treats user content as unsafe by default.
6 * Tag rendering has some special logic which implements security features:
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:`.
13 * These special cases can not be disabled.
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.
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.
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'];
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.
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.
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:'));
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,
81 $attr_string = '';
82 foreach ($attributes as $k => $v) {
83 if ($v === null) {
84 continue;
86 $v = phutil_escape_html($v);
87 $attr_string .= ' '.$k.'="'.$v.'"';
90 if ($content === null) {
91 if (isset($self_closing_tags[$tag])) {
92 return new PhutilSafeHTML('<'.$tag.$attr_string.' />');
93 } else {
94 $content = '';
96 } else {
97 $content = phutil_escape_html($content);
100 return new PhutilSafeHTML('<'.$tag.$attr_string.'>'.$content.'</'.$tag.'>');
103 function phutil_tag_div($class, $content = null) {
104 return phutil_tag('div', array('class' => $class), $content);
107 function phutil_escape_html($string) {
108 if ($string === null) {
109 return '';
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()'));
136 } else if (is_array($string)) {
137 $result = '';
138 foreach ($string as $item) {
139 $result .= phutil_escape_html($item);
141 return $result;
144 return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
147 function phutil_escape_html_newlines($string) {
148 return PhutilSafeHTML::applyFunction('nl2br', $string);
152 * Mark string as safe for use in HTML.
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);
165 * HTML safe version of `implode()`.
167 function phutil_implode_html($glue, array $pieces) {
168 $glue = phutil_escape_html($glue);
170 foreach ($pieces as $k => $piece) {
171 $pieces[$k] = phutil_escape_html($piece);
174 return phutil_safe_html(implode($glue, $pieces));
178 * Format a HTML code. This function behaves like `sprintf()`, except that all
179 * the normal conversions (like %s) will be properly escaped.
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)));