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

@ -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