Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/dev-code-quality.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,7 @@ URIScheme - needs to have callable generic checks
news - doesn't validate opaque path
nntp - doesn't constrain path
tel - doesn't validate phone numbers, only allows characters '+', '1-9', and 'x'
sms - doesn't validate phone numbers, only allows '+' and digits; of the
RFC 5724 parameters only 'body' is kept

vim: et sw=4 sts=4
1 change: 1 addition & 0 deletions library/HTMLPurifier.includes.php
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@
require 'HTMLPurifier/URIScheme/mailto.php';
require 'HTMLPurifier/URIScheme/news.php';
require 'HTMLPurifier/URIScheme/nntp.php';
require 'HTMLPurifier/URIScheme/sms.php';
require 'HTMLPurifier/URIScheme/tel.php';
require 'HTMLPurifier/VarParser/Flexible.php';
require 'HTMLPurifier/VarParser/Native.php';
1 change: 1 addition & 0 deletions library/HTMLPurifier.safe-includes.php
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@
require_once $__dir . '/HTMLPurifier/URIScheme/mailto.php';
require_once $__dir . '/HTMLPurifier/URIScheme/news.php';
require_once $__dir . '/HTMLPurifier/URIScheme/nntp.php';
require_once $__dir . '/HTMLPurifier/URIScheme/sms.php';
require_once $__dir . '/HTMLPurifier/URIScheme/tel.php';
require_once $__dir . '/HTMLPurifier/VarParser/Flexible.php';
require_once $__dir . '/HTMLPurifier/VarParser/Native.php';
2 changes: 1 addition & 1 deletion library/HTMLPurifier/ConfigSchema/schema.ser

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ array (
'nntp' => true,
'news' => true,
'tel' => true,
'sms' => true,
)
--DESCRIPTION--
Whitelist that defines the schemes that a URI is allowed to have. This
Expand Down
132 changes: 132 additions & 0 deletions library/HTMLPurifier/URIScheme/sms.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?php

/**
* Validates sms (for text messaging).
*
* The relevant specification for this protocol is RFC 5724, which spells
* the body parameter sms:number?body=message. The sms:number&body=message
* form is common on the web, so we take both and keep whichever was
* written: "&" leaves the body in the path, "?" leaves it in the query.
* Numbers are normalized as in tel, and we drop every parameter but body.
*
* Note we read the body after %URI.AllowedSymbols has been applied, so a
* configuration that drops "&" or "=" from it encodes the delimiters we
* look for and the body goes with them, leaving the number.
*/

class HTMLPurifier_URIScheme_sms extends HTMLPurifier_URIScheme
{
/**
* @type bool
*/
public $browsable = false;

/**
* @type bool
*/
public $may_omit_host = true;

/**
* @param HTMLPurifier_URI $uri
* @param HTMLPurifier_Config $config
* @param HTMLPurifier_Context $context
* @return bool
*/
public function doValidate(&$uri, $config, $context)
{
$authority = $uri->host; // sms://NUMBER hides the recipient here
$uri->userinfo = null;
$uri->host = null;
$uri->port = null;

$phone_number = $uri->path;
$body_content = null;
$body_in_path = false;

// "&" is no query delimiter, so this all lands in the path
if (strpos($phone_number, '&') !== false) {
$parts = explode('&', $phone_number);
$phone_number = array_shift($parts);
$body_content = $this->extractBody($parts);
$body_in_path = !is_null($body_content);
}

// query body wins, so a mixed URI comes out in spec form
if (!is_null($uri->query)) {
$query_body = $this->extractBody(explode('&', $uri->query));
if (!is_null($query_body)) {
$body_content = $query_body;
$body_in_path = false;
}
}

$phone_number = $this->cleanPhoneNumber($phone_number);
if ($phone_number === '' && !is_null($authority)) {
$phone_number = $this->cleanPhoneNumber($authority);
}

// nobody to send it to
if ($phone_number === '') {
$body_content = null;
}

if ($body_content !== null) {
$body_content = $this->sanitizeBody($body_content);
}

// an empty body keeps its parameter
$uri->path = $phone_number;
$uri->query = null;
if (!is_null($body_content)) {
if ($body_in_path) {
$uri->path .= '&body=' . $body_content;
} else {
$uri->query = 'body=' . $body_content;
}
}

return true;
}

/**
* Reduce a recipient to digits, EXCEPT for a leading plus sign.
* @param string $candidate
* @return string
*/
private function cleanPhoneNumber($candidate)
{
return preg_replace('/(?!^\+)[^\d]/', '', rawurldecode($candidate));
}

/**
* First 'body' value out of a list of name=value pairs, or null. RFC 5234
* makes the field name case-insensitive, so we take it in any case.
* @param string[] $params
* @return string|null
*/
private function extractBody($params)
{
foreach ($params as $param) {
if (strpos($param, '=') === false) {
continue;
}
list($param_name, $param_value) = explode('=', $param, 2);
if (strtolower($param_name) === 'body') {
return $param_value;
}
}
return null;
}

/**
* Percent-encode the body so it cannot escape the href; the generator
* escapes it again on output. We decode first so purifying the same URI
* twice does not stack encoding levels.
* @param string $body
* @return string
*/
private function sanitizeBody($body)
{
return rawurlencode(rawurldecode($body));
}
}
12 changes: 12 additions & 0 deletions tests/HTMLPurifier/AttrDef/URITest.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ public function testIntegration()
$this->assertDef('tel:+15555555555');
$this->assertDef('tel:+15555 555 555', 'tel:+15555555555');
$this->assertDef('tel:+15555%20555%20555', 'tel:+15555555555');
$this->assertDef('sms:+15555555555');
$this->assertDef('sms:+15555 555 555', 'sms:+15555555555');
$this->assertDef('sms:+15555%20555%20555', 'sms:+15555555555');
$this->assertDef('sms:5555&body=HOME', 'sms:5555&body=HOME');
$this->assertDef('sms:5555?body=HOME', 'sms:5555?body=HOME');
// an already normalized body is left alone, so purifying twice is stable
$this->assertDef('sms:5555?body=say%20%22hi%22');
// one decode, one encode: a double encoded body keeps its extra level
$this->assertDef('sms:5555?body=%253Cscript%253E');
// non-ASCII survives in both forms
$this->assertDef('sms:5555?body=%E2%9C%93%20ok');
$this->assertDef('sms:5555&body=%E2%9C%93');
}

public function testIntegrationWithPercentEncoder()
Expand Down
9 changes: 9 additions & 0 deletions tests/HTMLPurifier/URIFilter/MakeAbsoluteTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ public function testPreserveAltSchemeWithTel()
$this->assertFiltering('tel:+15555%20555%20555');
}

public function testPreserveAltSchemeWithSms()
{
$this->assertFiltering('sms:+15555555555');
$this->assertFiltering('sms:+15555 555 555');
$this->assertFiltering('sms:+15555%20555%20555');
$this->assertFiltering('sms:5555&body=HOME');
$this->assertFiltering('sms:5555?body=HOME');
}

public function testFilterIgnoreHTTPSpecialCase()
{
$this->assertFiltering('http:/', 'http://example.com/');
Expand Down
23 changes: 23 additions & 0 deletions tests/HTMLPurifier/URIParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,29 @@ public function testTelURI()
);
}

public function testSmsURI()
{
$this->assertParsing(
'sms:+1 (555) 555-5555',
'sms', null, null, null, '+1 (555) 555-5555', null, null
);
$this->assertParsing(
'sms:+1%20(555)%20555-5555',
'sms', null, null, null, '+1%20(555)%20555-5555', null, null
);
// RFC 3986 only treats "?" as the query delimiter, so the common-on-the-web
// sms:NUMBER&body=TEXT form parses entirely into the path. Splitting the
// body out is HTMLPurifier_URIScheme_sms's job, not the generic parser's.
$this->assertParsing(
'sms:5555&body=HOME',
'sms', null, null, null, '5555&body=HOME', null, null
);
$this->assertParsing(
'sms:5555?body=HOME',
'sms', null, null, null, '5555', 'body=HOME', null
);
}

public function testIPv4Address()
{
$this->assertParsing(
Expand Down
Loading
Loading