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; <?php - if ($exception instanceof \yii\base\HttpException) { + if ($exception instanceof \yii\web\HttpException) { echo (int) $exception->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{ <?php else: ?> <img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAA6CAIAAACPssguAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjcwRURBOUQxQzQ3RDExRTJCRjVFODJDQkFGODFDN0VBIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjcwRURBOUQyQzQ3RDExRTJCRjVFODJDQkFGODFDN0VBIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NzBFREE5Q0ZDNDdEMTFFMkJGNUU4MkNCQUY4MUM3RUEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NzBFREE5RDBDNDdEMTFFMkJGNUU4MkNCQUY4MUM3RUEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4L6mYsAAAE9klEQVR42tRa227aTBDGiwkhJtD8EanSQ6RKqKpSBbWqql5U7QP0TfsSvWhvStMEO6cqDUpjRylSQ1QC+IQN/mdrhBIb413vgsheIDDr9XwzOzPfzFrwPC/Fe8Cauiybe3uersNPQZJylYr04oUgCNyfJXJfcTAYXH/5YoH0qjq66LbbTrtdfPcOITTXALDuf/40FSWlabeuq6rpeZkHD6Ryma8dOOvDdV19Zycg/XBoml6r9ft9vk9EnNV/dOT8+RM1oXdxof/4wdfrEF/1G7Va+uIiagL8BfaBafMIAHy38/17/+/fydNgAkyDyXMHwLEsS1EmqH9kBOvgACbPFwDQaPfr11S9TjT7+JijEfgAsFst8/CQYv7BQe/6el4AQGTsfPuGbqSt2CGcnrarVS4hlRUAxETz9+/eyQntjXAL3MgeUhG7+vXtbSr1Dx+sqoYss3sCYlS/cXLinJ8nu713emqoKqMRmAD4xAElBQA3GpA62DwBsagfiIN7ecnyeHZyIbKoP4o46KurY2+Rrq7CeQ08YenZs0wmM1MAI+KQDv3VKpd7r1+L2WwQsGH0q9VCyN3dZrOrKPdevUpGsxMCmEAcMqVSaWNjcXExcN00zRbQ7BAAWMSs1ZY2N7NLSzMCMJk4ZBFaXl4OAxBF0Yza68fH3Z2dzNu3Ceo1lMB3raurScQhgqjBDhGiibS1v98zzVlEIax+CJ0TM9fYqAIXB9HRBsiFXq0myGuIVv2xxGGQNCaCVcG2tCGVDkBi4jBU80ThYFlwLVojICr1kxAHFLHRsWoNgzu5oABASBw8247ygVScZJhcyDIVuUDk6tf39oiIg+exUAOwMNiZfAVSAI7jGPv7sSUvhwLl/Jyqc0EEwA+dsR2H+EFWy4OdwdqERiACQNhxGIaaaCcWyDY3Zni7u2BzPgDoOg64Yo92YvJ43e2CzUlCKmIlDsSZ2FcG4Qq4faQoJO0jRKJ+uszFq+tWr5PkNRRPHM7O2IWBpdxGYxrkAk2DODihbAqKdJpNkRIDJhdxniDGEgdavgpSWp8+XX7+HADg9vsJCnDcPnr+XHr0KKpeE6fScdA0l1P7AIwAu2BxfR3qIYotREEcpj8mkwsxkjjs7iYmDuRdCXJykSuXx3YuxEji0O2mE0lvlEpWpZK6vWsxv2s0BCjeE1kVk4ujo2KlEvYEkZE4jMlBCK1sbUFdf7NCx22YlRVHURKuCeSiVpM2N8NGEFmJwzgAIH2hUAhYAD6vGQ6J/bOpe2/eBDoXiJ04jNm1CAVsDT8Zj7ijyAViJQ6zHPV6Z3s7kNfQrcylqlCVpuZ42IeHgbMpdJM4QD2auFc+mxE+m0JUHQfSqHd2Fsg7mMxxWjxwNiX43yBzNT9+9GSZj6I2NsKZH5e5Y9+hSODQ79+vfvjgP0K8SRzSvCw9jgvxJBe/fpmaln/yBAe3WXYcOHYuRmdTCFctmsah4zBjhtdo+D08BGHVhs1zd9Q/MgKIDcJjCwzyefv+/dRdG86/zItzvri+7knS3ZLefPgQPX7sMxSUy+W8ly+NtbW7Ir2+tjaoVHL5fDqdFgFENpstlMtQ/bdlOc3pJZLpjX6x6G1t/ff0KegdH1v96yV7EEk7nU6r1bJtm+PrVPx9FyFQd7FYBMa+sLAwBODnMgirAAM+p/EqLDcuJAiwbaCsgU+fsf8vwADjle3ME1OGEwAAAABJRU5ErkJggg==" alt="Attention"/> <h1><?php - if ($exception instanceof \yii\base\HttpException) { + if ($exception instanceof \yii\web\HttpException) { echo '<span>' . $context->createHttpStatusLink($exception->statusCode, $context->htmlEncode($exception->getName())) . '</span>'; echo ' – ' . $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 <qiang.xue@gmail.com> * @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 <qiang.xue@gmail.com> @@ -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: - * <ul> - * <li>Apache: {@link http://tn123.org/mod_xsendfile X-Sendfile}</li> - * <li>Lighttpd v1.4: {@link http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file X-LIGHTTPD-send-file}</li> - * <li>Lighttpd v1.5: {@link http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file X-Sendfile}</li> - * <li>Nginx: {@link http://wiki.nginx.org/XSendfile X-Accel-Redirect}</li> - * <li>Cherokee: {@link http://www.cherokee-project.com/doc/other_goodies.html#x-sendfile X-Sendfile and X-Accel-Redirect}</li> - * </ul> + * + * - 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. * - * <b>Note:</b> - * 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`. * - * <b>Side effects</b>: + * **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. * - * <b>Known issues</b>: + * **Known issues** + * * There is a Bug with Internet Explorer 6, 7 and 8 when X-SENDFILE is used over an SSL connection, it will show - * 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 <code>Pragma</code>-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'); + * ~~~ * - * <b>Example</b>: - * <pre> - * <?php - * Yii::app()->request->xSendFile('/home/user/Pictures/picture1.jpg', array( - * 'saveName' => 'image1.jpg', - * 'mimeType' => 'image/jpeg', - * 'terminate' => false, - * )); - * ?> - * </pre> * @param string $filePath file name with full path - * @param array $options additional options: - * <ul> - * <li>saveName: file name shown to the user, if not set real file name will be used</li> - * <li>mimeType: mime type of the file, if not set it will be guessed automatically based on the file name, if set to null no content-type header will be sent.</li> - * <li>xHeader: appropriate x-sendfile header, defaults to "X-Sendfile"</li> - * <li>terminate: whether to terminate the current application after calling this method, defaults to true</li> - * <li>forceDownload: specifies whether the file will be downloaded or shown inline, defaults to true</li> - * <li>addHeaders: an array of additional http headers in header-value pairs</li> - * </ul> - * @todo - */ - public function xSendFile($filePath, $options = array()) + * @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 @@ +<?php + +namespace yiiunit\framework\db\pgsql; + +use yii\base\NotSupportedException; +use yii\db\pgsql\Schema; +use yiiunit\framework\db\QueryBuilderTest; + +class PostgreSQLQueryBuilderTest extends QueryBuilderTest +{ + + public $driverName = 'pgsql'; + + public function columnTypes() + { + return array( + array(Schema::TYPE_PK, 'serial not null primary key'), + array(Schema::TYPE_PK . '(8)', 'serial not null primary key'), + array(Schema::TYPE_PK . ' CHECK (value > 5)', 'serial not null primary key CHECK (value > 5)'), + array(Schema::TYPE_PK . '(8) CHECK (value > 5)', 'serial not null primary key CHECK (value > 5)'), + array(Schema::TYPE_STRING, 'varchar(255)'), + array(Schema::TYPE_STRING . '(32)', 'varchar(32)'), + array(Schema::TYPE_STRING . ' CHECK (value LIKE "test%")', 'varchar(255) CHECK (value LIKE "test%")'), + array(Schema::TYPE_STRING . '(32) CHECK (value LIKE "test%")', 'varchar(32) CHECK (value LIKE "test%")'), + array(Schema::TYPE_STRING . ' NOT NULL', 'varchar(255) NOT NULL'), + array(Schema::TYPE_TEXT, 'text'), + array(Schema::TYPE_TEXT . '(255)', 'text'), + array(Schema::TYPE_TEXT . ' CHECK (value LIKE "test%")', 'text CHECK (value LIKE "test%")'), + array(Schema::TYPE_TEXT . '(255) CHECK (value LIKE "test%")', 'text CHECK (value LIKE "test%")'), + array(Schema::TYPE_TEXT . ' NOT NULL', 'text NOT NULL'), + array(Schema::TYPE_TEXT . '(255) NOT NULL', 'text NOT NULL'), + array(Schema::TYPE_SMALLINT, 'smallint'), + array(Schema::TYPE_SMALLINT . '(8)', 'smallint'), + array(Schema::TYPE_INTEGER, 'integer'), + array(Schema::TYPE_INTEGER . '(8)', 'integer'), + array(Schema::TYPE_INTEGER . ' CHECK (value > 5)', 'integer CHECK (value > 5)'), + array(Schema::TYPE_INTEGER . '(8) CHECK (value > 5)', 'integer CHECK (value > 5)'), + array(Schema::TYPE_INTEGER . ' NOT NULL', 'integer NOT NULL'), + array(Schema::TYPE_BIGINT, 'bigint'), + array(Schema::TYPE_BIGINT . '(8)', 'bigint'), + array(Schema::TYPE_BIGINT . ' CHECK (value > 5)', 'bigint CHECK (value > 5)'), + array(Schema::TYPE_BIGINT . '(8) CHECK (value > 5)', 'bigint CHECK (value > 5)'), + array(Schema::TYPE_BIGINT . ' NOT NULL', 'bigint NOT NULL'), + array(Schema::TYPE_FLOAT, 'double precision'), + array(Schema::TYPE_FLOAT . ' CHECK (value > 5.6)', 'double precision CHECK (value > 5.6)'), + array(Schema::TYPE_FLOAT . '(16,5) CHECK (value > 5.6)', 'double precision CHECK (value > 5.6)'), + array(Schema::TYPE_FLOAT . ' NOT NULL', 'double precision NOT NULL'), + array(Schema::TYPE_DECIMAL, 'numeric(10,0)'), + array(Schema::TYPE_DECIMAL . '(12,4)', 'numeric(12,4)'), + array(Schema::TYPE_DECIMAL . ' CHECK (value > 5.6)', 'numeric(10,0) CHECK (value > 5.6)'), + array(Schema::TYPE_DECIMAL . '(12,4) CHECK (value > 5.6)', 'numeric(12,4) CHECK (value > 5.6)'), + array(Schema::TYPE_DECIMAL . ' NOT NULL', 'numeric(10,0) NOT NULL'), + array(Schema::TYPE_DATETIME, 'timestamp'), + array(Schema::TYPE_DATETIME . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "timestamp CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"), + array(Schema::TYPE_DATETIME . ' NOT NULL', 'timestamp NOT NULL'), + array(Schema::TYPE_TIMESTAMP, 'timestamp'), + array(Schema::TYPE_TIMESTAMP . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "timestamp CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"), + array(Schema::TYPE_TIMESTAMP . ' NOT NULL', 'timestamp NOT NULL'), + array(Schema::TYPE_TIME, 'time'), + array(Schema::TYPE_TIME . " CHECK(value BETWEEN '12:00:00' AND '13:01:01')", "time CHECK(value BETWEEN '12:00:00' AND '13:01:01')"), + array(Schema::TYPE_TIME . ' NOT NULL', 'time NOT NULL'), + array(Schema::TYPE_DATE, 'date'), + array(Schema::TYPE_DATE . " CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')", "date CHECK(value BETWEEN '2011-01-01' AND '2013-01-01')"), + array(Schema::TYPE_DATE . ' NOT NULL', 'date NOT NULL'), + array(Schema::TYPE_BINARY, 'bytea'), + array(Schema::TYPE_BOOLEAN, 'boolean'), + array(Schema::TYPE_BOOLEAN . ' NOT NULL DEFAULT 1', 'boolean NOT NULL DEFAULT 1'), + array(Schema::TYPE_MONEY, 'numeric(19,4)'), + array(Schema::TYPE_MONEY . '(16,2)', 'numeric(16,2)'), + array(Schema::TYPE_MONEY . ' CHECK (value > 0.0)', 'numeric(19,4) CHECK (value > 0.0)'), + array(Schema::TYPE_MONEY . '(16,2) CHECK (value > 0.0)', 'numeric(16,2) CHECK (value > 0.0)'), + array(Schema::TYPE_MONEY . ' NOT NULL', 'numeric(19,4) NOT NULL'), + ); + } + +} \ No newline at end of file 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 @@ +<?php + +use yii\helpers\base\FileHelper; +use yii\test\TestCase; + +/** + * Unit test for [[yii\helpers\base\FileHelper]] + * @see FileHelper + */ +class FileHelperTest extends TestCase +{ + /** + * @var string test files path. + */ + private $testFilePath = ''; + + public function setUp() + { + $this->testFilePath = Yii::getAlias('@yiiunit/runtime') . DIRECTORY_SEPARATOR . get_class($this); + $this->createDir($this->testFilePath); + if (!file_exists($this->testFilePath)) { + $this->markTestIncomplete('Unit tests runtime directory should have writable permissions!'); + } + } + + public function tearDown() + { + $this->removeDir($this->testFilePath); + } + + /** + * Creates directory. + * @param string $dirName directory full name. + */ + protected function createDir($dirName) + { + if (!file_exists($dirName)) { + mkdir($dirName, 0777, true); + } + } + + /** + * Removes directory. + * @param string $dirName directory full name. + */ + protected function removeDir($dirName) + { + if (!empty($dirName) && is_dir($dirName)) { + if ($handle = opendir($dirName)) { + while (false !== ($entry = readdir($handle))) { + if ($entry != '.' && $entry != '..') { + if (is_dir($dirName . DIRECTORY_SEPARATOR . $entry) === true) { + $this->removeDir($dirName . DIRECTORY_SEPARATOR . $entry); + } else { + unlink($dirName . DIRECTORY_SEPARATOR . $entry); + } + } + } + closedir($handle); + rmdir($dirName); + } + } + } + + /** + * Get file permission mode. + * @param string $file file name. + * @return string permission mode. + */ + protected function getMode($file) + { + return substr(sprintf('%o', fileperms($file)), -4); + } + + /** + * Creates test files structure, + * @param array $items file system objects to be created in format: objectName => objectContent + * Arrays specifies directories, other values - files. + * @param string $basePath structure base file path. + */ + protected function createFileStructure(array $items, $basePath = '') + { + if (empty($basePath)) { + $basePath = $this->testFilePath; + } + foreach ($items as $name => $content) { + $itemName = $basePath . DIRECTORY_SEPARATOR . $name; + if (is_array($content)) { + mkdir($itemName, 0777, true); + $this->createFileStructure($content, $itemName); + } else { + file_put_contents($itemName, $content); + } + } + } + + /** + * Asserts that file has specific permission mode. + * @param integer $expectedMode expected file permission mode. + * @param string $fileName file name. + * @param string $message error message + */ + protected function assertFileMode($expectedMode, $fileName, $message='') + { + $expectedMode = sprintf('%o', $expectedMode); + $this->assertEquals($expectedMode, $this->getMode($fileName), $message); + } + + // Tests : + + public function testCopyDirectory() + { + $srcDirName = 'test_src_dir'; + $files = array( + 'file1.txt' => 'file 1 content', + 'file2.txt' => 'file 2 content', + ); + $this->createFileStructure(array( + $srcDirName => $files + )); + + $basePath = $this->testFilePath; + $srcDirName = $basePath . DIRECTORY_SEPARATOR . $srcDirName; + $dstDirName = $basePath . DIRECTORY_SEPARATOR . 'test_dst_dir'; + + FileHelper::copyDirectory($srcDirName, $dstDirName); + + $this->assertTrue(file_exists($dstDirName), 'Destination directory does not exist!'); + foreach ($files as $name => $content) { + $fileName = $dstDirName . DIRECTORY_SEPARATOR . $name; + $this->assertTrue(file_exists($fileName), 'Directory file is missing!'); + $this->assertEquals($content, file_get_contents($fileName), 'Incorrect file content!'); + } + } + + /** + * @depends testCopyDirectory + */ + public function testCopyDirectoryPermissions() + { + if (substr(PHP_OS, 0, 3) == 'WIN') { + $this->markTestSkipped("Can't reliably test it on Windows because fileperms() always return 0777."); + } + + $srcDirName = 'test_src_dir'; + $subDirName = 'test_sub_dir'; + $fileName = 'test_file.txt'; + $this->createFileStructure(array( + $srcDirName => array( + $subDirName => array(), + $fileName => 'test file content', + ), + )); + + $basePath = $this->testFilePath; + $srcDirName = $basePath . DIRECTORY_SEPARATOR . $srcDirName; + $dstDirName = $basePath . DIRECTORY_SEPARATOR . 'test_dst_dir'; + + $dirMode = 0755; + $fileMode = 0755; + $options = array( + 'dirMode' => $dirMode, + 'fileMode' => $fileMode, + ); + FileHelper::copyDirectory($srcDirName, $dstDirName, $options); + + $this->assertFileMode($dirMode, $dstDirName, 'Destination directory has wrong mode!'); + $this->assertFileMode($dirMode, $dstDirName . DIRECTORY_SEPARATOR . $subDirName, 'Copied sub directory has wrong mode!'); + $this->assertFileMode($fileMode, $dstDirName . DIRECTORY_SEPARATOR . $fileName, 'Copied file has wrong mode!'); + } + + public function stestRemoveDirectory() + { + $dirName = 'test_dir_for_remove'; + $this->createFileStructure(array( + $dirName => array( + 'file1.txt' => 'file 1 content', + 'file2.txt' => 'file 2 content', + 'test_sub_dir' => array( + 'sub_dir_file_1.txt' => 'sub dir file 1 content', + 'sub_dir_file_2.txt' => 'sub dir file 2 content', + ), + ), + )); + + $basePath = $this->testFilePath; + $dirName = $basePath . DIRECTORY_SEPARATOR . $dirName; + + FileHelper::removeDirectory($dirName); + + $this->assertFalse(file_exists($dirName), 'Unable to remove directory!'); + } + + public function testFindFiles() + { + $dirName = 'test_dir'; + $this->createFileStructure(array( + $dirName => array( + 'file_1.txt' => 'file 1 content', + 'file_2.txt' => 'file 2 content', + 'test_sub_dir' => array( + 'file_1_1.txt' => 'sub dir file 1 content', + 'file_1_2.txt' => 'sub dir file 2 content', + ), + ), + )); + $basePath = $this->testFilePath; + $dirName = $basePath . DIRECTORY_SEPARATOR . $dirName; + $expectedFiles = array( + $dirName . DIRECTORY_SEPARATOR . 'file_1.txt', + $dirName . DIRECTORY_SEPARATOR . 'file_2.txt', + $dirName . DIRECTORY_SEPARATOR . 'test_sub_dir' . DIRECTORY_SEPARATOR . 'file_1_1.txt', + $dirName . DIRECTORY_SEPARATOR . 'test_sub_dir' . DIRECTORY_SEPARATOR . 'file_1_2.txt', + ); + + $foundFiles = FileHelper::findFiles($dirName); + sort($expectedFiles); + sort($foundFiles); + $this->assertEquals($expectedFiles, $foundFiles); + } + + /** + * @depends testFindFiles + */ + public function testFindFileFilter() + { + $dirName = 'test_dir'; + $passedFileName = 'passed.txt'; + $this->createFileStructure(array( + $dirName => array( + $passedFileName => 'passed file content', + 'declined.txt' => 'declined file content', + ), + )); + $basePath = $this->testFilePath; + $dirName = $basePath . DIRECTORY_SEPARATOR . $dirName; + + $options = array( + 'filter' => function($path) use ($passedFileName) { + return $passedFileName == basename($path); + } + ); + $foundFiles = FileHelper::findFiles($dirName, $options); + $this->assertEquals(array($dirName . DIRECTORY_SEPARATOR . $passedFileName), $foundFiles); + } + + /** + * @depends testFindFiles + */ + public function testFindFilesExclude() + { + $dirName = 'test_dir'; + $fileName = 'test_file.txt'; + $excludeFileName = 'exclude_file.txt'; + $this->createFileStructure(array( + $dirName => array( + $fileName => 'file content', + $excludeFileName => 'exclude file content', + ), + )); + $basePath = $this->testFilePath; + $dirName = $basePath . DIRECTORY_SEPARATOR . $dirName; + + $options = array( + 'except' => array($excludeFileName), + ); + $foundFiles = FileHelper::findFiles($dirName, $options); + $this->assertEquals(array($dirName . DIRECTORY_SEPARATOR . $fileName), $foundFiles); + } + + /** + * @depends testFindFiles + */ + public function testFindFilesFileType() + { + $dirName = 'test_dir'; + $fileType = 'dat'; + $fileName = 'test_file.' . $fileType; + $excludeFileName = 'exclude_file.txt'; + $this->createFileStructure(array( + $dirName => array( + $fileName => 'file content', + $excludeFileName => 'exclude file content', + ), + )); + $basePath = $this->testFilePath; + $dirName = $basePath . DIRECTORY_SEPARATOR . $dirName; + + $options = array( + 'fileTypes' => array($fileType), + ); + $foundFiles = FileHelper::findFiles($dirName, $options); + $this->assertEquals(array($dirName . DIRECTORY_SEPARATOR . $fileName), $foundFiles); + } + + public function testMkdir() { + $basePath = $this->testFilePath; + $dirName = $basePath . DIRECTORY_SEPARATOR . 'test_dir_level_1' . DIRECTORY_SEPARATOR . 'test_dir_level_2'; + FileHelper::mkdir($dirName); + $this->assertTrue(file_exists($dirName), 'Unable to create directory recursively!'); + } + + public function testGetMimeTypeByExtension() + { + $magicFile = $this->testFilePath . DIRECTORY_SEPARATOR . 'mime_type.php'; + $mimeTypeMap = array( + 'txa' => 'application/json', + 'txb' => 'another/mime', + ); + $magicFileContent = '<?php return ' . var_export($mimeTypeMap, true) . ';'; + file_put_contents($magicFile, $magicFileContent); + + foreach ($mimeTypeMap as $extension => $mimeType) { + $fileName = 'test.' . $extension; + $this->assertNull(FileHelper::getMimeTypeByExtension($fileName)); + $this->assertEquals($mimeType, FileHelper::getMimeTypeByExtension($fileName, $magicFile)); + } + } +} 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 @@ <?php -namespace yii\web; - -use yiiunit\framework\web\ResponseTest; +namespace yiiunit\framework\web; -/** - * Mock PHP header function to check for sent headers - * @param string $string - * @param bool $replace - * @param int $httpResponseCode - */ -function header($string, $replace = true, $httpResponseCode = null) { - ResponseTest::$headers[] = $string; - // TODO implement replace +use Yii; +use yii\helpers\StringHelper; - if ($httpResponseCode !== null) { - ResponseTest::$httpResponseCode = $httpResponseCode; +class Response extends \yii\web\Response +{ + public function send() + { + // does nothing to allow testing } } -namespace yiiunit\framework\web; - -use yii\helpers\StringHelper; -use yii\web\Response; - class ResponseTest extends \yiiunit\TestCase { - public static $headers = array(); - public static $httpResponseCode = 200; + /** + * @var Response + */ + public $response; protected function setUp() { parent::setUp(); $this->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; - } }