Browse Source

Merge remote-tracking branch 'upstream/master'

tags/2.0.7
Tom Worster 9 years ago
parent
commit
2a746c381c
  1. 5
      docs/guide-ja/concept-aliases.md
  2. 76
      docs/guide-ja/concept-events.md
  3. 7
      docs/guide-ja/db-query-builder.md
  4. 68
      docs/guide-ru/concept-events.md
  5. 2
      docs/guide-ru/output-theming.md
  6. 2
      docs/guide-zh-CN/structure-controllers.md
  7. 8
      docs/guide/concept-aliases.md
  8. 72
      docs/guide/concept-events.md
  9. 6
      docs/guide/db-query-builder.md
  10. 3
      docs/guide/output-formatting.md
  11. 2
      docs/guide/rest-controllers.md
  12. 2
      docs/internals-ja/core-code-style.md
  13. 6
      docs/internals-ja/git-workflow.md
  14. 4
      docs/internals-ja/versions.md
  15. 3
      docs/internals-ru/translation-workflow.md
  16. 8
      framework/CHANGELOG.md
  17. 26
      framework/base/Event.php
  18. 5
      framework/console/Controller.php
  19. 25
      framework/db/ColumnSchemaBuilder.php
  20. 28
      framework/db/mssql/ColumnSchemaBuilder.php
  21. 1
      framework/db/mssql/QueryBuilder.php
  22. 8
      framework/db/mssql/Schema.php
  23. 1
      framework/db/mysql/Schema.php
  24. 1
      framework/db/oci/ColumnSchemaBuilder.php
  25. 28
      framework/db/pgsql/ColumnSchemaBuilder.php
  26. 8
      framework/db/pgsql/Schema.php
  27. 1
      framework/db/sqlite/QueryBuilder.php
  28. 8
      framework/di/Instance.php
  29. 39
      framework/i18n/Formatter.php
  30. 10
      framework/i18n/MessageFormatter.php
  31. 85
      framework/messages/fa/yii.php
  32. 3
      framework/web/Response.php
  33. 3
      framework/web/Session.php
  34. 36
      tests/framework/base/EventTest.php
  35. 23
      tests/framework/console/ControllerTest.php
  36. 69
      tests/framework/db/ColumnSchemaBuilderTest.php
  37. 37
      tests/framework/db/mssql/ColumnSchemaBuilderTest.php
  38. 22
      tests/framework/db/oci/ColumnSchemaBuilderTest.php
  39. 37
      tests/framework/db/pgsql/ColumnSchemaBuilderTest.php
  40. 73
      tests/framework/di/InstanceTest.php
  41. 7
      tests/framework/helpers/HtmlTest.php
  42. 55
      tests/framework/i18n/FallbackMessageFormatterTest.php
  43. 37
      tests/framework/i18n/FormatterDateTest.php
  44. 40
      tests/framework/i18n/MessageFormatterTest.php
  45. 2
      tests/framework/web/AssetBundleTest.php

5
docs/guide-ja/concept-aliases.md

@ -1,7 +1,10 @@
エイリアス
=======
ファイルパスや URL を表すのにエイリアスを使用すると、あなたはプロジェクト内で絶対パスや URL をハードコードする必要がなくなります。エイリアスは、通常のファイルパスや URL と区別するために、 `@` 文字で始まる必要があります。Yii はすでに利用可能な多くの事前定義エイリアスを持っています。
ファイルパスや URL を表すのにエイリアスを使用すると、あなたはプロジェクト内で絶対パスや URL をハードコードする必要がなくなります。エイリアスは、通常のファイルパスや URL と区別するために、 `@` 文字で始まる必要があります。
先頭に `@` を付けずに定義されたエイリアスは、`@` 文字が先頭に追加されます。
Yii はすでに利用可能な多くの事前定義エイリアスを持っています。
たとえば、 `@yii` というエイリアスは Yii フレームワークのインストールパスを表し、 `@web` は現在実行中の Web アプリケーションのベース URL を表します。

76
docs/guide-ja/concept-events.md

@ -98,7 +98,7 @@ $foo->on(Foo::EVENT_HELLO, function ($event) {
}, $data, false);
```
イベントのトリガ <span id="triggering-events"></span>
イベントのトリガ <span id="triggering-events"></span>
-----------------
イベントは、 [[yii\base\Component::trigger()]] メソッドを呼び出すことでトリガされます。このメソッドには **イベント名** が必須で、
@ -214,7 +214,7 @@ Event::on(ActiveRecord::className(), ActiveRecord::EVENT_AFTER_INSERT, function
```
[[yii\db\ActiveRecord|ActiveRecord]] またはその子クラスのいずれかが、 [[yii\db\BaseActiveRecord::EVENT_AFTER_INSERT|EVENT_AFTER_INSERT]]
をトリガするといつでも、このイベントハンドラが呼び出されます。ハンドラの中では、 `$event->sender` を通して、
をトリガするといつでも、このイベントハンドラが呼び出されます。ハンドラの中では、 `$event->sender` を通して、
イベントをトリガしたオブジェクトを取得することができます。
オブジェクトがイベントをトリガするときは、最初にインスタンスレベルのハンドラを呼び出し、続いてクラスレベルのハンドラとなります。
@ -233,7 +233,7 @@ Event::on(Foo::className(), Foo::EVENT_HELLO, function ($event) {
Event::trigger(Foo::className(), Foo::EVENT_HELLO);
```
この場合、`$event->sender` は、オブジェクトインスタンスではなく、イベントをトリガするクラスの名前を指すことに注意してください。
この場合、`$event->sender` は、オブジェクトインスタンスではなく、イベントをトリガするクラスの名前を指すことに注意してください。
> Note: クラスレベルのハンドラは、そのクラスのあらゆるインスタンス、またはあらゆる子クラスのインスタンスがトリガしたイベントに応答
してしまうため、よく注意して使わなければなりません。 [[yii\base\Object]] のように、クラスが低レベルの基底クラスの場合は特にそうです。
@ -249,6 +249,76 @@ Event::off(Foo::className(), Foo::EVENT_HELLO);
```
インターフェイスを使うイベント <span id="interface-level-event-handlers"></span>
------------------------------
イベントを扱うためには、もっと抽象的な方法もあります。
特定のイベントのために専用のインターフェイスを作っておき、必要な場合にいろいろなクラスでそれを実装するのです。
例えば、次のようなインタフェイスを作ります。
```php
interface DanceEventInterface
{
const EVENT_DANCE = 'dance';
}
```
そして、それを実装する二つのクラスを作ります。
```php
class Dog extends Component implements DanceEventInterface
{
public function meetBuddy()
{
echo "ワン!";
$this->trigger(DanceEventInterface::EVENT_DANCE);
}
}
class Developer extends Component implements DanceEventInterface
{
public function testsPassed()
{
echo "よっしゃ!";
$this->trigger(DanceEventInterface::EVENT_DANCE);
}
}
```
これらのクラスのどれかによってトリガされた `EVENT_DANCE` を扱うためには、インターフェイスの名前を最初の引数にして [[yii\base\Event::on()|Event::on()]] を呼びます。
```php
Event::on('DanceEventInterface', DanceEventInterface::EVENT_DANCE, function ($event) {
Yii::trace($event->sender->className . ' が躍り上がって喜んだ。'); // 犬または開発者が躍り上がって喜んだことをログに記録。
})
```
これらのクラスのイベントをトリガすることも出来ます。
```php
Event::trigger(DanceEventInterface::className(), DanceEventInterface::EVENT_DANCE);
```
ただし、このインタフェイスを実装する全クラスのイベントをトリガすることは出来ない、ということに注意して下さい。
```php
// これは動かない
Event::trigger('DanceEventInterface', DanceEventInterface::EVENT_DANCE); // エラー
```
イベントハンドラをデタッチするためには、[[yii\base\Event::off()|Event::off()]] を呼びます。
例えば、
```php
// $handler をデタッチ
Event::off('DanceEventInterface', DanceEventInterface::EVENT_DANCE, $handler);
// DanceEventInterface::EVENT_DANCE の全てのハンドラをデタッチ
Event::off('DanceEventInterface', DanceEventInterface::EVENT_DANCE);
```
グローバル・イベント <span id="global-events"></span>
-------------

7
docs/guide-ja/db-query-builder.md

@ -208,6 +208,10 @@ $userQuery = (new Query())->select('id')->from('user');
$query->where(['id' => $userQuery]);
```
ハッシュ形式を使う場合、Yii は内部的にパラメータバインディングを使用します。
従って、[文字列形式](#string-format) とは対照的に、ここでは手動でパラメータを追加する必要はありません。
#### 演算子形式 <span id="operator-format"></span>
演算子形式を使うと、任意の条件をプログラム的な方法で指定することが出来ます。
@ -269,6 +273,9 @@ $query->where(['id' => $userQuery]);
- `>`、`<=`、その他、二つのオペランドを取る有効な DB 演算子全て: 最初のオペランドはカラム名、第二のオペランドは値でなければなりません。
例えば、`['>', 'age', 10]` は `age>10` を生成します。
演算子形式を使う場合、Yii は内部的にパラメータバインディングを使用します。
従って、[文字列形式](#string-format) とは対照的に、ここでは手動でパラメータを追加する必要はありません。
#### 条件を追加する <span id="appending-conditions"></span>

68
docs/guide-ru/concept-events.md

@ -222,6 +222,74 @@ Event::off(Foo::className(), Foo::EVENT_HELLO, $handler);
Event::off(Foo::className(), Foo::EVENT_HELLO);
```
Обработчики событий на уровне интерфейсов <span id="interface-level-event-handlers"></span>
-------------
Существует еще более абстрактный способ обработки событий.
Вы можете создать отдельный интерфейс для общего события и реализовать его в классах, где это необходимо.
Например, создадим следующий интерфейс:
```php
interface DanceEventInterface
{
const EVENT_DANCE = 'dance';
}
```
И два класса, которые его реализовывают:
```php
class Dog extends Component implements DanceEventInterface
{
public function meetBuddy()
{
echo "Woof!";
$this->trigger(DanceEventInterface::EVENT_DANCE);
}
}
class Developer extends Component implements DanceEventInterface
{
public function testsPassed()
{
echo "Yay!";
$this->trigger(DanceEventInterface::EVENT_DANCE);
}
}
```
Для обработки события `EVENT_DANCE`, инициализированного любым из этих классов,
вызовите [[yii\base\Event::on()|Event:on()]], передав ему в качестве первого параметра имя интерфейса.
```php
Event::on('DanceEventInterface', DanceEventInterface::EVENT_DANCE, function ($event) {
Yii::trace($event->sender->className . ' just danced'); // Оставит запись в журнале о том, что кто-то танцевал
});
```
Вы можете также инициализировать эти события:
```php
Event::trigger(DanceEventInterface::className(), DanceEventInterface::EVENT_DANCE);
```
Однако, невозможно инициализировать событие во всех классах, которые реализуют интерфейс:
```php
// НЕ БУДЕТ РАБОТАТЬ
Event::trigger('DanceEventInterface', DanceEventInterface::EVENT_DANCE); // ошибка
```
Отсоединить обработчик события можно с помощью метода [[yii\base\Event::off()|Event::off()]]. Например:
```php
// отсоединяет $handler
Event::off('DanceEventInterface', DanceEventInterface::EVENT_DANCE, $handler);
// отсоединяются все обработчики DanceEventInterface::EVENT_DANCE
Event::off('DanceEventInterface', DanceEventInterface::EVENT_DANCE);
```
Глобальные события <span id="global-events"></span>
-------------

2
docs/guide-ru/output-theming.md

@ -100,7 +100,7 @@ $file = $theme->getPath('img/logo.gif');
]
```
В этом случае представление `@app/views/site/about.php` темизируется либо в `@app/themes/christmas/site/index.php`,
В этом случае представление `@app/views/site/index.php` темизируется либо в `@app/themes/christmas/site/index.php`,
либо в `@app/themes/basic/site/index.php` в зависимости от того, в какой из тем есть нужный файл. Если файлы присутствуют
и там и там, используется первый из них. На практике большинство темизированных файлов будут расположены
в `@app/themes/basic`, а их версии для праздников в `@app/themes/christmas`.

2
docs/guide-zh-CN/structure-controllers.md

@ -61,7 +61,7 @@ class PostController extends Controller
终端用户通过所谓的*路由*寻找到操作,路由是包含以下部分的字符串:
* 模ID: 仅存在于控制器属于非应用的[模块](structure-modules.md);
* 模ID: 仅存在于控制器属于非应用的[模块](structure-modules.md);
* 控制器ID: 同应用(或同模块如果为模块下的控制器)下唯一标识控制器的字符串;
* 操作ID: 同控制器下唯一标识操作的字符串。

8
docs/guide/concept-aliases.md

@ -1,10 +1,12 @@
Aliases
=======
Aliases are used to represent file paths or URLs so that you don't have to hard-code absolute paths or URLs in your project. An alias must start with the `@` character to be differentiated from normal file paths and URLs. Yii has many pre-defined aliases already available.
For example, the alias `@yii` represents the installation path of the Yii framework; `@web` represents
the base URL for the currently running Web application.
Aliases are used to represent file paths or URLs so that you don't have to hard-code absolute paths or URLs in your
project. An alias must start with the `@` character to be differentiated from normal file paths and URLs. Alias defined
without leading `@` will be prefixed with `@` character.
Yii has many pre-defined aliases already available. For example, the alias `@yii` represents the installation path of
the Yii framework; `@web` represents the base URL for the currently running Web application.
Defining Aliases <span id="defining-aliases"></span>
----------------

72
docs/guide/concept-events.md

@ -252,6 +252,76 @@ Event::off(Foo::className(), Foo::EVENT_HELLO);
```
Events using interfaces <span id="interface-level-event-handlers"></span>
-------------
There is even more abstract way to deal with events. You can create a separated interface for the special event and
implement it in classes, where you need it.
For example we can create the following interface:
```php
interface DanceEventInterface
{
const EVENT_DANCE = 'dance';
}
```
And two classes, that implement it:
```php
class Dog extends Component implements DanceEventInterface
{
public function meetBuddy()
{
echo "Woof!";
$this->trigger(DanceEventInterface::EVENT_DANCE);
}
}
class Developer extends Component implements DanceEventInterface
{
public function testsPassed()
{
echo "Yay!";
$this->trigger(DanceEventInterface::EVENT_DANCE);
}
}
```
To handle the `EVENT_DANCE`, triggered by any of these classes, call [[yii\base\Event::on()|Event::on()]] and
pass the interface name as the first argument:
```php
Event::on('DanceEventInterface', DanceEventInterface::EVENT_DANCE, function ($event) {
Yii::trace($event->sender->className . ' just danced'); // Will log that Dog or Developer danced
})
```
You can trigger the event of those classes:
```php
Event::trigger(DanceEventInterface::className(), DanceEventInterface::EVENT_DANCE);
```
But please notice, that you can not trigger all the classes, that implement the interface:
```php
// DOES NOT WORK
Event::trigger('DanceEventInterface', DanceEventInterface::EVENT_DANCE); // error
```
Do detach event handler, call [[yii\base\Event::off()|Event::off()]]. For example:
```php
// detaches $handler
Event::off('DanceEventInterface', DanceEventInterface::EVENT_DANCE, $handler);
// detaches all handlers of DanceEventInterface::EVENT_DANCE
Event::off('DanceEventInterface', DanceEventInterface::EVENT_DANCE);
```
Global Events <span id="global-events"></span>
-------------
@ -278,4 +348,4 @@ which will be triggered by the object. Instead, the handler attachment and the e
done through the Singleton (e.g. the application instance).
However, because the namespace of the global events is shared by all parties, you should name the global events
wisely, such as introducing some sort of namespace (e.g. "frontend.mail.sent", "backend.mail.sent").
wisely, such as introducing some sort of namespace (e.g. "frontend.mail.sent", "backend.mail.sent").

6
docs/guide/db-query-builder.md

@ -215,6 +215,9 @@ $userQuery = (new Query())->select('id')->from('user');
$query->where(['id' => $userQuery]);
```
Using the Hash Format, Yii internally uses parameter binding so in contrast to the [string format](#string-format), here
you do not have to add parameters manually.
#### Operator Format <span id="operator-format"></span>
@ -286,6 +289,9 @@ the operator can be one of the following:
- `>`, `<=`, or any other valid DB operator that takes two operands: the first operand must be a column name
while the second operand a value. For example, `['>', 'age', 10]` will generate `age>10`.
Using the Operator Format, Yii internally uses parameter binding so in contrast to the [string format](#string-format), here
you do not have to add parameters manually.
#### Appending Conditions <span id="appending-conditions"></span>

3
docs/guide/output-formatting.md

@ -107,6 +107,9 @@ The following format shortcuts are supported (the examples assume `en_GB` is the
- `long`: will output `6 October 2014` and `15:58:42 GMT`;
- `full`: will output `Monday, 6 October 2014` and `15:58:42 GMT`.
Since version 2.0.7 it is also possible to format dates in different calendar systems.
Please refer to the API documentation of the formatters [[yii\i18n\Formatter::$calendar|$calendar]]-property on how to set a different calendar.
### Time Zones <span id="time-zones"></span>

2
docs/guide/rest-controllers.md

@ -79,7 +79,7 @@ public function behaviors()
## Extending `ActiveController` <span id="extending-active-controller"></span>
If your controller class extends from [[yii\rest\ActiveController]], you should set
its [[yii\rest\ActiveController::modelClass||modelClass]] property to be the name of the resource class
its [[yii\rest\ActiveController::modelClass|modelClass]] property to be the name of the resource class
that you plan to serve through this controller. The class must extend from [[yii\db\ActiveRecord]].

2
docs/internals-ja/core-code-style.md

@ -8,7 +8,7 @@ Yii2 コアフレームワークのコードスタイル
なお、CodeSniffer のための設定をここで入手できます: https://github.com/yiisoft/yii2-coding-standards
> Note|注意: 以下では、説明のために、サンプル・コードのドキュメントやコメントを日本語に翻訳しています。
> Note: 以下では、説明のために、サンプル・コードのドキュメントやコメントを日本語に翻訳しています。
しかし、コアコードや公式エクステンションに対して実際に寄稿する場合には、それらを英語で書く必要があります。

6
docs/internals-ja/git-workflow.md

@ -33,7 +33,7 @@ git remote add upstream git://github.com/yiisoft/yii2.git
- `composer update` を実行して、依存パッケージをインストールします ([composer をグローバルにインストール](https://getcomposer.org/doc/00-intro.md#globally) したものと仮定しています)。
> Note|注意: `Problem 1 The requested package bower-asset/jquery could not be found in any version, there may be a typo in the package name.` というようなエラーが生ずる場合は、`composer global require "fxp/composer-asset-plugin:~1.1.1"` を実行する必要があります。
> Note: `Problem 1 The requested package bower-asset/jquery could not be found in any version, there may be a typo in the package name.` というようなエラーが生ずる場合は、`composer global require "fxp/composer-asset-plugin:~1.1.1"` を実行する必要があります。
- `php build/build dev/app basic` を実行して、ベーシックアプリケーションをクローンし、その依存パッケージをインストールします。
このコマンドは外部 composer パッケージは通常どおりインストールしますが、yii2 レポジトリは現在チェックアウトされているものをリンクします。
@ -44,7 +44,7 @@ git remote add upstream git://github.com/yiisoft/yii2.git
このコマンドは後日、依存パッケージを更新するためにも使用されます。
このコマンドは内部的に `composer update` を実行します。
> Note|注意: デフォルトの git レポジトリの Url を使うため、SSH 経由で github からクローンすることになります。
> Note: デフォルトの git レポジトリの Url を使うため、SSH 経由で github からクローンすることになります。
> `build` コマンドに `--useHttp` フラグを追加すれば、代りに HTTP を使うことが出来ます。
**これであなたは Yii 2 をハックするための作業用の遊び場を手に入れました。**
@ -79,7 +79,7 @@ php build/build dev/ext <extension-name>
`php build/build dev/app basic` を実行すると、エクステンションとその依存パッケージがインストールされ、`extensions/redis` に対するシンボリックリンクが作成されます。
こうすることで、composer の vendor ディレクトリではなく、直接に yii2 のレポジトリで作業をすることが出来るようになります。
> Note|注意: デフォルトの git レポジトリの Url を使うため、SSH 経由で github からクローンすることになります。
> Note: デフォルトの git レポジトリの Url を使うため、SSH 経由で github からクローンすることになります。
> `build` コマンドに `--useHttp` フラグを追加すれば、代りに HTTP を使うことが出来ます。
バグ修正と機能改良に取り組む

4
docs/internals-ja/versions.md

@ -44,7 +44,7 @@ ferver の記事は、Semantic Versioning を使おうが使うまいが、こ
## メジャーリリース `X.0.0`
1.0 に対する 2.0 など。
これは外部的な技術の進歩 (例えば、PHP の 5.0 から 5.4 へのアップグレードされた、など) に依存して、3年から5年の間に一度だけ生じるものであると私たちは予想しています。
これは外部的な技術の進歩 (例えば、PHP が 5.0 から 5.4 へアップグレードされた、など) に依存して、3年から5年の間に一度だけ生じるものであると私たちは予想しています。
> Note|注意: 公式エクステンションも同じバージョン付与ポリシーに従っていますが、フレームワークとは独立にリリースされることがあります。
> Note: 公式エクステンションも同じバージョン付与ポリシーに従っていますが、フレームワークとは独立にリリースされることがあります。
すなわち、フレームワークとエクステンションの間で、バージョン番号が異なることが予想されます。

3
docs/internals-ru/translation-workflow.md

@ -147,3 +147,6 @@ php build translation "../docs/guide" "../docs/guide-ru" "Russian guide translat
- view — представление.
- query builder — конструктор запросов.
- time zone — часовой пояс.
- to trigger — инициализировать
- event — событие
- to implement (class implements interface) — реализовывать (класс реализует интерфейс)

8
framework/CHANGELOG.md

@ -23,6 +23,7 @@ Yii Framework 2 Change Log
- Bug #9583: Server response on invalid JSON request included a wrong message about "Internal Server Error" with status 500 (cebe)
- Bug #9591: Fixed `yii.validation.js` code so it is compressable by YUICompressor (samdark, hofrob)
- Bug #9596: Fixed `\yii\web\UrlManager::createAbsoluteUrl(['site/index', '#' => 'testHash'])` losing hash (alchimik, samdark)
- Bug #9670: Fixed PJAX redirect in IE. `yii\web\Response::redirect()` - added check for `X-Ie-Redirect-Compatibility` header (silverfire)
- Bug #9678: `I18N::format()` wasn't able to handle named placeholder in "selectordinal" (samdark)
- Bug #9681: `Json::encode()` was erroring under CYGWIN (samdark)
- Bug #9689: Hidden input on `Html::activeFileInput()` had the wrong name if a name was explicitly given (graphcon, cebe)
@ -43,7 +44,10 @@ Yii Framework 2 Change Log
- Bug #10142: Fixed `yii\validators\EmailValidator` to check the length of email properly (silverfire)
- Bug #10278: Fixed `yii\helpers\BaseJson` support \SimpleXMLElement data (SilverFire, LAV45)
- Bug #10302: Fixed JS function `yii.getQueryParams`, which parsed array variables incorrectly (servocoder, silverfire)
- Bug #10363: Fixed fallback float and integer formatting (AnatolyRugalev, silverfire)
- Bug #10372: Fixed console controller including DI arguments in help (sammousa)
- Bug #10385: Fixed `yii\validators\CaptchaValidator` passed incorrect hashKey to JS validator when `captchaAction` begins with `/` (silverfire)
- Bug #10467: Fixed `yii\di\Instance::ensure()` to work with minimum settings (LAV45)
- Bug: Fixed generation of canonical URLs for `ViewAction` pages (samdark)
- Bug: Fixed `mb_*` functions calls to use `UTF-8` or `Yii::$app->charset` (silverfire)
- Enh #3506: Added `yii\validators\IpValidator` to perform validation of IP addresses and subnets (SilverFire, samdark)
@ -57,10 +61,12 @@ Yii Framework 2 Change Log
- Enh #8329: Added support of options for `message` console command (vchenin)
- Enh #8613: `yii\widgets\FragmentCache` will not store empty content anymore which fixes some problems related to `yii\filters\PageCache` (kidol)
- Enh #8649: Added total applied migrations to final report (vernik91)
- Enh #8687: Added support for non-gregorian calendars, e.g. persian, taiwan, islamic to `yii\i18n\Formatter` (cebe, z-avanes, hooman-pro)
- Enh #8995: `yii\validators\FileValidator::maxFiles` can be set to `0` to allow unlimited count of files (PowerGamer1, silverfire)
- Enh #9282: Improved JSON error handling to support PHP 5.5 error codes (freezy-sk)
- Enh #9337: Added `yii\db\ColumnSchemaBuilder::defaultExpression()` to support DB Expression as default value (kotchuprik)
- Enh #9412: `yii\web\Response::sendHeaders()` does now set the status header last which negates certain magic PHP behavior regarding the `header()` function (nd4c, kidol)
- Enh #9443: Added `unsigned()` to `ColumnSchemaBuilder` (samdark)
- Enh #9465: ./yii migrate/create now generates code based on migration name and --fields (pana1990)
- Enh #9476: Added DI injection via controller action method signature (mdmunir)
- Enh #9573: Added `yii\rbac\ManagerInterface::getUserIdsByRole()` and implementations (samdark)
@ -77,12 +83,14 @@ Yii Framework 2 Change Log
- Enh #10078: Added `csrf` option to `Html::beginForm()` to allow disabling the hidden csrf field generation (machour)
- Enh #10086: `yii\base\Controller::viewPath` is now configurable (Sibilino)
- Enh #10098: Changed `yii.confirm` context to the event's target DOM element which is triggered by clickable or changeable elements (lichunqiang)
- Enh #10108: Added support for events in interfaces (omnilight)
- Enh #10118: Allow easy extension of slug generation in `yii\behaviors\SluggableBehavior` (cebe, hesna)
- Enh #10149: Made `yii\db\Connection` serializable (Sam Mousa)
- Enh #10154: Implemented support of traversable objects in `RangeValidator::ranges`, added `ArrayHelper::isIn()` and `ArrayHelper::isSubset()` (Sam Mousa)
- Enh #10158: Added the possibility to specify CSS and Javascript options per file in `\yii\web\AssetBundle` (machour)
- Enh #10170: `Yii::powered()` now uses `Yii::t()` (SamMousa)
- Enh #10218: Support for selecting multiple filter values with the same name was added to `yii.gridView.js` (omnilight, silverfire)
- Enh #10255: Added Yii warning to session initialization if session was already started (AnatolyRugalev)
- Enh #10264: Generation of HTML tags in Yii's JavaScript now uses jQuery style (silverfire)
- Enh #10267: `yii.js` - added original event passing to `pjaxOptions` for links with `data-method` and `data-pjax` (servocoder, silverfire)
- Enh #10319: `yii\helpers\VarDumper::dump()` now respects PHP magic method `__debugInfo()` (klimov-paul)

26
framework/base/Event.php

@ -24,6 +24,7 @@ namespace yii\base;
*/
class Event extends Object
{
private static $_events = [];
/**
* @var string the event name. This property is set by [[Component::trigger()]] and [[trigger()]].
* Event handlers may use this property to check what event it is handling.
@ -48,9 +49,6 @@ class Event extends Object
*/
public $data;
private static $_events = [];
/**
* Attaches an event handler to a class-level event.
*
@ -145,11 +143,18 @@ class Event extends Object
} else {
$class = ltrim($class, '\\');
}
do {
$classes = array_merge(
[$class],
class_parents($class, true),
class_implements($class, true)
);
foreach ($classes as $class) {
if (!empty(self::$_events[$name][$class])) {
return true;
}
} while (($class = get_parent_class($class)) !== false);
}
return false;
}
@ -181,7 +186,14 @@ class Event extends Object
} else {
$class = ltrim($class, '\\');
}
do {
$classes = array_merge(
[$class],
class_parents($class, true),
class_implements($class, true)
);
foreach ($classes as $class) {
if (!empty(self::$_events[$name][$class])) {
foreach (self::$_events[$name][$class] as $handler) {
$event->data = $handler[1];
@ -191,6 +203,6 @@ class Event extends Object
}
}
}
} while (($class = get_parent_class($class)) !== false);
}
}
}

5
framework/console/Controller.php

@ -414,8 +414,13 @@ class Controller extends \yii\base\Controller
$params = isset($tags['param']) ? (array) $tags['param'] : [];
$args = [];
/** @var \ReflectionParameter $reflection */
foreach ($method->getParameters() as $i => $reflection) {
$name = $reflection->getName();
if ($reflection->getClass() !== null) {
continue;
}
$tag = isset($params[$i]) ? $params[$i] : '';
if (preg_match('/^(\S+)\s+(\$\w+\s+)?(.*)/s', $tag, $matches)) {
$type = $matches[1];

25
framework/db/ColumnSchemaBuilder.php

@ -46,6 +46,10 @@ class ColumnSchemaBuilder extends Object
* @var mixed default value of the column.
*/
protected $default;
/**
* @var boolean whether the column values should be unsigned. If this is `true`, an `UNSIGNED` keyword will be added.
*/
protected $isUnsigned = false;
/**
@ -105,6 +109,17 @@ class ColumnSchemaBuilder extends Object
}
/**
* Marks column as unsigned.
* @return $this
* @since 2.0.7
*/
public function unsigned()
{
$this->isUnsigned = true;
return $this;
}
/**
* Specify the default SQL expression for the column.
* @param string $default the default value expression.
* @return $this
@ -124,6 +139,7 @@ class ColumnSchemaBuilder extends Object
return
$this->type .
$this->buildLengthString() .
$this->buildUnsignedString() .
$this->buildNotNullString() .
$this->buildUniqueString() .
$this->buildDefaultString() .
@ -203,4 +219,13 @@ class ColumnSchemaBuilder extends Object
{
return $this->check !== null ? " CHECK ({$this->check})" : '';
}
/**
* Builds the unsigned string for column.
* @return string a string containing UNSIGNED keyword.
*/
protected function buildUnsignedString()
{
return $this->isUnsigned ? ' UNSIGNED' : '';
}
}

28
framework/db/mssql/ColumnSchemaBuilder.php

@ -0,0 +1,28 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\db\mssql;
use yii\db\ColumnSchemaBuilder as AbstractColumnSchemaBuilder;
/**
* ColumnSchemaBuilder is the schema builder for MSSQL databases.
*
* @author Alexander Makarov <sam@rmcreative.ru>
* @since 2.0.7
*/
class ColumnSchemaBuilder extends AbstractColumnSchemaBuilder
{
/**
* @inheritdoc
*/
protected function buildUnsignedString()
{
return '';
}
}

1
framework/db/mssql/QueryBuilder.php

@ -230,6 +230,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
* @param Query $values
* @param array $params
* @return string SQL
* @throws NotSupportedException
*/
protected function buildSubqueryInCondition($operator, $columns, $values, &$params)
{

8
framework/db/mssql/Schema.php

@ -426,4 +426,12 @@ SQL;
}
return $result;
}
/**
* @inheritdoc
*/
public function createColumnSchemaBuilder($type, $length = null)
{
return new ColumnSchemaBuilder($type, $length);
}
}

1
framework/db/mysql/Schema.php

@ -243,6 +243,7 @@ class Schema extends \yii\db\Schema
/**
* Collects the foreign key column details for the given table.
* @param TableSchema $table the table metadata
* @throws \Exception
*/
protected function findConstraints($table)
{

1
framework/db/oci/ColumnSchemaBuilder.php

@ -25,6 +25,7 @@ class ColumnSchemaBuilder extends AbstractColumnSchemaBuilder
return
$this->type .
$this->buildLengthString() .
$this->buildUnsignedString() .
$this->buildDefaultString() .
$this->buildNotNullString() .
$this->buildCheckString();

28
framework/db/pgsql/ColumnSchemaBuilder.php

@ -0,0 +1,28 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\db\pgsql;
use yii\db\ColumnSchemaBuilder as AbstractColumnSchemaBuilder;
/**
* ColumnSchemaBuilder is the schema builder for PostgreSQL databases.
*
* @author Alexander Makarov <sam@rmcreative.ru>
* @since 2.0.7
*/
class ColumnSchemaBuilder extends AbstractColumnSchemaBuilder
{
/**
* @inheritdoc
*/
protected function buildUnsignedString()
{
return '';
}
}

8
framework/db/pgsql/Schema.php

@ -474,4 +474,12 @@ SQL;
return !$command->pdoStatement->rowCount() ? false : $result;
}
/**
* @inheritdoc
*/
public function createColumnSchemaBuilder($type, $length = null)
{
return new ColumnSchemaBuilder($type, $length);
}
}

1
framework/db/sqlite/QueryBuilder.php

@ -315,6 +315,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
* @param Query $values
* @param array $params
* @return string SQL
* @throws NotSupportedException
*/
protected function buildSubqueryInCondition($operator, $columns, $values, &$params)
{

8
framework/di/Instance.php

@ -107,9 +107,7 @@ class Instance
*/
public static function ensure($reference, $type = null, $container = null)
{
if ($reference instanceof $type) {
return $reference;
} elseif (is_array($reference)) {
if (is_array($reference)) {
$class = isset($reference['class']) ? $reference['class'] : $type;
if (!$container instanceof Container) {
$container = Yii::$container;
@ -122,11 +120,13 @@ class Instance
if (is_string($reference)) {
$reference = new static($reference);
} elseif ($type === null || $reference instanceof $type) {
return $reference;
}
if ($reference instanceof self) {
$component = $reference->get($container);
if ($component instanceof $type || $type === null) {
if ($type === null || $component instanceof $type) {
return $component;
} else {
throw new InvalidConfigException('"' . $reference->id . '" refers to a ' . get_class($component) . " component. $type is expected.");

39
framework/i18n/Formatter.php

@ -139,6 +139,37 @@ class Formatter extends Component
*/
public $datetimeFormat = 'medium';
/**
* @var \IntlCalendar|int|null the calendar to be used for date formatting. The value of this property will be directly
* passed to the [constructor of the `IntlDateFormatter` class](http://php.net/manual/en/intldateformatter.create.php).
*
* Defaults to `null`, which means the Gregorian calendar will be used. You may also explicitly pass the constant
* `\IntlDateFormatter::GREGORIAN` for Gregorian calendar.
*
* To use an alternative calendar like for example the [Jalali calendar](https://en.wikipedia.org/wiki/Jalali_calendar),
* set this property to `\IntlDateFormatter::TRADITIONAL`.
* The calendar must then be specified in the [[locale]], for example for the persian calendar the configuration for the formatter would be:
*
* ```php
* 'formatter' => [
* 'locale' => 'fa_IR@calendar=persian',
* 'calendar' => \IntlDateFormatter::TRADITIONAL,
* ],
* ```
*
* Available calendar names can be found in the [ICU manual](http://userguide.icu-project.org/datetime/calendar).
*
* Since PHP 5.5 you may also use an instance of the [[\IntlCalendar]] class.
* Check the [PHP manual](http://php.net/manual/en/intldateformatter.create.php) for more details.
*
* If the [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, setting this property will have no effect.
*
* @see http://php.net/manual/en/intldateformatter.create.php
* @see http://php.net/manual/en/class.intldateformatter.php#intl.intldateformatter-constants.calendartypes
* @see http://php.net/manual/en/class.intlcalendar.php
* @since 2.0.7
*/
public $calendar;
/**
* @var string the character displayed as the decimal point when formatting a number.
* If not set, the decimal separator corresponding to [[locale]] will be used.
* If [PHP intl extension](http://php.net/manual/en/book.intl.php) is not available, the default value is '.'.
@ -574,14 +605,14 @@ class Formatter extends Component
}
if (isset($this->_dateFormats[$format])) {
if ($type === 'date') {
$formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], IntlDateFormatter::NONE, $timeZone);
$formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], IntlDateFormatter::NONE, $timeZone, $this->calendar);
} elseif ($type === 'time') {
$formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, $this->_dateFormats[$format], $timeZone);
$formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, $this->_dateFormats[$format], $timeZone, $this->calendar);
} else {
$formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], $this->_dateFormats[$format], $timeZone);
$formatter = new IntlDateFormatter($this->locale, $this->_dateFormats[$format], $this->_dateFormats[$format], $timeZone, $this->calendar);
}
} else {
$formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE, $timeZone, null, $format);
$formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::NONE, IntlDateFormatter::NONE, $timeZone, $this->calendar, $format);
}
if ($formatter === null) {
throw new InvalidConfigException(intl_get_error_message());

10
framework/i18n/MessageFormatter.php

@ -341,8 +341,14 @@ class MessageFormatter extends Component
case 'selectordinal':
throw new NotSupportedException("Message format '$type' is not supported. You have to install PHP intl extension to use this feature.");
case 'number':
if (is_int($arg) && (!isset($token[2]) || trim($token[2]) === 'integer')) {
return $arg;
$format = isset($token[2]) ? trim($token[2]) : null;
if (is_numeric($arg) && ($format === null || $format === 'integer')) {
$number = number_format($arg);
if ($format === null && ($pos = strpos($arg, '.')) !== false) {
// add decimals with unknown length
$number .= '.' . substr($arg, $pos + 1);
}
return $number;
}
throw new NotSupportedException("Message format 'number' is only supported for integer values. You have to install PHP intl extension to use this feature.");
case 'none':

85
framework/messages/fa/yii.php

@ -20,35 +20,9 @@
* NOTE: this file must be saved in UTF-8 encoding.
*/
return [
'Are you sure you want to delete this item?' => 'آیا اطمینان به حذف این مورد دارید؟',
'Only files with these MIME types are allowed: {mimeTypes}.' => 'فقط این نوع فایلها مجاز میباشند: {mimeTypes}.',
'The requested view "{name}" was not found.' => 'نمای درخواستی "{name}" یافت نشد.',
'in {delta, plural, =1{a second} other{# seconds}}' => 'در {delta} ثانیه',
'{delta, plural, =1{a year} other{# years}} ago' => '{delta} سال پیش',
'{nFormatted} B' => '{nFormatted} B',
'{nFormatted} GB' => '{nFormatted} GB',
'{nFormatted} GiB' => '{nFormatted} GiB',
'{nFormatted} KB' => '{nFormatted} KB',
'{nFormatted} KiB' => '{nFormatted} KiB',
'{nFormatted} MB' => '{nFormatted} MB',
'{nFormatted} MiB' => '{nFormatted} MiB',
'{nFormatted} PB' => '{nFormatted} PB',
'{nFormatted} PiB' => '{nFormatted} PiB',
'{nFormatted} TB' => '{nFormatted} TB',
'{nFormatted} TiB' => '{nFormatted} TiB',
'{nFormatted} {n, plural, =1{byte} other{bytes}}' => '{nFormatted} بایت',
'{nFormatted} {n, plural, =1{gibibyte} other{gibibytes}}' => '{nFormatted} گیبیبایت',
'{nFormatted} {n, plural, =1{gigabyte} other{gigabytes}}' => '{nFormatted} گیگابایت',
'{nFormatted} {n, plural, =1{kibibyte} other{kibibytes}}' => '{nFormatted} کیبیبایت',
'{nFormatted} {n, plural, =1{kilobyte} other{kilobytes}}' => '{nFormatted} کیلوبایت',
'{nFormatted} {n, plural, =1{mebibyte} other{mebibytes}}' => '{nFormatted} مبیبایت',
'{nFormatted} {n, plural, =1{megabyte} other{megabytes}}' => '{nFormatted} مگابایت',
'{nFormatted} {n, plural, =1{pebibyte} other{pebibytes}}' => '{nFormatted} پبیبایت',
'{nFormatted} {n, plural, =1{petabyte} other{petabytes}}' => '{nFormatted} پتابایت',
'{nFormatted} {n, plural, =1{tebibyte} other{tebibytes}}' => '{nFormatted} تبیبایت',
'{nFormatted} {n, plural, =1{terabyte} other{terabytes}}' => '{nFormatted} ترابایت',
'(not set)' => '(تنظیم نشده)',
'An internal server error occurred.' => 'خطای داخلی سرور رخ داده است.',
'Are you sure you want to delete this item?' => 'آیا اطمینان به حذف این مورد دارید؟',
'Delete' => 'حذف',
'Error' => 'خطا',
'File upload failed.' => 'آپلود فایل ناموفق بود.',
@ -59,19 +33,22 @@ return [
'Missing required parameters: {params}' => 'فاقد پارامترهای مورد نیاز: {params}',
'No' => 'خیر',
'No results found.' => 'نتیجهای یافت نشد.',
'Only files with these MIME types are allowed: {mimeTypes}.' => 'فقط این نوع فایلها مجاز میباشند: {mimeTypes}.',
'Only files with these extensions are allowed: {extensions}.' => 'فقط فایلهای با این پسوندها مجاز هستند: {extensions}.',
'Page not found.' => 'صفحهای یافت نشد.',
'Please fix the following errors:' => 'لطفاً خطاهای زیر را رفع نمائید:',
'Please upload a file.' => 'لطفاً یک فایل آپلود کنید.',
'Powered by {yii}' => 'طراحی شده توسط {yii}',
'Showing <b>{begin, number}-{end, number}</b> of <b>{totalCount, number}</b> {totalCount, plural, one{item} other{items}}.' => 'نمایش <b>{begin, number} تا {end, number}</b> مورد از کل <b>{totalCount, number}</b> مورد.',
'The file "{file}" is not an image.' => 'فایل "{file}" یک تصویر نیست.',
'The file "{file}" is too big. Its size cannot exceed {limit, number} {limit, plural, one{byte} other{bytes}}.' => 'حجم فایل "{file}" بیش از حد زیاد است. مقدار آن نمیتواند بیشتر از {limit, number} بایت باشد.',
'The file "{file}" is too small. Its size cannot be smaller than {limit, number} {limit, plural, one{byte} other{bytes}}.' => 'حجم فایل "{file}" بیش از حد کم است. مقدار آن نمیتواند کمتر از {limit, number} بایت باشد.',
'The file "{file}" is too big. Its size cannot exceed {formattedLimit}.' => 'حجم فایل "{file}" بسیار بیشتر می باشد. حجم آن نمی تواند از {formattedLimit} بیشتر باشد.',
'The file "{file}" is too small. Its size cannot be smaller than {formattedLimit}.' => 'حجم فایل "{file}" بسیار کم می باشد. حجم آن نمی تواند از {formattedLimit} کمتر باشد.',
'The format of {attribute} is invalid.' => 'قالب {attribute} نامعتبر است.',
'The image "{file}" is too large. The height cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'تصویر "{file}" خیلی بزرگ است. ارتفاع نمیتواند بزرگتر از {limit, number} پیکسل باشد.',
'The image "{file}" is too large. The width cannot be larger than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'تصویر "{file}" خیلی بزرگ است. عرض نمیتواند بزرگتر از {limit, number} پیکسل باشد.',
'The image "{file}" is too small. The height cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'تصویر "{file}" خیلی کوچک است. ارتفاع نمیتواند کوچکتر از {limit, number} پیکسل باشد.',
'The image "{file}" is too small. The width cannot be smaller than {limit, number} {limit, plural, one{pixel} other{pixels}}.' => 'تصویر "{file}" خیلی کوچک است. عرض نمیتواند کوچکتر از {limit, number} پیکسل باشد.',
'The requested view "{name}" was not found.' => 'نمای درخواستی "{name}" یافت نشد.',
'The verification code is incorrect.' => 'کد تائید اشتباه است.',
'Total <b>{count, number}</b> {count, plural, one{item} other{items}}.' => 'مجموع <b>{count, number}</b> مورد.',
'Unable to verify your data submission.' => 'قادر به تائید اطلاعات ارسالی شما نمیباشد.',
@ -79,39 +56,77 @@ return [
'Update' => 'بروزرسانی',
'View' => 'نما',
'Yes' => 'بله',
'Yii Framework' => 'فریم ورک یی',
'You are not allowed to perform this action.' => 'شما برای انجام این عملیات، دسترسی ندارید.',
'You can upload at most {limit, number} {limit, plural, one{file} other{files}}.' => 'شما حداکثر {limit, number} فایل را میتوانید آپلود کنید.',
'in {delta, plural, =1{a day} other{# days}}' => '{delta} روز دیگر',
'in {delta, plural, =1{a minute} other{# minutes}}' => '{delta} دقیقه دیگر',
'in {delta, plural, =1{a month} other{# months}}' => '{delta} ماه دیگر',
'in {delta, plural, =1{a second} other{# seconds}}' => 'در {delta} ثانیه',
'in {delta, plural, =1{a year} other{# years}}' => '{delta} سال دیگر',
'in {delta, plural, =1{an hour} other{# hours}}' => '{delta} ساعت دیگر',
'just now' => 'هم اکنون',
'the input value' => 'مقدار ورودی',
'{attribute} "{value}" has already been taken.' => '{attribute} با مقدار "{value}" در حال حاضر گرفتهشده است.',
'{attribute} cannot be blank.' => '{attribute} نمیتواند خالی باشد.',
'{attribute} contains wrong subnet mask.' => '{attribute} شامل فرمت زیرشبکه اشتباه است.',
'{attribute} is invalid.' => '{attribute} معتبر نیست.',
'{attribute} is not a valid URL.' => '{attribute} یک URL معتبر نیست.',
'{attribute} is not a valid email address.' => '{attribute} یک آدرس ایمیل معتبر نیست.',
'{attribute} is not in the allowed range.' => '{attribute} در محدوده مجاز نمی باشد.',
'{attribute} must be "{requiredValue}".' => '{attribute} باید "{requiredValue}" باشد.',
'{attribute} must be a number.' => '{attribute} باید یک عدد باشد.',
'{attribute} must be a string.' => '{attribute} باید یک رشته باشد.',
'{attribute} must be a valid IP address.' => '{attribute} باید IP صحیح باشد.',
'{attribute} must be an IP address with specified subnet.' => '{attribute} باید یک IP آدرسی با زیرشبکه بخصوص باشد.',
'{attribute} must be an integer.' => '{attribute} باید یک عدد صحیح باشد.',
'{attribute} must be either "{true}" or "{false}".' => '{attribute} باید "{true}" و یا "{false}" باشد.',
'{attribute} must be greater than "{compareValue}".' => '{attribute} باید بزرگتر از "{compareValue}" باشد.',
'{attribute} must be greater than or equal to "{compareValue}".' => '{attribute} باید بزرگتر و یا مساوی "{compareValue}" باشد.',
'{attribute} must be less than "{compareValue}".' => '{attribute} باید کوچکتر از "{compareValue}" باشد.',
'{attribute} must be less than or equal to "{compareValue}".' => '{attribute} باید کوچکتر و یا مساوی "{compareValue}" باشد.',
'{attribute} must be equal to "{compareValueOrAttribute}".' => '{attribute} باید با "{compareValueOrAttribute}" برابر باشد.',
'{attribute} must be greater than "{compareValueOrAttribute}".' => '{attribute} باید بزرگتر از "{compareValueOrAttribute}" باشد.',
'{attribute} must be greater than or equal to "{compareValueOrAttribute}".' => '{attribute} باید بزرکتر یا برابر با "{compareValueOrAttribute}" باشد.',
'{attribute} must be less than "{compareValueOrAttribute}".' => '{attribute} باید کمتر از "{compareValueOrAttribute}" باشد.',
'{attribute} must be less than or equal to "{compareValueOrAttribute}".' => '{attribute} باید کمتر یا برابر با "{compareValueOrAttribute}" باشد.',
'{attribute} must be no greater than {max}.' => '{attribute} نباید بیشتر از "{max}" باشد.',
'{attribute} must be no less than {min}.' => '{attribute} نباید کمتر از "{min}" باشد.',
'{attribute} must be repeated exactly.' => '{attribute} عیناً باید تکرار شود.',
'{attribute} must not be equal to "{compareValue}".' => '{attribute} نباید برابر با "{compareValue}" باشد.',
'{attribute} must not be a subnet.' => '{attribute} باید کمتر یا برابر "{compareValueOrAttribute}" باشد.',
'{attribute} must not be an IPv4 address.' => '{attribute} باید آدرس IPv4 نباشد.',
'{attribute} must not be an IPv6 address.' => '{attribute} باید آدرس IPv6 نباشد.',
'{attribute} must not be equal to "{compareValueOrAttribute}".' => '{attribute} باید مانند "{compareValueOrAttribute}" تکرار نشود.',
'{attribute} should contain at least {min, number} {min, plural, one{character} other{characters}}.' => '{attribute} حداقل باید شامل {min, number} کارکتر باشد.',
'{attribute} should contain at most {max, number} {max, plural, one{character} other{characters}}.' => '{attribute} حداکثر باید شامل {max, number} کارکتر باشد.',
'{attribute} should contain {length, number} {length, plural, one{character} other{characters}}.' => '{attribute} باید شامل {length, number} کارکتر باشد.',
'{delta, plural, =1{1 day} other{# days}}' => '{delta, plural, =1{1 روز} other{# چندین روز}}',
'{delta, plural, =1{1 hour} other{# hours}}' => '{delta, plural, =1{1 ساعت} other{# ساعات}}',
'{delta, plural, =1{1 minute} other{# minutes}}' => '{delta, plural, =1{1 دقیقه} other{# دقایق}}',
'{delta, plural, =1{1 month} other{# months}}' => '{delta, plural, =1{1 ماه} other{# ماه ها}}',
'{delta, plural, =1{1 second} other{# seconds}}' => '{delta, plural, =1{1 ثانیه} other{# ثانیه ها}}',
'{delta, plural, =1{1 year} other{# years}}' => '{delta, plural, =1{1 سال} other{# سالیان}}',
'{delta, plural, =1{a day} other{# days}} ago' => '{delta} روز قبل',
'{delta, plural, =1{a minute} other{# minutes}} ago' => '{delta} دقیقه قبل',
'{delta, plural, =1{a month} other{# months}} ago' => '{delta} ماه قبل',
'{delta, plural, =1{a second} other{# seconds}} ago' => '{delta} ثانیه قبل',
'{delta, plural, =1{a year} other{# years}} ago' => '{delta} سال پیش',
'{delta, plural, =1{an hour} other{# hours}} ago' => '{delta} ساعت قبل',
'{nFormatted} B' => '{nFormatted} B',
'{nFormatted} GB' => '{nFormatted} GB',
'{nFormatted} GiB' => '{nFormatted} GiB',
'{nFormatted} KB' => '{nFormatted} KB',
'{nFormatted} KiB' => '{nFormatted} KiB',
'{nFormatted} MB' => '{nFormatted} MB',
'{nFormatted} MiB' => '{nFormatted} MiB',
'{nFormatted} PB' => '{nFormatted} PB',
'{nFormatted} PiB' => '{nFormatted} PiB',
'{nFormatted} TB' => '{nFormatted} TB',
'{nFormatted} TiB' => '{nFormatted} TiB',
'{nFormatted} {n, plural, =1{byte} other{bytes}}' => '{nFormatted} بایت',
'{nFormatted} {n, plural, =1{gibibyte} other{gibibytes}}' => '{nFormatted} گیبیبایت',
'{nFormatted} {n, plural, =1{gigabyte} other{gigabytes}}' => '{nFormatted} گیگابایت',
'{nFormatted} {n, plural, =1{kibibyte} other{kibibytes}}' => '{nFormatted} کیبیبایت',
'{nFormatted} {n, plural, =1{kilobyte} other{kilobytes}}' => '{nFormatted} کیلوبایت',
'{nFormatted} {n, plural, =1{mebibyte} other{mebibytes}}' => '{nFormatted} مبیبایت',
'{nFormatted} {n, plural, =1{megabyte} other{megabytes}}' => '{nFormatted} مگابایت',
'{nFormatted} {n, plural, =1{pebibyte} other{pebibytes}}' => '{nFormatted} پبیبایت',
'{nFormatted} {n, plural, =1{petabyte} other{petabytes}}' => '{nFormatted} پتابایت',
'{nFormatted} {n, plural, =1{tebibyte} other{tebibytes}}' => '{nFormatted} تبیبایت',
'{nFormatted} {n, plural, =1{terabyte} other{terabytes}}' => '{nFormatted} ترابایت',
];

3
framework/web/Response.php

@ -745,6 +745,7 @@ class Response extends \yii\base\Response
* meaning if the current request is an AJAX or PJAX request, then calling this method will cause the browser
* to redirect to the given URL. If this is false, a `Location` header will be sent, which when received as
* an AJAX/PJAX response, may NOT cause browser redirection.
* Takes effect only when request header `X-Ie-Redirect-Compatibility` is absent.
* @return $this the response object itself
*/
public function redirect($url, $statusCode = 302, $checkAjax = true)
@ -758,7 +759,7 @@ class Response extends \yii\base\Response
$url = Yii::$app->getRequest()->getHostInfo() . $url;
}
if ($checkAjax) {
if ($checkAjax && Yii::$app->getRequest()->getHeaders()->get('X-Ie-Redirect-Compatibility') !== null) {
if (Yii::$app->getRequest()->getIsPjax()) {
$this->getHeaders()->set('X-Pjax-Url', $url);
} elseif (Yii::$app->getRequest()->getIsAjax()) {

3
framework/web/Session.php

@ -97,6 +97,9 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co
{
parent::init();
register_shutdown_function([$this, 'close']);
if ($this->getIsActive()) {
Yii::warning("Session is already started", __METHOD__);
}
}
/**

36
tests/framework/base/EventTest.php

@ -25,6 +25,7 @@ class EventTest extends TestCase
Event::off(ActiveRecord::className(), 'save');
Event::off(Post::className(), 'save');
Event::off(User::className(), 'save');
Event::off('yiiunit\framework\base\SomeInterface', SomeInterface::EVENT_SUPER_EVENT);
}
public function testOn()
@ -35,6 +36,9 @@ class EventTest extends TestCase
Event::on(ActiveRecord::className(), 'save', function ($event) {
$this->counter += 3;
});
Event::on('yiiunit\framework\base\SomeInterface', SomeInterface::EVENT_SUPER_EVENT, function ($event) {
$this->counter += 5;
});
$this->assertEquals(0, $this->counter);
$post = new Post;
$post->save();
@ -42,6 +46,12 @@ class EventTest extends TestCase
$user = new User;
$user->save();
$this->assertEquals(7, $this->counter);
$someClass = new SomeClass();
$someClass->emitEvent();
$this->assertEquals(12, $this->counter);
$childClass = new SomeSubclass();
$childClass->emitEventInSubclass();
$this->assertEquals(17, $this->counter);
}
public function testOff()
@ -60,9 +70,13 @@ class EventTest extends TestCase
{
$this->assertFalse(Event::hasHandlers(Post::className(), 'save'));
$this->assertFalse(Event::hasHandlers(ActiveRecord::className(), 'save'));
$this->assertFalse(Event::hasHandlers('yiiunit\framework\base\SomeInterface', SomeInterface::EVENT_SUPER_EVENT));
Event::on(Post::className(), 'save', function ($event) {
$this->counter += 1;
});
Event::on('yiiunit\framework\base\SomeInterface', SomeInterface::EVENT_SUPER_EVENT, function ($event) {
$this->counter ++;
});
$this->assertTrue(Event::hasHandlers(Post::className(), 'save'));
$this->assertFalse(Event::hasHandlers(ActiveRecord::className(), 'save'));
@ -72,6 +86,7 @@ class EventTest extends TestCase
});
$this->assertTrue(Event::hasHandlers(User::className(), 'save'));
$this->assertTrue(Event::hasHandlers(ActiveRecord::className(), 'save'));
$this->assertTrue(Event::hasHandlers('yiiunit\framework\base\SomeInterface', SomeInterface::EVENT_SUPER_EVENT));
}
}
@ -90,3 +105,24 @@ class Post extends ActiveRecord
class User extends ActiveRecord
{
}
interface SomeInterface
{
const EVENT_SUPER_EVENT = 'superEvent';
}
class SomeClass extends Component implements SomeInterface
{
public function emitEvent()
{
$this->trigger(self::EVENT_SUPER_EVENT);
}
}
class SomeSubclass extends SomeClass
{
public function emitEventInSubclass()
{
$this->trigger(self::EVENT_SUPER_EVENT);
}
}

23
tests/framework/console/ControllerTest.php

@ -92,4 +92,27 @@ class ControllerTest extends TestCase
$result = $controller->runAction('aksi7', $params);
}
/**
* Tests if action help does not include class-hinted arguments
* @see https://github.com/yiisoft/yii2/issues/10372
*/
public function testHelp()
{
$this->mockApplication([
'components' => [
'barBelongApp' => [
'class' => Bar::className(),
'foo' => 'belong_app'
],
'quxApp' => [
'class' => OtherQux::className(),
'b' => 'belong_app'
]
]
]);
$controller = new FakeController('fake', Yii::$app);
$this->assertArrayNotHasKey('bar', $controller->getActionArgsHelp($controller->createAction('aksi1')));
}
}

69
tests/framework/db/ColumnSchemaBuilderTest.php

@ -0,0 +1,69 @@
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yiiunit\framework\db;
use yii\db\ColumnSchemaBuilder;
use yii\db\Schema;
use yiiunit\TestCase;
/**
* ColumnSchemaBuilderTest tests ColumnSchemaBuilder
*/
class ColumnSchemaBuilderTest extends TestCase
{
/**
* @param string $type
* @param integer $length
* @return ColumnSchemaBuilder
*/
public function getColumnSchemaBuilder($type, $length = null)
{
return new ColumnSchemaBuilder($type, $length);
}
/**
* @return array
*/
public function unsignedProvider()
{
return [
['integer UNSIGNED', Schema::TYPE_INTEGER, null, [
['unsigned'],
]],
['integer(10) UNSIGNED', Schema::TYPE_INTEGER, 10, [
['unsigned'],
]],
];
}
/**
* @dataProvider unsignedProvider
*/
public function testUnsigned($expected, $type, $length, $calls)
{
$this->checkBuildString($expected, $type, $length, $calls);
}
/**
* @param string $expected
* @param string $type
* @param integer $length
* @param array $calls
*/
public function checkBuildString($expected, $type, $length, $calls)
{
$builder = $this->getColumnSchemaBuilder($type, $length);
foreach ($calls as $call) {
$method = array_shift($call);
call_user_func_array([$builder, $method], $call);
}
self::assertEquals($expected, $builder->__toString());
}
}

37
tests/framework/db/mssql/ColumnSchemaBuilderTest.php

@ -0,0 +1,37 @@
<?php
namespace yiiunit\framework\db\mssql;
use yii\db\mssql\ColumnSchemaBuilder;
use yii\db\Schema;
use \yiiunit\framework\db\ColumnSchemaBuilderTest as BaseColumnSchemaBuilderTest;
/**
* ColumnSchemaBuilderTest tests ColumnSchemaBuilder for MSSQL
*/
class ColumnSchemaBuilderTest extends BaseColumnSchemaBuilderTest
{
/**
* @param string $type
* @param integer $length
* @return ColumnSchemaBuilder
*/
public function getColumnSchemaBuilder($type, $length = null)
{
return new ColumnSchemaBuilder($type, $length);
}
/**
* @return array
*/
public function unsignedProvider()
{
return [
['integer', Schema::TYPE_INTEGER, null, [
['unsigned'],
]],
['integer(10)', Schema::TYPE_INTEGER, 10, [
['unsigned'],
]],
];
}
}

22
tests/framework/db/oci/ColumnSchemaBuilderTest.php

@ -0,0 +1,22 @@
<?php
namespace yiiunit\framework\db\oci;
use yii\db\oci\ColumnSchemaBuilder;
use yii\db\Schema;
use \yiiunit\framework\db\ColumnSchemaBuilderTest as BaseColumnSchemaBuilderTest;
/**
* ColumnSchemaBuilderTest tests ColumnSchemaBuilder for Oracle
*/
class ColumnSchemaBuilderTest extends BaseColumnSchemaBuilderTest
{
/**
* @param string $type
* @param integer $length
* @return ColumnSchemaBuilder
*/
public function getColumnSchemaBuilder($type, $length = null)
{
return new ColumnSchemaBuilder($type, $length);
}
}

37
tests/framework/db/pgsql/ColumnSchemaBuilderTest.php

@ -0,0 +1,37 @@
<?php
namespace yiiunit\framework\db\pgsql;
use yii\db\pgsql\ColumnSchemaBuilder;
use yii\db\Schema;
use \yiiunit\framework\db\ColumnSchemaBuilderTest as BaseColumnSchemaBuilderTest;
/**
* ColumnSchemaBuilderTest tests ColumnSchemaBuilder for PostgreSQL
*/
class ColumnSchemaBuilderTest extends BaseColumnSchemaBuilderTest
{
/**
* @param string $type
* @param integer $length
* @return ColumnSchemaBuilder
*/
public function getColumnSchemaBuilder($type, $length = null)
{
return new ColumnSchemaBuilder($type, $length);
}
/**
* @return array
*/
public function unsignedProvider()
{
return [
['integer', Schema::TYPE_INTEGER, null, [
['unsigned'],
]],
['integer(10)', Schema::TYPE_INTEGER, 10, [
['unsigned'],
]],
];
}
}

73
tests/framework/di/InstanceTest.php

@ -7,6 +7,7 @@
namespace yiiunit\framework\di;
use Yii;
use yii\base\Component;
use yii\db\Connection;
use yii\di\Container;
@ -41,9 +42,77 @@ class InstanceTest extends TestCase
$this->assertTrue(Instance::ensure('db', 'yii\db\Connection', $container) instanceof Connection);
$this->assertTrue(Instance::ensure(new Connection, 'yii\db\Connection', $container) instanceof Connection);
$this->assertTrue(Instance::ensure([
$this->assertTrue(Instance::ensure(['class' => 'yii\db\Connection', 'dsn' => 'test'], 'yii\db\Connection', $container) instanceof Connection);
}
public function testEnsureWithoutType()
{
$container = new Container;
$container->set('db', [
'class' => 'yii\db\Connection',
'dsn' => 'test',
]);
$this->assertTrue(Instance::ensure('db', null, $container) instanceof Connection);
$this->assertTrue(Instance::ensure(new Connection, null, $container) instanceof Connection);
$this->assertTrue(Instance::ensure(['class' => 'yii\db\Connection', 'dsn' => 'test'], null, $container) instanceof Connection);
}
public function testEnsureMinimalSettings()
{
Yii::$container->set('db', [
'class' => 'yii\db\Connection',
'dsn' => 'test',
]);
$this->assertTrue(Instance::ensure('db') instanceof Connection);
$this->assertTrue(Instance::ensure(new Connection) instanceof Connection);
$this->assertTrue(Instance::ensure(['class' => 'yii\db\Connection', 'dsn' => 'test']) instanceof Connection);
Yii::$container = new Container;
}
public function testExceptionRefersTo()
{
$container = new Container;
$container->set('db', [
'class' => 'yii\db\Connection',
'dsn' => 'test',
], 'yii\db\Connection', $container) instanceof Connection);
]);
$this->setExpectedException('yii\base\InvalidConfigException', '"db" refers to a yii\db\Connection component. yii\base\Widget is expected.');
Instance::ensure('db', 'yii\base\Widget', $container);
Instance::ensure(['class' => 'yii\db\Connection', 'dsn' => 'test'], 'yii\base\Widget', $container);
}
public function testExceptionInvalidDataType()
{
$this->setExpectedException('yii\base\InvalidConfigException', 'Invalid data type: yii\db\Connection. yii\base\Widget is expected.');
Instance::ensure(new Connection, 'yii\base\Widget');
}
public function testExceptionComponentIsNotSpecified()
{
$this->setExpectedException('yii\base\InvalidConfigException', 'The required component is not specified.');
Instance::ensure('');
}
public function testGet()
{
$this->mockApplication([
'components' => [
'db' => [
'class' => 'yii\db\Connection',
'dsn' => 'test',
]
]
]);
$container = Instance::of('db');
$this->assertTrue($container->get() instanceof Connection);
$this->destroyApplication();
}
}

7
tests/framework/helpers/HtmlTest.php

@ -5,6 +5,7 @@ namespace yiiunit\framework\helpers;
use Yii;
use yii\base\Model;
use yii\helpers\Html;
use yii\helpers\Url;
use yiiunit\TestCase;
/**
@ -20,6 +21,8 @@ class HtmlTest extends TestCase
'request' => [
'class' => 'yii\web\Request',
'url' => '/test',
'scriptUrl' => '/index.php',
'hostInfo' => 'http://www.example.com',
'enableCsrfValidation' => false,
],
'response' => [
@ -113,6 +116,7 @@ class HtmlTest extends TestCase
$this->assertEquals('<a href="/example">something</a>', Html::a('something', '/example'));
$this->assertEquals('<a href="/test">something</a>', Html::a('something', ''));
$this->assertEquals('<a href="http://www.быстроном.рф">http://www.быстроном.рф</a>', Html::a('http://www.быстроном.рф', 'http://www.быстроном.рф'));
$this->assertEquals('<a href="https://www.example.com/index.php?r=site%2Ftest">Test page</a>', Html::a('Test page', Url::to(['/site/test'], 'https')));
}
public function testMailto()
@ -336,6 +340,8 @@ EOD;
</select>
EOD;
$this->assertEqualsWithoutLE($expected, Html::listBox('test', null, [], ['multiple' => true]));
$this->assertEqualsWithoutLE($expected, Html::listBox('test[]', null, [], ['multiple' => true]));
$expected = <<<EOD
<input type="hidden" name="test" value="0"><select name="test" size="4">
@ -353,6 +359,7 @@ EOD;
<label><input type="checkbox" name="test[]" value="value2" checked> text2</label></div>
EOD;
$this->assertEqualsWithoutLE($expected, Html::checkboxList('test', ['value2'], $this->getDataItems()));
$this->assertEqualsWithoutLE($expected, Html::checkboxList('test[]', ['value2'], $this->getDataItems()));
$expected = <<<EOD
<div><label><input type="checkbox" name="test[]" value="value1&lt;&gt;"> text1&lt;&gt;</label>

55
tests/framework/i18n/FallbackMessageFormatterTest.php

@ -19,6 +19,13 @@ class FallbackMessageFormatterTest extends TestCase
{
const N = 'n';
const N_VALUE = 42;
const F = 'f';
const F_VALUE = 2e+8;
const F_VALUE_FORMATTED = "200,000,000";
const D = 'd';
const D_VALUE = 200000000.101;
const D_VALUE_FORMATTED = "200,000,000.101";
const D_VALUE_FORMATTED_INTEGER = "200,000,000";
const SUBJECT = 'сабж';
const SUBJECT_VALUE = 'Answer to the Ultimate Question of Life, the Universe, and Everything';
@ -52,6 +59,38 @@ class FallbackMessageFormatterTest extends TestCase
]
],
[
'Here is a big number: {'.self::F.', number}', // pattern
'Here is a big number: '.self::F_VALUE_FORMATTED, // expected
[ // params
self::F => self::F_VALUE
]
],
[
'Here is a big number: {'.self::F.', number, integer}', // pattern
'Here is a big number: '.self::F_VALUE_FORMATTED, // expected
[ // params
self::F => self::F_VALUE
]
],
[
'Here is a big number: {'.self::D.', number}', // pattern
'Here is a big number: '.self::D_VALUE_FORMATTED, // expected
[ // params
self::D => self::D_VALUE
]
],
[
'Here is a big number: {'.self::D.', number, integer}', // pattern
'Here is a big number: '.self::D_VALUE_FORMATTED_INTEGER, // expected
[ // params
self::D => self::D_VALUE
]
],
// This one was provided by Aura.Intl. Thanks!
[<<<_MSG_
{gender_of_host, select,
@ -168,6 +207,22 @@ _MSG_
$result = $formatter->fallbackFormat($pattern, ['begin' => 1, 'end' => 5, 'totalCount' => 10], 'en-US');
$this->assertEquals('Showing <b>1-5</b> of <b>10</b> items.', $result);
}
public function testUnsupportedPercentException()
{
$pattern = 'Number {'.self::N.', number, percent}';
$formatter = new FallbackMessageFormatter();
$this->setExpectedException('yii\base\NotSupportedException');
$formatter->fallbackFormat($pattern, [self::N => self::N_VALUE], 'en-US');
}
public function testUnsupportedCurrencyException()
{
$pattern = 'Number {'.self::N.', number, currency}';
$formatter = new FallbackMessageFormatter();
$this->setExpectedException('yii\base\NotSupportedException');
$formatter->fallbackFormat($pattern, [self::N => self::N_VALUE], 'en-US');
}
}
class FallbackMessageFormatter extends MessageFormatter

37
tests/framework/i18n/FormatterDateTest.php

@ -88,6 +88,43 @@ class FormatterDateTest extends TestCase
$this->assertSame($this->formatter->nullDisplay, $this->formatter->asDate(null));
}
public function testIntlAsDateOtherCalendars()
{
// Persian calendar
$this->formatter->locale = 'fa_IR@calendar=persian';
$this->formatter->calendar = \IntlDateFormatter::TRADITIONAL;
$this->formatter->timeZone = 'UTC';
$value = 1451606400; // Fri, 01 Jan 2016 00:00:00 (UTC)
$this->assertSame('۱۳۹۴', $this->formatter->asDate($value, 'php:Y'));
$value = new DateTime();
$value->setTimestamp(1451606400); // Fri, 01 Jan 2016 00:00:00 (UTC)
$this->assertSame('۱۳۹۴', $this->formatter->asDate($value, 'php:Y'));
if (version_compare(PHP_VERSION, '5.5.0', '>=')) {
$value = new \DateTimeImmutable('2016-01-01 00:00:00', new \DateTimeZone('UTC'));
$this->assertSame('۱۳۹۴', $this->formatter->asDate($value, 'php:Y'));
}
// Buddhist calendar
$this->formatter->locale = 'fr_FR@calendar=buddhist';
$this->formatter->calendar = \IntlDateFormatter::TRADITIONAL;
$this->formatter->timeZone = 'UTC';
$value = 1451606400; // Fri, 01 Jan 2016 00:00:00 (UTC)
$this->assertSame('2559', $this->formatter->asDate($value, 'php:Y'));
$value = new DateTime();
$value->setTimestamp(1451606400); // Fri, 01 Jan 2016 00:00:00 (UTC)
$this->assertSame('2559', $this->formatter->asDate($value, 'php:Y'));
if (version_compare(PHP_VERSION, '5.5.0', '>=')) {
$value = new \DateTimeImmutable('2016-01-01 00:00:00', new \DateTimeZone('UTC'));
$this->assertSame('2559', $this->formatter->asDate($value, 'php:Y'));
}
}
public function testIntlAsTime()
{
$this->testAsTime();

40
tests/framework/i18n/MessageFormatterTest.php

@ -19,6 +19,13 @@ class MessageFormatterTest extends TestCase
{
const N = 'n';
const N_VALUE = 42;
const F = 'f';
const F_VALUE = 2e+8;
const F_VALUE_FORMATTED = "200,000,000";
const D = 'd';
const D_VALUE = 200000000.101;
const D_VALUE_FORMATTED = "200,000,000.101";
const D_VALUE_FORMATTED_INTEGER = "200,000,000";
const SUBJECT = 'сабж';
const SUBJECT_VALUE = 'Answer to the Ultimate Question of Life, the Universe, and Everything';
@ -43,6 +50,39 @@ class MessageFormatterTest extends TestCase
]
],
[
'Here is a big number: {'.self::F.', number}', // pattern
'Here is a big number: '.self::F_VALUE_FORMATTED, // expected
[ // params
self::F => self::F_VALUE
]
],
[
'Here is a big number: {'.self::F.', number, integer}', // pattern
'Here is a big number: '.self::F_VALUE_FORMATTED, // expected
[ // params
self::F => self::F_VALUE
]
],
[
'Here is a big number: {'.self::D.', number}', // pattern
'Here is a big number: '.self::D_VALUE_FORMATTED, // expected
[ // params
self::D => self::D_VALUE
]
],
[
'Here is a big number: {'.self::D.', number, integer}', // pattern
'Here is a big number: '.self::D_VALUE_FORMATTED_INTEGER, // expected
[ // params
self::D => self::D_VALUE
]
],
// This one was provided by Aura.Intl. Thanks!
[<<<_MSG_
{gender_of_host, select,

2
tests/framework/web/AssetBundleTest.php

@ -59,7 +59,7 @@ class AssetBundleTest extends \yiiunit\TestCase
$this->assertTrue(is_dir($bundle->basePath));
foreach ($bundle->js as $filename) {
$publishedFile = $bundle->basePath . DIRECTORY_SEPARATOR . $filename;
$sourceFile = $bundle->basePath . DIRECTORY_SEPARATOR . $filename;
$sourceFile = $bundle->sourcePath . DIRECTORY_SEPARATOR . $filename;
$this->assertFileExists($publishedFile);
$this->assertFileEquals($publishedFile, $sourceFile);
$this->assertTrue(unlink($publishedFile));

Loading…
Cancel
Save