Page MenuHomec4science

render.php
No OneTemporary

File Metadata

Created
Sat, Apr 27, 03:33

render.php

<?php
/**
* Render an HTML tag in a way that treats user content as unsafe by default.
*
* Tag rendering has some special logic which implements security features:
*
* - When rendering `<a>` tags, if the `rel` attribute is not specified, it
* is interpreted as `rel="noreferrer"`.
* - When rendering `<a>` tags, the `href` attribute may not begin with
* `javascript:`.
*
* These special cases can not be disabled.
*
* IMPORTANT: The `$tag` attribute and the keys of the `$attributes` array are
* trusted blindly, and not escaped. You should not pass user data in these
* parameters.
*
* @param string The name of the tag, like `a` or `div`.
* @param map<string, string> A map of tag attributes.
* @param wild Content to put in the tag.
* @return PhutilSafeHTML Tag object.
*/
function phutil_tag($tag, array $attributes = array(), $content = null) {
// If the `href` attribute is present:
// - make sure it is not a "javascript:" URI. We never permit these.
// - if the tag is an `<a>` and the link is to some foreign resource,
// add `rel="nofollow"` by default.
if (!empty($attributes['href'])) {
// This might be a URI object, so cast it to a string.
$href = (string)$attributes['href'];
if (isset($href[0])) {
$is_anchor_href = ($href[0] == '#');
// Is this a link to a resource on the same domain? The second part of
// this excludes "//evil.com/" protocol-relative hrefs. The third part
// of this excludes "/\evil.com/" protocol-relative fantasy hrefs which
// are completely made up but which browsers all respect. Broadly,
// browsers will dutifuly treat "/" followed by ANY sequence of "/" and
// "\" as though it were "//".
$is_domain_href =
($href[0] == '/') &&
(!isset($href[1]) || ($href[1] != '/' && $href[1] != '\\'));
// If the `rel` attribute is not specified, fill in `rel="noreferrer"`.
// Effectively, this serves to make the default behavior for offsite
// links "do not send a referrer", which is broadly desirable. Specifying
// some non-null `rel` will skip this.
if (!isset($attributes['rel'])) {
if (!$is_anchor_href && !$is_domain_href) {
if ($tag == 'a') {
$attributes['rel'] = 'noreferrer';
}
}
}
// Block 'javascript:' hrefs at the tag level: no well-designed
// application should ever use them, and they are a potent attack vector.
// This function is deep in the core and performance sensitive, so we're
// doing a cheap version of this test first to avoid calling preg_match()
// on URIs which begin with '/' or `#`. These cover essentially all URIs
// in Phabricator.
if (!$is_anchor_href && !$is_domain_href) {
// Chrome 33 and IE 11 both interpret "javascript\n:" as a Javascript
// URI, and all browsers interpret " javascript:" as a Javascript URI,
// so be aggressive about looking for "javascript:" in the initial
// section of the string.
$normalized_href = preg_replace('([^a-z0-9/:]+)i', '', $href);
if (preg_match('/^javascript:/i', $normalized_href)) {
throw new Exception(
pht(
"Attempting to render a tag with an '%s' attribute that begins ".
"with '%s'. This is either a serious security concern or a ".
"serious architecture concern. Seek urgent remedy.",
'href',
'javascript:'));
}
}
}
}
// For tags which can't self-close, treat null as the empty string -- for
// example, always render `<div></div>`, never `<div />`.
static $self_closing_tags = array(
'area' => true,
'base' => true,
'br' => true,
'col' => true,
'command' => true,
'embed' => true,
'frame' => true,
'hr' => true,
'img' => true,
'input' => true,
'keygen' => true,
'link' => true,
'meta' => true,
'param' => true,
'source' => true,
'track' => true,
'wbr' => true,
);
$attr_string = '';
foreach ($attributes as $k => $v) {
if ($v === null) {
continue;
}
$v = phutil_escape_html($v);
$attr_string .= ' '.$k.'="'.$v.'"';
}
if ($content === null) {
if (isset($self_closing_tags[$tag])) {
return new PhutilSafeHTML('<'.$tag.$attr_string.' />');
} else {
$content = '';
}
} else {
$content = phutil_escape_html($content);
}
return new PhutilSafeHTML('<'.$tag.$attr_string.'>'.$content.'</'.$tag.'>');
}
function phutil_tag_div($class, $content = null) {
return phutil_tag('div', array('class' => $class), $content);
}
function phutil_escape_html($string) {
if ($string instanceof PhutilSafeHTML) {
return $string;
} else if ($string instanceof PhutilSafeHTMLProducerInterface) {
$result = $string->producePhutilSafeHTML();
if ($result instanceof PhutilSafeHTML) {
return phutil_escape_html($result);
} else if (is_array($result)) {
return phutil_escape_html($result);
} else if ($result instanceof PhutilSafeHTMLProducerInterface) {
return phutil_escape_html($result);
} else {
try {
assert_stringlike($result);
return phutil_escape_html((string)$result);
} catch (Exception $ex) {
throw new Exception(
pht(
"Object (of class '%s') implements %s but did not return anything ".
"renderable from %s.",
get_class($string),
'PhutilSafeHTMLProducerInterface',
'producePhutilSafeHTML()'));
}
}
} else if (is_array($string)) {
$result = '';
foreach ($string as $item) {
$result .= phutil_escape_html($item);
}
return $result;
}
return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
}
function phutil_escape_html_newlines($string) {
return PhutilSafeHTML::applyFunction('nl2br', $string);
}
/**
* Mark string as safe for use in HTML.
*/
function phutil_safe_html($string) {
if ($string == '') {
return $string;
} else if ($string instanceof PhutilSafeHTML) {
return $string;
} else {
return new PhutilSafeHTML($string);
}
}
/**
* HTML safe version of `implode()`.
*/
function phutil_implode_html($glue, array $pieces) {
$glue = phutil_escape_html($glue);
foreach ($pieces as $k => $piece) {
$pieces[$k] = phutil_escape_html($piece);
}
return phutil_safe_html(implode($glue, $pieces));
}
/**
* Format a HTML code. This function behaves like `sprintf()`, except that all
* the normal conversions (like %s) will be properly escaped.
*/
function hsprintf($html /* , ... */) {
$args = func_get_args();
array_shift($args);
return new PhutilSafeHTML(
vsprintf($html, array_map('phutil_escape_html', $args)));
}
/**
* Escape text for inclusion in a URI or a query parameter. Note that this
* method does NOT escape '/', because "%2F" is invalid in paths and Apache
* will automatically 404 the page if it's present. This will produce correct
* (the URIs will work) and desirable (the URIs will be readable) behavior in
* these cases:
*
* '/path/?param='.phutil_escape_uri($string); # OK: Query Parameter
* '/path/to/'.phutil_escape_uri($string); # OK: URI Suffix
*
* It will potentially produce the WRONG behavior in this special case:
*
* COUNTEREXAMPLE
* '/path/to/'.phutil_escape_uri($string).'/thing/'; # BAD: URI Infix
*
* In this case, any '/' characters in the string will not be escaped, so you
* will not be able to distinguish between the string and the suffix (unless
* you have more information, like you know the format of the suffix). For infix
* URI components, use @{function:phutil_escape_uri_path_component} instead.
*
* @param string Some string.
* @return string URI encoded string, except for '/'.
*/
function phutil_escape_uri($string) {
return str_replace('%2F', '/', rawurlencode($string));
}
/**
* Escape text for inclusion as an infix URI substring. See discussion at
* @{function:phutil_escape_uri}. This function covers an unusual special case;
* @{function:phutil_escape_uri} is usually the correct function to use.
*
* This function will escape a string into a format which is safe to put into
* a URI path and which does not contain '/' so it can be correctly parsed when
* embedded as a URI infix component.
*
* However, you MUST decode the string with
* @{function:phutil_unescape_uri_path_component} before it can be used in the
* application.
*
* @param string Some string.
* @return string URI encoded string that is safe for infix composition.
*/
function phutil_escape_uri_path_component($string) {
return rawurlencode(rawurlencode($string));
}
/**
* Unescape text that was escaped by
* @{function:phutil_escape_uri_path_component}. See
* @{function:phutil_escape_uri} for discussion.
*
* Note that this function is NOT the inverse of
* @{function:phutil_escape_uri_path_component}! It undoes additional escaping
* which is added to survive the implied unescaping performed by the webserver
* when interpreting the request.
*
* @param string Some string emitted
* from @{function:phutil_escape_uri_path_component} and
* then accessed via a web server.
* @return string Original string.
*/
function phutil_unescape_uri_path_component($string) {
return rawurldecode($string);
}

Event Timeline