Commit 41d1a354 by Klimov Paul

Merge branch 'master' of github.com:yiisoft/yii2 into message-command

parents 04848a45 9d3c9db4
...@@ -10,7 +10,7 @@ if you have a project to be deployed for production soon. ...@@ -10,7 +10,7 @@ if you have a project to be deployed for production soon.
Thank you for using Yii 2 Advanced Application Template - an application template Thank you for using Yii 2 Advanced Application Template - an application template
that works out-of-box and can be easily customized to fit for your needs. that works out-of-box and can be easily customized to fit for your needs.
Yii 2 Advanced Application Template is best suitable for large projects requiring frontend and backstage separation, Yii 2 Advanced Application Template is best suitable for large projects requiring frontend and backend separation,
deployment in different environments, configuration nesting etc. deployment in different environments, configuration nesting etc.
...@@ -20,18 +20,18 @@ DIRECTORY STRUCTURE ...@@ -20,18 +20,18 @@ DIRECTORY STRUCTURE
``` ```
common common
config/ contains shared configurations config/ contains shared configurations
models/ contains model classes used in both backstage and frontend models/ contains model classes used in both backend and frontend
console console
config/ contains console configurations config/ contains console configurations
controllers/ contains console controllers (commands) controllers/ contains console controllers (commands)
migrations/ contains database migrations migrations/ contains database migrations
models/ contains console-specific model classes models/ contains console-specific model classes
runtime/ contains files generated during runtime runtime/ contains files generated during runtime
backstage backend
assets/ contains application assets such as JavaScript and CSS assets/ contains application assets such as JavaScript and CSS
config/ contains backstage configurations config/ contains backend configurations
controllers/ contains Web controller classes controllers/ contains Web controller classes
models/ contains backstage-specific model classes models/ contains backend-specific model classes
runtime/ contains files generated during runtime runtime/ contains files generated during runtime
views/ contains view files for the Web application views/ contains view files for the Web application
www/ contains the entry script and Web resources www/ contains the entry script and Web resources
...@@ -107,7 +107,7 @@ the installed application. You only need to do these once for all. ...@@ -107,7 +107,7 @@ the installed application. You only need to do these once for all.
Now you should be able to access: Now you should be able to access:
- the frontend using the URL `http://localhost/yii-advanced/frontend/www/` - the frontend using the URL `http://localhost/yii-advanced/frontend/www/`
- the backstage using the URL `http://localhost/yii-advanced/backstage/www/` - the backend using the URL `http://localhost/yii-advanced/backend/www/`
assuming `yii-advanced` is directly under the document root of your Web server. assuming `yii-advanced` is directly under the document root of your Web server.
...@@ -13,7 +13,7 @@ return array( ...@@ -13,7 +13,7 @@ return array(
'basePath' => dirname(__DIR__), 'basePath' => dirname(__DIR__),
'vendorPath' => dirname(dirname(__DIR__)) . '/vendor', 'vendorPath' => dirname(dirname(__DIR__)) . '/vendor',
'preload' => array('log'), 'preload' => array('log'),
'controllerNamespace' => 'backstage\controllers', 'controllerNamespace' => 'backend\controllers',
'modules' => array( 'modules' => array(
), ),
'components' => array( 'components' => array(
......
<?php <?php
namespace backstage\controllers; namespace backend\controllers;
use Yii; use Yii;
use yii\web\Controller; use yii\web\Controller;
......
...@@ -25,8 +25,8 @@ ...@@ -25,8 +25,8 @@
}, },
"extra": { "extra": {
"yii-install-writable": [ "yii-install-writable": [
"backstage/runtime", "backend/runtime",
"backstage/www/assets", "backend/www/assets",
"console/runtime", "console/runtime",
"console/migrations", "console/migrations",
......
@echo off @echo off
rem ------------------------------------------------------------- rem -------------------------------------------------------------
rem Yii command line install script for Windows. rem Yii command line init script for Windows.
rem rem
rem @author Qiang Xue <qiang.xue@gmail.com> rem @author Qiang Xue <qiang.xue@gmail.com>
rem @link http://www.yiiframework.com/ rem @link http://www.yiiframework.com/
...@@ -15,6 +15,6 @@ set YII_PATH=%~dp0 ...@@ -15,6 +15,6 @@ set YII_PATH=%~dp0
if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe
"%PHP_COMMAND%" "%YII_PATH%install" %* "%PHP_COMMAND%" "%YII_PATH%init" %*
@endlocal @endlocal
...@@ -16,10 +16,13 @@ class ActionFilter extends Behavior ...@@ -16,10 +16,13 @@ class ActionFilter extends Behavior
/** /**
* @var array list of action IDs that this filter should apply to. If this property is not set, * @var array list of action IDs that this filter should apply to. If this property is not set,
* then the filter applies to all actions, unless they are listed in [[except]]. * then the filter applies to all actions, unless they are listed in [[except]].
* If an action ID appears in both [[only]] and [[except]], this filter will NOT apply to it.
* @see except
*/ */
public $only; public $only;
/** /**
* @var array list of action IDs that this filter should not apply to. * @var array list of action IDs that this filter should not apply to.
* @see only
*/ */
public $except = array(); public $except = array();
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
namespace yii\base; namespace yii\base;
use Yii; use Yii;
use yii\web\HttpException;
/** /**
* Application is the base class for all application classes. * Application is the base class for all application classes.
...@@ -17,8 +18,14 @@ use Yii; ...@@ -17,8 +18,14 @@ use Yii;
*/ */
class Application extends Module class Application extends Module
{ {
const EVENT_BEFORE_REQUEST = 'beforeRequest'; /**
const EVENT_AFTER_REQUEST = 'afterRequest'; * @event Event an event that is triggered at the beginning of [[run()]].
*/
const EVENT_BEFORE_RUN = 'beforeRun';
/**
* @event Event an event that is triggered at the end of [[run()]].
*/
const EVENT_AFTER_RUN = 'afterRun';
/** /**
* @var string the application name. * @var string the application name.
*/ */
...@@ -128,6 +135,10 @@ class Application extends Module ...@@ -128,6 +135,10 @@ class Application extends Module
ini_set('display_errors', 0); ini_set('display_errors', 0);
set_exception_handler(array($this, 'handleException')); set_exception_handler(array($this, 'handleException'));
set_error_handler(array($this, 'handleError'), error_reporting()); set_error_handler(array($this, 'handleError'), error_reporting());
// Allocating twice more than required to display memory exhausted error
// in case of trying to allocate last 1 byte while all memory is taken.
$this->_memoryReserve = str_repeat('x', 1024 * 256);
register_shutdown_function(array($this, 'handleFatalError'));
} }
} }
...@@ -142,11 +153,10 @@ class Application extends Module ...@@ -142,11 +153,10 @@ class Application extends Module
{ {
if (!$this->_ended) { if (!$this->_ended) {
$this->_ended = true; $this->_ended = true;
$this->afterRequest(); $this->getResponse()->end();
$this->afterRun();
} }
$this->handleFatalError();
if ($exit) { if ($exit) {
exit($status); exit($status);
} }
...@@ -159,30 +169,30 @@ class Application extends Module ...@@ -159,30 +169,30 @@ class Application extends Module
*/ */
public function run() public function run()
{ {
$this->beforeRequest(); $this->beforeRun();
// Allocating twice more than required to display memory exhausted error $response = $this->getResponse();
// in case of trying to allocate last 1 byte while all memory is taken. $response->begin();
$this->_memoryReserve = str_repeat('x', 1024 * 256);
register_shutdown_function(array($this, 'end'), 0, false); register_shutdown_function(array($this, 'end'), 0, false);
$status = $this->processRequest(); $status = $this->processRequest();
$this->afterRequest(); $response->end();
$this->afterRun();
return $status; return $status;
} }
/** /**
* Raises the [[EVENT_BEFORE_REQUEST]] event right BEFORE the application processes the request. * Raises the [[EVENT_BEFORE_RUN]] event right BEFORE the application processes the request.
*/ */
public function beforeRequest() public function beforeRun()
{ {
$this->trigger(self::EVENT_BEFORE_REQUEST); $this->trigger(self::EVENT_BEFORE_RUN);
} }
/** /**
* Raises the [[EVENT_AFTER_REQUEST]] event right AFTER the application processes the request. * Raises the [[EVENT_AFTER_RUN]] event right AFTER the application processes the request.
*/ */
public function afterRequest() public function afterRun()
{ {
$this->trigger(self::EVENT_AFTER_REQUEST); $this->trigger(self::EVENT_AFTER_RUN);
} }
/** /**
...@@ -315,6 +325,15 @@ class Application extends Module ...@@ -315,6 +325,15 @@ class Application extends Module
} }
/** /**
* Returns the response component.
* @return \yii\web\Response|\yii\console\Response the response component
*/
public function getResponse()
{
return $this->getComponent('response');
}
/**
* Returns the view object. * Returns the view object.
* @return View the view object that is used to render various view files. * @return View the view object that is used to render various view files.
*/ */
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
namespace yii\base; namespace yii\base;
use Yii; use Yii;
use yii\web\HttpException;
/** /**
* ErrorHandler handles uncaught PHP errors and exceptions. * ErrorHandler handles uncaught PHP errors and exceptions.
...@@ -82,11 +83,12 @@ class ErrorHandler extends Component ...@@ -82,11 +83,12 @@ class ErrorHandler extends Component
} elseif (!(Yii::$app instanceof \yii\web\Application)) { } elseif (!(Yii::$app instanceof \yii\web\Application)) {
Yii::$app->renderException($exception); Yii::$app->renderException($exception);
} else { } else {
$response = Yii::$app->getResponse();
if (!headers_sent()) { if (!headers_sent()) {
if ($exception instanceof HttpException) { if ($exception instanceof HttpException) {
header('HTTP/1.0 ' . $exception->statusCode . ' ' . $exception->getName()); $response->setStatusCode($exception->statusCode);
} else { } else {
header('HTTP/1.0 500 ' . get_class($exception)); $response->setStatusCode(500);
} }
} }
if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') { if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') {
...@@ -100,13 +102,13 @@ class ErrorHandler extends Component ...@@ -100,13 +102,13 @@ class ErrorHandler extends Component
$view = new View(); $view = new View();
$request = ''; $request = '';
foreach (array('GET', 'POST', 'SERVER', 'FILES', 'COOKIE', 'SESSION', 'ENV') as $name) { foreach (array('_GET', '_POST', '_SERVER', '_FILES', '_COOKIE', '_SESSION', '_ENV') as $name) {
if (!empty($GLOBALS['_' . $name])) { if (!empty($GLOBALS[$name])) {
$request .= '$_' . $name . ' = ' . var_export($GLOBALS['_' . $name], true) . ";\n\n"; $request .= '$' . $name . ' = ' . var_export($GLOBALS[$name], true) . ";\n\n";
} }
} }
$request = rtrim($request, "\n\n"); $request = rtrim($request, "\n\n");
echo $view->renderFile($this->mainView, array( $response->content = $view->renderFile($this->mainView, array(
'exception' => $exception, 'exception' => $exception,
'request' => $request, 'request' => $request,
), $this); ), $this);
......
...@@ -14,19 +14,28 @@ namespace yii\base; ...@@ -14,19 +14,28 @@ namespace yii\base;
class Response extends Component class Response extends Component
{ {
/** /**
* @event Event an event raised when the application begins to generate the response.
*/
const EVENT_BEGIN_RESPONSE = 'beginResponse';
/**
* @event Event an event raised when the generation of the response finishes.
*/
const EVENT_END_RESPONSE = 'endResponse';
/**
* Starts output buffering * Starts output buffering
*/ */
public function beginOutput() public function beginBuffer()
{ {
ob_start(); ob_start();
ob_implicit_flush(false); ob_implicit_flush(false);
} }
/** /**
* Returns contents of the output buffer and discards it * Returns contents of the output buffer and stops the buffer.
* @return string output buffer contents * @return string output buffer contents
*/ */
public function endOutput() public function endBuffer()
{ {
return ob_get_clean(); return ob_get_clean();
} }
...@@ -35,16 +44,16 @@ class Response extends Component ...@@ -35,16 +44,16 @@ class Response extends Component
* Returns contents of the output buffer * Returns contents of the output buffer
* @return string output buffer contents * @return string output buffer contents
*/ */
public function getOutput() public function getBuffer()
{ {
return ob_get_contents(); return ob_get_contents();
} }
/** /**
* Discards the output buffer * Discards the output buffer
* @param boolean $all if true recursively discards all output buffers used * @param boolean $all if true, it will discards all output buffers.
*/ */
public function cleanOutput($all = true) public function cleanBuffer($all = true)
{ {
if ($all) { if ($all) {
for ($level = ob_get_level(); $level > 0; --$level) { for ($level = ob_get_level(); $level > 0; --$level) {
...@@ -56,4 +65,28 @@ class Response extends Component ...@@ -56,4 +65,28 @@ class Response extends Component
ob_end_clean(); ob_end_clean();
} }
} }
/**
* Begins generating the response.
* This method is called at the beginning of [[Application::run()]].
* The default implementation will trigger the [[EVENT_BEGIN_RESPONSE]] event.
* If you overwrite this method, make sure you call the parent implementation so that
* the event can be triggered.
*/
public function begin()
{
$this->trigger(self::EVENT_BEGIN_RESPONSE);
}
/**
* Ends generating the response.
* This method is called at the end of [[Application::run()]].
* The default implementation will trigger the [[EVENT_END_RESPONSE]] event.
* If you overwrite this method, make sure you call the parent implementation so that
* the event can be triggered.
*/
public function end()
{
$this->trigger(self::EVENT_END_RESPONSE);
}
} }
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\console;
/**
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class Response extends \yii\base\Response
{
}
...@@ -654,6 +654,32 @@ class Command extends \yii\base\Component ...@@ -654,6 +654,32 @@ class Command extends \yii\base\Component
} }
/** /**
* Creates a SQL command for adding a primary key constraint to an existing table.
* The method will properly quote the table and column names.
* @param string $name the name of the primary key constraint.
* @param string $table the table that the primary key constraint will be added to.
* @param string|array $columns comma separated string or array of columns that the primary key will consist of.
* @return Command the command object itself.
*/
public function addPrimaryKey($name, $table, $columns)
{
$sql = $this->db->getQueryBuilder()->addPrimaryKey($name, $table, $columns);
return $this->setSql($sql);
}
/**
* Creates a SQL command for removing a primary key constraint to an existing table.
* @param string $name the name of the primary key constraint to be removed.
* @param string $table the table that the primary key constraint will be removed from.
* @return Command the command object itself
*/
public function dropPrimaryKey($name, $table)
{
$sql = $this->db->getQueryBuilder()->dropPrimaryKey($name, $table);
return $this->setSql($sql);
}
/**
* Creates a SQL command for adding a foreign key constraint to an existing table. * Creates a SQL command for adding a foreign key constraint to an existing table.
* The method will properly quote the table and column names. * The method will properly quote the table and column names.
* @param string $name the name of the foreign key constraint. * @param string $name the name of the foreign key constraint.
......
...@@ -310,6 +310,35 @@ class Migration extends \yii\base\Component ...@@ -310,6 +310,35 @@ class Migration extends \yii\base\Component
} }
/** /**
* Builds and executes a SQL statement for creating a primary key.
* The method will properly quote the table and column names.
* @param string $name the name of the primary key constraint.
* @param string $table the table that the primary key constraint will be added to.
* @param string|array $columns comma separated string or array of columns that the primary key will consist of.
*/
public function addPrimaryKey($name, $table, $columns)
{
echo " > add primary key $name on $table (".(is_array($columns) ? implode(',',$columns) : $columns).") ...";
$time = microtime(true);
$this->db->createCommand()->addPrimaryKey($name, $table, $columns)->execute();
echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n";
}
/**
* Builds and executes a SQL statement for dropping a primary key.
* @param string $name the name of the primary key constraint to be removed.
* @param string $table the table that the primary key constraint will be removed from.
* @return Command the command object itself
*/
public function dropPrimaryKey($name, $table)
{
echo " > drop primary key $name ...";
$time = microtime(true);
$this->db->createCommand()->dropPrimaryKey($name, $table)->execute();
echo " done (time: " . sprintf('%.3f', microtime(true) - $time) . "s)\n";
}
/**
* Builds a SQL statement for adding a foreign key constraint to an existing table. * Builds a SQL statement for adding a foreign key constraint to an existing table.
* The method will properly quote the table and column names. * The method will properly quote the table and column names.
* @param string $name the name of the foreign key constraint. * @param string $name the name of the foreign key constraint.
......
...@@ -270,6 +270,41 @@ class QueryBuilder extends \yii\base\Object ...@@ -270,6 +270,41 @@ class QueryBuilder extends \yii\base\Object
} }
/** /**
* Builds a SQL statement for adding a primary key constraint to an existing table.
* @param string $name the name of the primary key constraint.
* @param string $table the table that the primary key constraint will be added to.
* @param string|array $columns comma separated string or array of columns that the primary key will consist of.
* @return string the SQL statement for adding a primary key constraint to an existing table.
*/
public function addPrimaryKey($name, $table, $columns)
{
if (is_string($columns)) {
$columns=preg_split('/\s*,\s*/',$columns,-1,PREG_SPLIT_NO_EMPTY);
}
foreach ($columns as $i=>$col) {
$columns[$i]=$this->db->quoteColumnName($col);
}
return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' ADD CONSTRAINT '
. $this->db->quoteColumnName($name) . ' PRIMARY KEY ('
. implode(', ', $columns). ' )';
}
/**
* Builds a SQL statement for removing a primary key constraint to an existing table.
* @param string $name the name of the primary key constraint to be removed.
* @param string $table the table that the primary key constraint will be removed from.
* @return string the SQL statement for removing a primary key constraint from an existing table. *
*/
public function dropPrimaryKey($name, $table)
{
return 'ALTER TABLE ' . $this->db->quoteTableName($table)
. ' DROP CONSTRAINT ' . $this->db->quoteColumnName($name);
}
/**
* Builds a SQL statement for truncating a DB table. * Builds a SQL statement for truncating a DB table.
* @param string $table the table to be truncated. The name will be properly quoted by the method. * @param string $table the table to be truncated. The name will be properly quoted by the method.
* @return string the SQL statement for truncating a DB table. * @return string the SQL statement for truncating a DB table.
......
...@@ -89,6 +89,17 @@ class QueryBuilder extends \yii\db\QueryBuilder ...@@ -89,6 +89,17 @@ class QueryBuilder extends \yii\db\QueryBuilder
} }
/** /**
* Builds a SQL statement for removing a primary key constraint to an existing table.
* @param string $name the name of the primary key constraint to be removed.
* @param string $table the table that the primary key constraint will be removed from.
* @return string the SQL statement for removing a primary key constraint from an existing table.
*/
public function dropPrimaryKey($name, $table)
{
return 'ALTER TABLE ' . $this->db->quoteTableName($table) . ' DROP PRIMARY KEY';
}
/**
* Creates a SQL statement for resetting the sequence value of a table's primary key. * Creates a SQL statement for resetting the sequence value of a table's primary key.
* The sequence will be reset such that the primary key of the next new row inserted * The sequence will be reset such that the primary key of the next new row inserted
* will have the specified value or 1. * will have the specified value or 1.
...@@ -113,7 +124,7 @@ class QueryBuilder extends \yii\db\QueryBuilder ...@@ -113,7 +124,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
} elseif ($table === null) { } elseif ($table === null) {
throw new InvalidParamException("Table not found: $tableName"); throw new InvalidParamException("Table not found: $tableName");
} else { } else {
throw new InvalidParamException("There is not sequence associated with table '$tableName'.'"); throw new InvalidParamException("There is not sequence associated with table '$tableName'.");
} }
} }
......
...@@ -22,13 +22,13 @@ class QueryBuilder extends \yii\db\QueryBuilder ...@@ -22,13 +22,13 @@ class QueryBuilder extends \yii\db\QueryBuilder
*/ */
public $typeMap = array( public $typeMap = array(
Schema::TYPE_PK => 'serial not null primary key', Schema::TYPE_PK => 'serial not null primary key',
Schema::TYPE_STRING => 'varchar', Schema::TYPE_STRING => 'varchar(255)',
Schema::TYPE_TEXT => 'text', Schema::TYPE_TEXT => 'text',
Schema::TYPE_SMALLINT => 'smallint', Schema::TYPE_SMALLINT => 'smallint',
Schema::TYPE_INTEGER => 'integer', Schema::TYPE_INTEGER => 'integer',
Schema::TYPE_BIGINT => 'bigint', Schema::TYPE_BIGINT => 'bigint',
Schema::TYPE_FLOAT => 'double precision', Schema::TYPE_FLOAT => 'double precision',
Schema::TYPE_DECIMAL => 'numeric', Schema::TYPE_DECIMAL => 'numeric(10,0)',
Schema::TYPE_DATETIME => 'timestamp', Schema::TYPE_DATETIME => 'timestamp',
Schema::TYPE_TIMESTAMP => 'timestamp', Schema::TYPE_TIMESTAMP => 'timestamp',
Schema::TYPE_TIME => 'time', Schema::TYPE_TIME => 'time',
...@@ -37,5 +37,4 @@ class QueryBuilder extends \yii\db\QueryBuilder ...@@ -37,5 +37,4 @@ class QueryBuilder extends \yii\db\QueryBuilder
Schema::TYPE_BOOLEAN => 'boolean', Schema::TYPE_BOOLEAN => 'boolean',
Schema::TYPE_MONEY => 'numeric(19,4)', Schema::TYPE_MONEY => 'numeric(19,4)',
); );
} }
...@@ -43,6 +43,7 @@ class Schema extends \yii\db\Schema ...@@ -43,6 +43,7 @@ class Schema extends \yii\db\Schema
'circle' => self::TYPE_STRING, 'circle' => self::TYPE_STRING,
'date' => self::TYPE_DATE, 'date' => self::TYPE_DATE,
'real' => self::TYPE_FLOAT, 'real' => self::TYPE_FLOAT,
'decimal' => self::TYPE_DECIMAL,
'double precision' => self::TYPE_DECIMAL, 'double precision' => self::TYPE_DECIMAL,
'inet' => self::TYPE_STRING, 'inet' => self::TYPE_STRING,
'smallint' => self::TYPE_SMALLINT, 'smallint' => self::TYPE_SMALLINT,
...@@ -55,7 +56,6 @@ class Schema extends \yii\db\Schema ...@@ -55,7 +56,6 @@ class Schema extends \yii\db\Schema
'money' => self::TYPE_MONEY, 'money' => self::TYPE_MONEY,
'name' => self::TYPE_STRING, 'name' => self::TYPE_STRING,
'numeric' => self::TYPE_STRING, 'numeric' => self::TYPE_STRING,
'numrange' => self::TYPE_DECIMAL,
'oid' => self::TYPE_BIGINT, // should not be used. it's pg internal! 'oid' => self::TYPE_BIGINT, // should not be used. it's pg internal!
'path' => self::TYPE_STRING, 'path' => self::TYPE_STRING,
'point' => self::TYPE_STRING, 'point' => self::TYPE_STRING,
...@@ -165,11 +165,11 @@ SQL; ...@@ -165,11 +165,11 @@ SQL;
$columns = explode(',', $constraint['columns']); $columns = explode(',', $constraint['columns']);
$fcolumns = explode(',', $constraint['foreign_columns']); $fcolumns = explode(',', $constraint['foreign_columns']);
if ($constraint['foreign_table_schema'] !== $this->defaultSchema) { if ($constraint['foreign_table_schema'] !== $this->defaultSchema) {
$foreign_table = $constraint['foreign_table_schema'] . '.' . $constraint['foreign_table_name']; $foreignTable = $constraint['foreign_table_schema'] . '.' . $constraint['foreign_table_name'];
} else { } else {
$foreign_table = $constraint['foreign_table_name']; $foreignTable = $constraint['foreign_table_name'];
} }
$citem = array($foreign_table); $citem = array($foreignTable);
foreach ($columns as $idx => $column) { foreach ($columns as $idx => $column) {
$citem[] = array($fcolumns[$idx] => $column); $citem[] = array($fcolumns[$idx] => $column);
} }
...@@ -243,6 +243,9 @@ ORDER BY ...@@ -243,6 +243,9 @@ ORDER BY
SQL; SQL;
$columns = $this->db->createCommand($sql)->queryAll(); $columns = $this->db->createCommand($sql)->queryAll();
if (empty($columns)) {
return false;
}
foreach ($columns as $column) { foreach ($columns as $column) {
$column = $this->loadColumnSchema($column); $column = $this->loadColumnSchema($column);
$table->columns[$column->name] = $column; $table->columns[$column->name] = $column;
...@@ -285,5 +288,4 @@ SQL; ...@@ -285,5 +288,4 @@ SQL;
$column->phpType = $this->getColumnPhpType($column); $column->phpType = $this->getColumnPhpType($column);
return $column; return $column;
} }
} }
...@@ -179,4 +179,30 @@ class QueryBuilder extends \yii\db\QueryBuilder ...@@ -179,4 +179,30 @@ class QueryBuilder extends \yii\db\QueryBuilder
{ {
throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.'); throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
} }
/**
* Builds a SQL statement for adding a primary key constraint to an existing table.
* @param string $name the name of the primary key constraint.
* @param string $table the table that the primary key constraint will be added to.
* @param string|array $columns comma separated string or array of columns that the primary key will consist of.
* @return string the SQL statement for adding a primary key constraint to an existing table.
* @throws NotSupportedException this is not supported by SQLite
*/
public function addPrimaryKey($name, $table, $columns)
{
throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
}
/**
* Builds a SQL statement for removing a primary key constraint to an existing table.
* @param string $name the name of the primary key constraint to be removed.
* @param string $table the table that the primary key constraint will be removed from.
* @return string the SQL statement for removing a primary key constraint from an existing table.
* @throws NotSupportedException this is not supported by SQLite *
*/
public function dropPrimaryKey($name, $table)
{
throw new NotSupportedException(__METHOD__ . ' is not supported by SQLite.');
}
} }
...@@ -10,6 +10,7 @@ ...@@ -10,6 +10,7 @@
namespace yii\helpers\base; namespace yii\helpers\base;
use Yii; use Yii;
use yii\helpers\StringHelper;
/** /**
* Filesystem helper * Filesystem helper
...@@ -95,7 +96,7 @@ class FileHelper ...@@ -95,7 +96,7 @@ class FileHelper
} }
} }
return $checkExtension ? self::getMimeTypeByExtension($file) : null; return $checkExtension ? static::getMimeTypeByExtension($file) : null;
} }
/** /**
...@@ -133,12 +134,21 @@ class FileHelper ...@@ -133,12 +134,21 @@ class FileHelper
* *
* - dirMode: integer, the permission to be set for newly copied directories. Defaults to 0777. * - dirMode: integer, the permission to be set for newly copied directories. Defaults to 0777.
* - fileMode: integer, the permission to be set for newly copied files. Defaults to the current environment setting. * - fileMode: integer, the permission to be set for newly copied files. Defaults to the current environment setting.
* - beforeCopy: callback, a PHP callback that is called before copying each sub-directory or file. * - filter: callback, a PHP callback that is called for each sub-directory or file.
* If the callback returns false, the copy operation for the sub-directory or file will be cancelled. * If the callback returns false, the the sub-directory or file will not be copied.
* The signature of the callback should be: `function ($path)`, where `$path` refers the full path to be copied.
* - fileTypes: array, list of file name suffix (without dot). Only files with these suffixes will be copied.
* - only: array, list of patterns that the files or directories should match if they want to be copied.
* A path matches a pattern if it contains the pattern string at its end. For example,
* '/a/b' will match all files and directories ending with '/a/b'; and the '.svn' will match all files and
* directories whose name ends with '.svn'. Note, the '/' characters in a pattern matches both '/' and '\'.
* If a file/directory matches both a name in "only" and "except", it will NOT be copied.
* - except: array, list of patterns that the files or directories should NOT match if they want to be copied.
* For more details on how to specify the patterns, please refer to the "only" option.
* - recursive: boolean, whether the files under the subdirectories should also be copied. Defaults to true.
* - afterCopy: callback, a PHP callback that is called after each sub-directory or file is successfully copied.
* The signature of the callback should be: `function ($from, $to)`, where `$from` is the sub-directory or * The signature of the callback should be: `function ($from, $to)`, where `$from` is the sub-directory or
* file to be copied from, while `$to` is the copy target. * file copied from, while `$to` is the copy target.
* - afterCopy: callback, a PHP callback that is called after a sub-directory or file is successfully copied.
* The signature of the callback is similar to that of `beforeCopy`.
*/ */
public static function copyDirectory($src, $dst, $options = array()) public static function copyDirectory($src, $dst, $options = array())
{ {
...@@ -153,7 +163,7 @@ class FileHelper ...@@ -153,7 +163,7 @@ class FileHelper
} }
$from = $src . DIRECTORY_SEPARATOR . $file; $from = $src . DIRECTORY_SEPARATOR . $file;
$to = $dst . DIRECTORY_SEPARATOR . $file; $to = $dst . DIRECTORY_SEPARATOR . $file;
if (!isset($options['beforeCopy']) || call_user_func($options['beforeCopy'], $from, $to)) { if (static::filterPath($from, $options)) {
if (is_file($from)) { if (is_file($from)) {
copy($from, $to); copy($from, $to);
if (isset($options['fileMode'])) { if (isset($options['fileMode'])) {
...@@ -169,4 +179,129 @@ class FileHelper ...@@ -169,4 +179,129 @@ class FileHelper
} }
closedir($handle); closedir($handle);
} }
/**
* Removes a directory (and all its content) recursively.
* @param string $dir the directory to be deleted recursively.
*/
public static function removeDirectory($dir)
{
if (!is_dir($dir) || !($handle = opendir($dir))) {
return;
}
while (($file = readdir($handle)) !== false) {
if ($file === '.' || $file === '..') {
continue;
}
$path = $dir . DIRECTORY_SEPARATOR . $file;
if (is_file($path)) {
unlink($path);
} else {
static::removeDirectory($path);
}
}
closedir($handle);
rmdir($dir);
}
/**
* Returns the files found under the specified directory and subdirectories.
* @param string $dir the directory under which the files will be looked for.
* @param array $options options for file searching. Valid options are:
*
* - filter: callback, a PHP callback that is called for each sub-directory or file.
* If the callback returns false, the the sub-directory or file will be excluded from the returning result.
* The signature of the callback should be: `function ($path)`, where `$path` refers the full path to be filtered.
* - fileTypes: array, list of file name suffix (without dot). Only files with these suffixes will be returned.
* - only: array, list of patterns that the files or directories should match if they want to be returned.
* A path matches a pattern if it contains the pattern string at its end. For example,
* '/a/b' will match all files and directories ending with '/a/b'; and the '.svn' will match all files and
* directories whose name ends with '.svn'. Note, the '/' characters in a pattern matches both '/' and '\'.
* If a file/directory matches both a name in "only" and "except", it will NOT be returned.
* - except: array, list of patterns that the files or directories should NOT match if they want to be returned.
* For more details on how to specify the patterns, please refer to the "only" option.
* - recursive: boolean, whether the files under the subdirectories should also be lookied for. Defaults to true.
* @return array files found under the directory. The file list is sorted.
*/
public static function findFiles($dir, $options = array())
{
$list = array();
$handle = opendir($dir);
while (($file = readdir($handle)) !== false) {
if ($file === '.' || $file === '..') {
continue;
}
$path = $dir . DIRECTORY_SEPARATOR . $file;
if (static::filterPath($path, $options)) {
if (is_file($path)) {
$list[] = $path;
} elseif (!isset($options['recursive']) || $options['recursive']) {
$list = array_merge($list, static::findFiles($path, $options));
}
}
}
closedir($handle);
return $list;
}
/**
* Checks if the given file path satisfies the filtering options.
* @param string $path the path of the file or directory to be checked
* @param array $options the filtering options. See [[findFiles()]] for explanations of
* the supported options.
* @return boolean whether the file or directory satisfies the filtering options.
*/
public static function filterPath($path, $options)
{
if (isset($options['filter']) && !call_user_func($options['filter'], $path)) {
return false;
}
$path = str_replace('\\', '/', $path);
$n = StringHelper::strlen($path);
if (!empty($options['except'])) {
foreach ($options['except'] as $name) {
if (StringHelper::substr($path, -StringHelper::strlen($name), $n) === $name) {
return false;
}
}
}
if (!empty($options['only'])) {
foreach ($options['only'] as $name) {
if (StringHelper::substr($path, -StringHelper::strlen($name), $n) !== $name) {
return false;
}
}
}
if (!empty($options['fileTypes']) && is_file($path)) {
return in_array(pathinfo($path, PATHINFO_EXTENSION), $options['fileTypes']);
} else {
return true;
}
}
/**
* Makes directory.
*
* This method is similar to the PHP `mkdir()` function except that
* it uses `chmod()` to set the permission of the created directory
* in order to avoid the impact of the `umask` setting.
*
* @param string $path path to be created.
* @param integer $mode the permission to be set for created directory.
* @param boolean $recursive whether to create parent directories if they do not exist.
* @return boolean whether the directory is created successfully
*/
public static function mkdir($path, $mode = 0777, $recursive = true)
{
if (is_dir($path)) {
return true;
}
$parentDir = dirname($path);
if ($recursive && !is_dir($parentDir)) {
static::mkdir($parentDir, $mode, true);
}
$result = mkdir($path, $mode);
chmod($path, $mode);
return $result;
}
} }
...@@ -43,8 +43,10 @@ class StringHelper ...@@ -43,8 +43,10 @@ class StringHelper
/** /**
* Returns the trailing name component of a path. * Returns the trailing name component of a path.
* This method does the same as the php function basename() except that it will * This method does the same as the php function `basename()` except that it will
* always use \ and / as directory separators, independent of the operating system. * always use \ and / as directory separators, independent of the operating system.
* This method was mainly created to work on php namespaces. When working with real
* file paths, php's `basename()` should work fine for you.
* Note: this method is not aware of the actual filesystem, or path components such as "..". * Note: this method is not aware of the actual filesystem, or path components such as "..".
* @param string $path A path string. * @param string $path A path string.
* @param string $suffix If the name component ends in suffix this will also be cut off. * @param string $suffix If the name component ends in suffix this will also be cut off.
......
...@@ -14,7 +14,7 @@ $context = $this->context; ...@@ -14,7 +14,7 @@ $context = $this->context;
<meta charset="utf-8"/> <meta charset="utf-8"/>
<title><?php <title><?php
if ($exception instanceof \yii\base\HttpException) { if ($exception instanceof \yii\web\HttpException) {
echo (int) $exception->statusCode . ' ' . $context->htmlEncode($exception->getName()); echo (int) $exception->statusCode . ' ' . $context->htmlEncode($exception->getName());
} elseif ($exception instanceof \yii\base\Exception) { } elseif ($exception instanceof \yii\base\Exception) {
echo $context->htmlEncode($exception->getName() . ' – ' . get_class($exception)); echo $context->htmlEncode($exception->getName() . ' – ' . get_class($exception));
...@@ -362,7 +362,7 @@ pre .diff .change{ ...@@ -362,7 +362,7 @@ pre .diff .change{
<?php else: ?> <?php else: ?>
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAA6CAIAAACPssguAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjcwRURBOUQxQzQ3RDExRTJCRjVFODJDQkFGODFDN0VBIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjcwRURBOUQyQzQ3RDExRTJCRjVFODJDQkFGODFDN0VBIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NzBFREE5Q0ZDNDdEMTFFMkJGNUU4MkNCQUY4MUM3RUEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NzBFREE5RDBDNDdEMTFFMkJGNUU4MkNCQUY4MUM3RUEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4L6mYsAAAE9klEQVR42tRa227aTBDGiwkhJtD8EanSQ6RKqKpSBbWqql5U7QP0TfsSvWhvStMEO6cqDUpjRylSQ1QC+IQN/mdrhBIb413vgsheIDDr9XwzOzPfzFrwPC/Fe8Cauiybe3uersNPQZJylYr04oUgCNyfJXJfcTAYXH/5YoH0qjq66LbbTrtdfPcOITTXALDuf/40FSWlabeuq6rpeZkHD6Ryma8dOOvDdV19Zycg/XBoml6r9ft9vk9EnNV/dOT8+RM1oXdxof/4wdfrEF/1G7Va+uIiagL8BfaBafMIAHy38/17/+/fydNgAkyDyXMHwLEsS1EmqH9kBOvgACbPFwDQaPfr11S9TjT7+JijEfgAsFst8/CQYv7BQe/6el4AQGTsfPuGbqSt2CGcnrarVS4hlRUAxETz9+/eyQntjXAL3MgeUhG7+vXtbSr1Dx+sqoYss3sCYlS/cXLinJ8nu713emqoKqMRmAD4xAElBQA3GpA62DwBsagfiIN7ecnyeHZyIbKoP4o46KurY2+Rrq7CeQ08YenZs0wmM1MAI+KQDv3VKpd7r1+L2WwQsGH0q9VCyN3dZrOrKPdevUpGsxMCmEAcMqVSaWNjcXExcN00zRbQ7BAAWMSs1ZY2N7NLSzMCMJk4ZBFaXl4OAxBF0Yza68fH3Z2dzNu3Ceo1lMB3raurScQhgqjBDhGiibS1v98zzVlEIax+CJ0TM9fYqAIXB9HRBsiFXq0myGuIVv2xxGGQNCaCVcG2tCGVDkBi4jBU80ThYFlwLVojICr1kxAHFLHRsWoNgzu5oABASBw8247ygVScZJhcyDIVuUDk6tf39oiIg+exUAOwMNiZfAVSAI7jGPv7sSUvhwLl/Jyqc0EEwA+dsR2H+EFWy4OdwdqERiACQNhxGIaaaCcWyDY3Zni7u2BzPgDoOg64Yo92YvJ43e2CzUlCKmIlDsSZ2FcG4Qq4faQoJO0jRKJ+uszFq+tWr5PkNRRPHM7O2IWBpdxGYxrkAk2DODihbAqKdJpNkRIDJhdxniDGEgdavgpSWp8+XX7+HADg9vsJCnDcPnr+XHr0KKpeE6fScdA0l1P7AIwAu2BxfR3qIYotREEcpj8mkwsxkjjs7iYmDuRdCXJykSuXx3YuxEji0O2mE0lvlEpWpZK6vWsxv2s0BCjeE1kVk4ujo2KlEvYEkZE4jMlBCK1sbUFdf7NCx22YlRVHURKuCeSiVpM2N8NGEFmJwzgAIH2hUAhYAD6vGQ6J/bOpe2/eBDoXiJ04jNm1CAVsDT8Zj7ijyAViJQ6zHPV6Z3s7kNfQrcylqlCVpuZ42IeHgbMpdJM4QD2auFc+mxE+m0JUHQfSqHd2Fsg7mMxxWjxwNiX43yBzNT9+9GSZj6I2NsKZH5e5Y9+hSODQ79+vfvjgP0K8SRzSvCw9jgvxJBe/fpmaln/yBAe3WXYcOHYuRmdTCFctmsah4zBjhtdo+D08BGHVhs1zd9Q/MgKIDcJjCwzyefv+/dRdG86/zItzvri+7knS3ZLefPgQPX7sMxSUy+W8ly+NtbW7Ir2+tjaoVHL5fDqdFgFENpstlMtQ/bdlOc3pJZLpjX6x6G1t/ff0KegdH1v96yV7EEk7nU6r1bJtm+PrVPx9FyFQd7FYBMa+sLAwBODnMgirAAM+p/EqLDcuJAiwbaCsgU+fsf8vwADjle3ME1OGEwAAAABJRU5ErkJggg==" alt="Attention"/> <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAA6CAIAAACPssguAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjcwRURBOUQxQzQ3RDExRTJCRjVFODJDQkFGODFDN0VBIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjcwRURBOUQyQzQ3RDExRTJCRjVFODJDQkFGODFDN0VBIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NzBFREE5Q0ZDNDdEMTFFMkJGNUU4MkNCQUY4MUM3RUEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NzBFREE5RDBDNDdEMTFFMkJGNUU4MkNCQUY4MUM3RUEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4L6mYsAAAE9klEQVR42tRa227aTBDGiwkhJtD8EanSQ6RKqKpSBbWqql5U7QP0TfsSvWhvStMEO6cqDUpjRylSQ1QC+IQN/mdrhBIb413vgsheIDDr9XwzOzPfzFrwPC/Fe8Cauiybe3uersNPQZJylYr04oUgCNyfJXJfcTAYXH/5YoH0qjq66LbbTrtdfPcOITTXALDuf/40FSWlabeuq6rpeZkHD6Ryma8dOOvDdV19Zycg/XBoml6r9ft9vk9EnNV/dOT8+RM1oXdxof/4wdfrEF/1G7Va+uIiagL8BfaBafMIAHy38/17/+/fydNgAkyDyXMHwLEsS1EmqH9kBOvgACbPFwDQaPfr11S9TjT7+JijEfgAsFst8/CQYv7BQe/6el4AQGTsfPuGbqSt2CGcnrarVS4hlRUAxETz9+/eyQntjXAL3MgeUhG7+vXtbSr1Dx+sqoYss3sCYlS/cXLinJ8nu713emqoKqMRmAD4xAElBQA3GpA62DwBsagfiIN7ecnyeHZyIbKoP4o46KurY2+Rrq7CeQ08YenZs0wmM1MAI+KQDv3VKpd7r1+L2WwQsGH0q9VCyN3dZrOrKPdevUpGsxMCmEAcMqVSaWNjcXExcN00zRbQ7BAAWMSs1ZY2N7NLSzMCMJk4ZBFaXl4OAxBF0Yza68fH3Z2dzNu3Ceo1lMB3raurScQhgqjBDhGiibS1v98zzVlEIax+CJ0TM9fYqAIXB9HRBsiFXq0myGuIVv2xxGGQNCaCVcG2tCGVDkBi4jBU80ThYFlwLVojICr1kxAHFLHRsWoNgzu5oABASBw8247ygVScZJhcyDIVuUDk6tf39oiIg+exUAOwMNiZfAVSAI7jGPv7sSUvhwLl/Jyqc0EEwA+dsR2H+EFWy4OdwdqERiACQNhxGIaaaCcWyDY3Zni7u2BzPgDoOg64Yo92YvJ43e2CzUlCKmIlDsSZ2FcG4Qq4faQoJO0jRKJ+uszFq+tWr5PkNRRPHM7O2IWBpdxGYxrkAk2DODihbAqKdJpNkRIDJhdxniDGEgdavgpSWp8+XX7+HADg9vsJCnDcPnr+XHr0KKpeE6fScdA0l1P7AIwAu2BxfR3qIYotREEcpj8mkwsxkjjs7iYmDuRdCXJykSuXx3YuxEji0O2mE0lvlEpWpZK6vWsxv2s0BCjeE1kVk4ujo2KlEvYEkZE4jMlBCK1sbUFdf7NCx22YlRVHURKuCeSiVpM2N8NGEFmJwzgAIH2hUAhYAD6vGQ6J/bOpe2/eBDoXiJ04jNm1CAVsDT8Zj7ijyAViJQ6zHPV6Z3s7kNfQrcylqlCVpuZ42IeHgbMpdJM4QD2auFc+mxE+m0JUHQfSqHd2Fsg7mMxxWjxwNiX43yBzNT9+9GSZj6I2NsKZH5e5Y9+hSODQ79+vfvjgP0K8SRzSvCw9jgvxJBe/fpmaln/yBAe3WXYcOHYuRmdTCFctmsah4zBjhtdo+D08BGHVhs1zd9Q/MgKIDcJjCwzyefv+/dRdG86/zItzvri+7knS3ZLefPgQPX7sMxSUy+W8ly+NtbW7Ir2+tjaoVHL5fDqdFgFENpstlMtQ/bdlOc3pJZLpjX6x6G1t/ff0KegdH1v96yV7EEk7nU6r1bJtm+PrVPx9FyFQd7FYBMa+sLAwBODnMgirAAM+p/EqLDcuJAiwbaCsgU+fsf8vwADjle3ME1OGEwAAAABJRU5ErkJggg==" alt="Attention"/>
<h1><?php <h1><?php
if ($exception instanceof \yii\base\HttpException) { if ($exception instanceof \yii\web\HttpException) {
echo '<span>' . $context->createHttpStatusLink($exception->statusCode, $context->htmlEncode($exception->getName())) . '</span>'; echo '<span>' . $context->createHttpStatusLink($exception->statusCode, $context->htmlEncode($exception->getName())) . '</span>';
echo ' &ndash; ' . $context->addTypeLinks(get_class($exception)); echo ' &ndash; ' . $context->addTypeLinks(get_class($exception));
} elseif ($exception instanceof \yii\base\Exception) { } elseif ($exception instanceof \yii\base\Exception) {
......
...@@ -10,7 +10,7 @@ namespace yii\web; ...@@ -10,7 +10,7 @@ namespace yii\web;
use Yii; use Yii;
use yii\base\Action; use yii\base\Action;
use yii\base\ActionFilter; use yii\base\ActionFilter;
use yii\base\HttpException; use yii\web\HttpException;
/** /**
* AccessControl provides simple access control based on a set of rules. * AccessControl provides simple access control based on a set of rules.
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
namespace yii\web; namespace yii\web;
use Yii; use Yii;
use yii\base\HttpException; use yii\web\HttpException;
use yii\base\InvalidRouteException; use yii\base\InvalidRouteException;
/** /**
......
...@@ -277,11 +277,8 @@ class CaptchaAction extends Action ...@@ -277,11 +277,8 @@ class CaptchaAction extends Action
imagecolordeallocate($image, $foreColor); imagecolordeallocate($image, $foreColor);
header('Pragma: public'); $this->sendHttpHeaders();
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Content-Transfer-Encoding: binary');
header("Content-type: image/png");
imagepng($image); imagepng($image);
imagedestroy($image); imagedestroy($image);
} }
...@@ -319,12 +316,21 @@ class CaptchaAction extends Action ...@@ -319,12 +316,21 @@ class CaptchaAction extends Action
$x += (int)($fontMetrics['textWidth']) + $this->offset; $x += (int)($fontMetrics['textWidth']) + $this->offset;
} }
header('Pragma: public');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Content-Transfer-Encoding: binary');
header("Content-type: image/png");
$image->setImageFormat('png'); $image->setImageFormat('png');
echo $image; Yii::$app->getResponse()->content = (string)$image;
$this->sendHttpHeaders();
}
/**
* Sends the HTTP headers needed by image response.
*/
protected function sendHttpHeaders()
{
Yii::$app->getResponse()->getHeaders()
->set('Pragma', 'public')
->set('Expires', '0')
->set('Cache-Control', 'must-revalidate, post-check=0, pre-check=0')
->set('Content-Transfer-Encoding', 'binary')
->set('Content-type', 'image/png');
} }
} }
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
namespace yii\web; namespace yii\web;
use Yii; use Yii;
use yii\base\HttpException; use yii\web\HttpException;
use yii\base\InlineAction; use yii\base\InlineAction;
/** /**
......
...@@ -45,7 +45,7 @@ class Cookie extends \yii\base\Object ...@@ -45,7 +45,7 @@ class Cookie extends \yii\base\Object
* By setting this property to true, the cookie will not be accessible by scripting languages, * By setting this property to true, the cookie will not be accessible by scripting languages,
* such as JavaScript, which can effectively help to reduce identity theft through XSS attacks. * such as JavaScript, which can effectively help to reduce identity theft through XSS attacks.
*/ */
public $httponly = false; public $httpOnly = false;
/** /**
* Magic method to turn a cookie object into a string without having to explicitly access [[value]]. * Magic method to turn a cookie object into a string without having to explicitly access [[value]].
......
...@@ -9,7 +9,8 @@ namespace yii\web; ...@@ -9,7 +9,8 @@ namespace yii\web;
use Yii; use Yii;
use ArrayIterator; use ArrayIterator;
use yii\helpers\SecurityHelper; use yii\base\InvalidCallException;
use yii\base\Object;
/** /**
* CookieCollection maintains the cookies available in the current request. * CookieCollection maintains the cookies available in the current request.
...@@ -19,17 +20,12 @@ use yii\helpers\SecurityHelper; ...@@ -19,17 +20,12 @@ use yii\helpers\SecurityHelper;
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0 * @since 2.0
*/ */
class CookieCollection extends \yii\base\Object implements \IteratorAggregate, \ArrayAccess, \Countable class CookieCollection extends Object implements \IteratorAggregate, \ArrayAccess, \Countable
{ {
/** /**
* @var boolean whether to enable cookie validation. By setting this property to true, * @var boolean whether this collection is read only.
* if a cookie is tampered on the client side, it will be ignored when received on the server side.
*/ */
public $enableValidation = true; public $readOnly = false;
/**
* @var string the secret key used for cookie validation. If not set, a random key will be generated and used.
*/
public $validationKey;
/** /**
* @var Cookie[] the cookies in this collection (indexed by the cookie names) * @var Cookie[] the cookies in this collection (indexed by the cookie names)
...@@ -38,12 +34,14 @@ class CookieCollection extends \yii\base\Object implements \IteratorAggregate, \ ...@@ -38,12 +34,14 @@ class CookieCollection extends \yii\base\Object implements \IteratorAggregate, \
/** /**
* Constructor. * Constructor.
* @param array $cookies the cookies that this collection initially contains. This should be
* an array of name-value pairs.s
* @param array $config name-value pairs that will be used to initialize the object properties * @param array $config name-value pairs that will be used to initialize the object properties
*/ */
public function __construct($config = array()) public function __construct($cookies = array(), $config = array())
{ {
$this->_cookies = $cookies;
parent::__construct($config); parent::__construct($config);
$this->_cookies = $this->loadCookies();
} }
/** /**
...@@ -114,50 +112,53 @@ class CookieCollection extends \yii\base\Object implements \IteratorAggregate, \ ...@@ -114,50 +112,53 @@ class CookieCollection extends \yii\base\Object implements \IteratorAggregate, \
* Adds a cookie to the collection. * Adds a cookie to the collection.
* If there is already a cookie with the same name in the collection, it will be removed first. * If there is already a cookie with the same name in the collection, it will be removed first.
* @param Cookie $cookie the cookie to be added * @param Cookie $cookie the cookie to be added
* @throws InvalidCallException if the cookie collection is read only
*/ */
public function add($cookie) public function add($cookie)
{ {
if (isset($this->_cookies[$cookie->name])) { if ($this->readOnly) {
$c = $this->_cookies[$cookie->name]; throw new InvalidCallException('The cookie collection is read only.');
setcookie($c->name, '', 0, $c->path, $c->domain, $c->secure, $c->httponly);
} }
$value = $cookie->value;
if ($this->enableValidation) {
if ($this->validationKey === null) {
$key = SecurityHelper::getSecretKey(__CLASS__ . '/' . Yii::$app->id);
} else {
$key = $this->validationKey;
}
$value = SecurityHelper::hashData(serialize($value), $key);
}
setcookie($cookie->name, $value, $cookie->expire, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httponly);
$this->_cookies[$cookie->name] = $cookie; $this->_cookies[$cookie->name] = $cookie;
} }
/** /**
* Removes a cookie from the collection. * Removes a cookie.
* If `$removeFromBrowser` is true, the cookie will be removed from the browser.
* In this case, a cookie with outdated expiry will be added to the collection.
* @param Cookie|string $cookie the cookie object or the name of the cookie to be removed. * @param Cookie|string $cookie the cookie object or the name of the cookie to be removed.
* @param boolean $removeFromBrowser whether to remove the cookie from browser
* @throws InvalidCallException if the cookie collection is read only
*/ */
public function remove($cookie) public function remove($cookie, $removeFromBrowser = true)
{ {
if (is_string($cookie) && isset($this->_cookies[$cookie])) { if ($this->readOnly) {
$cookie = $this->_cookies[$cookie]; throw new InvalidCallException('The cookie collection is read only.');
} }
if ($cookie instanceof Cookie) { if ($cookie instanceof Cookie) {
setcookie($cookie->name, '', 0, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httponly); $cookie->expire = 1;
$cookie->value = '';
} else {
$cookie = new Cookie(array(
'name' => $cookie,
'expire' => 1,
));
}
if ($removeFromBrowser) {
$this->_cookies[$cookie->name] = $cookie;
} else {
unset($this->_cookies[$cookie->name]); unset($this->_cookies[$cookie->name]);
} }
} }
/** /**
* Removes all cookies. * Removes all cookies.
* @throws InvalidCallException if the cookie collection is read only
*/ */
public function removeAll() public function removeAll()
{ {
foreach ($this->_cookies as $cookie) { if ($this->readOnly) {
setcookie($cookie->name, '', 0, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httponly); throw new InvalidCallException('The cookie collection is read only.');
} }
$this->_cookies = array(); $this->_cookies = array();
} }
...@@ -222,36 +223,4 @@ class CookieCollection extends \yii\base\Object implements \IteratorAggregate, \ ...@@ -222,36 +223,4 @@ class CookieCollection extends \yii\base\Object implements \IteratorAggregate, \
{ {
$this->remove($name); $this->remove($name);
} }
/**
* Returns the current cookies in terms of [[Cookie]] objects.
* @return Cookie[] list of current cookies
*/
protected function loadCookies()
{
$cookies = array();
if ($this->enableValidation) {
if ($this->validationKey === null) {
$key = SecurityHelper::getSecretKey(__CLASS__ . '/' . Yii::$app->id);
} else {
$key = $this->validationKey;
}
foreach ($_COOKIE as $name => $value) {
if (is_string($value) && ($value = SecurityHelper::validateData($value, $key)) !== false) {
$cookies[$name] = new Cookie(array(
'name' => $name,
'value' => @unserialize($value),
));
}
}
} else {
foreach ($_COOKIE as $name => $value) {
$cookies[$name] = new Cookie(array(
'name' => $name,
'value' => $value,
));
}
}
return $cookies;
}
} }
...@@ -79,11 +79,13 @@ class HeaderCollection extends Object implements \IteratorAggregate, \ArrayAcces ...@@ -79,11 +79,13 @@ class HeaderCollection extends Object implements \IteratorAggregate, \ArrayAcces
* If there is already a header with the same name, it will be replaced. * If there is already a header with the same name, it will be replaced.
* @param string $name the name of the header * @param string $name the name of the header
* @param string $value the value of the header * @param string $value the value of the header
* @return HeaderCollection the collection object itself
*/ */
public function set($name, $value) public function set($name, $value = '')
{ {
$name = strtolower($name); $name = strtolower($name);
$this->_headers[$name] = (array)$value; $this->_headers[$name] = (array)$value;
return $this;
} }
/** /**
...@@ -92,11 +94,29 @@ class HeaderCollection extends Object implements \IteratorAggregate, \ArrayAcces ...@@ -92,11 +94,29 @@ class HeaderCollection extends Object implements \IteratorAggregate, \ArrayAcces
* be appended to it instead of replacing it. * be appended to it instead of replacing it.
* @param string $name the name of the header * @param string $name the name of the header
* @param string $value the value of the header * @param string $value the value of the header
* @return HeaderCollection the collection object itself
*/ */
public function add($name, $value) public function add($name, $value)
{ {
$name = strtolower($name); $name = strtolower($name);
$this->_headers[$name][] = $value; $this->_headers[$name][] = $value;
return $this;
}
/**
* Adds a new header only if it does not exist yet.
* If there is already a header with the same name, the new one will be ignored.
* @param string $name the name of the header
* @param string $value the value of the header
* @return HeaderCollection the collection object itself
*/
public function addDefault($name, $value)
{
$name = strtolower($name);
if (empty($this->_headers[$name])) {
$this->_headers[$name][] = $value;
}
return $this;
} }
/** /**
......
...@@ -50,7 +50,7 @@ class HttpCache extends ActionFilter ...@@ -50,7 +50,7 @@ class HttpCache extends ActionFilter
/** /**
* @var string HTTP cache control header. If null, the header will not be sent. * @var string HTTP cache control header. If null, the header will not be sent.
*/ */
public $cacheControlHeader = 'Cache-Control: max-age=3600, public'; public $cacheControlHeader = 'max-age=3600, public';
/** /**
* This method is invoked right before an action is to be executed (after all possible filters.) * This method is invoked right before an action is to be executed (after all possible filters.)
...@@ -60,7 +60,7 @@ class HttpCache extends ActionFilter ...@@ -60,7 +60,7 @@ class HttpCache extends ActionFilter
*/ */
public function beforeAction($action) public function beforeAction($action)
{ {
$verb = Yii::$app->request->getMethod(); $verb = Yii::$app->getRequest()->getMethod();
if ($verb !== 'GET' && $verb !== 'HEAD' || $this->lastModified === null && $this->etagSeed === null) { if ($verb !== 'GET' && $verb !== 'HEAD' || $this->lastModified === null && $this->etagSeed === null) {
return true; return true;
} }
...@@ -75,17 +75,18 @@ class HttpCache extends ActionFilter ...@@ -75,17 +75,18 @@ class HttpCache extends ActionFilter
} }
$this->sendCacheControlHeader(); $this->sendCacheControlHeader();
$response = Yii::$app->getResponse();
if ($etag !== null) { if ($etag !== null) {
header("ETag: $etag"); $response->getHeaders()->set('Etag', $etag);
} }
if ($this->validateCache($lastModified, $etag)) { if ($this->validateCache($lastModified, $etag)) {
header('HTTP/1.1 304 Not Modified'); $response->setStatusCode(304);
return false; return false;
} }
if ($lastModified !== null) { if ($lastModified !== null) {
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $lastModified) . ' GMT'); $response->getHeaders()->set('Last-Modified', gmdate('D, d M Y H:i:s', $lastModified) . ' GMT');
} }
return true; return true;
} }
...@@ -113,9 +114,10 @@ class HttpCache extends ActionFilter ...@@ -113,9 +114,10 @@ class HttpCache extends ActionFilter
protected function sendCacheControlHeader() protected function sendCacheControlHeader()
{ {
session_cache_limiter('public'); session_cache_limiter('public');
header('Pragma:', true); $headers = Yii::$app->getResponse()->getHeaders();
$headers->set('Pragma');
if ($this->cacheControlHeader !== null) { if ($this->cacheControlHeader !== null) {
header($this->cacheControlHeader, true); $headers->set('Cache-Control', $this->cacheControlHeader);
} }
} }
......
...@@ -5,8 +5,10 @@ ...@@ -5,8 +5,10 @@
* @license http://www.yiiframework.com/license/ * @license http://www.yiiframework.com/license/
*/ */
namespace yii\base; namespace yii\web;
use yii\base\UserException;
use yii\web\Response;
/** /**
* HttpException represents an exception caused by an improper request of the end-user. * HttpException represents an exception caused by an improper request of the end-user.
...@@ -43,8 +45,8 @@ class HttpException extends UserException ...@@ -43,8 +45,8 @@ class HttpException extends UserException
*/ */
public function getName() public function getName()
{ {
if (isset(\yii\web\Response::$statusTexts[$this->statusCode])) { if (isset(Response::$httpStatuses[$this->statusCode])) {
return \yii\web\Response::$statusTexts[$this->statusCode]; return Response::$httpStatuses[$this->statusCode];
} else { } else {
return 'Error'; return 'Error';
} }
......
...@@ -8,8 +8,9 @@ ...@@ -8,8 +8,9 @@
namespace yii\web; namespace yii\web;
use Yii; use Yii;
use yii\base\HttpException; use yii\web\HttpException;
use yii\base\InvalidConfigException; use yii\base\InvalidConfigException;
use yii\helpers\SecurityHelper;
/** /**
* @author Qiang Xue <qiang.xue@gmail.com> * @author Qiang Xue <qiang.xue@gmail.com>
...@@ -37,16 +38,12 @@ class Request extends \yii\base\Request ...@@ -37,16 +38,12 @@ class Request extends \yii\base\Request
* @var array the configuration of the CSRF cookie. This property is used only when [[enableCsrfValidation]] is true. * @var array the configuration of the CSRF cookie. This property is used only when [[enableCsrfValidation]] is true.
* @see Cookie * @see Cookie
*/ */
public $csrfCookie = array('httponly' => true); public $csrfCookie = array('httpOnly' => true);
/** /**
* @var boolean whether cookies should be validated to ensure they are not tampered. Defaults to true. * @var boolean whether cookies should be validated to ensure they are not tampered. Defaults to true.
*/ */
public $enableCookieValidation = true; public $enableCookieValidation = true;
/** /**
* @var string the secret key used for cookie validation. If not set, a random key will be generated and used.
*/
public $cookieValidationKey;
/**
* @var string|boolean the name of the POST parameter that is used to indicate if a request is a PUT or DELETE * @var string|boolean the name of the POST parameter that is used to indicate if a request is a PUT or DELETE
* request tunneled through POST. Default to '_method'. * request tunneled through POST. Default to '_method'.
* @see getMethod * @see getMethod
...@@ -717,14 +714,64 @@ class Request extends \yii\base\Request ...@@ -717,14 +714,64 @@ class Request extends \yii\base\Request
public function getCookies() public function getCookies()
{ {
if ($this->_cookies === null) { if ($this->_cookies === null) {
$this->_cookies = new CookieCollection(array( $this->_cookies = new CookieCollection($this->loadCookies(), array(
'enableValidation' => $this->enableCookieValidation, 'readOnly' => true,
'validationKey' => $this->cookieValidationKey,
)); ));
} }
return $this->_cookies; return $this->_cookies;
} }
/**
* Converts `$_COOKIE` into an array of [[Cookie]].
* @return array the cookies obtained from request
*/
protected function loadCookies()
{
$cookies = array();
if ($this->enableCookieValidation) {
$key = $this->getCookieValidationKey();
foreach ($_COOKIE as $name => $value) {
if (is_string($value) && ($value = SecurityHelper::validateData($value, $key)) !== false) {
$cookies[$name] = new Cookie(array(
'name' => $name,
'value' => @unserialize($value),
));
}
}
} else {
foreach ($_COOKIE as $name => $value) {
$cookies[$name] = new Cookie(array(
'name' => $name,
'value' => $value,
));
}
}
return $cookies;
}
private $_cookieValidationKey;
/**
* @return string the secret key used for cookie validation. If it was set previously,
* a random key will be generated and used.
*/
public function getCookieValidationKey()
{
if ($this->_cookieValidationKey === null) {
$this->_cookieValidationKey = SecurityHelper::getSecretKey(__CLASS__ . '/' . Yii::$app->id);
}
return $this->_cookieValidationKey;
}
/**
* Sets the secret key used for cookie validation.
* @param string $value the secret key used for cookie validation.
*/
public function setCookieValidationKey($value)
{
$this->_cookieValidationKey = $value;
}
private $_csrfToken; private $_csrfToken;
/** /**
......
...@@ -8,11 +8,12 @@ ...@@ -8,11 +8,12 @@
namespace yii\web; namespace yii\web;
use Yii; use Yii;
use yii\base\HttpException; use yii\web\HttpException;
use yii\base\InvalidParamException; use yii\base\InvalidParamException;
use yii\helpers\FileHelper; use yii\helpers\FileHelper;
use yii\helpers\Html; use yii\helpers\Html;
use yii\helpers\Json; use yii\helpers\Json;
use yii\helpers\SecurityHelper;
use yii\helpers\StringHelper; use yii\helpers\StringHelper;
/** /**
...@@ -45,11 +46,10 @@ class Response extends \yii\base\Response ...@@ -45,11 +46,10 @@ class Response extends \yii\base\Response
* @var string the version of the HTTP protocol to use * @var string the version of the HTTP protocol to use
*/ */
public $version = '1.0'; public $version = '1.0';
/** /**
* @var array list of HTTP status codes and the corresponding texts * @var array list of HTTP status codes and the corresponding texts
*/ */
public static $statusTexts = array( public static $httpStatuses = array(
100 => 'Continue', 100 => 'Continue',
101 => 'Switching Protocols', 101 => 'Switching Protocols',
102 => 'Processing', 102 => 'Processing',
...@@ -93,7 +93,7 @@ class Response extends \yii\base\Response ...@@ -93,7 +93,7 @@ class Response extends \yii\base\Response
415 => 'Unsupported Media Type', 415 => 'Unsupported Media Type',
416 => 'Requested range unsatisfiable', 416 => 'Requested range unsatisfiable',
417 => 'Expectation failed', 417 => 'Expectation failed',
418 => 'Im a teapot', 418 => 'I\'m a teapot',
422 => 'Unprocessable entity', 422 => 'Unprocessable entity',
423 => 'Locked', 423 => 'Locked',
424 => 'Method failure', 424 => 'Method failure',
...@@ -117,7 +117,10 @@ class Response extends \yii\base\Response ...@@ -117,7 +117,10 @@ class Response extends \yii\base\Response
511 => 'Network Authentication Required', 511 => 'Network Authentication Required',
); );
private $_statusCode = 200; /**
* @var integer the HTTP status code to send with the response.
*/
private $_statusCode;
/** /**
* @var HeaderCollection * @var HeaderCollection
*/ */
...@@ -131,18 +134,38 @@ class Response extends \yii\base\Response ...@@ -131,18 +134,38 @@ class Response extends \yii\base\Response
} }
} }
public function begin()
{
parent::begin();
$this->beginBuffer();
}
public function end()
{
$this->content .= $this->endBuffer();
$this->send();
parent::end();
}
/**
* @return integer the HTTP status code to send with the response.
*/
public function getStatusCode() public function getStatusCode()
{ {
return $this->_statusCode; return $this->_statusCode;
} }
public function setStatusCode($value) public function setStatusCode($value, $text = null)
{ {
$this->_statusCode = (int)$value; $this->_statusCode = (int)$value;
if ($this->isInvalid()) { if ($this->getIsInvalid()) {
throw new InvalidParamException("The HTTP status code is invalid: $value"); throw new InvalidParamException("The HTTP status code is invalid: $value");
} }
$this->statusText = isset(self::$statusTexts[$this->_statusCode]) ? self::$statusTexts[$this->_statusCode] : ''; if ($text === null) {
$this->statusText = isset(self::$httpStatuses[$this->_statusCode]) ? self::$httpStatuses[$this->_statusCode] : '';
} else {
$this->statusText = $text;
}
} }
/** /**
...@@ -160,15 +183,17 @@ class Response extends \yii\base\Response ...@@ -160,15 +183,17 @@ class Response extends \yii\base\Response
public function renderJson($data) public function renderJson($data)
{ {
$this->getHeaders()->set('content-type', 'application/json'); $this->getHeaders()->set('Content-Type', 'application/json');
$this->content = Json::encode($data); $this->content = Json::encode($data);
$this->send();
} }
public function renderJsonp($data, $callbackName) public function renderJsonp($data, $callbackName)
{ {
$this->getHeaders()->set('content-type', 'text/javascript'); $this->getHeaders()->set('Content-Type', 'text/javascript');
$data = Json::encode($data); $data = Json::encode($data);
$this->content = "$callbackName($data);"; $this->content = "$callbackName($data);";
$this->send();
} }
/** /**
...@@ -179,6 +204,25 @@ class Response extends \yii\base\Response ...@@ -179,6 +204,25 @@ class Response extends \yii\base\Response
{ {
$this->sendHeaders(); $this->sendHeaders();
$this->sendContent(); $this->sendContent();
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
} else {
for ($level = ob_get_level(); $level > 0; --$level) {
if (!@ob_end_flush()) {
ob_clean();
}
}
flush();
}
}
public function reset()
{
$this->_headers = null;
$this->_statusCode = null;
$this->statusText = null;
$this->content = null;
} }
/** /**
...@@ -186,13 +230,45 @@ class Response extends \yii\base\Response ...@@ -186,13 +230,45 @@ class Response extends \yii\base\Response
*/ */
protected function sendHeaders() protected function sendHeaders()
{ {
header("HTTP/{$this->version} " . $this->getStatusCode() . " {$this->statusText}"); if (headers_sent()) {
foreach ($this->_headers as $name => $values) { return;
}
$statusCode = $this->getStatusCode();
if ($statusCode !== null) {
header("HTTP/{$this->version} $statusCode {$this->statusText}");
}
if ($this->_headers) {
$headers = $this->getHeaders();
foreach ($headers as $name => $values) {
foreach ($values as $value) { foreach ($values as $value) {
header("$name: $value"); header("$name: $value", false);
}
}
$headers->removeAll();
}
$this->sendCookies();
}
/**
* Sends the cookies to the client.
*/
protected function sendCookies()
{
if ($this->_cookies === null) {
return;
}
$request = Yii::$app->getRequest();
if ($request->enableCookieValidation) {
$validationKey = $request->getCookieValidationKey();
} }
foreach ($this->getCookies() as $cookie) {
$value = $cookie->value;
if ($cookie->expire != 1 && isset($validationKey)) {
$value = SecurityHelper::hashData(serialize($value), $validationKey);
} }
$this->_headers->removeAll(); setcookie($cookie->name, $value, $cookie->expire, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httpOnly);
}
$this->getCookies()->removeAll();
} }
/** /**
...@@ -205,89 +281,132 @@ class Response extends \yii\base\Response ...@@ -205,89 +281,132 @@ class Response extends \yii\base\Response
} }
/** /**
* Sends a file to user. * Sends a file to the browser.
* @param string $fileName file name * @param string $filePath the path of the file to be sent.
* @param string $content content to be set. * @param string $attachmentName the file name shown to the user. If null, it will be determined from `$filePath`.
* @param string $mimeType mime type of the content. If null, it will be guessed automatically based on the given file name. * @param string $mimeType the MIME type of the content. If null, it will be guessed based on `$filePath`
* @param boolean $terminate whether to terminate the current application after calling this method
* @throws \yii\base\HttpException when range request is not satisfiable.
*/ */
public function sendFile($fileName, $content, $mimeType = null, $terminate = true) public function sendFile($filePath, $attachmentName = null, $mimeType = null)
{ {
if ($mimeType === null && (($mimeType = FileHelper::getMimeTypeByExtension($fileName)) === null)) { if ($mimeType === null && ($mimeType = FileHelper::getMimeTypeByExtension($filePath)) === null) {
$mimeType = 'application/octet-stream'; $mimeType = 'application/octet-stream';
} }
if ($attachmentName === null) {
$attachmentName = basename($filePath);
}
$handle = fopen($filePath, 'rb');
$this->sendStreamAsFile($handle, $attachmentName, $mimeType);
}
$fileSize = StringHelper::strlen($content); /**
$contentStart = 0; * Sends the specified content as a file to the browser.
$contentEnd = $fileSize - 1; * @param string $content the content to be sent. The existing [[content]] will be discarded.
* @param string $attachmentName the file name shown to the user.
// tell the client that we accept range requests * @param string $mimeType the MIME type of the content.
header('Accept-Ranges: bytes'); */
public function sendContentAsFile($content, $attachmentName, $mimeType = 'application/octet-stream')
{
$this->getHeaders()
->addDefault('Pragma', 'public')
->addDefault('Accept-Ranges', 'bytes')
->addDefault('Expires', '0')
->addDefault('Content-Type', $mimeType)
->addDefault('Cache-Control', 'must-revalidate, post-check=0, pre-check=0')
->addDefault('Content-Transfer-Encoding', 'binary')
->addDefault('Content-Length', StringHelper::strlen($content))
->addDefault('Content-Disposition', "attachment; filename=\"$attachmentName\"");
if (isset($_SERVER['HTTP_RANGE'])) { $this->content = $content;
// client sent us a multibyte range, can not hold this one for now $this->send();
if (strpos($_SERVER['HTTP_RANGE'], ',') !== false) {
header("Content-Range: bytes $contentStart-$contentEnd/$fileSize");
throw new HttpException(416, 'Requested Range Not Satisfiable');
} }
$range = str_replace('bytes=', '', $_SERVER['HTTP_RANGE']); /**
* Sends the specified stream as a file to the browser.
* @param resource $handle the handle of the stream to be sent.
* @param string $attachmentName the file name shown to the user.
* @param string $mimeType the MIME type of the stream content.
* @throws HttpException if the requested range cannot be satisfied.
*/
public function sendStreamAsFile($handle, $attachmentName, $mimeType = 'application/octet-stream')
{
$headers = $this->getHeaders();
fseek($handle, 0, SEEK_END);
$fileSize = ftell($handle);
$range = $this->getHttpRange($fileSize);
if ($range === false) {
$headers->set('Content-Range', "bytes */$fileSize");
throw new HttpException(416, Yii::t('yii', 'Requested range not satisfiable'));
}
// range requests starts from "-", so it means that data must be dumped the end point. list($begin, $end) = $range;
if ($range[0] === '-') { if ($begin !=0 || $end != $fileSize - 1) {
$contentStart = $fileSize - substr($range, 1); $this->setStatusCode(206);
$headers->set('Content-Range', "bytes $begin-$end/$fileSize");
} else { } else {
$range = explode('-', $range); $this->setStatusCode(200);
$contentStart = $range[0];
// check if the last-byte-pos presents in header
if ((isset($range[1]) && is_numeric($range[1]))) {
$contentEnd = $range[1];
} }
if (isset($options['mimeType'])) {
$headers->set('Content-Type', $options['mimeType']);
} }
/* Check the range and make sure it's treated according to the specs. $length = $end - $begin + 1;
* http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
*/ $headers->addDefault('Pragma', 'public')
// End bytes can not be larger than $end. ->addDefault('Accept-Ranges', 'bytes')
$contentEnd = ($contentEnd > $fileSize) ? $fileSize - 1 : $contentEnd; ->addDefault('Expires', '0')
->addDefault('Content-Type', $mimeType)
->addDefault('Cache-Control', 'must-revalidate, post-check=0, pre-check=0')
->addDefault('Content-Transfer-Encoding', 'binary')
->addDefault('Content-Length', $length)
->addDefault('Content-Disposition', "attachment; filename=\"$attachmentName\"");
// Validate the requested range and return an error if it's not correct. $this->send();
$wrongContentStart = ($contentStart > $contentEnd || $contentStart > $fileSize - 1 || $contentStart < 0);
if ($wrongContentStart) { fseek($handle, $begin);
header("Content-Range: bytes $contentStart-$contentEnd/$fileSize"); set_time_limit(0); // Reset time limit for big files
throw new HttpException(416, 'Requested Range Not Satisfiable'); $chunkSize = 8 * 1024 * 1024; // 8MB per chunk
while (!feof($handle) && ($pos = ftell($handle)) <= $end) {
if ($pos + $chunkSize > $end) {
$chunkSize = $end - $pos + 1;
}
echo fread($handle, $chunkSize);
flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit.
}
fclose($handle);
} }
header('HTTP/1.1 206 Partial Content'); /**
header("Content-Range: bytes $contentStart-$contentEnd/$fileSize"); * Determines the HTTP range given in the request.
* @param integer $fileSize the size of the file that will be used to validate the requested HTTP range.
* @return array|boolean the range (begin, end), or false if the range request is invalid.
*/
protected function getHttpRange($fileSize)
{
if (!isset($_SERVER['HTTP_RANGE']) || $_SERVER['HTTP_RANGE'] === '-') {
return array(0, $fileSize - 1);
}
if (!preg_match('/^bytes=(\d*)-(\d*)$/', $_SERVER['HTTP_RANGE'], $matches)) {
return false;
}
if ($matches[1] === '') {
$start = $fileSize - $matches[2];
$end = $fileSize - 1;
} elseif ($matches[2] !== '') {
$start = $matches[1];
$end = $matches[2];
if ($end >= $fileSize) {
$end = $fileSize - 1;
}
} else { } else {
header('HTTP/1.1 200 OK'); $start = $matches[1];
$end = $fileSize - 1;
} }
if ($start < 0 || $start > $end) {
$length = $contentEnd - $contentStart + 1; // Calculate new content length return false;
header('Pragma: public');
header('Expires: 0');
header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
header('Content-Type: ' . $mimeType);
header('Content-Length: ' . $length);
header('Content-Disposition: attachment; filename="' . $fileName . '"');
header('Content-Transfer-Encoding: binary');
$content = StringHelper::substr($content, $contentStart, $length);
if ($terminate) {
// clean up the application first because the file downloading could take long time
// which may cause timeout of some resources (such as DB connection)
ob_start();
Yii::$app->end(0, false);
ob_end_clean();
echo $content;
exit(0);
} else { } else {
echo $content; return array($start, $end);
} }
} }
...@@ -305,86 +424,58 @@ class Response extends \yii\base\Response ...@@ -305,86 +424,58 @@ class Response extends \yii\base\Response
* specified by that header using web server internals including all optimizations like caching-headers. * specified by that header using web server internals including all optimizations like caching-headers.
* *
* As this header directive is non-standard different directives exists for different web servers applications: * As this header directive is non-standard different directives exists for different web servers applications:
* <ul> *
* <li>Apache: {@link http://tn123.org/mod_xsendfile X-Sendfile}</li> * - Apache: [X-Sendfile](http://tn123.org/mod_xsendfile)
* <li>Lighttpd v1.4: {@link http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file X-LIGHTTPD-send-file}</li> * - Lighttpd v1.4: [X-LIGHTTPD-send-file](http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file)
* <li>Lighttpd v1.5: {@link http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file X-Sendfile}</li> * - Lighttpd v1.5: [X-Sendfile](http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file)
* <li>Nginx: {@link http://wiki.nginx.org/XSendfile X-Accel-Redirect}</li> * - Nginx: [X-Accel-Redirect](http://wiki.nginx.org/XSendfile)
* <li>Cherokee: {@link http://www.cherokee-project.com/doc/other_goodies.html#x-sendfile X-Sendfile and X-Accel-Redirect}</li> * - Cherokee: [X-Sendfile and X-Accel-Redirect](http://www.cherokee-project.com/doc/other_goodies.html#x-sendfile)
* </ul> *
* So for this method to work the X-SENDFILE option/module should be enabled by the web server and * So for this method to work the X-SENDFILE option/module should be enabled by the web server and
* a proper xHeader should be sent. * a proper xHeader should be sent.
* *
* <b>Note:</b> * **Note**
* This option allows to download files that are not under web folders, and even files that are otherwise protected (deny from all) like .htaccess *
* This option allows to download files that are not under web folders, and even files that are otherwise protected
* (deny from all) like `.htaccess`.
*
* **Side effects**
* *
* <b>Side effects</b>:
* If this option is disabled by the web server, when this method is called a download configuration dialog * If this option is disabled by the web server, when this method is called a download configuration dialog
* will open but the downloaded file will have 0 bytes. * will open but the downloaded file will have 0 bytes.
* *
* <b>Known issues</b>: * **Known issues**
*
* There is a Bug with Internet Explorer 6, 7 and 8 when X-SENDFILE is used over an SSL connection, it will show * There is a Bug with Internet Explorer 6, 7 and 8 when X-SENDFILE is used over an SSL connection, it will show
* an error message like this: "Internet Explorer was not able to open this Internet site. The requested site is either unavailable or cannot be found.". * an error message like this: "Internet Explorer was not able to open this Internet site. The requested site
* You can work around this problem by removing the <code>Pragma</code>-header. * is either unavailable or cannot be found.". You can work around this problem by removing the `Pragma`-header.
*
* **Example**
*
* ~~~
* Yii::app()->request->xSendFile('/home/user/Pictures/picture1.jpg');
* ~~~
* *
* <b>Example</b>:
* <pre>
* <?php
* Yii::app()->request->xSendFile('/home/user/Pictures/picture1.jpg', array(
* 'saveName' => 'image1.jpg',
* 'mimeType' => 'image/jpeg',
* 'terminate' => false,
* ));
* ?>
* </pre>
* @param string $filePath file name with full path * @param string $filePath file name with full path
* @param array $options additional options: * @param string $mimeType the MIME type of the file. If null, it will be determined based on `$filePath`.
* <ul> * @param string $attachmentName file name shown to the user. If null, it will be determined from `$filePath`.
* <li>saveName: file name shown to the user, if not set real file name will be used</li> * @param string $xHeader the name of the x-sendfile header.
* <li>mimeType: mime type of the file, if not set it will be guessed automatically based on the file name, if set to null no content-type header will be sent.</li> */
* <li>xHeader: appropriate x-sendfile header, defaults to "X-Sendfile"</li> public function xSendFile($filePath, $attachmentName = null, $mimeType = null, $xHeader = 'X-Sendfile')
* <li>terminate: whether to terminate the current application after calling this method, defaults to true</li>
* <li>forceDownload: specifies whether the file will be downloaded or shown inline, defaults to true</li>
* <li>addHeaders: an array of additional http headers in header-value pairs</li>
* </ul>
* @todo
*/
public function xSendFile($filePath, $options = array())
{ {
if (!isset($options['forceDownload']) || $options['forceDownload']) { if ($mimeType === null && ($mimeType = FileHelper::getMimeTypeByExtension($filePath)) === null) {
$disposition = 'attachment'; $mimeType = 'application/octet-stream';
} else {
$disposition = 'inline';
}
if (!isset($options['saveName'])) {
$options['saveName'] = basename($filePath);
}
if (!isset($options['mimeType'])) {
if (($options['mimeType'] = FileHelper::getMimeTypeByExtension($filePath)) === null) {
$options['mimeType'] = 'text/plain';
} }
if ($attachmentName === null) {
$attachmentName = basename($filePath);
} }
if (!isset($options['xHeader'])) { $this->getHeaders()
$options['xHeader'] = 'X-Sendfile'; ->addDefault($xHeader, $filePath)
} ->addDefault('Content-Type', $mimeType)
->addDefault('Content-Disposition', "attachment; filename=\"$attachmentName\"");
if ($options['mimeType'] !== null) { $this->send();
header('Content-type: ' . $options['mimeType']);
}
header('Content-Disposition: ' . $disposition . '; filename="' . $options['saveName'] . '"');
if (isset($options['addHeaders'])) {
foreach ($options['addHeaders'] as $header => $value) {
header($header . ': ' . $value);
}
}
header(trim($options['xHeader']) . ': ' . $filePath);
if (!isset($options['terminate']) || $options['terminate']) {
Yii::$app->end();
}
} }
/** /**
...@@ -422,7 +513,8 @@ class Response extends \yii\base\Response ...@@ -422,7 +513,8 @@ class Response extends \yii\base\Response
if (Yii::$app->getRequest()->getIsAjax()) { if (Yii::$app->getRequest()->getIsAjax()) {
$statusCode = $this->ajaxRedirectCode; $statusCode = $this->ajaxRedirectCode;
} }
header('Location: ' . $url, true, $statusCode); $this->getHeaders()->set('Location', $url);
$this->setStatusCode($statusCode);
if ($terminate) { if ($terminate) {
Yii::$app->end(); Yii::$app->end();
} }
...@@ -441,6 +533,8 @@ class Response extends \yii\base\Response ...@@ -441,6 +533,8 @@ class Response extends \yii\base\Response
$this->redirect(Yii::$app->getRequest()->getUrl() . $anchor, $terminate); $this->redirect(Yii::$app->getRequest()->getUrl() . $anchor, $terminate);
} }
private $_cookies;
/** /**
* Returns the cookie collection. * Returns the cookie collection.
* Through the returned cookie collection, you add or remove cookies as follows, * Through the returned cookie collection, you add or remove cookies as follows,
...@@ -462,13 +556,16 @@ class Response extends \yii\base\Response ...@@ -462,13 +556,16 @@ class Response extends \yii\base\Response
*/ */
public function getCookies() public function getCookies()
{ {
return Yii::$app->getRequest()->getCookies(); if ($this->_cookies === null) {
$this->_cookies = new CookieCollection;
}
return $this->_cookies;
} }
/** /**
* @return boolean whether this response has a valid [[statusCode]]. * @return boolean whether this response has a valid [[statusCode]].
*/ */
public function isInvalid() public function getIsInvalid()
{ {
return $this->getStatusCode() < 100 || $this->getStatusCode() >= 600; return $this->getStatusCode() < 100 || $this->getStatusCode() >= 600;
} }
...@@ -476,15 +573,15 @@ class Response extends \yii\base\Response ...@@ -476,15 +573,15 @@ class Response extends \yii\base\Response
/** /**
* @return boolean whether this response is informational * @return boolean whether this response is informational
*/ */
public function isInformational() public function getIsInformational()
{ {
return $this->getStatusCode() >= 100 && $this->getStatusCode() < 200; return $this->getStatusCode() >= 100 && $this->getStatusCode() < 200;
} }
/** /**
* @return boolean whether this response is successfully * @return boolean whether this response is successful
*/ */
public function isSuccessful() public function getIsSuccessful()
{ {
return $this->getStatusCode() >= 200 && $this->getStatusCode() < 300; return $this->getStatusCode() >= 200 && $this->getStatusCode() < 300;
} }
...@@ -492,7 +589,7 @@ class Response extends \yii\base\Response ...@@ -492,7 +589,7 @@ class Response extends \yii\base\Response
/** /**
* @return boolean whether this response is a redirection * @return boolean whether this response is a redirection
*/ */
public function isRedirection() public function getIsRedirection()
{ {
return $this->getStatusCode() >= 300 && $this->getStatusCode() < 400; return $this->getStatusCode() >= 300 && $this->getStatusCode() < 400;
} }
...@@ -500,7 +597,7 @@ class Response extends \yii\base\Response ...@@ -500,7 +597,7 @@ class Response extends \yii\base\Response
/** /**
* @return boolean whether this response indicates a client error * @return boolean whether this response indicates a client error
*/ */
public function isClientError() public function getIsClientError()
{ {
return $this->getStatusCode() >= 400 && $this->getStatusCode() < 500; return $this->getStatusCode() >= 400 && $this->getStatusCode() < 500;
} }
...@@ -508,7 +605,7 @@ class Response extends \yii\base\Response ...@@ -508,7 +605,7 @@ class Response extends \yii\base\Response
/** /**
* @return boolean whether this response indicates a server error * @return boolean whether this response indicates a server error
*/ */
public function isServerError() public function getIsServerError()
{ {
return $this->getStatusCode() >= 500 && $this->getStatusCode() < 600; return $this->getStatusCode() >= 500 && $this->getStatusCode() < 600;
} }
...@@ -516,7 +613,7 @@ class Response extends \yii\base\Response ...@@ -516,7 +613,7 @@ class Response extends \yii\base\Response
/** /**
* @return boolean whether this response is OK * @return boolean whether this response is OK
*/ */
public function isOk() public function getIsOk()
{ {
return 200 === $this->getStatusCode(); return 200 === $this->getStatusCode();
} }
...@@ -524,7 +621,7 @@ class Response extends \yii\base\Response ...@@ -524,7 +621,7 @@ class Response extends \yii\base\Response
/** /**
* @return boolean whether this response indicates the current request is forbidden * @return boolean whether this response indicates the current request is forbidden
*/ */
public function isForbidden() public function getIsForbidden()
{ {
return 403 === $this->getStatusCode(); return 403 === $this->getStatusCode();
} }
...@@ -532,7 +629,7 @@ class Response extends \yii\base\Response ...@@ -532,7 +629,7 @@ class Response extends \yii\base\Response
/** /**
* @return boolean whether this response indicates the currently requested resource is not found * @return boolean whether this response indicates the currently requested resource is not found
*/ */
public function isNotFound() public function getIsNotFound()
{ {
return 404 === $this->getStatusCode(); return 404 === $this->getStatusCode();
} }
...@@ -540,7 +637,7 @@ class Response extends \yii\base\Response ...@@ -540,7 +637,7 @@ class Response extends \yii\base\Response
/** /**
* @return boolean whether this response is empty * @return boolean whether this response is empty
*/ */
public function isEmpty() public function getIsEmpty()
{ {
return in_array($this->getStatusCode(), array(201, 204, 304)); return in_array($this->getStatusCode(), array(201, 204, 304));
} }
......
...@@ -63,7 +63,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co ...@@ -63,7 +63,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co
* @var array parameter-value pairs to override default session cookie parameters * @var array parameter-value pairs to override default session cookie parameters
*/ */
public $cookieParams = array( public $cookieParams = array(
'httponly' => true 'httpOnly' => true
); );
/** /**
...@@ -241,26 +241,31 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co ...@@ -241,26 +241,31 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co
*/ */
public function getCookieParams() public function getCookieParams()
{ {
return session_get_cookie_params(); $params = session_get_cookie_params();
if (isset($params['httponly'])) {
$params['httpOnly'] = $params['httponly'];
unset($params['httponly']);
}
return $params;
} }
/** /**
* Sets the session cookie parameters. * Sets the session cookie parameters.
* The effect of this method only lasts for the duration of the script. * The effect of this method only lasts for the duration of the script.
* Call this method before the session starts. * Call this method before the session starts.
* @param array $value cookie parameters, valid keys include: lifetime, path, domain, secure and httponly. * @param array $value cookie parameters, valid keys include: `lifetime`, `path`, `domain`, `secure` and `httpOnly`.
* @throws InvalidParamException if the parameters are incomplete. * @throws InvalidParamException if the parameters are incomplete.
* @see http://us2.php.net/manual/en/function.session-set-cookie-params.php * @see http://us2.php.net/manual/en/function.session-set-cookie-params.php
*/ */
public function setCookieParams($value) public function setCookieParams($value)
{ {
$data = session_get_cookie_params(); $data = $this->getCookieParams();
extract($data); extract($data);
extract($value); extract($value);
if (isset($lifetime, $path, $domain, $secure, $httponly)) { if (isset($lifetime, $path, $domain, $secure, $httpOnly)) {
session_set_cookie_params($lifetime, $path, $domain, $secure, $httponly); session_set_cookie_params($lifetime, $path, $domain, $secure, $httpOnly);
} else { } else {
throw new InvalidParamException('Please make sure these parameters are provided: lifetime, path, domain, secure and httponly.'); throw new InvalidParamException('Please make sure these parameters are provided: lifetime, path, domain, secure and httpOnly.');
} }
} }
......
...@@ -9,7 +9,7 @@ namespace yii\web; ...@@ -9,7 +9,7 @@ namespace yii\web;
use Yii; use Yii;
use yii\base\Component; use yii\base\Component;
use yii\base\HttpException; use yii\web\HttpException;
use yii\base\InvalidConfigException; use yii\base\InvalidConfigException;
/** /**
...@@ -56,7 +56,7 @@ class User extends Component ...@@ -56,7 +56,7 @@ class User extends Component
* @var array the configuration of the identity cookie. This property is used only when [[enableAutoLogin]] is true. * @var array the configuration of the identity cookie. This property is used only when [[enableAutoLogin]] is true.
* @see Cookie * @see Cookie
*/ */
public $identityCookie = array('name' => '_identity', 'httponly' => true); public $identityCookie = array('name' => '_identity', 'httpOnly' => true);
/** /**
* @var integer the number of seconds in which the user will be logged out automatically if he * @var integer the number of seconds in which the user will be logged out automatically if he
* remains inactive. If this property is not set, the user will be logged out after * remains inactive. If this property is not set, the user will be logged out after
......
...@@ -10,7 +10,7 @@ namespace yii\web; ...@@ -10,7 +10,7 @@ namespace yii\web;
use Yii; use Yii;
use yii\base\ActionEvent; use yii\base\ActionEvent;
use yii\base\Behavior; use yii\base\Behavior;
use yii\base\HttpException; use yii\web\HttpException;
/** /**
* VerbFilter is an action filter that filters by HTTP request methods. * VerbFilter is an action filter that filters by HTTP request methods.
...@@ -70,7 +70,7 @@ class VerbFilter extends Behavior ...@@ -70,7 +70,7 @@ class VerbFilter extends Behavior
/** /**
* @param ActionEvent $event * @param ActionEvent $event
* @return boolean * @return boolean
* @throws \yii\base\HttpException when the request method is not allowed. * @throws HttpException when the request method is not allowed.
*/ */
public function beforeAction($event) public function beforeAction($event)
{ {
...@@ -81,7 +81,7 @@ class VerbFilter extends Behavior ...@@ -81,7 +81,7 @@ class VerbFilter extends Behavior
if (!in_array($verb, $allowed)) { if (!in_array($verb, $allowed)) {
$event->isValid = false; $event->isValid = false;
// http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.7 // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.7
header('Allow: ' . implode(', ', $allowed)); Yii::$app->getResponse()->getHeaders()->set('Allow', implode(', ', $allowed));
throw new HttpException(405, 'Method Not Allowed. This url can only handle the following request methods: ' . implode(', ', $allowed)); throw new HttpException(405, 'Method Not Allowed. This url can only handle the following request methods: ' . implode(', ', $allowed));
} }
} }
......
...@@ -9,6 +9,14 @@ DROP TABLE IF EXISTS tbl_order CASCADE; ...@@ -9,6 +9,14 @@ DROP TABLE IF EXISTS tbl_order CASCADE;
DROP TABLE IF EXISTS tbl_category CASCADE; DROP TABLE IF EXISTS tbl_category CASCADE;
DROP TABLE IF EXISTS tbl_customer CASCADE; DROP TABLE IF EXISTS tbl_customer CASCADE;
DROP TABLE IF EXISTS tbl_type CASCADE; DROP TABLE IF EXISTS tbl_type CASCADE;
DROP TABLE IF EXISTS tbl_constraints CASCADE;
CREATE TABLE `tbl_constraints`
(
`id` integer not null,
`field1` varchar(255)
);
CREATE TABLE `tbl_customer` ( CREATE TABLE `tbl_customer` (
`id` int(11) NOT NULL AUTO_INCREMENT, `id` int(11) NOT NULL AUTO_INCREMENT,
......
...@@ -10,6 +10,13 @@ DROP TABLE IF EXISTS tbl_order CASCADE; ...@@ -10,6 +10,13 @@ DROP TABLE IF EXISTS tbl_order CASCADE;
DROP TABLE IF EXISTS tbl_category CASCADE; DROP TABLE IF EXISTS tbl_category CASCADE;
DROP TABLE IF EXISTS tbl_customer CASCADE; DROP TABLE IF EXISTS tbl_customer CASCADE;
DROP TABLE IF EXISTS tbl_type CASCADE; DROP TABLE IF EXISTS tbl_type CASCADE;
DROP TABLE IF EXISTS tbl_constraints CASCADE;
CREATE TABLE tbl_constraints
(
id integer not null,
field1 varchar(255)
);
CREATE TABLE tbl_customer ( CREATE TABLE tbl_customer (
id serial not null primary key, id serial not null primary key,
......
12ёжик3456798áèabcdefghijklmnopqrstuvwxyz!"§$%&/(ёжик)=?
\ No newline at end of file
...@@ -7,23 +7,26 @@ use yii\db\Schema; ...@@ -7,23 +7,26 @@ use yii\db\Schema;
use yii\db\mysql\QueryBuilder as MysqlQueryBuilder; use yii\db\mysql\QueryBuilder as MysqlQueryBuilder;
use yii\db\sqlite\QueryBuilder as SqliteQueryBuilder; use yii\db\sqlite\QueryBuilder as SqliteQueryBuilder;
use yii\db\mssql\QueryBuilder as MssqlQueryBuilder; use yii\db\mssql\QueryBuilder as MssqlQueryBuilder;
use yii\db\pgsql\QueryBuilder as PgsqlQueryBuilder;
class QueryBuilderTest extends DatabaseTestCase class QueryBuilderTest extends DatabaseTestCase
{ {
/** /**
* @throws \Exception * @throws \Exception
* @return QueryBuilder * @return QueryBuilder
*/ */
protected function getQueryBuilder() protected function getQueryBuilder()
{ {
switch($this->driverName) switch ($this->driverName) {
{
case 'mysql': case 'mysql':
return new MysqlQueryBuilder($this->getConnection()); return new MysqlQueryBuilder($this->getConnection());
case 'sqlite': case 'sqlite':
return new SqliteQueryBuilder($this->getConnection()); return new SqliteQueryBuilder($this->getConnection());
case 'mssql': case 'mssql':
return new MssqlQueryBuilder($this->getConnection()); return new MssqlQueryBuilder($this->getConnection());
case 'pgsql':
return new PgsqlQueryBuilder($this->getConnection());
} }
throw new \Exception('Test is not implemented for ' . $this->driverName); throw new \Exception('Test is not implemented for ' . $this->driverName);
} }
...@@ -95,15 +98,31 @@ class QueryBuilderTest extends DatabaseTestCase ...@@ -95,15 +98,31 @@ class QueryBuilderTest extends DatabaseTestCase
); );
} }
/**
*
*/
public function testGetColumnType() public function testGetColumnType()
{ {
$qb = $this->getQueryBuilder(); $qb = $this->getQueryBuilder();
foreach($this->columnTypes() as $item) { foreach ($this->columnTypes() as $item) {
list ($column, $expected) = $item; list ($column, $expected) = $item;
$this->assertEquals($expected, $qb->getColumnType($column)); $this->assertEquals($expected, $qb->getColumnType($column));
} }
} }
public function testAddDropPrimayKey()
{
$tableName = 'tbl_constraints';
$pkeyName = $tableName . "_pkey";
// ADD
$qb = $this->getQueryBuilder();
$qb->db->createCommand()->addPrimaryKey($pkeyName, $tableName, array('id'))->execute();
$tableSchema = $qb->db->getSchema()->getTableSchema($tableName);
$this->assertEquals(1, count($tableSchema->primaryKey));
//DROP
$qb->db->createCommand()->dropPrimaryKey($pkeyName, $tableName)->execute();
$qb = $this->getQueryBuilder(); // resets the schema
$tableSchema = $qb->db->getSchema()->getTableSchema($tableName);
$this->assertEquals(0, count($tableSchema->primaryKey));
}
} }
<?php
namespace yiiunit\framework\db\pgsql;
use yii\base\NotSupportedException;
use yii\db\pgsql\Schema;
use yiiunit\framework\db\QueryBuilderTest;
class PostgreSQLQueryBuilderTest extends QueryBuilderTest
{
public $driverName = 'pgsql';
public function columnTypes()
{
return array(
array(Schema::TYPE_PK, 'serial not null primary key'),
array(Schema::TYPE_PK . '(8)', 'serial not null primary key'),
array(Schema::TYPE_PK . ' CHECK (value > 5)', 'serial not null primary key CHECK (value > 5)'),
array(Schema::TYPE_PK . '(8) CHECK (value > 5)', 'serial not null primary key CHECK (value > 5)'),
array(Schema::TYPE_STRING, 'varchar(255)'),
array(Schema::TYPE_STRING . '(32)', 'varchar(32)'),
array(Schema::TYPE_STRING . ' CHECK (value LIKE "test%")', 'varchar(255) CHECK (value LIKE "test%")'),
array(Schema::TYPE_STRING . '(32) CHECK (value LIKE "test%")', 'varchar(32) CHECK (value LIKE "test%")'),
array(Schema::TYPE_STRING . ' NOT NULL', 'varchar(255) NOT NULL'),
array(Schema::TYPE_TEXT, 'text'),
array(Schema::TYPE_TEXT . '(255)', 'text'),
array(Schema::TYPE_TEXT . ' CHECK (value LIKE "test%")', 'text CHECK (value LIKE "test%")'),
array(Schema::TYPE_TEXT . '(255) CHECK (value LIKE "test%")', 'text CHECK (value LIKE "test%")'),
array(Schema::TYPE_TEXT . ' NOT NULL', 'text NOT NULL'),
array(Schema::TYPE_TEXT . '(255) NOT NULL', 'text NOT NULL'),
array(Schema::TYPE_SMALLINT, 'smallint'),
array(Schema::TYPE_SMALLINT . '(8)', 'smallint'),
array(Schema::TYPE_INTEGER, 'integer'),
array(Schema::TYPE_INTEGER . '(8)', 'integer'),
array(Schema::TYPE_INTEGER . ' CHECK (value > 5)', 'integer CHECK (value > 5)'),
array(Schema::TYPE_INTEGER . '(8) CHECK (value > 5)', 'integer CHECK (value > 5)'),
array(Schema::TYPE_INTEGER . ' NOT NULL', 'integer NOT NULL'),
array(Schema::TYPE_BIGINT, 'bigint'),
array(Schema::TYPE_BIGINT . '(8)', 'bigint'),
array(Schema::TYPE_BIGINT . ' CHECK (value > 5)', 'bigint CHECK (value > 5)'),
array(Schema::TYPE_BIGINT . '(8) CHECK (value > 5)', 'bigint CHECK (value > 5)'),
array(Schema::TYPE_BIGINT . ' NOT NULL', 'bigint NOT NULL'),
array(Schema::TYPE_FLOAT, 'double precision'),
array(Schema::TYPE_FLOAT . ' CHECK (value > 5.6)', 'double precision CHECK (value > 5.6)'),
array(Schema::TYPE_FLOAT . '(16,5) CHECK (value > 5.6)', 'double precision CHECK (value > 5.6)'),
array(Schema::TYPE_FLOAT . ' NOT NULL', 'double precision NOT NULL'),
array(Schema::TYPE_DECIMAL, 'numeric(10,0)'),
array(Schema::TYPE_DECIMAL . '(12,4)', 'numeric(12,4)'),
array(Schema::TYPE_DECIMAL . ' CHECK (value > 5.6)', 'numeric(10,0) CHECK (value > 5.6)'),
array(Schema::TYPE_DECIMAL . '(12,4) CHECK (value > 5.6)', 'numeric(12,4) CHECK (value > 5.6)'),
array(Schema::TYPE_DECIMAL . ' NOT NULL', 'numeric(10,0) NOT NULL'),
array(Schema::TYPE_DATETIME, 'timestamp'),
array(Schema::TYPE_DATETIME . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "timestamp CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"),
array(Schema::TYPE_DATETIME . ' NOT NULL', 'timestamp NOT NULL'),
array(Schema::TYPE_TIMESTAMP, 'timestamp'),
array(Schema::TYPE_TIMESTAMP . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "timestamp CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"),
array(Schema::TYPE_TIMESTAMP . ' NOT NULL', 'timestamp NOT NULL'),
array(Schema::TYPE_TIME, 'time'),
array(Schema::TYPE_TIME . " CHECK(value BETWEEN '12:00:00' AND '13:01:01')", "time CHECK(value BETWEEN '12:00:00' AND '13:01:01')"),
array(Schema::TYPE_TIME . ' NOT NULL', 'time NOT NULL'),
array(Schema::TYPE_DATE, 'date'),
array(Schema::TYPE_DATE . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "date CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"),
array(Schema::TYPE_DATE . ' NOT NULL', 'date NOT NULL'),
array(Schema::TYPE_BINARY, 'bytea'),
array(Schema::TYPE_BOOLEAN, 'boolean'),
array(Schema::TYPE_BOOLEAN . ' NOT NULL DEFAULT 1', 'boolean NOT NULL DEFAULT 1'),
array(Schema::TYPE_MONEY, 'numeric(19,4)'),
array(Schema::TYPE_MONEY . '(16,2)', 'numeric(16,2)'),
array(Schema::TYPE_MONEY . ' CHECK (value > 0.0)', 'numeric(19,4) CHECK (value > 0.0)'),
array(Schema::TYPE_MONEY . '(16,2) CHECK (value > 0.0)', 'numeric(16,2) CHECK (value > 0.0)'),
array(Schema::TYPE_MONEY . ' NOT NULL', 'numeric(19,4) NOT NULL'),
);
}
}
\ No newline at end of file
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
namespace yiiunit\framework\db\sqlite; namespace yiiunit\framework\db\sqlite;
use yii\base\NotSupportedException;
use yii\db\sqlite\Schema; use yii\db\sqlite\Schema;
use yiiunit\framework\db\QueryBuilderTest; use yiiunit\framework\db\QueryBuilderTest;
...@@ -71,4 +72,11 @@ class SqliteQueryBuilderTest extends QueryBuilderTest ...@@ -71,4 +72,11 @@ class SqliteQueryBuilderTest extends QueryBuilderTest
array(Schema::TYPE_MONEY . ' NOT NULL', 'decimal(19,4) NOT NULL'), array(Schema::TYPE_MONEY . ' NOT NULL', 'decimal(19,4) NOT NULL'),
); );
} }
public function testAddDropPrimayKey()
{
$this->setExpectedException('yii\base\NotSupportedException');
parent::testAddDropPrimayKey();
}
} }
\ No newline at end of file
<?php
use yii\helpers\base\FileHelper;
use yii\test\TestCase;
/**
* Unit test for [[yii\helpers\base\FileHelper]]
* @see FileHelper
*/
class FileHelperTest extends TestCase
{
/**
* @var string test files path.
*/
private $testFilePath = '';
public function setUp()
{
$this->testFilePath = Yii::getAlias('@yiiunit/runtime') . DIRECTORY_SEPARATOR . get_class($this);
$this->createDir($this->testFilePath);
if (!file_exists($this->testFilePath)) {
$this->markTestIncomplete('Unit tests runtime directory should have writable permissions!');
}
}
public function tearDown()
{
$this->removeDir($this->testFilePath);
}
/**
* Creates directory.
* @param string $dirName directory full name.
*/
protected function createDir($dirName)
{
if (!file_exists($dirName)) {
mkdir($dirName, 0777, true);
}
}
/**
* Removes directory.
* @param string $dirName directory full name.
*/
protected function removeDir($dirName)
{
if (!empty($dirName) && is_dir($dirName)) {
if ($handle = opendir($dirName)) {
while (false !== ($entry = readdir($handle))) {
if ($entry != '.' && $entry != '..') {
if (is_dir($dirName . DIRECTORY_SEPARATOR . $entry) === true) {
$this->removeDir($dirName . DIRECTORY_SEPARATOR . $entry);
} else {
unlink($dirName . DIRECTORY_SEPARATOR . $entry);
}
}
}
closedir($handle);
rmdir($dirName);
}
}
}
/**
* Get file permission mode.
* @param string $file file name.
* @return string permission mode.
*/
protected function getMode($file)
{
return substr(sprintf('%o', fileperms($file)), -4);
}
/**
* Creates test files structure,
* @param array $items file system objects to be created in format: objectName => objectContent
* Arrays specifies directories, other values - files.
* @param string $basePath structure base file path.
*/
protected function createFileStructure(array $items, $basePath = '')
{
if (empty($basePath)) {
$basePath = $this->testFilePath;
}
foreach ($items as $name => $content) {
$itemName = $basePath . DIRECTORY_SEPARATOR . $name;
if (is_array($content)) {
mkdir($itemName, 0777, true);
$this->createFileStructure($content, $itemName);
} else {
file_put_contents($itemName, $content);
}
}
}
/**
* Asserts that file has specific permission mode.
* @param integer $expectedMode expected file permission mode.
* @param string $fileName file name.
* @param string $message error message
*/
protected function assertFileMode($expectedMode, $fileName, $message='')
{
$expectedMode = sprintf('%o', $expectedMode);
$this->assertEquals($expectedMode, $this->getMode($fileName), $message);
}
// Tests :
public function testCopyDirectory()
{
$srcDirName = 'test_src_dir';
$files = array(
'file1.txt' => 'file 1 content',
'file2.txt' => 'file 2 content',
);
$this->createFileStructure(array(
$srcDirName => $files
));
$basePath = $this->testFilePath;
$srcDirName = $basePath . DIRECTORY_SEPARATOR . $srcDirName;
$dstDirName = $basePath . DIRECTORY_SEPARATOR . 'test_dst_dir';
FileHelper::copyDirectory($srcDirName, $dstDirName);
$this->assertTrue(file_exists($dstDirName), 'Destination directory does not exist!');
foreach ($files as $name => $content) {
$fileName = $dstDirName . DIRECTORY_SEPARATOR . $name;
$this->assertTrue(file_exists($fileName), 'Directory file is missing!');
$this->assertEquals($content, file_get_contents($fileName), 'Incorrect file content!');
}
}
/**
* @depends testCopyDirectory
*/
public function testCopyDirectoryPermissions()
{
if (substr(PHP_OS, 0, 3) == 'WIN') {
$this->markTestSkipped("Can't reliably test it on Windows because fileperms() always return 0777.");
}
$srcDirName = 'test_src_dir';
$subDirName = 'test_sub_dir';
$fileName = 'test_file.txt';
$this->createFileStructure(array(
$srcDirName => array(
$subDirName => array(),
$fileName => 'test file content',
),
));
$basePath = $this->testFilePath;
$srcDirName = $basePath . DIRECTORY_SEPARATOR . $srcDirName;
$dstDirName = $basePath . DIRECTORY_SEPARATOR . 'test_dst_dir';
$dirMode = 0755;
$fileMode = 0755;
$options = array(
'dirMode' => $dirMode,
'fileMode' => $fileMode,
);
FileHelper::copyDirectory($srcDirName, $dstDirName, $options);
$this->assertFileMode($dirMode, $dstDirName, 'Destination directory has wrong mode!');
$this->assertFileMode($dirMode, $dstDirName . DIRECTORY_SEPARATOR . $subDirName, 'Copied sub directory has wrong mode!');
$this->assertFileMode($fileMode, $dstDirName . DIRECTORY_SEPARATOR . $fileName, 'Copied file has wrong mode!');
}
public function stestRemoveDirectory()
{
$dirName = 'test_dir_for_remove';
$this->createFileStructure(array(
$dirName => array(
'file1.txt' => 'file 1 content',
'file2.txt' => 'file 2 content',
'test_sub_dir' => array(
'sub_dir_file_1.txt' => 'sub dir file 1 content',
'sub_dir_file_2.txt' => 'sub dir file 2 content',
),
),
));
$basePath = $this->testFilePath;
$dirName = $basePath . DIRECTORY_SEPARATOR . $dirName;
FileHelper::removeDirectory($dirName);
$this->assertFalse(file_exists($dirName), 'Unable to remove directory!');
}
public function testFindFiles()
{
$dirName = 'test_dir';
$this->createFileStructure(array(
$dirName => array(
'file_1.txt' => 'file 1 content',
'file_2.txt' => 'file 2 content',
'test_sub_dir' => array(
'file_1_1.txt' => 'sub dir file 1 content',
'file_1_2.txt' => 'sub dir file 2 content',
),
),
));
$basePath = $this->testFilePath;
$dirName = $basePath . DIRECTORY_SEPARATOR . $dirName;
$expectedFiles = array(
$dirName . DIRECTORY_SEPARATOR . 'file_1.txt',
$dirName . DIRECTORY_SEPARATOR . 'file_2.txt',
$dirName . DIRECTORY_SEPARATOR . 'test_sub_dir' . DIRECTORY_SEPARATOR . 'file_1_1.txt',
$dirName . DIRECTORY_SEPARATOR . 'test_sub_dir' . DIRECTORY_SEPARATOR . 'file_1_2.txt',
);
$foundFiles = FileHelper::findFiles($dirName);
sort($expectedFiles);
sort($foundFiles);
$this->assertEquals($expectedFiles, $foundFiles);
}
/**
* @depends testFindFiles
*/
public function testFindFileFilter()
{
$dirName = 'test_dir';
$passedFileName = 'passed.txt';
$this->createFileStructure(array(
$dirName => array(
$passedFileName => 'passed file content',
'declined.txt' => 'declined file content',
),
));
$basePath = $this->testFilePath;
$dirName = $basePath . DIRECTORY_SEPARATOR . $dirName;
$options = array(
'filter' => function($path) use ($passedFileName) {
return $passedFileName == basename($path);
}
);
$foundFiles = FileHelper::findFiles($dirName, $options);
$this->assertEquals(array($dirName . DIRECTORY_SEPARATOR . $passedFileName), $foundFiles);
}
/**
* @depends testFindFiles
*/
public function testFindFilesExclude()
{
$dirName = 'test_dir';
$fileName = 'test_file.txt';
$excludeFileName = 'exclude_file.txt';
$this->createFileStructure(array(
$dirName => array(
$fileName => 'file content',
$excludeFileName => 'exclude file content',
),
));
$basePath = $this->testFilePath;
$dirName = $basePath . DIRECTORY_SEPARATOR . $dirName;
$options = array(
'except' => array($excludeFileName),
);
$foundFiles = FileHelper::findFiles($dirName, $options);
$this->assertEquals(array($dirName . DIRECTORY_SEPARATOR . $fileName), $foundFiles);
}
/**
* @depends testFindFiles
*/
public function testFindFilesFileType()
{
$dirName = 'test_dir';
$fileType = 'dat';
$fileName = 'test_file.' . $fileType;
$excludeFileName = 'exclude_file.txt';
$this->createFileStructure(array(
$dirName => array(
$fileName => 'file content',
$excludeFileName => 'exclude file content',
),
));
$basePath = $this->testFilePath;
$dirName = $basePath . DIRECTORY_SEPARATOR . $dirName;
$options = array(
'fileTypes' => array($fileType),
);
$foundFiles = FileHelper::findFiles($dirName, $options);
$this->assertEquals(array($dirName . DIRECTORY_SEPARATOR . $fileName), $foundFiles);
}
public function testMkdir() {
$basePath = $this->testFilePath;
$dirName = $basePath . DIRECTORY_SEPARATOR . 'test_dir_level_1' . DIRECTORY_SEPARATOR . 'test_dir_level_2';
FileHelper::mkdir($dirName);
$this->assertTrue(file_exists($dirName), 'Unable to create directory recursively!');
}
public function testGetMimeTypeByExtension()
{
$magicFile = $this->testFilePath . DIRECTORY_SEPARATOR . 'mime_type.php';
$mimeTypeMap = array(
'txa' => 'application/json',
'txb' => 'another/mime',
);
$magicFileContent = '<?php return ' . var_export($mimeTypeMap, true) . ';';
file_put_contents($magicFile, $magicFileContent);
foreach ($mimeTypeMap as $extension => $mimeType) {
$fileName = 'test.' . $extension;
$this->assertNull(FileHelper::getMimeTypeByExtension($fileName));
$this->assertEquals($mimeType, FileHelper::getMimeTypeByExtension($fileName, $magicFile));
}
}
}
<?php <?php
namespace yii\web; namespace yiiunit\framework\web;
use yiiunit\framework\web\ResponseTest;
/** use Yii;
* Mock PHP header function to check for sent headers use yii\helpers\StringHelper;
* @param string $string
* @param bool $replace
* @param int $httpResponseCode
*/
function header($string, $replace = true, $httpResponseCode = null) {
ResponseTest::$headers[] = $string;
// TODO implement replace
if ($httpResponseCode !== null) { class Response extends \yii\web\Response
ResponseTest::$httpResponseCode = $httpResponseCode; {
public function send()
{
// does nothing to allow testing
} }
} }
namespace yiiunit\framework\web;
use yii\helpers\StringHelper;
use yii\web\Response;
class ResponseTest extends \yiiunit\TestCase class ResponseTest extends \yiiunit\TestCase
{ {
public static $headers = array(); /**
public static $httpResponseCode = 200; * @var Response
*/
public $response;
protected function setUp() protected function setUp()
{ {
parent::setUp(); parent::setUp();
$this->mockApplication(); $this->mockApplication();
$this->reset(); $this->response = new Response;
}
protected function reset()
{
static::$headers = array();
static::$httpResponseCode = 200;
} }
public function rightRanges() public function rightRanges()
...@@ -56,18 +41,22 @@ class ResponseTest extends \yiiunit\TestCase ...@@ -56,18 +41,22 @@ class ResponseTest extends \yiiunit\TestCase
/** /**
* @dataProvider rightRanges * @dataProvider rightRanges
*/ */
public function testSendFileRanges($rangeHeader, $expectedHeader, $length, $expectedFile) public function testSendFileRanges($rangeHeader, $expectedHeader, $length, $expectedContent)
{ {
$content = $this->generateTestFileContent(); $dataFile = \Yii::getAlias('@yiiunit/data/web/data.txt');
$fullContent = file_get_contents($dataFile);
$_SERVER['HTTP_RANGE'] = 'bytes=' . $rangeHeader; $_SERVER['HTTP_RANGE'] = 'bytes=' . $rangeHeader;
$sent = $this->runSendFile('testFile.txt', $content, null); ob_start();
$this->response->sendFile($dataFile);
$this->assertEquals($expectedFile, $sent); $content = ob_get_clean();
$this->assertTrue(in_array('HTTP/1.1 206 Partial Content', static::$headers));
$this->assertTrue(in_array('Accept-Ranges: bytes', static::$headers)); $this->assertEquals($expectedContent, $content);
$this->assertArrayHasKey('Content-Range: bytes ' . $expectedHeader . '/' . StringHelper::strlen($content), array_flip(static::$headers)); $this->assertEquals(206, $this->response->statusCode);
$this->assertTrue(in_array('Content-Type: text/plain', static::$headers)); $headers = $this->response->headers;
$this->assertTrue(in_array('Content-Length: ' . $length, static::$headers)); $this->assertEquals("bytes", $headers->get('Accept-Ranges'));
$this->assertEquals("bytes " . $expectedHeader . '/' . StringHelper::strlen($fullContent), $headers->get('Content-Range'));
$this->assertEquals('text/plain', $headers->get('Content-Type'));
$this->assertEquals("$length", $headers->get('Content-Length'));
} }
public function wrongRanges() public function wrongRanges()
...@@ -87,25 +76,15 @@ class ResponseTest extends \yiiunit\TestCase ...@@ -87,25 +76,15 @@ class ResponseTest extends \yiiunit\TestCase
*/ */
public function testSendFileWrongRanges($rangeHeader) public function testSendFileWrongRanges($rangeHeader)
{ {
$this->setExpectedException('yii\base\HttpException', 'Requested Range Not Satisfiable'); $this->setExpectedException('yii\web\HttpException');
$content = $this->generateTestFileContent(); $dataFile = \Yii::getAlias('@yiiunit/data/web/data.txt');
$_SERVER['HTTP_RANGE'] = 'bytes=' . $rangeHeader; $_SERVER['HTTP_RANGE'] = 'bytes=' . $rangeHeader;
$this->runSendFile('testFile.txt', $content, null); $this->response->sendFile($dataFile);
} }
protected function generateTestFileContent() protected function generateTestFileContent()
{ {
return '12ёжик3456798áèabcdefghijklmnopqrstuvwxyz!"§$%&/(ёжик)=?'; return '12ёжик3456798áèabcdefghijklmnopqrstuvwxyz!"§$%&/(ёжик)=?';
} }
protected function runSendFile($fileName, $content, $mimeType)
{
ob_start();
ob_implicit_flush(false);
$response = new Response();
$response->sendFile($fileName, $content, $mimeType, false);
$file = ob_get_clean();
return $file;
}
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment