From 8a0992d5c012f2042ce418d1edf0e225cdbcaa06 Mon Sep 17 00:00:00 2001 From: Janne Date: Sat, 19 Oct 2024 20:37:49 +0300 Subject: [PATCH 01/17] Fixed called class check in `Widget::end()` when widget configured using callable See #20267 --- framework/CHANGELOG.md | 1 + framework/base/Widget.php | 10 ++++++---- tests/framework/base/WidgetTest.php | 21 +++++++++++++++++++++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 294810f9fc7..81392001ce7 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -9,6 +9,7 @@ Yii Framework 2 Change Log - Enh #20247: Support for variadic console controller action methods (brandonkelly) - Bug #20256: Add support for dropping views in MSSQL server when running migrate/fresh (ambrozt) - Enh #20248: Add support for attaching behaviors in configurations with Closure (timkelty) +- Enh #20267: Fixed called class check in `Widget::end()` when widget configured using callable (rob006, jrajamaki) 2.0.51 July 18, 2024 -------------------- diff --git a/framework/base/Widget.php b/framework/base/Widget.php index 450caac9c9b..1267643df62 100644 --- a/framework/base/Widget.php +++ b/framework/base/Widget.php @@ -60,6 +60,10 @@ class Widget extends Component implements ViewContextInterface */ public static $stack = []; + /** + * @var string[] used widget classes that have been resolved to their actual class name. + */ + private static $resolvedClasses = []; /** * Initializes the object. @@ -88,6 +92,7 @@ public static function begin($config = []) /* @var $widget Widget */ $widget = Yii::createObject($config); self::$stack[] = $widget; + self::$resolvedClasses[get_called_class()] = get_class($widget); return $widget; } @@ -104,10 +109,7 @@ public static function end() if (!empty(self::$stack)) { $widget = array_pop(self::$stack); - $calledClass = get_called_class(); - if (Yii::$container->has($calledClass) && isset(Yii::$container->getDefinitions()[$calledClass]['class'])) { - $calledClass = Yii::$container->getDefinitions()[$calledClass]['class']; - } + $calledClass = self::$resolvedClasses[get_called_class()] ?? get_called_class(); if (get_class($widget) === $calledClass) { /* @var $widget Widget */ diff --git a/tests/framework/base/WidgetTest.php b/tests/framework/base/WidgetTest.php index 067e9480125..c67cbedd885 100644 --- a/tests/framework/base/WidgetTest.php +++ b/tests/framework/base/WidgetTest.php @@ -72,6 +72,27 @@ public function testDependencyInjection() $this->assertSame('', $output); } + public function testDependencyInjectionWithCallableConfiguration() + { + Yii::$container = new Container(); + Yii::$container->setDefinitions([ + TestWidgetB::className() => function () { + return new TestWidget(['id' => 'test']); + } + ]); + + ob_start(); + ob_implicit_flush(false); + + $widget = TestWidgetB::begin(['id' => 'test']); + $this->assertTrue($widget instanceof TestWidget); + TestWidgetB::end(); + + $output = ob_get_clean(); + + $this->assertSame('', $output); + } + /** * @depends testBeginEnd */ From 02ef942c5335266903947565e093671984da63e0 Mon Sep 17 00:00:00 2001 From: Janne Date: Sat, 19 Oct 2024 20:48:31 +0300 Subject: [PATCH 02/17] Added missing underscore in front of private property #20267 --- framework/base/Widget.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/framework/base/Widget.php b/framework/base/Widget.php index 1267643df62..b1340188449 100644 --- a/framework/base/Widget.php +++ b/framework/base/Widget.php @@ -63,7 +63,7 @@ class Widget extends Component implements ViewContextInterface /** * @var string[] used widget classes that have been resolved to their actual class name. */ - private static $resolvedClasses = []; + private static $_resolvedClasses = []; /** * Initializes the object. @@ -92,7 +92,7 @@ public static function begin($config = []) /* @var $widget Widget */ $widget = Yii::createObject($config); self::$stack[] = $widget; - self::$resolvedClasses[get_called_class()] = get_class($widget); + self::$_resolvedClasses[get_called_class()] = get_class($widget); return $widget; } @@ -109,7 +109,7 @@ public static function end() if (!empty(self::$stack)) { $widget = array_pop(self::$stack); - $calledClass = self::$resolvedClasses[get_called_class()] ?? get_called_class(); + $calledClass = self::$_resolvedClasses[get_called_class()] ?? get_called_class(); if (get_class($widget) === $calledClass) { /* @var $widget Widget */ From 25059c4e0880bedd705c65b3329c658b6efe85a4 Mon Sep 17 00:00:00 2001 From: 101000june <39669835+101000june@users.noreply.github.com> Date: Sun, 27 Oct 2024 11:08:06 +0200 Subject: [PATCH 03/17] =?UTF-8?q?Docs:=20Ukrainian=20=E2=80=94=20Add=20Ses?= =?UTF-8?q?sions&Cookies=20page=20(#20272)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/guide-uk/runtime-sessions-cookies.md | 285 ++++++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 docs/guide-uk/runtime-sessions-cookies.md diff --git a/docs/guide-uk/runtime-sessions-cookies.md b/docs/guide-uk/runtime-sessions-cookies.md new file mode 100644 index 00000000000..7edf8e00f8b --- /dev/null +++ b/docs/guide-uk/runtime-sessions-cookies.md @@ -0,0 +1,285 @@ +Сесії та кукі +==================== + +Сесії та кукі дозволяють зберігати користувацькі дані між запитами. При використанні чистого PHP можна отримати доступ до цих даних через глобальні змінні `$_SESSION` та `$_COOKIE`, відповідно. Yii інкапсулює сесії та кукі в об'єкти, що дає можливість звертатися до них в об'єктноорієнтованому стилі та забезпечує додаткову зручність в роботі. + + +## Сесії + +За аналогією з [запитами](runtime-requests.md) та [відповідями](runtime-responses.md), до сесій можна отримати доступ через `session` [компонент додатка](structure-application-components.md), який за замовчуванням є екземпляром [[yii\web\Session]]. + + +### Відкриття та закриття сесії + +Відкрити та закрити сесію можна наступним чином: + +```php +$session = Yii::$app->session; + +// перевіряєм що сесія вже відкрита +if ($session->isActive) ... + +// відкиваєм сесію +$session->open(); + +// закриваємо сесію +$session->close(); + +// знищуємо сесію і всі пов'язані з нею дані. +$session->destroy(); +``` + +Можна викликати [[yii\web\Session::open()|open()]] і [[yii\web\Session::close()|close()]] багаторазово без виникнення помилок; всередині компонента всі методи перевіряють сесію на те, відкрита вона чи ні. + + +### Доступ до даних сесії + +Отримати доступ до збережених в сесію даних можна наступним чином: + +```php +$session = Yii::$app->session; + +// отримання змінної з сесії. Наступні способи використання еквівалентні: +$language = $session->get('language'); +$language = $session['language']; +$language = isset($_SESSION['language']) ? $_SESSION['language'] : null; + +// запис змінної в сесію. Наступні способи використання еквівалентні: +$session->set('language', 'en-US'); +$session['language'] = 'en-US'; +$_SESSION['language'] = 'en-US'; + +// видалення змінної з сесії. Наступні способи використання еквівалентні: +$session->remove('language'); +unset($session['language']); +unset($_SESSION['language']); + +// перевірка на існування змінної в сесії. Наступні способи використання еквівалентні: +if ($session->has('language')) ... +if (isset($session['language'])) ... +if (isset($_SESSION['language'])) ... + +// обхід усіх змінних у сесії. Наступні способи використання еквівалентні: +foreach ($session as $name => $value) ... +foreach ($_SESSION as $name => $value) ... +``` + +> Info: При отриманні даних з сесії через компонент `session`, сесія буде автоматично відкрита, якщо вона не була відкрита до цього. У цьому полягає відмінність від отримання даних з глобальної змінної `$_SESSION`, що вимагає обов'язкового виклику `session_start()`. + +При роботі з сесійними даними, які є масивами, компонент `session` має обмеження, що забороняє пряму модифікацію окремих елементів масиву. Наприклад, + +```php +$session = Yii::$app->session; + +// наступний код НЕ БУДЕ працювати +$session['captcha']['number'] = 5; +$session['captcha']['lifetime'] = 3600; + +// а цей буде: +$session['captcha'] = [ + 'number' => 5, + 'lifetime' => 3600, +]; + +// цей код також буде працювати: +echo $session['captcha']['lifetime']; +``` + +Для вирішення цієї проблеми можна використовувати такі обхідні прийоми: + +```php +$session = Yii::$app->session; + +// пряме використання $_SESSION (переконайтеся, що Yii::$app->session->open() був викликаний) +$_SESSION['captcha']['number'] = 5; +$_SESSION['captcha']['lifetime'] = 3600; + +// отримайте весь масив, модифікуйте і збережіть назад у сесію +$captcha = $session['captcha']; +$captcha['number'] = 5; +$captcha['lifetime'] = 3600; +$session['captcha'] = $captcha; + +// використовуйте ArrayObject замість масиву +$session['captcha'] = new \ArrayObject; +... +$session['captcha']['number'] = 5; +$session['captcha']['lifetime'] = 3600; + +// записуйте дані з ключами, які мають однаковий префікс +$session['captcha.number'] = 5; +$session['captcha.lifetime'] = 3600; +``` + +Для покращення продуктивності та читабельності коду рекомендується використовувати останній прийом. Іншими словами, замість того, щоб зберігати масив як одну змінну сесії, ми зберігаємо кожен елемент масиву як звичайну сесійну змінну зі спільним префіксом. + + +### Користувацьке сховище для сесії + +За замовчуванням клас [[yii\web\Session]] зберігає дані сесії у вигляді файлів на сервері. Однак Yii надає ряд класів, які реалізують різні способи зберігання даних сесії: + +* [[yii\web\DbSession]]: зберігає дані сесії в базі даних. +* [[yii\web\CacheSession]]: зберігання даних сесії в попередньо сконфігурованому компоненті кешу [кеш](caching-data.md#cache-components). +* [[yii\redis\Session]]: зберігання даних сесії в [redis](https://redis.io/). +* [[yii\mongodb\Session]]: зберігання сесії в [MongoDB](https://www.mongodb.com/). + +Усі ці класи підтримують однаковий набір методів API. В результаті ви можете перемикатися між різними сховищами сесій без модифікації коду додатку. + +> Note: Якщо ви хочете отримати дані з змінної `$_SESSION` при використанні користувацького сховища, ви повинні бути впевнені, що сесія вже стартувала [[yii\web\Session::open()]], оскільки обробники зберігання користувацьких сесій реєструються в цьому методі. + +Щоб дізнатися, як налаштувати і використовувати ці компоненти, зверніться до документації по API. Нижче наведено приклад конфігурації [[yii\web\DbSession]] для використання бази даних для зберігання сесії: + +```php +return [ + 'components' => [ + 'session' => [ + 'class' => 'yii\web\DbSession', + // 'db' => 'mydb', // ID компонента для взаємодії з БД. По замовчуванню 'db'. + // 'sessionTable' => 'my_session', // назва таблиці для даних сесії. По замовчуванню 'session'. + ], + ], +]; +``` + +Також необхідно створити таблицю для зберігання даних сесії: + +```sql +CREATE TABLE session +( + id CHAR(40) NOT NULL PRIMARY KEY, + expire INTEGER, + data BLOB +) +``` + +де 'BLOB' відповідає типу даних вашої DBMS. Нижче наведені приклади відповідності типів BLOB у найбільш популярних DBMS: + +- MySQL: LONGBLOB +- PostgreSQL: BYTEA +- MSSQL: BLOB + +> Note: В залежності від налаштувань параметра `session.hash_function` у вашому php.ini, може знадобитися змінити довжину поля `id`. Наприклад, якщо `session.hash_function=sha256`, потрібно встановити довжину поля на 64 замість 40. + +### Flash-повідомлення + +Flash-повідомлення - це особливий тип даних у сесії, які встановлюються один раз під час запиту і доступні лише протягом наступного запиту, після чого вони автоматично видаляються. Такий спосіб зберігання інформації в сесії найчастіше використовується для реалізації повідомлень, які будуть відображені кінцевому користувачу один раз, наприклад, підтвердження про успішну відправку форми. + +Встановити та отримати flash-повідомлення можна через компонент програми `session`. Наприклад: + +```php +$session = Yii::$app->session; + +// Запит #1 +// встановлення flash-повідомлення з назвою "postDeleted" +$session->setFlash('postDeleted', 'Ви успішно видалили пост.'); + +// Запит #2 +// відображення flash-повідомлення "postDeleted" +echo $session->getFlash('postDeleted'); + +// Запит #3 +// змінна $result буде мати значення false, оскільки flash-повідомлення було автоматично видалено +$result = $session->hasFlash('postDeleted'); +``` + +Оскільки flash-повідомлення зберігаються в сесії як звичайні дані, в них можна записувати довільну інформацію, і вона буде доступна лише в наступному запиті. + +При виклику [[yii\web\Session::setFlash()]] відбувається перезаписування flash-повідомлень з таким же назвою. Для того, щоб додати нові дані до вже існуючого flash-повідомлення, необхідно викликати [[yii\web\Session::addFlash()]]. +Наприклад: + +```php +$session = Yii::$app->session; + +// Запит #1 +// додати нове flash-повідомлення з назвою "alerts" +$session->addFlash('alerts', 'Ви успішно видалили пост.'); +$session->addFlash('alerts', 'Ви успішно додали нового друга.'); +$session->addFlash('alerts', 'Дякуємо.'); + +// Запит #2 +// Змінна $alerts тепер містить масив flash-повідомлень з назвою "alerts" +$alerts = $session->getFlash('alerts'); +``` + +> Note: Намагайтеся не використовувати [[yii\web\Session::setFlash()]] спільно з [[yii\web\Session::addFlash()]] для flash-повідомлень з однаковою назвою. Це пов'язано з тим, що останній метод автоматично перетворює збережені дані в масив, щоб мати можливість зберігати та додавати нові дані в flash-повідомлення з тією ж назвою. В результаті, при виклику [[yii\web\Session::getFlash()]] можна виявити, що повертається масив, тоді як очікувалася строка. + +## Кукі + +Yii представляє кожну з cookie як об'єкт [[yii\web\Cookie]]. Обидва компоненти програми [[yii\web\Request]] і [[yii\web\Response]] +підтримують колекції кукі через своє властивість cookies. У першому випадку колекція кукі є їх представленням з HTTP-запиту, у другому — представляє кукі, які будуть відправлені користувачу. + +### Читання кукі + +Отримати кукі з поточного запиту можна наступним чином: + +```php +// отримання колекції кукі (yii\web\CookieCollection) з компонента "request" +$cookies = Yii::$app->request->cookies; + +// отримання кукі з назвою "language". Якщо кукі не існує, "en" буде повернуто як значення за замовчуванням. +$language = $cookies->getValue('language', 'en'); + +// альтернативний спосіб отримання кукі "language" +if (($cookie = $cookies->get('language')) !== null) { + $language = $cookie->value; +} + +// тепер змінну $cookies можна використовувати як масив +if (isset($cookies['language'])) { + $language = $cookies['language']->value; +} + +// перевірка на існування кукі "language" +if ($cookies->has('language')) ... +if (isset($cookies['language'])) ... +``` + + +### Відправка кукі + +Відправити кукі кінцевому користувачу можна наступним чином: + +```php +// отримання колекції (yii\web\CookieCollection) з компонента "response" +$cookies = Yii::$app->response->cookies; + +// додавання нової кукі в HTTP-відповідь +$cookies->add(new \yii\web\Cookie([ + 'name' => 'language', + 'value' => 'zh-CN', +])); + +// видалення кукі... +$cookies->remove('language'); +// ...що еквівалентно наступному: +unset($cookies['language']); +``` + +Крім властивостей [[yii\web\Cookie::name|name]] та [[yii\web\Cookie::value|value]], клас [[yii\web\Cookie]] також надає ряд властивостей для отримання інформації про куки: [[yii\web\Cookie::domain|domain]], [[yii\web\Cookie::expire|expire]]. Ці властивості можна сконфігурувати, а потім додати кукі в колекцію для HTTP-відповіді. + +> Note: Для більшої безпеки значення властивості [[yii\web\Cookie::httpOnly]] за замовчуванням встановлено в `true`. Це зменшує ризики доступу до захищеної кукі на клієнтській стороні (якщо браузер підтримує таку можливість). Ви можете звернутися до [httpOnly wiki](https://owasp.org/www-community/HttpOnly) для додаткової інформації. + +### Валідація кукі + +Під час запису та читання куків через компоненти `request` та `response`, як буде показано в двох наступних підрозділах, фреймворк надає автоматичну валідацію, яка забезпечує захист кукі від модифікації на стороні клієнта. Це досягається завдяки підписанню кожної кукі секретним ключем, що дозволяє додатку розпізнавати кукі, які були модифіковані на клієнтській стороні. У такому випадку кукі НЕ БУДЕ доступна через властивість [[yii\web\Request::cookies|cookie collection]] компонента `request`. + +> Note: Валідація кукі захищає тільки від їх модифікації. Якщо валідація не була пройдена, отримати доступ до кукі все ще можна через глобальну змінну `$_COOKIE`. Це пов'язано з тим, що додаткові пакети та бібліотеки можуть маніпулювати кукі без виклику валідації, яку забезпечує Yii. + + +За замовчуванням валідація кукі увімкнена. Її можна вимкнути, встановивши властивість [[yii\web\Request::enableCookieValidation]] в `false`, однак ми настійливо не рекомендуємо цього робити. + +> Note: Кукі, які безпосередньо читаються/пишуться через `$_COOKIE` та `setcookie()`, НЕ БУДУТЬ валідовуватися. + +При використанні валідації кукі необхідно вказати значення властивості [[yii\web\Request::cookieValidationKey]], яке буде використано для генерації згаданого вище секретного ключа. Це можна зробити, налаштувавши компонент `request` у конфігурації додатка: + +```php +return [ + 'components' => [ + 'request' => [ + 'cookieValidationKey' => 'fill in a secret key here', + ], + ], +]; +``` + +> Note: Властивість [[yii\web\Request::cookieValidationKey|cookieValidationKey]] є секретним значенням і повинно бути відомо лише тим, кому ви довіряєте. Не розміщуйте цю інформацію в системі контролю версій. From e4d5d73490bc4d382f2646573d0e1edc2be8461a Mon Sep 17 00:00:00 2001 From: Christina Reichel <123160582+chriscpty@users.noreply.github.com> Date: Sun, 27 Oct 2024 11:37:09 +0100 Subject: [PATCH 04/17] Fix #20268: Minor optimisation in `\yii\helpers\BaseArrayHelper::map` --- framework/CHANGELOG.md | 1 + framework/helpers/BaseArrayHelper.php | 3 ++ framework/helpers/BaseStringHelper.php | 33 +++++++------ tests/framework/helpers/ArrayHelperTest.php | 53 ++++++++++++++++++++- 4 files changed, 75 insertions(+), 15 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 294810f9fc7..1935d097811 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -9,6 +9,7 @@ Yii Framework 2 Change Log - Enh #20247: Support for variadic console controller action methods (brandonkelly) - Bug #20256: Add support for dropping views in MSSQL server when running migrate/fresh (ambrozt) - Enh #20248: Add support for attaching behaviors in configurations with Closure (timkelty) +- Enh #20268: Minor optimisation in `\yii\helpers\BaseArrayHelper::map` (chriscpty) 2.0.51 July 18, 2024 -------------------- diff --git a/framework/helpers/BaseArrayHelper.php b/framework/helpers/BaseArrayHelper.php index 56411163e1e..bc770f96cb7 100644 --- a/framework/helpers/BaseArrayHelper.php +++ b/framework/helpers/BaseArrayHelper.php @@ -595,6 +595,9 @@ public static function getColumn($array, $name, $keepKeys = true) */ public static function map($array, $from, $to, $group = null) { + if (is_string($from) && is_string($to) && $group === null && strpos($from, '.') === false && strpos($to, '.') === false) { + return array_column($array, $to, $from); + } $result = []; foreach ($array as $element) { $key = static::getValue($element, $from); diff --git a/framework/helpers/BaseStringHelper.php b/framework/helpers/BaseStringHelper.php index ec9252aa4c4..5854e29766d 100644 --- a/framework/helpers/BaseStringHelper.php +++ b/framework/helpers/BaseStringHelper.php @@ -313,9 +313,14 @@ public static function explode($string, $delimiter = ',', $trim = true, $skipEmp } if ($skipEmpty) { // Wrapped with array_values to make array keys sequential after empty values removing - $result = array_values(array_filter($result, function ($value) { - return $value !== ''; - })); + $result = array_values( + array_filter( + $result, + function ($value) { + return $value !== ''; + } + ) + ); } return $result; @@ -343,7 +348,7 @@ public static function countWords($string) */ public static function normalizeNumber($value) { - $value = (string) $value; + $value = (string)$value; $localeInfo = localeconv(); $decimalSeparator = isset($localeInfo['decimal_point']) ? $localeInfo['decimal_point'] : null; @@ -396,7 +401,7 @@ public static function floatToString($number) { // . and , are the only decimal separators known in ICU data, // so its safe to call str_replace here - return str_replace(',', '.', (string) $number); + return str_replace(',', '.', (string)$number); } /** @@ -422,14 +427,14 @@ public static function matchWildcard($pattern, $string, $options = []) $replacements = [ '\\\\\\\\' => '\\\\', - '\\\\\\*' => '[*]', - '\\\\\\?' => '[?]', - '\*' => '.*', - '\?' => '.', - '\[\!' => '[^', - '\[' => '[', - '\]' => ']', - '\-' => '-', + '\\\\\\*' => '[*]', + '\\\\\\?' => '[?]', + '\*' => '.*', + '\?' => '.', + '\[\!' => '[^', + '\[' => '[', + '\]' => ']', + '\-' => '-', ]; if (isset($options['escape']) && !$options['escape']) { @@ -483,7 +488,7 @@ public static function mb_ucfirst($string, $encoding = 'UTF-8') */ public static function mb_ucwords($string, $encoding = 'UTF-8') { - $string = (string) $string; + $string = (string)$string; if (empty($string)) { return $string; } diff --git a/tests/framework/helpers/ArrayHelperTest.php b/tests/framework/helpers/ArrayHelperTest.php index f450b281f7a..a086503006b 100644 --- a/tests/framework/helpers/ArrayHelperTest.php +++ b/tests/framework/helpers/ArrayHelperTest.php @@ -734,6 +734,57 @@ public function testMap() '345' => 'ccc', ], ], $result); + + $result = ArrayHelper::map($array, + static function (array $group) { + return $group['id'] . $group['name']; + }, + static function (array $group) { + return $group['name'] . $group['class']; + } + ); + + $this->assertEquals([ + '123aaa' => 'aaax', + '124bbb' => 'bbbx', + '345ccc' => 'cccy', + ], $result); + + $result = ArrayHelper::map($array, + static function (array $group) { + return $group['id'] . $group['name']; + }, + static function (array $group) { + return $group['name'] . $group['class']; + }, + static function (array $group) { + return $group['class'] . '-' . $group['class']; + } + ); + + $this->assertEquals([ + 'x-x' => [ + '123aaa' => 'aaax', + '124bbb' => 'bbbx', + ], + 'y-y' => [ + '345ccc' => 'cccy', + ], + ], $result); + + $array = [ + ['id' => '123', 'name' => 'aaa', 'class' => 'x', 'map' => ['a' => '11', 'b' => '22']], + ['id' => '124', 'name' => 'bbb', 'class' => 'x', 'map' => ['a' => '33', 'b' => '44']], + ['id' => '345', 'name' => 'ccc', 'class' => 'y', 'map' => ['a' => '55', 'b' => '66']], + ]; + + $result = ArrayHelper::map($array, 'map.a', 'map.b'); + + $this->assertEquals([ + '11' => '22', + '33' => '44', + '55' => '66' + ], $result); } public function testKeyExists() @@ -759,7 +810,7 @@ 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. From 8bd71d3d83d7483ec749c831a5acda334a7427e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Tue, 5 Nov 2024 00:58:05 +0100 Subject: [PATCH 05/17] Fix #20273: Remove unnecessary `paragonie/random_compat` dependency --- composer.json | 3 +-- composer.lock | 52 +---------------------------------------- framework/CHANGELOG.md | 1 + framework/composer.json | 3 +-- 4 files changed, 4 insertions(+), 55 deletions(-) diff --git a/composer.json b/composer.json index af172dbb762..af4ee91298d 100644 --- a/composer.json +++ b/composer.json @@ -78,8 +78,7 @@ "bower-asset/jquery": "3.7.*@stable | 3.6.*@stable | 3.5.*@stable | 3.4.*@stable | 3.3.*@stable | 3.2.*@stable | 3.1.*@stable | 2.2.*@stable | 2.1.*@stable | 1.11.*@stable | 1.12.*@stable", "bower-asset/inputmask": "^5.0.8 ", "bower-asset/punycode": "^2.2", - "bower-asset/yii2-pjax": "~2.0.1", - "paragonie/random_compat": ">=1" + "bower-asset/yii2-pjax": "~2.0.1" }, "require-dev": { "cebe/indent": "~1.0.2", diff --git a/composer.lock b/composer.lock index b1b16dc9347..f14bc829821 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "67d678d7fc90991fe6c1967c41f6264d", + "content-hash": "3faf6ba20beedc1db7758907d00f6681", "packages": [ { "name": "bower-asset/inputmask", @@ -206,56 +206,6 @@ }, "time": "2023-11-17T15:01:25+00:00" }, - { - "name": "paragonie/random_compat", - "version": "v9.99.100", - "source": { - "type": "git", - "url": "https://github.com/paragonie/random_compat.git", - "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a", - "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a", - "shasum": "" - }, - "require": { - "php": ">= 7" - }, - "require-dev": { - "phpunit/phpunit": "4.*|5.*", - "vimeo/psalm": "^1" - }, - "suggest": { - "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." - }, - "type": "library", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Paragon Initiative Enterprises", - "email": "security@paragonie.com", - "homepage": "https://paragonie.com" - } - ], - "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", - "keywords": [ - "csprng", - "polyfill", - "pseudorandom", - "random" - ], - "support": { - "email": "info@paragonie.com", - "issues": "https://github.com/paragonie/random_compat/issues", - "source": "https://github.com/paragonie/random_compat" - }, - "time": "2020-10-15T08:29:30+00:00" - }, { "name": "yiisoft/yii2-composer", "version": "2.0.10", diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index c020b20ee94..d0178f83068 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -11,6 +11,7 @@ Yii Framework 2 Change Log - Enh #20248: Add support for attaching behaviors in configurations with Closure (timkelty) - Enh #20267: Fixed called class check in `Widget::end()` when widget configured using callable (rob006, jrajamaki) - Enh #20268: Minor optimisation in `\yii\helpers\BaseArrayHelper::map` (chriscpty) +- Enh #20273: Remove unnecessary `paragonie/random_compat` dependency (timwolla) 2.0.51 July 18, 2024 -------------------- diff --git a/framework/composer.json b/framework/composer.json index d24703fb442..f5564bc959d 100644 --- a/framework/composer.json +++ b/framework/composer.json @@ -73,8 +73,7 @@ "bower-asset/jquery": "3.7.*@stable | 3.6.*@stable | 3.5.*@stable | 3.4.*@stable | 3.3.*@stable | 3.2.*@stable | 3.1.*@stable | 2.2.*@stable | 2.1.*@stable | 1.11.*@stable | 1.12.*@stable", "bower-asset/inputmask": "^5.0.8 ", "bower-asset/punycode": "^2.2", - "bower-asset/yii2-pjax": "~2.0.1", - "paragonie/random_compat": ">=1" + "bower-asset/yii2-pjax": "~2.0.1" }, "autoload": { "psr-4": {"yii\\": ""} From 5c168215d3a36d9567848b8b1e0202b05bbc14ef Mon Sep 17 00:00:00 2001 From: Mihail Date: Tue, 12 Nov 2024 01:57:49 +0300 Subject: [PATCH 06/17] Fix #20276: Removed autogenerated migration phpdoc --- framework/CHANGELOG.md | 1 + framework/views/migration.php | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index d0178f83068..5e1986a4056 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -12,6 +12,7 @@ Yii Framework 2 Change Log - Enh #20267: Fixed called class check in `Widget::end()` when widget configured using callable (rob006, jrajamaki) - Enh #20268: Minor optimisation in `\yii\helpers\BaseArrayHelper::map` (chriscpty) - Enh #20273: Remove unnecessary `paragonie/random_compat` dependency (timwolla) +- Chg #20276: Removed autogenerated migration phpdoc (userator) 2.0.51 July 18, 2024 -------------------- diff --git a/framework/views/migration.php b/framework/views/migration.php index 9c5c7cf9be8..6a725cbb48a 100644 --- a/framework/views/migration.php +++ b/framework/views/migration.php @@ -17,9 +17,6 @@ use yii\db\Migration; -/** - * Class - */ class extends Migration { /** From 717b285115e69e0034132158e6b892d3cfe504d0 Mon Sep 17 00:00:00 2001 From: Oleg Baturin Date: Sat, 23 Nov 2024 18:58:06 +0700 Subject: [PATCH 07/17] add CSRF validation by custom HTTP header --- framework/CHANGELOG.md | 3 + framework/web/Request.php | 38 ++++++++-- tests/framework/web/RequestTest.php | 108 ++++++++++++++++++++++++++++ 3 files changed, 145 insertions(+), 4 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 5e1986a4056..bca7572015c 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -13,6 +13,9 @@ Yii Framework 2 Change Log - Enh #20268: Minor optimisation in `\yii\helpers\BaseArrayHelper::map` (chriscpty) - Enh #20273: Remove unnecessary `paragonie/random_compat` dependency (timwolla) - Chg #20276: Removed autogenerated migration phpdoc (userator) +- New #20279: Add to the `\yii\web\Request` CSRF validation by custom HTTP header (olegbaturin) +- Enh #20279: Add to the `\yii\web\Request` `csrfHeader` property to configure a custom HTTP header for CSRF validation (olegbaturin) +- Enh #20279: Add to the `\yii\web\Request` `csrfTokenSafeMethods` property to configure a custom safe HTTP methods list (olegbaturin) 2.0.51 July 18, 2024 -------------------- diff --git a/framework/web/Request.php b/framework/web/Request.php index 270c9e796ff..0a66c3380f4 100644 --- a/framework/web/Request.php +++ b/framework/web/Request.php @@ -117,6 +117,26 @@ class Request extends \yii\base\Request * @see https://en.wikipedia.org/wiki/Cross-site_request_forgery */ public $enableCsrfValidation = true; + /** + * @var string the name of the HTTP header for sending CSRF token. + */ + public $csrfHeader = self::CSRF_HEADER; + /** + * @var array the name of the HTTP header for sending CSRF token. + * by default validate CSRF token on non-"safe" methods only + * @see https://tools.ietf.org/html/rfc2616#section-9.1.1 + */ + public $csrfTokenSafeMethods = ['GET', 'HEAD', 'OPTIONS']; + /** + * @var array "unsafe" methods not triggered a CORS-preflight request + * @see https://fetch.spec.whatwg.org/#http-cors-protocol + */ + public $csrfHeaderUnafeMethods = ['GET', 'HEAD', 'POST']; + /** + * @var bool whether to use custom header only to CSRF validation. Defaults to false. + * @link https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#employing-custom-request-headers-for-ajaxapi + */ + public $validateCsrfHeaderOnly = false; /** * @var string the name of the token used to prevent CSRF. Defaults to '_csrf'. * This property is used only when [[enableCsrfValidation]] is true. @@ -1772,10 +1792,14 @@ protected function loadCookies() * along via a hidden field of an HTML form or an HTTP header value to support CSRF validation. * @param bool $regenerate whether to regenerate CSRF token. When this parameter is true, each time * this method is called, a new CSRF token will be generated and persisted (in session or cookie). - * @return string the token used to perform CSRF validation. + * @return null|string the token used to perform CSRF validation. */ public function getCsrfToken($regenerate = false) { + if ($this->validateCsrfHeaderOnly) { + return null; + } + if ($this->_csrfToken === null || $regenerate) { $token = $this->loadCsrfToken(); if ($regenerate || empty($token)) { @@ -1823,7 +1847,7 @@ protected function generateCsrfToken() */ public function getCsrfTokenFromHeader() { - return $this->headers->get(static::CSRF_HEADER); + return $this->headers->get($this->csrfHeader); } /** @@ -1860,8 +1884,14 @@ protected function createCsrfCookie($token) public function validateCsrfToken($clientSuppliedToken = null) { $method = $this->getMethod(); - // only validate CSRF token on non-"safe" methods https://tools.ietf.org/html/rfc2616#section-9.1.1 - if (!$this->enableCsrfValidation || in_array($method, ['GET', 'HEAD', 'OPTIONS'], true)) { + + if ($this->validateCsrfHeaderOnly) { + return in_array($method, $this->csrfHeaderUnafeMethods, true) + ? $this->headers->has($this->csrfHeader) + : true; + } + + if (!$this->enableCsrfValidation || in_array($method, $this->csrfTokenSafeMethods, true)) { return true; } diff --git a/tests/framework/web/RequestTest.php b/tests/framework/web/RequestTest.php index ec493e95b7c..c6673c2bf5f 100644 --- a/tests/framework/web/RequestTest.php +++ b/tests/framework/web/RequestTest.php @@ -211,6 +211,114 @@ public function testCsrfTokenHeader() } } + public function testCustomSafeMethodsCsrfTokenValidation() + { + $this->mockWebApplication(); + + $request = new Request(); + $request->csrfTokenSafeMethods = ['OPTIONS']; + $request->enableCsrfCookie = false; + $request->enableCsrfValidation = true; + + $token = $request->getCsrfToken(); + + // accept any value on custom safe request + foreach (['OPTIONS'] as $method) { + $_SERVER['REQUEST_METHOD'] = $method; + $this->assertTrue($request->validateCsrfToken($token)); + $this->assertTrue($request->validateCsrfToken($token . 'a')); + $this->assertTrue($request->validateCsrfToken([])); + $this->assertTrue($request->validateCsrfToken([$token])); + $this->assertTrue($request->validateCsrfToken(0)); + $this->assertTrue($request->validateCsrfToken(null)); + $this->assertTrue($request->validateCsrfToken()); + } + + // only accept valid token on other requests + foreach (['GET', 'HEAD', 'POST'] as $method) { + $_SERVER['REQUEST_METHOD'] = $method; + $this->assertTrue($request->validateCsrfToken($token)); + $this->assertFalse($request->validateCsrfToken($token . 'a')); + $this->assertFalse($request->validateCsrfToken([])); + $this->assertFalse($request->validateCsrfToken([$token])); + $this->assertFalse($request->validateCsrfToken(0)); + $this->assertFalse($request->validateCsrfToken(null)); + $this->assertFalse($request->validateCsrfToken()); + } + } + + public function testCsrfHeaderValidation() + { + $this->mockWebApplication(); + + $request = new Request(); + $request->validateCsrfHeaderOnly = true; + $request->enableCsrfValidation = true; + + // only accept valid header on unsafe requests + foreach (['GET', 'HEAD', 'POST'] as $method) { + $_SERVER['REQUEST_METHOD'] = $method; + $request->headers->remove(Request::CSRF_HEADER); + $this->assertFalse($request->validateCsrfToken()); + + $request->headers->add(Request::CSRF_HEADER, ''); + $this->assertTrue($request->validateCsrfToken()); + } + + // accept no value on other requests + foreach (['DELETE', 'PATCH', 'PUT', 'OPTIONS'] as $method) { + $_SERVER['REQUEST_METHOD'] = $method; + $this->assertTrue($request->validateCsrfToken()); + } + } + + public function testCustomHeaderCsrfHeaderValidation() + { + $this->mockWebApplication(); + + $request = new Request(); + $request->csrfHeader = 'X-JGURDA'; + $request->validateCsrfHeaderOnly = true; + $request->enableCsrfValidation = true; + + // only accept valid header on unsafe requests + foreach (['GET', 'HEAD', 'POST'] as $method) { + $_SERVER['REQUEST_METHOD'] = $method; + $request->headers->remove('X-JGURDA'); + $this->assertFalse($request->validateCsrfToken()); + + $request->headers->add('X-JGURDA', ''); + $this->assertTrue($request->validateCsrfToken()); + } + } + + public function testCustomUnsafeMethodsCsrfHeaderValidation() + { + $this->mockWebApplication(); + + $request = new Request(); + $request->csrfHeaderUnafeMethods = ['POST']; + $request->validateCsrfHeaderOnly = true; + $request->enableCsrfValidation = true; + + // only accept valid custom header on unsafe requests + foreach (['POST'] as $method) { + $_SERVER['REQUEST_METHOD'] = $method; + $request->headers->remove(Request::CSRF_HEADER); + $this->assertFalse($request->validateCsrfToken()); + + $request->headers->add(Request::CSRF_HEADER, ''); + $this->assertTrue($request->validateCsrfToken()); + } + + // accept no value on other requests + foreach (['GET', 'HEAD'] as $method) { + $_SERVER['REQUEST_METHOD'] = $method; + $request->headers->remove(Request::CSRF_HEADER); + $this->assertTrue($request->validateCsrfToken()); + } + } + public function testResolve() { $this->mockWebApplication([ From 2ef9471af629f44a658217cfa79a074395d171d1 Mon Sep 17 00:00:00 2001 From: Oleg Baturin Date: Sat, 23 Nov 2024 19:15:20 +0700 Subject: [PATCH 08/17] update tests --- tests/framework/web/RequestTest.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/framework/web/RequestTest.php b/tests/framework/web/RequestTest.php index c6673c2bf5f..1c29c4ed200 100644 --- a/tests/framework/web/RequestTest.php +++ b/tests/framework/web/RequestTest.php @@ -319,6 +319,16 @@ public function testCustomUnsafeMethodsCsrfHeaderValidation() } } + public function testNoCsrfTokenCsrfHeaderValidation() + { + $this->mockWebApplication(); + + $request = new Request(); + $request->validateCsrfHeaderOnly = true; + + $this->assertEquals($request->getCsrfToken(), null); + } + public function testResolve() { $this->mockWebApplication([ From 9a3797b11f0dda2002dbceb5dc32d6529884b593 Mon Sep 17 00:00:00 2001 From: Oleg Baturin Date: Sun, 24 Nov 2024 17:42:00 +0700 Subject: [PATCH 09/17] update phpdoc --- framework/web/Request.php | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/framework/web/Request.php b/framework/web/Request.php index 0a66c3380f4..7ecc9b86a0f 100644 --- a/framework/web/Request.php +++ b/framework/web/Request.php @@ -42,7 +42,7 @@ * not available. * @property-read CookieCollection $cookies The cookie collection. * @property-read string $csrfToken The token used to perform CSRF validation. - * @property-read string|null $csrfTokenFromHeader The CSRF token sent via [[CSRF_HEADER]] by browser. Null is + * @property-read string|null $csrfTokenFromHeader The CSRF token sent via [[csrfHeader]] by browser. Null is * returned if no such header is sent. * @property-read array $eTags The entity tags. * @property-read HeaderCollection $headers The header collection. @@ -91,7 +91,7 @@ class Request extends \yii\base\Request { /** - * The name of the HTTP header for sending CSRF token. + * Default name of the HTTP header for sending CSRF token. */ const CSRF_HEADER = 'X-CSRF-Token'; /** @@ -113,28 +113,38 @@ class Request extends \yii\base\Request * `yii.getCsrfToken()`, respectively. The [[\yii\web\YiiAsset]] asset must be registered. * You also need to include CSRF meta tags in your pages by using [[\yii\helpers\Html::csrfMetaTags()]]. * + * For SPA, you can use CSRF validation by custom header with a random or an empty value. + * Include a header with the name specified by [[csrfHeader]] to requests that must be validated. + * Warning! CSRF validation by custom header can be used only for same-origin requests or + * with CORS configured to allow requests from the list of specific origins only. + * * @see Controller::enableCsrfValidation * @see https://en.wikipedia.org/wiki/Cross-site_request_forgery */ public $enableCsrfValidation = true; - /** - * @var string the name of the HTTP header for sending CSRF token. + /** + * @var string the name of the HTTP header for sending CSRF token. Defaults [[CSRF_HEADER]]. + * This property can be changed for Yii API applications only. + * Don't change this property for Yii Web application. */ public $csrfHeader = self::CSRF_HEADER; /** * @var array the name of the HTTP header for sending CSRF token. * by default validate CSRF token on non-"safe" methods only + * This property is used only when [[enableCsrfValidation]] is true. * @see https://tools.ietf.org/html/rfc2616#section-9.1.1 */ public $csrfTokenSafeMethods = ['GET', 'HEAD', 'OPTIONS']; /** * @var array "unsafe" methods not triggered a CORS-preflight request + * This property is used only when both [[enableCsrfValidation]] and [[validateCsrfHeaderOnly]] are true. * @see https://fetch.spec.whatwg.org/#http-cors-protocol */ public $csrfHeaderUnafeMethods = ['GET', 'HEAD', 'POST']; /** - * @var bool whether to use custom header only to CSRF validation. Defaults to false. - * @link https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#employing-custom-request-headers-for-ajaxapi + * @var bool whether to use custom header only to CSRF validation of SPA. Defaults to false. + * If false and [[enableCsrfValidation]] is true, CSRF validation by token will used. + * @see https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#employing-custom-request-headers-for-ajaxapi */ public $validateCsrfHeaderOnly = false; /** @@ -1792,7 +1802,7 @@ protected function loadCookies() * along via a hidden field of an HTML form or an HTTP header value to support CSRF validation. * @param bool $regenerate whether to regenerate CSRF token. When this parameter is true, each time * this method is called, a new CSRF token will be generated and persisted (in session or cookie). - * @return null|string the token used to perform CSRF validation. + * @return null|string the token used to perform CSRF validation. Null is returned if the [[validateCsrfHeaderOnly]] is true. */ public function getCsrfToken($regenerate = false) { @@ -1843,7 +1853,7 @@ protected function generateCsrfToken() } /** - * @return string|null the CSRF token sent via [[CSRF_HEADER]] by browser. Null is returned if no such header is sent. + * @return string|null the CSRF token sent via [[csrfHeader]] by browser. Null is returned if no such header is sent. */ public function getCsrfTokenFromHeader() { From e6e8311d575ed2f8e1389767b087e97362731f10 Mon Sep 17 00:00:00 2001 From: Oleg Baturin Date: Mon, 25 Nov 2024 13:58:16 +0700 Subject: [PATCH 10/17] fix phpdoc --- framework/web/Request.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/framework/web/Request.php b/framework/web/Request.php index 7ecc9b86a0f..0e6af8cbee9 100644 --- a/framework/web/Request.php +++ b/framework/web/Request.php @@ -123,8 +123,8 @@ class Request extends \yii\base\Request */ public $enableCsrfValidation = true; /** - * @var string the name of the HTTP header for sending CSRF token. Defaults [[CSRF_HEADER]]. - * This property can be changed for Yii API applications only. + * @var string the name of the HTTP header for sending CSRF token. Defaults to [[CSRF_HEADER]]. + * This property may be changed for Yii API applications only. * Don't change this property for Yii Web application. */ public $csrfHeader = self::CSRF_HEADER; @@ -132,7 +132,7 @@ class Request extends \yii\base\Request * @var array the name of the HTTP header for sending CSRF token. * by default validate CSRF token on non-"safe" methods only * This property is used only when [[enableCsrfValidation]] is true. - * @see https://tools.ietf.org/html/rfc2616#section-9.1.1 + * @see https://datatracker.ietf.org/doc/html/rfc9110#name-safe-methods */ public $csrfTokenSafeMethods = ['GET', 'HEAD', 'OPTIONS']; /** @@ -144,6 +144,7 @@ class Request extends \yii\base\Request /** * @var bool whether to use custom header only to CSRF validation of SPA. Defaults to false. * If false and [[enableCsrfValidation]] is true, CSRF validation by token will used. + * Warning! CSRF validation by custom header can be used for Yii API applications only. * @see https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#employing-custom-request-headers-for-ajaxapi */ public $validateCsrfHeaderOnly = false; From 927740ee670f7116b8a8679078e2ddde6cb03bae Mon Sep 17 00:00:00 2001 From: "Stefano D. Mtangoo" Date: Tue, 26 Nov 2024 19:55:50 +0300 Subject: [PATCH 11/17] Revert punycode to 1.4.x which supports pre ES6 format --- framework/composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/composer.json b/framework/composer.json index f5564bc959d..72e5079cdff 100644 --- a/framework/composer.json +++ b/framework/composer.json @@ -72,7 +72,7 @@ "cebe/markdown": "~1.0.0 | ~1.1.0 | ~1.2.0", "bower-asset/jquery": "3.7.*@stable | 3.6.*@stable | 3.5.*@stable | 3.4.*@stable | 3.3.*@stable | 3.2.*@stable | 3.1.*@stable | 2.2.*@stable | 2.1.*@stable | 1.11.*@stable | 1.12.*@stable", "bower-asset/inputmask": "^5.0.8 ", - "bower-asset/punycode": "^2.2", + "bower-asset/punycode": "^1.4", "bower-asset/yii2-pjax": "~2.0.1" }, "autoload": { From b49fa88f23b50f5d23af5f1340ebee937c1a0043 Mon Sep 17 00:00:00 2001 From: "Stefano D. Mtangoo" Date: Wed, 27 Nov 2024 18:26:41 +0300 Subject: [PATCH 12/17] Include missing changelog for #20284 (#20286) * Include missing changelog for #20284 * Update CHANGELOG.md --- framework/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 5e1986a4056..c1531675bdb 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -13,6 +13,7 @@ Yii Framework 2 Change Log - Enh #20268: Minor optimisation in `\yii\helpers\BaseArrayHelper::map` (chriscpty) - Enh #20273: Remove unnecessary `paragonie/random_compat` dependency (timwolla) - Chg #20276: Removed autogenerated migration phpdoc (userator) +- Bug #20284: Revert punycode to 1.4.x which supports pre ES6 format (mtangoo) 2.0.51 July 18, 2024 -------------------- From 36b34b0dc1e029c9686c771ba27f2d960b4afda4 Mon Sep 17 00:00:00 2001 From: Oleg Baturin Date: Thu, 28 Nov 2024 12:01:15 +0700 Subject: [PATCH 13/17] fix 'unafe' typo --- framework/web/Request.php | 4 ++-- tests/framework/web/RequestTest.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/framework/web/Request.php b/framework/web/Request.php index 0e6af8cbee9..312654fc679 100644 --- a/framework/web/Request.php +++ b/framework/web/Request.php @@ -140,7 +140,7 @@ class Request extends \yii\base\Request * This property is used only when both [[enableCsrfValidation]] and [[validateCsrfHeaderOnly]] are true. * @see https://fetch.spec.whatwg.org/#http-cors-protocol */ - public $csrfHeaderUnafeMethods = ['GET', 'HEAD', 'POST']; + public $csrfHeaderUnsafeMethods = ['GET', 'HEAD', 'POST']; /** * @var bool whether to use custom header only to CSRF validation of SPA. Defaults to false. * If false and [[enableCsrfValidation]] is true, CSRF validation by token will used. @@ -1897,7 +1897,7 @@ public function validateCsrfToken($clientSuppliedToken = null) $method = $this->getMethod(); if ($this->validateCsrfHeaderOnly) { - return in_array($method, $this->csrfHeaderUnafeMethods, true) + return in_array($method, $this->csrfHeaderUnsafeMethods, true) ? $this->headers->has($this->csrfHeader) : true; } diff --git a/tests/framework/web/RequestTest.php b/tests/framework/web/RequestTest.php index 1c29c4ed200..932392e187d 100644 --- a/tests/framework/web/RequestTest.php +++ b/tests/framework/web/RequestTest.php @@ -297,7 +297,7 @@ public function testCustomUnsafeMethodsCsrfHeaderValidation() $this->mockWebApplication(); $request = new Request(); - $request->csrfHeaderUnafeMethods = ['POST']; + $request->csrfHeaderUnsafeMethods = ['POST']; $request->validateCsrfHeaderOnly = true; $request->enableCsrfValidation = true; From 8fe8cdd4d99cd748577b1de58ceca22fa7cb7f60 Mon Sep 17 00:00:00 2001 From: Tobias Munk Date: Fri, 6 Dec 2024 07:10:17 +0100 Subject: [PATCH 14/17] reset useCookie value (#20287) * reset useCookie value see also https://github.com/yiisoft/yii2-docker/issues/174 * removed empty line --- tests/framework/web/session/SessionTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/framework/web/session/SessionTest.php b/tests/framework/web/session/SessionTest.php index 1270aa4afed..7dfe1f17d90 100644 --- a/tests/framework/web/session/SessionTest.php +++ b/tests/framework/web/session/SessionTest.php @@ -65,6 +65,7 @@ public function testParamsAfterSessionStart() $this->assertNotEquals($oldUseCookies, $newUseCookies); $this->assertFalse($newUseCookies); } + $session->setUseCookies($oldUseCookies); $oldGcProbability = $session->getGCProbability(); $session->setGCProbability(100); From 80f2545cf147abc731db3cd06961aa3530bb76ac Mon Sep 17 00:00:00 2001 From: Viktor Khokhryakov Date: Fri, 6 Dec 2024 10:11:14 +0400 Subject: [PATCH 15/17] Fixes CS (#20288) --- framework/db/BaseActiveRecord.php | 2 +- framework/db/mssql/Schema.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/framework/db/BaseActiveRecord.php b/framework/db/BaseActiveRecord.php index 10021e362cc..761bc2cb994 100644 --- a/framework/db/BaseActiveRecord.php +++ b/framework/db/BaseActiveRecord.php @@ -1783,7 +1783,7 @@ private function isValueDifferent($newValue, $oldValue) { if (is_array($newValue) && is_array($oldValue)) { // Only sort associative arrays - $sorter = function(&$array) { + $sorter = function (&$array) { if (ArrayHelper::isAssociative($array)) { ksort($array); } diff --git a/framework/db/mssql/Schema.php b/framework/db/mssql/Schema.php index 35908437f48..1b1fb267e51 100644 --- a/framework/db/mssql/Schema.php +++ b/framework/db/mssql/Schema.php @@ -823,5 +823,4 @@ public function createColumnSchemaBuilder($type, $length = null) { return Yii::createObject(ColumnSchemaBuilder::className(), [$type, $length, $this->db]); } - } From 5df412df2c9b4e60e7ec84008693b94ae472eae6 Mon Sep 17 00:00:00 2001 From: Viktor Khokhryakov Date: Fri, 6 Dec 2024 13:31:20 +0400 Subject: [PATCH 16/17] Fix #20282: Fix compatibility with PHP 8.4: deprecated constant E_STRICT --- framework/CHANGELOG.md | 1 + framework/base/ErrorException.php | 5 ++--- tests/framework/base/ErrorExceptionTest.php | 9 +++++++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 8263b086376..f78849fa0cb 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -13,6 +13,7 @@ Yii Framework 2 Change Log - Enh #20268: Minor optimisation in `\yii\helpers\BaseArrayHelper::map` (chriscpty) - Enh #20273: Remove unnecessary `paragonie/random_compat` dependency (timwolla) - Chg #20276: Removed autogenerated migration phpdoc (userator) +- Bug #20282: Fix compatibility with PHP 8.4: deprecated constant E_STRICT (Izumi-kun) - Bug #20284: Revert punycode to 1.4.x which supports pre ES6 format (mtangoo) - New #20279: Add to the `\yii\web\Request` CSRF validation by custom HTTP header (olegbaturin) - Enh #20279: Add to the `\yii\web\Request` `csrfHeader` property to configure a custom HTTP header for CSRF validation (olegbaturin) diff --git a/framework/base/ErrorException.php b/framework/base/ErrorException.php index f9c388a5453..a5af2a8027a 100644 --- a/framework/base/ErrorException.php +++ b/framework/base/ErrorException.php @@ -124,15 +124,14 @@ public function getName() E_NOTICE => 'PHP Notice', E_PARSE => 'PHP Parse Error', E_RECOVERABLE_ERROR => 'PHP Recoverable Error', - E_STRICT => 'PHP Strict Warning', E_USER_DEPRECATED => 'PHP User Deprecated Warning', E_USER_ERROR => 'PHP User Error', E_USER_NOTICE => 'PHP User Notice', E_USER_WARNING => 'PHP User Warning', E_WARNING => 'PHP Warning', self::E_HHVM_FATAL_ERROR => 'HHVM Fatal Error', - ]; + ] + (PHP_VERSION_ID < 80400 ? [E_STRICT => 'PHP Strict Warning'] : []); - return isset($names[$this->getCode()]) ? $names[$this->getCode()] : 'Error'; + return $names[$this->getCode()] ?? 'Error'; } } diff --git a/tests/framework/base/ErrorExceptionTest.php b/tests/framework/base/ErrorExceptionTest.php index 615b706fa6d..0054136ad7f 100644 --- a/tests/framework/base/ErrorExceptionTest.php +++ b/tests/framework/base/ErrorExceptionTest.php @@ -41,4 +41,13 @@ public function testXdebugTrace() $this->assertEquals(__FUNCTION__, $e->getTrace()[0]['function']); } } + + public function testStrictError() + { + if (!defined('E_STRICT')) { + $this->markTestSkipped('E_STRICT has been removed.'); + } + $e = new ErrorException('', @E_STRICT); + $this->assertEquals(PHP_VERSION_ID < 80400 ? 'PHP Strict Warning' : 'Error', $e->getName()); + } } From 65e3369e1663e3596c16ed09ce6b08f3ad3271d9 Mon Sep 17 00:00:00 2001 From: Viktor Khokhryakov Date: Fri, 6 Dec 2024 14:34:47 +0400 Subject: [PATCH 17/17] Fix #20140: Fix compatibility with PHP 8.4: calling `session_set_save_handler()` --- framework/CHANGELOG.md | 1 + framework/UPGRADE.md | 7 ++ framework/web/CacheSession.php | 2 +- framework/web/DbSession.php | 8 +- framework/web/Session.php | 37 +++------ framework/web/SessionHandler.php | 79 +++++++++++++++++++ .../web/session/AbstractDbSessionTest.php | 3 +- 7 files changed, 106 insertions(+), 31 deletions(-) create mode 100644 framework/web/SessionHandler.php diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index f78849fa0cb..600a3420746 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -18,6 +18,7 @@ Yii Framework 2 Change Log - New #20279: Add to the `\yii\web\Request` CSRF validation by custom HTTP header (olegbaturin) - Enh #20279: Add to the `\yii\web\Request` `csrfHeader` property to configure a custom HTTP header for CSRF validation (olegbaturin) - Enh #20279: Add to the `\yii\web\Request` `csrfTokenSafeMethods` property to configure a custom safe HTTP methods list (olegbaturin) +- Bug #20140: Fix compatibility with PHP 8.4: calling `session_set_save_handler()` (Izumi-kun) 2.0.51 July 18, 2024 -------------------- diff --git a/framework/UPGRADE.md b/framework/UPGRADE.md index f75f025301c..72aa39821d6 100644 --- a/framework/UPGRADE.md +++ b/framework/UPGRADE.md @@ -51,6 +51,13 @@ if you want to upgrade from version A to version C and there is version B between A and C, you need to follow the instructions for both A and B. +Upgrade from Yii 2.0.51 +----------------------- + +* The function signature for `yii\web\Session::readSession()` and `yii\web\Session::gcSession()` have been changed. + They now have the same return types as `\SessionHandlerInterface::read()` and `\SessionHandlerInterface::gc()` respectively. + In case those methods have overwritten you will need to update your child classes accordingly. + Upgrade from Yii 2.0.50 ----------------------- diff --git a/framework/web/CacheSession.php b/framework/web/CacheSession.php index 5763a854096..f3d0d87a1af 100644 --- a/framework/web/CacheSession.php +++ b/framework/web/CacheSession.php @@ -92,7 +92,7 @@ public function openSession($savePath, $sessionName) * Session read handler. * @internal Do not call this method directly. * @param string $id session ID - * @return string the session data + * @return string|false the session data, or false on failure */ public function readSession($id) { diff --git a/framework/web/DbSession.php b/framework/web/DbSession.php index 7c7743b509d..1ef79cb7c6a 100644 --- a/framework/web/DbSession.php +++ b/framework/web/DbSession.php @@ -171,7 +171,7 @@ public function close() * Session read handler. * @internal Do not call this method directly. * @param string $id session ID - * @return string the session data + * @return string|false the session data, or false on failure */ public function readSession($id) { @@ -247,15 +247,13 @@ public function destroySession($id) * Session GC (garbage collection) handler. * @internal Do not call this method directly. * @param int $maxLifetime the number of seconds after which data will be seen as 'garbage' and cleaned up. - * @return bool whether session is GCed successfully + * @return int|false the number of deleted sessions on success, or false on failure */ public function gcSession($maxLifetime) { - $this->db->createCommand() + return $this->db->createCommand() ->delete($this->sessionTable, '[[expire]]<:expire', [':expire' => time()]) ->execute(); - - return true; } /** diff --git a/framework/web/Session.php b/framework/web/Session.php index e9c3ebd5c48..98e80495500 100644 --- a/framework/web/Session.php +++ b/framework/web/Session.php @@ -175,34 +175,23 @@ protected function registerSessionHandler() static::$_originalSessionModule = $sessionModuleName; } + if ($this->handler === null && $this->getUseCustomStorage()) { + $this->handler = Yii::createObject( + [ + '__class' => SessionHandler::class, + '__construct()' => [$this], + ] + ); + } + if ($this->handler !== null) { - if (!is_object($this->handler)) { + if (is_array($this->handler)) { $this->handler = Yii::createObject($this->handler); } if (!$this->handler instanceof \SessionHandlerInterface) { throw new InvalidConfigException('"' . get_class($this) . '::handler" must implement the SessionHandlerInterface.'); } YII_DEBUG ? session_set_save_handler($this->handler, false) : @session_set_save_handler($this->handler, false); - } elseif ($this->getUseCustomStorage()) { - if (YII_DEBUG) { - session_set_save_handler( - [$this, 'openSession'], - [$this, 'closeSession'], - [$this, 'readSession'], - [$this, 'writeSession'], - [$this, 'destroySession'], - [$this, 'gcSession'] - ); - } else { - @session_set_save_handler( - [$this, 'openSession'], - [$this, 'closeSession'], - [$this, 'readSession'], - [$this, 'writeSession'], - [$this, 'destroySession'], - [$this, 'gcSession'] - ); - } } elseif ( $sessionModuleName !== static::$_originalSessionModule && static::$_originalSessionModule !== null @@ -610,7 +599,7 @@ public function closeSession() * This method should be overridden if [[useCustomStorage]] returns true. * @internal Do not call this method directly. * @param string $id session ID - * @return string the session data + * @return string|false the session data, or false on failure */ public function readSession($id) { @@ -647,11 +636,11 @@ public function destroySession($id) * This method should be overridden if [[useCustomStorage]] returns true. * @internal Do not call this method directly. * @param int $maxLifetime the number of seconds after which data will be seen as 'garbage' and cleaned up. - * @return bool whether session is GCed successfully + * @return int|false the number of deleted sessions on success, or false on failure */ public function gcSession($maxLifetime) { - return true; + return 0; } /** diff --git a/framework/web/SessionHandler.php b/framework/web/SessionHandler.php new file mode 100644 index 00000000000..7382f4545d6 --- /dev/null +++ b/framework/web/SessionHandler.php @@ -0,0 +1,79 @@ + + * @since 2.0.52 + */ +class SessionHandler implements SessionHandlerInterface +{ + /** + * @var Session + */ + private $_session; + + public function __construct(Session $session) + { + $this->_session = $session; + } + + /** + * @inheritDoc + */ + public function close(): bool + { + return $this->_session->closeSession(); + } + + /** + * @inheritDoc + */ + public function destroy($id): bool + { + return $this->_session->destroySession($id); + } + + /** + * @inheritDoc + */ + #[\ReturnTypeWillChange] + public function gc($max_lifetime) + { + return $this->_session->gcSession($max_lifetime); + } + + /** + * @inheritDoc + */ + public function open($path, $name): bool + { + return $this->_session->openSession($path, $name); + } + + /** + * @inheritDoc + */ + #[\ReturnTypeWillChange] + public function read($id) + { + return $this->_session->readSession($id); + } + + /** + * @inheritDoc + */ + public function write($id, $data): bool + { + return $this->_session->writeSession($id, $data); + } +} diff --git a/tests/framework/web/session/AbstractDbSessionTest.php b/tests/framework/web/session/AbstractDbSessionTest.php index f8dbb32f550..2fe0fa2cdff 100644 --- a/tests/framework/web/session/AbstractDbSessionTest.php +++ b/tests/framework/web/session/AbstractDbSessionTest.php @@ -127,8 +127,9 @@ public function testGarbageCollection() $session->db->createCommand() ->update('session', ['expire' => time() - 100], 'id = :id', ['id' => 'expire']) ->execute(); - $session->gcSession(1); + $deleted = $session->gcSession(1); + $this->assertEquals(1, $deleted); $this->assertEquals('', $session->readSession('expire')); $this->assertEquals('new data', $session->readSession('new')); }