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