Skip to content
Merged
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
18 changes: 18 additions & 0 deletions src/main/php/web/Headers.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* @test web.unittest.HeadersTest
*/
abstract class Headers {
const ATTR_CHAR= 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_';

/**
* Formats a date for use in headers
Expand Down Expand Up @@ -162,6 +163,23 @@ protected function next($input, &$offset) {
// consistency with PHP, see https://github.com/php/php-src/issues/8206
$parameters[$name]= strtr(substr($input, $offset + 1, $p - $offset - 2), ['\"' => '"']);
$offset= $p + 1;
} else if ('*' === $name[strlen($name) - 1]) {

// RFC 8187: Character Encoding and Language for HTTP Header Field Parameters
$s= strcspn($input, "'", $offset);
$charset= substr($input, $offset, $s);
$offset+= $s + 1;

$s= strcspn($input, "'", $offset);
$lang= substr($input, $offset, $s);
$offset+= $s + 1;

$s= strcspn($input, ',;', $offset);
$parameters[$name]= [
'lang' => $lang ?: null,
'value' => iconv($charset, \xp::ENCODING, urldecode(substr($input, $offset, $s)))
];
$offset+= $s + 1;
} else {
$s= strcspn($input, ',;', $offset);
$parameters[$name]= substr($input, $offset, $s);
Expand Down
68 changes: 63 additions & 5 deletions src/main/php/web/Parameterized.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,28 +7,86 @@ class Parameterized {
* Creates a new instance
*
* @param string $value
* @param [:string] $params
* @param [:var] $params
*/
public function __construct($value, array $params) {
public function __construct($value, array $params= []) {
$this->value= $value;
$this->params= $params;
}

/**
* Passes parameters and optionally, their ASCII equivalents.
*
* @param string $name
* @param string|[:string] $value
* @param ?string $equivalent
* @return self
*/
public function with($name, $value, $equivalent= null) {
$name= rtrim($name, '*');
null === $equivalent || $this->params[$name]= $equivalent;

if (is_array($value)) {
$this->params[$name.'*']= 1 === sizeof($value)
? ['lang' => current($value), 'value' => key($value)]
: $value
;
} else if (isset($equivalent) || preg_match('/[\x7f-\xff]/', $value)) {
$this->params[$name.'*']= ['lang' => null, 'value' => $value];
} else {
$this->params[$name]= $value;
}

return $this;
}

/** @return string */
public function value() { return $this->value; }

/** @return [:string] */
/** @return [:var] */
public function params() { return $this->params; }

/**
* Gets a parameter by its name, returning a default value if it's
* not present.
* Gets a parameter by its name, returning a default value if it's not
* present. Prefers the RFC 8187 encoded parameter ending with `*`.
*
* @param string $name
* @param var $default
* @return var
*/
public function param($name, $default= null) {
if ($param= $this->params[$name.'*'] ?? null) {
return $param['value'];
} else {
return $this->params[$name] ?? $default;
}
}

/**
* Gets a parameter equivalent by its name, returning a default value
* if it's not present.
*
* @param string $name
* @param var $default
* @return var
*/
public function equivalent($name, $default= null) {
return $this->params[$name] ?? $default;
}

/** @return string */
public function __toString() {
$s= $this->value;
foreach ($this->params as $name => $value) {
$s.= '; '.$name;
if (is_array($value)) {
$s.= "=UTF-8'{$value['lang']}'".rawurlencode($value['value']);
} else if (strspn($value, Headers::ATTR_CHAR) < strlen($value)) {
$s.= '="'.strtr($value, ['\\' => '\\\\', '"' => '\\"']).'"';
} else {
$s.= '='.$value;
}
}
return $s;
}
}
32 changes: 32 additions & 0 deletions src/test/php/web/unittest/HeadersTest.class.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,38 @@ public function content_disposition($header) {
);
}

#[Test]
public function rfc8187_encoding() {
$parameterized= Headers::parameterized()->parse("attachment; filename*=UTF-8''%C3%BCber%20name.jpg");

Assert::equals('attachment', $parameterized->value());
Assert::equals(['filename*' => ['lang' => null, 'value' => 'über name.jpg']], $parameterized->params());
Assert::equals('über name.jpg', $parameterized->param('filename'));
Assert::null($parameterized->equivalent('filename'));
}

#[Test]
public function rfc8187_encoding_and_language() {
$parameterized= Headers::parameterized()->parse("attachment; filename*=UTF-8'en'file%20name.jpg");

Assert::equals('attachment', $parameterized->value());
Assert::equals(['filename*' => ['lang' => 'en', 'value' => 'file name.jpg']], $parameterized->params());
Assert::equals('file name.jpg', $parameterized->param('filename'));
Assert::null($parameterized->equivalent('filename'));
}

#[Test]
public function rfc8187_encoded_takes_precedence() {
$parameterized= Headers::parameterized()->parse("attachment; filename=\"ascii.jpg\"; filename*=UTF-8''unicode.jpg");

Assert::equals(
['filename' => 'ascii.jpg', 'filename*' => ['lang' => null, 'value' => 'unicode.jpg']],
$parameterized->params()
);
Assert::equals('unicode.jpg', $parameterized->param('filename'));
Assert::equals('ascii.jpg', $parameterized->equivalent('filename'));
}

#[Test, Values(['5;url=http://www.w3.org/pub/WWW/People.html', '5; url=http://www.w3.org/pub/WWW/People.html',])]
public function refresh($header) {
Assert::equals(
Expand Down
Loading
Loading