diff --git a/apps/advanced/README.md b/apps/advanced/README.md
index d8c1e17..f2abc1e 100644
--- a/apps/advanced/README.md
+++ b/apps/advanced/README.md
@@ -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
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.
@@ -20,18 +20,18 @@ DIRECTORY STRUCTURE
```
common
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
config/ contains console configurations
controllers/ contains console controllers (commands)
migrations/ contains database migrations
models/ contains console-specific model classes
runtime/ contains files generated during runtime
-backstage
+backend
assets/ contains application assets such as JavaScript and CSS
- config/ contains backstage configurations
+ config/ contains backend configurations
controllers/ contains Web controller classes
- models/ contains backstage-specific model classes
+ models/ contains backend-specific model classes
runtime/ contains files generated during runtime
views/ contains view files for the Web application
www/ contains the entry script and Web resources
@@ -107,7 +107,7 @@ the installed application. You only need to do these once for all.
Now you should be able to access:
- 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.
diff --git a/apps/advanced/backstage/assets/.gitkeep b/apps/advanced/backend/assets/.gitkeep
similarity index 100%
rename from apps/advanced/backstage/assets/.gitkeep
rename to apps/advanced/backend/assets/.gitkeep
diff --git a/apps/advanced/backstage/config/.gitignore b/apps/advanced/backend/config/.gitignore
similarity index 100%
rename from apps/advanced/backstage/config/.gitignore
rename to apps/advanced/backend/config/.gitignore
diff --git a/apps/advanced/backstage/config/assets.php b/apps/advanced/backend/config/assets.php
similarity index 100%
rename from apps/advanced/backstage/config/assets.php
rename to apps/advanced/backend/config/assets.php
diff --git a/apps/advanced/backstage/config/main.php b/apps/advanced/backend/config/main.php
similarity index 94%
rename from apps/advanced/backstage/config/main.php
rename to apps/advanced/backend/config/main.php
index 6e55c47..3140cd2 100644
--- a/apps/advanced/backstage/config/main.php
+++ b/apps/advanced/backend/config/main.php
@@ -13,7 +13,7 @@ return array(
'basePath' => dirname(__DIR__),
'vendorPath' => dirname(dirname(__DIR__)) . '/vendor',
'preload' => array('log'),
- 'controllerNamespace' => 'backstage\controllers',
+ 'controllerNamespace' => 'backend\controllers',
'modules' => array(
),
'components' => array(
diff --git a/apps/advanced/backstage/config/params.php b/apps/advanced/backend/config/params.php
similarity index 100%
rename from apps/advanced/backstage/config/params.php
rename to apps/advanced/backend/config/params.php
diff --git a/apps/advanced/backstage/controllers/SiteController.php b/apps/advanced/backend/controllers/SiteController.php
similarity index 94%
rename from apps/advanced/backstage/controllers/SiteController.php
rename to apps/advanced/backend/controllers/SiteController.php
index d40738a..0306c97 100644
--- a/apps/advanced/backstage/controllers/SiteController.php
+++ b/apps/advanced/backend/controllers/SiteController.php
@@ -1,6 +1,6 @@
rem @link http://www.yiiframework.com/
@@ -15,6 +15,6 @@ set YII_PATH=%~dp0
if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe
-"%PHP_COMMAND%" "%YII_PATH%install" %*
+"%PHP_COMMAND%" "%YII_PATH%init" %*
@endlocal
diff --git a/framework/yii/base/ActionFilter.php b/framework/yii/base/ActionFilter.php
index 20ff142..1e957d5 100644
--- a/framework/yii/base/ActionFilter.php
+++ b/framework/yii/base/ActionFilter.php
@@ -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,
* 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;
/**
* @var array list of action IDs that this filter should not apply to.
+ * @see only
*/
public $except = array();
diff --git a/framework/yii/base/Application.php b/framework/yii/base/Application.php
index 09951bd..9969ecd 100644
--- a/framework/yii/base/Application.php
+++ b/framework/yii/base/Application.php
@@ -8,6 +8,7 @@
namespace yii\base;
use Yii;
+use yii\web\HttpException;
/**
* Application is the base class for all application classes.
@@ -17,8 +18,14 @@ use Yii;
*/
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.
*/
@@ -128,6 +135,10 @@ class Application extends Module
ini_set('display_errors', 0);
set_exception_handler(array($this, 'handleException'));
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
{
if (!$this->_ended) {
$this->_ended = true;
- $this->afterRequest();
+ $this->getResponse()->end();
+ $this->afterRun();
}
- $this->handleFatalError();
-
if ($exit) {
exit($status);
}
@@ -159,30 +169,30 @@ class Application extends Module
*/
public function run()
{
- $this->beforeRequest();
- // 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);
+ $this->beforeRun();
+ $response = $this->getResponse();
+ $response->begin();
register_shutdown_function(array($this, 'end'), 0, false);
$status = $this->processRequest();
- $this->afterRequest();
+ $response->end();
+ $this->afterRun();
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
}
/**
+ * 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.
* @return View the view object that is used to render various view files.
*/
diff --git a/framework/yii/base/ErrorHandler.php b/framework/yii/base/ErrorHandler.php
index 7bf9e7e..fe9eef3 100644
--- a/framework/yii/base/ErrorHandler.php
+++ b/framework/yii/base/ErrorHandler.php
@@ -8,6 +8,7 @@
namespace yii\base;
use Yii;
+use yii\web\HttpException;
/**
* ErrorHandler handles uncaught PHP errors and exceptions.
@@ -82,11 +83,12 @@ class ErrorHandler extends Component
} elseif (!(Yii::$app instanceof \yii\web\Application)) {
Yii::$app->renderException($exception);
} else {
+ $response = Yii::$app->getResponse();
if (!headers_sent()) {
if ($exception instanceof HttpException) {
- header('HTTP/1.0 ' . $exception->statusCode . ' ' . $exception->getName());
+ $response->setStatusCode($exception->statusCode);
} 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') {
@@ -100,13 +102,13 @@ class ErrorHandler extends Component
$view = new View();
$request = '';
- foreach (array('GET', 'POST', 'SERVER', 'FILES', 'COOKIE', 'SESSION', 'ENV') as $name) {
- if (!empty($GLOBALS['_' . $name])) {
- $request .= '$_' . $name . ' = ' . var_export($GLOBALS['_' . $name], true) . ";\n\n";
+ foreach (array('_GET', '_POST', '_SERVER', '_FILES', '_COOKIE', '_SESSION', '_ENV') as $name) {
+ if (!empty($GLOBALS[$name])) {
+ $request .= '$' . $name . ' = ' . var_export($GLOBALS[$name], true) . ";\n\n";
}
}
$request = rtrim($request, "\n\n");
- echo $view->renderFile($this->mainView, array(
+ $response->content = $view->renderFile($this->mainView, array(
'exception' => $exception,
'request' => $request,
), $this);
diff --git a/framework/yii/base/Response.php b/framework/yii/base/Response.php
index 396b073..29bddb0 100644
--- a/framework/yii/base/Response.php
+++ b/framework/yii/base/Response.php
@@ -14,19 +14,28 @@ namespace yii\base;
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
*/
- public function beginOutput()
+ public function beginBuffer()
{
ob_start();
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
*/
- public function endOutput()
+ public function endBuffer()
{
return ob_get_clean();
}
@@ -35,16 +44,16 @@ class Response extends Component
* Returns contents of the output buffer
* @return string output buffer contents
*/
- public function getOutput()
+ public function getBuffer()
{
return ob_get_contents();
}
/**
* 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) {
for ($level = ob_get_level(); $level > 0; --$level) {
@@ -56,4 +65,28 @@ class Response extends Component
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);
+ }
}
diff --git a/framework/yii/console/Response.php b/framework/yii/console/Response.php
new file mode 100644
index 0000000..34f105d
--- /dev/null
+++ b/framework/yii/console/Response.php
@@ -0,0 +1,17 @@
+
+ * @since 2.0
+ */
+class Response extends \yii\base\Response
+{
+
+}
diff --git a/framework/yii/db/Command.php b/framework/yii/db/Command.php
index 17accf4..a32e892 100644
--- a/framework/yii/db/Command.php
+++ b/framework/yii/db/Command.php
@@ -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.
* The method will properly quote the table and column names.
* @param string $name the name of the foreign key constraint.
diff --git a/framework/yii/db/Migration.php b/framework/yii/db/Migration.php
index 774ac14..38b1bdc 100644
--- a/framework/yii/db/Migration.php
+++ b/framework/yii/db/Migration.php
@@ -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.
* The method will properly quote the table and column names.
* @param string $name the name of the foreign key constraint.
diff --git a/framework/yii/db/QueryBuilder.php b/framework/yii/db/QueryBuilder.php
index 04f1969..0d221bc 100644
--- a/framework/yii/db/QueryBuilder.php
+++ b/framework/yii/db/QueryBuilder.php
@@ -268,6 +268,41 @@ class QueryBuilder extends \yii\base\Object
{
return "DROP TABLE " . $this->db->quoteTableName($table);
}
+
+ /**
+ * 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.
diff --git a/framework/yii/db/mysql/QueryBuilder.php b/framework/yii/db/mysql/QueryBuilder.php
index 4b35e24..b4ac996 100644
--- a/framework/yii/db/mysql/QueryBuilder.php
+++ b/framework/yii/db/mysql/QueryBuilder.php
@@ -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.
* The sequence will be reset such that the primary key of the next new row inserted
* will have the specified value or 1.
@@ -113,7 +124,7 @@ class QueryBuilder extends \yii\db\QueryBuilder
} elseif ($table === null) {
throw new InvalidParamException("Table not found: $tableName");
} else {
- throw new InvalidParamException("There is not sequence associated with table '$tableName'.'");
+ throw new InvalidParamException("There is not sequence associated with table '$tableName'.");
}
}
diff --git a/framework/yii/db/pgsql/QueryBuilder.php b/framework/yii/db/pgsql/QueryBuilder.php
index 3417ad9..9701fd6 100644
--- a/framework/yii/db/pgsql/QueryBuilder.php
+++ b/framework/yii/db/pgsql/QueryBuilder.php
@@ -21,21 +21,20 @@ class QueryBuilder extends \yii\db\QueryBuilder
* @var array mapping from abstract column types (keys) to physical column types (values).
*/
public $typeMap = array(
- Schema::TYPE_PK => 'serial not null primary key',
- Schema::TYPE_STRING => 'varchar',
- Schema::TYPE_TEXT => 'text',
- Schema::TYPE_SMALLINT => 'smallint',
- Schema::TYPE_INTEGER => 'integer',
- Schema::TYPE_BIGINT => 'bigint',
- Schema::TYPE_FLOAT => 'double precision',
- Schema::TYPE_DECIMAL => 'numeric',
- Schema::TYPE_DATETIME => 'timestamp',
- Schema::TYPE_TIMESTAMP => 'timestamp',
- Schema::TYPE_TIME => 'time',
- Schema::TYPE_DATE => 'date',
- Schema::TYPE_BINARY => 'bytea',
- Schema::TYPE_BOOLEAN => 'boolean',
- Schema::TYPE_MONEY => 'numeric(19,4)',
+ Schema::TYPE_PK => 'serial not null primary key',
+ Schema::TYPE_STRING => 'varchar(255)',
+ Schema::TYPE_TEXT => 'text',
+ Schema::TYPE_SMALLINT => 'smallint',
+ Schema::TYPE_INTEGER => 'integer',
+ Schema::TYPE_BIGINT => 'bigint',
+ Schema::TYPE_FLOAT => 'double precision',
+ Schema::TYPE_DECIMAL => 'numeric(10,0)',
+ Schema::TYPE_DATETIME => 'timestamp',
+ Schema::TYPE_TIMESTAMP => 'timestamp',
+ Schema::TYPE_TIME => 'time',
+ Schema::TYPE_DATE => 'date',
+ Schema::TYPE_BINARY => 'bytea',
+ Schema::TYPE_BOOLEAN => 'boolean',
+ Schema::TYPE_MONEY => 'numeric(19,4)',
);
-
}
diff --git a/framework/yii/db/pgsql/Schema.php b/framework/yii/db/pgsql/Schema.php
index 8cfb535..8acb7bd 100644
--- a/framework/yii/db/pgsql/Schema.php
+++ b/framework/yii/db/pgsql/Schema.php
@@ -43,6 +43,7 @@ class Schema extends \yii\db\Schema
'circle' => self::TYPE_STRING,
'date' => self::TYPE_DATE,
'real' => self::TYPE_FLOAT,
+ 'decimal' => self::TYPE_DECIMAL,
'double precision' => self::TYPE_DECIMAL,
'inet' => self::TYPE_STRING,
'smallint' => self::TYPE_SMALLINT,
@@ -55,7 +56,6 @@ class Schema extends \yii\db\Schema
'money' => self::TYPE_MONEY,
'name' => self::TYPE_STRING,
'numeric' => self::TYPE_STRING,
- 'numrange' => self::TYPE_DECIMAL,
'oid' => self::TYPE_BIGINT, // should not be used. it's pg internal!
'path' => self::TYPE_STRING,
'point' => self::TYPE_STRING,
@@ -165,11 +165,11 @@ SQL;
$columns = explode(',', $constraint['columns']);
$fcolumns = explode(',', $constraint['foreign_columns']);
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 {
- $foreign_table = $constraint['foreign_table_name'];
+ $foreignTable = $constraint['foreign_table_name'];
}
- $citem = array($foreign_table);
+ $citem = array($foreignTable);
foreach ($columns as $idx => $column) {
$citem[] = array($fcolumns[$idx] => $column);
}
@@ -243,6 +243,9 @@ ORDER BY
SQL;
$columns = $this->db->createCommand($sql)->queryAll();
+ if (empty($columns)) {
+ return false;
+ }
foreach ($columns as $column) {
$column = $this->loadColumnSchema($column);
$table->columns[$column->name] = $column;
@@ -285,5 +288,4 @@ SQL;
$column->phpType = $this->getColumnPhpType($column);
return $column;
}
-
}
diff --git a/framework/yii/db/sqlite/QueryBuilder.php b/framework/yii/db/sqlite/QueryBuilder.php
index 52c101b..99198ae 100644
--- a/framework/yii/db/sqlite/QueryBuilder.php
+++ b/framework/yii/db/sqlite/QueryBuilder.php
@@ -179,4 +179,30 @@ class QueryBuilder extends \yii\db\QueryBuilder
{
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.');
+ }
}
+
diff --git a/framework/yii/helpers/base/FileHelper.php b/framework/yii/helpers/base/FileHelper.php
index 954c86e..fc1f91a 100644
--- a/framework/yii/helpers/base/FileHelper.php
+++ b/framework/yii/helpers/base/FileHelper.php
@@ -10,6 +10,7 @@
namespace yii\helpers\base;
use Yii;
+use yii\helpers\StringHelper;
/**
* Filesystem helper
@@ -95,7 +96,7 @@ class FileHelper
}
}
- return $checkExtension ? self::getMimeTypeByExtension($file) : null;
+ return $checkExtension ? static::getMimeTypeByExtension($file) : null;
}
/**
@@ -133,12 +134,21 @@ class FileHelper
*
* - 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.
- * - beforeCopy: callback, a PHP callback that is called before copying each sub-directory or file.
- * If the callback returns false, the copy operation for the sub-directory or file will be cancelled.
+ * - 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 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
- * file to be 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`.
+ * file copied from, while `$to` is the copy target.
*/
public static function copyDirectory($src, $dst, $options = array())
{
@@ -153,7 +163,7 @@ class FileHelper
}
$from = $src . 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)) {
copy($from, $to);
if (isset($options['fileMode'])) {
@@ -169,4 +179,129 @@ class FileHelper
}
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;
+ }
}
diff --git a/framework/yii/helpers/base/StringHelper.php b/framework/yii/helpers/base/StringHelper.php
index 5134bf6..7fbb960 100644
--- a/framework/yii/helpers/base/StringHelper.php
+++ b/framework/yii/helpers/base/StringHelper.php
@@ -43,8 +43,10 @@ class StringHelper
/**
* 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.
+ * 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 "..".
* @param string $path A path string.
* @param string $suffix If the name component ends in suffix this will also be cut off.
diff --git a/framework/yii/views/errorHandler/main.php b/framework/yii/views/errorHandler/main.php
index d7bbb3d..05e217e 100644
--- a/framework/yii/views/errorHandler/main.php
+++ b/framework/yii/views/errorHandler/main.php
@@ -14,7 +14,7 @@ $context = $this->context;
statusCode . ' ' . $context->htmlEncode($exception->getName());
} elseif ($exception instanceof \yii\base\Exception) {
echo $context->htmlEncode($exception->getName() . ' – ' . get_class($exception));
@@ -362,7 +362,7 @@ pre .diff .change{
' . $context->createHttpStatusLink($exception->statusCode, $context->htmlEncode($exception->getName())) . '';
echo ' – ' . $context->addTypeLinks(get_class($exception));
} elseif ($exception instanceof \yii\base\Exception) {
diff --git a/framework/yii/web/AccessControl.php b/framework/yii/web/AccessControl.php
index ce64533..7dedaf9 100644
--- a/framework/yii/web/AccessControl.php
+++ b/framework/yii/web/AccessControl.php
@@ -10,7 +10,7 @@ namespace yii\web;
use Yii;
use yii\base\Action;
use yii\base\ActionFilter;
-use yii\base\HttpException;
+use yii\web\HttpException;
/**
* AccessControl provides simple access control based on a set of rules.
diff --git a/framework/yii/web/Application.php b/framework/yii/web/Application.php
index 12c9295..ce326a2 100644
--- a/framework/yii/web/Application.php
+++ b/framework/yii/web/Application.php
@@ -8,7 +8,7 @@
namespace yii\web;
use Yii;
-use yii\base\HttpException;
+use yii\web\HttpException;
use yii\base\InvalidRouteException;
/**
diff --git a/framework/yii/web/CaptchaAction.php b/framework/yii/web/CaptchaAction.php
index cff2314..1ed1fb0 100644
--- a/framework/yii/web/CaptchaAction.php
+++ b/framework/yii/web/CaptchaAction.php
@@ -277,11 +277,8 @@ class CaptchaAction extends Action
imagecolordeallocate($image, $foreColor);
- 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");
+ $this->sendHttpHeaders();
+
imagepng($image);
imagedestroy($image);
}
@@ -319,12 +316,21 @@ class CaptchaAction extends Action
$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');
- 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');
}
}
diff --git a/framework/yii/web/Controller.php b/framework/yii/web/Controller.php
index 026c078..22a2ebd 100644
--- a/framework/yii/web/Controller.php
+++ b/framework/yii/web/Controller.php
@@ -8,7 +8,7 @@
namespace yii\web;
use Yii;
-use yii\base\HttpException;
+use yii\web\HttpException;
use yii\base\InlineAction;
/**
diff --git a/framework/yii/web/Cookie.php b/framework/yii/web/Cookie.php
index 610e5aa..8cbb412 100644
--- a/framework/yii/web/Cookie.php
+++ b/framework/yii/web/Cookie.php
@@ -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,
* 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]].
diff --git a/framework/yii/web/CookieCollection.php b/framework/yii/web/CookieCollection.php
index fc9375e..3e22092 100644
--- a/framework/yii/web/CookieCollection.php
+++ b/framework/yii/web/CookieCollection.php
@@ -9,7 +9,8 @@ namespace yii\web;
use Yii;
use ArrayIterator;
-use yii\helpers\SecurityHelper;
+use yii\base\InvalidCallException;
+use yii\base\Object;
/**
* CookieCollection maintains the cookies available in the current request.
@@ -19,17 +20,12 @@ use yii\helpers\SecurityHelper;
* @author Qiang Xue
* @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,
- * if a cookie is tampered on the client side, it will be ignored when received on the server side.
+ * @var boolean whether this collection is read only.
*/
- public $enableValidation = true;
- /**
- * @var string the secret key used for cookie validation. If not set, a random key will be generated and used.
- */
- public $validationKey;
+ public $readOnly = false;
/**
* @var Cookie[] the cookies in this collection (indexed by the cookie names)
@@ -38,12 +34,14 @@ class CookieCollection extends \yii\base\Object implements \IteratorAggregate, \
/**
* 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
*/
- public function __construct($config = array())
+ public function __construct($cookies = array(), $config = array())
{
+ $this->_cookies = $cookies;
parent::__construct($config);
- $this->_cookies = $this->loadCookies();
}
/**
@@ -114,50 +112,53 @@ class CookieCollection extends \yii\base\Object implements \IteratorAggregate, \
* Adds a cookie to the collection.
* 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
+ * @throws InvalidCallException if the cookie collection is read only
*/
public function add($cookie)
{
- if (isset($this->_cookies[$cookie->name])) {
- $c = $this->_cookies[$cookie->name];
- 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);
+ if ($this->readOnly) {
+ throw new InvalidCallException('The cookie collection is read only.');
}
-
- setcookie($cookie->name, $value, $cookie->expire, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httponly);
$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 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])) {
- $cookie = $this->_cookies[$cookie];
+ if ($this->readOnly) {
+ throw new InvalidCallException('The cookie collection is read only.');
}
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]);
}
}
/**
* Removes all cookies.
+ * @throws InvalidCallException if the cookie collection is read only
*/
public function removeAll()
{
- foreach ($this->_cookies as $cookie) {
- setcookie($cookie->name, '', 0, $cookie->path, $cookie->domain, $cookie->secure, $cookie->httponly);
+ if ($this->readOnly) {
+ throw new InvalidCallException('The cookie collection is read only.');
}
$this->_cookies = array();
}
@@ -222,36 +223,4 @@ class CookieCollection extends \yii\base\Object implements \IteratorAggregate, \
{
$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;
- }
}
diff --git a/framework/yii/web/HeaderCollection.php b/framework/yii/web/HeaderCollection.php
index c7e1462..aa3e01f 100644
--- a/framework/yii/web/HeaderCollection.php
+++ b/framework/yii/web/HeaderCollection.php
@@ -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.
* @param string $name the name 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);
$this->_headers[$name] = (array)$value;
+ return $this;
}
/**
@@ -92,11 +94,29 @@ class HeaderCollection extends Object implements \IteratorAggregate, \ArrayAcces
* be appended to it instead of replacing it.
* @param string $name the name of the header
* @param string $value the value of the header
+ * @return HeaderCollection the collection object itself
*/
public function add($name, $value)
{
$name = strtolower($name);
$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;
}
/**
diff --git a/framework/yii/web/HttpCache.php b/framework/yii/web/HttpCache.php
index 5b7682d..cc9e6ed 100644
--- a/framework/yii/web/HttpCache.php
+++ b/framework/yii/web/HttpCache.php
@@ -50,7 +50,7 @@ class HttpCache extends ActionFilter
/**
* @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.)
@@ -60,7 +60,7 @@ class HttpCache extends ActionFilter
*/
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) {
return true;
}
@@ -75,17 +75,18 @@ class HttpCache extends ActionFilter
}
$this->sendCacheControlHeader();
+ $response = Yii::$app->getResponse();
if ($etag !== null) {
- header("ETag: $etag");
+ $response->getHeaders()->set('Etag', $etag);
}
if ($this->validateCache($lastModified, $etag)) {
- header('HTTP/1.1 304 Not Modified');
+ $response->setStatusCode(304);
return false;
}
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;
}
@@ -113,9 +114,10 @@ class HttpCache extends ActionFilter
protected function sendCacheControlHeader()
{
session_cache_limiter('public');
- header('Pragma:', true);
+ $headers = Yii::$app->getResponse()->getHeaders();
+ $headers->set('Pragma');
if ($this->cacheControlHeader !== null) {
- header($this->cacheControlHeader, true);
+ $headers->set('Cache-Control', $this->cacheControlHeader);
}
}
diff --git a/framework/yii/base/HttpException.php b/framework/yii/web/HttpException.php
similarity index 87%
rename from framework/yii/base/HttpException.php
rename to framework/yii/web/HttpException.php
index cce0bb0..384a5b4 100644
--- a/framework/yii/base/HttpException.php
+++ b/framework/yii/web/HttpException.php
@@ -5,8 +5,10 @@
* @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.
@@ -43,8 +45,8 @@ class HttpException extends UserException
*/
public function getName()
{
- if (isset(\yii\web\Response::$statusTexts[$this->statusCode])) {
- return \yii\web\Response::$statusTexts[$this->statusCode];
+ if (isset(Response::$httpStatuses[$this->statusCode])) {
+ return Response::$httpStatuses[$this->statusCode];
} else {
return 'Error';
}
diff --git a/framework/yii/web/Request.php b/framework/yii/web/Request.php
index 7cec044..afd2f8a 100644
--- a/framework/yii/web/Request.php
+++ b/framework/yii/web/Request.php
@@ -8,8 +8,9 @@
namespace yii\web;
use Yii;
-use yii\base\HttpException;
+use yii\web\HttpException;
use yii\base\InvalidConfigException;
+use yii\helpers\SecurityHelper;
/**
* @author Qiang Xue
@@ -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.
* @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.
*/
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
* request tunneled through POST. Default to '_method'.
* @see getMethod
@@ -717,14 +714,64 @@ class Request extends \yii\base\Request
public function getCookies()
{
if ($this->_cookies === null) {
- $this->_cookies = new CookieCollection(array(
- 'enableValidation' => $this->enableCookieValidation,
- 'validationKey' => $this->cookieValidationKey,
+ $this->_cookies = new CookieCollection($this->loadCookies(), array(
+ 'readOnly' => true,
));
}
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;
/**
diff --git a/framework/yii/web/Response.php b/framework/yii/web/Response.php
index 86978d5..051850f 100644
--- a/framework/yii/web/Response.php
+++ b/framework/yii/web/Response.php
@@ -8,11 +8,12 @@
namespace yii\web;
use Yii;
-use yii\base\HttpException;
+use yii\web\HttpException;
use yii\base\InvalidParamException;
use yii\helpers\FileHelper;
use yii\helpers\Html;
use yii\helpers\Json;
+use yii\helpers\SecurityHelper;
use yii\helpers\StringHelper;
/**
@@ -45,11 +46,10 @@ class Response extends \yii\base\Response
* @var string the version of the HTTP protocol to use
*/
public $version = '1.0';
-
/**
* @var array list of HTTP status codes and the corresponding texts
*/
- public static $statusTexts = array(
+ public static $httpStatuses = array(
100 => 'Continue',
101 => 'Switching Protocols',
102 => 'Processing',
@@ -93,7 +93,7 @@ class Response extends \yii\base\Response
415 => 'Unsupported Media Type',
416 => 'Requested range unsatisfiable',
417 => 'Expectation failed',
- 418 => 'I’m a teapot',
+ 418 => 'I\'m a teapot',
422 => 'Unprocessable entity',
423 => 'Locked',
424 => 'Method failure',
@@ -117,7 +117,10 @@ class Response extends \yii\base\Response
511 => 'Network Authentication Required',
);
- private $_statusCode = 200;
+ /**
+ * @var integer the HTTP status code to send with the response.
+ */
+ private $_statusCode;
/**
* @var HeaderCollection
*/
@@ -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()
{
return $this->_statusCode;
}
- public function setStatusCode($value)
+ public function setStatusCode($value, $text = null)
{
$this->_statusCode = (int)$value;
- if ($this->isInvalid()) {
+ if ($this->getIsInvalid()) {
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
public function renderJson($data)
{
- $this->getHeaders()->set('content-type', 'application/json');
+ $this->getHeaders()->set('Content-Type', 'application/json');
$this->content = Json::encode($data);
+ $this->send();
}
public function renderJsonp($data, $callbackName)
{
- $this->getHeaders()->set('content-type', 'text/javascript');
+ $this->getHeaders()->set('Content-Type', 'text/javascript');
$data = Json::encode($data);
$this->content = "$callbackName($data);";
+ $this->send();
}
/**
@@ -179,6 +204,25 @@ class Response extends \yii\base\Response
{
$this->sendHeaders();
$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
*/
protected function sendHeaders()
{
- header("HTTP/{$this->version} " . $this->getStatusCode() . " {$this->statusText}");
- foreach ($this->_headers as $name => $values) {
- foreach ($values as $value) {
- header("$name: $value");
+ if (headers_sent()) {
+ 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) {
+ header("$name: $value", false);
+ }
}
+ $headers->removeAll();
}
- $this->_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);
+ }
+ 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
}
/**
- * Sends a file to user.
- * @param string $fileName file name
- * @param string $content content to be set.
- * @param string $mimeType mime type of the content. If null, it will be guessed automatically based on the given file name.
- * @param boolean $terminate whether to terminate the current application after calling this method
- * @throws \yii\base\HttpException when range request is not satisfiable.
+ * Sends a file to the browser.
+ * @param string $filePath the path of the file to be sent.
+ * @param string $attachmentName the file name shown to the user. If null, it will be determined from `$filePath`.
+ * @param string $mimeType the MIME type of the content. If null, it will be guessed based on `$filePath`
*/
- 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';
}
+ if ($attachmentName === null) {
+ $attachmentName = basename($filePath);
+ }
+ $handle = fopen($filePath, 'rb');
+ $this->sendStreamAsFile($handle, $attachmentName, $mimeType);
+ }
- $fileSize = StringHelper::strlen($content);
- $contentStart = 0;
- $contentEnd = $fileSize - 1;
-
- // tell the client that we accept range requests
- header('Accept-Ranges: bytes');
+ /**
+ * Sends the specified content as a file to the browser.
+ * @param string $content the content to be sent. The existing [[content]] will be discarded.
+ * @param string $attachmentName the file name shown to the user.
+ * @param string $mimeType the MIME type of the content.
+ */
+ 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\"");
+
+ $this->content = $content;
+ $this->send();
+ }
- if (isset($_SERVER['HTTP_RANGE'])) {
- // client sent us a multibyte range, can not hold this one for now
- if (strpos($_SERVER['HTTP_RANGE'], ',') !== false) {
- header("Content-Range: bytes $contentStart-$contentEnd/$fileSize");
- throw new HttpException(416, 'Requested Range Not Satisfiable');
- }
+ /**
+ * 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 = str_replace('bytes=', '', $_SERVER['HTTP_RANGE']);
+ list($begin, $end) = $range;
+ if ($begin !=0 || $end != $fileSize - 1) {
+ $this->setStatusCode(206);
+ $headers->set('Content-Range', "bytes $begin-$end/$fileSize");
+ } else {
+ $this->setStatusCode(200);
+ }
- // range requests starts from "-", so it means that data must be dumped the end point.
- if ($range[0] === '-') {
- $contentStart = $fileSize - substr($range, 1);
- } else {
- $range = explode('-', $range);
- $contentStart = $range[0];
+ if (isset($options['mimeType'])) {
+ $headers->set('Content-Type', $options['mimeType']);
+ }
- // check if the last-byte-pos presents in header
- if ((isset($range[1]) && is_numeric($range[1]))) {
- $contentEnd = $range[1];
- }
+ $length = $end - $begin + 1;
+
+ $headers->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', $length)
+ ->addDefault('Content-Disposition', "attachment; filename=\"$attachmentName\"");
+
+ $this->send();
+
+ fseek($handle, $begin);
+ set_time_limit(0); // Reset time limit for big files
+ $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);
+ }
- /* Check the range and make sure it's treated according to the specs.
- * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
- */
- // End bytes can not be larger than $end.
- $contentEnd = ($contentEnd > $fileSize) ? $fileSize - 1 : $contentEnd;
-
- // Validate the requested range and return an error if it's not correct.
- $wrongContentStart = ($contentStart > $contentEnd || $contentStart > $fileSize - 1 || $contentStart < 0);
-
- if ($wrongContentStart) {
- header("Content-Range: bytes $contentStart-$contentEnd/$fileSize");
- throw new HttpException(416, 'Requested Range Not Satisfiable');
+ /**
+ * 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;
}
-
- header('HTTP/1.1 206 Partial Content');
- header("Content-Range: bytes $contentStart-$contentEnd/$fileSize");
} else {
- header('HTTP/1.1 200 OK');
+ $start = $matches[1];
+ $end = $fileSize - 1;
}
-
- $length = $contentEnd - $contentStart + 1; // Calculate new content length
-
- 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);
+ if ($start < 0 || $start > $end) {
+ return false;
} else {
- echo $content;
+ return array($start, $end);
}
}
@@ -305,86 +424,58 @@ class Response extends \yii\base\Response
* 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:
- *
- * - Apache: {@link http://tn123.org/mod_xsendfile X-Sendfile}
- * - Lighttpd v1.4: {@link http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file X-LIGHTTPD-send-file}
- * - Lighttpd v1.5: {@link http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file X-Sendfile}
- * - Nginx: {@link http://wiki.nginx.org/XSendfile X-Accel-Redirect}
- * - Cherokee: {@link http://www.cherokee-project.com/doc/other_goodies.html#x-sendfile X-Sendfile and X-Accel-Redirect}
- *
+ *
+ * - Apache: [X-Sendfile](http://tn123.org/mod_xsendfile)
+ * - Lighttpd v1.4: [X-LIGHTTPD-send-file](http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file)
+ * - Lighttpd v1.5: [X-Sendfile](http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file)
+ * - Nginx: [X-Accel-Redirect](http://wiki.nginx.org/XSendfile)
+ * - Cherokee: [X-Sendfile and X-Accel-Redirect](http://www.cherokee-project.com/doc/other_goodies.html#x-sendfile)
+ *
* 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.
*
- * 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
+ * **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`.
*
- * Side effects:
+ * **Side effects**
+ *
* 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.
*
- * Known issues:
+ * **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
- * 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.".
- * You can work around this problem by removing the Pragma
-header.
+ * 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.". You can work around this problem by removing the `Pragma`-header.
+ *
+ * **Example**
+ *
+ * ~~~
+ * Yii::app()->request->xSendFile('/home/user/Pictures/picture1.jpg');
+ * ~~~
*
- * Example:
- *
- * request->xSendFile('/home/user/Pictures/picture1.jpg', array(
- * 'saveName' => 'image1.jpg',
- * 'mimeType' => 'image/jpeg',
- * 'terminate' => false,
- * ));
- * ?>
- *
* @param string $filePath file name with full path
- * @param array $options additional options:
- *
- * - saveName: file name shown to the user, if not set real file name will be used
- * - 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.
- * - xHeader: appropriate x-sendfile header, defaults to "X-Sendfile"
- * - terminate: whether to terminate the current application after calling this method, defaults to true
- * - forceDownload: specifies whether the file will be downloaded or shown inline, defaults to true
- * - addHeaders: an array of additional http headers in header-value pairs
- *
- * @todo
- */
- public function xSendFile($filePath, $options = array())
+ * @param string $mimeType the MIME type of the file. If null, it will be determined based on `$filePath`.
+ * @param string $attachmentName file name shown to the user. If null, it will be determined from `$filePath`.
+ * @param string $xHeader the name of the x-sendfile header.
+ */
+ public function xSendFile($filePath, $attachmentName = null, $mimeType = null, $xHeader = 'X-Sendfile')
{
- if (!isset($options['forceDownload']) || $options['forceDownload']) {
- $disposition = 'attachment';
- } 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 ($mimeType === null && ($mimeType = FileHelper::getMimeTypeByExtension($filePath)) === null) {
+ $mimeType = 'application/octet-stream';
}
-
- if (!isset($options['xHeader'])) {
- $options['xHeader'] = 'X-Sendfile';
+ if ($attachmentName === null) {
+ $attachmentName = basename($filePath);
}
- if ($options['mimeType'] !== null) {
- 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);
+ $this->getHeaders()
+ ->addDefault($xHeader, $filePath)
+ ->addDefault('Content-Type', $mimeType)
+ ->addDefault('Content-Disposition', "attachment; filename=\"$attachmentName\"");
- if (!isset($options['terminate']) || $options['terminate']) {
- Yii::$app->end();
- }
+ $this->send();
}
/**
@@ -422,7 +513,8 @@ class Response extends \yii\base\Response
if (Yii::$app->getRequest()->getIsAjax()) {
$statusCode = $this->ajaxRedirectCode;
}
- header('Location: ' . $url, true, $statusCode);
+ $this->getHeaders()->set('Location', $url);
+ $this->setStatusCode($statusCode);
if ($terminate) {
Yii::$app->end();
}
@@ -441,6 +533,8 @@ class Response extends \yii\base\Response
$this->redirect(Yii::$app->getRequest()->getUrl() . $anchor, $terminate);
}
+ private $_cookies;
+
/**
* Returns the cookie collection.
* Through the returned cookie collection, you add or remove cookies as follows,
@@ -462,13 +556,16 @@ class Response extends \yii\base\Response
*/
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]].
*/
- public function isInvalid()
+ public function getIsInvalid()
{
return $this->getStatusCode() < 100 || $this->getStatusCode() >= 600;
}
@@ -476,15 +573,15 @@ class Response extends \yii\base\Response
/**
* @return boolean whether this response is informational
*/
- public function isInformational()
+ public function getIsInformational()
{
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;
}
@@ -492,7 +589,7 @@ class Response extends \yii\base\Response
/**
* @return boolean whether this response is a redirection
*/
- public function isRedirection()
+ public function getIsRedirection()
{
return $this->getStatusCode() >= 300 && $this->getStatusCode() < 400;
}
@@ -500,7 +597,7 @@ class Response extends \yii\base\Response
/**
* @return boolean whether this response indicates a client error
*/
- public function isClientError()
+ public function getIsClientError()
{
return $this->getStatusCode() >= 400 && $this->getStatusCode() < 500;
}
@@ -508,7 +605,7 @@ class Response extends \yii\base\Response
/**
* @return boolean whether this response indicates a server error
*/
- public function isServerError()
+ public function getIsServerError()
{
return $this->getStatusCode() >= 500 && $this->getStatusCode() < 600;
}
@@ -516,7 +613,7 @@ class Response extends \yii\base\Response
/**
* @return boolean whether this response is OK
*/
- public function isOk()
+ public function getIsOk()
{
return 200 === $this->getStatusCode();
}
@@ -524,7 +621,7 @@ class Response extends \yii\base\Response
/**
* @return boolean whether this response indicates the current request is forbidden
*/
- public function isForbidden()
+ public function getIsForbidden()
{
return 403 === $this->getStatusCode();
}
@@ -532,7 +629,7 @@ class Response extends \yii\base\Response
/**
* @return boolean whether this response indicates the currently requested resource is not found
*/
- public function isNotFound()
+ public function getIsNotFound()
{
return 404 === $this->getStatusCode();
}
@@ -540,7 +637,7 @@ class Response extends \yii\base\Response
/**
* @return boolean whether this response is empty
*/
- public function isEmpty()
+ public function getIsEmpty()
{
return in_array($this->getStatusCode(), array(201, 204, 304));
}
diff --git a/framework/yii/web/Session.php b/framework/yii/web/Session.php
index 1b48433..cf1fa21 100644
--- a/framework/yii/web/Session.php
+++ b/framework/yii/web/Session.php
@@ -63,7 +63,7 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co
* @var array parameter-value pairs to override default session cookie parameters
*/
public $cookieParams = array(
- 'httponly' => true
+ 'httpOnly' => true
);
/**
@@ -241,26 +241,31 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co
*/
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.
* The effect of this method only lasts for the duration of the script.
* 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.
* @see http://us2.php.net/manual/en/function.session-set-cookie-params.php
*/
public function setCookieParams($value)
{
- $data = session_get_cookie_params();
+ $data = $this->getCookieParams();
extract($data);
extract($value);
- if (isset($lifetime, $path, $domain, $secure, $httponly)) {
- session_set_cookie_params($lifetime, $path, $domain, $secure, $httponly);
+ if (isset($lifetime, $path, $domain, $secure, $httpOnly)) {
+ session_set_cookie_params($lifetime, $path, $domain, $secure, $httpOnly);
} 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.');
}
}
diff --git a/framework/yii/web/User.php b/framework/yii/web/User.php
index 005f987..f273c1a 100644
--- a/framework/yii/web/User.php
+++ b/framework/yii/web/User.php
@@ -9,7 +9,7 @@ namespace yii\web;
use Yii;
use yii\base\Component;
-use yii\base\HttpException;
+use yii\web\HttpException;
use yii\base\InvalidConfigException;
/**
@@ -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.
* @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
* remains inactive. If this property is not set, the user will be logged out after
diff --git a/framework/yii/web/VerbFilter.php b/framework/yii/web/VerbFilter.php
index ca6d47d..a3fd662 100644
--- a/framework/yii/web/VerbFilter.php
+++ b/framework/yii/web/VerbFilter.php
@@ -10,7 +10,7 @@ namespace yii\web;
use Yii;
use yii\base\ActionEvent;
use yii\base\Behavior;
-use yii\base\HttpException;
+use yii\web\HttpException;
/**
* VerbFilter is an action filter that filters by HTTP request methods.
@@ -70,7 +70,7 @@ class VerbFilter extends Behavior
/**
* @param ActionEvent $event
* @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)
{
@@ -81,7 +81,7 @@ class VerbFilter extends Behavior
if (!in_array($verb, $allowed)) {
$event->isValid = false;
// 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));
}
}
diff --git a/tests/unit/data/mysql.sql b/tests/unit/data/mysql.sql
index 1bb5558..2e9458e 100644
--- a/tests/unit/data/mysql.sql
+++ b/tests/unit/data/mysql.sql
@@ -9,6 +9,14 @@ DROP TABLE IF EXISTS tbl_order CASCADE;
DROP TABLE IF EXISTS tbl_category CASCADE;
DROP TABLE IF EXISTS tbl_customer 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` (
`id` int(11) NOT NULL AUTO_INCREMENT,
diff --git a/tests/unit/data/postgres.sql b/tests/unit/data/postgres.sql
index 52fad0f..f8fb0eb 100644
--- a/tests/unit/data/postgres.sql
+++ b/tests/unit/data/postgres.sql
@@ -10,6 +10,13 @@ DROP TABLE IF EXISTS tbl_order CASCADE;
DROP TABLE IF EXISTS tbl_category CASCADE;
DROP TABLE IF EXISTS tbl_customer 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 (
id serial not null primary key,
diff --git a/tests/unit/data/web/data.txt b/tests/unit/data/web/data.txt
new file mode 100644
index 0000000..8e58281
--- /dev/null
+++ b/tests/unit/data/web/data.txt
@@ -0,0 +1 @@
+12ёжик3456798áèabcdefghijklmnopqrstuvwxyz!"§$%&/(ёжик)=?
\ No newline at end of file
diff --git a/tests/unit/framework/db/QueryBuilderTest.php b/tests/unit/framework/db/QueryBuilderTest.php
index 7dc4731..869b501 100644
--- a/tests/unit/framework/db/QueryBuilderTest.php
+++ b/tests/unit/framework/db/QueryBuilderTest.php
@@ -7,23 +7,26 @@ use yii\db\Schema;
use yii\db\mysql\QueryBuilder as MysqlQueryBuilder;
use yii\db\sqlite\QueryBuilder as SqliteQueryBuilder;
use yii\db\mssql\QueryBuilder as MssqlQueryBuilder;
+use yii\db\pgsql\QueryBuilder as PgsqlQueryBuilder;
class QueryBuilderTest extends DatabaseTestCase
{
+
/**
* @throws \Exception
* @return QueryBuilder
*/
protected function getQueryBuilder()
{
- switch($this->driverName)
- {
+ switch ($this->driverName) {
case 'mysql':
return new MysqlQueryBuilder($this->getConnection());
case 'sqlite':
return new SqliteQueryBuilder($this->getConnection());
case 'mssql':
return new MssqlQueryBuilder($this->getConnection());
+ case 'pgsql':
+ return new PgsqlQueryBuilder($this->getConnection());
}
throw new \Exception('Test is not implemented for ' . $this->driverName);
}
@@ -95,15 +98,31 @@ class QueryBuilderTest extends DatabaseTestCase
);
}
- /**
- *
- */
public function testGetColumnType()
{
$qb = $this->getQueryBuilder();
- foreach($this->columnTypes() as $item) {
+ foreach ($this->columnTypes() as $item) {
list ($column, $expected) = $item;
$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));
+ }
+
}
diff --git a/tests/unit/framework/db/pgsql/PostgreSQLQueryBuilderTest.php b/tests/unit/framework/db/pgsql/PostgreSQLQueryBuilderTest.php
new file mode 100644
index 0000000..9c5d1e1
--- /dev/null
+++ b/tests/unit/framework/db/pgsql/PostgreSQLQueryBuilderTest.php
@@ -0,0 +1,76 @@
+ 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
diff --git a/tests/unit/framework/db/sqlite/SqliteQueryBuilderTest.php b/tests/unit/framework/db/sqlite/SqliteQueryBuilderTest.php
index c36628f..8e769af 100644
--- a/tests/unit/framework/db/sqlite/SqliteQueryBuilderTest.php
+++ b/tests/unit/framework/db/sqlite/SqliteQueryBuilderTest.php
@@ -2,6 +2,7 @@
namespace yiiunit\framework\db\sqlite;
+use yii\base\NotSupportedException;
use yii\db\sqlite\Schema;
use yiiunit\framework\db\QueryBuilderTest;
@@ -71,4 +72,11 @@ class SqliteQueryBuilderTest extends QueryBuilderTest
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
diff --git a/tests/unit/framework/helpers/FileHelperTest.php b/tests/unit/framework/helpers/FileHelperTest.php
new file mode 100644
index 0000000..8c812b1
--- /dev/null
+++ b/tests/unit/framework/helpers/FileHelperTest.php
@@ -0,0 +1,319 @@
+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 = ' $mimeType) {
+ $fileName = 'test.' . $extension;
+ $this->assertNull(FileHelper::getMimeTypeByExtension($fileName));
+ $this->assertEquals($mimeType, FileHelper::getMimeTypeByExtension($fileName, $magicFile));
+ }
+ }
+}
diff --git a/tests/unit/framework/web/ResponseTest.php b/tests/unit/framework/web/ResponseTest.php
index 5da9b8c..f35dda4 100644
--- a/tests/unit/framework/web/ResponseTest.php
+++ b/tests/unit/framework/web/ResponseTest.php
@@ -1,45 +1,30 @@
mockApplication();
- $this->reset();
- }
-
- protected function reset()
- {
- static::$headers = array();
- static::$httpResponseCode = 200;
+ $this->response = new Response;
}
public function rightRanges()
@@ -56,18 +41,22 @@ class ResponseTest extends \yiiunit\TestCase
/**
* @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;
- $sent = $this->runSendFile('testFile.txt', $content, null);
-
- $this->assertEquals($expectedFile, $sent);
- $this->assertTrue(in_array('HTTP/1.1 206 Partial Content', static::$headers));
- $this->assertTrue(in_array('Accept-Ranges: bytes', static::$headers));
- $this->assertArrayHasKey('Content-Range: bytes ' . $expectedHeader . '/' . StringHelper::strlen($content), array_flip(static::$headers));
- $this->assertTrue(in_array('Content-Type: text/plain', static::$headers));
- $this->assertTrue(in_array('Content-Length: ' . $length, static::$headers));
+ ob_start();
+ $this->response->sendFile($dataFile);
+ $content = ob_get_clean();
+
+ $this->assertEquals($expectedContent, $content);
+ $this->assertEquals(206, $this->response->statusCode);
+ $headers = $this->response->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()
@@ -87,25 +76,15 @@ class ResponseTest extends \yiiunit\TestCase
*/
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;
- $this->runSendFile('testFile.txt', $content, null);
+ $this->response->sendFile($dataFile);
}
protected function generateTestFileContent()
{
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;
- }
}