Merge branch 'yiisoft:master' into master

This commit is contained in:
Roberto Braga
2023-09-22 09:06:09 +02:00
committed by GitHub
40 changed files with 951 additions and 2372 deletions

View File

@ -373,7 +373,7 @@ $query->orderBy([
```
В данном коде, ключи массива - это имена столбцов, а значения массива - это соответствующее направление сортировки.
PHP константа `SORT_ASC` определяет сортировку по возрастанию и `SORT_DESC` сортировка по умолчанию.
PHP константа `SORT_ASC` определяет сортировку по возрастанию и `SORT_DESC` сортировку по убыванию.
Если `ORDER BY` содержит только простые имена столбцов, вы можете определить их с помощью столбцов, также
как и при написании обычного SQL. Например,

View File

@ -10,7 +10,7 @@
В конструкторе приложения происходит следующий процесс предзагрузки:
1. Вызывается метод [[yii\base\Application::preInit()|preInit()]], которые конфигурирует свойства приложения, имеющие
1. Вызывается метод [[yii\base\Application::preInit()|preInit()]], который конфигурирует свойства приложения, имеющие
наивысший приоритет, такие как [[yii\base\Application::basePath|basePath]];
2. Регистрируется [[yii\base\Application::errorHandler|обработчик ошибок]];
3. Происходит инициализация свойств приложения согласно заданной конфигурации;

View File

@ -263,6 +263,12 @@ Further reading on the topic:
- <https://owasp.org/www-community/SameSite>
Avoiding arbitrary object instantiations
----------------------------------------
Yii [configurations](concept-configurations.md) are associative arrays used by the framework to instantiate new objects through `Yii::createObject($config)`. These arrays specify the class name for instantiation, and it is important to ensure that this class name does not originate from untrusted sources. Otherwise, it can lead to Unsafe Reflection, a vulnerability that allows the execution of malicious code by exploiting the loading of specific classes. Additionally, when you need to dynamically add keys to an object derived from a framework class, such as the base `Component` class, it's essential to validate these dynamic properties using a whitelist approach. This precaution is necessary because the framework might employ `Yii::createObject($config)` within the `__set()` magic method.
Avoiding file exposure
----------------------

View File

@ -68,7 +68,7 @@ class BaseYii
*/
public static $classMap = [];
/**
* @var \yii\console\Application|\yii\web\Application|\yii\base\Application the application instance
* @var \yii\console\Application|\yii\web\Application the application instance
*/
public static $app;
/**
@ -93,7 +93,7 @@ class BaseYii
*/
public static function getVersion()
{
return '2.0.49-dev';
return '2.0.50-dev';
}
/**

View File

@ -1,15 +1,34 @@
Yii Framework 2 Change Log
==========================
2.0.49 under development
2.0.50 under development
------------------------
- Bug #19857: Fix AttributeTypecastBehavior::resetOldAttributes() causes "class has no attribute named" InvalidArgumentException (uaoleg)
- Bug #19925: Improved PHP version check when handling MIME types (schmunk42)
- Bug #19940: File Log writer without newline (terabytesoftw)
- Bug #19951: Removed unneeded MIME file tests (schmunk42)
- Bug #19950: Fix `Query::groupBy(null)` causes error for PHP 8.1: `trim(): Passing null to parameter #1 ($string) of type string is deprecated` (uaoleg)
2.0.49 August 29, 2023
----------------------
- Bug #9899: Fix caching a MSSQL query with BLOB data type (terabytesoftw)
- Bug #16208: Fix `yii\log\FileTarget` to not export empty messages (terabytesoftw)
- Bug #18859: Fix `yii\web\Controller::bindInjectedParams()` to not throw error when argument of `ReflectionUnionType` type is passed (bizley)
- Bug #19857: Fix AttributeTypecastBehavior::resetOldAttributes() causes "class has no attribute named" InvalidArgumentException (uaoleg)
- Bug #19868: Added whitespace sanitation for tests, due to updates in ICU 72 (schmunk42)
- Bug #19872: Fixed the definition of dirty attributes in AR properties for a non-associative array in case of changing the order of elements (eegusakov)
- Bug #19899: Fixed `GridView` in some cases calling `Model::generateAttributeLabel()` to generate label values that are never used (PowerGamer1)
- Bug #19906: Fixed multiline strings in the `\yii\console\widgets\Table` widget (rhertogh)
- Bug #19908: Fix associative array cell content rendering in Table widget (rhertogh)
- Bug #19911: Resolved inconsistency in `ActiveRecord::getAttributeLabel()` with regard of overriding in primary model labels for attributes of related model in favor of allowing such overriding for all levels of relation nesting (PowerGamer1)
- Bug #19914: Fixed `ArrayHelper::keyExists()` and `::remove()` functions when the key is a float and the value is `null` (rhertogh)
- Bug #19924: Fix `yii\i18n\Formatter` to not throw error `Unknown named parameter` under PHP 8 (arollmann)
- Enh #19841: Allow jQuery 3.7 to be installed (wouter90)
- Enh #19853: Added support for default value for `\yii\helpers\Console::select()` (rhertogh)
- Bug #19868: Added whitespace sanitation for tests, due to updates in ICU 72 (schmunk42)
- Enh #19884: Added support Enums in Query Builder (sk1t0n)
- Enh #19920: Broadened the accepted type of `Cookie::$expire` from `int` to `int|string|\DateTimeInterface|null` (rhertogh)
2.0.48.1 May 24, 2023

View File

@ -100,6 +100,11 @@ Upgrade from Yii 2.0.45
2.0.45 behavior, [introduce your own method](https://github.com/yiisoft/yii2/pull/19495/files).
* `yii\log\FileTarget::$rotateByCopy` is now deprecated and setting it to `false` has no effect since rotating of
the files is done only by copy.
* `yii\validators\UniqueValidator` and `yii\validators\ExistValidator`, when used on multiple attributes, now only
generate an error on a single attribute. Previously, they would report a separate error on each attribute.
Old behavior can be achieved by setting `'skipOnError' => false`, but this might have undesired side effects with
additional validators on one of the target attributes.
See [issue #19407](https://github.com/yiisoft/yii2/issues/19407)
Upgrade from Yii 2.0.44
-----------------------

View File

@ -317,7 +317,7 @@ class DbCache extends Cache
*/
protected function getDataFieldName()
{
return $this->isVarbinaryDataField() ? 'convert(nvarchar(max),[data]) data' : 'data';
return $this->isVarbinaryDataField() ? 'CONVERT(VARCHAR(MAX), [[data]]) data' : 'data';
}
/**

View File

@ -136,7 +136,11 @@ class Table extends Widget
{
$this->rows = array_map(function($row) {
return array_map(function($value) {
return empty($value) && !is_numeric($value) ? ' ' : $value;
return empty($value) && !is_numeric($value)
? ' '
: (is_array($value)
? array_values($value)
: $value);
}, array_values($row));
}, $rows);
return $this;
@ -252,18 +256,32 @@ class Table extends Widget
if ($index !== 0) {
$buffer .= $spanMiddle . ' ';
}
$arrayFromMultilineString = false;
if (is_string($cell)) {
$cellLines = explode(PHP_EOL, $cell);
if (count($cellLines) > 1) {
$cell = $cellLines;
$arrayFromMultilineString = true;
}
}
if (is_array($cell)) {
if (empty($renderedChunkTexts[$index])) {
$renderedChunkTexts[$index] = '';
$start = 0;
$prefix = $this->listPrefix;
$prefix = $arrayFromMultilineString ? '' : $this->listPrefix;
if (!isset($arrayPointer[$index])) {
$arrayPointer[$index] = 0;
}
} else {
$start = mb_strwidth($renderedChunkTexts[$index], Yii::$app->charset);
}
$chunk = Console::ansiColorizedSubstr($cell[$arrayPointer[$index]], $start, $cellSize - 4);
$chunk = Console::ansiColorizedSubstr(
$cell[$arrayPointer[$index]],
$start,
$cellSize - 2 - Console::ansiStrwidth($prefix)
);
$renderedChunkTexts[$index] .= Console::stripAnsiFormat($chunk);
$fullChunkText = Console::stripAnsiFormat($cell[$arrayPointer[$index]]);
if (isset($cell[$arrayPointer[$index] + 1]) && $renderedChunkTexts[$index] === $fullChunkText) {
@ -339,6 +357,9 @@ class Table extends Widget
if (is_array($val)) {
return max(array_map('yii\helpers\Console::ansiStrwidth', $val)) + Console::ansiStrwidth($this->listPrefix);
}
if (is_string($val)) {
return max(array_map('yii\helpers\Console::ansiStrwidth', explode(PHP_EOL, $val)));
}
return Console::ansiStrwidth($val);
}, $column)) + 2;
$this->columnWidths[] = $columnWidth;
@ -388,6 +409,9 @@ class Table extends Widget
if (is_array($val)) {
return array_map('yii\helpers\Console::ansiStrwidth', $val);
}
if (is_string($val)) {
return array_map('yii\helpers\Console::ansiStrwidth', explode(PHP_EOL, $val));
}
return Console::ansiStrwidth($val);
}, $row));
return max($rowsPerCell);

View File

@ -183,15 +183,11 @@ class ActiveDataProvider extends BaseDataProvider
$sort->attributes[$attribute] = [
'asc' => [$attribute => SORT_ASC],
'desc' => [$attribute => SORT_DESC],
'label' => $model->getAttributeLabel($attribute),
];
}
} else {
foreach ($sort->attributes as $attribute => $config) {
if (!isset($config['label'])) {
$sort->attributes[$attribute]['label'] = $model->getAttributeLabel($attribute);
}
}
}
if ($sort->modelClass === null) {
$sort->modelClass = $modelClass;
}
}
}

View File

@ -191,6 +191,12 @@ class Sort extends BaseObject
* @since 2.0.33
*/
public $sortFlags = SORT_REGULAR;
/**
* @var string|null the name of the [[\yii\base\Model]]-based class used by the [[link()]] method to retrieve
* attributes' labels. See [[link]] method for details.
* @since 2.0.49
*/
public $modelClass;
/**
@ -363,7 +369,8 @@ class Sort extends BaseObject
* @param array $options additional HTML attributes for the hyperlink tag.
* There is one special attribute `label` which will be used as the label of the hyperlink.
* If this is not set, the label defined in [[attributes]] will be used.
* If no label is defined, [[\yii\helpers\Inflector::camel2words()]] will be called to get a label.
* If no label is defined, it will be retrieved from the instance of [[modelClass]] (if [[modelClass]] is not null)
* or generated from attribute name using [[\yii\helpers\Inflector::camel2words()]].
* Note that it will not be HTML-encoded.
* @return string the generated hyperlink
* @throws InvalidConfigException if the attribute is unknown
@ -388,6 +395,11 @@ class Sort extends BaseObject
} else {
if (isset($this->attributes[$attribute]['label'])) {
$label = $this->attributes[$attribute]['label'];
} elseif ($this->modelClass !== null) {
$modelClass = $this->modelClass;
/** @var \yii\base\Model $model */
$model = $modelClass::instance();
$label = $model->getAttributeLabel($attribute);
} else {
$label = Inflector::camel2words($attribute);
}

View File

@ -144,6 +144,7 @@ interface ActiveRecordInterface extends StaticInstanceInterface
* // Use where() to ignore the default condition
* // SELECT FROM customer WHERE age>30
* $customers = Customer::find()->where('age>30')->all();
* ```
*
* @return ActiveQueryInterface the newly created [[ActiveQueryInterface]] instance.
*/

View File

@ -282,7 +282,7 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
*/
public function __get($name)
{
if (isset($this->_attributes[$name]) || array_key_exists($name, $this->_attributes)) {
if (array_key_exists($name, $this->_attributes)) {
return $this->_attributes[$name];
}
@ -290,7 +290,7 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
return null;
}
if (isset($this->_related[$name]) || array_key_exists($name, $this->_related)) {
if (array_key_exists($name, $this->_related)) {
return $this->_related[$name];
}
$value = parent::__get($name);
@ -1610,40 +1610,46 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
/**
* Returns the text label for the specified attribute.
* If the attribute looks like `relatedModel.attribute`, then the attribute will be received from the related model.
* The attribute may be specified in a dot format to retrieve the label from related model or allow this model to override the label defined in related model.
* For example, if the attribute is specified as 'relatedModel1.relatedModel2.attr' the function will return the first label definition it can find
* in the following order:
* - the label for 'relatedModel1.relatedModel2.attr' defined in [[attributeLabels()]] of this model;
* - the label for 'relatedModel2.attr' defined in related model represented by relation 'relatedModel1' of this model;
* - the label for 'attr' defined in related model represented by relation 'relatedModel2' of relation 'relatedModel1'.
* If no label definition was found then the value of $this->generateAttributeLabel('relatedModel1.relatedModel2.attr') will be returned.
* @param string $attribute the attribute name
* @return string the attribute label
* @see generateAttributeLabel()
* @see attributeLabels()
* @see generateAttributeLabel()
*/
public function getAttributeLabel($attribute)
{
$labels = $this->attributeLabels();
if (isset($labels[$attribute])) {
return $labels[$attribute];
} elseif (strpos($attribute, '.')) {
$attributeParts = explode('.', $attribute);
$neededAttribute = array_pop($attributeParts);
$relatedModel = $this;
foreach ($attributeParts as $relationName) {
if ($relatedModel->isRelationPopulated($relationName) && $relatedModel->$relationName instanceof self) {
$relatedModel = $relatedModel->$relationName;
} else {
try {
$relation = $relatedModel->getRelation($relationName);
} catch (InvalidParamException $e) {
return $this->generateAttributeLabel($attribute);
}
/* @var $modelClass ActiveRecordInterface */
$modelClass = $relation->modelClass;
$relatedModel = $modelClass::instance();
}
$model = $this;
$modelAttribute = $attribute;
for (;;) {
$labels = $model->attributeLabels();
if (isset($labels[$modelAttribute])) {
return $labels[$modelAttribute];
}
$labels = $relatedModel->attributeLabels();
if (isset($labels[$neededAttribute])) {
return $labels[$neededAttribute];
$parts = explode('.', $modelAttribute, 2);
if (count($parts) < 2) {
break;
}
list ($relationName, $modelAttribute) = $parts;
if ($model->isRelationPopulated($relationName) && $model->$relationName instanceof self) {
$model = $model->$relationName;
} else {
try {
$relation = $model->getRelation($relationName);
} catch (InvalidArgumentException $e) {
break;
}
/* @var $modelClass ActiveRecordInterface */
$modelClass = $relation->modelClass;
$model = $modelClass::instance();
}
}
@ -1774,7 +1780,7 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
*/
private function isValueDifferent($newValue, $oldValue)
{
if (is_array($newValue) && is_array($oldValue) && !ArrayHelper::isAssociative($oldValue)) {
if (is_array($newValue) && is_array($oldValue) && ArrayHelper::isAssociative($oldValue)) {
$newValue = ArrayHelper::recursiveSort($newValue);
$oldValue = ArrayHelper::recursiveSort($oldValue);
}

View File

@ -322,7 +322,7 @@ class Migration extends Component implements MigrationInterface
* // ...
* 'column_name double precision null default null',
* ```
*
*
* @param string $table the name of the table to be created. The name will be properly quoted by the method.
* @param array $columns the columns (name => definition) in the new table.

View File

@ -1049,7 +1049,7 @@ PATTERN;
/**
* Sets the GROUP BY part of the query.
* @param string|array|ExpressionInterface $columns the columns to be grouped by.
* @param string|array|ExpressionInterface|null $columns the columns to be grouped by.
* Columns can be specified in either a string (e.g. "id, name") or an array (e.g. ['id', 'name']).
* The method will automatically quote the column names unless a column contains some parenthesis
* (which means the column contains a DB expression).
@ -1067,7 +1067,7 @@ PATTERN;
{
if ($columns instanceof ExpressionInterface) {
$columns = [$columns];
} elseif (!is_array($columns)) {
} elseif (!is_array($columns) && !is_null($columns)) {
$columns = preg_split('/\s*,\s*/', trim($columns), -1, PREG_SPLIT_NO_EMPTY);
}
$this->groupBy = $columns;

View File

@ -460,10 +460,9 @@ class QueryBuilder extends \yii\db\QueryBuilder
$columnSchemas = $tableSchema->columns;
foreach ($columns as $name => $value) {
// @see https://github.com/yiisoft/yii2/issues/12599
if (isset($columnSchemas[$name]) && $columnSchemas[$name]->type === Schema::TYPE_BINARY && $columnSchemas[$name]->dbType === 'varbinary' && (is_string($value) || $value === null)) {
$phName = $this->bindParam($value, $params);
if (isset($columnSchemas[$name]) && $columnSchemas[$name]->type === Schema::TYPE_BINARY && $columnSchemas[$name]->dbType === 'varbinary' && (is_string($value))) {
// @see https://github.com/yiisoft/yii2/issues/12599
$columns[$name] = new Expression("CONVERT(VARBINARY(MAX), $phName)", $params);
$columns[$name] = new Expression('CONVERT(VARBINARY(MAX), ' . ('0x' . bin2hex($value)) . ')');
}
}
}

View File

@ -327,7 +327,12 @@ class BaseArrayHelper
*/
public static function remove(&$array, $key, $default = null)
{
if (is_array($array) && (isset($array[$key]) || array_key_exists($key, $array))) {
// ToDo: This check can be removed when the minimum PHP version is >= 8.1 (Yii2.2)
if (is_float($key)) {
$key = (int)$key;
}
if (is_array($array) && array_key_exists($key, $array)) {
$value = $array[$key];
unset($array[$key]);
@ -608,17 +613,20 @@ class BaseArrayHelper
* Checks if the given array contains the specified key.
* This method enhances the `array_key_exists()` function by supporting case-insensitive
* key comparison.
* @param string $key the key to check
* @param string|int $key the key to check
* @param array|ArrayAccess $array the array with keys to check
* @param bool $caseSensitive whether the key comparison should be case-sensitive
* @return bool whether the array contains the specified key
*/
public static function keyExists($key, $array, $caseSensitive = true)
{
// ToDo: This check can be removed when the minimum PHP version is >= 8.1 (Yii2.2)
if (is_float($key)) {
$key = (int)$key;
}
if ($caseSensitive) {
// Function `isset` checks key faster but skips `null`, `array_key_exists` handles this case
// https://www.php.net/manual/en/function.array-key-exists.php#107786
if (is_array($array) && (isset($array[$key]) || array_key_exists($key, $array))) {
if (is_array($array) && array_key_exists($key, $array)) {
return true;
}
// Cannot use `array_has_key` on Objects for PHP 7.4+, therefore we need to check using [[ArrayAccess::offsetExists()]]

View File

@ -299,6 +299,7 @@ return [
'application/vnd.fuzzysheet' => 'fzs',
'application/vnd.genomatix.tuxedo' => 'txd',
'application/vnd.geogebra.file' => 'ggb',
'application/vnd.geogebra.slides' => 'ggs',
'application/vnd.geogebra.tool' => 'ggt',
'application/vnd.geometry-explorer' => [
'gex',
@ -655,6 +656,7 @@ return [
],
'application/vnd.zzazz.deck+xml' => 'zaz',
'application/voicexml+xml' => 'vxml',
'application/wasm' => 'wasm',
'application/widget' => 'wgt',
'application/winhlp' => 'hlp',
'application/wsdl+xml' => 'wsdl',

View File

@ -282,6 +282,7 @@ $mimeTypes = [
'geo' => 'application/vnd.dynageo',
'gex' => 'application/vnd.geometry-explorer',
'ggb' => 'application/vnd.geogebra.file',
'ggs' => 'application/vnd.geogebra.slides',
'ggt' => 'application/vnd.geogebra.tool',
'ghf' => 'application/vnd.groove-help',
'gif' => 'image/gif',
@ -887,6 +888,7 @@ $mimeTypes = [
'vxml' => 'application/voicexml+xml',
'w3d' => 'application/x-director',
'wad' => 'application/x-doom',
'wasm' => 'application/wasm',
'wav' => 'audio/x-wav',
'wax' => 'audio/x-ms-wax',
'wbmp' => 'image/vnd.wap.wbmp',
@ -1001,7 +1003,8 @@ $mimeTypes = [
'zmm' => 'application/vnd.handheld-entertainment+xml',
];
if (PHP_VERSION_ID >= 80100) {
# fix for bundled libmagic bug, see also https://github.com/yiisoft/yii2/issues/19925
if ((PHP_VERSION_ID >= 80100 && PHP_VERSION_ID < 80122) || (PHP_VERSION_ID >= 80200 && PHP_VERSION_ID < 80209)) {
$mimeTypes = array_replace($mimeTypes, array('xz' => 'application/octet-stream'));
}

View File

@ -460,7 +460,7 @@ class Formatter extends Component
}
$method = 'as' . $format;
if ($this->hasMethod($method)) {
return call_user_func_array([$this, $method], $params);
return call_user_func_array([$this, $method], array_values($params));
}
throw new InvalidArgumentException("Unknown format type: $format");

View File

@ -106,12 +106,18 @@ class FileTarget extends Target
*/
public function export()
{
$text = implode("\n", array_map([$this, 'formatMessage'], $this->messages)) . "\n";
$trimmedText = trim($text);
if (empty($trimmedText)) {
return; // No messages to export, so we exit the function early
}
if (strpos($this->logFile, '://') === false || strncmp($this->logFile, 'file://', 7) === 0) {
$logPath = dirname($this->logFile);
FileHelper::createDirectory($logPath, $this->dirMode, true);
}
$text = implode("\n", array_map([$this, 'formatMessage'], $this->messages)) . "\n";
if (($fp = @fopen($this->logFile, 'a')) === false) {
throw new InvalidConfigException("Unable to append to log file: {$this->logFile}");
}

View File

@ -57,8 +57,8 @@ class Cookie extends \yii\base\BaseObject
*/
public $domain = '';
/**
* @var int the timestamp at which the cookie expires. This is the server timestamp.
* Defaults to 0, meaning "until the browser is closed".
* @var int|string|\DateTimeInterface|null the timestamp or date at which the cookie expires. This is the server timestamp.
* Defaults to 0, meaning "until the browser is closed" (the same applies to `null`).
*/
public $expire = 0;
/**

View File

@ -51,7 +51,7 @@ class CookieCollection extends BaseObject implements \IteratorAggregate, \ArrayA
* Returns an iterator for traversing the cookies in the collection.
* This method is required by the SPL interface [[\IteratorAggregate]].
* It will be implicitly called when you use `foreach` to traverse the collection.
* @return ArrayIterator an iterator for traversing the cookies in the collection.
* @return ArrayIterator<string, Cookie> an iterator for traversing the cookies in the collection.
*/
#[\ReturnTypeWillChange]
public function getIterator()
@ -113,7 +113,18 @@ class CookieCollection extends BaseObject implements \IteratorAggregate, \ArrayA
public function has($name)
{
return isset($this->_cookies[$name]) && $this->_cookies[$name]->value !== ''
&& ($this->_cookies[$name]->expire === null || $this->_cookies[$name]->expire === 0 || $this->_cookies[$name]->expire >= time());
&& ($this->_cookies[$name]->expire === null
|| $this->_cookies[$name]->expire === 0
|| (
(is_string($this->_cookies[$name]->expire) && strtotime($this->_cookies[$name]->expire) >= time())
|| (
interface_exists('\\DateTimeInterface')
&& $this->_cookies[$name]->expire instanceof \DateTimeInterface
&& $this->_cookies[$name]->expire->getTimestamp() >= time()
)
|| $this->_cookies[$name]->expire >= time()
)
);
}
/**
@ -174,7 +185,7 @@ class CookieCollection extends BaseObject implements \IteratorAggregate, \ArrayA
/**
* Returns the collection as a PHP array.
* @return array the array representation of the collection.
* @return Cookie[] the array representation of the collection.
* The array keys are cookie names, and the array values are the corresponding cookie objects.
*/
public function toArray()

View File

@ -401,12 +401,21 @@ class Response extends \yii\base\Response
}
foreach ($this->getCookies() as $cookie) {
$value = $cookie->value;
if ($cookie->expire != 1 && isset($validationKey)) {
$expire = $cookie->expire;
if (is_string($expire)) {
$expire = strtotime($expire);
} elseif (interface_exists('\\DateTimeInterface') && $expire instanceof \DateTimeInterface) {
$expire = $expire->getTimestamp();
}
if ($expire === null || $expire === false) {
$expire = 0;
}
if ($expire != 1 && isset($validationKey)) {
$value = Yii::$app->getSecurity()->hashData(serialize([$cookie->name, $value]), $validationKey);
}
if (PHP_VERSION_ID >= 70300) {
setcookie($cookie->name, $value, [
'expires' => $cookie->expire,
'expires' => $expire,
'path' => $cookie->path,
'domain' => $cookie->domain,
'secure' => $cookie->secure,
@ -420,7 +429,7 @@ class Response extends \yii\base\Response
if (!is_null($cookie->sameSite)) {
$cookiePath .= '; samesite=' . $cookie->sameSite;
}
setcookie($cookie->name, $value, $cookie->expire, $cookiePath, $cookie->domain, $cookie->secure, $cookie->httpOnly);
setcookie($cookie->name, $value, $expire, $cookiePath, $cookie->domain, $cookie->secure, $cookie->httpOnly);
}
}
}

View File

@ -405,7 +405,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co
* 'sameSite' => PHP_VERSION_ID >= 70300 ? yii\web\Cookie::SAME_SITE_LAX : null,
* ]
* ```
* See https://www.owasp.org/index.php/SameSite for more information about `sameSite`.
* See https://owasp.org/www-community/SameSite for more information about `sameSite`.
*
* @throws InvalidArgumentException if the parameters are incomplete.
* @see https://www.php.net/manual/en/function.session-set-cookie-params.php

View File

@ -0,0 +1,27 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yiiunit\data\ar;
/**
* @property int $attr1
* @property int $attr2
*/
class NoAutoLabels extends ActiveRecord
{
public function attributeLabels()
{
return [
'attr1' => 'Label for attr1',
];
}
public function generateAttributeLabel($name)
{
throw new \yii\base\InvalidArgumentException('Label not defined!');
}
}

View File

@ -58,6 +58,121 @@ class TableTest extends TestCase
║ testcontent21 │ testcontent22 │ testcontent23 ║
╚═══════════════╧═══════════════╧═══════════════╝
EXPECTED;
$tableContent = $table
->setHeaders($headers)
->setRows($rows)
->setScreenWidth(200)
->run();
$this->assertEqualsWithoutLE($expected, $tableContent);
}
public function getMultiLineTableData()
{
return [
[
['test1', 'test2', 'test3' . PHP_EOL . 'multiline'],
[
['test' . PHP_EOL . 'content1', 'testcontent2', 'test' . PHP_EOL . 'content3'],
[
'testcontent21',
'testcontent22' . PHP_EOL
. 'loooooooooooooooooooooooooooooooooooong' . PHP_EOL
. 'content',
'testcontent23' . PHP_EOL
. 'loooooooooooooooooooooooooooooooooooong content'
],
]
],
[
['key1' => 'test1', 'key2' => 'test2', 'key3' => 'test3' . PHP_EOL . 'multiline'],
[
[
'key1' => 'test' . PHP_EOL . 'content1',
'key2' => 'testcontent2',
'key3' => 'test' . PHP_EOL . 'content3'
],
[
'key1' => 'testcontent21',
'key2' => 'testcontent22' . PHP_EOL
. 'loooooooooooooooooooooooooooooooooooong' . PHP_EOL
. 'content',
'key3' => 'testcontent23' . PHP_EOL
. 'loooooooooooooooooooooooooooooooooooong content'
],
]
]
];
}
/**
* @dataProvider getMultiLineTableData
*/
public function testMultiLineTable($headers, $rows)
{
$table = new Table();
$expected = <<<'EXPECTED'
╔═════════════╤═════════════════════════════════════╤═════════════════════════════════════════════╗
║ test1 │ test2 │ test3 ║
║ │ │ multiline ║
╟─────────────┼─────────────────────────────────────┼─────────────────────────────────────────────╢
║ test │ testcontent2 │ test ║
║ content1 │ │ content3 ║
╟─────────────┼─────────────────────────────────────┼─────────────────────────────────────────────╢
║ testcontent │ testcontent22 │ testcontent23 ║
║ 21 │ loooooooooooooooooooooooooooooooooo │ loooooooooooooooooooooooooooooooooooong con ║
║ │ oong │ tent ║
║ │ content │ ║
╚═════════════╧═════════════════════════════════════╧═════════════════════════════════════════════╝
EXPECTED;
$tableContent = $table
->setHeaders($headers)
->setRows($rows)
->setScreenWidth(100)
->run();
$this->assertEqualsWithoutLE($expected, $tableContent);
}
public function getNumericTableData()
{
return [
[
[1, 2, 3],
[
[1, 1.2, -1.3],
[-2, 2.2, 2.3],
]
],
[
['key1' => 1, 'key2' => 2, 'key3' => 3],
[
['key1' => 1, 'key2' => 1.2, 'key3' => -1.3],
['key1' => -2, 'key2' => 2.2, 'key3' => 2.3],
]
]
];
}
/**
* @dataProvider getNumericTableData
*/
public function testNumericTable($headers, $rows)
{
$table = new Table();
$expected = <<<'EXPECTED'
╔════╤═════╤══════╗
║ 1 │ 2 │ 3 ║
╟────┼─────┼──────╢
║ 1 │ 1.2 │ -1.3 ║
╟────┼─────┼──────╢
║ -2 │ 2.2 │ 2.3 ║
╚════╧═════╧══════╝
EXPECTED;
$tableContent = $table
@ -101,7 +216,8 @@ EXPECTED;
╔═══════════════╤═══════════════╤══════════════╗
║ test1 │ test2 │ test3 ║
╟───────────────┼───────────────┼──────────────╢
testcontent1 │ testcontent2 │ testcontent3 ║
• col1 │ testcontent2 │ testcontent3 ║
║ • col2 │ │ ║
╟───────────────┼───────────────┼──────────────╢
║ testcontent21 │ testcontent22 │ • col1 ║
║ │ │ • col2 ║
@ -111,7 +227,7 @@ EXPECTED;
$this->assertEqualsWithoutLE($expected, $table->setHeaders(['test1', 'test2', 'test3'])
->setRows([
['testcontent1', 'testcontent2', 'testcontent3'],
[['key1' => 'col1', 'key2' => 'col2'], 'testcontent2', 'testcontent3'],
['testcontent21', 'testcontent22', ['col1', 'col2']],
])->setScreenWidth(200)->run()
);
@ -141,6 +257,35 @@ EXPECTED;
);
}
public function testLongerListPrefix()
{
$table = new Table();
$expected = <<<'EXPECTED'
╔═════════════════════════════════╤═════════════════════════════════╤═════════════════════════════╗
║ test1 │ test2 │ test3 ║
╟─────────────────────────────────┼─────────────────────────────────┼─────────────────────────────╢
║ testcontent1 │ testcontent2 │ testcontent3 ║
╟─────────────────────────────────┼─────────────────────────────────┼─────────────────────────────╢
║ testcontent21 with looooooooooo │ testcontent22 with looooooooooo │ -- col1 with looooooooooooo ║
║ ooooooooooooong content │ ooooooooooooong content │ ooooooooooong content ║
║ │ │ -- col2 with long content ║
╚═════════════════════════════════╧═════════════════════════════════╧═════════════════════════════╝
EXPECTED;
$this->assertEqualsWithoutLE($expected, $table->setHeaders(['test1', 'test2', 'test3'])
->setRows([
['testcontent1', 'testcontent2', 'testcontent3'],
[
'testcontent21 with loooooooooooooooooooooooong content',
'testcontent22 with loooooooooooooooooooooooong content',
['col1 with loooooooooooooooooooooooong content', 'col2 with long content']
],
])->setScreenWidth(100)->setListPrefix('-- ')->run()
);
}
public function testCustomChars()
{
$table = new Table();

View File

@ -2192,4 +2192,87 @@ abstract class ActiveRecordTest extends DatabaseTestCase
$this->assertNotNull($order->virtualCustomer);
}
public function labelTestModelProvider()
{
$data = [];
// Model 2 and 3 are represented by objects.
$model1 = new LabelTestModel1();
$model2 = new LabelTestModel2();
$model3 = new LabelTestModel3();
$model2->populateRelation('model3', $model3);
$model1->populateRelation('model2', $model2);
$data[] = [$model1];
// Model 2 and 3 are represented by arrays instead of objects.
$model1 = new LabelTestModel1();
$model2 = ['model3' => []];
$model1->populateRelation('model2', $model2);
$data[] = [$model1];
return $data;
}
/**
* @dataProvider labelTestModelProvider
* @param \yii\db\ActiveRecord $model
*/
public function testGetAttributeLabel($model)
{
$this->assertEquals('model3.attr1 from model2', $model->getAttributeLabel('model2.model3.attr1'));
$this->assertEquals('attr2 from model3', $model->getAttributeLabel('model2.model3.attr2'));
$this->assertEquals('model3.attr3 from model2', $model->getAttributeLabel('model2.model3.attr3'));
$attr = 'model2.doesNotExist.attr1';
$this->assertEquals($model->generateAttributeLabel($attr), $model->getAttributeLabel($attr));
}
}
class LabelTestModel1 extends \yii\db\ActiveRecord
{
public function attributes()
{
return [];
}
public function getModel2()
{
return $this->hasOne(LabelTestModel2::className(), []);
}
}
class LabelTestModel2 extends \yii\db\ActiveRecord
{
public function attributes()
{
return [];
}
public function getModel3()
{
return $this->hasOne(LabelTestModel3::className(), []);
}
public function attributeLabels()
{
return [
'model3.attr1' => 'model3.attr1 from model2', // Override label defined in model3.
'model3.attr3' => 'model3.attr3 from model2', // Define label not defined in model3.
];
}
}
class LabelTestModel3 extends \yii\db\ActiveRecord
{
public function attributes()
{
return ['attr1', 'attr2', 'attr3'];
}
public function attributeLabels()
{
return [
'attr1' => 'attr1 from model3',
'attr2' => 'attr2 from model3',
];
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace yiiunit\framework\db;
use yiiunit\data\ar\ActiveRecord;
abstract class BaseActiveRecordTest extends DatabaseTestCase
{
protected function setUp()
{
parent::setUp();
ActiveRecord::$db = $this->getConnection();
}
public function provideArrayValueWithChange()
{
return [
'not an associative array with data change' => [
[1, 2, 3],
[1, 3, 2],
],
'associative array with data change case 1' => [
['pineapple' => 2, 'apple' => 5, 'banana' => 1],
['apple' => 5, 'pineapple' => 1, 'banana' => 3],
],
'associative array with data change case 2' => [
['pineapple' => 2, 'apple' => 5, 'banana' => 1],
['pineapple' => 2, 'apple' => 3, 'banana' => 1],
],
'filling an empty array' => [
[],
['pineapple' => 3, 'apple' => 1, 'banana' => 1],
],
'zeroing the array' => [
['pineapple' => 3, 'apple' => 1, 'banana' => 17],
[],
],
];
}
}

View File

@ -134,22 +134,19 @@ class CommandTest extends \yiiunit\framework\db\CommandTest
{
$db = $this->getConnection();
$testData = json_encode(['test' => 'string', 'test2' => 'integer']);
$qb = $db->getQueryBuilder();
$testData = json_encode(['test' => 'string', 'test2' => 'integer'], JSON_THROW_ON_ERROR);
$params = [];
$qb = $db->getQueryBuilder();
$sql = $qb->upsert('T_upsert_varbinary', ['id' => 1, 'blob_col' => $testData] , ['blob_col' => $testData], $params);
$sql = $qb->upsert('T_upsert_varbinary', ['id' => 1, 'blob_col' => $testData], ['blob_col' => $testData], $params);
$result = $db->createCommand($sql, $params)->execute();
$this->assertEquals(1, $result);
$query = (new Query())
->select(['convert(nvarchar(max),blob_col) as blob_col'])
->from('T_upsert_varbinary')
->where(['id' => 1]);
$this->assertSame(1, $result);
$query = (new Query())->select(['blob_col'])->from('T_upsert_varbinary')->where(['id' => 1]);
$resultData = $query->createCommand($db)->queryOne();
$this->assertEquals($testData, $resultData['blob_col']);
$this->assertSame($testData, $resultData['blob_col']);
}
}

View File

@ -0,0 +1,55 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yiiunit\framework\db\mssql;
use yii\caching\FileCache;
use yii\db\Query;
use yiiunit\framework\db\DatabaseTestCase;
/**
* @group db
* @group mssql
*/
class QueryCacheTest extends DatabaseTestCase
{
protected $driverName = 'sqlsrv';
public function testQueryCacheFileCache()
{
$db = $this->getConnection();
$db->enableQueryCache = true;
$db->queryCache = new FileCache(['cachePath' => '@yiiunit/runtime/cache']);
$db->createCommand()->delete('type')->execute();
$db->createCommand()->insert('type', [
'int_col' => $key = 1,
'char_col' => '',
'char_col2' => '6a3ce1a0bffe8eeb6fa986caf443e24c',
'float_col' => 0.0,
'blob_col' => 'a:1:{s:13:"template";s:1:"1";}',
'bool_col' => true,
])->execute();
$function = function($db) use ($key){
return (new Query())
->select(['blob_col'])
->from('type')
->where(['int_col' => $key])
->createCommand($db)
->queryScalar();
};
// First run return
$result = $db->cache($function);
$this->assertSame('a:1:{s:13:"template";s:1:"1";}', $result);
// After the request has been cached return
$result = $db->cache($function);
$this->assertSame('a:1:{s:13:"template";s:1:"1";}', $result);
}
}

View File

@ -0,0 +1,44 @@
<?php
/**
* @link https://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license https://www.yiiframework.com/license/
*/
namespace yiiunit\framework\db\mssql\Type;
use yii\db\Query;
use yiiunit\framework\db\DatabaseTestCase;
/**
* @group db
* @group mssql
*/
class VarbinaryTest extends DatabaseTestCase
{
protected $driverName = 'sqlsrv';
public function testVarbinary()
{
$db = $this->getConnection();
$db->createCommand()->delete('type')->execute();
$db->createCommand()->insert('type', [
'int_col' => $key = 1,
'char_col' => '',
'char_col2' => '6a3ce1a0bffe8eeb6fa986caf443e24c',
'float_col' => 0.0,
'blob_col' => 'a:1:{s:13:"template";s:1:"1";}',
'bool_col' => true,
])->execute();
$result = (new Query())
->select(['blob_col'])
->from('type')
->where(['int_col' => $key])
->createCommand($db)
->queryScalar();
$this->assertSame('a:1:{s:13:"template";s:1:"1";}', $result);
}
}

View File

@ -0,0 +1,37 @@
<?php
namespace yiiunit\framework\db\mysql;
use yiiunit\data\ar\Storage;
class BaseActiveRecordTest extends \yiiunit\framework\db\BaseActiveRecordTest
{
public $driverName = 'mysql';
/**
* @see https://github.com/yiisoft/yii2/issues/19872
*
* @dataProvider provideArrayValueWithChange
*/
public function testJsonDirtyAttributesWithDataChange($actual, $modified)
{
if (version_compare($this->getConnection()->getSchema()->getServerVersion(), '5.7', '<')) {
$this->markTestSkipped('JSON columns are not supported in MySQL < 5.7');
}
if (version_compare(PHP_VERSION, '5.6', '<')) {
$this->markTestSkipped('JSON columns are not supported in PDO for PHP < 5.6');
}
$createdStorage = new Storage(['data' => $actual]);
$createdStorage->save();
$foundStorage = Storage::find()->limit(1)->one();
$this->assertNotNull($foundStorage);
$foundStorage->data = $modified;
$this->assertSame(['data' => $modified], $foundStorage->getDirtyAttributes());
}
}

View File

@ -0,0 +1,46 @@
<?php
namespace yiiunit\framework\db\pgsql;
use yii\db\JsonExpression;
use yiiunit\data\ar\ActiveRecord;
class BaseActiveRecordTest extends \yiiunit\framework\db\BaseActiveRecordTest
{
public $driverName = 'pgsql';
/**
* @see https://github.com/yiisoft/yii2/issues/19872
*
* @dataProvider provideArrayValueWithChange
*/
public function testJsonDirtyAttributesWithDataChange($actual, $modified)
{
$createdStorage = new ArrayAndJsonType([
'json_col' => new JsonExpression($actual),
]);
$createdStorage->save();
$foundStorage = ArrayAndJsonType::find()->limit(1)->one();
$this->assertNotNull($foundStorage);
$foundStorage->json_col = $modified;
$this->assertSame(['json_col' => $modified], $foundStorage->getDirtyAttributes());
}
}
/**
* {@inheritdoc}
* @property array id
* @property array json_col
*/
class ArrayAndJsonType extends ActiveRecord
{
public static function tableName()
{
return '{{%array_and_json_types}}';
}
}

View File

@ -7,10 +7,12 @@
namespace yiiunit\framework\grid;
use Yii;
use yii\data\ArrayDataProvider;
use yii\grid\DataColumn;
use yii\grid\GridView;
use yii\web\View;
use yiiunit\data\ar\NoAutoLabels;
/**
* @author Evgeniy Tkachenko <et.coder@gmail.com>
@ -150,4 +152,53 @@ class GridViewTest extends \yiiunit\TestCase
$this->assertTrue(preg_match("/<\/tbody><tfoot>/", $html) === 1);
}
public function testHeaderLabels()
{
// Ensure GridView does not call Model::generateAttributeLabel() to generate labels unless the labels are explicitly used.
$this->mockApplication([
'components' => [
'db' => [
'class' => \yii\db\Connection::className(),
'dsn' => 'sqlite::memory:',
],
],
]);
NoAutoLabels::$db = Yii::$app->getDb();
Yii::$app->getDb()->createCommand()->createTable(NoAutoLabels::tableName(), ['attr1' => 'int', 'attr2' => 'int'])->execute();
$urlManager = new \yii\web\UrlManager([
'baseUrl' => '/',
'scriptUrl' => '/index.php',
]);
$grid = new GridView([
'dataProvider' => new \yii\data\ActiveDataProvider([
'query' => NoAutoLabels::find(),
]),
'columns' => [
'attr1',
'attr2:text:Label for attr2',
],
]);
// NoAutoLabels::generateAttributeLabel() should not be called.
$grid->dataProvider->setSort([
'route' => '/',
'urlManager' => $urlManager,
]);
$grid->renderTableHeader();
// NoAutoLabels::generateAttributeLabel() should not be called.
$grid->dataProvider->setSort([
'route' => '/',
'urlManager' => $urlManager,
'attributes' => ['attr1', 'attr2'],
]);
$grid->renderTableHeader();
// If NoAutoLabels::generateAttributeLabel() has not been called no exception will be thrown meaning this test passed successfully.
}
}

View File

@ -135,6 +135,29 @@ class ArrayHelperTest extends TestCase
$this->assertEquals('defaultValue', $default);
}
/**
* @return void
*/
public function testRemoveWithFloat()
{
if (version_compare(PHP_VERSION, '8.1.0', '>=')) {
$this->markTestSkipped('Using floats as array key is deprecated.');
}
$array = ['name' => 'b', 'age' => 3, 1.1 => null];
$name = ArrayHelper::remove($array, 'name');
$this->assertEquals($name, 'b');
$this->assertEquals($array, ['age' => 3, 1.1 => null]);
$floatVal = ArrayHelper::remove($array, 1.1);
$this->assertNull($floatVal);
$this->assertEquals($array, ['age' => 3]);
$default = ArrayHelper::remove($array, 'nonExisting', 'defaultValue');
$this->assertEquals('defaultValue', $default);
}
public function testRemoveValueMultiple()
{
$array = [
@ -506,14 +529,21 @@ class ArrayHelperTest extends TestCase
/**
* @see https://github.com/yiisoft/yii2/pull/11549
*/
public function test()
public function testGetValueWithFloatKeys()
{
if (version_compare(PHP_VERSION, '8.1.0', '>=')) {
$this->markTestSkipped('Using floats as array key is deprecated.');
}
$array = [];
$array[1.0] = 'some value';
$result = ArrayHelper::getValue($array, 1.0);
$array[1.1] = 'some value';
$array[2.1] = null;
$result = ArrayHelper::getValue($array, 1.2);
$this->assertEquals('some value', $result);
$result = ArrayHelper::getValue($array, 2.2);
$this->assertNull($result);
}
public function testIndex()
@ -712,6 +742,7 @@ class ArrayHelperTest extends TestCase
'a' => 1,
'B' => 2,
];
$this->assertTrue(ArrayHelper::keyExists('a', $array));
$this->assertFalse(ArrayHelper::keyExists('b', $array));
$this->assertTrue(ArrayHelper::keyExists('B', $array));
@ -723,6 +754,27 @@ class ArrayHelperTest extends TestCase
$this->assertFalse(ArrayHelper::keyExists('c', $array, false));
}
public function testKeyExistsWithFloat()
{
if (version_compare(PHP_VERSION, '8.1.0', '>=')) {
$this->markTestSkipped('Using floats as array key is deprecated.');
}
$array = [
1 => 3,
2.2 => 4, // Note: Floats are cast to ints, which means that the fractional part will be truncated.
3.3 => null,
];
$this->assertTrue(ArrayHelper::keyExists(1, $array));
$this->assertTrue(ArrayHelper::keyExists(1.1, $array));
$this->assertTrue(ArrayHelper::keyExists(2, $array));
$this->assertTrue(ArrayHelper::keyExists('2', $array));
$this->assertTrue(ArrayHelper::keyExists(2.2, $array));
$this->assertTrue(ArrayHelper::keyExists(3, $array));
$this->assertTrue(ArrayHelper::keyExists(3.3, $array));
}
public function testKeyExistsArrayAccess()
{
$array = new TraversableArrayAccessibleObject([

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@ use yii\helpers\FileHelper;
use yii\log\Dispatcher;
use yii\log\FileTarget;
use yii\log\Logger;
use yiiunit\framework\log\mocks\CustomLogger;
use yiiunit\TestCase;
/**
@ -110,4 +111,53 @@ class FileTargetTest extends TestCase
$this->assertFileNotExists($logFile . '.3');
$this->assertFileNotExists($logFile . '.4');
}
public function testLogEmptyStrings()
{
$logFile = Yii::getAlias('@yiiunit/runtime/log/filetargettest.log');
$this->clearLogFile($logFile);
$logger = new CustomLogger();
$logger->logFile = $logFile;
$logger->messages = array_fill(0, 1, 'xxx');
$logger->export();
$test = file($logFile);
$this->assertEquals("xxx\n", $test[0]);
$this->clearLogFile($logFile);
$logger = new CustomLogger();
$logger->logFile = $logFile;
$logger->messages = array_fill(0, 3, 'xxx');
$logger->export();
$test = file($logFile);
$this->assertEquals("xxx\n", $test[0]);
$this->assertEquals("xxx\n", $test[1]);
$this->assertEquals("xxx\n", $test[2]);
$this->clearLogFile($logFile);
$logger->messages = array_fill(0, 1, 'yyy');
$logger->export();
$this->assertFileNotExists($logFile);
$logger->messages = array_fill(0, 10, '');
$logger->export();
$this->assertFileNotExists($logFile);
$logger->messages = array_fill(0, 10, null);
$logger->export();
$this->assertFileNotExists($logFile);
}
private function clearLogFile($logFile)
{
FileHelper::removeDirectory(dirname($logFile));
mkdir(dirname($logFile), 0777, true);
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace yiiunit\framework\log\mocks;
use yii\log\FileTarget;
class CustomLogger extends FileTarget
{
/**
* @param array $message
*
* @return null|string|array
*/
public function formatMessage($message)
{
if ($message == 'yyy') {
return null;
}
return $message;
}
}

View File

@ -546,7 +546,8 @@ class FileValidatorTest extends TestCase
['test.tar.xz', 'application/x-xz', 'tar.xz'],
]);
if (PHP_VERSION_ID >= 80100) {
# fix for bundled libmagic bug, see also https://github.com/yiisoft/yii2/issues/19925
if ((PHP_VERSION_ID >= 80100 && PHP_VERSION_ID < 80122) || (PHP_VERSION_ID >= 80200 && PHP_VERSION_ID < 80209)) {
$v81_zx = ['test.tar.xz', 'application/octet-stream', 'tar.xz'];
array_pop($validMimeTypes);
$validMimeTypes[] = $v81_zx;

View File

@ -380,21 +380,116 @@ class ResponseTest extends \yiiunit\TestCase
);
}
public function testSameSiteCookie()
/**
* @dataProvider cookiesTestProvider
*/
public function testCookies($cookieConfig, $expected)
{
$response = new Response();
$response->cookies->add(new Cookie([
'name' => 'test',
'value' => 'testValue',
'sameSite' => Cookie::SAME_SITE_STRICT,
]));
$response->cookies->add(new Cookie(array_merge(
[
'name' => 'test',
'value' => 'testValue',
],
$cookieConfig
)));
ob_start();
$response->send();
$content = ob_get_clean();
// Only way to test is that it doesn't create any errors
$this->assertEquals('', $content);
$cookies = $this->parseHeaderCookies();
if ($cookies === false) {
// Unable to resolve cookies, only way to test is that it doesn't create any errors
$this->assertEquals('', $content);
} else {
$testCookie = $cookies['test'];
$actual = array_intersect_key($testCookie, $expected);
ksort($actual);
ksort($expected);
$this->assertEquals($expected, $actual);
}
}
public function cookiesTestProvider()
{
$expireInt = time() + 3600;
$expireString = date('D, d-M-Y H:i:s', $expireInt) . ' GMT';
$testCases = [
'same-site' => [
['sameSite' => Cookie::SAME_SITE_STRICT],
['samesite' => Cookie::SAME_SITE_STRICT],
],
'expire-as-int' => [
['expire' => $expireInt],
['expires' => $expireString],
],
'expire-as-string' => [
['expire' => $expireString],
['expires' => $expireString],
],
];
if (version_compare(PHP_VERSION, '5.5.0', '>=')) {
$testCases = array_merge($testCases, [
'expire-as-date_time' => [
['expire' => new \DateTime('@' . $expireInt)],
['expires' => $expireString],
],
'expire-as-date_time_immutable' => [
['expire' => new \DateTimeImmutable('@' . $expireInt)],
['expires' => $expireString],
],
]);
}
return $testCases;
}
/**
* Tries to parse cookies set in the response headers.
* When running PHP on the CLI headers are not available (the `headers_list()` function always returns an
* empty array). If possible use xDebug: http://xdebug.org/docs/all_functions#xdebug_get_headers
* @param $name
* @return array|false
*/
protected function parseHeaderCookies() {
if (!function_exists('xdebug_get_headers')) {
return false;
}
$cookies = [];
foreach(xdebug_get_headers() as $header) {
if (strpos($header, 'Set-Cookie: ') !== 0) {
continue;
}
$name = null;
$params = [];
$pairs = explode(';', substr($header, 12));
foreach ($pairs as $index => $pair) {
$pair = trim($pair);
if (strpos($pair, '=') === false) {
$params[strtolower($pair)] = true;
} else {
list($paramName, $paramValue) = explode('=', $pair, 2);
if ($index === 0) {
$name = $paramName;
$params['value'] = urldecode($paramValue);
} else {
$params[strtolower($paramName)] = urldecode($paramValue);
}
}
}
if ($name === null) {
throw new \Exception('Could not determine cookie name for header "' . $header . '".');
}
$cookies[$name] = $params;
}
return $cookies;
}
/**