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
11 changes: 5 additions & 6 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,21 @@
# Yii HTTP Change Log


## 1.2.1 under development

- no changes in this release.
- Enh #45: Improve `HeaderValueHelper` methods' annotations (@vjik)

## 1.2.0 November 09, 2021

- New #26: Add `HeaderValueHelper` that has static methods to parse header value parameters (devanych)
- New #26: Add `HeaderValueHelper` that has static methods to parse header value parameters (@devanych)

## 1.1.1 February 10, 2021

- Chg: Update yiisoft/strings dependency (samdark)
- Chg: Update yiisoft/strings dependency (@samdark)

## 1.1.0 December 28, 2020

- Enh #12: Add `Method::ALL` and deprecated `Method::ANY` (samdark)
- Enh #20: Add `ContentDispositionHeader` that generate `Content-Disposition` header name and value (vjik)
- Enh #12: Add `Method::ALL` and deprecated `Method::ANY` (@samdark)
- Enh #20: Add `ContentDispositionHeader` that generate `Content-Disposition` header name and value (@vjik)

## 1.0.0 September 1, 2020

Expand Down
5 changes: 4 additions & 1 deletion psalm.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0"?>
<psalm
errorLevel="2"
errorLevel="1"
findUnusedBaselineEntry="true"
findUnusedCode="false"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
Expand All @@ -13,4 +13,7 @@
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
<issueHandlers>
<MixedAssignment errorLevel="suppress" />
</issueHandlers>
</psalm>
50 changes: 40 additions & 10 deletions src/HeaderValueHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
use function usort;

/**
* HeaderValueHelper parses the header value parameters.
* `HeaderValueHelper` parses the header value parameters.
*
* @psalm-type QFactorHeader = array{q: float}&non-empty-array<array-key, string>
*/
final class HeaderValueHelper
{
Expand Down Expand Up @@ -70,7 +72,7 @@ final class HeaderValueHelper
* @param bool $lowerCaseParameter Whether should cast header parameter name to lowercase.
* @param bool $lowerCaseParameterValue Whether should cast header parameter value to lowercase.
*
* @return array First element is the value, and key-value are the parameters.
* @return string[] First element is the value, and key-value are the parameters.
*/
public static function getValueAndParameters(
string $headerValue,
Expand Down Expand Up @@ -104,7 +106,9 @@ public static function getValueAndParameters(
* @param bool $lowerCaseParameter Whether should cast header parameter name to lowercase.
* @param bool $lowerCaseParameterValue Whether should cast header parameter value to lowercase.
*
* @return array Key-value are the parameters.
* @return string[] Key-value are the parameters.
*
* @psalm-return array<string,string>
*/
public static function getParameters(
string $headerValueParameters,
Expand Down Expand Up @@ -142,6 +146,7 @@ static function (array $matches) use (&$output, $lowerCaseParameter, $lowerCaseP
return;
}

/** @psalm-suppress MixedArrayAssignment False-positive error */
$output[$key] = $lowerCaseParameterValue ? mb_strtolower($value) : $value;
},
$headerValueParameters,
Expand All @@ -153,49 +158,65 @@ static function (array $matches) use (&$output, $lowerCaseParameter, $lowerCaseP
throw new InvalidArgumentException('Invalid input: ' . $headerValueParameters);
}
} while ($headerValueParameters !== '');
/** @var array<string,string> $output */

return $output;
}

/**
* Returns a header value as "q" factor sorted list.
*
* @param mixed $values Header value as a comma-separated string or already exploded string array.
* @link https://developer.mozilla.org/en-US/docs/Glossary/Quality_values
* @link https://www.ietf.org/rfc/rfc2045.html#section-2
* @see getValueAndParameters
*
* @param string|string[] $values Header value as a comma-separated string or already exploded string array.
* @param bool $lowerCaseValue Whether should cast header value to lowercase.
* @param bool $lowerCaseParameter Whether should cast header parameter name to lowercase.
* @param bool $lowerCaseParameterValue Whether should cast header parameter value to lowercase.
*
* @return array The q factor sorted list.
* @return array[] The q factor sorted list.
*
* @link https://developer.mozilla.org/en-US/docs/Glossary/Quality_values
* @link https://www.ietf.org/rfc/rfc2045.html#section-2
* @see getValueAndParameters
* @psalm-return list<QFactorHeader>
* @psalm-suppress MoreSpecificReturnType, LessSpecificReturnStatement Need for Psalm 4.30
*/
public static function getSortedValueAndParameters(
$values,
bool $lowerCaseValue = true,
bool $lowerCaseParameter = true,
bool $lowerCaseParameterValue = true
): array {
/** @var mixed $values Don't trust to annotations. */

if (!is_array($values) && !is_string($values)) {
throw new InvalidArgumentException('Values are neither array nor string.');
}

$list = [];

foreach ((array) $values as $headerValue) {
if (!is_string($headerValue)) {
throw new InvalidArgumentException('Values must be array of strings.');
}

/** @psalm-suppress InvalidOperand Presume that `preg_split` never returns false here. */
$list = [...$list, ...preg_split('/\s*,\s*/', trim($headerValue), -1, PREG_SPLIT_NO_EMPTY)];
}

/** @var string[] $list */

if (count($list) === 0) {
return [];
}

$output = [];

foreach ($list as $value) {
$parse = self::getValueAndParameters($value, $lowerCaseValue, $lowerCaseParameter, $lowerCaseParameterValue);
$parse = self::getValueAndParameters(
$value,
$lowerCaseValue,
$lowerCaseParameter,
$lowerCaseParameterValue
);
// case-insensitive "q" parameter
$q = $parse['q'] ?? $parse['Q'] ?? 1.0;

Expand Down Expand Up @@ -237,12 +258,20 @@ public static function getSortedAcceptTypes($values): array
$output = self::getSortedValueAndParameters($values);

usort($output, static function (array $a, array $b) {
/**
* @psalm-var QFactorHeader $a
* @psalm-var QFactorHeader $b
*/

if ($a['q'] !== $b['q']) {
// The higher q value wins
return $a['q'] > $b['q'] ? -1 : 1;
}

/** @var string $typeA */
$typeA = reset($a);

/** @var string $typeB */
$typeB = reset($b);

if (strpos($typeA, '*') === false && strpos($typeB, '*') === false) {
Expand Down Expand Up @@ -285,6 +314,7 @@ public static function getSortedAcceptTypes($values): array
asort($value, SORT_STRING);
$output[$key] = $type . ';' . implode(';', $value);
}
/** @var string[] $output */

return $output;
}
Expand Down
24 changes: 24 additions & 0 deletions tests/HeaderValueHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
use stdClass;
use Yiisoft\Http\HeaderValueHelper;

final class HeaderValueHelperTest extends TestCase
Expand Down Expand Up @@ -65,6 +66,29 @@ public function testSortedValuesAndParameters($input, array $expected): void
$this->assertSame($expected, HeaderValueHelper::getSortedValueAndParameters($input));
}

public function dataSortedValuesAndParametersInvalidValue(): array
{
$notArrayAndNotString = 'Values are neither array nor string.';
$notArrayOfStrings = 'Values must be array of strings.';

return [
'object' => [new stdClass(), $notArrayAndNotString],
'int' => [7, $notArrayAndNotString],
'bool' => [true, $notArrayAndNotString],
'array-of-int' => [[1, 2, 3], $notArrayOfStrings],
];
}

/**
* @dataProvider dataSortedValuesAndParametersInvalidValue
*/
public function testSortedValuesAndParametersInvalidValue($value, string $message): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage($message);
HeaderValueHelper::getSortedValueAndParameters($value);
}

public function qFactorSortFailDataProvider(): array
{
return [
Expand Down