diff --git a/src/markup/__tests__/PhutilMarkupTestCase.php b/src/markup/__tests__/PhutilMarkupTestCase.php
index 81301cd..33df7d4 100644
--- a/src/markup/__tests__/PhutilMarkupTestCase.php
+++ b/src/markup/__tests__/PhutilMarkupTestCase.php
@@ -1,179 +1,182 @@
assertEqual(
- (string)phutil_tag('x'),
- (string)phutil_tag('x', array()));
+ (string)phutil_tag('br'),
+ (string)phutil_tag('br', array()));
$this->assertEqual(
- (string)phutil_tag('x', array()),
- (string)phutil_tag('x', array(), null));
+ (string)phutil_tag('br', array()),
+ (string)phutil_tag('br', array(), null));
}
public function testTagEmpty() {
$this->assertEqual(
- '',
- (string)phutil_tag('x', array(), null));
+ '
',
+ (string)phutil_tag('br', array(), null));
$this->assertEqual(
- '',
- (string)phutil_tag('x', array(), ''));
- }
+ '
',
+ (string)phutil_tag('div', array(), null));
+ $this->assertEqual(
+ '',
+ (string)phutil_tag('div', array(), ''));
+ }
public function testTagBasics() {
$this->assertEqual(
- '',
- (string)phutil_tag('x'));
+ '
',
+ (string)phutil_tag('br'));
$this->assertEqual(
- 'y',
- (string)phutil_tag('x', array(), 'y'));
+ 'y
',
+ (string)phutil_tag('div', array(), 'y'));
}
public function testTagAttributes() {
$this->assertEqual(
- 'y',
- (string)phutil_tag('x', array('u' => 'v'), 'y'));
+ 'y
',
+ (string)phutil_tag('div', array('u' => 'v'), 'y'));
$this->assertEqual(
- '',
- (string)phutil_tag('x', array('u' => 'v')));
+ '
',
+ (string)phutil_tag('br', array('u' => 'v')));
}
public function testTagEscapes() {
$this->assertEqual(
- '',
- (string)phutil_tag('x', array('u' => '<')));
+ '
',
+ (string)phutil_tag('br', array('u' => '<')));
$this->assertEqual(
- '',
- (string)phutil_tag('x', array(), phutil_tag('y')));
+ '
',
+ (string)phutil_tag('div', array(), phutil_tag('br')));
}
public function testTagNullAttribute() {
$this->assertEqual(
- '',
- (string)phutil_tag('x', array('y' => null)));
+ '
',
+ (string)phutil_tag('br', array('y' => null)));
}
public function testTagJavascriptProtocolRejection() {
$hrefs = array(
'javascript:alert(1)' => true,
'JAVASCRIPT:alert(1)' => true,
' javascript:alert(1)' => true,
'/' => false,
'/path/to/stuff/' => false,
'' => false,
'http://example.com/' => false,
'#' => false,
);
foreach (array(true, false) as $use_uri) {
foreach ($hrefs as $href => $expect) {
if ($use_uri) {
$href = new PhutilURI($href);
}
$caught = null;
try {
phutil_tag('a', array('href' => $href), 'click for candy');
} catch (Exception $ex) {
$caught = $ex;
}
$this->assertEqual(
$expect,
$caught instanceof Exception,
"Rejected href: {$href}");
}
}
}
public function testURIEscape() {
$this->assertEqual(
'%2B/%20%3F%23%26%3A%21xyz%25',
phutil_escape_uri('+/ ?#&:!xyz%'));
}
public function testURIPathComponentEscape() {
$this->assertEqual(
'a%252Fb',
phutil_escape_uri_path_component('a/b'));
$str = '';
for ($ii = 0; $ii <= 255; $ii++) {
$str .= chr($ii);
}
$this->assertEqual(
$str,
phutil_unescape_uri_path_component(
rawurldecode( // Simulates webserver.
phutil_escape_uri_path_component($str))));
}
public function testHsprintf() {
$this->assertEqual(
'<3
',
(string)hsprintf('%s
', '<3'));
}
public function testAppendHTML() {
- $html = phutil_tag('span');
- $html->appendHTML(phutil_tag('em'), '');
- $this->assertEqual('<evil>', $html->getHTMLContent());
+ $html = phutil_tag('hr');
+ $html->appendHTML(phutil_tag('br'), '');
+ $this->assertEqual('
<evil>', $html->getHTMLContent());
}
public function testArrayEscaping() {
$this->assertEqual(
'<div>
',
phutil_escape_html(
array(
hsprintf(''),
array(
array(
'<',
array(
'd',
array(
array(
hsprintf('i'),
),
'v',
),
),
array(
array(
'>',
),
),
),
),
hsprintf('
'),
)));
$this->assertEqual(
- '
',
+ '
',
phutil_tag(
'div',
array(
),
array(
array(
array(
- phutil_tag('x'),
+ phutil_tag('br'),
array(
- phutil_tag('y'),
+ phutil_tag('hr'),
),
- phutil_tag('z'),
+ phutil_tag('wbr'),
),
),
))->getHTMLContent());
}
}
diff --git a/src/markup/render.php b/src/markup/render.php
index 82c4a19..bf0ba24 100644
--- a/src/markup/render.php
+++ b/src/markup/render.php
@@ -1,209 +1,234 @@
getHTMLContent();
}
/**
* @group markup
*/
function phutil_tag($tag, array $attributes = array(), $content = null) {
if (!empty($attributes['href'])) {
// This might be a URI object, so cast it to a string.
$href = (string)$attributes['href'];
// 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 skip the relatively
// expensive preg_match() call if the initial character is '/' (this is the
// case with essentially every URI Phabricator renders).
if (isset($href[0]) &&
($href[0] != '/') &&
preg_match('/^\s*javascript:/i', $href)) {
throw new Exception(
"Attempting to render a tag with an 'href' attribute that begins ".
"with 'javascript:'. This is either a serious security concern or a ".
"serious architecture concern. Seek urgent remedy.");
}
}
+ // For tags which can't self-close, treat null as the empty string -- for
+ // example, always render ``, never ``.
+ static $self_closing_tags = array(
+ 'area' => true,
+ 'base' => true,
+ 'br' => true,
+ 'col' => true,
+ 'command' => true,
+ 'embed' => true,
+ 'hr' => true,
+ 'img' => true,
+ 'input' => true,
+ 'keygen' => true,
+ 'link' => true,
+ 'meta' => true,
+ 'param' => true,
+ 'source' => true,
+ 'track' => true,
+ 'wbr' => true,
+ );
+
+ if ($content === null && empty($self_closing_tags[$tag])) {
+ $content = '';
+ }
+
foreach ($attributes as $k => $v) {
if ($v === null) {
continue;
}
$v = phutil_escape_html($v);
$attributes[$k] = ' '.$k.'="'.$v.'"';
}
$attributes = implode('', $attributes);
if ($content === null) {
return new PhutilSafeHTML('<'.$tag.$attributes.' />');
}
$content = phutil_escape_html($content);
return new PhutilSafeHTML('<'.$tag.$attributes.'>'.$content.''.$tag.'>');
}
/**
* @group markup
*/
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) {
$class = get_class($string);
throw new Exception(
"Object (of class '{$class}') implements ".
"PhutilSafeHTMLProducerInterface but did not return anything ".
"renderable from producePhutilSafeHTML().");
}
}
} else if (is_array($string)) {
return implode('', array_map('phutil_escape_html', $string));
}
return htmlspecialchars($string, ENT_QUOTES, 'UTF-8');
}
/**
* @group markup
*/
function phutil_escape_html_newlines($string) {
return PhutilSafeHTML::applyFunction('nl2br', $string);
}
/**
* Mark string as safe for use in HTML.
*
* @group markup
*/
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().
*
* @group markup
*/
function phutil_implode_html($glue, array $pieces) {
$glue = phutil_escape_html($glue);
$pieces = array_map('phutil_escape_html', $pieces);
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.
*
* @group markup
*/
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 '/'.
*
* @group markup
*/
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.
*
* @group markup
*/
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.
* @group markup
*/
function phutil_unescape_uri_path_component($string) {
return rawurldecode($string);
}