diff --git a/app/assets/.gitignore b/app/assets/.gitignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/app/assets/.gitignore @@ -0,0 +1 @@ +* diff --git a/app/index.php b/app/index.php new file mode 100644 index 0000000..8f98090 --- /dev/null +++ b/app/index.php @@ -0,0 +1,9 @@ +run(); diff --git a/app/protected/config/main.php b/app/protected/config/main.php new file mode 100644 index 0000000..e18ead8 --- /dev/null +++ b/app/protected/config/main.php @@ -0,0 +1,15 @@ + 'hello', + 'basePath' => dirname(__DIR__), + 'components' => array( + 'cache' => array( + 'class' => 'yii\caching\FileCache', + ), + 'user' => array( + 'class' => 'yii\web\User', + 'identityClass' => 'app\models\User', + ) + ), +); \ No newline at end of file diff --git a/app/protected/controllers/SiteController.php b/app/protected/controllers/SiteController.php new file mode 100644 index 0000000..58e9568 --- /dev/null +++ b/app/protected/controllers/SiteController.php @@ -0,0 +1,22 @@ +render('index'); + } + + public function actionLogin() + { + $user = app\models\User::findIdentity(100); + Yii::$app->getUser()->login($user); + Yii::$app->getResponse()->redirect(array('site/index')); + } + + public function actionLogout() + { + Yii::$app->getUser()->logout(); + Yii::$app->getResponse()->redirect(array('site/index')); + } +} \ No newline at end of file diff --git a/app/protected/models/User.php b/app/protected/models/User.php new file mode 100644 index 0000000..cebf1da --- /dev/null +++ b/app/protected/models/User.php @@ -0,0 +1,43 @@ + array( + 'id' => '100', + 'authKey' => 'test100key', + 'name' => 'admin', + ), + '101' => array( + 'id' => '101', + 'authKey' => 'test101key', + 'name' => 'demo', + ), + ); + + public static function findIdentity($id) + { + return isset(self::$users[$id]) ? new self(self::$users[$id]) : null; + } + + public function getId() + { + return $this->id; + } + + public function getAuthKey() + { + return $this->authKey; + } + + public function validateAuthKey($authKey) + { + return $this->authKey === $authKey; + } +} \ No newline at end of file diff --git a/app/protected/runtime/.gitignore b/app/protected/runtime/.gitignore new file mode 100644 index 0000000..72e8ffc --- /dev/null +++ b/app/protected/runtime/.gitignore @@ -0,0 +1 @@ +* diff --git a/app/protected/views/layouts/main.php b/app/protected/views/layouts/main.php new file mode 100644 index 0000000..092e665 --- /dev/null +++ b/app/protected/views/layouts/main.php @@ -0,0 +1,22 @@ + + + +beginPage(); ?> + + <?php echo Html::encode($this->title); ?> + head(); ?> + + +

Welcome

+beginBody(); ?> + +endBody(); ?> + +endPage(); ?> + diff --git a/app/protected/views/site/index.php b/app/protected/views/site/index.php new file mode 100644 index 0000000..3b83080 --- /dev/null +++ b/app/protected/views/site/index.php @@ -0,0 +1,17 @@ +title = 'Hello World'; + +$user = Yii::$app->getUser(); +if ($user->isGuest) { + echo Html::a('login', array('login')); +} else { + echo "You are logged in as " . $user->identity->name . "
"; + echo Html::a('logout', array('logout')); +} +?> + + diff --git a/build/.htaccess b/build/.htaccess new file mode 100644 index 0000000..e019832 --- /dev/null +++ b/build/.htaccess @@ -0,0 +1 @@ +deny from all diff --git a/build/build b/build/build new file mode 100755 index 0000000..fff4282 --- /dev/null +++ b/build/build @@ -0,0 +1,20 @@ +#!/usr/bin/env php +run(); diff --git a/build/build.bat b/build/build.bat new file mode 100644 index 0000000..a1ae41f --- /dev/null +++ b/build/build.bat @@ -0,0 +1,23 @@ +@echo off + +rem ------------------------------------------------------------- +rem build script for Windows. +rem +rem This is the bootstrap script for running build on Windows. +rem +rem @author Qiang Xue +rem @link http://www.yiiframework.com/ +rem @copyright 2008 Yii Software LLC +rem @license http://www.yiiframework.com/license/ +rem @version $Id$ +rem ------------------------------------------------------------- + +@setlocal + +set BUILD_PATH=%~dp0 + +if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe + +%PHP_COMMAND% "%BUILD_PATH%build" %* + +@endlocal \ No newline at end of file diff --git a/build/build.xml b/build/build.xml new file mode 100644 index 0000000..18a420d --- /dev/null +++ b/build/build.xml @@ -0,0 +1,276 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Building package ${pkgname}... + Copying files to build directory... + + + + + Changing file permissions... + + + + + + + + Generating source release file... + + + + + + + + + + + + + + + + + + + + + + + + Building documentation... + + Building Guide PDF... + + + + + + + Building Blog PDF... + + + + + + + Building API... + + + + + Generating doc release file... + + + + + + + + + + + + + + + + Building online API... + + + + Copying tutorials... + + + + + + + + Copying release text files... + + + + + + +Finished building Web files. +Please update yiisite/common/data/versions.php file with the following code: + + '1.1'=>array( + 'version'=>'${yii.version}', + 'revision'=>'${yii.revision}', + 'date'=>'${yii.date}', + 'latest'=>true, + ), + + + + + + Synchronizing code changes for ${pkgname}... + + Building autoload map... + + + Building yiilite.php... + + + + + Extracting i18n messages... + + + + + + + Cleaning up the build... + + + + + + + Welcome to use Yii build script! + -------------------------------- + You may use the following command format to build a target: + + phing <target name> + + where <target name> can be one of the following: + + - sync : synchronize yiilite.php and YiiBase.php + - message : extract i18n messages of the framework + - src : build source release + - doc : build documentation release (Windows only) + - clean : clean up the build + + + + diff --git a/build/controllers/LocaleController.php b/build/controllers/LocaleController.php new file mode 100644 index 0000000..d471c0d --- /dev/null +++ b/build/controllers/LocaleController.php @@ -0,0 +1,112 @@ + + * @since 2.0 + */ +class LocaleController extends Controller +{ + public $defaultAction = 'plural'; + + /** + * Generates the plural rules data. + * + * This command will parse the plural rule XML file from CLDR and convert them + * into appropriate PHP representation to support Yii message translation feature. + * @param string $xmlFile the original plural rule XML file (from CLDR). This file may be found in + * http://www.unicode.org/Public/cldr/latest/core.zip + * Extract the zip file and locate the file "common/supplemental/plurals.xml". + * @throws Exception + */ + public function actionPlural($xmlFile) + { + if (!is_file($xmlFile)) { + throw new Exception("The source plural rule file does not exist: $xmlFile"); + } + + $xml = simplexml_load_file($xmlFile); + + $allRules = array(); + + $patterns = array( + '/n in 0..1/' => '(n==0||n==1)', + '/\s+is\s+not\s+/i' => '!=', //is not + '/\s+is\s+/i' => '==', //is + '/n\s+mod\s+(\d+)/i' => 'fmod(n,$1)', //mod (CLDR's "mod" is "fmod()", not "%") + '/^(.*?)\s+not\s+in\s+(\d+)\.\.(\d+)/i' => '!in_array($1,range($2,$3))', //not in + '/^(.*?)\s+in\s+(\d+)\.\.(\d+)/i' => 'in_array($1,range($2,$3))', //in + '/^(.*?)\s+not\s+within\s+(\d+)\.\.(\d+)/i' => '($1<$2||$1>$3)', //not within + '/^(.*?)\s+within\s+(\d+)\.\.(\d+)/i' => '($1>=$2&&$1<=$3)', //within + ); + foreach ($xml->plurals->pluralRules as $node) { + $attributes = $node->attributes(); + $locales = explode(' ', $attributes['locales']); + $rules = array(); + + if (!empty($node->pluralRule)) { + foreach ($node->pluralRule as $rule) { + $expr_or = preg_split('/\s+or\s+/i', $rule); + foreach ($expr_or as $key_or => $val_or) { + $expr_and = preg_split('/\s+and\s+/i', $val_or); + $expr_and = preg_replace(array_keys($patterns), array_values($patterns), $expr_and); + $expr_or[$key_or] = implode('&&', $expr_and); + } + $expr = preg_replace('/\\bn\\b/', '$n', implode('||', $expr_or)); + $rules[] = preg_replace_callback('/range\((\d+),(\d+)\)/', function ($matches) { + if ($matches[2] - $matches[1] <= 5) { + return 'array(' . implode(',', range($matches[1], $matches[2])) . ')'; + } else { + return $matches[0]; + } + }, $expr); + + } + foreach ($locales as $locale) { + $allRules[$locale] = $rules; + } + } + } + // hard fix for "br": the rule is too complex + $allRules['br'] = array( + 0 => 'fmod($n,10)==1&&!in_array(fmod($n,100),array(11,71,91))', + 1 => 'fmod($n,10)==2&&!in_array(fmod($n,100),array(12,72,92))', + 2 => 'in_array(fmod($n,10),array(3,4,9))&&!in_array(fmod($n,100),array_merge(range(10,19),range(70,79),range(90,99)))', + 3 => 'fmod($n,1000000)==0&&$n!=0', + ); + if (preg_match('/\d+/', $xml->version['number'], $matches)) { + $revision = $matches[0]; + } else { + $revision = -1; + } + + echo "on('add', function($event) { +$post->on('update', function($event) { // send email notification }); ~~~ -In the above, we attach an anonymous function to the "add" event of the comment. -Valid event handlers include: +In the above, an anonymous function is attached to the "update" event of the post. You may attach +the following types of event handlers: - anonymous function: `function($event) { ... }` - object method: `array($object, 'handleAdd')` @@ -35,8 +37,8 @@ function foo($event) where `$event` is an [[Event]] object which includes parameters associated with the event. -You can also attach an event handler to an event when configuring a component with a configuration array. The syntax is -like the following: +You can also attach a handler to an event when configuring a component with a configuration array. +The syntax is like the following: ~~~ array( @@ -46,15 +48,13 @@ array( where `on add` stands for attaching an event to the `add` event. -You can call [[getEventHandlers()]] to retrieve all event handlers that are attached to a specified event. Because this -method returns a [[Vector]] object, we can manipulate this object to attach/detach event handlers, or adjust their -relative orders. +Sometimes, you may want to associate extra data with an event handler when you attach it to an event +and then access it when the handler is invoked. You may do so by ~~~ -$handlers = $comment->getEventHandlers('add'); -$handlers->insertAt(0, $callback); // attach a handler as the first one -$handlers[] = $callback; // attach a handler as the last one -unset($handlers[0]); // detach the first handler +$post->on('update', function($event) { + // the data can be accessed via $event->data +}, $data); ~~~ diff --git a/docs/api/base/Object.md b/docs/api/base/Object.md index a2cea6c..1b9fca0 100644 --- a/docs/api/base/Object.md +++ b/docs/api/base/Object.md @@ -1,3 +1,5 @@ +Object is the base class that implements the *property* feature. + A property is defined by a getter method (e.g. `getLabel`), and/or a setter method (e.g. `setLabel`). For example, the following getter and setter methods define a property named `label`: @@ -30,4 +32,30 @@ $object->label = 'abc'; If a property has only a getter method and has no setter method, it is considered as *read-only*. In this case, trying to modify the property value will cause an exception. -One can call [[hasProperty]], [[canGetProperty]] and/or [[canSetProperty]] to check the existence of a property. +One can call [[hasProperty()]], [[canGetProperty()]] and/or [[canSetProperty()]] to check the existence of a property. + + +Besides the property feature, Object also introduces an important object initialization life cycle. In particular, +creating an new instance of Object or its derived class will involve the following life cycles sequentially: + +1. the class constructor is invoked; +2. object properties are initialized according to the given configuration; +3. the `init()` method is invoked. + +In the above, both Step 2 and 3 occur at the end of the class constructor. It is recommended that +you perform object initialization in the `init()` method because at that stage, the object configuration +is already applied. + +In order to ensure the above life cycles, if a child class of Object needs to override the constructor, +it should be done like the following: + +~~~ +public function __construct($param1, $param2, ..., $config = array()) +{ + ... + parent::__construct($config); +} +~~~ + +That is, a `$config` parameter (defaults to `array()`) should be declared as the last parameter +of the constructor, and the parent implementation should be called at the end of the constructor. diff --git a/docs/code_style.md b/docs/code_style.md deleted file mode 100644 index 92a934b..0000000 --- a/docs/code_style.md +++ /dev/null @@ -1,328 +0,0 @@ -Yii2 code standard -================== - -This code standard is used for all the Yii2 core classes and can be applied to -your application in order to achieve consistency among your team. Also it will -help in case you want to opensource code. - -PHP file formatting -------------------- - -### General - -- Do not end file with `?>` if it contains PHP code only. -- Do not use ` 'Yii', - 'options' => array( - 'usePHP' => true, - ), -); -~~~ - -### Classes - -- Classes should be named using `CamelCase`. -- The brace should always be written on the line underneath the class name. -- Every class must have a documentation block that conforms to the PHPDoc. -- All code in a class must be indented with a single tab. -- There should be only one class in a single PHP file. -- All classes should be namespaced. -- Class name should match file name. Class namespace should match directory structure. - -~~~ -/** - * Documentation - */ -class MyClass extends \yii\Object implements MyInterface -{ - // code -} -~~~ - - -### Class members and variables - -- When declaring public class members specify `public` keyword explicitly. -- Variables should be declared at the top of the class before any method declarations. -- Private and protected variables should be named like `$_varName`. -- Public class members and standalone variables should be named using `$camelCase` - with first letter lowercase. -- Use descriptive names. Variables such as `$i` and `$j` are better not to be used. - -### Constants - -Both class level constants and global constants should be named in uppercase. Words -are separated by underscore. - -~~~ -class User { - const STATUS_ACTIVE = 1; - const STATUS_BANNED = 2; -} -~~~ - -It's preferable to define class level constants rather than global ones. - -### Functions and methods - -- Functions and methods should be named using `camelCase` with first letter lowercase. -- Name should be descriptive by itself indicating the purpose of the function. -- Class methods should always declare visibility using `private`, `protected` and - `public` modifiers. `var` is not allowed. -- Opening brace of a function should be on the line after the function declaration. - -~~~ -/** - * Documentation - */ -class Foo -{ - /** - * Documentation - */ - public function bar() - { - // code - return $value; - } -} -~~~ - -Use type hinting where possible: - -~~~ -public function __construct(CDbConnection $connection) -{ - $this->connection = $connection; -} -~~~ - -### Function and method calls - -~~~ -doIt(2, 3); - -doIt(array( - 'a' => 'b', -)); - -doIt('a', array( - 'a' => 'b', -)); -~~~ - -### Control statements - -- Control statement condition must have single space before and after parenthesis. -- Operators inside of parenthesis should be separated by spaces. -- Opening brace is on the same line. -- Closing brace is on a new line. -- Always use braces for single line statements. - -~~~ -if ($event === null) { - return new Event(); -} elseif ($event instanceof CoolEvent) { - return $event->instance(); -} else { - return null; -} - -// the following is NOT allowed: -if(!$model) - throw new Exception('test'); -~~~ - - -### Switch - -Use the following formatting for switch: - -~~~ -switch ($this->phpType) { - case 'string': - $a = (string)$value; - break; - case 'integer': - case 'int': - $a = (integer)$value; - break; - case 'boolean': - $a = (boolean)$value; - break; - default: - $a = null; -} -~~~ - -### Code documentation - -- Refer ot [phpDoc](http://phpdoc.org/) for documentation syntax. -- Code without documentation is not allowed. -- All class files must contain a "file-level" docblock at the top of each file - and a "class-level" docblock immediately above each class. -- There is no need to use `@return` if method does return nothing. - -#### File - -~~~ - - * @since 2.0 - */ -class Component extends \yii\base\Object -~~~ - - -#### Function / method - -~~~ -/** - * Returns the list of attached event handlers for an event. - * You may manipulate the returned [[Vector]] object by adding or removing handlers. - * For example, - * - * ~~~ - * $component->getEventHandlers($eventName)->insertAt(0, $eventHandler); - * ~~~ - * - * @param string $name the event name - * @return Vector list of attached event handlers for the event - * @throws Exception if the event is not defined - */ -public function getEventHandlers($name) -{ - if (!isset($this->_e[$name])) { - $this->_e[$name] = new Vector; - } - $this->ensureBehaviors(); - return $this->_e[$name]; -} -~~~ - -#### Comments - -- One-line comments should be started with `//` and not `#`. -- One-line comment should be on its own line. - -Yii application naming conventions ----------------------------------- - - - -Other library and framework standards -------------------------------------- - -It's good to be consistent with other frameworks and libraries whose components -will be possibly used with Yii2. That's why when there are no objective reasons -to use different style we should use one that's common among most of the popular -libraries and frameworks. - -That's not only about PHP but about JavaScript as well. Since we're using jQuery -a lot it's better to be consistent with its style as well. - -Application style consistency is much more important than consistency with other frameworks and libraries. - -- [Symfony 2](http://symfony.com/doc/current/contributing/code/standards.html) -- [Zend Framework 1](http://framework.zend.com/manual/en/coding-standard.coding-style.html) -- [Zend Framework 2](http://framework.zend.com/wiki/display/ZFDEV2/Coding+Standards) -- [Pear](http://pear.php.net/manual/en/standards.php) -- [jQuery](http://docs.jquery.com/JQuery_Core_Style_Guidelines) \ No newline at end of file diff --git a/docs/full_2011_11_12.png b/docs/full_2011_11_12.png deleted file mode 100644 index ef50fcb..0000000 Binary files a/docs/full_2011_11_12.png and /dev/null differ diff --git a/docs/hierarchy_2011_11_12.png b/docs/hierarchy_2011_11_12.png deleted file mode 100644 index b5f8abc..0000000 Binary files a/docs/hierarchy_2011_11_12.png and /dev/null differ diff --git a/docs/review_2011_11_12_alex.md b/docs/review_2011_11_12_alex.md deleted file mode 100644 index 38ffda2..0000000 --- a/docs/review_2011_11_12_alex.md +++ /dev/null @@ -1,192 +0,0 @@ -Alex's Code Review, 2011.11.12 -============================== - -Overall hierarchy ------------------- - -Generally is OK. Like that `Object` and `Component` are now separated. -I've generated 2 diagrams under `docs/` to see it better as a whole. - -> The purpose of separating `Object` from `Component` is to make `Object` -> a super-light base class that supports properties defined by getter/setters. -> Note that `Component` is a bit of heavy because it uses two extra member -> variables to support events and behaviors. - - -Object ------- - -### property feature - -Is it OK that `canGetProperty` and `canSetProperty` will return `false` for real -class members? - -> Added $checkVar parameter - -### callbacks and expressions - -We're using 5.3. What's the reason to support `eval()` in `evaluateExpression` if -we have anonymous functions? Is that for storing code as string inside of DB (RBAC)? - -If we're going to get rid of `eval()`, cosider remaning method to something about callback. -If not then we definitely need to use anonymous functions in API docs and the guide -where possible. - -> The purpose of evaluateExpression() is to provide a way of evaluating a PHP expression -> in the context of an object. Will remove it before release if we find no use of it. - ->> mdomba: ->> As eval() is controversial, and anonymous functions can replace all Yii 1 usage of eval() ->> how about removing it from the beginning and add it only if we find it necessary. ->> This way we would not be tempted to stick with eval() and will be forced to first try to find alternatives - -### Object::create() - -#### `__construct` issue - -Often a class doesn't have `__construct` implementation and `stdClass` doesn't have -default one either but Object::create() always expects constructor to be -defined. See `ObjectTest`. Either `method_exists` call or `Object::__construct` needed. - -> Added Object::__construct. - -#### How to support object factory like we do with CWidgetFactory? - -~~~ -class ObjectConfig -{ - public function configure($class) - { - $config = $this->load($class); - // apply config to $class - } - - private function load($class) - { - // get class properties from a config file - // in this method we need to walk all the - // inheritance hierarchy down to Object itself - return array( - 'property' => 'value', - // … - ); - } -} -~~~ - -Then we need to add `__construct` to `Object` (or implement `Initalbe`): - -~~~ -class Object -{ - public function __construct() - { - $conf = new ObjectConfig(); - $conf->configure($this); - } -} -~~~ - -This way we'll be able to set defaults for any object. - -> The key issue here is about how to process the config file. Clearly, we cannot -> do this for every type of component because it would mean an extra file access -> for every component type - -#### Do we need to support lazy class injection? - -Currently there's no way to lazy-inject class into another class property via -config. Do we need it? If yes then we can probably extend component config to support -the following: - -~~~ -class Foo extends Object -{ - public $prop; -} - -class Bar extends Object -{ - public $prop; -} - -$config = array( - 'prop' => array( - 'class' => 'Bar', - 'prop' => 'Hello!', - ), -); - -$foo = Foo::create($config); -echo $foo->bar->prop; -// will output Hello! -~~~ - -Should it support infinite nesting level? - -> I don't think we need this. Foo::$prop cannot be an object unless it needs it to be. -> In that case, it can be defined with a setter in which it can handle the object creation -> based on a configuration array. This is a bit inconvenient, but I think such usage is -> not very common. - -### Why `Event` is `Object`? - -There's no need to extend from `Object`. Is there a plan to use `Object` features -later? - -> To use properties defined via getter/setter. - - -Behaviors ---------- - -Overall I wasn't able to use behaviors. See `BehaviorTest`. - -### Should behaviors be able to define events for owner components? - -Why not? Should be a very good feature in order to make behaviors customizable. - -> It's a bit hard to implement it efficiently. I tend not to support it for now -> unless enough people are requesting for it. - -### Multiple behaviors can be attached to the same component - -What if we'll have multiple methods / properties / events with the same name? - -> The first one takes precedence. This is the same as we do in 1.1. - -### How to use Behavior::attach? - -Looks like it is used by `Component::attachBehavior` but can't be used without it. -Why it's public then? Can we move it to `Component?` - -> It's public because it is called by Component. It is in Behavior such that -> it can be overridden by behavior classes to customize the attach process. - -Events ------- - -Class itself looks OK. Component part is OK as well but I've not tested -it carefully. Overall it seems concept is the same as in Yii1. - -### Event declaration: the on-method is mostly repetitive for every event. Should we choose a different way of declaring events? - -Maybe. People complained previously about too many code for event declaration. - -### Should we implement some additional event mechanism, such as global events? - -Why use two different implementations in a single application? - -Exceptions ----------- - -- Should we convert all errors, warnings and notices to exceptions? - -> I think not. We used to do this in early versions of 1.0. We found sometimes -> very mysterious things would happen which makes error fixing harder rather than -> easier. - -Coding style ------------- - -See `docs/code_style.md`. \ No newline at end of file diff --git a/framework/YiiBase.php b/framework/YiiBase.php index 16e237d..9d501b1 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -7,6 +7,7 @@ use yii\base\Exception; use yii\base\InvalidConfigException; use yii\base\InvalidParamException; +use yii\base\UnknownClassException; use yii\logging\Logger; /** @@ -46,20 +47,18 @@ class YiiBase { /** * @var array class map used by the Yii autoloading mechanism. - * The array keys are the class names, and the array values are the corresponding class file paths. - * This property mainly affects how [[autoload]] works. + * The array keys are the class names (without leading backslashes), and the array values + * are the corresponding class file paths (or path aliases). This property mainly affects + * how [[autoload()]] works. * @see import * @see autoload */ public static $classMap = array(); /** - * @var array list of directories where Yii will search for new classes to be included. - * The first directory in the array will be searched first, and so on. - * This property mainly affects how [[autoload]] works. - * @see import - * @see autoload + * @var boolean whether to search PHP include_path when autoloading unknown classes. + * You may want to turn this off if you are also using autoloaders from other libraries. */ - public static $classPath = array(); + public static $enableIncludePath = true; /** * @var yii\console\Application|yii\web\Application the application instance */ @@ -106,108 +105,115 @@ class YiiBase } /** - * Imports a class or a directory. - * - * Importing a class is like including the corresponding class file. - * The main difference is that importing a class is much lighter because it only - * includes the class file when the class is referenced in the code the first time. - * - * Importing a directory will add the directory to the front of the [[classPath]] array. - * When [[autoload]] is loading an unknown class, it will search in the directories - * specified in [[classPath]] to find the corresponding class file to include. - * For this reason, if multiple directories are imported, the directories imported later - * will take precedence in class file searching. - * - * The same class or directory can be imported multiple times. Only the first importing - * will count. Importing a directory does not import any of its subdirectories. - * - * To import a class or a directory, one can use either path alias or class name (can be namespaced): - * - * - `@app/components/GoogleMap`: importing the `GoogleMap` class with a path alias; - * - `@app/components/*`: importing the whole `components` directory with a path alias; - * - `GoogleMap`: importing the `GoogleMap` class with a class name. [[autoload()]] will be used - * when this class is used for the first time. - * - * @param string $alias path alias or a simple class name to be imported - * @param boolean $forceInclude whether to include the class file immediately. If false, the class file - * will be included only when the class is being used. This parameter is used only when - * the path alias refers to a class. - * @return string the class name or the directory that this alias refers to - * @throws Exception if the path alias is invalid + * Imports a class by its alias. + * + * This method is provided to support autoloading of non-namespaced classes. + * Such a class can be specified in terms of an alias. For example, the alias `@old/code/Sample` + * may represent the `Sample` class under the directory `@old/code` (a path alias). + * + * By importing a class, the class is put in an internal storage such that when + * the class is used for the first time, the class autoloader will be able to + * find the corresponding class file and include it. For this reason, this method + * is much lighter than `include()`. + * + * You may import the same class multiple times. Only the first importing will count. + * + * @param string $alias the class to be imported. This may be either a class alias or a fully-qualified class name. + * If the latter, it will be returned back without change. + * @return string the actual class name that `$alias` refers to + * @throws Exception if the alias is invalid */ - public static function import($alias, $forceInclude = false) + public static function import($alias) { - if (isset(self::$_imported[$alias])) { - return self::$_imported[$alias]; - } - - if ($alias[0] !== '@') { - // a simple class name - if (class_exists($alias, false) || interface_exists($alias, false)) { - return self::$_imported[$alias] = $alias; - } - if ($forceInclude && static::autoload($alias)) { - self::$_imported[$alias] = $alias; - } + if (strncmp($alias, '@', 1)) { return $alias; + } else { + $alias = static::getAlias($alias); + if (!isset(self::$_imported[$alias])) { + $className = basename($alias); + self::$_imported[$alias] = $className; + self::$classMap[$className] = $alias . '.php'; + } + return self::$_imported[$alias]; } + } - $className = basename($alias); - $isClass = $className !== '*'; - - if ($isClass && (class_exists($className, false) || interface_exists($className, false))) { - return self::$_imported[$alias] = $className; - } - - $path = static::getAlias(dirname($alias)); - - if ($isClass) { - if ($forceInclude) { - require($path . "/$className.php"); - self::$_imported[$alias] = $className; - } else { - self::$classMap[$className] = $path . DIRECTORY_SEPARATOR . "$className.php"; + /** + * Imports a set of namespaces. + * + * By importing a namespace, the method will create an alias for the directory corresponding + * to the namespace. For example, if "foo\bar" is a namespace associated with the directory + * "path/to/foo/bar", then an alias "@foo/bar" will be created for this directory. + * + * This method is typically invoked in the bootstrap file to import the namespaces of + * the installed extensions. By default, Composer, when installing new extensions, will + * generate such a mapping file which can be loaded and passed to this method. + * + * @param array $namespaces the namespaces to be imported. The keys are the namespaces, + * and the values are the corresponding directories. + */ + public static function importNamespaces($namespaces) + { + foreach ($namespaces as $name => $path) { + if ($name !== '') { + $name = '@' . str_replace('\\', '/', $name); + static::setAlias($name, $path); } - return $className; - } else { - // a directory - array_unshift(self::$classPath, $path); - return self::$_imported[$alias] = $path; } } /** * Translates a path alias into an actual path. * - * The path alias can be either a root alias registered via [[setAlias]] or an - * alias starting with a root alias (e.g. `@yii/base/Component.php`). - * In the latter case, the root alias will be replaced by the corresponding registered path - * and the remaining part will be appended to it. + * The translation is done according to the following procedure: + * + * 1. If the given alias does not start with '@', it is returned back without change; + * 2. Otherwise, look for the longest registered alias that matches the beginning part + * of the given alias. If it exists, replace the matching part of the given alias with + * the corresponding registered path. + * 3. Throw an exception or return false, depending on the `$throwException` parameter. + * + * For example, by default '@yii' is registered as the alias to the Yii framework directory, + * say '/path/to/yii'. The alias '@yii/web' would then be translated into '/path/to/yii/web'. + * + * If you have registered two aliases '@foo' and '@foo/bar'. Then translating '@foo/bar/config' + * would replace the part '@foo/bar' (instead of '@foo') with the corresponding registered path. + * This is because the longest alias takes precedence. * - * In case the given parameter is not an alias (i.e., not starting with '@'), - * it will be returned back without change. + * However, if the alias to be translated is '@foo/barbar/config', then '@foo' will be replaced + * instead of '@foo/bar', because '/' serves as the boundary character. * - * Note, this method does not ensure the existence of the resulting path. - * @param string $alias alias + * Note, this method does not check if the returned path exists or not. + * + * @param string $alias the alias to be translated. * @param boolean $throwException whether to throw an exception if the given alias is invalid. * If this is false and an invalid alias is given, false will be returned by this method. - * @return string|boolean path corresponding to the alias, false if the root alias is not previously registered. + * @return string|boolean the path corresponding to the alias, false if the root alias is not previously registered. + * @throws InvalidParamException if the alias is invalid while $throwException is true. * @see setAlias */ public static function getAlias($alias, $throwException = true) { - if (is_string($alias)) { - if (isset(self::$aliases[$alias])) { - return self::$aliases[$alias]; - } elseif ($alias === '' || $alias[0] !== '@') { // not an alias - return $alias; - } elseif (($pos = strpos($alias, '/')) !== false || ($pos = strpos($alias, '\\')) !== false) { - $rootAlias = substr($alias, 0, $pos); - if (isset(self::$aliases[$rootAlias])) { - return self::$aliases[$alias] = self::$aliases[$rootAlias] . substr($alias, $pos); + if (strncmp($alias, '@', 1)) { + // not an alias + return $alias; + } + + $pos = strpos($alias, '/'); + $root = $pos === false ? $alias : substr($alias, 0, $pos); + + if (isset(self::$aliases[$root])) { + if (is_string(self::$aliases[$root])) { + return $pos === false ? self::$aliases[$root] : self::$aliases[$root] . substr($alias, $pos); + } else { + foreach (self::$aliases[$root] as $name => $path) { + if (strpos($alias . '/', $name . '/') === 0) { + return $path . substr($alias, strlen($name)); + } } } } + if ($throwException) { throw new InvalidParamException("Invalid path alias: $alias"); } else { @@ -216,39 +222,96 @@ class YiiBase } /** + * Returns the root alias part of a given alias. + * A root alias is an alias that has been registered via [[setAlias()]] previously. + * If a given alias matches multiple root aliases, the longest one will be returned. + * @param string $alias the alias + * @return string|boolean the root alias, or false if no root alias is found + */ + public static function getRootAlias($alias) + { + $pos = strpos($alias, '/'); + $root = $pos === false ? $alias : substr($alias, 0, $pos); + + if (isset(self::$aliases[$root])) { + if (is_string(self::$aliases[$root])) { + return $root; + } else { + foreach (self::$aliases[$root] as $name => $path) { + if (strpos($alias . '/', $name . '/') === 0) { + return $name; + } + } + } + } + return false; + } + + /** * Registers a path alias. * - * A path alias is a short name representing a path (a file path, a URL, etc.) - * A path alias must start with '@' (e.g. '@yii'). + * A path alias is a short name representing a long path (a file path, a URL, etc.) + * For example, we use '@yii' as the alias of the path to the Yii framework directory. + * + * A path alias must start with the character '@' so that it can be easily differentiated + * from non-alias paths. * - * Note that this method neither checks the existence of the path nor normalizes the path. - * Any trailing '/' and '\' characters in the path will be trimmed. + * Note that this method does not check if the given path exists or not. All it does is + * to associate the alias with the path. * - * @param string $alias alias to the path. The alias must start with '@'. - * @param string $path the path corresponding to the alias. This can be + * Any trailing '/' and '\' characters in the given path will be trimmed. + * + * @param string $alias the alias name (e.g. "@yii"). It must start with a '@' character. + * It may contain the forward slash '/' which serves as boundary character when performing + * alias translation by [[getAlias()]]. + * @param string $path the path corresponding to the alias. Trailing '/' and '\' characters + * will be trimmed. This can be * * - a directory or a file path (e.g. `/tmp`, `/tmp/main.txt`) * - a URL (e.g. `http://www.yiiframework.com`) * - a path alias (e.g. `@yii/base`). In this case, the path alias will be converted into the - * actual path first by calling [[getAlias]]. - * @throws Exception if $path is an invalid alias + * actual path first by calling [[getAlias()]]. + * + * @throws InvalidParamException if $path is an invalid alias. * @see getAlias */ public static function setAlias($alias, $path) { - if ($path === null) { - unset(self::$aliases[$alias]); - } elseif ($path[0] !== '@') { - self::$aliases[$alias] = rtrim($path, '\\/'); - } else { - self::$aliases[$alias] = static::getAlias($path); + if (strncmp($alias, '@', 1)) { + $alias = '@' . $alias; + } + $pos = strpos($alias, '/'); + $root = $pos === false ? $alias : substr($alias, 0, $pos); + if ($path !== null) { + $path = strncmp($path, '@', 1) ? rtrim($path, '\\/') : static::getAlias($path); + if (!isset(self::$aliases[$root])) { + self::$aliases[$root] = $path; + } elseif (is_string(self::$aliases[$root])) { + if ($pos === false) { + self::$aliases[$root] = $path; + } else { + self::$aliases[$root] = array( + $alias => $path, + $root => self::$aliases[$root], + ); + } + } else { + self::$aliases[$root][$alias] = $path; + krsort(self::$aliases[$root]); + } + } elseif (isset(self::$aliases[$root])) { + if (is_array(self::$aliases[$root])) { + unset(self::$aliases[$root][$alias]); + } elseif ($pos === false) { + unset(self::$aliases[$root]); + } } } /** * Class autoload loader. - * This method is invoked automatically when the execution encounters an unknown class. - * The method will attempt to include the class file as follows: + * This method is invoked automatically when PHP sees an unknown class. + * The method will attempt to include the class file according to the following procedure: * * 1. Search in [[classMap]]; * 2. If the class is namespaced (e.g. `yii\base\Component`), it will attempt @@ -257,73 +320,75 @@ class YiiBase * 3. If the class is named in PEAR style (e.g. `PHPUnit_Framework_TestCase`), * it will attempt to include the file associated with the corresponding path alias * (e.g. `@PHPUnit/Framework/TestCase.php`); - * 4. Search in [[classPath]]; + * 4. Search PHP include_path for the actual class file if [[enableIncludePath]] is true; * 5. Return false so that other autoloaders have chance to include the class file. * * @param string $className class name * @return boolean whether the class has been loaded successfully - * @throws Exception if the class file does not exist + * @throws InvalidConfigException if the class file does not exist + * @throws UnknownClassException if the class does not exist in the class file */ public static function autoload($className) { - if (isset(self::$classMap[$className])) { - include(self::$classMap[$className]); - return true; - } + $className = ltrim($className, '\\'); - if (strpos($className, '\\') !== false) { - // namespaced class, e.g. yii\base\Component - // convert namespace to path alias, e.g. yii\base\Component to @yii/base/Component - $alias = '@' . str_replace('\\', '/', ltrim($className, '\\')); - if (($path = static::getAlias($alias, false)) !== false) { - $classFile = $path . '.php'; + if (isset(self::$classMap[$className])) { + $classFile = static::getAlias(self::$classMap[$className]); + if (!is_file($classFile)) { + throw new InvalidConfigException("Class file does not exist: $classFile"); } - } elseif (($pos = strpos($className, '_')) !== false) { - // PEAR-styled class, e.g. PHPUnit_Framework_TestCase - // convert class name to path alias, e.g. PHPUnit_Framework_TestCase to @PHPUnit/Framework/TestCase - $alias = '@' . str_replace('_', '/', $className); - if (($path = static::getAlias($alias, false)) !== false) { - $classFile = $path . '.php'; + } else { + // follow PSR-0 to determine the class file + if (($pos = strrpos($className, '\\')) !== false) { + // namespaced class, e.g. yii\base\Component + $path = str_replace('\\', '/', substr($className, 0, $pos + 1)) + . str_replace('_', '/', substr($className, $pos + 1)) . '.php'; + } else { + $path = str_replace('_', '/', $className) . '.php'; } - } - if (!isset($classFile)) { - // search in include paths - foreach (self::$classPath as $path) { - $path .= DIRECTORY_SEPARATOR . $className . '.php'; - if (is_file($path)) { - $classFile = $path; - $alias = $className; + // try via path alias first + if (strpos($path, '/') !== false) { + $fullPath = static::getAlias('@' . $path, false); + if ($fullPath !== false && is_file($fullPath)) { + $classFile = $fullPath; } } - } - if (isset($classFile, $alias) && is_file($classFile)) { - if (!YII_DEBUG || basename(realpath($classFile)) === basename($alias) . '.php') { - include($classFile); - return true; - } else { - throw new Exception("Class name '$className' does not match the class file '" . realpath($classFile) . "'. Have you checked their case sensitivity?"); + // search include_path + if (!isset($classFile) && self::$enableIncludePath && ($fullPath = stream_resolve_include_path($path)) !== false) { + $classFile = $fullPath; + } + + if (!isset($classFile)) { + // return false to let other autoloaders to try loading the class + return false; } } - return false; + include($classFile); + + if (class_exists($className, false) || interface_exists($className, false)) { + return true; + } else { + throw new UnknownClassException("Unable to find '$className' in file: $classFile"); + } } /** * Creates a new object using the given configuration. * * The configuration can be either a string or an array. - * If a string, it is treated as the *object type*; if an array, - * it must contain a `class` element specifying the *object type*, and + * If a string, it is treated as the *object class*; if an array, + * it must contain a `class` element specifying the *object class*, and * the rest of the name-value pairs in the array will be used to initialize * the corresponding object properties. * - * The object type can be either a class name or the [[getAlias|alias]] of + * The object type can be either a class name or the [[getAlias()|alias]] of * the class. For example, * - * - `\app\components\GoogleMap`: fully-qualified namespaced class. - * - `@app/components/GoogleMap`: an alias + * - `app\components\GoogleMap`: fully-qualified namespaced class. + * - `@app/components/GoogleMap`: an alias, used for non-namespaced class. * * Below are some usage examples: * @@ -366,7 +431,13 @@ class YiiBase } if (!class_exists($class, false)) { - $class = static::import($class, true); + $class = static::import($class); + } + + $class = ltrim($class, '\\'); + + if (isset(self::$objectConfig[$class])) { + $config = array_merge(self::$objectConfig[$class], $config); } if (($n = func_num_args()) > 1) { @@ -504,23 +575,31 @@ class YiiBase /** * Translates a message to the specified language. - * This method supports choice format (see {@link CChoiceFormat}), - * i.e., the message returned will be chosen from a few candidates according to the given - * number value. This feature is mainly used to solve plural format issue in case - * a message has different plural forms in some languages. - * @param string $message the original message - * @param array $params parameters to be applied to the message using strtr. - * The first parameter can be a number without key. - * And in this case, the method will call {@link CChoiceFormat::format} to choose - * an appropriate message translation. - * You can pass parameter for {@link CChoiceFormat::format} - * or plural forms format without wrapping it with array. - * @param string $language the target language. If null (default), the {@link CApplication::getLanguage application language} will be used. - * @return string the translated message - * @see CMessageSource + * + * The translation will be conducted according to the message category and the target language. + * To specify the category of the message, prefix the message with the category name and separate it + * with "|". For example, "app|hello world". If the category is not specified, the default category "app" + * will be used. The actual message translation is done by a [[\yii\i18n\MessageSource|message source]]. + * + * In case when a translated message has different plural forms (separated by "|"), this method + * will also attempt to choose an appropriate one according to a given numeric value which is + * specified as the first parameter (indexed by 0) in `$params`. + * + * For example, if a translated message is "I have an apple.|I have {n} apples.", and the first + * parameter is 2, the message returned will be "I have 2 apples.". Note that the placeholder "{n}" + * will be replaced with the given number. + * + * For more details on how plural rules are applied, please refer to: + * [[http://www.unicode.org/cldr/charts/supplemental/language_plural_rules.html]] + * + * @param string $message the message to be translated. + * @param array $params the parameters that will be used to replace the corresponding placeholders in the message. + * @param string $language the language code (e.g. `en_US`, `en`). If this is null, the current + * [[\yii\base\Application::language|application language]] will be used. + * @return string the translated message. */ public static function t($message, $params = array(), $language = null) { - return Yii::$app->getI18N()->translate($message, $params, $language); + return self::$app->getI18N()->translate($message, $params, $language); } } diff --git a/framework/assets.php b/framework/assets.php new file mode 100644 index 0000000..5efa94a --- /dev/null +++ b/framework/assets.php @@ -0,0 +1,5 @@ + __DIR__ . '/web/assets', +); \ No newline at end of file diff --git a/framework/base/ActionEvent.php b/framework/base/ActionEvent.php index 7c5a40c..9507b12 100644 --- a/framework/base/ActionEvent.php +++ b/framework/base/ActionEvent.php @@ -22,7 +22,9 @@ class ActionEvent extends Event */ public $action; /** - * @var boolean whether to continue running the action. + * @var boolean whether to continue running the action. Event handlers of + * [[Controller::EVENT_BEFORE_ACTION]] may set this property to decide whether + * to continue running the current action. */ public $isValid = true; diff --git a/framework/base/Application.php b/framework/base/Application.php index 9be1939..c498a8e 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -13,36 +13,6 @@ use yii\helpers\FileHelper; /** * Application is the base class for all application classes. * - * An application serves as the global context that the user request - * is being processed. It manages a set of application components that - * provide specific functionalities to the whole application. - * - * The core application components provided by Application are the following: - * - * - * Application will undergo the following life cycles when processing a user request: - *
    - *
  1. load application configuration;
  2. - *
  3. set up class autoloader and error handling;
  4. - *
  5. load static application components;
  6. - *
  7. {@link beforeRequest}: preprocess the user request; `beforeRequest` event raised.
  8. - *
  9. {@link processRequest}: process the user request;
  10. - *
  11. {@link afterRequest}: postprocess the user request; `afterRequest` event raised.
  12. - *
- * - * Starting from lifecycle 3, if a PHP error or an uncaught exception occurs, - * the application will switch to its error handling logic and jump to step 6 afterwards. - * * @author Qiang Xue * @since 2.0 */ @@ -87,50 +57,51 @@ class Application extends Module */ public $layout = 'main'; - // todo - public $localeDataPath = '@yii/i18n/data'; - - private $_runtimePath; private $_ended = false; /** - * @var string Used to reserve memory for fatal error handler. This memory - * reserve can be removed if it's OK to write to PHP log only in this particular case. + * @var string Used to reserve memory for fatal error handler. */ private $_memoryReserve; /** * Constructor. - * @param string $id the ID of this application. The ID should uniquely identify the application from others. - * @param string $basePath the base path of this application. This should point to - * the directory containing all application logic, template and data. - * @param array $config name-value pairs that will be used to initialize the object properties + * @param array $config name-value pairs that will be used to initialize the object properties. + * Note that the configuration must contain both [[id]] and [[basePath]]. + * @throws InvalidConfigException if either [[id]] or [[basePath]] configuration is missing. */ - public function __construct($id, $basePath, $config = array()) + public function __construct($config = array()) { Yii::$app = $this; - $this->id = $id; - $this->setBasePath($basePath); - if (YII_ENABLE_ERROR_HANDLER) { - ini_set('display_errors', 0); - set_exception_handler(array($this, 'handleException')); - set_error_handler(array($this, 'handleError'), error_reporting()); + if (!isset($config['id'])) { + throw new InvalidConfigException('The "id" configuration is required.'); } - $this->registerDefaultAliases(); + if (isset($config['basePath'])) { + $this->setBasePath($config['basePath']); + Yii::setAlias('@app', $this->getBasePath()); + unset($config['basePath']); + } else { + throw new InvalidConfigException('The "basePath" configuration is required.'); + } + + $this->registerErrorHandlers(); $this->registerCoreComponents(); Component::__construct($config); } /** - * Initializes the application by loading components declared in [[preload]]. - * If you override this method, make sure the parent implementation is invoked. + * Registers error handlers. */ - public function init() + public function registerErrorHandlers() { - $this->preloadComponents(); + if (YII_ENABLE_ERROR_HANDLER) { + ini_set('display_errors', 0); + set_exception_handler(array($this, 'handleException')); + set_error_handler(array($this, 'handleError'), error_reporting()); + } } /** @@ -155,55 +126,6 @@ class Application extends Module } /** - * Handles fatal PHP errors - */ - public function handleFatalError() - { - if (YII_ENABLE_ERROR_HANDLER) { - $error = error_get_last(); - - if (ErrorException::isFatalErorr($error)) { - unset($this->_memoryReserve); - $exception = new ErrorException($error['message'], $error['type'], $error['type'], $error['file'], $error['line']); - - if (function_exists('xdebug_get_function_stack')) { - $trace = array_slice(array_reverse(xdebug_get_function_stack()), 4, -1); - foreach ($trace as &$frame) { - if (!isset($frame['function'])) { - $frame['function'] = 'unknown'; - } - - // XDebug < 2.1.1: http://bugs.xdebug.org/view.php?id=695 - if (!isset($frame['type'])) { - $frame['type'] = '::'; - } - - // XDebug has a different key name - $frame['args'] = array(); - if (isset($frame['params']) && !isset($frame['args'])) { - $frame['args'] = $frame['params']; - } - } - - $ref = new \ReflectionProperty('Exception', 'trace'); - $ref->setAccessible(true); - $ref->setValue($exception, $trace); - } - - $this->logException($exception); - - if (($handler = $this->getErrorHandler()) !== null) { - @$handler->handle($exception); - } else { - $this->renderException($exception); - } - - exit(1); - } - } - } - - /** * Runs the application. * This is the main entrance of an application. * @return integer the exit status (0 means normal, non-zero values mean abnormal) @@ -246,6 +168,8 @@ class Application extends Module return 0; } + private $_runtimePath; + /** * Returns the directory that stores runtime files. * @return string the directory that stores runtime files. Defaults to 'protected/runtime'. @@ -265,12 +189,35 @@ class Application extends Module */ public function setRuntimePath($path) { - $p = FileHelper::ensureDirectory($path); - if (is_writable($p)) { - $this->_runtimePath = $p; + $path = Yii::getAlias($path); + if (is_dir($path) && is_writable($path)) { + $this->_runtimePath = $path; } else { - throw new InvalidConfigException("Runtime path must be writable by the Web server process: $path"); + throw new InvalidConfigException("Runtime path must be a directory writable by the Web server process: $path"); + } + } + + private $_vendorPath; + + /** + * Returns the directory that stores vendor files. + * @return string the directory that stores vendor files. Defaults to 'protected/vendor'. + */ + public function getVendorPath() + { + if ($this->_vendorPath === null) { + $this->setVendorPath($this->getBasePath() . DIRECTORY_SEPARATOR . 'vendor'); } + return $this->_vendorPath; + } + + /** + * Sets the directory that stores vendor files. + * @param string $path the directory that stores vendor files. + */ + public function setVendorPath($path) + { + $this->_vendorPath = Yii::getAlias($path); } /** @@ -295,37 +242,6 @@ class Application extends Module date_default_timezone_set($value); } - // - // /** - // * Returns the locale instance. - // * @param string $localeID the locale ID (e.g. en_US). If null, the {@link getLanguage application language ID} will be used. - // * @return CLocale the locale instance - // */ - // public function getLocale($localeID = null) - // { - // return CLocale::getInstance($localeID === null ? $this->getLanguage() : $localeID); - // } - // - // /** - // * @return CNumberFormatter the locale-dependent number formatter. - // * The current {@link getLocale application locale} will be used. - // */ - // public function getNumberFormatter() - // { - // return $this->getLocale()->getNumberFormatter(); - // } - // - // /** - // * Returns the locale-dependent date formatter. - // * @return CDateFormatter the locale-dependent date formatter. - // * The current {@link getLocale application locale} will be used. - // */ - // public function getDateFormatter() - // { - // return $this->getLocale()->getDateFormatter(); - // } - // - /** * Returns the database connection component. * @return \yii\db\Connection the database connection @@ -390,14 +306,6 @@ class Application extends Module } /** - * Sets default path aliases. - */ - public function registerDefaultAliases() - { - Yii::$aliases['@app'] = $this->getBasePath(); - } - - /** * Registers the core application components. * @see setComponents */ @@ -420,6 +328,45 @@ class Application extends Module } /** + * Handles uncaught PHP exceptions. + * + * This method is implemented as a PHP exception handler. It requires + * that constant YII_ENABLE_ERROR_HANDLER be defined true. + * + * @param \Exception $exception exception that is not caught + */ + public function handleException($exception) + { + // disable error capturing to avoid recursive errors while handling exceptions + restore_error_handler(); + restore_exception_handler(); + + try { + $this->logException($exception); + + if (($handler = $this->getErrorHandler()) !== null) { + $handler->handle($exception); + } else { + $this->renderException($exception); + } + + $this->end(1); + + } catch (\Exception $e) { + // exception could be thrown in end() or ErrorHandler::handle() + $msg = (string)$e; + $msg .= "\nPrevious exception:\n"; + $msg .= (string)$exception; + if (YII_DEBUG) { + echo $msg; + } + $msg .= "\n\$_SERVER = " . var_export($_SERVER, true); + error_log($msg); + exit(1); + } + } + + /** * Handles PHP execution errors such as warnings, notices. * * This method is used as a PHP error handler. It will simply raise an `ErrorException`. @@ -450,41 +397,27 @@ class Application extends Module } /** - * Handles uncaught PHP exceptions. - * - * This method is implemented as a PHP exception handler. It requires - * that constant YII_ENABLE_ERROR_HANDLER be defined true. - * - * @param \Exception $exception exception that is not caught + * Handles fatal PHP errors */ - public function handleException($exception) + public function handleFatalError() { - // disable error capturing to avoid recursive errors while handling exceptions - restore_error_handler(); - restore_exception_handler(); - - try { - $this->logException($exception); + if (YII_ENABLE_ERROR_HANDLER) { + $error = error_get_last(); - if (($handler = $this->getErrorHandler()) !== null) { - $handler->handle($exception); - } else { - $this->renderException($exception); - } + if (ErrorException::isFatalError($error)) { + unset($this->_memoryReserve); + $exception = new ErrorException($error['message'], $error['type'], $error['type'], $error['file'], $error['line']); + // use error_log because it's too late to use Yii log + error_log($exception); - $this->end(1); + if (($handler = $this->getErrorHandler()) !== null) { + $handler->handle($exception); + } else { + $this->renderException($exception); + } - } catch (\Exception $e) { - // exception could be thrown in end() or ErrorHandler::handle() - $msg = (string)$e; - $msg .= "\nPrevious exception:\n"; - $msg .= (string)$exception; - if (YII_DEBUG) { - echo $msg; + exit(1); } - $msg .= "\n\$_SERVER = " . var_export($_SERVER, true); - error_log($msg); - exit(1); } } diff --git a/framework/base/Component.php b/framework/base/Component.php index f1d549b..80259e7 100644 --- a/framework/base/Component.php +++ b/framework/base/Component.php @@ -7,26 +7,23 @@ namespace yii\base; +use Yii; + /** - * Component is the base class that provides the *property*, *event* and *behavior* features. - * * @include @yii/base/Component.md - * - * @property Behavior[] behaviors list of behaviors currently attached to this component - * * @author Qiang Xue * @since 2.0 */ -class Component extends \yii\base\Object +class Component extends Object { /** - * @var Vector[] the attached event handlers (event name => handlers) + * @var array the attached event handlers (event name => handlers) */ - private $_e; + private $_events; /** * @var Behavior[] the attached behaviors (behavior name => behavior) */ - private $_b; + private $_behaviors; /** * Returns the value of a component property. @@ -52,7 +49,7 @@ class Component extends \yii\base\Object } else { // behavior property $this->ensureBehaviors(); - foreach ($this->_b as $behavior) { + foreach ($this->_behaviors as $behavior) { if ($behavior->canGetProperty($name)) { return $behavior->$name; } @@ -87,17 +84,16 @@ class Component extends \yii\base\Object return; } elseif (strncmp($name, 'on ', 3) === 0) { // on event: attach event handler - $name = trim(substr($name, 3)); - $this->getEventHandlers($name)->add($value); + $this->on(trim(substr($name, 3)), $value); return; } elseif (strncmp($name, 'as ', 3) === 0) { // as behavior: attach behavior $name = trim(substr($name, 3)); - $this->attachBehavior($name, $value instanceof Behavior ? $value : \Yii::createObject($value)); + $this->attachBehavior($name, $value instanceof Behavior ? $value : Yii::createObject($value)); } else { // behavior property $this->ensureBehaviors(); - foreach ($this->_b as $behavior) { + foreach ($this->_behaviors as $behavior) { if ($behavior->canSetProperty($name)) { $behavior->$name = $value; return; @@ -131,7 +127,7 @@ class Component extends \yii\base\Object } else { // behavior property $this->ensureBehaviors(); - foreach ($this->_b as $behavior) { + foreach ($this->_behaviors as $behavior) { if ($behavior->canGetProperty($name)) { return $behavior->$name !== null; } @@ -161,7 +157,7 @@ class Component extends \yii\base\Object } else { // behavior property $this->ensureBehaviors(); - foreach ($this->_b as $behavior) { + foreach ($this->_behaviors as $behavior) { if ($behavior->canSetProperty($name)) { $behavior->$name = null; return; @@ -198,7 +194,7 @@ class Component extends \yii\base\Object } $this->ensureBehaviors(); - foreach ($this->_b as $object) { + foreach ($this->_behaviors as $object) { if (method_exists($object, $name)) { return call_user_func_array(array($object, $name), $params); } @@ -213,8 +209,8 @@ class Component extends \yii\base\Object */ public function __clone() { - $this->_e = null; - $this->_b = null; + $this->_events = null; + $this->_behaviors = null; } /** @@ -259,7 +255,7 @@ class Component extends \yii\base\Object return true; } else { $this->ensureBehaviors(); - foreach ($this->_b as $behavior) { + foreach ($this->_behaviors as $behavior) { if ($behavior->canGetProperty($name, $checkVar)) { return true; } @@ -289,7 +285,7 @@ class Component extends \yii\base\Object return true; } else { $this->ensureBehaviors(); - foreach ($this->_b as $behavior) { + foreach ($this->_behaviors as $behavior) { if ($behavior->canSetProperty($name, $checkVar)) { return true; } @@ -337,44 +333,17 @@ class Component extends \yii\base\Object public function hasEventHandlers($name) { $this->ensureBehaviors(); - return isset($this->_e[$name]) && $this->_e[$name]->getCount(); - } - - /** - * Returns the list of attached event handlers for an event. - * You may manipulate the returned [[Vector]] object by adding or removing handlers. - * For example, - * - * ~~~ - * $component->getEventHandlers($eventName)->insertAt(0, $eventHandler); - * ~~~ - * - * @param string $name the event name - * @return Vector list of attached event handlers for the event - */ - public function getEventHandlers($name) - { - if (!isset($this->_e[$name])) { - $this->_e[$name] = new Vector; - } - $this->ensureBehaviors(); - return $this->_e[$name]; + return !empty($this->_events[$name]); } /** * Attaches an event handler to an event. * - * This is equivalent to the following code: - * - * ~~~ - * $component->getEventHandlers($eventName)->add($eventHandler); - * ~~~ - * * An event handler must be a valid PHP callback. The followings are * some examples: * * ~~~ - * function($event) { ... } // anonymous function + * function ($event) { ... } // anonymous function * array($object, 'handleClick') // $object->handleClick() * array('Page', 'handleClick') // Page::handleClick() * 'handleClick' // global function handleClick() @@ -383,31 +352,53 @@ class Component extends \yii\base\Object * An event handler must be defined with the following signature, * * ~~~ - * function handlerName($event) {} + * function ($event) * ~~~ * * where `$event` is an [[Event]] object which includes parameters associated with the event. * * @param string $name the event name - * @param string|array|\Closure $handler the event handler + * @param callback $handler the event handler + * @param mixed $data the data to be passed to the event handler when the event is triggered. + * When the event handler is invoked, this data can be accessed via [[Event::data]]. * @see off() */ - public function on($name, $handler) + public function on($name, $handler, $data = null) { - $this->getEventHandlers($name)->add($handler); + $this->ensureBehaviors(); + $this->_events[$name][] = array($handler, $data); } /** * Detaches an existing event handler from this component. * This method is the opposite of [[on()]]. * @param string $name event name - * @param string|array|\Closure $handler the event handler to be removed + * @param callback $handler the event handler to be removed. + * If it is null, all handlers attached to the named event will be removed. * @return boolean if a handler is found and detached * @see on() */ - public function off($name, $handler) + public function off($name, $handler = null) { - return $this->getEventHandlers($name)->remove($handler) !== false; + $this->ensureBehaviors(); + if (isset($this->_events[$name])) { + if ($handler === null) { + $this->_events[$name] = array(); + } else { + $removed = false; + foreach ($this->_events[$name] as $i => $event) { + if ($event[0] === $handler) { + unset($this->_events[$name][$i]); + $removed = true; + } + } + if ($removed) { + $this->_events[$name] = array_values($this->_events[$name]); + } + return $removed; + } + } + return false; } /** @@ -420,7 +411,7 @@ class Component extends \yii\base\Object public function trigger($name, $event = null) { $this->ensureBehaviors(); - if (isset($this->_e[$name]) && $this->_e[$name]->getCount()) { + if (!empty($this->_events[$name])) { if ($event === null) { $event = new Event; } @@ -429,8 +420,9 @@ class Component extends \yii\base\Object } $event->handled = false; $event->name = $name; - foreach ($this->_e[$name] as $handler) { - call_user_func($handler, $event); + foreach ($this->_events[$name] as $handler) { + $event->data = $handler[1]; + call_user_func($handler[0], $event); // stop further handling if the event is handled if ($event instanceof Event && $event->handled) { return; @@ -447,7 +439,7 @@ class Component extends \yii\base\Object public function getBehavior($name) { $this->ensureBehaviors(); - return isset($this->_b[$name]) ? $this->_b[$name] : null; + return isset($this->_behaviors[$name]) ? $this->_behaviors[$name] : null; } /** @@ -457,20 +449,20 @@ class Component extends \yii\base\Object public function getBehaviors() { $this->ensureBehaviors(); - return $this->_b; + return $this->_behaviors; } /** * Attaches a behavior to this component. * This method will create the behavior object based on the given * configuration. After that, the behavior object will be attached to - * this component by calling the [[Behavior::attach]] method. + * this component by calling the [[Behavior::attach()]] method. * @param string $name the name of the behavior. * @param string|array|Behavior $behavior the behavior configuration. This can be one of the following: * * - a [[Behavior]] object * - a string specifying the behavior class - * - an object configuration array that will be passed to [[\Yii::createObject()]] to create the behavior object. + * - an object configuration array that will be passed to [[Yii::createObject()]] to create the behavior object. * * @return Behavior the behavior object * @see detachBehavior @@ -498,15 +490,15 @@ class Component extends \yii\base\Object /** * Detaches a behavior from the component. - * The behavior's [[Behavior::detach]] method will be invoked. + * The behavior's [[Behavior::detach()]] method will be invoked. * @param string $name the behavior's name. * @return Behavior the detached behavior. Null if the behavior does not exist. */ public function detachBehavior($name) { - if (isset($this->_b[$name])) { - $behavior = $this->_b[$name]; - unset($this->_b[$name]); + if (isset($this->_behaviors[$name])) { + $behavior = $this->_behaviors[$name]; + unset($this->_behaviors[$name]); $behavior->detach(); return $behavior; } else { @@ -519,12 +511,12 @@ class Component extends \yii\base\Object */ public function detachBehaviors() { - if ($this->_b !== null) { - foreach ($this->_b as $name => $behavior) { + if ($this->_behaviors !== null) { + foreach ($this->_behaviors as $name => $behavior) { $this->detachBehavior($name); } } - $this->_b = array(); + $this->_behaviors = array(); } /** @@ -532,8 +524,8 @@ class Component extends \yii\base\Object */ public function ensureBehaviors() { - if ($this->_b === null) { - $this->_b = array(); + if ($this->_behaviors === null) { + $this->_behaviors = array(); foreach ($this->behaviors() as $name => $behavior) { $this->attachBehaviorInternal($name, $behavior); } @@ -549,12 +541,12 @@ class Component extends \yii\base\Object private function attachBehaviorInternal($name, $behavior) { if (!($behavior instanceof Behavior)) { - $behavior = \Yii::createObject($behavior); + $behavior = Yii::createObject($behavior); } - if (isset($this->_b[$name])) { - $this->_b[$name]->detach(); + if (isset($this->_behaviors[$name])) { + $this->_behaviors[$name]->detach(); } $behavior->attach($this); - return $this->_b[$name] = $behavior; + return $this->_behaviors[$name] = $behavior; } } diff --git a/framework/base/Controller.php b/framework/base/Controller.php index ff6d8f7..d11d19b 100644 --- a/framework/base/Controller.php +++ b/framework/base/Controller.php @@ -47,6 +47,11 @@ class Controller extends Component * by [[run()]] when it is called by [[Application]] to run an action. */ public $action; + /** + * @var View the view object that can be used to render views or view files. + */ + private $_view; + /** * @param string $id the ID of this controller @@ -135,7 +140,7 @@ class Controller extends Component } elseif ($pos > 0) { return $this->module->runAction($route, $params); } else { - return \Yii::$app->runAction(ltrim($route, '/'), $params); + return Yii::$app->runAction(ltrim($route, '/'), $params); } } @@ -293,6 +298,37 @@ class Controller extends Component /** * Renders a view and applies layout if available. + * + * The view to be rendered can be specified in one of the following formats: + * + * - path alias (e.g. "@app/views/site/index"); + * - absolute path within application (e.g. "//site/index"): the view name starts with double slashes. + * The actual view file will be looked for under the [[Application::viewPath|view path]] of the application. + * - absolute path within module (e.g. "/site/index"): the view name starts with a single slash. + * The actual view file will be looked for under the [[Module::viewPath|view path]] of [[module]]. + * - relative path (e.g. "index"): the actual view file will be looked for under [[viewPath]]. + * + * To determine which layout should be applied, the following two steps are conducted: + * + * 1. In the first step, it determines the layout name and the context module: + * + * - If [[layout]] is specified as a string, use it as the layout name and [[module]] as the context module; + * - If [[layout]] is null, search through all ancestor modules of this controller and find the first + * module whose [[Module::layout|layout]] is not null. The layout and the corresponding module + * are used as the layout name and the context module, respectively. If such a module is not found + * or the corresponding layout is not a string, it will return false, meaning no applicable layout. + * + * 2. In the second step, it determines the actual layout file according to the previously found layout name + * and context module. The layout name can be + * + * - a path alias (e.g. "@app/views/layouts/main"); + * - an absolute path (e.g. "/main"): the layout name starts with a slash. The actual layout file will be + * looked for under the [[Application::layoutPath|layout path]] of the application; + * - a relative path (e.g. "main"): the actual layout layout file will be looked for under the + * [[Module::viewPath|view path]] of the context module. + * + * If the layout name does not contain a file extension, it will use the default one `.php`. + * * @param string $view the view name. Please refer to [[findViewFile()]] on how to specify a view name. * @param array $params the parameters (name-value pairs) that should be made available in the view. * These parameters will not be available in the layout. @@ -301,10 +337,11 @@ class Controller extends Component */ public function render($view, $params = array()) { - $output = Yii::$app->getView()->render($view, $params, $this); + $viewFile = $this->findViewFile($view); + $output = $this->getView()->renderFile($viewFile, $params, $this); $layoutFile = $this->findLayoutFile(); if ($layoutFile !== false) { - return Yii::$app->getView()->renderFile($layoutFile, array('content' => $output), $this); + return $this->getView()->renderFile($layoutFile, array('content' => $output), $this); } else { return $output; } @@ -313,14 +350,15 @@ class Controller extends Component /** * Renders a view. * This method differs from [[render()]] in that it does not apply any layout. - * @param string $view the view name. Please refer to [[findViewFile()]] on how to specify a view name. + * @param string $view the view name. Please refer to [[render()]] on how to specify a view name. * @param array $params the parameters (name-value pairs) that should be made available in the view. * @return string the rendering result. * @throws InvalidParamException if the view file does not exist. */ public function renderPartial($view, $params = array()) { - return Yii::$app->getView()->render($view, $params, $this); + $viewFile = $this->findViewFile($view); + return $this->getView()->renderFile($viewFile, $params, $this); } /** @@ -332,7 +370,30 @@ class Controller extends Component */ public function renderFile($file, $params = array()) { - return Yii::$app->getView()->renderFile($file, $params, $this); + return $this->getView()->renderFile($file, $params, $this); + } + + /** + * Returns the view object that can be used to render views or view files. + * The [[render()]], [[renderPartial()]] and [[renderFile()]] methods will use + * this view object to implement the actual view rendering. + * @return View the view object that can be used to render views or view files. + */ + public function getView() + { + if ($this->_view === null) { + $this->_view = Yii::$app->getView(); + } + return $this->_view; + } + + /** + * Sets the view object to be used by this controller. + * @param View $view the view object that can be used to render views or view files. + */ + public function setView($view) + { + $this->_view = $view; } /** @@ -347,30 +408,33 @@ class Controller extends Component } /** + * Finds the view file based on the given view name. + * @param string $view the view name or the path alias of the view file. Please refer to [[render()]] + * on how to specify this parameter. + * @return string the view file path. Note that the file may not exist. + */ + protected function findViewFile($view) + { + if (strncmp($view, '@', 1) === 0) { + // e.g. "@app/views/main" + $file = Yii::getAlias($view); + } elseif (strncmp($view, '//', 2) === 0) { + // e.g. "//layouts/main" + $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + } elseif (strncmp($view, '/', 1) === 0) { + // e.g. "/site/index" + $file = $this->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + } else { + $file = $this->getViewPath() . DIRECTORY_SEPARATOR . $view; + } + + return pathinfo($file, PATHINFO_EXTENSION) === '' ? $file . '.php' : $file; + } + + /** * Finds the applicable layout file. - * - * This method locates an applicable layout file via two steps. - * - * In the first step, it determines the layout name and the context module: - * - * - If [[layout]] is specified as a string, use it as the layout name and [[module]] as the context module; - * - If [[layout]] is null, search through all ancestor modules of this controller and find the first - * module whose [[Module::layout|layout]] is not null. The layout and the corresponding module - * are used as the layout name and the context module, respectively. If such a module is not found - * or the corresponding layout is not a string, it will return false, meaning no applicable layout. - * - * In the second step, it determines the actual layout file according to the previously found layout name - * and context module. The layout name can be - * - * - a path alias (e.g. "@app/views/layouts/main"); - * - an absolute path (e.g. "/main"): the layout name starts with a slash. The actual layout file will be - * looked for under the [[Application::layoutPath|layout path]] of the application; - * - a relative path (e.g. "main"): the actual layout layout file will be looked for under the - * [[Module::viewPath|view path]] of the context module. - * - * If the layout name does not contain a file extension, it will use the default one `.php`. - * * @return string|boolean the layout file path, or false if layout is not needed. + * Please refer to [[render()]] on how to specify this parameter. * @throws InvalidParamException if an invalid path alias is used to specify the layout */ protected function findLayoutFile() @@ -399,7 +463,7 @@ class Controller extends Component $file = $module->getLayoutPath() . DIRECTORY_SEPARATOR . $view; } - if (FileHelper::getExtension($file) === '') { + if (pathinfo($file, PATHINFO_EXTENSION) === '') { $file .= '.php'; } return $file; diff --git a/framework/base/ErrorException.php b/framework/base/ErrorException.php index 465d839..b41e9ed 100644 --- a/framework/base/ErrorException.php +++ b/framework/base/ErrorException.php @@ -7,6 +7,8 @@ namespace yii\base; +use Yii; + /** * ErrorException represents a PHP error. * @@ -33,6 +35,32 @@ class ErrorException extends Exception $this->severity = $severity; $this->file = $filename; $this->line = $lineno; + + if (function_exists('xdebug_get_function_stack')) { + $trace = array_slice(array_reverse(xdebug_get_function_stack()), 3, -1); + foreach ($trace as &$frame) { + if (!isset($frame['function'])) { + $frame['function'] = 'unknown'; + } + + // XDebug < 2.1.1: http://bugs.xdebug.org/view.php?id=695 + if (!isset($frame['type']) || $frame['type'] === 'static') { + $frame['type'] = '::'; + } elseif ($frame['type'] === 'dynamic') { + $frame['type'] = '->'; + } + + // XDebug has a different key name + $frame['args'] = array(); + if (isset($frame['params']) && !isset($frame['args'])) { + $frame['args'] = $frame['params']; + } + } + + $ref = new \ReflectionProperty('Exception', 'trace'); + $ref->setAccessible(true); + $ref->setValue($this, $trace); + } } /** @@ -51,7 +79,7 @@ class ErrorException extends Exception * @param array $error error got from error_get_last() * @return bool if error is one of fatal type */ - public static function isFatalErorr($error) + public static function isFatalError($error) { return isset($error['type']) && in_array($error['type'], array(E_ERROR, E_PARSE, E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING)); } @@ -62,20 +90,20 @@ class ErrorException extends Exception public function getName() { $names = array( - E_ERROR => \Yii::t('yii|Fatal Error'), - E_PARSE => \Yii::t('yii|Parse Error'), - E_CORE_ERROR => \Yii::t('yii|Core Error'), - E_COMPILE_ERROR => \Yii::t('yii|Compile Error'), - E_USER_ERROR => \Yii::t('yii|User Error'), - E_WARNING => \Yii::t('yii|Warning'), - E_CORE_WARNING => \Yii::t('yii|Core Warning'), - E_COMPILE_WARNING => \Yii::t('yii|Compile Warning'), - E_USER_WARNING => \Yii::t('yii|User Warning'), - E_STRICT => \Yii::t('yii|Strict'), - E_NOTICE => \Yii::t('yii|Notice'), - E_RECOVERABLE_ERROR => \Yii::t('yii|Recoverable Error'), - E_DEPRECATED => \Yii::t('yii|Deprecated'), + E_ERROR => Yii::t('yii|Fatal Error'), + E_PARSE => Yii::t('yii|Parse Error'), + E_CORE_ERROR => Yii::t('yii|Core Error'), + E_COMPILE_ERROR => Yii::t('yii|Compile Error'), + E_USER_ERROR => Yii::t('yii|User Error'), + E_WARNING => Yii::t('yii|Warning'), + E_CORE_WARNING => Yii::t('yii|Core Warning'), + E_COMPILE_WARNING => Yii::t('yii|Compile Warning'), + E_USER_WARNING => Yii::t('yii|User Warning'), + E_STRICT => Yii::t('yii|Strict'), + E_NOTICE => Yii::t('yii|Notice'), + E_RECOVERABLE_ERROR => Yii::t('yii|Recoverable Error'), + E_DEPRECATED => Yii::t('yii|Deprecated'), ); - return isset($names[$this->getCode()]) ? $names[$this->getCode()] : \Yii::t('yii|Error'); + return isset($names[$this->getCode()]) ? $names[$this->getCode()] : Yii::t('yii|Error'); } } diff --git a/framework/base/ErrorHandler.php b/framework/base/ErrorHandler.php index f71b8c8..98a061d 100644 --- a/framework/base/ErrorHandler.php +++ b/framework/base/ErrorHandler.php @@ -16,8 +16,6 @@ namespace yii\base; * @author Qiang Xue * @since 2.0 */ -use yii\helpers\VarDumper; - class ErrorHandler extends Component { /** @@ -53,6 +51,7 @@ class ErrorHandler extends Component /** + * Handles exception * @param \Exception $exception */ public function handle($exception) @@ -63,10 +62,14 @@ class ErrorHandler extends Component $this->clearOutput(); } - $this->render($exception); + $this->renderException($exception); } - protected function render($exception) + /** + * Renders exception + * @param \Exception $exception + */ + protected function renderException($exception) { if ($this->errorAction !== null) { \Yii::$app->runAction($this->errorAction); @@ -78,13 +81,19 @@ class ErrorHandler extends Component if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') { \Yii::$app->renderException($exception); } else { + // if there is an error during error rendering it's useful to + // display PHP error in debug mode instead of a blank screen + if(YII_DEBUG) { + ini_set('display_errors', 1); + } + $view = new View; if (!YII_DEBUG || $exception instanceof UserException) { $viewName = $this->errorView; } else { $viewName = $this->exceptionView; } - echo $view->render($viewName, array( + echo $view->renderFile($viewName, array( 'exception' => $exception, ), $this); } @@ -198,6 +207,10 @@ class ErrorHandler extends Component echo '
' . $output . '
'; } + /** + * Renders calls stack trace + * @param array $trace + */ public function renderTrace($trace) { $count = 0; @@ -235,6 +248,11 @@ class ErrorHandler extends Component echo ''; } + /** + * Converts special characters to HTML entities + * @param string $text text to encode + * @return string + */ public function htmlEncode($text) { return htmlspecialchars($text, ENT_QUOTES, \Yii::$app->charset); @@ -255,7 +273,7 @@ class ErrorHandler extends Component { $view = new View; $name = !YII_DEBUG || $exception instanceof HttpException ? $this->errorView : $this->exceptionView; - echo $view->render($name, array( + echo $view->renderFile($name, array( 'exception' => $exception, ), $this); } diff --git a/framework/base/Event.php b/framework/base/Event.php index b86ed7c..5d40736 100644 --- a/framework/base/Event.php +++ b/framework/base/Event.php @@ -15,12 +15,14 @@ namespace yii\base; * And the [[handled]] property indicates if the event is handled. * If an event handler sets [[handled]] to be true, the rest of the * uninvoked handlers will no longer be called to handle the event. - * Additionally, an event may specify extra parameters via the [[data]] property. + * + * Additionally, when attaching an event handler, extra data may be passed + * and be available via the [[data]] property when the event handler is invoked. * * @author Qiang Xue * @since 2.0 */ -class Event extends \yii\base\Object +class Event extends Object { /** * @var string the event name. This property is set by [[Component::trigger()]]. @@ -39,7 +41,8 @@ class Event extends \yii\base\Object */ public $handled = false; /** - * @var mixed extra custom data associated with the event. + * @var mixed the data that is passed to [[Component::on()]] when attaching an event handler. + * Note that this varies according to which event handler is currently executing. */ public $data; } diff --git a/framework/base/HttpException.php b/framework/base/HttpException.php index 94a9a55..948d96b 100644 --- a/framework/base/HttpException.php +++ b/framework/base/HttpException.php @@ -29,11 +29,12 @@ class HttpException extends UserException * @param integer $status HTTP status code, such as 404, 500, etc. * @param string $message error message * @param integer $code error code + * @param \Exception $previous The previous exception used for the exception chaining. */ - public function __construct($status, $message = null, $code = 0) + public function __construct($status, $message = null, $code = 0, \Exception $previous = null) { $this->statusCode = $status; - parent::__construct($message, $code); + parent::__construct($message, $code, $previous); } /** diff --git a/framework/base/Model.php b/framework/base/Model.php index 13e567d..7f55239 100644 --- a/framework/base/Model.php +++ b/framework/base/Model.php @@ -8,8 +8,8 @@ namespace yii\base; use yii\helpers\StringHelper; -use yii\validators\Validator; use yii\validators\RequiredValidator; +use yii\validators\Validator; /** * Model is the base class for data models. @@ -169,6 +169,26 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess } /** + * Returns the form name that this model class should use. + * + * The form name is mainly used by [[\yii\web\ActiveForm]] to determine how to name + * the input fields for the attributes in a model. If the form name is "A" and an attribute + * name is "b", then the corresponding input name would be "A[b]". If the form name is + * an empty string, then the input name would be "b". + * + * By default, this method returns the model class name (without the namespace part) + * as the form name. You may override it when the model is used in different forms. + * + * @return string the form name of this model class. + */ + public function formName() + { + $class = get_class($this); + $pos = strrpos($class, '\\'); + return $pos === false ? $class : substr($class, $pos + 1); + } + + /** * Returns the list of attribute names. * By default, this method returns all public non-static properties of the class. * You may override this method to change the default behavior. @@ -541,7 +561,7 @@ class Model extends Component implements \IteratorAggregate, \ArrayAccess public function onUnsafeAttribute($name, $value) { if (YII_DEBUG) { - \Yii::info("Failed to set unsafe attribute '$name' in '" . get_class($this) . "'.", __CLASS__); + \Yii::info("Failed to set unsafe attribute '$name' in '" . get_class($this) . "'.", __METHOD__); } } diff --git a/framework/base/Module.php b/framework/base/Module.php index 6b82157..d99778d 100644 --- a/framework/base/Module.php +++ b/framework/base/Module.php @@ -170,7 +170,6 @@ abstract class Module extends Component */ public function init() { - Yii::setAlias('@' . $this->id, $this->getBasePath()); $this->preloadComponents(); } @@ -208,11 +207,17 @@ abstract class Module extends Component * Sets the root directory of the module. * This method can only be invoked at the beginning of the constructor. * @param string $path the root directory of the module. This can be either a directory name or a path alias. - * @throws Exception if the directory does not exist. + * @throws InvalidParamException if the directory does not exist. */ public function setBasePath($path) { - $this->_basePath = FileHelper::ensureDirectory($path); + $path = Yii::getAlias($path); + $p = realpath($path); + if ($p !== false && is_dir($p)) { + $this->_basePath = $p; + } else { + throw new InvalidParamException("The directory does not exist: $path"); + } } /** @@ -237,7 +242,7 @@ abstract class Module extends Component */ public function setControllerPath($path) { - $this->_controllerPath = FileHelper::ensureDirectory($path); + $this->_controllerPath = Yii::getAlias($path); } /** @@ -260,7 +265,7 @@ abstract class Module extends Component */ public function setViewPath($path) { - $this->_viewPath = FileHelper::ensureDirectory($path); + $this->_viewPath = Yii::getAlias($path); } /** @@ -283,20 +288,7 @@ abstract class Module extends Component */ public function setLayoutPath($path) { - $this->_layoutPath = FileHelper::ensureDirectory($path); - } - - /** - * Imports the specified path aliases. - * This method is provided so that you can import a set of path aliases when configuring a module. - * The path aliases will be imported by calling [[Yii::import()]]. - * @param array $aliases list of path aliases to be imported - */ - public function setImport($aliases) - { - foreach ($aliases as $alias) { - Yii::import($alias); - } + $this->_layoutPath = Yii::getAlias($path); } /** @@ -346,7 +338,7 @@ abstract class Module extends Component if ($this->_modules[$id] instanceof Module) { return $this->_modules[$id]; } elseif ($load) { - Yii::trace("Loading module: $id", __CLASS__); + Yii::trace("Loading module: $id", __METHOD__); return $this->_modules[$id] = Yii::createObject($this->_modules[$id], $id, $this); } } @@ -452,7 +444,7 @@ abstract class Module extends Component if ($this->_components[$id] instanceof Component) { return $this->_components[$id]; } elseif ($load) { - Yii::trace("Loading component: $id", __CLASS__); + Yii::trace("Loading component: $id", __METHOD__); return $this->_components[$id] = Yii::createObject($this->_components[$id]); } } @@ -580,8 +572,9 @@ abstract class Module extends Component * instance of it. * * @param string $route the route consisting of module, controller and action IDs. - * @return array|boolean if the controller is created successfully, it will be returned together - * with the remainder of the route which represents the action ID. Otherwise false will be returned. + * @return array|boolean If the controller is created successfully, it will be returned together + * with the requested action ID. Otherwise false will be returned. + * @throws InvalidConfigException if the controller class and its file do not match. */ public function createController($route) { @@ -605,16 +598,16 @@ abstract class Module extends Component $controller = Yii::createObject($this->controllerMap[$id], $id, $this); } elseif (preg_match('/^[a-z0-9\\-_]+$/', $id)) { $className = StringHelper::id2camel($id) . 'Controller'; - $classFile = $this->controllerPath . DIRECTORY_SEPARATOR . $className . '.php'; - if (is_file($classFile)) { - $className = $this->controllerNamespace . '\\' . $className; - if (!class_exists($className, false)) { - require($classFile); - } - if (class_exists($className, false) && is_subclass_of($className, '\yii\base\Controller')) { - $controller = new $className($id, $this); - } + if (!is_file($classFile)) { + return false; + } + $className = ltrim($this->controllerNamespace . '\\' . $className, '\\'); + Yii::$classMap[$className] = $classFile; + if (is_subclass_of($className, 'yii\base\Controller')) { + $controller = new $className($id, $this); + } elseif (YII_DEBUG) { + throw new InvalidConfigException("Controller class must extend from \\yii\\base\\Controller."); } } diff --git a/framework/base/Object.php b/framework/base/Object.php index 3bd8378..a547990 100644 --- a/framework/base/Object.php +++ b/framework/base/Object.php @@ -8,10 +8,7 @@ namespace yii\base; /** - * Object is the base class that provides the *property* feature. - * * @include @yii/base/Object.md - * * @author Qiang Xue * @since 2.0 */ diff --git a/framework/base/Response.php b/framework/base/Response.php index a53fd61..396b073 100644 --- a/framework/base/Response.php +++ b/framework/base/Response.php @@ -13,28 +13,38 @@ namespace yii\base; */ class Response extends Component { + /** + * Starts output buffering + */ public function beginOutput() { ob_start(); ob_implicit_flush(false); } + /** + * Returns contents of the output buffer and discards it + * @return string output buffer contents + */ public function endOutput() { return ob_get_clean(); } + /** + * Returns contents of the output buffer + * @return string output buffer contents + */ public function getOutput() { return ob_get_contents(); } - public function cleanOutput() - { - ob_clean(); - } - - public function removeOutput($all = true) + /** + * Discards the output buffer + * @param boolean $all if true recursively discards all output buffers used + */ + public function cleanOutput($all = true) { if ($all) { for ($level = ob_get_level(); $level > 0; --$level) { diff --git a/framework/base/Theme.php b/framework/base/Theme.php index 88ecb0a..a60d56e 100644 --- a/framework/base/Theme.php +++ b/framework/base/Theme.php @@ -33,11 +33,17 @@ use yii\helpers\FileHelper; class Theme extends Component { /** - * @var string the root path of this theme. + * @var string the root path or path alias of this theme. All resources of this theme are located + * under this directory. This property must be set if [[pathMap]] is not set. * @see pathMap */ public $basePath; /** + * @var string the base URL (or path alias) for this theme. All resources of this theme are considered + * to be under this base URL. This property must be set. It is mainly used by [[getUrl()]]. + */ + public $baseUrl; + /** * @var array the mapping between view directories and their corresponding themed versions. * If not set, it will be initialized as a mapping from [[Application::basePath]] to [[basePath]]. * This property is used by [[applyTo()]] when a view is trying to apply the theme. @@ -45,7 +51,6 @@ class Theme extends Component */ public $pathMap; - private $_baseUrl; /** * Initializes the theme. @@ -56,10 +61,10 @@ class Theme extends Component parent::init(); if (empty($this->pathMap)) { if ($this->basePath !== null) { - $this->basePath = FileHelper::ensureDirectory($this->basePath); + $this->basePath = Yii::getAlias($this->basePath); $this->pathMap = array(Yii::$app->getBasePath() => $this->basePath); } else { - throw new InvalidConfigException("Theme::basePath must be set."); + throw new InvalidConfigException('The "basePath" property must be set.'); } } $paths = array(); @@ -69,25 +74,11 @@ class Theme extends Component $paths[$from . DIRECTORY_SEPARATOR] = $to . DIRECTORY_SEPARATOR; } $this->pathMap = $paths; - } - - /** - * Returns the base URL for this theme. - * The method [[getUrl()]] will prefix this to the given URL. - * @return string the base URL for this theme. - */ - public function getBaseUrl() - { - return $this->_baseUrl; - } - - /** - * Sets the base URL for this theme. - * @param string $value the base URL for this theme. - */ - public function setBaseUrl($value) - { - $this->_baseUrl = rtrim(Yii::getAlias($value), '/'); + if ($this->baseUrl === null) { + throw new InvalidConfigException('The "baseUrl" property must be set.'); + } else { + $this->baseUrl = rtrim(Yii::getAlias($this->baseUrl), '/'); + } } /** @@ -112,7 +103,7 @@ class Theme extends Component } /** - * Converts a relative URL into an absolute URL using [[basePath]]. + * Converts a relative URL into an absolute URL using [[baseUrl]]. * @param string $url the relative URL to be converted. * @return string the absolute URL */ diff --git a/framework/base/UnknownClassException.php b/framework/base/UnknownClassException.php new file mode 100644 index 0000000..ac44746 --- /dev/null +++ b/framework/base/UnknownClassException.php @@ -0,0 +1,26 @@ + + * @since 2.0 + */ +class UnknownClassException extends Exception +{ + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return \Yii::t('yii|Unknown Class'); + } +} + diff --git a/framework/base/UnknownMethodException.php b/framework/base/UnknownMethodException.php index 29bedca..440e76e 100644 --- a/framework/base/UnknownMethodException.php +++ b/framework/base/UnknownMethodException.php @@ -8,7 +8,7 @@ namespace yii\base; /** - * UnknownMethodException represents an exception caused by accessing unknown object methods. + * UnknownMethodException represents an exception caused by accessing an unknown object method. * * @author Qiang Xue * @since 2.0 diff --git a/framework/base/View.php b/framework/base/View.php index c7087c1..10a7053 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -10,6 +10,7 @@ namespace yii\base; use Yii; use yii\base\Application; use yii\helpers\FileHelper; +use yii\helpers\Html; /** * View represents a view object in the MVC pattern. @@ -22,7 +23,46 @@ use yii\helpers\FileHelper; class View extends Component { /** - * @var object the object that owns this view. This can be a controller, a widget, or any other object. + * @event ViewEvent an event that is triggered by [[renderFile()]] right before it renders a view file. + */ + const EVENT_BEFORE_RENDER = 'beforeRender'; + /** + * @event ViewEvent an event that is triggered by [[renderFile()]] right after it renders a view file. + */ + const EVENT_AFTER_RENDER = 'afterRender'; + + /** + * The location of registered JavaScript code block or files. + * This means the location is in the head section. + */ + const POS_HEAD = 1; + /** + * The location of registered JavaScript code block or files. + * This means the location is at the beginning of the body section. + */ + const POS_BEGIN = 2; + /** + * The location of registered JavaScript code block or files. + * This means the location is at the end of the body section. + */ + const POS_END = 3; + /** + * This is internally used as the placeholder for receiving the content registered for the head section. + */ + const PL_HEAD = ''; + /** + * This is internally used as the placeholder for receiving the content registered for the beginning of the body section. + */ + const PL_BODY_BEGIN = ''; + /** + * This is internally used as the placeholder for receiving the content registered for the end of the body section. + */ + const PL_BODY_END = ''; + + + /** + * @var object the context under which the [[renderFile()]] method is being invoked. + * This can be a controller, a widget, or any other object. */ public $context; /** @@ -35,31 +75,75 @@ class View extends Component */ public $renderer; /** - * @var Theme|array the theme object or the configuration array for creating the theme. + * @var Theme|array the theme object or the configuration array for creating the theme object. * If not set, it means theming is not enabled. */ public $theme; /** - * @var array a list of named output clips. You can call [[beginClip()]] and [[endClip()]] - * to capture small fragments of a view. They can be later accessed at somewhere else + * @var array a list of named output blocks. The keys are the block names and the values + * are the corresponding block content. You can call [[beginBlock()]] and [[endBlock()]] + * to capture small fragments of a view. They can be later accessed somewhere else * through this property. */ - public $clips; + public $blocks; /** * @var Widget[] the widgets that are currently being rendered (not ended). This property * is maintained by [[beginWidget()]] and [[endWidget()]] methods. Do not modify it directly. + * @internal */ public $widgetStack = array(); /** * @var array a list of currently active fragment cache widgets. This property - * is used internally to implement the content caching feature. Do not modify it. + * is used internally to implement the content caching feature. Do not modify it directly. + * @internal */ public $cacheStack = array(); /** * @var array a list of placeholders for embedding dynamic contents. This property - * is used internally to implement the content caching feature. Do not modify it. + * is used internally to implement the content caching feature. Do not modify it directly. + * @internal */ public $dynamicPlaceholders = array(); + /** + * @var array the registered asset bundles. The keys are the bundle names, and the values + * are the corresponding [[AssetBundle]] objects. + * @see registerAssetBundle + */ + public $assetBundles; + /** + * @var string the page title + */ + public $title; + /** + * @var array the registered meta tags. + * @see registerMetaTag + */ + public $metaTags; + /** + * @var array the registered link tags. + * @see registerLinkTag + */ + public $linkTags; + /** + * @var array the registered CSS code blocks. + * @see registerCss + */ + public $css; + /** + * @var array the registered CSS files. + * @see registerCssFile + */ + public $cssFiles; + /** + * @var array the registered JS code blocks + * @see registerJs + */ + public $js; + /** + * @var array the registered JS files. + * @see registerJsFile + */ + public $jsFiles; /** @@ -79,22 +163,29 @@ class View extends Component /** * Renders a view. * - * This method will call [[findViewFile()]] to convert the view name into the corresponding view - * file path, and it will then call [[renderFile()]] to render the view. + * This method delegates the call to the [[context]] object: + * + * - If [[context]] is a controller, the [[Controller::renderPartial()]] method will be called; + * - If [[context]] is a widget, the [[Widget::render()]] method will be called; + * - Otherwise, an InvalidCallException exception will be thrown. * - * @param string $view the view name. Please refer to [[findViewFile()]] on how to specify this parameter. + * @param string $view the view name. Please refer to [[Controller::findViewFile()]] + * and [[Widget::findViewFile()]] on how to specify this parameter. * @param array $params the parameters (name-value pairs) that will be extracted and made available in the view file. - * @param object $context the context that the view should use for rendering the view. If null, - * existing [[context]] will be used. * @return string the rendering result + * @throws InvalidCallException if [[context]] is neither a controller nor a widget. * @throws InvalidParamException if the view cannot be resolved or the view file does not exist. * @see renderFile - * @see findViewFile */ - public function render($view, $params = array(), $context = null) + public function render($view, $params = array()) { - $viewFile = $this->findViewFile($context, $view); - return $this->renderFile($viewFile, $params, $context); + if ($this->context instanceof Controller) { + return $this->context->renderPartial($view, $params); + } elseif ($this->context instanceof Widget) { + return $this->context->render($view, $params); + } else { + throw new InvalidCallException('View::render() is not supported for the current context.'); + } } /** @@ -133,10 +224,14 @@ class View extends Component $this->context = $context; } - if ($this->renderer !== null) { - $output = $this->renderer->render($this, $viewFile, $params); - } else { - $output = $this->renderPhpFile($viewFile, $params); + $output = ''; + if ($this->beforeRender($viewFile)) { + if ($this->renderer !== null) { + $output = $this->renderer->render($this, $viewFile, $params); + } else { + $output = $this->renderPhpFile($viewFile, $params); + } + $this->afterRender($viewFile, $output); } $this->context = $oldContext; @@ -145,6 +240,38 @@ class View extends Component } /** + * This method is invoked right before [[renderFile()]] renders a view file. + * The default implementation will trigger the [[EVENT_BEFORE_RENDER]] event. + * If you override this method, make sure you call the parent implementation first. + * @param string $viewFile the view file to be rendered + * @return boolean whether to continue rendering the view file. + */ + public function beforeRender($viewFile) + { + $event = new ViewEvent($viewFile); + $this->trigger(self::EVENT_BEFORE_RENDER, $event); + return $event->isValid; + } + + /** + * This method is invoked right after [[renderFile()]] renders a view file. + * The default implementation will trigger the [[EVENT_AFTER_RENDER]] event. + * If you override this method, make sure you call the parent implementation first. + * @param string $viewFile the view file to be rendered + * @param string $output the rendering result of the view file. Updates to this parameter + * will be passed back and returned by [[renderFile()]]. + */ + public function afterRender($viewFile, &$output) + { + if ($this->hasEventHandlers(self::EVENT_AFTER_RENDER)) { + $event = new ViewEvent($viewFile); + $event->output = $output; + $this->trigger(self::EVENT_AFTER_RENDER, $event); + $output = $event->output; + } + } + + /** * Renders a view file as a PHP script. * * This method treats the view file as a PHP script and includes the file. @@ -179,7 +306,7 @@ class View extends Component { if (!empty($this->cacheStack)) { $n = count($this->dynamicPlaceholders); - $placeholder = ""; + $placeholder = ""; $this->addDynamicPlaceholder($placeholder, $statements); return $placeholder; } else { @@ -213,49 +340,6 @@ class View extends Component } /** - * Finds the view file based on the given view name. - * - * A view name can be specified in one of the following formats: - * - * - path alias (e.g. "@app/views/site/index"); - * - absolute path within application (e.g. "//site/index"): the view name starts with double slashes. - * The actual view file will be looked for under the [[Application::viewPath|view path]] of the application. - * - absolute path within module (e.g. "/site/index"): the view name starts with a single slash. - * The actual view file will be looked for under the [[Module::viewPath|view path]] of the currently - * active module. - * - relative path (e.g. "index"): the actual view file will be looked for under [[Controller::viewPath|viewPath]] - * of the context object, assuming the context is either a [[Controller]] or a [[Widget]]. - * - * If the view name does not contain a file extension, it will use the default one `.php`. - * - * @param object $context the view context object - * @param string $view the view name or the path alias of the view file. - * @return string the view file path. Note that the file may not exist. - * @throws InvalidParamException if the view file is an invalid path alias or the context cannot be - * used to determine the actual view file corresponding to the specified view. - */ - protected function findViewFile($context, $view) - { - if (strncmp($view, '@', 1) === 0) { - // e.g. "@app/views/main" - $file = Yii::getAlias($view); - } elseif (strncmp($view, '//', 2) === 0) { - // e.g. "//layouts/main" - $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); - } elseif (strncmp($view, '/', 1) === 0) { - // e.g. "/site/index" - $file = Yii::$app->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); - } elseif ($context instanceof Controller || $context instanceof Widget) { - /** @var $context Controller|Widget */ - $file = $context->getViewPath() . DIRECTORY_SEPARATOR . $view; - } else { - throw new InvalidParamException("Unable to resolve the view file for '$view'."); - } - - return FileHelper::getExtension($file) === '' ? $file . '.php' : $file; - } - - /** * Creates a widget. * This method will use [[Yii::createObject()]] to create the widget. * @param string $class the widget class name or path alias @@ -265,7 +349,10 @@ class View extends Component public function createWidget($class, $properties = array()) { $properties['class'] = $class; - return Yii::createObject($properties, $this->context); + if (!isset($properties['view'])) { + $properties['view'] = $this; + } + return Yii::createObject($properties, $this); } /** @@ -328,44 +415,50 @@ class View extends Component } /** - * Begins recording a clip. - * This method is a shortcut to beginning [[yii\widgets\Clip]] - * @param string $id the clip ID. - * @param boolean $renderInPlace whether to render the clip content in place. - * Defaults to false, meaning the captured clip will not be displayed. - * @return \yii\widgets\Clip the Clip widget instance - * @see \yii\widgets\Clip + * Begins recording a block. + * This method is a shortcut to beginning [[yii\widgets\Block]] + * @param string $id the block ID. + * @param boolean $renderInPlace whether to render the block content in place. + * Defaults to false, meaning the captured block will not be displayed. + * @return \yii\widgets\Block the Block widget instance */ - public function beginClip($id, $renderInPlace = false) + public function beginBlock($id, $renderInPlace = false) { - return $this->beginWidget('yii\widgets\Clip', array( + return $this->beginWidget('yii\widgets\Block', array( 'id' => $id, 'renderInPlace' => $renderInPlace, - 'view' => $this, )); } /** - * Ends recording a clip. + * Ends recording a block. */ - public function endClip() + public function endBlock() { $this->endWidget(); } /** * Begins the rendering of content that is to be decorated by the specified view. - * @param string $view the name of the view that will be used to decorate the content enclosed by this widget. - * Please refer to [[View::findViewFile()]] on how to set this property. + * This method can be used to implement nested layout. For example, a layout can be embedded + * in another layout file specified as '@app/view/layouts/base' like the following: + * + * ~~~ + * beginContent('@app/view/layouts/base'); ?> + * ...layout content here... + * endContent(); ?> + * ~~~ + * + * @param string $viewFile the view file that will be used to decorate the content enclosed by this widget. + * This can be specified as either the view file path or path alias. * @param array $params the variables (name=>value) to be extracted and made available in the decorative view. * @return \yii\widgets\ContentDecorator the ContentDecorator widget instance * @see \yii\widgets\ContentDecorator */ - public function beginContent($view, $params = array()) + public function beginContent($viewFile, $params = array()) { return $this->beginWidget('yii\widgets\ContentDecorator', array( - 'view' => $this, - 'viewName' => $view, + 'viewFile' => $viewFile, 'params' => $params, )); } @@ -400,7 +493,6 @@ class View extends Component public function beginCache($id, $properties = array()) { $properties['id'] = $id; - $properties['view'] = $this; /** @var $cache \yii\widgets\FragmentCache */ $cache = $this->beginWidget('yii\widgets\FragmentCache', $properties); if ($cache->getCachedContent() !== false) { @@ -418,4 +510,273 @@ class View extends Component { $this->endWidget(); } + + + private $_assetManager; + + /** + * Registers the asset manager being used by this view object. + * @return \yii\web\AssetManager the asset manager. Defaults to the "assetManager" application component. + */ + public function getAssetManager() + { + return $this->_assetManager ?: Yii::$app->getAssetManager(); + } + + /** + * Sets the asset manager. + * @param \yii\web\AssetManager $value the asset manager + */ + public function setAssetManager($value) + { + $this->_assetManager = $value; + } + + /** + * Marks the beginning of an HTML page. + */ + public function beginPage() + { + ob_start(); + ob_implicit_flush(false); + } + + /** + * Marks the ending of an HTML page. + */ + public function endPage() + { + $content = ob_get_clean(); + echo strtr($content, array( + self::PL_HEAD => $this->renderHeadHtml(), + self::PL_BODY_BEGIN => $this->renderBodyBeginHtml(), + self::PL_BODY_END => $this->renderBodyEndHtml(), + )); + + unset( + $this->assetBundles, + $this->metaTags, + $this->linkTags, + $this->css, + $this->cssFiles, + $this->js, + $this->jsFiles + ); + } + + /** + * Marks the beginning of an HTML body section. + */ + public function beginBody() + { + echo self::PL_BODY_BEGIN; + } + + /** + * Marks the ending of an HTML body section. + */ + public function endBody() + { + echo self::PL_BODY_END; + } + + /** + * Marks the position of an HTML head section. + */ + public function head() + { + echo self::PL_HEAD; + } + + /** + * Registers the named asset bundle. + * All dependent asset bundles will be registered. + * @param string $name the name of the asset bundle. + * @throws InvalidConfigException if the asset bundle does not exist or a circular dependency is detected + */ + public function registerAssetBundle($name) + { + if (!isset($this->assetBundles[$name])) { + $am = $this->getAssetManager(); + $bundle = $am->getBundle($name); + if ($bundle !== null) { + $this->assetBundles[$name] = false; + $bundle->registerAssets($this); + $this->assetBundles[$name] = true; + } else { + throw new InvalidConfigException("Unknown asset bundle: $name"); + } + } elseif ($this->assetBundles[$name] === false) { + throw new InvalidConfigException("A circular dependency is detected for bundle '$name'."); + } + } + + /** + * Registers a meta tag. + * @param array $options the HTML attributes for the meta tag. + * @param string $key the key that identifies the meta tag. If two meta tags are registered + * with the same key, the latter will overwrite the former. If this is null, the new meta tag + * will be appended to the existing ones. + */ + public function registerMetaTag($options, $key = null) + { + if ($key === null) { + $this->metaTags[] = Html::tag('meta', '', $options); + } else { + $this->metaTags[$key] = Html::tag('meta', '', $options); + } + } + + /** + * Registers a link tag. + * @param array $options the HTML attributes for the link tag. + * @param string $key the key that identifies the link tag. If two link tags are registered + * with the same key, the latter will overwrite the former. If this is null, the new link tag + * will be appended to the existing ones. + */ + public function registerLinkTag($options, $key = null) + { + if ($key === null) { + $this->linkTags[] = Html::tag('link', '', $options); + } else { + $this->linkTags[$key] = Html::tag('link', '', $options); + } + } + + /** + * Registers a CSS code block. + * @param string $css the CSS code block to be registered + * @param array $options the HTML attributes for the style tag. + * @param string $key the key that identifies the CSS code block. If null, it will use + * $css as the key. If two CSS code blocks are registered with the same key, the latter + * will overwrite the former. + */ + public function registerCss($css, $options = array(), $key = null) + { + $key = $key ?: $css; + $this->css[$key] = Html::style($css, $options); + } + + /** + * Registers a CSS file. + * @param string $url the CSS file to be registered. + * @param array $options the HTML attributes for the link tag. + * @param string $key the key that identifies the CSS script file. If null, it will use + * $url as the key. If two CSS files are registered with the same key, the latter + * will overwrite the former. + */ + public function registerCssFile($url, $options = array(), $key = null) + { + $key = $key ?: $url; + $this->cssFiles[$key] = Html::cssFile($url, $options); + } + + /** + * Registers a JS code block. + * @param string $js the JS code block to be registered + * @param array $options the HTML attributes for the script tag. A special option + * named "position" is supported which specifies where the JS script tag should be inserted + * in a page. The possible values of "position" are: + * + * - [[POS_HEAD]]: in the head section + * - [[POS_BEGIN]]: at the beginning of the body section + * - [[POS_END]]: at the end of the body section + * + * @param string $key the key that identifies the JS code block. If null, it will use + * $js as the key. If two JS code blocks are registered with the same key, the latter + * will overwrite the former. + */ + public function registerJs($js, $options = array(), $key = null) + { + $position = isset($options['position']) ? $options['position'] : self::POS_END; + unset($options['position']); + $key = $key ?: $js; + $this->js[$position][$key] = Html::script($js, $options); + } + + /** + * Registers a JS file. + * @param string $url the JS file to be registered. + * @param array $options the HTML attributes for the script tag. A special option + * named "position" is supported which specifies where the JS script tag should be inserted + * in a page. The possible values of "position" are: + * + * - [[POS_HEAD]]: in the head section + * - [[POS_BEGIN]]: at the beginning of the body section + * - [[POS_END]]: at the end of the body section + * + * @param string $key the key that identifies the JS script file. If null, it will use + * $url as the key. If two JS files are registered with the same key, the latter + * will overwrite the former. + */ + public function registerJsFile($url, $options = array(), $key = null) + { + $position = isset($options['position']) ? $options['position'] : self::POS_END; + unset($options['position']); + $key = $key ?: $url; + $this->jsFiles[$position][$key] = Html::jsFile($url, $options); + } + + /** + * Renders the content to be inserted in the head section. + * The content is rendered using the registered meta tags, link tags, CSS/JS code blocks and files. + * @return string the rendered content + */ + protected function renderHeadHtml() + { + $lines = array(); + if (!empty($this->metaTags)) { + $lines[] = implode("\n", $this->cssFiles); + } + if (!empty($this->linkTags)) { + $lines[] = implode("\n", $this->cssFiles); + } + if (!empty($this->cssFiles)) { + $lines[] = implode("\n", $this->cssFiles); + } + if (!empty($this->css)) { + $lines[] = implode("\n", $this->css); + } + if (!empty($this->jsFiles[self::POS_HEAD])) { + $lines[] = implode("\n", $this->jsFiles[self::POS_HEAD]); + } + if (!empty($this->js[self::POS_HEAD])) { + $lines[] = implode("\n", $this->js[self::POS_HEAD]); + } + return implode("\n", $lines); + } + + /** + * Renders the content to be inserted at the beginning of the body section. + * The content is rendered using the registered JS code blocks and files. + * @return string the rendered content + */ + protected function renderBodyBeginHtml() + { + $lines = array(); + if (!empty($this->jsFiles[self::POS_BEGIN])) { + $lines[] = implode("\n", $this->jsFiles[self::POS_BEGIN]); + } + if (!empty($this->js[self::POS_BEGIN])) { + $lines[] = implode("\n", $this->js[self::POS_BEGIN]); + } + return implode("\n", $lines); + } + + /** + * Renders the content to be inserted at the end of the body section. + * The content is rendered using the registered JS code blocks and files. + * @return string the rendered content + */ + protected function renderBodyEndHtml() + { + $lines = array(); + if (!empty($this->jsFiles[self::POS_END])) { + $lines[] = implode("\n", $this->jsFiles[self::POS_END]); + } + if (!empty($this->js[self::POS_END])) { + $lines[] = implode("\n", $this->js[self::POS_END]); + } + return implode("\n", $lines); + } } \ No newline at end of file diff --git a/framework/base/ViewEvent.php b/framework/base/ViewEvent.php new file mode 100644 index 0000000..cac7be4 --- /dev/null +++ b/framework/base/ViewEvent.php @@ -0,0 +1,44 @@ + + * @since 2.0 + */ +class ViewEvent extends Event +{ + /** + * @var string the rendering result of [[View::renderFile()]]. + * Event handlers may modify this property and the modified output will be + * returned by [[View::renderFile()]]. This property is only used + * by [[View::EVENT_AFTER_RENDER]] event. + */ + public $output; + /** + * @var string the view file path that is being rendered by [[View::renderFile()]]. + */ + public $viewFile; + /** + * @var boolean whether to continue rendering the view file. Event handlers of + * [[View::EVENT_BEFORE_RENDER]] may set this property to decide whether + * to continue rendering the current view file. + */ + public $isValid = true; + + /** + * Constructor. + * @param string $viewFile the view file path that is being rendered by [[View::renderFile()]]. + * @param array $config name-value pairs that will be used to initialize the object properties + */ + public function __construct($viewFile, $config = array()) + { + $this->viewFile = $viewFile; + parent::__construct($config); + } +} \ No newline at end of file diff --git a/framework/base/Widget.php b/framework/base/Widget.php index 24d0685..13e6d30 100644 --- a/framework/base/Widget.php +++ b/framework/base/Widget.php @@ -19,9 +19,11 @@ use yii\helpers\FileHelper; class Widget extends Component { /** - * @var Widget|Controller the owner/creator of this widget. It could be either a widget or a controller. + * @var View the view object that is used to create this widget. + * This property is automatically set by [[View::createWidget()]]. + * This property is required by [[render()]] and [[renderFile()]]. */ - public $owner; + public $view; /** * @var string id of the widget. */ @@ -32,17 +34,6 @@ class Widget extends Component private static $_counter = 0; /** - * Constructor. - * @param Widget|Controller $owner owner/creator of this widget. - * @param array $config name-value pairs that will be used to initialize the object properties - */ - public function __construct($owner, $config = array()) - { - $this->owner = $owner; - parent::__construct($config); - } - - /** * Returns the ID of the widget. * @param boolean $autoGenerate whether to generate an ID if it is not set previously * @return string ID of the widget. @@ -73,6 +64,18 @@ class Widget extends Component /** * Renders a view. + * The view to be rendered can be specified in one of the following formats: + * + * - path alias (e.g. "@app/views/site/index"); + * - absolute path within application (e.g. "//site/index"): the view name starts with double slashes. + * The actual view file will be looked for under the [[Application::viewPath|view path]] of the application. + * - absolute path within module (e.g. "/site/index"): the view name starts with a single slash. + * The actual view file will be looked for under the [[Module::viewPath|view path]] of the currently + * active module. + * - relative path (e.g. "index"): the actual view file will be looked for under [[viewPath]]. + * + * If the view name does not contain a file extension, it will use the default one `.php`. + * @param string $view the view name. Please refer to [[findViewFile()]] on how to specify a view name. * @param array $params the parameters (name-value pairs) that should be made available in the view. * @return string the rendering result. @@ -80,7 +83,7 @@ class Widget extends Component */ public function render($view, $params = array()) { - return Yii::$app->getView()->render($view, $params, $this); + return $this->view->render($view, $params, $this); } /** @@ -92,7 +95,7 @@ class Widget extends Component */ public function renderFile($file, $params = array()) { - return Yii::$app->getView()->renderFile($file, $params, $this); + return $this->view->renderFile($file, $params, $this); } /** @@ -106,4 +109,28 @@ class Widget extends Component $class = new \ReflectionClass($className); return dirname($class->getFileName()) . DIRECTORY_SEPARATOR . 'views'; } + + /** + * Finds the view file based on the given view name. + * @param string $view the view name or the path alias of the view file. Please refer to [[render()]] + * on how to specify this parameter. + * @return string the view file path. Note that the file may not exist. + */ + protected function findViewFile($view) + { + if (strncmp($view, '@', 1) === 0) { + // e.g. "@app/views/main" + $file = Yii::getAlias($view); + } elseif (strncmp($view, '//', 2) === 0) { + // e.g. "//layouts/main" + $file = Yii::$app->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + } elseif (strncmp($view, '/', 1) === 0 && Yii::$app->controller !== null) { + // e.g. "/site/index" + $file = Yii::$app->controller->module->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + } else { + $file = $this->getViewPath() . DIRECTORY_SEPARATOR . ltrim($view, '/'); + } + + return pathinfo($file, PATHINFO_EXTENSION) === '' ? $file . '.php' : $file; + } } \ No newline at end of file diff --git a/framework/caching/ApcCache.php b/framework/caching/ApcCache.php index dd954cc..391851d 100644 --- a/framework/caching/ApcCache.php +++ b/framework/caching/ApcCache.php @@ -11,6 +11,7 @@ namespace yii\caching; * ApcCache provides APC caching in terms of an application component. * * To use this application component, the [APC PHP extension](http://www.php.net/apc) must be loaded. + * In order to enable APC for CLI you should add "apc.enable_cli = 1" to your php.ini. * * See [[Cache]] for common cache operations that ApcCache supports. * diff --git a/framework/caching/ChainedDependency.php b/framework/caching/ChainedDependency.php index 9c4e547..7c7058e 100644 --- a/framework/caching/ChainedDependency.php +++ b/framework/caching/ChainedDependency.php @@ -22,11 +22,10 @@ namespace yii\caching; class ChainedDependency extends Dependency { /** - * @var array list of dependencies that this dependency is composed of. - * Each array element should be a dependency object or a configuration array - * that can be used to create a dependency object via [[\Yii::createObject()]]. + * @var Dependency[] list of dependencies that this dependency is composed of. + * Each array element must be a dependency object. */ - public $dependencies = array(); + public $dependencies; /** * @var boolean whether this dependency is depending on every dependency in [[dependencies]]. * Defaults to true, meaning if any of the dependencies has changed, this dependency is considered changed. @@ -37,9 +36,8 @@ class ChainedDependency extends Dependency /** * Constructor. - * @param array $dependencies list of dependencies that this dependency is composed of. - * Each array element should be a dependency object or a configuration array - * that can be used to create a dependency object via [[\Yii::createObject()]]. + * @param Dependency[] $dependencies list of dependencies that this dependency is composed of. + * Each array element should be a dependency object. * @param array $config name-value pairs that will be used to initialize the object properties */ public function __construct($dependencies = array(), $config = array()) @@ -54,10 +52,7 @@ class ChainedDependency extends Dependency public function evaluateDependency() { foreach ($this->dependencies as $dependency) { - if (!$dependency instanceof Dependency) { - $dependency = \Yii::createObject($dependency); - } - $dependency->evalulateDependency(); + $dependency->evaluateDependency(); } } @@ -79,10 +74,7 @@ class ChainedDependency extends Dependency */ public function getHasChanged() { - foreach ($this->dependencies as $i => $dependency) { - if (!$dependency instanceof Dependency) { - $this->dependencies[$i] = $dependency = \Yii::createObject($dependency); - } + foreach ($this->dependencies as $dependency) { if ($this->dependOnAll && $dependency->getHasChanged()) { return true; } elseif (!$this->dependOnAll && !$dependency->getHasChanged()) { diff --git a/framework/caching/DbCache.php b/framework/caching/DbCache.php index 3952852..dee8c7a 100644 --- a/framework/caching/DbCache.php +++ b/framework/caching/DbCache.php @@ -99,7 +99,7 @@ class DbCache extends Cache $query = new Query; $query->select(array('data')) ->from($this->cacheTable) - ->where('id = :id AND (expire = 0 OR expire >' . time() . ')', array(':id' => $key)); + ->where('[[id]] = :id AND ([[expire]] = 0 OR [[expire]] >' . time() . ')', array(':id' => $key)); if ($this->db->enableQueryCache) { // temporarily disable and re-enable query caching $this->db->enableQueryCache = false; @@ -125,7 +125,7 @@ class DbCache extends Cache $query->select(array('id', 'data')) ->from($this->cacheTable) ->where(array('id' => $keys)) - ->andWhere('(expire = 0 OR expire > ' . time() . ')'); + ->andWhere('([[expire]] = 0 OR [[expire]] > ' . time() . ')'); if ($this->db->enableQueryCache) { $this->db->enableQueryCache = false; @@ -227,7 +227,7 @@ class DbCache extends Cache { if ($force || mt_rand(0, 1000000) < $this->gcProbability) { $this->db->createCommand() - ->delete($this->cacheTable, 'expire > 0 AND expire < ' . time()) + ->delete($this->cacheTable, '[[expire]] > 0 AND [[expire]] < ' . time()) ->execute(); } } diff --git a/framework/caching/DbDependency.php b/framework/caching/DbDependency.php index cbe0ae1..7d45223 100644 --- a/framework/caching/DbDependency.php +++ b/framework/caching/DbDependency.php @@ -52,6 +52,7 @@ class DbDependency extends Dependency /** * Generates the data needed to determine if dependency has been changed. * This method returns the value of the global state. + * @throws InvalidConfigException * @return mixed the data needed to determine if dependency has been changed. */ protected function generateDependencyData() diff --git a/framework/caching/FileCache.php b/framework/caching/FileCache.php index e565cad..cc1a871 100644 --- a/framework/caching/FileCache.php +++ b/framework/caching/FileCache.php @@ -7,7 +7,7 @@ namespace yii\caching; -use yii\base\InvalidConfigException; +use Yii; /** * FileCache implements a cache component using files. @@ -51,7 +51,7 @@ class FileCache extends Cache public function init() { parent::init(); - $this->cachePath = \Yii::getAlias($this->cachePath); + $this->cachePath = Yii::getAlias($this->cachePath); if (!is_dir($this->cachePath)) { mkdir($this->cachePath, 0777, true); } diff --git a/framework/caching/MemCache.php b/framework/caching/MemCache.php index df07b8e..20aff21 100644 --- a/framework/caching/MemCache.php +++ b/framework/caching/MemCache.php @@ -106,7 +106,7 @@ class MemCache extends Cache /** * Returns the underlying memcache (or memcached) object. * @return \Memcache|\Memcached the memcache (or memcached) object used by this cache component. - * @throws Exception if memcache or memcached extension is not loaded + * @throws InvalidConfigException if memcache or memcached extension is not loaded */ public function getMemcache() { diff --git a/framework/caching/ZendDataCache.php b/framework/caching/ZendDataCache.php index 669733d..5b41a8d 100644 --- a/framework/caching/ZendDataCache.php +++ b/framework/caching/ZendDataCache.php @@ -10,7 +10,7 @@ namespace yii\caching; /** * ZendDataCache provides Zend data caching in terms of an application component. * - * To use this application component, the [Zend Data Cache PHP extensionn](http://www.zend.com/en/products/server/) + * To use this application component, the [Zend Data Cache PHP extension](http://www.zend.com/en/products/server/) * must be loaded. * * See [[Cache]] for common cache operations that ZendDataCache supports. diff --git a/framework/console/Application.php b/framework/console/Application.php index 574495b..2f28cac 100644 --- a/framework/console/Application.php +++ b/framework/console/Application.php @@ -129,6 +129,7 @@ class Application extends \yii\base\Application 'migrate' => 'yii\console\controllers\MigrateController', 'app' => 'yii\console\controllers\AppController', 'cache' => 'yii\console\controllers\CacheController', + 'asset' => 'yii\console\controllers\AssetController', ); } diff --git a/framework/console/Controller.php b/framework/console/Controller.php index 9924822..c7c5642 100644 --- a/framework/console/Controller.php +++ b/framework/console/Controller.php @@ -24,7 +24,6 @@ use yii\base\InvalidRouteException; * ~~~ * * @author Qiang Xue - * * @since 2.0 */ class Controller extends \yii\base\Controller diff --git a/framework/console/controllers/AppController.php b/framework/console/controllers/AppController.php index 93ef5f5..a47acfe 100644 --- a/framework/console/controllers/AppController.php +++ b/framework/console/controllers/AppController.php @@ -86,7 +86,7 @@ class AppController extends Controller $sourceDir = $this->getSourceDir(); $config = $this->getConfig(); - $list = FileHelper::buildFileList($sourceDir, $path); + $list = $this->buildFileList($sourceDir, $path); if(is_array($config)) { foreach($config as $file => $settings) { @@ -96,7 +96,7 @@ class AppController extends Controller } } - FileHelper::copyFiles($list); + $this->copyFiles($list); if(is_array($config)) { foreach($config as $file => $settings) { @@ -159,7 +159,7 @@ class AppController extends Controller * @param string $pathTo path to file we want to get relative path for * @param string $varName variable name w/o $ to replace value with relative path for * - * @return string target file contetns + * @return string target file contents */ public function replaceRelativePath($source, $pathTo, $varName) { @@ -204,4 +204,121 @@ class AppController extends Controller return '__DIR__.\''.$up.'/'.basename($path1).'\''; } + + + /** + * Copies a list of files from one place to another. + * @param array $fileList the list of files to be copied (name=>spec). + * The array keys are names displayed during the copy process, and array values are specifications + * for files to be copied. Each array value must be an array of the following structure: + *
    + *
  • source: required, the full path of the file/directory to be copied from
  • + *
  • target: required, the full path of the file/directory to be copied to
  • + *
  • callback: optional, the callback to be invoked when copying a file. The callback function + * should be declared as follows: + *
    +	 *   function foo($source,$params)
    +	 *   
    + * where $source parameter is the source file path, and the content returned + * by the function will be saved into the target file.
  • + *
  • params: optional, the parameters to be passed to the callback
  • + *
+ * @see buildFileList + */ + protected function copyFiles($fileList) + { + $overwriteAll = false; + foreach($fileList as $name=>$file) { + $source = strtr($file['source'], '/\\', DIRECTORY_SEPARATOR); + $target = strtr($file['target'], '/\\', DIRECTORY_SEPARATOR); + $callback = isset($file['callback']) ? $file['callback'] : null; + $params = isset($file['params']) ? $file['params'] : null; + + if(is_dir($source)) { + if (!is_dir($target)) { + mkdir($target, 0777, true); + } + continue; + } + + if($callback !== null) { + $content = call_user_func($callback, $source, $params); + } + else { + $content = file_get_contents($source); + } + if(is_file($target)) { + if($content === file_get_contents($target)) { + echo " unchanged $name\n"; + continue; + } + if($overwriteAll) { + echo " overwrite $name\n"; + } + else { + echo " exist $name\n"; + echo " ...overwrite? [Yes|No|All|Quit] "; + $answer = trim(fgets(STDIN)); + if(!strncasecmp($answer, 'q', 1)) { + return; + } + elseif(!strncasecmp($answer, 'y', 1)) { + echo " overwrite $name\n"; + } + elseif(!strncasecmp($answer, 'a', 1)) { + echo " overwrite $name\n"; + $overwriteAll = true; + } + else { + echo " skip $name\n"; + continue; + } + } + } + else { + if (!is_dir(dirname($target))) { + mkdir(dirname($target), 0777, true); + } + echo " generate $name\n"; + } + file_put_contents($target, $content); + } + } + + /** + * Builds the file list of a directory. + * This method traverses through the specified directory and builds + * a list of files and subdirectories that the directory contains. + * The result of this function can be passed to {@link copyFiles}. + * @param string $sourceDir the source directory + * @param string $targetDir the target directory + * @param string $baseDir base directory + * @param array $ignoreFiles list of the names of files that should + * be ignored in list building process. + * @param array $renameMap hash array of file names that should be + * renamed. Example value: array('1.old.txt'=>'2.new.txt'). + * @return array the file list (see {@link copyFiles}) + */ + protected function buildFileList($sourceDir, $targetDir, $baseDir='', $ignoreFiles=array(), $renameMap=array()) + { + $list = array(); + $handle = opendir($sourceDir); + while(($file = readdir($handle)) !== false) { + if(in_array($file, array('.', '..', '.svn', '.gitignore', '.hgignore')) || in_array($file, $ignoreFiles)) { + continue; + } + $sourcePath = $sourceDir.DIRECTORY_SEPARATOR.$file; + $targetPath = $targetDir.DIRECTORY_SEPARATOR.strtr($file, $renameMap); + $name = $baseDir === '' ? $file : $baseDir.'/'.$file; + $list[$name] = array( + 'source' => $sourcePath, + 'target' => $targetPath, + ); + if(is_dir($sourcePath)) { + $list = array_merge($list, self::buildFileList($sourcePath, $targetPath, $name, $ignoreFiles, $renameMap)); + } + } + closedir($handle); + return $list; + } } \ No newline at end of file diff --git a/framework/console/controllers/AssetController.php b/framework/console/controllers/AssetController.php new file mode 100644 index 0000000..71a2cae --- /dev/null +++ b/framework/console/controllers/AssetController.php @@ -0,0 +1,353 @@ + + * @since 2.0 + */ +class AssetController extends Controller +{ + public $defaultAction = 'compress'; + + public $bundles = array(); + public $extensions = array(); + /** + * @var array + * ~~~ + * 'all' => array( + * 'css' => 'all.css', + * 'js' => 'js.css', + * 'depends' => array( ... ), + * ) + * ~~~ + */ + public $targets = array(); + public $assetManager = array(); + public $jsCompressor = 'java -jar compiler.jar --js {from} --js_output_file {to}'; + public $cssCompressor = 'java -jar yuicompressor.jar {from} -o {to}'; + + public function actionCompress($configFile, $bundleFile) + { + $this->loadConfiguration($configFile); + $bundles = $this->loadBundles($this->bundles, $this->extensions); + $targets = $this->loadTargets($this->targets, $bundles); + $this->publishBundles($bundles, $this->publishOptions); + $timestamp = time(); + foreach ($targets as $target) { + if (!empty($target->js)) { + $this->buildTarget($target, 'js', $bundles, $timestamp); + } + if (!empty($target->css)) { + $this->buildTarget($target, 'css', $bundles, $timestamp); + } + } + + $targets = $this->adjustDependency($targets, $bundles); + $this->saveTargets($targets, $bundleFile); + } + + protected function loadConfiguration($configFile) + { + foreach (require($configFile) as $name => $value) { + if (property_exists($this, $name)) { + $this->$name = $value; + } else { + throw new Exception("Unknown configuration option: $name"); + } + } + + if (!isset($this->assetManager['basePath'])) { + throw new Exception("Please specify 'basePath' for the 'assetManager' option."); + } + if (!isset($this->assetManager['baseUrl'])) { + throw new Exception("Please specify 'baseUrl' for the 'assetManager' option."); + } + } + + protected function loadBundles($bundles, $extensions) + { + $result = array(); + foreach ($bundles as $name => $bundle) { + $bundle['class'] = 'yii\\web\\AssetBundle'; + $result[$name] = Yii::createObject($bundle); + } + foreach ($extensions as $path) { + $manifest = $path . '/assets.php'; + if (!is_file($manifest)) { + continue; + } + foreach (require($manifest) as $name => $bundle) { + if (!isset($result[$name])) { + $bundle['class'] = 'yii\\web\\AssetBundle'; + $result[$name] = Yii::createObject($bundle); + } + } + } + return $result; + } + + protected function loadTargets($targets, $bundles) + { + // build the dependency order of bundles + $registered = array(); + foreach ($bundles as $name => $bundle) { + $this->registerBundle($bundles, $name, $registered); + } + $bundleOrders = array_combine(array_keys($registered), range(0, count($bundles) - 1)); + + // fill up the target which has empty 'depends'. + $referenced = array(); + foreach ($targets as $name => $target) { + if (empty($target['depends'])) { + if (!isset($all)) { + $all = $name; + } else { + throw new Exception("Only one target can have empty 'depends' option. Found two now: $all, $name"); + } + } else { + foreach ($target['depends'] as $bundle) { + if (!isset($referenced[$bundle])) { + $referenced[$bundle] = $name; + } else { + throw new Exception("Target '{$referenced[$bundle]}' and '$name' cannot contain the bundle '$bundle' at the same time."); + } + } + } + } + if (isset($all)) { + $targets[$all]['depends'] = array_diff(array_keys($registered), array_keys($referenced)); + } + + // adjust the 'depends' order for each target according to the dependency order of bundles + // create an AssetBundle object for each target + foreach ($targets as $name => $target) { + if (!isset($target['basePath'])) { + throw new Exception("Please specify 'basePath' for the '$name' target."); + } + if (!isset($target['baseUrl'])) { + throw new Exception("Please specify 'baseUrl' for the '$name' target."); + } + usort($target['depends'], function ($a, $b) use ($bundleOrders) { + if ($bundleOrders[$a] == $bundleOrders[$b]) { + return 0; + } else { + return $bundleOrders[$a] > $bundleOrders[$b] ? 1 : -1; + } + }); + $target['class'] = 'yii\\web\\AssetBundle'; + $targets[$name] = Yii::createObject($target); + } + return $targets; + } + + /** + * @param \yii\web\AssetBundle[] $bundles + * @param array $options + */ + protected function publishBundles($bundles, $options) + { + if (!isset($options['class'])) { + $options['class'] = 'yii\\web\\AssetManager'; + } + $am = Yii::createObject($options); + foreach ($bundles as $bundle) { + $bundle->publish($am); + } + } + + /** + * @param \yii\web\AssetBundle $target + * @param string $type either "js" or "css" + * @param \yii\web\AssetBundle[] $bundles + * @param integer $timestamp + * @throws Exception + */ + protected function buildTarget($target, $type, $bundles, $timestamp) + { + $outputFile = strtr($target->$type, array( + '{ts}' => $timestamp, + )); + $inputFiles = array(); + + foreach ($target->depends as $name) { + if (isset($bundles[$name])) { + foreach ($bundles[$name]->$type as $file) { + $inputFiles[] = $bundles[$name]->basePath . '/' . $file; + } + } else { + throw new Exception("Unknown bundle: $name"); + } + } + if ($type === 'js') { + $this->compressJsFiles($inputFiles, $target->basePath . '/' . $outputFile); + } else { + $this->compressCssFiles($inputFiles, $target->basePath . '/' . $outputFile); + } + $target->$type = array($outputFile); + } + + protected function adjustDependency($targets, $bundles) + { + $map = array(); + foreach ($targets as $name => $target) { + foreach ($target->depends as $bundle) { + $map[$bundle] = $name; + } + } + + foreach ($targets as $name => $target) { + $depends = array(); + foreach ($target->depends as $bn) { + foreach ($bundles[$bn]->depends as $bundle) { + $depends[$map[$bundle]] = true; + } + } + unset($depends[$name]); + $target->depends = array_keys($depends); + } + + // detect possible circular dependencies + foreach ($targets as $name => $target) { + $registered = array(); + $this->registerBundle($targets, $name, $registered); + } + + foreach ($map as $bundle => $target) { + $targets[$bundle] = Yii::createObject(array( + 'class' => 'yii\\web\\AssetBundle', + 'depends' => array($target), + )); + } + return $targets; + } + + protected function registerBundle($bundles, $name, &$registered) + { + if (!isset($registered[$name])) { + $registered[$name] = false; + $bundle = $bundles[$name]; + foreach ($bundle->depends as $depend) { + $this->registerBundle($bundles, $depend, $registered); + } + unset($registered[$name]); + $registered[$name] = true; + } elseif ($registered[$name] === false) { + throw new Exception("A circular dependency is detected for target '$name'."); + } + } + + protected function saveTargets($targets, $bundleFile) + { + $array = array(); + foreach ($targets as $name => $target) { + foreach (array('js', 'css', 'depends', 'basePath', 'baseUrl') as $prop) { + if (!empty($target->$prop)) { + $array[$name][$prop] = $target->$prop; + } + } + } + $array = var_export($array, true); + $version = date('Y-m-d H:i:s', time()); + file_put_contents($bundleFile, <<jsCompressor)) { + $tmpFile = $outputFile . '.tmp'; + $this->combineJsFiles($inputFiles, $tmpFile); + $log = shell_exec(strtr($this->jsCompressor, array( + '{from}' => $tmpFile, + '{to}' => $outputFile, + ))); + @unlink($tmpFile); + } else { + $log = call_user_func($this->jsCompressor, $this, $inputFiles, $outputFile); + } + } + + protected function compressCssFiles($inputFiles, $outputFile) + { + if (is_string($this->cssCompressor)) { + $tmpFile = $outputFile . '.tmp'; + $this->combineCssFiles($inputFiles, $tmpFile); + $log = shell_exec(strtr($this->cssCompressor, array( + '{from}' => $inputFiles, + '{to}' => $outputFile, + ))); + } else { + $log = call_user_func($this->cssCompressor, $this, $inputFiles, $outputFile); + } + } + + public function combineJsFiles($files, $tmpFile) + { + $content = ''; + foreach ($files as $file) { + $content .= "/*** BEGIN FILE: $file ***/\n" + . file_get_contents($file) + . "/*** END FILE: $file ***/\n"; + } + file_put_contents($tmpFile, $content); + } + + public function combineCssFiles($files, $tmpFile) + { + // todo: adjust url() references in CSS files + $content = ''; + foreach ($files as $file) { + $content .= "/*** BEGIN FILE: $file ***/\n" + . file_get_contents($file) + . "/*** END FILE: $file ***/\n"; + } + file_put_contents($tmpFile, $content); + } + + public function actionTemplate($configFile) + { + $template = << require('path/to/bundles.php'), + // + 'extensions' => require('path/to/namespaces.php'), + // + 'targets' => array( + 'all' => array( + 'basePath' => __DIR__, + 'baseUrl' => '/test', + 'js' => 'all-{ts}.js', + 'css' => 'all-{ts}.css', + ), + ), + + 'assetManager' => array( + 'basePath' => __DIR__, + 'baseUrl' => '/test', + ), +); +EOD; + file_put_contents($configFile, $template); + } +} \ No newline at end of file diff --git a/framework/console/controllers/HelpController.php b/framework/console/controllers/HelpController.php index ea7e3d5..74c354b 100644 --- a/framework/console/controllers/HelpController.php +++ b/framework/console/controllers/HelpController.php @@ -9,9 +9,9 @@ namespace yii\console\controllers; use Yii; use yii\base\Application; -use yii\console\Exception; use yii\base\InlineAction; use yii\console\Controller; +use yii\console\Exception; use yii\console\Request; use yii\helpers\StringHelper; @@ -128,7 +128,7 @@ class HelpController extends Controller $files = scandir($module->getControllerPath()); foreach ($files as $file) { - if(strcmp(substr($file,-14),'Controller.php') === 0 && is_file($file)) { + if (strcmp(substr($file, -14), 'Controller.php') === 0) { $commands[] = $prefix . lcfirst(substr(basename($file), 0, -14)); } } diff --git a/framework/console/webapp/default/index.php b/framework/console/webapp/default/index.php index 461b364..b84e257 100644 --- a/framework/console/webapp/default/index.php +++ b/framework/console/webapp/default/index.php @@ -1,10 +1,10 @@ run(); \ No newline at end of file diff --git a/framework/console/webapp/default/protected/config/main.php b/framework/console/webapp/default/protected/config/main.php index 1e3f981..795811e 100644 --- a/framework/console/webapp/default/protected/config/main.php +++ b/framework/console/webapp/default/protected/config/main.php @@ -1,5 +1,6 @@ 'webapp', 'name' => 'My Web Application', 'components' => array( @@ -12,5 +13,8 @@ return array( 'password' => '', ), */ + 'cache' => array( + 'class' => 'yii\caching\DummyCache', + ), ), ); \ No newline at end of file diff --git a/framework/console/webapp/default/protected/views/layouts/main.php b/framework/console/webapp/default/protected/views/layouts/main.php index 197b4a2..5c883e6 100644 --- a/framework/console/webapp/default/protected/views/layouts/main.php +++ b/framework/console/webapp/default/protected/views/layouts/main.php @@ -1,11 +1,12 @@ + - + - <?php echo $this->context->pageTitle?> + <?php echo Html::encode($this->title)?> -

context->pageTitle?>

+

title)?>

diff --git a/framework/db/ActiveRecord.php b/framework/db/ActiveRecord.php index d8f2f65..45c53fb 100644 --- a/framework/db/ActiveRecord.php +++ b/framework/db/ActiveRecord.php @@ -191,15 +191,12 @@ class ActiveRecord extends Model */ public static function updateAllCounters($counters, $condition = '', $params = array()) { - $db = static::getDb(); $n = 0; foreach ($counters as $name => $value) { - $quotedName = $db->quoteColumnName($name); - $counters[$name] = new Expression("$quotedName+:bp{$n}"); - $params[":bp{$n}"] = $value; + $counters[$name] = new Expression("[[$name]]+:bp{$n}", array(":bp{$n}" => $value)); $n++; } - $command = $db->createCommand(); + $command = static::getDb()->createCommand(); $command->update(static::tableName(), $counters, $condition, $params); return $command->execute(); } @@ -280,6 +277,34 @@ class ActiveRecord extends Model } /** + * Returns the name of the column that stores the lock version for implementing optimistic locking. + * + * Optimistic locking allows multiple users to access the same record for edits and avoids + * potential conflicts. In case when a user attempts to save the record upon some staled data + * (because another user has modified the data), a [[StaleObjectException]] exception will be thrown, + * and the update or deletion is skipped. + * + * Optimized locking is only supported by [[update()]] and [[delete()]]. + * + * To use optimized locking: + * + * 1. Create a column to store the version number of each row. The column type should be `BIGINT DEFAULT 0`. + * Override this method to return the name of this column. + * 2. In the Web form that collects the user input, add a hidden field that stores + * the lock version of the recording being updated. + * 3. In the controller action that does the data updating, try to catch the [[StaleObjectException]] + * and implement necessary business logic (e.g. merging the changes, prompting stated data) + * to resolve the conflict. + * + * @return string the column name that stores the lock version of a table row. + * If null is returned (default implemented), optimistic locking will not be supported. + */ + public function optimisticLock() + { + return null; + } + + /** * PHP getter magic method. * This method is overridden so that attributes and related objects can be accessed like properties. * @param string $name property name @@ -530,8 +555,8 @@ class ActiveRecord extends Model */ public function isAttributeChanged($name) { - if (isset($this->_attribute[$name], $this->_oldAttributes[$name])) { - return $this->_attribute[$name] !== $this->_oldAttributes[$name]; + if (isset($this->_attributes[$name], $this->_oldAttributes[$name])) { + return $this->_attributes[$name] !== $this->_oldAttributes[$name]; } else { return isset($this->_attributes[$name]) || isset($this->_oldAttributes); } @@ -590,7 +615,11 @@ class ActiveRecord extends Model */ public function save($runValidation = true, $attributes = null) { - return $this->getIsNewRecord() ? $this->insert($runValidation, $attributes) : $this->update($runValidation, $attributes); + if ($this->getIsNewRecord()) { + return $this->insert($runValidation, $attributes); + } else { + return $this->update($runValidation, $attributes) !== false; + } } /** @@ -692,11 +721,26 @@ class ActiveRecord extends Model * $customer->update(); * ~~~ * + * Note that it is possible the update does not affect any row in the table. + * In this case, this method will return 0. For this reason, you should use the following + * code to check if update() is successful or not: + * + * ~~~ + * if ($this->update() !== false) { + * // update successful + * } else { + * // update failed + * } + * ~~~ + * * @param boolean $runValidation whether to perform validation before saving the record. * If the validation fails, the record will not be inserted into the database. * @param array $attributes list of attributes that need to be saved. Defaults to null, * meaning all attributes that are loaded from DB will be saved. - * @return boolean whether the attributes are valid and the record is updated successfully. + * @return integer|boolean the number of rows affected, or false if validation fails + * or [[beforeSave()]] stops the updating process. + * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data + * being updated is outdated. */ public function update($runValidation = true, $attributes = null) { @@ -706,15 +750,31 @@ class ActiveRecord extends Model if ($this->beforeSave(false)) { $values = $this->getDirtyAttributes($attributes); if ($values !== array()) { + $condition = $this->getOldPrimaryKey(true); + $lock = $this->optimisticLock(); + if ($lock !== null) { + if (!isset($values[$lock])) { + $values[$lock] = $this->$lock + 1; + } + $condition[$lock] = $this->$lock; + } // We do not check the return value of updateAll() because it's possible // that the UPDATE statement doesn't change anything and thus returns 0. - $this->updateAll($values, $this->getOldPrimaryKey(true)); + $rows = $this->updateAll($values, $condition); + + if ($lock !== null && !$rows) { + throw new StaleObjectException('The object being updated is outdated.'); + } + foreach ($values as $name => $value) { $this->_oldAttributes[$name] = $this->_attributes[$name]; } + $this->afterSave(false); + return $rows; + } else { + return 0; } - return true; } else { return false; } @@ -763,17 +823,28 @@ class ActiveRecord extends Model * In the above step 1 and 3, events named [[EVENT_BEFORE_DELETE]] and [[EVENT_AFTER_DELETE]] * will be raised by the corresponding methods. * - * @return boolean whether the deletion is successful. + * @return integer|boolean the number of rows deleted, or false if the deletion is unsuccessful for some reason. + * Note that it is possible the number of rows deleted is 0, even though the deletion execution is successful. + * @throws StaleObjectException if [[optimisticLock|optimistic locking]] is enabled and the data + * being deleted is outdated. */ public function delete() { if ($this->beforeDelete()) { // we do not check the return value of deleteAll() because it's possible // the record is already deleted in the database and thus the method will return 0 - $this->deleteAll($this->getPrimaryKey(true)); + $condition = $this->getOldPrimaryKey(true); + $lock = $this->optimisticLock(); + if ($lock !== null) { + $condition[$lock] = $this->$lock; + } + $rows = $this->deleteAll($condition); + if ($lock !== null && !$rows) { + throw new StaleObjectException('The object being deleted is outdated.'); + } $this->_oldAttributes = null; $this->afterDelete(); - return true; + return $rows; } else { return false; } diff --git a/framework/db/Command.php b/framework/db/Command.php index ecd3674..dc6c972 100644 --- a/framework/db/Command.php +++ b/framework/db/Command.php @@ -84,39 +84,51 @@ class Command extends \yii\base\Component /** * Specifies the SQL statement to be executed. - * Any previous execution will be terminated or cancelled. + * The previous SQL execution (if any) will be cancelled, and [[params]] will be cleared as well. * @param string $sql the SQL statement to be set. * @return Command this command instance */ public function setSql($sql) { if ($sql !== $this->_sql) { - if ($this->db->enableAutoQuoting && $sql != '') { - $sql = $this->expandSql($sql); - } $this->cancel(); - $this->_sql = $sql; + $this->_sql = $this->db->quoteSql($sql); $this->_params = array(); } return $this; } /** - * Expands a SQL statement by quoting table and column names and replacing table prefixes. - * @param string $sql the SQL to be expanded - * @return string the expanded SQL + * Returns the raw SQL by inserting parameter values into the corresponding placeholders in [[sql]]. + * Note that the return value of this method should mainly be used for logging purpose. + * It is likely that this method returns an invalid SQL due to improper replacement of parameter placeholders. + * @return string the raw SQL */ - protected function expandSql($sql) + public function getRawSql() { - $db = $this->db; - return preg_replace_callback('/(\\{\\{(.*?)\\}\\}|\\[\\[(.*?)\\]\\])/', function($matches) use($db) { - if (isset($matches[3])) { - return $db->quoteColumnName($matches[3]); + if ($this->_params === array()) { + return $this->_sql; + } else { + $params = array(); + foreach ($this->_params as $name => $value) { + if (is_string($value)) { + $params[$name] = $this->db->quoteValue($value); + } elseif ($value === null) { + $params[$name] = 'NULL'; + } else { + $params[$name] = $value; + } + } + if (isset($params[1])) { + $sql = ''; + foreach (explode('?', $this->_sql) as $i => $part) { + $sql .= (isset($params[$i]) ? $params[$i] : '') . $part; + } + return $sql; } else { - $name = str_replace('%', $db->tablePrefix, $matches[2]); - return $db->quoteTableName($name); + return strtr($this->_sql, $params); } - }, $sql); + } } /** @@ -134,7 +146,7 @@ class Command extends \yii\base\Component try { $this->pdoStatement = $this->db->pdo->prepare($sql); } catch (\Exception $e) { - Yii::error($e->getMessage() . "\nFailed to prepare SQL: $sql", __CLASS__); + Yii::error($e->getMessage() . "\nFailed to prepare SQL: $sql", __METHOD__); $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; throw new Exception($e->getMessage(), $errorInfo, (int)$e->getCode()); } @@ -243,6 +255,7 @@ class Command extends \yii\base\Component 'boolean' => \PDO::PARAM_BOOL, 'integer' => \PDO::PARAM_INT, 'string' => \PDO::PARAM_STR, + 'resource' => \PDO::PARAM_LOB, 'NULL' => \PDO::PARAM_NULL, ); $type = gettype($data); @@ -260,21 +273,18 @@ class Command extends \yii\base\Component { $sql = $this->getSql(); - if ($this->_params === array()) { - $paramLog = ''; - } else { - $paramLog = "\nParameters: " . var_export($this->_params, true); - } + $rawSql = $this->getRawSql(); - Yii::trace("Executing SQL: {$sql}{$paramLog}", __CLASS__); + Yii::trace("Executing SQL: $rawSql", __METHOD__); if ($sql == '') { return 0; } try { + $token = "SQL: $sql"; if ($this->db->enableProfiling) { - Yii::beginProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::beginProfile($token, __METHOD__); } $this->prepare(); @@ -282,16 +292,16 @@ class Command extends \yii\base\Component $n = $this->pdoStatement->rowCount(); if ($this->db->enableProfiling) { - Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::endProfile($token, __METHOD__); } return $n; } catch (\Exception $e) { if ($this->db->enableProfiling) { - Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::endProfile($token, __METHOD__); } $message = $e->getMessage(); - Yii::error("$message\nFailed to execute SQL: {$sql}{$paramLog}", __CLASS__); + Yii::error("$message\nFailed to execute SQL: $rawSql", __METHOD__); $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; throw new Exception($message, $errorInfo, (int)$e->getCode()); @@ -377,13 +387,9 @@ class Command extends \yii\base\Component { $db = $this->db; $sql = $this->getSql(); - if ($this->_params === array()) { - $paramLog = ''; - } else { - $paramLog = "\nParameters: " . var_export($this->_params, true); - } + $rawSql = $this->getRawSql(); - Yii::trace("Querying SQL: {$sql}{$paramLog}", __CLASS__); + Yii::trace("Querying SQL: $rawSql", __METHOD__); /** @var $cache \yii\caching\Cache */ if ($db->enableQueryCache && $method !== '') { @@ -395,18 +401,18 @@ class Command extends \yii\base\Component __CLASS__, $db->dsn, $db->username, - $sql, - $paramLog, + $rawSql, )); if (($result = $cache->get($cacheKey)) !== false) { - Yii::trace('Query result served from cache', __CLASS__); + Yii::trace('Query result served from cache', __METHOD__); return $result; } } try { + $token = "SQL: $sql"; if ($db->enableProfiling) { - Yii::beginProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::beginProfile($token, __METHOD__); } $this->prepare(); @@ -423,21 +429,21 @@ class Command extends \yii\base\Component } if ($db->enableProfiling) { - Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::endProfile($token, __METHOD__); } if (isset($cache, $cacheKey) && $cache instanceof Cache) { $cache->set($cacheKey, $result, $db->queryCacheDuration, $db->queryCacheDependency); - Yii::trace('Saved query result in cache', __CLASS__); + Yii::trace('Saved query result in cache', __METHOD__); } return $result; } catch (\Exception $e) { if ($db->enableProfiling) { - Yii::endProfile(__METHOD__ . "($sql)", __CLASS__); + Yii::endProfile($token, __METHOD__); } $message = $e->getMessage(); - Yii::error("$message\nCommand::$method() failed: {$sql}{$paramLog}", __CLASS__); + Yii::error("$message\nCommand::$method() failed: $rawSql", __METHOD__); $errorInfo = $e instanceof \PDOException ? $e->errorInfo : null; throw new Exception($message, $errorInfo, (int)$e->getCode()); } @@ -541,7 +547,7 @@ class Command extends \yii\base\Component */ public function delete($table, $condition = '', $params = array()) { - $sql = $this->db->getQueryBuilder()->delete($table, $condition); + $sql = $this->db->getQueryBuilder()->delete($table, $condition, $params); return $this->setSql($sql)->bindValues($params); } diff --git a/framework/db/Connection.php b/framework/db/Connection.php index 59e8422..797508a 100644 --- a/framework/db/Connection.php +++ b/framework/db/Connection.php @@ -223,21 +223,10 @@ class Connection extends Component * @var string the common prefix or suffix for table names. If a table name is given * as `{{%TableName}}`, then the percentage character `%` will be replaced with this * property value. For example, `{{%post}}` becomes `{{tbl_post}}` if this property is - * set as `"tbl_"`. Note that this property is only effective when [[enableAutoQuoting]] - * is true. - * @see enableAutoQuoting + * set as `"tbl_"`. */ public $tablePrefix; /** - * @var boolean whether to enable automatic quoting of table names and column names. - * Defaults to true. When this property is true, any token enclosed within double curly brackets - * (e.g. `{{post}}`) in a SQL statement will be treated as a table name and will be quoted - * accordingly when the SQL statement is executed; and any token enclosed within double square - * brackets (e.g. `[[name]]`) will be treated as a column name and quoted accordingly. - * @see tablePrefix - */ - public $enableAutoQuoting = true; - /** * @var array mapping between PDO driver names and [[Schema]] classes. * The keys of the array are PDO driver names while the values the corresponding * schema class name or configuration. Please refer to [[\Yii::createObject()]] for @@ -248,15 +237,15 @@ class Connection extends Component * [[Schema]] class to support DBMS that is not supported by Yii. */ public $schemaMap = array( - 'pgsql' => 'yii\db\pgsql\Schema', // PostgreSQL - 'mysqli' => 'yii\db\mysql\Schema', // MySQL - 'mysql' => 'yii\db\mysql\Schema', // MySQL - 'sqlite' => 'yii\db\sqlite\Schema', // sqlite 3 + 'pgsql' => 'yii\db\pgsql\Schema', // PostgreSQL + 'mysqli' => 'yii\db\mysql\Schema', // MySQL + 'mysql' => 'yii\db\mysql\Schema', // MySQL + 'sqlite' => 'yii\db\sqlite\Schema', // sqlite 3 'sqlite2' => 'yii\db\sqlite\Schema', // sqlite 2 'mssql' => 'yi\db\dao\mssql\Schema', // Mssql driver on windows hosts - 'dblib' => 'yii\db\mssql\Schema', // dblib drivers on linux (and maybe others os) hosts - 'sqlsrv' => 'yii\db\mssql\Schema', // Mssql - 'oci' => 'yii\db\oci\Schema', // Oracle driver + 'sqlsrv' => 'yii\db\mssql\Schema', // Mssql + 'oci' => 'yii\db\oci\Schema', // Oracle driver + 'dblib' => 'yii\db\mssql\Schema', // dblib drivers on linux (and maybe others os) hosts ); /** * @var Transaction the currently active transaction @@ -324,12 +313,12 @@ class Connection extends Component throw new InvalidConfigException('Connection::dsn cannot be empty.'); } try { - \Yii::trace('Opening DB connection: ' . $this->dsn, __CLASS__); + \Yii::trace('Opening DB connection: ' . $this->dsn, __METHOD__); $this->pdo = $this->createPdoInstance(); $this->initConnection(); } catch (\PDOException $e) { - \Yii::error("Failed to open DB connection ({$this->dsn}): " . $e->getMessage(), __CLASS__); + \Yii::error("Failed to open DB connection ({$this->dsn}): " . $e->getMessage(), __METHOD__); $message = YII_DEBUG ? 'Failed to open DB connection: ' . $e->getMessage() : 'Failed to open DB connection.'; throw new Exception($message, $e->errorInfo, (int)$e->getCode()); } @@ -343,7 +332,7 @@ class Connection extends Component public function close() { if ($this->pdo !== null) { - \Yii::trace('Closing DB connection: ' . $this->dsn, __CLASS__); + \Yii::trace('Closing DB connection: ' . $this->dsn, __METHOD__); $this->pdo = null; $this->_schema = null; $this->_transaction = null; @@ -518,6 +507,27 @@ class Connection extends Component } /** + * Processes a SQL statement by quoting table and column names that are enclosed within double brackets. + * Tokens enclosed within double curly brackets are treated as table names, while + * tokens enclosed within double square brackets are column names. They will be quoted accordingly. + * Also, the percentage character "%" in a table name will be replaced with [[tablePrefix]]. + * @param string $sql the SQL to be quoted + * @return string the quoted SQL + */ + public function quoteSql($sql) + { + $db = $this; + return preg_replace_callback('/(\\{\\{([\w\-\. ]+)\\}\\}|\\[\\[([\w\-\. ]+)\\]\\])/', + function($matches) use($db) { + if (isset($matches[3])) { + return $db->quoteColumnName($matches[3]); + } else { + return str_replace('%', $db->tablePrefix, $db->quoteTableName($matches[2])); + } + }, $sql); + } + + /** * Returns the name of the DB driver for the current [[dsn]]. * @return string name of the DB driver */ diff --git a/framework/db/QueryBuilder.php b/framework/db/QueryBuilder.php index 75375cc..441d287 100644 --- a/framework/db/QueryBuilder.php +++ b/framework/db/QueryBuilder.php @@ -22,6 +22,11 @@ use yii\base\NotSupportedException; class QueryBuilder extends \yii\base\Object { /** + * The prefix for automatically generated query binding parameters. + */ + const PARAM_PREFIX = ':qp'; + + /** * @var Connection the database connection. */ public $db; @@ -58,11 +63,11 @@ class QueryBuilder extends \yii\base\Object $clauses = array( $this->buildSelect($query->select, $query->distinct, $query->selectOption), $this->buildFrom($query->from), - $this->buildJoin($query->join), - $this->buildWhere($query->where), + $this->buildJoin($query->join, $query->params), + $this->buildWhere($query->where, $query->params), $this->buildGroupBy($query->groupBy), - $this->buildHaving($query->having), - $this->buildUnion($query->union), + $this->buildHaving($query->having, $query->params), + $this->buildUnion($query->union, $query->params), $this->buildOrderBy($query->orderBy), $this->buildLimit($query->limit, $query->offset), ); @@ -92,7 +97,6 @@ class QueryBuilder extends \yii\base\Object { $names = array(); $placeholders = array(); - $count = 0; foreach ($columns as $name => $value) { $names[] = $this->db->quoteColumnName($name); if ($value instanceof Expression) { @@ -101,9 +105,9 @@ class QueryBuilder extends \yii\base\Object $params[$n] = $v; } } else { - $placeholders[] = ':p' . $count; - $params[':p' . $count] = $value; - $count++; + $phName = self::PARAM_PREFIX . count($params); + $placeholders[] = $phName; + $params[$phName] = $value; } } @@ -159,10 +163,9 @@ class QueryBuilder extends \yii\base\Object * so that they can be bound to the DB command later. * @return string the UPDATE SQL */ - public function update($table, $columns, $condition = '', &$params) + public function update($table, $columns, $condition, &$params) { $lines = array(); - $count = 0; foreach ($columns as $name => $value) { if ($value instanceof Expression) { $lines[] = $this->db->quoteColumnName($name) . '=' . $value->expression; @@ -170,17 +173,15 @@ class QueryBuilder extends \yii\base\Object $params[$n] = $v; } } else { - $lines[] = $this->db->quoteColumnName($name) . '=:p' . $count; - $params[':p' . $count] = $value; - $count++; + $phName = self::PARAM_PREFIX . count($params); + $lines[] = $this->db->quoteColumnName($name) . '=' . $phName; + $params[$phName] = $value; } } - $sql = 'UPDATE ' . $this->db->quoteTableName($table) . ' SET ' . implode(', ', $lines); - if (($where = $this->buildCondition($condition)) !== '') { - $sql .= ' WHERE ' . $where; - } - return $sql; + $sql = 'UPDATE ' . $this->db->quoteTableName($table) . ' SET ' . implode(', ', $lines); + $where = $this->buildWhere($condition, $params); + return $where === '' ? $sql : $sql . ' ' . $where; } /** @@ -196,15 +197,15 @@ class QueryBuilder extends \yii\base\Object * @param string $table the table where the data will be deleted from. * @param mixed $condition the condition that will be put in the WHERE part. Please * refer to [[Query::where()]] on how to specify condition. + * @param array $params the binding parameters that will be modified by this method + * so that they can be bound to the DB command later. * @return string the DELETE SQL */ - public function delete($table, $condition = '') + public function delete($table, $condition, &$params) { $sql = 'DELETE FROM ' . $this->db->quoteTableName($table); - if (($where = $this->buildCondition($condition)) !== '') { - $sql .= ' WHERE ' . $where; - } - return $sql; + $where = $this->buildWhere($condition, $params); + return $where === '' ? $sql : $sql . ' ' . $where; } /** @@ -479,200 +480,6 @@ class QueryBuilder extends \yii\base\Object } /** - * Parses the condition specification and generates the corresponding SQL expression. - * @param string|array $condition the condition specification. Please refer to [[Query::where()]] - * on how to specify a condition. - * @return string the generated SQL expression - * @throws \yii\db\Exception if the condition is in bad format - */ - public function buildCondition($condition) - { - static $builders = array( - 'AND' => 'buildAndCondition', - 'OR' => 'buildAndCondition', - 'BETWEEN' => 'buildBetweenCondition', - 'NOT BETWEEN' => 'buildBetweenCondition', - 'IN' => 'buildInCondition', - 'NOT IN' => 'buildInCondition', - 'LIKE' => 'buildLikeCondition', - 'NOT LIKE' => 'buildLikeCondition', - 'OR LIKE' => 'buildLikeCondition', - 'OR NOT LIKE' => 'buildLikeCondition', - ); - - if (!is_array($condition)) { - return (string)$condition; - } elseif ($condition === array()) { - return ''; - } - if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... - $operator = strtoupper($condition[0]); - if (isset($builders[$operator])) { - $method = $builders[$operator]; - array_shift($condition); - return $this->$method($operator, $condition); - } else { - throw new Exception('Found unknown operator in query: ' . $operator); - } - } else { // hash format: 'column1'=>'value1', 'column2'=>'value2', ... - return $this->buildHashCondition($condition); - } - } - - private function buildHashCondition($condition) - { - $parts = array(); - foreach ($condition as $column => $value) { - if (is_array($value)) { // IN condition - $parts[] = $this->buildInCondition('in', array($column, $value)); - } else { - if (strpos($column, '(') === false) { - $column = $this->db->quoteColumnName($column); - } - if ($value === null) { - $parts[] = "$column IS NULL"; - } elseif (is_string($value)) { - $parts[] = "$column=" . $this->db->quoteValue($value); - } else { - $parts[] = "$column=$value"; - } - } - } - return count($parts) === 1 ? $parts[0] : '(' . implode(') AND (', $parts) . ')'; - } - - private function buildAndCondition($operator, $operands) - { - $parts = array(); - foreach ($operands as $operand) { - if (is_array($operand)) { - $operand = $this->buildCondition($operand); - } - if ($operand !== '') { - $parts[] = $operand; - } - } - if ($parts !== array()) { - return '(' . implode(") $operator (", $parts) . ')'; - } else { - return ''; - } - } - - private function buildBetweenCondition($operator, $operands) - { - if (!isset($operands[0], $operands[1], $operands[2])) { - throw new Exception("Operator '$operator' requires three operands."); - } - - list($column, $value1, $value2) = $operands; - - if (strpos($column, '(') === false) { - $column = $this->db->quoteColumnName($column); - } - $value1 = is_string($value1) ? $this->db->quoteValue($value1) : (string)$value1; - $value2 = is_string($value2) ? $this->db->quoteValue($value2) : (string)$value2; - - return "$column $operator $value1 AND $value2"; - } - - private function buildInCondition($operator, $operands) - { - if (!isset($operands[0], $operands[1])) { - throw new Exception("Operator '$operator' requires two operands."); - } - - list($column, $values) = $operands; - - $values = (array)$values; - - if ($values === array() || $column === array()) { - return $operator === 'IN' ? '0=1' : ''; - } - - if (count($column) > 1) { - return $this->buildCompositeInCondition($operator, $column, $values); - } elseif (is_array($column)) { - $column = reset($column); - } - foreach ($values as $i => $value) { - if (is_array($value)) { - $value = isset($value[$column]) ? $value[$column] : null; - } - if ($value === null) { - $values[$i] = 'NULL'; - } else { - $values[$i] = is_string($value) ? $this->db->quoteValue($value) : (string)$value; - } - } - if (strpos($column, '(') === false) { - $column = $this->db->quoteColumnName($column); - } - - if (count($values) > 1) { - return "$column $operator (" . implode(', ', $values) . ')'; - } else { - $operator = $operator === 'IN' ? '=' : '<>'; - return "$column$operator{$values[0]}"; - } - } - - protected function buildCompositeInCondition($operator, $columns, $values) - { - foreach ($columns as $i => $column) { - if (strpos($column, '(') === false) { - $columns[$i] = $this->db->quoteColumnName($column); - } - } - $vss = array(); - foreach ($values as $value) { - $vs = array(); - foreach ($columns as $column) { - if (isset($value[$column])) { - $vs[] = is_string($value[$column]) ? $this->db->quoteValue($value[$column]) : (string)$value[$column]; - } else { - $vs[] = 'NULL'; - } - } - $vss[] = '(' . implode(', ', $vs) . ')'; - } - return '(' . implode(', ', $columns) . ") $operator (" . implode(', ', $vss) . ')'; - } - - private function buildLikeCondition($operator, $operands) - { - if (!isset($operands[0], $operands[1])) { - throw new Exception("Operator '$operator' requires two operands."); - } - - list($column, $values) = $operands; - - $values = (array)$values; - - if ($values === array()) { - return $operator === 'LIKE' || $operator === 'OR LIKE' ? '0=1' : ''; - } - - if ($operator === 'LIKE' || $operator === 'NOT LIKE') { - $andor = ' AND '; - } else { - $andor = ' OR '; - $operator = $operator === 'OR LIKE' ? 'LIKE' : 'NOT LIKE'; - } - - if (strpos($column, '(') === false) { - $column = $this->db->quoteColumnName($column); - } - - $parts = array(); - foreach ($values as $value) { - $parts[] = "$column $operator " . $this->db->quoteValue($value); - } - - return implode($andor, $parts); - } - - /** * @param array $columns * @param boolean $distinct * @param string $selectOption @@ -737,10 +544,11 @@ class QueryBuilder extends \yii\base\Object /** * @param string|array $joins + * @param array $params the binding parameters to be populated * @return string the JOIN clause built from [[query]]. * @throws Exception if the $joins parameter is not in proper format */ - public function buildJoin($joins) + public function buildJoin($joins, &$params) { if (empty($joins)) { return ''; @@ -761,9 +569,9 @@ class QueryBuilder extends \yii\base\Object } $joins[$i] = $join[0] . ' ' . $table; if (isset($join[2])) { - $condition = $this->buildCondition($join[2]); + $condition = $this->buildCondition($join[2], $params); if ($condition !== '') { - $joins[$i] .= ' ON ' . $this->buildCondition($join[2]); + $joins[$i] .= ' ON ' . $condition; } } } else { @@ -776,11 +584,12 @@ class QueryBuilder extends \yii\base\Object /** * @param string|array $condition + * @param array $params the binding parameters to be populated * @return string the WHERE clause built from [[query]]. */ - public function buildWhere($condition) + public function buildWhere($condition, &$params) { - $where = $this->buildCondition($condition); + $where = $this->buildCondition($condition, $params); return $where === '' ? '' : 'WHERE ' . $where; } @@ -795,11 +604,12 @@ class QueryBuilder extends \yii\base\Object /** * @param string|array $condition + * @param array $params the binding parameters to be populated * @return string the HAVING clause built from [[query]]. */ - public function buildHaving($condition) + public function buildHaving($condition, &$params) { - $having = $this->buildCondition($condition); + $having = $this->buildCondition($condition, $params); return $having === '' ? '' : 'HAVING ' . $having; } @@ -843,16 +653,19 @@ class QueryBuilder extends \yii\base\Object /** * @param array $unions + * @param array $params the binding parameters to be populated * @return string the UNION clause built from [[query]]. */ - public function buildUnion($unions) + public function buildUnion($unions, &$params) { if (empty($unions)) { return ''; } foreach ($unions as $i => $union) { if ($union instanceof Query) { + $union->addParams($params); $unions[$i] = $this->build($union); + $params = $union->params; } } return "UNION (\n" . implode("\n) UNION (\n", $unions) . "\n)"; @@ -864,7 +677,7 @@ class QueryBuilder extends \yii\base\Object * @param string|array $columns the columns to be processed * @return string the processing result */ - protected function buildColumns($columns) + public function buildColumns($columns) { if (!is_array($columns)) { if (strpos($columns, '(') !== false) { @@ -882,4 +695,218 @@ class QueryBuilder extends \yii\base\Object } return is_array($columns) ? implode(', ', $columns) : $columns; } + + + /** + * Parses the condition specification and generates the corresponding SQL expression. + * @param string|array $condition the condition specification. Please refer to [[Query::where()]] + * on how to specify a condition. + * @param array $params the binding parameters to be populated + * @return string the generated SQL expression + * @throws \yii\db\Exception if the condition is in bad format + */ + public function buildCondition($condition, &$params) + { + static $builders = array( + 'AND' => 'buildAndCondition', + 'OR' => 'buildAndCondition', + 'BETWEEN' => 'buildBetweenCondition', + 'NOT BETWEEN' => 'buildBetweenCondition', + 'IN' => 'buildInCondition', + 'NOT IN' => 'buildInCondition', + 'LIKE' => 'buildLikeCondition', + 'NOT LIKE' => 'buildLikeCondition', + 'OR LIKE' => 'buildLikeCondition', + 'OR NOT LIKE' => 'buildLikeCondition', + ); + + if (!is_array($condition)) { + return (string)$condition; + } elseif ($condition === array()) { + return ''; + } + if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... + $operator = strtoupper($condition[0]); + if (isset($builders[$operator])) { + $method = $builders[$operator]; + array_shift($condition); + return $this->$method($operator, $condition, $params); + } else { + throw new Exception('Found unknown operator in query: ' . $operator); + } + } else { // hash format: 'column1'=>'value1', 'column2'=>'value2', ... + return $this->buildHashCondition($condition, $params); + } + } + + private function buildHashCondition($condition, &$params) + { + $parts = array(); + foreach ($condition as $column => $value) { + if (is_array($value)) { // IN condition + $parts[] = $this->buildInCondition('in', array($column, $value), $params); + } else { + if (strpos($column, '(') === false) { + $column = $this->db->quoteColumnName($column); + } + if ($value === null) { + $parts[] = "$column IS NULL"; + } elseif ($value instanceof Expression) { + $parts[] = "$column=" . $value->expression; + foreach ($value->params as $n => $v) { + $params[$n] = $v; + } + } else { + $phName = self::PARAM_PREFIX . count($params); + $parts[] = "$column=$phName"; + $params[$phName] = $value; + } + } + } + return count($parts) === 1 ? $parts[0] : '(' . implode(') AND (', $parts) . ')'; + } + + private function buildAndCondition($operator, $operands, &$params) + { + $parts = array(); + foreach ($operands as $operand) { + if (is_array($operand)) { + $operand = $this->buildCondition($operand, $params); + } + if ($operand !== '') { + $parts[] = $operand; + } + } + if ($parts !== array()) { + return '(' . implode(") $operator (", $parts) . ')'; + } else { + return ''; + } + } + + private function buildBetweenCondition($operator, $operands, &$params) + { + if (!isset($operands[0], $operands[1], $operands[2])) { + throw new Exception("Operator '$operator' requires three operands."); + } + + list($column, $value1, $value2) = $operands; + + if (strpos($column, '(') === false) { + $column = $this->db->quoteColumnName($column); + } + $phName1 = self::PARAM_PREFIX . count($params); + $phName2 = self::PARAM_PREFIX . count($params); + $params[$phName1] = $value1; + $params[$phName2] = $value2; + + return "$column $operator $phName1 AND $phName2"; + } + + private function buildInCondition($operator, $operands, &$params) + { + if (!isset($operands[0], $operands[1])) { + throw new Exception("Operator '$operator' requires two operands."); + } + + list($column, $values) = $operands; + + $values = (array)$values; + + if ($values === array() || $column === array()) { + return $operator === 'IN' ? '0=1' : ''; + } + + if (count($column) > 1) { + return $this->buildCompositeInCondition($operator, $column, $values, $params); + } elseif (is_array($column)) { + $column = reset($column); + } + foreach ($values as $i => $value) { + if (is_array($value)) { + $value = isset($value[$column]) ? $value[$column] : null; + } + if ($value === null) { + $values[$i] = 'NULL'; + } elseif ($value instanceof Expression) { + $values[$i] = $value->expression; + foreach ($value->params as $n => $v) { + $params[$n] = $v; + } + } else { + $phName = self::PARAM_PREFIX . count($params); + $params[$phName] = $value; + $values[$i] = $phName; + } + } + if (strpos($column, '(') === false) { + $column = $this->db->quoteColumnName($column); + } + + if (count($values) > 1) { + return "$column $operator (" . implode(', ', $values) . ')'; + } else { + $operator = $operator === 'IN' ? '=' : '<>'; + return "$column$operator{$values[0]}"; + } + } + + protected function buildCompositeInCondition($operator, $columns, $values, &$params) + { + foreach ($columns as $i => $column) { + if (strpos($column, '(') === false) { + $columns[$i] = $this->db->quoteColumnName($column); + } + } + $vss = array(); + foreach ($values as $value) { + $vs = array(); + foreach ($columns as $column) { + if (isset($value[$column])) { + $phName = self::PARAM_PREFIX . count($params); + $params[$phName] = $value[$column]; + $vs[] = $phName; + } else { + $vs[] = 'NULL'; + } + } + $vss[] = '(' . implode(', ', $vs) . ')'; + } + return '(' . implode(', ', $columns) . ") $operator (" . implode(', ', $vss) . ')'; + } + + private function buildLikeCondition($operator, $operands, &$params) + { + if (!isset($operands[0], $operands[1])) { + throw new Exception("Operator '$operator' requires two operands."); + } + + list($column, $values) = $operands; + + $values = (array)$values; + + if ($values === array()) { + return $operator === 'LIKE' || $operator === 'OR LIKE' ? '0=1' : ''; + } + + if ($operator === 'LIKE' || $operator === 'NOT LIKE') { + $andor = ' AND '; + } else { + $andor = ' OR '; + $operator = $operator === 'OR LIKE' ? 'LIKE' : 'NOT LIKE'; + } + + if (strpos($column, '(') === false) { + $column = $this->db->quoteColumnName($column); + } + + $parts = array(); + foreach ($values as $value) { + $phName = self::PARAM_PREFIX . count($params); + $params[$phName] = $value; + $parts[] = "$column $operator $phName"; + } + + return implode($andor, $parts); + } } diff --git a/framework/db/Schema.php b/framework/db/Schema.php index 71bc9a2..9538e4c 100644 --- a/framework/db/Schema.php +++ b/framework/db/Schema.php @@ -83,7 +83,7 @@ abstract class Schema extends \yii\base\Object } $db = $this->db; - $realName = $this->getRealTableName($name); + $realName = $this->getRawTableName($name); if ($db->enableSchemaCache && !in_array($name, $db->schemaCacheExclude, true)) { /** @var $cache Cache */ @@ -248,7 +248,7 @@ abstract class Schema extends \yii\base\Object /** * Quotes a table name for use in a query. * If the table name contains schema prefix, the prefix will also be properly quoted. - * If the table name is already quoted or contains special characters including '(', '[[' and '{{', + * If the table name is already quoted or contains '(' or '{{', * then this method will do nothing. * @param string $name table name * @return string the properly quoted table name @@ -256,7 +256,7 @@ abstract class Schema extends \yii\base\Object */ public function quoteTableName($name) { - if (strpos($name, '(') !== false || strpos($name, '[[') !== false || strpos($name, '{{') !== false) { + if (strpos($name, '(') !== false || strpos($name, '{{') !== false) { return $name; } if (strpos($name, '.') === false) { @@ -273,7 +273,7 @@ abstract class Schema extends \yii\base\Object /** * Quotes a column name for use in a query. * If the column name contains prefix, the prefix will also be properly quoted. - * If the column name is already quoted or contains special characters including '(', '[[' and '{{', + * If the column name is already quoted or contains '(', '[[' or '{{', * then this method will do nothing. * @param string $name column name * @return string the properly quoted column name @@ -318,15 +318,15 @@ abstract class Schema extends \yii\base\Object } /** - * Returns the real name of a table name. + * Returns the actual name of a given table name. * This method will strip off curly brackets from the given table name - * and replace the percentage character in the name with [[Connection::tablePrefix]]. + * and replace the percentage character '%' with [[Connection::tablePrefix]]. * @param string $name the table name to be converted * @return string the real name of the given table name */ - public function getRealTableName($name) + public function getRawTableName($name) { - if ($this->db->enableAutoQuoting && strpos($name, '{{') !== false) { + if (strpos($name, '{{') !== false) { $name = preg_replace('/\\{\\{(.*?)\\}\\}/', '\1', $name); return str_replace('%', $this->db->tablePrefix, $name); } else { diff --git a/framework/db/StaleObjectException.php b/framework/db/StaleObjectException.php new file mode 100644 index 0000000..860c9fc --- /dev/null +++ b/framework/db/StaleObjectException.php @@ -0,0 +1,23 @@ + + * @since 2.0 + */ +class StaleObjectException extends Exception +{ + /** + * @return string the user-friendly name of this exception + */ + public function getName() + { + return \Yii::t('yii|Stale Object Exception'); + } +} \ No newline at end of file diff --git a/framework/db/Transaction.php b/framework/db/Transaction.php index 177d2cb..d66c38e 100644 --- a/framework/db/Transaction.php +++ b/framework/db/Transaction.php @@ -66,7 +66,7 @@ class Transaction extends \yii\base\Object if ($this->db === null) { throw new InvalidConfigException('Transaction::db must be set.'); } - \Yii::trace('Starting transaction', __CLASS__); + \Yii::trace('Starting transaction', __METHOD__); $this->db->open(); $this->db->pdo->beginTransaction(); $this->_active = true; @@ -80,7 +80,7 @@ class Transaction extends \yii\base\Object public function commit() { if ($this->_active && $this->db && $this->db->isActive) { - \Yii::trace('Committing transaction', __CLASS__); + \Yii::trace('Committing transaction', __METHOD__); $this->db->pdo->commit(); $this->_active = false; } else { @@ -95,7 +95,7 @@ class Transaction extends \yii\base\Object public function rollback() { if ($this->_active && $this->db && $this->db->isActive) { - \Yii::trace('Rolling back transaction', __CLASS__); + \Yii::trace('Rolling back transaction', __METHOD__); $this->db->pdo->rollBack(); $this->_active = false; } else { diff --git a/framework/helpers/ArrayHelper.php b/framework/helpers/ArrayHelper.php index 65fa962..3061717 100644 --- a/framework/helpers/ArrayHelper.php +++ b/framework/helpers/ArrayHelper.php @@ -7,9 +7,6 @@ namespace yii\helpers; -use Yii; -use yii\base\InvalidParamException; - /** * ArrayHelper provides additional array functionality you can use in your * application. @@ -17,324 +14,6 @@ use yii\base\InvalidParamException; * @author Qiang Xue * @since 2.0 */ -class ArrayHelper +class ArrayHelper extends base\ArrayHelper { - /** - * Merges two or more arrays into one recursively. - * If each array has an element with the same string key value, the latter - * will overwrite the former (different from array_merge_recursive). - * Recursive merging will be conducted if both arrays have an element of array - * type and are having the same key. - * For integer-keyed elements, the elements from the latter array will - * be appended to the former array. - * @param array $a array to be merged to - * @param array $b array to be merged from. You can specify additional - * arrays via third argument, fourth argument etc. - * @return array the merged array (the original arrays are not changed.) - */ - public static function merge($a, $b) - { - $args = func_get_args(); - $res = array_shift($args); - while ($args !== array()) { - $next = array_shift($args); - foreach ($next as $k => $v) { - if (is_integer($k)) { - isset($res[$k]) ? $res[] = $v : $res[$k] = $v; - } elseif (is_array($v) && isset($res[$k]) && is_array($res[$k])) { - $res[$k] = self::merge($res[$k], $v); - } else { - $res[$k] = $v; - } - } - } - return $res; - } - - /** - * Retrieves the value of an array element or object property with the given key or property name. - * If the key does not exist in the array, the default value will be returned instead. - * - * Below are some usage examples, - * - * ~~~ - * // working with array - * $username = \yii\helpers\ArrayHelper::getValue($_POST, 'username'); - * // working with object - * $username = \yii\helpers\ArrayHelper::getValue($user, 'username'); - * // working with anonymous function - * $fullName = \yii\helpers\ArrayHelper::getValue($user, function($user, $defaultValue) { - * return $user->firstName . ' ' . $user->lastName; - * }); - * ~~~ - * - * @param array|object $array array or object to extract value from - * @param string|\Closure $key key name of the array element, or property name of the object, - * or an anonymous function returning the value. The anonymous function signature should be: - * `function($array, $defaultValue)`. - * @param mixed $default the default value to be returned if the specified key does not exist - * @return mixed the value of the - */ - public static function getValue($array, $key, $default = null) - { - if ($key instanceof \Closure) { - return $key($array, $default); - } elseif (is_array($array)) { - return isset($array[$key]) || array_key_exists($key, $array) ? $array[$key] : $default; - } else { - return $array->$key; - } - } - - /** - * Indexes an array according to a specified key. - * The input array should be multidimensional or an array of objects. - * - * The key can be a key name of the sub-array, a property name of object, or an anonymous - * function which returns the key value given an array element. - * - * If a key value is null, the corresponding array element will be discarded and not put in the result. - * - * For example, - * - * ~~~ - * $array = array( - * array('id' => '123', 'data' => 'abc'), - * array('id' => '345', 'data' => 'def'), - * ); - * $result = ArrayHelper::index($array, 'id'); - * // the result is: - * // array( - * // '123' => array('id' => '123', 'data' => 'abc'), - * // '345' => array('id' => '345', 'data' => 'def'), - * // ) - * - * // using anonymous function - * $result = ArrayHelper::index($array, function(element) { - * return $element['id']; - * }); - * ~~~ - * - * @param array $array the array that needs to be indexed - * @param string|\Closure $key the column name or anonymous function whose result will be used to index the array - * @return array the indexed array - */ - public static function index($array, $key) - { - $result = array(); - foreach ($array as $element) { - $value = static::getValue($element, $key); - $result[$value] = $element; - } - return $result; - } - - /** - * Returns the values of a specified column in an array. - * The input array should be multidimensional or an array of objects. - * - * For example, - * - * ~~~ - * $array = array( - * array('id' => '123', 'data' => 'abc'), - * array('id' => '345', 'data' => 'def'), - * ); - * $result = ArrayHelper::getColumn($array, 'id'); - * // the result is: array( '123', '345') - * - * // using anonymous function - * $result = ArrayHelper::getColumn($array, function(element) { - * return $element['id']; - * }); - * ~~~ - * - * @param array $array - * @param string|\Closure $name - * @param boolean $keepKeys whether to maintain the array keys. If false, the resulting array - * will be re-indexed with integers. - * @return array the list of column values - */ - public static function getColumn($array, $name, $keepKeys = true) - { - $result = array(); - if ($keepKeys) { - foreach ($array as $k => $element) { - $result[$k] = static::getValue($element, $name); - } - } else { - foreach ($array as $element) { - $result[] = static::getValue($element, $name); - } - } - - return $result; - } - - /** - * Builds a map (key-value pairs) from a multidimensional array or an array of objects. - * The `$from` and `$to` parameters specify the key names or property names to set up the map. - * Optionally, one can further group the map according to a grouping field `$group`. - * - * For example, - * - * ~~~ - * $array = array( - * array('id' => '123', 'name' => 'aaa', 'class' => 'x'), - * array('id' => '124', 'name' => 'bbb', 'class' => 'x'), - * array('id' => '345', 'name' => 'ccc', 'class' => 'y'), - * ); - * - * $result = ArrayHelper::map($array, 'id', 'name'); - * // the result is: - * // array( - * // '123' => 'aaa', - * // '124' => 'bbb', - * // '345' => 'ccc', - * // ) - * - * $result = ArrayHelper::map($array, 'id', 'name', 'class'); - * // the result is: - * // array( - * // 'x' => array( - * // '123' => 'aaa', - * // '124' => 'bbb', - * // ), - * // 'y' => array( - * // '345' => 'ccc', - * // ), - * // ) - * ~~~ - * - * @param array $array - * @param string|\Closure $from - * @param string|\Closure $to - * @param string|\Closure $group - * @return array - */ - public static function map($array, $from, $to, $group = null) - { - $result = array(); - foreach ($array as $element) { - $key = static::getValue($element, $from); - $value = static::getValue($element, $to); - if ($group !== null) { - $result[static::getValue($element, $group)][$key] = $value; - } else { - $result[$key] = $value; - } - } - return $result; - } - - /** - * Sorts an array of objects or arrays (with the same structure) by one or several keys. - * @param array $array the array to be sorted. The array will be modified after calling this method. - * @param string|\Closure|array $key the key(s) to be sorted by. This refers to a key name of the sub-array - * elements, a property name of the objects, or an anonymous function returning the values for comparison - * purpose. The anonymous function signature should be: `function($item)`. - * To sort by multiple keys, provide an array of keys here. - * @param boolean|array $ascending whether to sort in ascending or descending order. When - * sorting by multiple keys with different ascending orders, use an array of ascending flags. - * @param integer|array $sortFlag the PHP sort flag. Valid values include: - * `SORT_REGULAR`, `SORT_NUMERIC`, `SORT_STRING`, and `SORT_STRING | SORT_FLAG_CASE`. The last - * value is for sorting strings in case-insensitive manner. Please refer to - * See [PHP manual](http://php.net/manual/en/function.sort.php) for more details. - * When sorting by multiple keys with different sort flags, use an array of sort flags. - * @throws InvalidParamException if the $ascending or $sortFlag parameters do not have - * correct number of elements as that of $key. - */ - public static function multisort(&$array, $key, $ascending = true, $sortFlag = SORT_REGULAR) - { - $keys = is_array($key) ? $key : array($key); - if (empty($keys) || empty($array)) { - return; - } - $n = count($keys); - if (is_scalar($ascending)) { - $ascending = array_fill(0, $n, $ascending); - } elseif (count($ascending) !== $n) { - throw new InvalidParamException('The length of $ascending parameter must be the same as that of $keys.'); - } - if (is_scalar($sortFlag)) { - $sortFlag = array_fill(0, $n, $sortFlag); - } elseif (count($sortFlag) !== $n) { - throw new InvalidParamException('The length of $ascending parameter must be the same as that of $keys.'); - } - $args = array(); - foreach ($keys as $i => $key) { - $flag = $sortFlag[$i]; - if ($flag == (SORT_STRING | SORT_FLAG_CASE)) { - $flag = SORT_STRING; - $column = array(); - foreach (static::getColumn($array, $key) as $k => $value) { - $column[$k] = strtolower($value); - } - $args[] = $column; - } else { - $args[] = static::getColumn($array, $key); - } - $args[] = $ascending[$i] ? SORT_ASC : SORT_DESC; - $args[] = $flag; - } - $args[] = &$array; - call_user_func_array('array_multisort', $args); - } - - /** - * Encodes special characters in an array of strings into HTML entities. - * Both the array keys and values will be encoded. - * If a value is an array, this method will also encode it recursively. - * @param array $data data to be encoded - * @param boolean $valuesOnly whether to encode array values only. If false, - * both the array keys and array values will be encoded. - * @param string $charset the charset that the data is using. If not set, - * [[\yii\base\Application::charset]] will be used. - * @return array the encoded data - * @see http://www.php.net/manual/en/function.htmlspecialchars.php - */ - public static function htmlEncode($data, $valuesOnly = true, $charset = null) - { - if ($charset === null) { - $charset = Yii::$app->charset; - } - $d = array(); - foreach ($data as $key => $value) { - if (!$valuesOnly && is_string($key)) { - $key = htmlspecialchars($key, ENT_QUOTES, $charset); - } - if (is_string($value)) { - $d[$key] = htmlspecialchars($value, ENT_QUOTES, $charset); - } elseif (is_array($value)) { - $d[$key] = static::htmlEncode($value, $charset); - } - } - return $d; - } - - /** - * Decodes HTML entities into the corresponding characters in an array of strings. - * Both the array keys and values will be decoded. - * If a value is an array, this method will also decode it recursively. - * @param array $data data to be decoded - * @param boolean $valuesOnly whether to decode array values only. If false, - * both the array keys and array values will be decoded. - * @return array the decoded data - * @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php - */ - public static function htmlDecode($data, $valuesOnly = true) - { - $d = array(); - foreach ($data as $key => $value) { - if (!$valuesOnly && is_string($key)) { - $key = htmlspecialchars_decode($key, ENT_QUOTES); - } - if (is_string($value)) { - $d[$key] = htmlspecialchars_decode($value, ENT_QUOTES); - } elseif (is_array($value)) { - $d[$key] = static::htmlDecode($value); - } - } - return $d; - } } \ No newline at end of file diff --git a/framework/helpers/ConsoleColor.php b/framework/helpers/ConsoleColor.php index 429aeb1..794b9c8 100644 --- a/framework/helpers/ConsoleColor.php +++ b/framework/helpers/ConsoleColor.php @@ -18,453 +18,6 @@ namespace yii\helpers; * @author Carsten Brandt * @since 2.0 */ -class ConsoleColor +class ConsoleColor extends base\ConsoleColor { - const FG_BLACK = 30; - const FG_RED = 31; - const FG_GREEN = 32; - const FG_YELLOW = 33; - const FG_BLUE = 34; - const FG_PURPLE = 35; - const FG_CYAN = 36; - const FG_GREY = 37; - - const BG_BLACK = 40; - const BG_RED = 41; - const BG_GREEN = 42; - const BG_YELLOW = 43; - const BG_BLUE = 44; - const BG_PURPLE = 45; - const BG_CYAN = 46; - const BG_GREY = 47; - - const BOLD = 1; - const ITALIC = 3; - const UNDERLINE = 4; - const BLINK = 5; - const NEGATIVE = 7; - const CONCEALED = 8; - const CROSSED_OUT = 9; - const FRAMED = 51; - const ENCIRCLED = 52; - const OVERLINED = 53; - - /** - * Moves the terminal cursor up by sending ANSI control code CUU to the terminal. - * If the cursor is already at the edge of the screen, this has no effect. - * @param integer $rows number of rows the cursor should be moved up - */ - public static function moveCursorUp($rows=1) - { - echo "\033[" . (int) $rows . 'A'; - } - - /** - * Moves the terminal cursor down by sending ANSI control code CUD to the terminal. - * If the cursor is already at the edge of the screen, this has no effect. - * @param integer $rows number of rows the cursor should be moved down - */ - public static function moveCursorDown($rows=1) - { - echo "\033[" . (int) $rows . 'B'; - } - - /** - * Moves the terminal cursor forward by sending ANSI control code CUF to the terminal. - * If the cursor is already at the edge of the screen, this has no effect. - * @param integer $steps number of steps the cursor should be moved forward - */ - public static function moveCursorForward($steps=1) - { - echo "\033[" . (int) $steps . 'C'; - } - - /** - * Moves the terminal cursor backward by sending ANSI control code CUB to the terminal. - * If the cursor is already at the edge of the screen, this has no effect. - * @param integer $steps number of steps the cursor should be moved backward - */ - public static function moveCursorBackward($steps=1) - { - echo "\033[" . (int) $steps . 'D'; - } - - /** - * Moves the terminal cursor to the beginning of the next line by sending ANSI control code CNL to the terminal. - * @param integer $lines number of lines the cursor should be moved down - */ - public static function moveCursorNextLine($lines=1) - { - echo "\033[" . (int) $lines . 'E'; - } - - /** - * Moves the terminal cursor to the beginning of the previous line by sending ANSI control code CPL to the terminal. - * @param integer $lines number of lines the cursor should be moved up - */ - public static function moveCursorPrevLine($lines=1) - { - echo "\033[" . (int) $lines . 'F'; - } - - /** - * Moves the cursor to an absolute position given as column and row by sending ANSI control code CUP or CHA to the terminal. - * @param integer $column 1-based column number, 1 is the left edge of the screen. - * @param integer|null $row 1-based row number, 1 is the top edge of the screen. if not set, will move cursor only in current line. - */ - public static function moveCursorTo($column, $row=null) - { - if ($row === null) { - echo "\033[" . (int) $column . 'G'; - } else { - echo "\033[" . (int) $row . ';' . (int) $column . 'H'; - } - } - - /** - * Scrolls whole page up by sending ANSI control code SU to the terminal. - * New lines are added at the bottom. This is not supported by ANSI.SYS used in windows. - * @param int $lines number of lines to scroll up - */ - public static function scrollUp($lines=1) - { - echo "\033[".(int)$lines."S"; - } - - /** - * Scrolls whole page down by sending ANSI control code SD to the terminal. - * New lines are added at the top. This is not supported by ANSI.SYS used in windows. - * @param int $lines number of lines to scroll down - */ - public static function scrollDown($lines=1) - { - echo "\033[".(int)$lines."T"; - } - - /** - * Saves the current cursor position by sending ANSI control code SCP to the terminal. - * Position can then be restored with {@link restoreCursorPosition}. - */ - public static function saveCursorPosition() - { - echo "\033[s"; - } - - /** - * Restores the cursor position saved with {@link saveCursorPosition} by sending ANSI control code RCP to the terminal. - */ - public static function restoreCursorPosition() - { - echo "\033[u"; - } - - /** - * Hides the cursor by sending ANSI DECTCEM code ?25l to the terminal. - * Use {@link showCursor} to bring it back. - * Do not forget to show cursor when your application exits. Cursor might stay hidden in terminal after exit. - */ - public static function hideCursor() - { - echo "\033[?25l"; - } - - /** - * Will show a cursor again when it has been hidden by {@link hideCursor} by sending ANSI DECTCEM code ?25h to the terminal. - */ - public static function showCursor() - { - echo "\033[?25h"; - } - - /** - * Clears entire screen content by sending ANSI control code ED with argument 2 to the terminal. - * Cursor position will not be changed. - * **Note:** ANSI.SYS implementation used in windows will reset cursor position to upper left corner of the screen. - */ - public static function clearScreen() - { - echo "\033[2J"; - } - - /** - * Clears text from cursor to the beginning of the screen by sending ANSI control code ED with argument 1 to the terminal. - * Cursor position will not be changed. - */ - public static function clearScreenBeforeCursor() - { - echo "\033[1J"; - } - - /** - * Clears text from cursor to the end of the screen by sending ANSI control code ED with argument 0 to the terminal. - * Cursor position will not be changed. - */ - public static function clearScreenAfterCursor() - { - echo "\033[0J"; - } - - /** - * Clears the line, the cursor is currently on by sending ANSI control code EL with argument 2 to the terminal. - * Cursor position will not be changed. - */ - public static function clearLine() - { - echo "\033[2K"; - } - - /** - * Clears text from cursor position to the beginning of the line by sending ANSI control code EL with argument 1 to the terminal. - * Cursor position will not be changed. - */ - public static function clearLineBeforeCursor() - { - echo "\033[1K"; - } - - /** - * Clears text from cursor position to the end of the line by sending ANSI control code EL with argument 0 to the terminal. - * Cursor position will not be changed. - */ - public static function clearLineAfterCursor() - { - echo "\033[0K"; - } - - /** - * Will send ANSI format for following output - * - * You can pass any of the FG_*, BG_* and TEXT_* constants and also xterm256ColorBg - * TODO: documentation - */ - public static function ansiStyle() - { - echo "\033[" . implode(';', func_get_args()) . 'm'; - } - - /** - * Will return a string formatted with the given ANSI style - * - * See {@link ansiStyle} for possible arguments. - * @param string $string the string to be formatted - * @return string - */ - public static function ansiStyleString($string) - { - $args = func_get_args(); - array_shift($args); - $code = implode(';', $args); - return "\033[0m" . ($code !== '' ? "\033[" . $code . "m" : '') . $string."\033[0m"; - } - - //const COLOR_XTERM256 = 38;// http://en.wikipedia.org/wiki/Talk:ANSI_escape_code#xterm-256colors - public static function xterm256ColorFg($i) // TODO naming! - { - return '38;5;'.$i; - } - - public static function xterm256ColorBg($i) // TODO naming! - { - return '48;5;'.$i; - } - - /** - * Usage: list($w, $h) = ConsoleHelper::getScreenSize(); - * - * @return array - */ - public static function getScreenSize() - { - // TODO implement - return array(150,50); - } - - /** - * resets any ansi style set by previous method {@link ansiStyle} - * Any output after this is will have default text style. - */ - public static function reset() - { - echo "\033[0m"; - } - - /** - * Strips ANSI control codes from a string - * - * @param string $string String to strip - * @return string - */ - public static function strip($string) - { - return preg_replace('/\033\[[\d;]+m/', '', $string); // TODO currently only strips color - } - - // TODO refactor and review - public static function ansiToHtml($string) - { - $tags = 0; - return preg_replace_callback('/\033\[[\d;]+m/', function($ansi) use (&$tags) { - $styleA = array(); - foreach(explode(';', $ansi) as $controlCode) - { - switch($controlCode) - { - case static::FG_BLACK: $style = array('color' => '#000000'); break; - case static::FG_BLUE: $style = array('color' => '#000078'); break; - case static::FG_CYAN: $style = array('color' => '#007878'); break; - case static::FG_GREEN: $style = array('color' => '#007800'); break; - case static::FG_GREY: $style = array('color' => '#787878'); break; - case static::FG_PURPLE: $style = array('color' => '#780078'); break; - case static::FG_RED: $style = array('color' => '#780000'); break; - case static::FG_YELLOW: $style = array('color' => '#787800'); break; - case static::BG_BLACK: $style = array('background-color' => '#000000'); break; - case static::BG_BLUE: $style = array('background-color' => '#000078'); break; - case static::BG_CYAN: $style = array('background-color' => '#007878'); break; - case static::BG_GREEN: $style = array('background-color' => '#007800'); break; - case static::BG_GREY: $style = array('background-color' => '#787878'); break; - case static::BG_PURPLE: $style = array('background-color' => '#780078'); break; - case static::BG_RED: $style = array('background-color' => '#780000'); break; - case static::BG_YELLOW: $style = array('background-color' => '#787800'); break; - case static::BOLD: $style = array('font-weight' => 'bold'); break; - case static::ITALIC: $style = array('font-style' => 'italic'); break; - case static::UNDERLINE: $style = array('text-decoration' => array('underline')); break; - case static::OVERLINED: $style = array('text-decoration' => array('overline')); break; - case static::CROSSED_OUT:$style = array('text-decoration' => array('line-through')); break; - case static::BLINK: $style = array('text-decoration' => array('blink')); break; - case static::NEGATIVE: // ??? - case static::CONCEALED: - case static::ENCIRCLED: - case static::FRAMED: - // TODO allow resetting codes - break; - case 0: // ansi reset - $return = ''; - for($n=$tags; $tags>0; $tags--) { - $return .= ''; - } - return $return; - } - - $styleA = ArrayHelper::merge($styleA, $style); - } - $styleString[] = array(); - foreach($styleA as $name => $content) { - if ($name === 'text-decoration') { - $content = implode(' ', $content); - } - $styleString[] = $name.':'.$content; - } - $tags++; - return ' $ds, '\\' => $ds)), $ds); - } - - /** - * Returns the localized version of a specified file. - * - * The searching is based on the specified language code. In particular, - * a file with the same name will be looked for under the subdirectory - * whose name is same as the language code. For example, given the file "path/to/view.php" - * and language code "zh_cn", the localized file will be looked for as - * "path/to/zh_cn/view.php". If the file is not found, the original file - * will be returned. - * - * If the target and the source language codes are the same, - * the original file will be returned. - * - * For consistency, it is recommended that the language code is given - * in lower case and in the format of LanguageID_RegionID (e.g. "en_us"). - * - * @param string $file the original file - * @param string $language the target language that the file should be localized to. - * If not set, the value of [[\yii\base\Application::language]] will be used. - * @param string $sourceLanguage the language that the original file is in. - * If not set, the value of [[\yii\base\Application::sourceLanguage]] will be used. - * @return string the matching localized file, or the original file if the localized version is not found. - * If the target and the source language codes are the same, the original file will be returned. - */ - public static function localize($file, $language = null, $sourceLanguage = null) - { - if ($language === null) { - $language = \Yii::$app->language; - } - if ($sourceLanguage === null) { - $sourceLanguage = \Yii::$app->sourceLanguage; - } - if ($language === $sourceLanguage) { - return $file; - } - $desiredFile = dirname($file) . DIRECTORY_SEPARATOR . $sourceLanguage . DIRECTORY_SEPARATOR . basename($file); - return is_file($desiredFile) ? $desiredFile : $file; - } - - /** - * Determines the MIME type of the specified file. - * This method will first try to determine the MIME type based on - * [finfo_open](http://php.net/manual/en/function.finfo-open.php). If this doesn't work, it will - * fall back to [[getMimeTypeByExtension()]]. - * @param string $file the file name. - * @param string $magicFile name of the optional magic database file, usually something like `/path/to/magic.mime`. - * This will be passed as the second parameter to [finfo_open](http://php.net/manual/en/function.finfo-open.php). - * @param boolean $checkExtension whether to use the file extension to determine the MIME type in case - * `finfo_open()` cannot determine it. - * @return string the MIME type (e.g. `text/plain`). Null is returned if the MIME type cannot be determined. - */ - public static function getMimeType($file, $magicFile = null, $checkExtension = true) - { - if (function_exists('finfo_open')) { - $info = finfo_open(FILEINFO_MIME_TYPE, $magicFile); - if ($info && ($result = finfo_file($info, $file)) !== false) { - return $result; - } - } - - return $checkExtension ? self::getMimeTypeByExtension($file) : null; - } - - /** - * Determines the MIME type based on the extension name of the specified file. - * This method will use a local map between extension names and MIME types. - * @param string $file the file name. - * @param string $magicFile the path of the file that contains all available MIME type information. - * If this is not set, the default file aliased by `@yii/util/mimeTypes.php` will be used. - * @return string the MIME type. Null is returned if the MIME type cannot be determined. - */ - public static function getMimeTypeByExtension($file, $magicFile = null) - { - if ($magicFile === null) { - $magicFile = \Yii::getAlias('@yii/util/mimeTypes.php'); - } - $mimeTypes = require($magicFile); - if (($ext = pathinfo($file, PATHINFO_EXTENSION)) !== '') { - $ext = strtolower($ext); - if (isset($mimeTypes[$ext])) { - return $mimeTypes[$ext]; - } - } - return null; - } - - /** - * Copies a list of files from one place to another. - * @param array $fileList the list of files to be copied (name=>spec). - * The array keys are names displayed during the copy process, and array values are specifications - * for files to be copied. Each array value must be an array of the following structure: - *
    - *
  • source: required, the full path of the file/directory to be copied from
  • - *
  • target: required, the full path of the file/directory to be copied to
  • - *
  • callback: optional, the callback to be invoked when copying a file. The callback function - * should be declared as follows: - *
    -	 *   function foo($source,$params)
    -	 *   
    - * where $source parameter is the source file path, and the content returned - * by the function will be saved into the target file.
  • - *
  • params: optional, the parameters to be passed to the callback
  • - *
- * @see buildFileList - */ - public static function copyFiles($fileList) - { - $overwriteAll = false; - foreach($fileList as $name=>$file) { - $source = strtr($file['source'], '/\\', DIRECTORY_SEPARATOR); - $target = strtr($file['target'], '/\\', DIRECTORY_SEPARATOR); - $callback = isset($file['callback']) ? $file['callback'] : null; - $params = isset($file['params']) ? $file['params'] : null; - - if(is_dir($source)) { - try { - self::ensureDirectory($target); - } - catch (Exception $e) { - mkdir($target, true, 0777); - } - continue; - } - - if($callback !== null) { - $content = call_user_func($callback, $source, $params); - } - else { - $content = file_get_contents($source); - } - if(is_file($target)) { - if($content === file_get_contents($target)) { - echo " unchanged $name\n"; - continue; - } - if($overwriteAll) { - echo " overwrite $name\n"; - } - else { - echo " exist $name\n"; - echo " ...overwrite? [Yes|No|All|Quit] "; - $answer = trim(fgets(STDIN)); - if(!strncasecmp($answer, 'q', 1)) { - return; - } - elseif(!strncasecmp($answer, 'y', 1)) { - echo " overwrite $name\n"; - } - elseif(!strncasecmp($answer, 'a', 1)) { - echo " overwrite $name\n"; - $overwriteAll = true; - } - else { - echo " skip $name\n"; - continue; - } - } - } - else { - try { - self::ensureDirectory(dirname($target)); - } - catch (Exception $e) { - mkdir(dirname($target), true, 0777); - } - echo " generate $name\n"; - } - file_put_contents($target, $content); - } - } - - /** - * Builds the file list of a directory. - * This method traverses through the specified directory and builds - * a list of files and subdirectories that the directory contains. - * The result of this function can be passed to {@link copyFiles}. - * @param string $sourceDir the source directory - * @param string $targetDir the target directory - * @param string $baseDir base directory - * @param array $ignoreFiles list of the names of files that should - * be ignored in list building process. Argument available since 1.1.11. - * @param array $renameMap hash array of file names that should be - * renamed. Example value: array('1.old.txt'=>'2.new.txt'). - * @return array the file list (see {@link copyFiles}) - */ - public static function buildFileList($sourceDir, $targetDir, $baseDir='', $ignoreFiles=array(), $renameMap=array()) - { - $list = array(); - $handle = opendir($sourceDir); - while(($file = readdir($handle)) !== false) { - if(in_array($file, array('.', '..', '.svn', '.gitignore')) || in_array($file, $ignoreFiles)) { - continue; - } - $sourcePath = $sourceDir.DIRECTORY_SEPARATOR.$file; - $targetPath = $targetDir.DIRECTORY_SEPARATOR.strtr($file, $renameMap); - $name = $baseDir === '' ? $file : $baseDir.'/'.$file; - $list[$name] = array( - 'source' => $sourcePath, - 'target' => $targetPath, - ); - if(is_dir($sourcePath)) { - $list = array_merge($list, self::buildFileList($sourcePath, $targetPath, $name, $ignoreFiles, $renameMap)); - } - } - closedir($handle); - return $list; - } } \ No newline at end of file diff --git a/framework/helpers/Html.php b/framework/helpers/Html.php index b2ca576..b3a0743 100644 --- a/framework/helpers/Html.php +++ b/framework/helpers/Html.php @@ -7,975 +7,12 @@ namespace yii\helpers; -use Yii; -use yii\base\InvalidParamException; - /** * Html provides a set of static methods for generating commonly used HTML tags. * * @author Qiang Xue * @since 2.0 */ -class Html +class Html extends base\Html { - /** - * @var boolean whether to close void (empty) elements. Defaults to true. - * @see voidElements - */ - public static $closeVoidElements = true; - /** - * @var array list of void elements (element name => 1) - * @see closeVoidElements - * @see http://www.w3.org/TR/html-markup/syntax.html#void-element - */ - public static $voidElements = array( - 'area' => 1, - 'base' => 1, - 'br' => 1, - 'col' => 1, - 'command' => 1, - 'embed' => 1, - 'hr' => 1, - 'img' => 1, - 'input' => 1, - 'keygen' => 1, - 'link' => 1, - 'meta' => 1, - 'param' => 1, - 'source' => 1, - 'track' => 1, - 'wbr' => 1, - ); - /** - * @var boolean whether to show the values of boolean attributes in element tags. - * If false, only the attribute names will be generated. - * @see booleanAttributes - */ - public static $showBooleanAttributeValues = true; - /** - * @var array list of boolean attributes. The presence of a boolean attribute on - * an element represents the true value, and the absence of the attribute represents the false value. - * @see showBooleanAttributeValues - * @see http://www.w3.org/TR/html5/infrastructure.html#boolean-attributes - */ - public static $booleanAttributes = array( - 'async' => 1, - 'autofocus' => 1, - 'autoplay' => 1, - 'checked' => 1, - 'controls' => 1, - 'declare' => 1, - 'default' => 1, - 'defer' => 1, - 'disabled' => 1, - 'formnovalidate' => 1, - 'hidden' => 1, - 'ismap' => 1, - 'loop' => 1, - 'multiple' => 1, - 'muted' => 1, - 'nohref' => 1, - 'noresize' => 1, - 'novalidate' => 1, - 'open' => 1, - 'readonly' => 1, - 'required' => 1, - 'reversed' => 1, - 'scoped' => 1, - 'seamless' => 1, - 'selected' => 1, - 'typemustmatch' => 1, - ); - /** - * @var array the preferred order of attributes in a tag. This mainly affects the order of the attributes - * that are rendered by [[renderAttributes()]]. - */ - public static $attributeOrder = array( - 'type', - 'id', - 'class', - 'name', - 'value', - - 'href', - 'src', - 'action', - 'method', - - 'selected', - 'checked', - 'readonly', - 'disabled', - 'multiple', - - 'size', - 'maxlength', - 'width', - 'height', - 'rows', - 'cols', - - 'alt', - 'title', - 'rel', - 'media', - ); - - /** - * Encodes special characters into HTML entities. - * The [[yii\base\Application::charset|application charset]] will be used for encoding. - * @param string $content the content to be encoded - * @return string the encoded content - * @see decode - * @see http://www.php.net/manual/en/function.htmlspecialchars.php - */ - public static function encode($content) - { - return htmlspecialchars($content, ENT_QUOTES, Yii::$app->charset); - } - - /** - * Decodes special HTML entities back to the corresponding characters. - * This is the opposite of [[encode()]]. - * @param string $content the content to be decoded - * @return string the decoded content - * @see encode - * @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php - */ - public static function decode($content) - { - return htmlspecialchars_decode($content, ENT_QUOTES); - } - - /** - * Generates a complete HTML tag. - * @param string $name the tag name - * @param string $content the content to be enclosed between the start and end tags. It will not be HTML-encoded. - * If this is coming from end users, you should consider [[encode()]] it to prevent XSS attacks. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated HTML tag - * @see beginTag - * @see endTag - */ - public static function tag($name, $content = '', $options = array()) - { - $html = '<' . $name . static::renderTagAttributes($options); - if (isset(static::$voidElements[strtolower($name)])) { - return $html . (static::$closeVoidElements ? ' />' : '>'); - } else { - return $html . ">$content"; - } - } - - /** - * Generates a start tag. - * @param string $name the tag name - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated start tag - * @see endTag - * @see tag - */ - public static function beginTag($name, $options = array()) - { - return '<' . $name . static::renderTagAttributes($options) . '>'; - } - - /** - * Generates an end tag. - * @param string $name the tag name - * @return string the generated end tag - * @see beginTag - * @see tag - */ - public static function endTag($name) - { - return ""; - } - - /** - * Encloses the given content within a CDATA tag. - * @param string $content the content to be enclosed within the CDATA tag - * @return string the CDATA tag with the enclosed content. - */ - public static function cdata($content) - { - return ''; - } - - /** - * Generates a style tag. - * @param string $content the style content - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * If the options does not contain "type", a "type" attribute with value "text/css" will be used. - * @return string the generated style tag - */ - public static function style($content, $options = array()) - { - if (!isset($options['type'])) { - $options['type'] = 'text/css'; - } - return static::tag('style', "/**/", $options); - } - - /** - * Generates a script tag. - * @param string $content the script content - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * If the options does not contain "type", a "type" attribute with value "text/javascript" will be rendered. - * @return string the generated script tag - */ - public static function script($content, $options = array()) - { - if (!isset($options['type'])) { - $options['type'] = 'text/javascript'; - } - return static::tag('script', "/**/", $options); - } - - /** - * Generates a link tag that refers to an external CSS file. - * @param array|string $url the URL of the external CSS file. This parameter will be processed by [[url()]]. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated link tag - * @see url - */ - public static function cssFile($url, $options = array()) - { - $options['rel'] = 'stylesheet'; - $options['type'] = 'text/css'; - $options['href'] = static::url($url); - return static::tag('link', '', $options); - } - - /** - * Generates a script tag that refers to an external JavaScript file. - * @param string $url the URL of the external JavaScript file. This parameter will be processed by [[url()]]. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated script tag - * @see url - */ - public static function jsFile($url, $options = array()) - { - $options['type'] = 'text/javascript'; - $options['src'] = static::url($url); - return static::tag('script', '', $options); - } - - /** - * Generates a form start tag. - * @param array|string $action the form action URL. This parameter will be processed by [[url()]]. - * @param string $method the form submission method, either "post" or "get" (case-insensitive) - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated form start tag. - * @see endForm - */ - public static function beginForm($action = '', $method = 'post', $options = array()) - { - $action = static::url($action); - - // query parameters in the action are ignored for GET method - // we use hidden fields to add them back - $hiddens = array(); - if (!strcasecmp($method, 'get') && ($pos = strpos($action, '?')) !== false) { - foreach (explode('&', substr($action, $pos + 1)) as $pair) { - if (($pos1 = strpos($pair, '=')) !== false) { - $hiddens[] = static::hiddenInput(urldecode(substr($pair, 0, $pos1)), urldecode(substr($pair, $pos1 + 1))); - } else { - $hiddens[] = static::hiddenInput(urldecode($pair), ''); - } - } - $action = substr($action, 0, $pos); - } - - $options['action'] = $action; - $options['method'] = $method; - $form = static::beginTag('form', $options); - if ($hiddens !== array()) { - $form .= "\n" . implode("\n", $hiddens); - } - - return $form; - } - - /** - * Generates a form end tag. - * @return string the generated tag - * @see beginForm - */ - public static function endForm() - { - return ''; - } - - /** - * Generates a hyperlink tag. - * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code - * such as an image tag. If this is is coming from end users, you should consider [[encode()]] - * it to prevent XSS attacks. - * @param array|string|null $url the URL for the hyperlink tag. This parameter will be processed by [[url()]] - * and will be used for the "href" attribute of the tag. If this parameter is null, the "href" attribute - * will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated hyperlink - * @see url - */ - public static function a($text, $url = null, $options = array()) - { - if ($url !== null) { - $options['href'] = static::url($url); - } - return static::tag('a', $text, $options); - } - - /** - * Generates a mailto hyperlink. - * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code - * such as an image tag. If this is is coming from end users, you should consider [[encode()]] - * it to prevent XSS attacks. - * @param string $email email address. If this is null, the first parameter (link body) will be treated - * as the email address and used. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated mailto link - */ - public static function mailto($text, $email = null, $options = array()) - { - return static::a($text, 'mailto:' . ($email === null ? $text : $email), $options); - } - - /** - * Generates an image tag. - * @param string $src the image URL. This parameter will be processed by [[url()]]. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated image tag - */ - public static function img($src, $options = array()) - { - $options['src'] = static::url($src); - if (!isset($options['alt'])) { - $options['alt'] = ''; - } - return static::tag('img', null, $options); - } - - /** - * Generates a label tag. - * @param string $content label text. It will NOT be HTML-encoded. Therefore you can pass in HTML code - * such as an image tag. If this is is coming from end users, you should consider [[encode()]] - * it to prevent XSS attacks. - * @param string $for the ID of the HTML element that this label is associated with. - * If this is null, the "for" attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated label tag - */ - public static function label($content, $for = null, $options = array()) - { - $options['for'] = $for; - return static::tag('label', $content, $options); - } - - /** - * Generates a button tag. - * @param string $name the name attribute. If it is null, the name attribute will not be generated. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. - * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, - * you should consider [[encode()]] it to prevent XSS attacks. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * If the options does not contain "type", a "type" attribute with value "button" will be rendered. - * @return string the generated button tag - */ - public static function button($name = null, $value = null, $content = 'Button', $options = array()) - { - $options['name'] = $name; - $options['value'] = $value; - if (!isset($options['type'])) { - $options['type'] = 'button'; - } - return static::tag('button', $content, $options); - } - - /** - * Generates a submit button tag. - * @param string $name the name attribute. If it is null, the name attribute will not be generated. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. - * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, - * you should consider [[encode()]] it to prevent XSS attacks. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated submit button tag - */ - public static function submitButton($name = null, $value = null, $content = 'Submit', $options = array()) - { - $options['type'] = 'submit'; - return static::button($name, $value, $content, $options); - } - - /** - * Generates a reset button tag. - * @param string $name the name attribute. If it is null, the name attribute will not be generated. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. - * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, - * you should consider [[encode()]] it to prevent XSS attacks. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated reset button tag - */ - public static function resetButton($name = null, $value = null, $content = 'Reset', $options = array()) - { - $options['type'] = 'reset'; - return static::button($name, $value, $content, $options); - } - - /** - * Generates an input type of the given type. - * @param string $type the type attribute. - * @param string $name the name attribute. If it is null, the name attribute will not be generated. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated input tag - */ - public static function input($type, $name = null, $value = null, $options = array()) - { - $options['type'] = $type; - $options['name'] = $name; - $options['value'] = $value; - return static::tag('input', null, $options); - } - - /** - * Generates an input button. - * @param string $name the name attribute. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated button tag - */ - public static function buttonInput($name, $value = 'Button', $options = array()) - { - return static::input('button', $name, $value, $options); - } - - /** - * Generates a submit input button. - * @param string $name the name attribute. If it is null, the name attribute will not be generated. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated button tag - */ - public static function submitInput($name = null, $value = 'Submit', $options = array()) - { - return static::input('submit', $name, $value, $options); - } - - /** - * Generates a reset input button. - * @param string $name the name attribute. If it is null, the name attribute will not be generated. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the tag returned. - * @return string the generated button tag - */ - public static function resetInput($name = null, $value = 'Reset', $options = array()) - { - return static::input('reset', $name, $value, $options); - } - - /** - * Generates a text input field. - * @param string $name the name attribute. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated button tag - */ - public static function textInput($name, $value = null, $options = array()) - { - return static::input('text', $name, $value, $options); - } - - /** - * Generates a hidden input field. - * @param string $name the name attribute. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated button tag - */ - public static function hiddenInput($name, $value = null, $options = array()) - { - return static::input('hidden', $name, $value, $options); - } - - /** - * Generates a password input field. - * @param string $name the name attribute. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated button tag - */ - public static function passwordInput($name, $value = null, $options = array()) - { - return static::input('password', $name, $value, $options); - } - - /** - * Generates a file input field. - * To use a file input field, you should set the enclosing form's "enctype" attribute to - * be "multipart/form-data". After the form is submitted, the uploaded file information - * can be obtained via $_FILES[$name] (see PHP documentation). - * @param string $name the name attribute. - * @param string $value the value attribute. If it is null, the value attribute will not be generated. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated button tag - */ - public static function fileInput($name, $value = null, $options = array()) - { - return static::input('file', $name, $value, $options); - } - - /** - * Generates a text area input. - * @param string $name the input name - * @param string $value the input value. Note that it will be encoded using [[encode()]]. - * @param array $options the tag options in terms of name-value pairs. These will be rendered as - * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. - * If a value is null, the corresponding attribute will not be rendered. - * @return string the generated text area tag - */ - public static function textarea($name, $value = '', $options = array()) - { - $options['name'] = $name; - return static::tag('textarea', static::encode($value), $options); - } - - /** - * Generates a radio button input. - * @param string $name the name attribute. - * @param boolean $checked whether the radio button should be checked. - * @param string $value the value attribute. If it is null, the value attribute will not be rendered. - * @param array $options the tag options in terms of name-value pairs. The following options are supported: - * - * - uncheck: string, the value associated with the uncheck state of the radio button. When this attribute - * is present, a hidden input will be generated so that if the radio button is not checked and is submitted, - * the value of this attribute will still be submitted to the server via the hidden input. - * - * The rest of the options will be rendered as the attributes of the resulting tag. The values will - * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. - * - * @return string the generated radio button tag - */ - public static function radio($name, $checked = false, $value = '1', $options = array()) - { - $options['checked'] = $checked; - $options['value'] = $value; - if (isset($options['uncheck'])) { - // add a hidden field so that if the radio button is not selected, it still submits a value - $hidden = static::hiddenInput($name, $options['uncheck']); - unset($options['uncheck']); - } else { - $hidden = ''; - } - return $hidden . static::input('radio', $name, $value, $options); - } - - /** - * Generates a checkbox input. - * @param string $name the name attribute. - * @param boolean $checked whether the checkbox should be checked. - * @param string $value the value attribute. If it is null, the value attribute will not be rendered. - * @param array $options the tag options in terms of name-value pairs. The following options are supported: - * - * - uncheck: string, the value associated with the uncheck state of the checkbox. When this attribute - * is present, a hidden input will be generated so that if the checkbox is not checked and is submitted, - * the value of this attribute will still be submitted to the server via the hidden input. - * - * The rest of the options will be rendered as the attributes of the resulting tag. The values will - * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. - * - * @return string the generated checkbox tag - */ - public static function checkbox($name, $checked = false, $value = '1', $options = array()) - { - $options['checked'] = $checked; - $options['value'] = $value; - if (isset($options['uncheck'])) { - // add a hidden field so that if the checkbox is not selected, it still submits a value - $hidden = static::hiddenInput($name, $options['uncheck']); - unset($options['uncheck']); - } else { - $hidden = ''; - } - return $hidden . static::input('checkbox', $name, $value, $options); - } - - /** - * Generates a drop-down list. - * @param string $name the input name - * @param string $selection the selected value - * @param array $items the option data items. The array keys are option values, and the array values - * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). - * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. - * If you have a list of data models, you may convert them into the format described above using - * [[\yii\helpers\ArrayHelper::map()]]. - * - * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in - * the labels will also be HTML-encoded. - * @param array $options the tag options in terms of name-value pairs. The following options are supported: - * - * - prompt: string, a prompt text to be displayed as the first option; - * - options: array, the attributes for the select option tags. The array keys must be valid option values, - * and the array values are the extra attributes for the corresponding option tags. For example, - * - * ~~~ - * array( - * 'value1' => array('disabled' => true), - * 'value2' => array('label' => 'value 2'), - * ); - * ~~~ - * - * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', - * except that the array keys represent the optgroup labels specified in $items. - * - * The rest of the options will be rendered as the attributes of the resulting tag. The values will - * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. - * - * @return string the generated drop-down list tag - */ - public static function dropDownList($name, $selection = null, $items = array(), $options = array()) - { - $options['name'] = $name; - $selectOptions = static::renderSelectOptions($selection, $items, $options); - return static::tag('select', "\n" . $selectOptions . "\n", $options); - } - - /** - * Generates a list box. - * @param string $name the input name - * @param string|array $selection the selected value(s) - * @param array $items the option data items. The array keys are option values, and the array values - * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). - * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. - * If you have a list of data models, you may convert them into the format described above using - * [[\yii\helpers\ArrayHelper::map()]]. - * - * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in - * the labels will also be HTML-encoded. - * @param array $options the tag options in terms of name-value pairs. The following options are supported: - * - * - prompt: string, a prompt text to be displayed as the first option; - * - options: array, the attributes for the select option tags. The array keys must be valid option values, - * and the array values are the extra attributes for the corresponding option tags. For example, - * - * ~~~ - * array( - * 'value1' => array('disabled' => true), - * 'value2' => array('label' => 'value 2'), - * ); - * ~~~ - * - * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', - * except that the array keys represent the optgroup labels specified in $items. - * - unselect: string, the value that will be submitted when no option is selected. - * When this attribute is set, a hidden field will be generated so that if no option is selected in multiple - * mode, we can still obtain the posted unselect value. - * - * The rest of the options will be rendered as the attributes of the resulting tag. The values will - * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. - * - * @return string the generated list box tag - */ - public static function listBox($name, $selection = null, $items = array(), $options = array()) - { - if (!isset($options['size'])) { - $options['size'] = 4; - } - if (isset($options['multiple']) && $options['multiple'] && substr($name, -2) !== '[]') { - $name .= '[]'; - } - $options['name'] = $name; - if (isset($options['unselect'])) { - // add a hidden field so that if the list box has no option being selected, it still submits a value - if (substr($name, -2) === '[]') { - $name = substr($name, 0, -2); - } - $hidden = static::hiddenInput($name, $options['unselect']); - unset($options['unselect']); - } else { - $hidden = ''; - } - $selectOptions = static::renderSelectOptions($selection, $items, $options); - return $hidden . static::tag('select', "\n" . $selectOptions . "\n", $options); - } - - /** - * Generates a list of checkboxes. - * A checkbox list allows multiple selection, like [[listBox()]]. - * As a result, the corresponding submitted value is an array. - * @param string $name the name attribute of each checkbox. - * @param string|array $selection the selected value(s). - * @param array $items the data item used to generate the checkboxes. - * The array keys are the labels, while the array values are the corresponding checkbox values. - * Note that the labels will NOT be HTML-encoded, while the values will. - * @param array $options options (name => config) for the checkbox list. The following options are supported: - * - * - unselect: string, the value that should be submitted when none of the checkboxes is selected. - * By setting this option, a hidden input will be generated. - * - separator: string, the HTML code that separates items. - * - item: callable, a callback that can be used to customize the generation of the HTML code - * corresponding to a single item in $items. The signature of this callback must be: - * - * ~~~ - * function ($index, $label, $name, $checked, $value) - * ~~~ - * - * where $index is the zero-based index of the checkbox in the whole list; $label - * is the label for the checkbox; and $name, $value and $checked represent the name, - * value and the checked status of the checkbox input. - * @return string the generated checkbox list - */ - public static function checkboxList($name, $selection = null, $items = array(), $options = array()) - { - if (substr($name, -2) !== '[]') { - $name .= '[]'; - } - - $formatter = isset($options['item']) ? $options['item'] : null; - $lines = array(); - $index = 0; - foreach ($items as $value => $label) { - $checked = $selection !== null && - (!is_array($selection) && !strcmp($value, $selection) - || is_array($selection) && in_array($value, $selection)); - if ($formatter !== null) { - $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); - } else { - $lines[] = static::label(static::checkbox($name, $checked, $value) . ' ' . $label); - } - $index++; - } - - if (isset($options['unselect'])) { - // add a hidden field so that if the list box has no option being selected, it still submits a value - $name2 = substr($name, -2) === '[]' ? substr($name, 0, -2) : $name; - $hidden = static::hiddenInput($name2, $options['unselect']); - } else { - $hidden = ''; - } - $separator = isset($options['separator']) ? $options['separator'] : "\n"; - - return $hidden . implode($separator, $lines); - } - - /** - * Generates a list of radio buttons. - * A radio button list is like a checkbox list, except that it only allows single selection. - * @param string $name the name attribute of each radio button. - * @param string|array $selection the selected value(s). - * @param array $items the data item used to generate the radio buttons. - * The array keys are the labels, while the array values are the corresponding radio button values. - * Note that the labels will NOT be HTML-encoded, while the values will. - * @param array $options options (name => config) for the radio button list. The following options are supported: - * - * - unselect: string, the value that should be submitted when none of the radio buttons is selected. - * By setting this option, a hidden input will be generated. - * - separator: string, the HTML code that separates items. - * - item: callable, a callback that can be used to customize the generation of the HTML code - * corresponding to a single item in $items. The signature of this callback must be: - * - * ~~~ - * function ($index, $label, $name, $checked, $value) - * ~~~ - * - * where $index is the zero-based index of the radio button in the whole list; $label - * is the label for the radio button; and $name, $value and $checked represent the name, - * value and the checked status of the radio button input. - * @return string the generated radio button list - */ - public static function radioList($name, $selection = null, $items = array(), $options = array()) - { - $formatter = isset($options['item']) ? $options['item'] : null; - $lines = array(); - $index = 0; - foreach ($items as $value => $label) { - $checked = $selection !== null && - (!is_array($selection) && !strcmp($value, $selection) - || is_array($selection) && in_array($value, $selection)); - if ($formatter !== null) { - $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); - } else { - $lines[] = static::label(static::radio($name, $checked, $value) . ' ' . $label); - } - $index++; - } - - $separator = isset($options['separator']) ? $options['separator'] : "\n"; - if (isset($options['unselect'])) { - // add a hidden field so that if the list box has no option being selected, it still submits a value - $hidden = static::hiddenInput($name, $options['unselect']); - } else { - $hidden = ''; - } - - return $hidden . implode($separator, $lines); - } - - /** - * Renders the option tags that can be used by [[dropDownList()]] and [[listBox()]]. - * @param string|array $selection the selected value(s). This can be either a string for single selection - * or an array for multiple selections. - * @param array $items the option data items. The array keys are option values, and the array values - * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). - * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. - * If you have a list of data models, you may convert them into the format described above using - * [[\yii\helpers\ArrayHelper::map()]]. - * - * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in - * the labels will also be HTML-encoded. - * @param array $tagOptions the $options parameter that is passed to the [[dropDownList()]] or [[listBox()]] call. - * This method will take out these elements, if any: "prompt", "options" and "groups". See more details - * in [[dropDownList()]] for the explanation of these elements. - * - * @return string the generated list options - */ - public static function renderSelectOptions($selection, $items, &$tagOptions = array()) - { - $lines = array(); - if (isset($tagOptions['prompt'])) { - $prompt = str_replace(' ', ' ', static::encode($tagOptions['prompt'])); - $lines[] = static::tag('option', $prompt, array('value' => '')); - } - - $options = isset($tagOptions['options']) ? $tagOptions['options'] : array(); - $groups = isset($tagOptions['groups']) ? $tagOptions['groups'] : array(); - unset($tagOptions['prompt'], $tagOptions['options'], $tagOptions['groups']); - - foreach ($items as $key => $value) { - if (is_array($value)) { - $groupAttrs = isset($groups[$key]) ? $groups[$key] : array(); - $groupAttrs['label'] = $key; - $attrs = array('options' => $options, 'groups' => $groups); - $content = static::renderSelectOptions($selection, $value, $attrs); - $lines[] = static::tag('optgroup', "\n" . $content . "\n", $groupAttrs); - } else { - $attrs = isset($options[$key]) ? $options[$key] : array(); - $attrs['value'] = $key; - $attrs['selected'] = $selection !== null && - (!is_array($selection) && !strcmp($key, $selection) - || is_array($selection) && in_array($key, $selection)); - $lines[] = static::tag('option', str_replace(' ', ' ', static::encode($value)), $attrs); - } - } - - return implode("\n", $lines); - } - - /** - * Renders the HTML tag attributes. - * Boolean attributes such as s 'checked', 'disabled', 'readonly', will be handled specially - * according to [[booleanAttributes]] and [[showBooleanAttributeValues]]. - * @param array $attributes attributes to be rendered. The attribute values will be HTML-encoded using [[encode()]]. - * Attributes whose value is null will be ignored and not put in the rendering result. - * @return string the rendering result. If the attributes are not empty, they will be rendered - * into a string with a leading white space (such that it can be directly appended to the tag name - * in a tag. If there is no attribute, an empty string will be returned. - */ - public static function renderTagAttributes($attributes) - { - if (count($attributes) > 1) { - $sorted = array(); - foreach (static::$attributeOrder as $name) { - if (isset($attributes[$name])) { - $sorted[$name] = $attributes[$name]; - } - } - $attributes = array_merge($sorted, $attributes); - } - - $html = ''; - foreach ($attributes as $name => $value) { - if (isset(static::$booleanAttributes[strtolower($name)])) { - if ($value || strcasecmp($name, $value) === 0) { - $html .= static::$showBooleanAttributeValues ? " $name=\"$name\"" : " $name"; - } - } elseif ($value !== null) { - $html .= " $name=\"" . static::encode($value) . '"'; - } - } - return $html; - } - - /** - * Normalizes the input parameter to be a valid URL. - * - * If the input parameter - * - * - is an empty string: the currently requested URL will be returned; - * - is a non-empty string: it will be processed by [[Yii::getAlias()]] and returned; - * - is an array: the first array element is considered a route, while the rest of the name-value - * pairs are treated as the parameters to be used for URL creation using [[\yii\web\Controller::createUrl()]]. - * For example: `array('post/index', 'page' => 2)`, `array('index')`. - * - * @param array|string $url the parameter to be used to generate a valid URL - * @return string the normalized URL - * @throws InvalidParamException if the parameter is invalid. - */ - public static function url($url) - { - if (is_array($url)) { - if (isset($url[0])) { - $route = $url[0]; - $params = array_splice($url, 1); - if (Yii::$app->controller !== null) { - return Yii::$app->controller->createUrl($route, $params); - } else { - return Yii::$app->getUrlManager()->createUrl($route, $params); - } - } else { - throw new InvalidParamException('The array specifying a URL must contain at least one element.'); - } - } elseif ($url === '') { - return Yii::$app->getRequest()->getUrl(); - } else { - return Yii::getAlias($url); - } - } } diff --git a/framework/helpers/SecurityHelper.php b/framework/helpers/SecurityHelper.php index 5029dd6..d3cb2ad 100644 --- a/framework/helpers/SecurityHelper.php +++ b/framework/helpers/SecurityHelper.php @@ -7,11 +7,6 @@ namespace yii\helpers; -use Yii; -use yii\base\Exception; -use yii\base\InvalidConfigException; -use yii\base\InvalidParamException; - /** * SecurityHelper provides a set of methods to handle common security-related tasks. * @@ -29,244 +24,6 @@ use yii\base\InvalidParamException; * @author Tom Worster * @since 2.0 */ -class SecurityHelper +class SecurityHelper extends base\SecurityHelper { - /** - * Encrypts data. - * @param string $data data to be encrypted. - * @param string $key the encryption secret key - * @return string the encrypted data - * @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized - * @see decrypt() - */ - public static function encrypt($data, $key) - { - $module = static::openCryptModule(); - $key = StringHelper::substr($key, 0, mcrypt_enc_get_key_size($module)); - srand(); - $iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($module), MCRYPT_RAND); - mcrypt_generic_init($module, $key, $iv); - $encrypted = $iv . mcrypt_generic($module, $data); - mcrypt_generic_deinit($module); - mcrypt_module_close($module); - return $encrypted; - } - - /** - * Decrypts data - * @param string $data data to be decrypted. - * @param string $key the decryption secret key - * @return string the decrypted data - * @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized - * @see encrypt() - */ - public static function decrypt($data, $key) - { - $module = static::openCryptModule(); - $key = StringHelper::substr($key, 0, mcrypt_enc_get_key_size($module)); - $ivSize = mcrypt_enc_get_iv_size($module); - $iv = StringHelper::substr($data, 0, $ivSize); - mcrypt_generic_init($module, $key, $iv); - $decrypted = mdecrypt_generic($module, StringHelper::substr($data, $ivSize, StringHelper::strlen($data))); - mcrypt_generic_deinit($module); - mcrypt_module_close($module); - return rtrim($decrypted, "\0"); - } - - /** - * Prefixes data with a keyed hash value so that it can later be detected if it is tampered. - * @param string $data the data to be protected - * @param string $key the secret key to be used for generating hash - * @param string $algorithm the hashing algorithm (e.g. "md5", "sha1", "sha256", etc.). Call PHP "hash_algos()" - * function to see the supported hashing algorithms on your system. - * @return string the data prefixed with the keyed hash - * @see validateData() - * @see getSecretKey() - */ - public static function hashData($data, $key, $algorithm = 'sha256') - { - return hash_hmac($algorithm, $data, $key) . $data; - } - - /** - * Validates if the given data is tampered. - * @param string $data the data to be validated. The data must be previously - * generated by [[hashData()]]. - * @param string $key the secret key that was previously used to generate the hash for the data in [[hashData()]]. - * @param string $algorithm the hashing algorithm (e.g. "md5", "sha1", "sha256", etc.). Call PHP "hash_algos()" - * function to see the supported hashing algorithms on your system. This must be the same - * as the value passed to [[hashData()]] when generating the hash for the data. - * @return string the real data with the hash stripped off. False if the data is tampered. - * @see hashData() - */ - public static function validateData($data, $key, $algorithm = 'sha256') - { - $hashSize = StringHelper::strlen(hash_hmac($algorithm, 'test', $key)); - $n = StringHelper::strlen($data); - if ($n >= $hashSize) { - $hash = StringHelper::substr($data, 0, $hashSize); - $data2 = StringHelper::substr($data, $hashSize, $n - $hashSize); - return $hash === hash_hmac($algorithm, $data2, $key) ? $data2 : false; - } else { - return false; - } - } - - /** - * Returns a secret key associated with the specified name. - * If the secret key does not exist, a random key will be generated - * and saved in the file "keys.php" under the application's runtime directory - * so that the same secret key can be returned in future requests. - * @param string $name the name that is associated with the secret key - * @param integer $length the length of the key that should be generated if not exists - * @return string the secret key associated with the specified name - */ - public static function getSecretKey($name, $length = 32) - { - static $keys; - $keyFile = Yii::$app->getRuntimePath() . '/keys.php'; - if ($keys === null) { - $keys = is_file($keyFile) ? require($keyFile) : array(); - } - if (!isset($keys[$name])) { - // generate a 32-char random key - $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - $keys[$name] = substr(str_shuffle(str_repeat($chars, 5)), 0, $length); - file_put_contents($keyFile, " 30) { - throw new InvalidParamException('Hash is invalid.'); - } - - $test = crypt($password, $hash); - $n = strlen($test); - if (strlen($test) < 32 || $n !== strlen($hash)) { - return false; - } - - // Use a for-loop to compare two strings to prevent timing attacks. See: - // http://codereview.stackexchange.com/questions/13512 - $check = 0; - for ($i = 0; $i < $n; ++$i) { - $check |= (ord($test[$i]) ^ ord($hash[$i])); - } - - return $check === 0; - } - - /** - * Generates a salt that can be used to generate a password hash. - * - * The PHP [crypt()](http://php.net/manual/en/function.crypt.php) built-in function - * requires, for the Blowfish hash algorithm, a salt string in a specific format: - * "$2a$", "$2x$" or "$2y$", a two digit cost parameter, "$", and 22 characters - * from the alphabet "./0-9A-Za-z". - * - * @param integer $cost the cost parameter - * @return string the random salt value. - * @throws InvalidParamException if the cost parameter is not between 4 and 30 - */ - protected static function generateSalt($cost = 13) - { - $cost = (int)$cost; - if ($cost < 4 || $cost > 30) { - throw new InvalidParamException('Cost must be between 4 and 31.'); - } - - // Get 20 * 8bits of pseudo-random entropy from mt_rand(). - $rand = ''; - for ($i = 0; $i < 20; ++$i) { - $rand .= chr(mt_rand(0, 255)); - } - - // Add the microtime for a little more entropy. - $rand .= microtime(); - // Mix the bits cryptographically into a 20-byte binary string. - $rand = sha1($rand, true); - // Form the prefix that specifies Blowfish algorithm and cost parameter. - $salt = sprintf("$2y$%02d$", $cost); - // Append the random salt data in the required base64 format. - $salt .= str_replace('+', '.', substr(base64_encode($rand), 0, 22)); - return $salt; - } } \ No newline at end of file diff --git a/framework/helpers/StringHelper.php b/framework/helpers/StringHelper.php index ace34db..22b881a 100644 --- a/framework/helpers/StringHelper.php +++ b/framework/helpers/StringHelper.php @@ -14,112 +14,6 @@ namespace yii\helpers; * @author Alex Makarov * @since 2.0 */ -class StringHelper +class StringHelper extends base\StringHelper { - /** - * Returns the number of bytes in the given string. - * This method ensures the string is treated as a byte array. - * It will use `mb_strlen()` if it is available. - * @param string $string the string being measured for length - * @return integer the number of bytes in the given string. - */ - public static function strlen($string) - { - return function_exists('mb_strlen') ? mb_strlen($string, '8bit') : strlen($string); - } - - /** - * Returns the portion of string specified by the start and length parameters. - * This method ensures the string is treated as a byte array. - * It will use `mb_substr()` if it is available. - * @param string $string the input string. Must be one character or longer. - * @param integer $start the starting position - * @param integer $length the desired portion length - * @return string the extracted part of string, or FALSE on failure or an empty string. - * @see http://www.php.net/manual/en/function.substr.php - */ - public static function substr($string, $start, $length) - { - return function_exists('mb_substr') ? mb_substr($string, $start, $length, '8bit') : substr($string, $start, $length); - } - - /** - * Converts a word to its plural form. - * Note that this is for English only! - * For example, 'apple' will become 'apples', and 'child' will become 'children'. - * @param string $name the word to be pluralized - * @return string the pluralized word - */ - public static function pluralize($name) - { - static $rules = array( - '/(m)ove$/i' => '\1oves', - '/(f)oot$/i' => '\1eet', - '/(c)hild$/i' => '\1hildren', - '/(h)uman$/i' => '\1umans', - '/(m)an$/i' => '\1en', - '/(s)taff$/i' => '\1taff', - '/(t)ooth$/i' => '\1eeth', - '/(p)erson$/i' => '\1eople', - '/([m|l])ouse$/i' => '\1ice', - '/(x|ch|ss|sh|us|as|is|os)$/i' => '\1es', - '/([^aeiouy]|qu)y$/i' => '\1ies', - '/(?:([^f])fe|([lr])f)$/i' => '\1\2ves', - '/(shea|lea|loa|thie)f$/i' => '\1ves', - '/([ti])um$/i' => '\1a', - '/(tomat|potat|ech|her|vet)o$/i' => '\1oes', - '/(bu)s$/i' => '\1ses', - '/(ax|test)is$/i' => '\1es', - '/s$/' => 's', - ); - foreach ($rules as $rule => $replacement) { - if (preg_match($rule, $name)) { - return preg_replace($rule, $replacement, $name); - } - } - return $name . 's'; - } - - /** - * Converts a CamelCase name into space-separated words. - * For example, 'PostTag' will be converted to 'Post Tag'. - * @param string $name the string to be converted - * @param boolean $ucwords whether to capitalize the first letter in each word - * @return string the resulting words - */ - public static function camel2words($name, $ucwords = true) - { - $label = trim(strtolower(str_replace(array('-', '_', '.'), ' ', preg_replace('/(? * @since 2.0 */ -class CVarDumper +class VarDumper extends base\VarDumper { - private static $_objects; - private static $_output; - private static $_depth; - - /** - * Displays a variable. - * This method achieves the similar functionality as var_dump and print_r - * but is more robust when handling complex objects such as Yii controllers. - * @param mixed $var variable to be dumped - * @param integer $depth maximum depth that the dumper should go into the variable. Defaults to 10. - * @param boolean $highlight whether the result should be syntax-highlighted - */ - public static function dump($var, $depth = 10, $highlight = false) - { - echo self::dumpAsString($var, $depth, $highlight); - } - - /** - * Dumps a variable in terms of a string. - * This method achieves the similar functionality as var_dump and print_r - * but is more robust when handling complex objects such as Yii controllers. - * @param mixed $var variable to be dumped - * @param integer $depth maximum depth that the dumper should go into the variable. Defaults to 10. - * @param boolean $highlight whether the result should be syntax-highlighted - * @return string the string representation of the variable - */ - public static function dumpAsString($var, $depth = 10, $highlight = false) - { - self::$_output = ''; - self::$_objects = array(); - self::$_depth = $depth; - self::dumpInternal($var, 0); - if ($highlight) { - $result = highlight_string("/', '', $result, 1); - } - return self::$_output; - } - - /* - * @param mixed $var variable to be dumped - * @param integer $level depth level - */ - private static function dumpInternal($var, $level) - { - switch (gettype($var)) { - case 'boolean': - self::$_output .= $var ? 'true' : 'false'; - break; - case 'integer': - self::$_output .= "$var"; - break; - case 'double': - self::$_output .= "$var"; - break; - case 'string': - self::$_output .= "'" . addslashes($var) . "'"; - break; - case 'resource': - self::$_output .= '{resource}'; - break; - case 'NULL': - self::$_output .= "null"; - break; - case 'unknown type': - self::$_output .= '{unknown}'; - break; - case 'array': - if (self::$_depth <= $level) { - self::$_output .= 'array(...)'; - } elseif (empty($var)) { - self::$_output .= 'array()'; - } else { - $keys = array_keys($var); - $spaces = str_repeat(' ', $level * 4); - self::$_output .= "array\n" . $spaces . '('; - foreach ($keys as $key) { - self::$_output .= "\n" . $spaces . ' '; - self::dumpInternal($key, 0); - self::$_output .= ' => '; - self::dumpInternal($var[$key], $level + 1); - } - self::$_output .= "\n" . $spaces . ')'; - } - break; - case 'object': - if (($id = array_search($var, self::$_objects, true)) !== false) { - self::$_output .= get_class($var) . '#' . ($id + 1) . '(...)'; - } elseif (self::$_depth <= $level) { - self::$_output .= get_class($var) . '(...)'; - } else { - $id = self::$_objects[] = $var; - $className = get_class($var); - $members = (array)$var; - $spaces = str_repeat(' ', $level * 4); - self::$_output .= "$className#$id\n" . $spaces . '('; - foreach ($members as $key => $value) { - $keyDisplay = strtr(trim($key), array("\0" => ':')); - self::$_output .= "\n" . $spaces . " [$keyDisplay] => "; - self::dumpInternal($value, $level + 1); - } - self::$_output .= "\n" . $spaces . ')'; - } - break; - } - } } \ No newline at end of file diff --git a/framework/helpers/base/ArrayHelper.php b/framework/helpers/base/ArrayHelper.php new file mode 100644 index 0000000..9870542 --- /dev/null +++ b/framework/helpers/base/ArrayHelper.php @@ -0,0 +1,340 @@ + + * @since 2.0 + */ +class ArrayHelper +{ + /** + * Merges two or more arrays into one recursively. + * If each array has an element with the same string key value, the latter + * will overwrite the former (different from array_merge_recursive). + * Recursive merging will be conducted if both arrays have an element of array + * type and are having the same key. + * For integer-keyed elements, the elements from the latter array will + * be appended to the former array. + * @param array $a array to be merged to + * @param array $b array to be merged from. You can specify additional + * arrays via third argument, fourth argument etc. + * @return array the merged array (the original arrays are not changed.) + */ + public static function merge($a, $b) + { + $args = func_get_args(); + $res = array_shift($args); + while ($args !== array()) { + $next = array_shift($args); + foreach ($next as $k => $v) { + if (is_integer($k)) { + isset($res[$k]) ? $res[] = $v : $res[$k] = $v; + } elseif (is_array($v) && isset($res[$k]) && is_array($res[$k])) { + $res[$k] = self::merge($res[$k], $v); + } else { + $res[$k] = $v; + } + } + } + return $res; + } + + /** + * Retrieves the value of an array element or object property with the given key or property name. + * If the key does not exist in the array, the default value will be returned instead. + * + * Below are some usage examples, + * + * ~~~ + * // working with array + * $username = \yii\helpers\ArrayHelper::getValue($_POST, 'username'); + * // working with object + * $username = \yii\helpers\ArrayHelper::getValue($user, 'username'); + * // working with anonymous function + * $fullName = \yii\helpers\ArrayHelper::getValue($user, function($user, $defaultValue) { + * return $user->firstName . ' ' . $user->lastName; + * }); + * ~~~ + * + * @param array|object $array array or object to extract value from + * @param string|\Closure $key key name of the array element, or property name of the object, + * or an anonymous function returning the value. The anonymous function signature should be: + * `function($array, $defaultValue)`. + * @param mixed $default the default value to be returned if the specified key does not exist + * @return mixed the value of the + */ + public static function getValue($array, $key, $default = null) + { + if ($key instanceof \Closure) { + return $key($array, $default); + } elseif (is_array($array)) { + return isset($array[$key]) || array_key_exists($key, $array) ? $array[$key] : $default; + } else { + return $array->$key; + } + } + + /** + * Indexes an array according to a specified key. + * The input array should be multidimensional or an array of objects. + * + * The key can be a key name of the sub-array, a property name of object, or an anonymous + * function which returns the key value given an array element. + * + * If a key value is null, the corresponding array element will be discarded and not put in the result. + * + * For example, + * + * ~~~ + * $array = array( + * array('id' => '123', 'data' => 'abc'), + * array('id' => '345', 'data' => 'def'), + * ); + * $result = ArrayHelper::index($array, 'id'); + * // the result is: + * // array( + * // '123' => array('id' => '123', 'data' => 'abc'), + * // '345' => array('id' => '345', 'data' => 'def'), + * // ) + * + * // using anonymous function + * $result = ArrayHelper::index($array, function(element) { + * return $element['id']; + * }); + * ~~~ + * + * @param array $array the array that needs to be indexed + * @param string|\Closure $key the column name or anonymous function whose result will be used to index the array + * @return array the indexed array + */ + public static function index($array, $key) + { + $result = array(); + foreach ($array as $element) { + $value = static::getValue($element, $key); + $result[$value] = $element; + } + return $result; + } + + /** + * Returns the values of a specified column in an array. + * The input array should be multidimensional or an array of objects. + * + * For example, + * + * ~~~ + * $array = array( + * array('id' => '123', 'data' => 'abc'), + * array('id' => '345', 'data' => 'def'), + * ); + * $result = ArrayHelper::getColumn($array, 'id'); + * // the result is: array( '123', '345') + * + * // using anonymous function + * $result = ArrayHelper::getColumn($array, function(element) { + * return $element['id']; + * }); + * ~~~ + * + * @param array $array + * @param string|\Closure $name + * @param boolean $keepKeys whether to maintain the array keys. If false, the resulting array + * will be re-indexed with integers. + * @return array the list of column values + */ + public static function getColumn($array, $name, $keepKeys = true) + { + $result = array(); + if ($keepKeys) { + foreach ($array as $k => $element) { + $result[$k] = static::getValue($element, $name); + } + } else { + foreach ($array as $element) { + $result[] = static::getValue($element, $name); + } + } + + return $result; + } + + /** + * Builds a map (key-value pairs) from a multidimensional array or an array of objects. + * The `$from` and `$to` parameters specify the key names or property names to set up the map. + * Optionally, one can further group the map according to a grouping field `$group`. + * + * For example, + * + * ~~~ + * $array = array( + * array('id' => '123', 'name' => 'aaa', 'class' => 'x'), + * array('id' => '124', 'name' => 'bbb', 'class' => 'x'), + * array('id' => '345', 'name' => 'ccc', 'class' => 'y'), + * ); + * + * $result = ArrayHelper::map($array, 'id', 'name'); + * // the result is: + * // array( + * // '123' => 'aaa', + * // '124' => 'bbb', + * // '345' => 'ccc', + * // ) + * + * $result = ArrayHelper::map($array, 'id', 'name', 'class'); + * // the result is: + * // array( + * // 'x' => array( + * // '123' => 'aaa', + * // '124' => 'bbb', + * // ), + * // 'y' => array( + * // '345' => 'ccc', + * // ), + * // ) + * ~~~ + * + * @param array $array + * @param string|\Closure $from + * @param string|\Closure $to + * @param string|\Closure $group + * @return array + */ + public static function map($array, $from, $to, $group = null) + { + $result = array(); + foreach ($array as $element) { + $key = static::getValue($element, $from); + $value = static::getValue($element, $to); + if ($group !== null) { + $result[static::getValue($element, $group)][$key] = $value; + } else { + $result[$key] = $value; + } + } + return $result; + } + + /** + * Sorts an array of objects or arrays (with the same structure) by one or several keys. + * @param array $array the array to be sorted. The array will be modified after calling this method. + * @param string|\Closure|array $key the key(s) to be sorted by. This refers to a key name of the sub-array + * elements, a property name of the objects, or an anonymous function returning the values for comparison + * purpose. The anonymous function signature should be: `function($item)`. + * To sort by multiple keys, provide an array of keys here. + * @param boolean|array $ascending whether to sort in ascending or descending order. When + * sorting by multiple keys with different ascending orders, use an array of ascending flags. + * @param integer|array $sortFlag the PHP sort flag. Valid values include: + * `SORT_REGULAR`, `SORT_NUMERIC`, `SORT_STRING`, and `SORT_STRING | SORT_FLAG_CASE`. The last + * value is for sorting strings in case-insensitive manner. Please refer to + * See [PHP manual](http://php.net/manual/en/function.sort.php) for more details. + * When sorting by multiple keys with different sort flags, use an array of sort flags. + * @throws InvalidParamException if the $ascending or $sortFlag parameters do not have + * correct number of elements as that of $key. + */ + public static function multisort(&$array, $key, $ascending = true, $sortFlag = SORT_REGULAR) + { + $keys = is_array($key) ? $key : array($key); + if (empty($keys) || empty($array)) { + return; + } + $n = count($keys); + if (is_scalar($ascending)) { + $ascending = array_fill(0, $n, $ascending); + } elseif (count($ascending) !== $n) { + throw new InvalidParamException('The length of $ascending parameter must be the same as that of $keys.'); + } + if (is_scalar($sortFlag)) { + $sortFlag = array_fill(0, $n, $sortFlag); + } elseif (count($sortFlag) !== $n) { + throw new InvalidParamException('The length of $ascending parameter must be the same as that of $keys.'); + } + $args = array(); + foreach ($keys as $i => $key) { + $flag = $sortFlag[$i]; + if ($flag == (SORT_STRING | SORT_FLAG_CASE)) { + $flag = SORT_STRING; + $column = array(); + foreach (static::getColumn($array, $key) as $k => $value) { + $column[$k] = strtolower($value); + } + $args[] = $column; + } else { + $args[] = static::getColumn($array, $key); + } + $args[] = $ascending[$i] ? SORT_ASC : SORT_DESC; + $args[] = $flag; + } + $args[] = &$array; + call_user_func_array('array_multisort', $args); + } + + /** + * Encodes special characters in an array of strings into HTML entities. + * Both the array keys and values will be encoded. + * If a value is an array, this method will also encode it recursively. + * @param array $data data to be encoded + * @param boolean $valuesOnly whether to encode array values only. If false, + * both the array keys and array values will be encoded. + * @param string $charset the charset that the data is using. If not set, + * [[\yii\base\Application::charset]] will be used. + * @return array the encoded data + * @see http://www.php.net/manual/en/function.htmlspecialchars.php + */ + public static function htmlEncode($data, $valuesOnly = true, $charset = null) + { + if ($charset === null) { + $charset = Yii::$app->charset; + } + $d = array(); + foreach ($data as $key => $value) { + if (!$valuesOnly && is_string($key)) { + $key = htmlspecialchars($key, ENT_QUOTES, $charset); + } + if (is_string($value)) { + $d[$key] = htmlspecialchars($value, ENT_QUOTES, $charset); + } elseif (is_array($value)) { + $d[$key] = static::htmlEncode($value, $charset); + } + } + return $d; + } + + /** + * Decodes HTML entities into the corresponding characters in an array of strings. + * Both the array keys and values will be decoded. + * If a value is an array, this method will also decode it recursively. + * @param array $data data to be decoded + * @param boolean $valuesOnly whether to decode array values only. If false, + * both the array keys and array values will be decoded. + * @return array the decoded data + * @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php + */ + public static function htmlDecode($data, $valuesOnly = true) + { + $d = array(); + foreach ($data as $key => $value) { + if (!$valuesOnly && is_string($key)) { + $key = htmlspecialchars_decode($key, ENT_QUOTES); + } + if (is_string($value)) { + $d[$key] = htmlspecialchars_decode($value, ENT_QUOTES); + } elseif (is_array($value)) { + $d[$key] = static::htmlDecode($value); + } + } + return $d; + } +} \ No newline at end of file diff --git a/framework/helpers/base/ConsoleColor.php b/framework/helpers/base/ConsoleColor.php new file mode 100644 index 0000000..5e7f577 --- /dev/null +++ b/framework/helpers/base/ConsoleColor.php @@ -0,0 +1,470 @@ + + * @since 2.0 + */ +class ConsoleColor +{ + const FG_BLACK = 30; + const FG_RED = 31; + const FG_GREEN = 32; + const FG_YELLOW = 33; + const FG_BLUE = 34; + const FG_PURPLE = 35; + const FG_CYAN = 36; + const FG_GREY = 37; + + const BG_BLACK = 40; + const BG_RED = 41; + const BG_GREEN = 42; + const BG_YELLOW = 43; + const BG_BLUE = 44; + const BG_PURPLE = 45; + const BG_CYAN = 46; + const BG_GREY = 47; + + const BOLD = 1; + const ITALIC = 3; + const UNDERLINE = 4; + const BLINK = 5; + const NEGATIVE = 7; + const CONCEALED = 8; + const CROSSED_OUT = 9; + const FRAMED = 51; + const ENCIRCLED = 52; + const OVERLINED = 53; + + /** + * Moves the terminal cursor up by sending ANSI control code CUU to the terminal. + * If the cursor is already at the edge of the screen, this has no effect. + * @param integer $rows number of rows the cursor should be moved up + */ + public static function moveCursorUp($rows=1) + { + echo "\033[" . (int) $rows . 'A'; + } + + /** + * Moves the terminal cursor down by sending ANSI control code CUD to the terminal. + * If the cursor is already at the edge of the screen, this has no effect. + * @param integer $rows number of rows the cursor should be moved down + */ + public static function moveCursorDown($rows=1) + { + echo "\033[" . (int) $rows . 'B'; + } + + /** + * Moves the terminal cursor forward by sending ANSI control code CUF to the terminal. + * If the cursor is already at the edge of the screen, this has no effect. + * @param integer $steps number of steps the cursor should be moved forward + */ + public static function moveCursorForward($steps=1) + { + echo "\033[" . (int) $steps . 'C'; + } + + /** + * Moves the terminal cursor backward by sending ANSI control code CUB to the terminal. + * If the cursor is already at the edge of the screen, this has no effect. + * @param integer $steps number of steps the cursor should be moved backward + */ + public static function moveCursorBackward($steps=1) + { + echo "\033[" . (int) $steps . 'D'; + } + + /** + * Moves the terminal cursor to the beginning of the next line by sending ANSI control code CNL to the terminal. + * @param integer $lines number of lines the cursor should be moved down + */ + public static function moveCursorNextLine($lines=1) + { + echo "\033[" . (int) $lines . 'E'; + } + + /** + * Moves the terminal cursor to the beginning of the previous line by sending ANSI control code CPL to the terminal. + * @param integer $lines number of lines the cursor should be moved up + */ + public static function moveCursorPrevLine($lines=1) + { + echo "\033[" . (int) $lines . 'F'; + } + + /** + * Moves the cursor to an absolute position given as column and row by sending ANSI control code CUP or CHA to the terminal. + * @param integer $column 1-based column number, 1 is the left edge of the screen. + * @param integer|null $row 1-based row number, 1 is the top edge of the screen. if not set, will move cursor only in current line. + */ + public static function moveCursorTo($column, $row=null) + { + if ($row === null) { + echo "\033[" . (int) $column . 'G'; + } else { + echo "\033[" . (int) $row . ';' . (int) $column . 'H'; + } + } + + /** + * Scrolls whole page up by sending ANSI control code SU to the terminal. + * New lines are added at the bottom. This is not supported by ANSI.SYS used in windows. + * @param int $lines number of lines to scroll up + */ + public static function scrollUp($lines=1) + { + echo "\033[".(int)$lines."S"; + } + + /** + * Scrolls whole page down by sending ANSI control code SD to the terminal. + * New lines are added at the top. This is not supported by ANSI.SYS used in windows. + * @param int $lines number of lines to scroll down + */ + public static function scrollDown($lines=1) + { + echo "\033[".(int)$lines."T"; + } + + /** + * Saves the current cursor position by sending ANSI control code SCP to the terminal. + * Position can then be restored with {@link restoreCursorPosition}. + */ + public static function saveCursorPosition() + { + echo "\033[s"; + } + + /** + * Restores the cursor position saved with {@link saveCursorPosition} by sending ANSI control code RCP to the terminal. + */ + public static function restoreCursorPosition() + { + echo "\033[u"; + } + + /** + * Hides the cursor by sending ANSI DECTCEM code ?25l to the terminal. + * Use {@link showCursor} to bring it back. + * Do not forget to show cursor when your application exits. Cursor might stay hidden in terminal after exit. + */ + public static function hideCursor() + { + echo "\033[?25l"; + } + + /** + * Will show a cursor again when it has been hidden by {@link hideCursor} by sending ANSI DECTCEM code ?25h to the terminal. + */ + public static function showCursor() + { + echo "\033[?25h"; + } + + /** + * Clears entire screen content by sending ANSI control code ED with argument 2 to the terminal. + * Cursor position will not be changed. + * **Note:** ANSI.SYS implementation used in windows will reset cursor position to upper left corner of the screen. + */ + public static function clearScreen() + { + echo "\033[2J"; + } + + /** + * Clears text from cursor to the beginning of the screen by sending ANSI control code ED with argument 1 to the terminal. + * Cursor position will not be changed. + */ + public static function clearScreenBeforeCursor() + { + echo "\033[1J"; + } + + /** + * Clears text from cursor to the end of the screen by sending ANSI control code ED with argument 0 to the terminal. + * Cursor position will not be changed. + */ + public static function clearScreenAfterCursor() + { + echo "\033[0J"; + } + + /** + * Clears the line, the cursor is currently on by sending ANSI control code EL with argument 2 to the terminal. + * Cursor position will not be changed. + */ + public static function clearLine() + { + echo "\033[2K"; + } + + /** + * Clears text from cursor position to the beginning of the line by sending ANSI control code EL with argument 1 to the terminal. + * Cursor position will not be changed. + */ + public static function clearLineBeforeCursor() + { + echo "\033[1K"; + } + + /** + * Clears text from cursor position to the end of the line by sending ANSI control code EL with argument 0 to the terminal. + * Cursor position will not be changed. + */ + public static function clearLineAfterCursor() + { + echo "\033[0K"; + } + + /** + * Will send ANSI format for following output + * + * You can pass any of the FG_*, BG_* and TEXT_* constants and also xterm256ColorBg + * TODO: documentation + */ + public static function ansiStyle() + { + echo "\033[" . implode(';', func_get_args()) . 'm'; + } + + /** + * Will return a string formatted with the given ANSI style + * + * See {@link ansiStyle} for possible arguments. + * @param string $string the string to be formatted + * @return string + */ + public static function ansiStyleString($string) + { + $args = func_get_args(); + array_shift($args); + $code = implode(';', $args); + return "\033[0m" . ($code !== '' ? "\033[" . $code . "m" : '') . $string."\033[0m"; + } + + //const COLOR_XTERM256 = 38;// http://en.wikipedia.org/wiki/Talk:ANSI_escape_code#xterm-256colors + public static function xterm256ColorFg($i) // TODO naming! + { + return '38;5;'.$i; + } + + public static function xterm256ColorBg($i) // TODO naming! + { + return '48;5;'.$i; + } + + /** + * Usage: list($w, $h) = ConsoleHelper::getScreenSize(); + * + * @return array + */ + public static function getScreenSize() + { + // TODO implement + return array(150,50); + } + + /** + * resets any ansi style set by previous method {@link ansiStyle} + * Any output after this is will have default text style. + */ + public static function reset() + { + echo "\033[0m"; + } + + /** + * Strips ANSI control codes from a string + * + * @param string $string String to strip + * @return string + */ + public static function strip($string) + { + return preg_replace('/\033\[[\d;]+m/', '', $string); // TODO currently only strips color + } + + // TODO refactor and review + public static function ansiToHtml($string) + { + $tags = 0; + return preg_replace_callback('/\033\[[\d;]+m/', function($ansi) use (&$tags) { + $styleA = array(); + foreach(explode(';', $ansi) as $controlCode) + { + switch($controlCode) + { + case static::FG_BLACK: $style = array('color' => '#000000'); break; + case static::FG_BLUE: $style = array('color' => '#000078'); break; + case static::FG_CYAN: $style = array('color' => '#007878'); break; + case static::FG_GREEN: $style = array('color' => '#007800'); break; + case static::FG_GREY: $style = array('color' => '#787878'); break; + case static::FG_PURPLE: $style = array('color' => '#780078'); break; + case static::FG_RED: $style = array('color' => '#780000'); break; + case static::FG_YELLOW: $style = array('color' => '#787800'); break; + case static::BG_BLACK: $style = array('background-color' => '#000000'); break; + case static::BG_BLUE: $style = array('background-color' => '#000078'); break; + case static::BG_CYAN: $style = array('background-color' => '#007878'); break; + case static::BG_GREEN: $style = array('background-color' => '#007800'); break; + case static::BG_GREY: $style = array('background-color' => '#787878'); break; + case static::BG_PURPLE: $style = array('background-color' => '#780078'); break; + case static::BG_RED: $style = array('background-color' => '#780000'); break; + case static::BG_YELLOW: $style = array('background-color' => '#787800'); break; + case static::BOLD: $style = array('font-weight' => 'bold'); break; + case static::ITALIC: $style = array('font-style' => 'italic'); break; + case static::UNDERLINE: $style = array('text-decoration' => array('underline')); break; + case static::OVERLINED: $style = array('text-decoration' => array('overline')); break; + case static::CROSSED_OUT:$style = array('text-decoration' => array('line-through')); break; + case static::BLINK: $style = array('text-decoration' => array('blink')); break; + case static::NEGATIVE: // ??? + case static::CONCEALED: + case static::ENCIRCLED: + case static::FRAMED: + // TODO allow resetting codes + break; + case 0: // ansi reset + $return = ''; + for($n=$tags; $tags>0; $tags--) { + $return .= ''; + } + return $return; + } + + $styleA = ArrayHelper::merge($styleA, $style); + } + $styleString[] = array(); + foreach($styleA as $name => $content) { + if ($name === 'text-decoration') { + $content = implode(' ', $content); + } + $styleString[] = $name.':'.$content; + } + $tags++; + return 'language; + } + if ($sourceLanguage === null) { + $sourceLanguage = Yii::$app->sourceLanguage; + } + if ($language === $sourceLanguage) { + return $file; + } + $desiredFile = dirname($file) . DIRECTORY_SEPARATOR . $sourceLanguage . DIRECTORY_SEPARATOR . basename($file); + return is_file($desiredFile) ? $desiredFile : $file; + } + + /** + * Determines the MIME type of the specified file. + * This method will first try to determine the MIME type based on + * [finfo_open](http://php.net/manual/en/function.finfo-open.php). If this doesn't work, it will + * fall back to [[getMimeTypeByExtension()]]. + * @param string $file the file name. + * @param string $magicFile name of the optional magic database file, usually something like `/path/to/magic.mime`. + * This will be passed as the second parameter to [finfo_open](http://php.net/manual/en/function.finfo-open.php). + * @param boolean $checkExtension whether to use the file extension to determine the MIME type in case + * `finfo_open()` cannot determine it. + * @return string the MIME type (e.g. `text/plain`). Null is returned if the MIME type cannot be determined. + */ + public static function getMimeType($file, $magicFile = null, $checkExtension = true) + { + if (function_exists('finfo_open')) { + $info = finfo_open(FILEINFO_MIME_TYPE, $magicFile); + if ($info) { + $result = finfo_file($info, $file); + finfo_close($info); + if ($result !== false) { + return $result; + } + } + } + + return $checkExtension ? self::getMimeTypeByExtension($file) : null; + } + + /** + * Determines the MIME type based on the extension name of the specified file. + * This method will use a local map between extension names and MIME types. + * @param string $file the file name. + * @param string $magicFile the path of the file that contains all available MIME type information. + * If this is not set, the default file aliased by `@yii/util/mimeTypes.php` will be used. + * @return string the MIME type. Null is returned if the MIME type cannot be determined. + */ + public static function getMimeTypeByExtension($file, $magicFile = null) + { + static $mimeTypes = array(); + if ($magicFile === null) { + $magicFile = __DIR__ . '/mimeTypes.php'; + } + if (!isset($mimeTypes[$magicFile])) { + $mimeTypes[$magicFile] = require($magicFile); + } + if (($ext = pathinfo($file, PATHINFO_EXTENSION)) !== '') { + $ext = strtolower($ext); + if (isset($mimeTypes[$magicFile][$ext])) { + return $mimeTypes[$magicFile][$ext]; + } + } + return null; + } + + /** + * Copies a whole directory as another one. + * The files and sub-directories will also be copied over. + * @param string $src the source directory + * @param string $dst the destination directory + * @param array $options options for directory copy. Valid options are: + * + * - 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. + * 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`. + */ + public static function copyDirectory($src, $dst, $options = array()) + { + if (!is_dir($dst)) { + mkdir($dst, isset($options['dirMode']) ? $options['dirMode'] : 0777, true); + } + + $handle = opendir($src); + while (($file = readdir($handle)) !== false) { + if ($file === '.' || $file === '..') { + continue; + } + $from = $src . DIRECTORY_SEPARATOR . $file; + $to = $dst . DIRECTORY_SEPARATOR . $file; + if (!isset($options['beforeCopy']) || call_user_func($options['beforeCopy'], $from, $to)) { + if (is_file($from)) { + copy($from, $to); + if (isset($options['fileMode'])) { + @chmod($to, $options['fileMode']); + } + } else { + static::copyDirectory($from, $to, $options); + } + if (isset($options['afterCopy'])) { + call_user_func($options['afterCopy'], $from, $to); + } + } + } + closedir($handle); + } +} \ No newline at end of file diff --git a/framework/helpers/base/Html.php b/framework/helpers/base/Html.php new file mode 100644 index 0000000..bb1fed3 --- /dev/null +++ b/framework/helpers/base/Html.php @@ -0,0 +1,981 @@ + + * @since 2.0 + */ +class Html +{ + /** + * @var boolean whether to close void (empty) elements. Defaults to true. + * @see voidElements + */ + public static $closeVoidElements = true; + /** + * @var array list of void elements (element name => 1) + * @see closeVoidElements + * @see http://www.w3.org/TR/html-markup/syntax.html#void-element + */ + public static $voidElements = array( + 'area' => 1, + 'base' => 1, + 'br' => 1, + 'col' => 1, + 'command' => 1, + 'embed' => 1, + 'hr' => 1, + 'img' => 1, + 'input' => 1, + 'keygen' => 1, + 'link' => 1, + 'meta' => 1, + 'param' => 1, + 'source' => 1, + 'track' => 1, + 'wbr' => 1, + ); + /** + * @var boolean whether to show the values of boolean attributes in element tags. + * If false, only the attribute names will be generated. + * @see booleanAttributes + */ + public static $showBooleanAttributeValues = true; + /** + * @var array list of boolean attributes. The presence of a boolean attribute on + * an element represents the true value, and the absence of the attribute represents the false value. + * @see showBooleanAttributeValues + * @see http://www.w3.org/TR/html5/infrastructure.html#boolean-attributes + */ + public static $booleanAttributes = array( + 'async' => 1, + 'autofocus' => 1, + 'autoplay' => 1, + 'checked' => 1, + 'controls' => 1, + 'declare' => 1, + 'default' => 1, + 'defer' => 1, + 'disabled' => 1, + 'formnovalidate' => 1, + 'hidden' => 1, + 'ismap' => 1, + 'loop' => 1, + 'multiple' => 1, + 'muted' => 1, + 'nohref' => 1, + 'noresize' => 1, + 'novalidate' => 1, + 'open' => 1, + 'readonly' => 1, + 'required' => 1, + 'reversed' => 1, + 'scoped' => 1, + 'seamless' => 1, + 'selected' => 1, + 'typemustmatch' => 1, + ); + /** + * @var array the preferred order of attributes in a tag. This mainly affects the order of the attributes + * that are rendered by [[renderAttributes()]]. + */ + public static $attributeOrder = array( + 'type', + 'id', + 'class', + 'name', + 'value', + + 'href', + 'src', + 'action', + 'method', + + 'selected', + 'checked', + 'readonly', + 'disabled', + 'multiple', + + 'size', + 'maxlength', + 'width', + 'height', + 'rows', + 'cols', + + 'alt', + 'title', + 'rel', + 'media', + ); + + /** + * Encodes special characters into HTML entities. + * The [[yii\base\Application::charset|application charset]] will be used for encoding. + * @param string $content the content to be encoded + * @return string the encoded content + * @see decode + * @see http://www.php.net/manual/en/function.htmlspecialchars.php + */ + public static function encode($content) + { + return htmlspecialchars($content, ENT_QUOTES, Yii::$app->charset); + } + + /** + * Decodes special HTML entities back to the corresponding characters. + * This is the opposite of [[encode()]]. + * @param string $content the content to be decoded + * @return string the decoded content + * @see encode + * @see http://www.php.net/manual/en/function.htmlspecialchars-decode.php + */ + public static function decode($content) + { + return htmlspecialchars_decode($content, ENT_QUOTES); + } + + /** + * Generates a complete HTML tag. + * @param string $name the tag name + * @param string $content the content to be enclosed between the start and end tags. It will not be HTML-encoded. + * If this is coming from end users, you should consider [[encode()]] it to prevent XSS attacks. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated HTML tag + * @see beginTag + * @see endTag + */ + public static function tag($name, $content = '', $options = array()) + { + $html = '<' . $name . static::renderTagAttributes($options); + if (isset(static::$voidElements[strtolower($name)])) { + return $html . (static::$closeVoidElements ? ' />' : '>'); + } else { + return $html . ">$content"; + } + } + + /** + * Generates a start tag. + * @param string $name the tag name + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated start tag + * @see endTag + * @see tag + */ + public static function beginTag($name, $options = array()) + { + return '<' . $name . static::renderTagAttributes($options) . '>'; + } + + /** + * Generates an end tag. + * @param string $name the tag name + * @return string the generated end tag + * @see beginTag + * @see tag + */ + public static function endTag($name) + { + return ""; + } + + /** + * Encloses the given content within a CDATA tag. + * @param string $content the content to be enclosed within the CDATA tag + * @return string the CDATA tag with the enclosed content. + */ + public static function cdata($content) + { + return ''; + } + + /** + * Generates a style tag. + * @param string $content the style content + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * If the options does not contain "type", a "type" attribute with value "text/css" will be used. + * @return string the generated style tag + */ + public static function style($content, $options = array()) + { + if (!isset($options['type'])) { + $options['type'] = 'text/css'; + } + return static::tag('style', "/**/", $options); + } + + /** + * Generates a script tag. + * @param string $content the script content + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * If the options does not contain "type", a "type" attribute with value "text/javascript" will be rendered. + * @return string the generated script tag + */ + public static function script($content, $options = array()) + { + if (!isset($options['type'])) { + $options['type'] = 'text/javascript'; + } + return static::tag('script', "/**/", $options); + } + + /** + * Generates a link tag that refers to an external CSS file. + * @param array|string $url the URL of the external CSS file. This parameter will be processed by [[url()]]. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated link tag + * @see url + */ + public static function cssFile($url, $options = array()) + { + $options['rel'] = 'stylesheet'; + $options['type'] = 'text/css'; + $options['href'] = static::url($url); + return static::tag('link', '', $options); + } + + /** + * Generates a script tag that refers to an external JavaScript file. + * @param string $url the URL of the external JavaScript file. This parameter will be processed by [[url()]]. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated script tag + * @see url + */ + public static function jsFile($url, $options = array()) + { + $options['type'] = 'text/javascript'; + $options['src'] = static::url($url); + return static::tag('script', '', $options); + } + + /** + * Generates a form start tag. + * @param array|string $action the form action URL. This parameter will be processed by [[url()]]. + * @param string $method the form submission method, either "post" or "get" (case-insensitive) + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated form start tag. + * @see endForm + */ + public static function beginForm($action = '', $method = 'post', $options = array()) + { + $action = static::url($action); + + // query parameters in the action are ignored for GET method + // we use hidden fields to add them back + $hiddens = array(); + if (!strcasecmp($method, 'get') && ($pos = strpos($action, '?')) !== false) { + foreach (explode('&', substr($action, $pos + 1)) as $pair) { + if (($pos1 = strpos($pair, '=')) !== false) { + $hiddens[] = static::hiddenInput(urldecode(substr($pair, 0, $pos1)), urldecode(substr($pair, $pos1 + 1))); + } else { + $hiddens[] = static::hiddenInput(urldecode($pair), ''); + } + } + $action = substr($action, 0, $pos); + } + + $options['action'] = $action; + $options['method'] = $method; + $form = static::beginTag('form', $options); + if ($hiddens !== array()) { + $form .= "\n" . implode("\n", $hiddens); + } + + return $form; + } + + /** + * Generates a form end tag. + * @return string the generated tag + * @see beginForm + */ + public static function endForm() + { + return ''; + } + + /** + * Generates a hyperlink tag. + * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code + * such as an image tag. If this is is coming from end users, you should consider [[encode()]] + * it to prevent XSS attacks. + * @param array|string|null $url the URL for the hyperlink tag. This parameter will be processed by [[url()]] + * and will be used for the "href" attribute of the tag. If this parameter is null, the "href" attribute + * will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated hyperlink + * @see url + */ + public static function a($text, $url = null, $options = array()) + { + if ($url !== null) { + $options['href'] = static::url($url); + } + return static::tag('a', $text, $options); + } + + /** + * Generates a mailto hyperlink. + * @param string $text link body. It will NOT be HTML-encoded. Therefore you can pass in HTML code + * such as an image tag. If this is is coming from end users, you should consider [[encode()]] + * it to prevent XSS attacks. + * @param string $email email address. If this is null, the first parameter (link body) will be treated + * as the email address and used. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated mailto link + */ + public static function mailto($text, $email = null, $options = array()) + { + return static::a($text, 'mailto:' . ($email === null ? $text : $email), $options); + } + + /** + * Generates an image tag. + * @param string $src the image URL. This parameter will be processed by [[url()]]. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated image tag + */ + public static function img($src, $options = array()) + { + $options['src'] = static::url($src); + if (!isset($options['alt'])) { + $options['alt'] = ''; + } + return static::tag('img', null, $options); + } + + /** + * Generates a label tag. + * @param string $content label text. It will NOT be HTML-encoded. Therefore you can pass in HTML code + * such as an image tag. If this is is coming from end users, you should consider [[encode()]] + * it to prevent XSS attacks. + * @param string $for the ID of the HTML element that this label is associated with. + * If this is null, the "for" attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated label tag + */ + public static function label($content, $for = null, $options = array()) + { + $options['for'] = $for; + return static::tag('label', $content, $options); + } + + /** + * Generates a button tag. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. + * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, + * you should consider [[encode()]] it to prevent XSS attacks. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * If the options does not contain "type", a "type" attribute with value "button" will be rendered. + * @return string the generated button tag + */ + public static function button($name = null, $value = null, $content = 'Button', $options = array()) + { + $options['name'] = $name; + $options['value'] = $value; + if (!isset($options['type'])) { + $options['type'] = 'button'; + } + return static::tag('button', $content, $options); + } + + /** + * Generates a submit button tag. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. + * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, + * you should consider [[encode()]] it to prevent XSS attacks. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated submit button tag + */ + public static function submitButton($name = null, $value = null, $content = 'Submit', $options = array()) + { + $options['type'] = 'submit'; + return static::button($name, $value, $content, $options); + } + + /** + * Generates a reset button tag. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param string $content the content enclosed within the button tag. It will NOT be HTML-encoded. + * Therefore you can pass in HTML code such as an image tag. If this is is coming from end users, + * you should consider [[encode()]] it to prevent XSS attacks. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated reset button tag + */ + public static function resetButton($name = null, $value = null, $content = 'Reset', $options = array()) + { + $options['type'] = 'reset'; + return static::button($name, $value, $content, $options); + } + + /** + * Generates an input type of the given type. + * @param string $type the type attribute. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated input tag + */ + public static function input($type, $name = null, $value = null, $options = array()) + { + $options['type'] = $type; + $options['name'] = $name; + $options['value'] = $value; + return static::tag('input', null, $options); + } + + /** + * Generates an input button. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function buttonInput($name, $value = 'Button', $options = array()) + { + return static::input('button', $name, $value, $options); + } + + /** + * Generates a submit input button. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function submitInput($name = null, $value = 'Submit', $options = array()) + { + return static::input('submit', $name, $value, $options); + } + + /** + * Generates a reset input button. + * @param string $name the name attribute. If it is null, the name attribute will not be generated. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the attributes of the button tag. The values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the tag returned. + * @return string the generated button tag + */ + public static function resetInput($name = null, $value = 'Reset', $options = array()) + { + return static::input('reset', $name, $value, $options); + } + + /** + * Generates a text input field. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function textInput($name, $value = null, $options = array()) + { + return static::input('text', $name, $value, $options); + } + + /** + * Generates a hidden input field. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function hiddenInput($name, $value = null, $options = array()) + { + return static::input('hidden', $name, $value, $options); + } + + /** + * Generates a password input field. + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function passwordInput($name, $value = null, $options = array()) + { + return static::input('password', $name, $value, $options); + } + + /** + * Generates a file input field. + * To use a file input field, you should set the enclosing form's "enctype" attribute to + * be "multipart/form-data". After the form is submitted, the uploaded file information + * can be obtained via $_FILES[$name] (see PHP documentation). + * @param string $name the name attribute. + * @param string $value the value attribute. If it is null, the value attribute will not be generated. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated button tag + */ + public static function fileInput($name, $value = null, $options = array()) + { + return static::input('file', $name, $value, $options); + } + + /** + * Generates a text area input. + * @param string $name the input name + * @param string $value the input value. Note that it will be encoded using [[encode()]]. + * @param array $options the tag options in terms of name-value pairs. These will be rendered as + * the attributes of the resulting tag. The values will be HTML-encoded using [[encode()]]. + * If a value is null, the corresponding attribute will not be rendered. + * @return string the generated text area tag + */ + public static function textarea($name, $value = '', $options = array()) + { + $options['name'] = $name; + return static::tag('textarea', static::encode($value), $options); + } + + /** + * Generates a radio button input. + * @param string $name the name attribute. + * @param boolean $checked whether the radio button should be checked. + * @param string $value the value attribute. If it is null, the value attribute will not be rendered. + * @param array $options the tag options in terms of name-value pairs. The following options are supported: + * + * - uncheck: string, the value associated with the uncheck state of the radio button. When this attribute + * is present, a hidden input will be generated so that if the radio button is not checked and is submitted, + * the value of this attribute will still be submitted to the server via the hidden input. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * + * @return string the generated radio button tag + */ + public static function radio($name, $checked = false, $value = '1', $options = array()) + { + $options['checked'] = $checked; + $options['value'] = $value; + if (isset($options['uncheck'])) { + // add a hidden field so that if the radio button is not selected, it still submits a value + $hidden = static::hiddenInput($name, $options['uncheck']); + unset($options['uncheck']); + } else { + $hidden = ''; + } + return $hidden . static::input('radio', $name, $value, $options); + } + + /** + * Generates a checkbox input. + * @param string $name the name attribute. + * @param boolean $checked whether the checkbox should be checked. + * @param string $value the value attribute. If it is null, the value attribute will not be rendered. + * @param array $options the tag options in terms of name-value pairs. The following options are supported: + * + * - uncheck: string, the value associated with the uncheck state of the checkbox. When this attribute + * is present, a hidden input will be generated so that if the checkbox is not checked and is submitted, + * the value of this attribute will still be submitted to the server via the hidden input. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * + * @return string the generated checkbox tag + */ + public static function checkbox($name, $checked = false, $value = '1', $options = array()) + { + $options['checked'] = $checked; + $options['value'] = $value; + if (isset($options['uncheck'])) { + // add a hidden field so that if the checkbox is not selected, it still submits a value + $hidden = static::hiddenInput($name, $options['uncheck']); + unset($options['uncheck']); + } else { + $hidden = ''; + } + return $hidden . static::input('checkbox', $name, $value, $options); + } + + /** + * Generates a drop-down list. + * @param string $name the input name + * @param string $selection the selected value + * @param array $items the option data items. The array keys are option values, and the array values + * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). + * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. + * If you have a list of data models, you may convert them into the format described above using + * [[\yii\helpers\ArrayHelper::map()]]. + * + * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in + * the labels will also be HTML-encoded. + * @param array $options the tag options in terms of name-value pairs. The following options are supported: + * + * - prompt: string, a prompt text to be displayed as the first option; + * - options: array, the attributes for the select option tags. The array keys must be valid option values, + * and the array values are the extra attributes for the corresponding option tags. For example, + * + * ~~~ + * array( + * 'value1' => array('disabled' => true), + * 'value2' => array('label' => 'value 2'), + * ); + * ~~~ + * + * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', + * except that the array keys represent the optgroup labels specified in $items. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * + * @return string the generated drop-down list tag + */ + public static function dropDownList($name, $selection = null, $items = array(), $options = array()) + { + $options['name'] = $name; + $selectOptions = static::renderSelectOptions($selection, $items, $options); + return static::tag('select', "\n" . $selectOptions . "\n", $options); + } + + /** + * Generates a list box. + * @param string $name the input name + * @param string|array $selection the selected value(s) + * @param array $items the option data items. The array keys are option values, and the array values + * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). + * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. + * If you have a list of data models, you may convert them into the format described above using + * [[\yii\helpers\ArrayHelper::map()]]. + * + * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in + * the labels will also be HTML-encoded. + * @param array $options the tag options in terms of name-value pairs. The following options are supported: + * + * - prompt: string, a prompt text to be displayed as the first option; + * - options: array, the attributes for the select option tags. The array keys must be valid option values, + * and the array values are the extra attributes for the corresponding option tags. For example, + * + * ~~~ + * array( + * 'value1' => array('disabled' => true), + * 'value2' => array('label' => 'value 2'), + * ); + * ~~~ + * + * - groups: array, the attributes for the optgroup tags. The structure of this is similar to that of 'options', + * except that the array keys represent the optgroup labels specified in $items. + * - unselect: string, the value that will be submitted when no option is selected. + * When this attribute is set, a hidden field will be generated so that if no option is selected in multiple + * mode, we can still obtain the posted unselect value. + * + * The rest of the options will be rendered as the attributes of the resulting tag. The values will + * be HTML-encoded using [[encode()]]. If a value is null, the corresponding attribute will not be rendered. + * + * @return string the generated list box tag + */ + public static function listBox($name, $selection = null, $items = array(), $options = array()) + { + if (!isset($options['size'])) { + $options['size'] = 4; + } + if (!empty($options['multiple']) && substr($name, -2) !== '[]') { + $name .= '[]'; + } + $options['name'] = $name; + if (isset($options['unselect'])) { + // add a hidden field so that if the list box has no option being selected, it still submits a value + if (substr($name, -2) === '[]') { + $name = substr($name, 0, -2); + } + $hidden = static::hiddenInput($name, $options['unselect']); + unset($options['unselect']); + } else { + $hidden = ''; + } + $selectOptions = static::renderSelectOptions($selection, $items, $options); + return $hidden . static::tag('select', "\n" . $selectOptions . "\n", $options); + } + + /** + * Generates a list of checkboxes. + * A checkbox list allows multiple selection, like [[listBox()]]. + * As a result, the corresponding submitted value is an array. + * @param string $name the name attribute of each checkbox. + * @param string|array $selection the selected value(s). + * @param array $items the data item used to generate the checkboxes. + * The array keys are the labels, while the array values are the corresponding checkbox values. + * Note that the labels will NOT be HTML-encoded, while the values will. + * @param array $options options (name => config) for the checkbox list. The following options are supported: + * + * - unselect: string, the value that should be submitted when none of the checkboxes is selected. + * By setting this option, a hidden input will be generated. + * - separator: string, the HTML code that separates items. + * - item: callable, a callback that can be used to customize the generation of the HTML code + * corresponding to a single item in $items. The signature of this callback must be: + * + * ~~~ + * function ($index, $label, $name, $checked, $value) + * ~~~ + * + * where $index is the zero-based index of the checkbox in the whole list; $label + * is the label for the checkbox; and $name, $value and $checked represent the name, + * value and the checked status of the checkbox input. + * @return string the generated checkbox list + */ + public static function checkboxList($name, $selection = null, $items = array(), $options = array()) + { + if (substr($name, -2) !== '[]') { + $name .= '[]'; + } + + $formatter = isset($options['item']) ? $options['item'] : null; + $lines = array(); + $index = 0; + foreach ($items as $value => $label) { + $checked = $selection !== null && + (!is_array($selection) && !strcmp($value, $selection) + || is_array($selection) && in_array($value, $selection)); + if ($formatter !== null) { + $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); + } else { + $lines[] = static::label(static::checkbox($name, $checked, $value) . ' ' . $label); + } + $index++; + } + + if (isset($options['unselect'])) { + // add a hidden field so that if the list box has no option being selected, it still submits a value + $name2 = substr($name, -2) === '[]' ? substr($name, 0, -2) : $name; + $hidden = static::hiddenInput($name2, $options['unselect']); + } else { + $hidden = ''; + } + $separator = isset($options['separator']) ? $options['separator'] : "\n"; + + return $hidden . implode($separator, $lines); + } + + /** + * Generates a list of radio buttons. + * A radio button list is like a checkbox list, except that it only allows single selection. + * @param string $name the name attribute of each radio button. + * @param string|array $selection the selected value(s). + * @param array $items the data item used to generate the radio buttons. + * The array keys are the labels, while the array values are the corresponding radio button values. + * Note that the labels will NOT be HTML-encoded, while the values will. + * @param array $options options (name => config) for the radio button list. The following options are supported: + * + * - unselect: string, the value that should be submitted when none of the radio buttons is selected. + * By setting this option, a hidden input will be generated. + * - separator: string, the HTML code that separates items. + * - item: callable, a callback that can be used to customize the generation of the HTML code + * corresponding to a single item in $items. The signature of this callback must be: + * + * ~~~ + * function ($index, $label, $name, $checked, $value) + * ~~~ + * + * where $index is the zero-based index of the radio button in the whole list; $label + * is the label for the radio button; and $name, $value and $checked represent the name, + * value and the checked status of the radio button input. + * @return string the generated radio button list + */ + public static function radioList($name, $selection = null, $items = array(), $options = array()) + { + $formatter = isset($options['item']) ? $options['item'] : null; + $lines = array(); + $index = 0; + foreach ($items as $value => $label) { + $checked = $selection !== null && + (!is_array($selection) && !strcmp($value, $selection) + || is_array($selection) && in_array($value, $selection)); + if ($formatter !== null) { + $lines[] = call_user_func($formatter, $index, $label, $name, $checked, $value); + } else { + $lines[] = static::label(static::radio($name, $checked, $value) . ' ' . $label); + } + $index++; + } + + $separator = isset($options['separator']) ? $options['separator'] : "\n"; + if (isset($options['unselect'])) { + // add a hidden field so that if the list box has no option being selected, it still submits a value + $hidden = static::hiddenInput($name, $options['unselect']); + } else { + $hidden = ''; + } + + return $hidden . implode($separator, $lines); + } + + /** + * Renders the option tags that can be used by [[dropDownList()]] and [[listBox()]]. + * @param string|array $selection the selected value(s). This can be either a string for single selection + * or an array for multiple selections. + * @param array $items the option data items. The array keys are option values, and the array values + * are the corresponding option labels. The array can also be nested (i.e. some array values are arrays too). + * For each sub-array, an option group will be generated whose label is the key associated with the sub-array. + * If you have a list of data models, you may convert them into the format described above using + * [[\yii\helpers\ArrayHelper::map()]]. + * + * Note, the values and labels will be automatically HTML-encoded by this method, and the blank spaces in + * the labels will also be HTML-encoded. + * @param array $tagOptions the $options parameter that is passed to the [[dropDownList()]] or [[listBox()]] call. + * This method will take out these elements, if any: "prompt", "options" and "groups". See more details + * in [[dropDownList()]] for the explanation of these elements. + * + * @return string the generated list options + */ + public static function renderSelectOptions($selection, $items, &$tagOptions = array()) + { + $lines = array(); + if (isset($tagOptions['prompt'])) { + $prompt = str_replace(' ', ' ', static::encode($tagOptions['prompt'])); + $lines[] = static::tag('option', $prompt, array('value' => '')); + } + + $options = isset($tagOptions['options']) ? $tagOptions['options'] : array(); + $groups = isset($tagOptions['groups']) ? $tagOptions['groups'] : array(); + unset($tagOptions['prompt'], $tagOptions['options'], $tagOptions['groups']); + + foreach ($items as $key => $value) { + if (is_array($value)) { + $groupAttrs = isset($groups[$key]) ? $groups[$key] : array(); + $groupAttrs['label'] = $key; + $attrs = array('options' => $options, 'groups' => $groups); + $content = static::renderSelectOptions($selection, $value, $attrs); + $lines[] = static::tag('optgroup', "\n" . $content . "\n", $groupAttrs); + } else { + $attrs = isset($options[$key]) ? $options[$key] : array(); + $attrs['value'] = $key; + $attrs['selected'] = $selection !== null && + (!is_array($selection) && !strcmp($key, $selection) + || is_array($selection) && in_array($key, $selection)); + $lines[] = static::tag('option', str_replace(' ', ' ', static::encode($value)), $attrs); + } + } + + return implode("\n", $lines); + } + + /** + * Renders the HTML tag attributes. + * Boolean attributes such as s 'checked', 'disabled', 'readonly', will be handled specially + * according to [[booleanAttributes]] and [[showBooleanAttributeValues]]. + * @param array $attributes attributes to be rendered. The attribute values will be HTML-encoded using [[encode()]]. + * Attributes whose value is null will be ignored and not put in the rendering result. + * @return string the rendering result. If the attributes are not empty, they will be rendered + * into a string with a leading white space (such that it can be directly appended to the tag name + * in a tag. If there is no attribute, an empty string will be returned. + */ + public static function renderTagAttributes($attributes) + { + if (count($attributes) > 1) { + $sorted = array(); + foreach (static::$attributeOrder as $name) { + if (isset($attributes[$name])) { + $sorted[$name] = $attributes[$name]; + } + } + $attributes = array_merge($sorted, $attributes); + } + + $html = ''; + foreach ($attributes as $name => $value) { + if (isset(static::$booleanAttributes[strtolower($name)])) { + if ($value || strcasecmp($name, $value) === 0) { + $html .= static::$showBooleanAttributeValues ? " $name=\"$name\"" : " $name"; + } + } elseif ($value !== null) { + $html .= " $name=\"" . static::encode($value) . '"'; + } + } + return $html; + } + + /** + * Normalizes the input parameter to be a valid URL. + * + * If the input parameter + * + * - is an empty string: the currently requested URL will be returned; + * - is a non-empty string: it will be processed by [[Yii::getAlias()]] and returned; + * - is an array: the first array element is considered a route, while the rest of the name-value + * pairs are treated as the parameters to be used for URL creation using [[\yii\web\Controller::createUrl()]]. + * For example: `array('post/index', 'page' => 2)`, `array('index')`. + * + * @param array|string $url the parameter to be used to generate a valid URL + * @return string the normalized URL + * @throws InvalidParamException if the parameter is invalid. + */ + public static function url($url) + { + if (is_array($url)) { + if (isset($url[0])) { + $route = $url[0]; + $params = array_splice($url, 1); + if (Yii::$app->controller !== null) { + return Yii::$app->controller->createUrl($route, $params); + } else { + return Yii::$app->getUrlManager()->createUrl($route, $params); + } + } else { + throw new InvalidParamException('The array specifying a URL must contain at least one element.'); + } + } elseif ($url === '') { + return Yii::$app->getRequest()->getUrl(); + } else { + return Yii::getAlias($url); + } + } +} diff --git a/framework/helpers/base/SecurityHelper.php b/framework/helpers/base/SecurityHelper.php new file mode 100644 index 0000000..6ba48ba --- /dev/null +++ b/framework/helpers/base/SecurityHelper.php @@ -0,0 +1,272 @@ + + * @author Tom Worster + * @since 2.0 + */ +class SecurityHelper +{ + /** + * Encrypts data. + * @param string $data data to be encrypted. + * @param string $key the encryption secret key + * @return string the encrypted data + * @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized + * @see decrypt() + */ + public static function encrypt($data, $key) + { + $module = static::openCryptModule(); + $key = StringHelper::substr($key, 0, mcrypt_enc_get_key_size($module)); + srand(); + $iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($module), MCRYPT_RAND); + mcrypt_generic_init($module, $key, $iv); + $encrypted = $iv . mcrypt_generic($module, $data); + mcrypt_generic_deinit($module); + mcrypt_module_close($module); + return $encrypted; + } + + /** + * Decrypts data + * @param string $data data to be decrypted. + * @param string $key the decryption secret key + * @return string the decrypted data + * @throws Exception if PHP Mcrypt extension is not loaded or failed to be initialized + * @see encrypt() + */ + public static function decrypt($data, $key) + { + $module = static::openCryptModule(); + $key = StringHelper::substr($key, 0, mcrypt_enc_get_key_size($module)); + $ivSize = mcrypt_enc_get_iv_size($module); + $iv = StringHelper::substr($data, 0, $ivSize); + mcrypt_generic_init($module, $key, $iv); + $decrypted = mdecrypt_generic($module, StringHelper::substr($data, $ivSize, StringHelper::strlen($data))); + mcrypt_generic_deinit($module); + mcrypt_module_close($module); + return rtrim($decrypted, "\0"); + } + + /** + * Prefixes data with a keyed hash value so that it can later be detected if it is tampered. + * @param string $data the data to be protected + * @param string $key the secret key to be used for generating hash + * @param string $algorithm the hashing algorithm (e.g. "md5", "sha1", "sha256", etc.). Call PHP "hash_algos()" + * function to see the supported hashing algorithms on your system. + * @return string the data prefixed with the keyed hash + * @see validateData() + * @see getSecretKey() + */ + public static function hashData($data, $key, $algorithm = 'sha256') + { + return hash_hmac($algorithm, $data, $key) . $data; + } + + /** + * Validates if the given data is tampered. + * @param string $data the data to be validated. The data must be previously + * generated by [[hashData()]]. + * @param string $key the secret key that was previously used to generate the hash for the data in [[hashData()]]. + * @param string $algorithm the hashing algorithm (e.g. "md5", "sha1", "sha256", etc.). Call PHP "hash_algos()" + * function to see the supported hashing algorithms on your system. This must be the same + * as the value passed to [[hashData()]] when generating the hash for the data. + * @return string the real data with the hash stripped off. False if the data is tampered. + * @see hashData() + */ + public static function validateData($data, $key, $algorithm = 'sha256') + { + $hashSize = StringHelper::strlen(hash_hmac($algorithm, 'test', $key)); + $n = StringHelper::strlen($data); + if ($n >= $hashSize) { + $hash = StringHelper::substr($data, 0, $hashSize); + $data2 = StringHelper::substr($data, $hashSize, $n - $hashSize); + return $hash === hash_hmac($algorithm, $data2, $key) ? $data2 : false; + } else { + return false; + } + } + + /** + * Returns a secret key associated with the specified name. + * If the secret key does not exist, a random key will be generated + * and saved in the file "keys.php" under the application's runtime directory + * so that the same secret key can be returned in future requests. + * @param string $name the name that is associated with the secret key + * @param integer $length the length of the key that should be generated if not exists + * @return string the secret key associated with the specified name + */ + public static function getSecretKey($name, $length = 32) + { + static $keys; + $keyFile = Yii::$app->getRuntimePath() . '/keys.php'; + if ($keys === null) { + $keys = is_file($keyFile) ? require($keyFile) : array(); + } + if (!isset($keys[$name])) { + // generate a 32-char random key + $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + $keys[$name] = substr(str_shuffle(str_repeat($chars, 5)), 0, $length); + file_put_contents($keyFile, " 30) { + throw new InvalidParamException('Hash is invalid.'); + } + + $test = crypt($password, $hash); + $n = strlen($test); + if (strlen($test) < 32 || $n !== strlen($hash)) { + return false; + } + + // Use a for-loop to compare two strings to prevent timing attacks. See: + // http://codereview.stackexchange.com/questions/13512 + $check = 0; + for ($i = 0; $i < $n; ++$i) { + $check |= (ord($test[$i]) ^ ord($hash[$i])); + } + + return $check === 0; + } + + /** + * Generates a salt that can be used to generate a password hash. + * + * The PHP [crypt()](http://php.net/manual/en/function.crypt.php) built-in function + * requires, for the Blowfish hash algorithm, a salt string in a specific format: + * "$2a$", "$2x$" or "$2y$", a two digit cost parameter, "$", and 22 characters + * from the alphabet "./0-9A-Za-z". + * + * @param integer $cost the cost parameter + * @return string the random salt value. + * @throws InvalidParamException if the cost parameter is not between 4 and 30 + */ + protected static function generateSalt($cost = 13) + { + $cost = (int)$cost; + if ($cost < 4 || $cost > 30) { + throw new InvalidParamException('Cost must be between 4 and 31.'); + } + + // Get 20 * 8bits of pseudo-random entropy from mt_rand(). + $rand = ''; + for ($i = 0; $i < 20; ++$i) { + $rand .= chr(mt_rand(0, 255)); + } + + // Add the microtime for a little more entropy. + $rand .= microtime(); + // Mix the bits cryptographically into a 20-byte binary string. + $rand = sha1($rand, true); + // Form the prefix that specifies Blowfish algorithm and cost parameter. + $salt = sprintf("$2y$%02d$", $cost); + // Append the random salt data in the required base64 format. + $salt .= str_replace('+', '.', substr(base64_encode($rand), 0, 22)); + return $salt; + } +} \ No newline at end of file diff --git a/framework/helpers/base/StringHelper.php b/framework/helpers/base/StringHelper.php new file mode 100644 index 0000000..cb4b09b --- /dev/null +++ b/framework/helpers/base/StringHelper.php @@ -0,0 +1,125 @@ + + * @author Alex Makarov + * @since 2.0 + */ +class StringHelper +{ + /** + * Returns the number of bytes in the given string. + * This method ensures the string is treated as a byte array. + * It will use `mb_strlen()` if it is available. + * @param string $string the string being measured for length + * @return integer the number of bytes in the given string. + */ + public static function strlen($string) + { + return function_exists('mb_strlen') ? mb_strlen($string, '8bit') : strlen($string); + } + + /** + * Returns the portion of string specified by the start and length parameters. + * This method ensures the string is treated as a byte array. + * It will use `mb_substr()` if it is available. + * @param string $string the input string. Must be one character or longer. + * @param integer $start the starting position + * @param integer $length the desired portion length + * @return string the extracted part of string, or FALSE on failure or an empty string. + * @see http://www.php.net/manual/en/function.substr.php + */ + public static function substr($string, $start, $length) + { + return function_exists('mb_substr') ? mb_substr($string, $start, $length, '8bit') : substr($string, $start, $length); + } + + /** + * Converts a word to its plural form. + * Note that this is for English only! + * For example, 'apple' will become 'apples', and 'child' will become 'children'. + * @param string $name the word to be pluralized + * @return string the pluralized word + */ + public static function pluralize($name) + { + static $rules = array( + '/(m)ove$/i' => '\1oves', + '/(f)oot$/i' => '\1eet', + '/(c)hild$/i' => '\1hildren', + '/(h)uman$/i' => '\1umans', + '/(m)an$/i' => '\1en', + '/(s)taff$/i' => '\1taff', + '/(t)ooth$/i' => '\1eeth', + '/(p)erson$/i' => '\1eople', + '/([m|l])ouse$/i' => '\1ice', + '/(x|ch|ss|sh|us|as|is|os)$/i' => '\1es', + '/([^aeiouy]|qu)y$/i' => '\1ies', + '/(?:([^f])fe|([lr])f)$/i' => '\1\2ves', + '/(shea|lea|loa|thie)f$/i' => '\1ves', + '/([ti])um$/i' => '\1a', + '/(tomat|potat|ech|her|vet)o$/i' => '\1oes', + '/(bu)s$/i' => '\1ses', + '/(ax|test)is$/i' => '\1es', + '/s$/' => 's', + ); + foreach ($rules as $rule => $replacement) { + if (preg_match($rule, $name)) { + return preg_replace($rule, $replacement, $name); + } + } + return $name . 's'; + } + + /** + * Converts a CamelCase name into space-separated words. + * For example, 'PostTag' will be converted to 'Post Tag'. + * @param string $name the string to be converted + * @param boolean $ucwords whether to capitalize the first letter in each word + * @return string the resulting words + */ + public static function camel2words($name, $ucwords = true) + { + $label = trim(strtolower(str_replace(array('-', '_', '.'), ' ', preg_replace('/(? + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +namespace yii\helpers\base; + +/** + * VarDumper is intended to replace the buggy PHP function var_dump and print_r. + * It can correctly identify the recursively referenced objects in a complex + * object structure. It also has a recursive depth control to avoid indefinite + * recursive display of some peculiar variables. + * + * VarDumper can be used as follows, + * + * ~~~ + * VarDumper::dump($var); + * ~~~ + * + * @author Qiang Xue + * @since 2.0 + */ +class VarDumper +{ + private static $_objects; + private static $_output; + private static $_depth; + + /** + * Displays a variable. + * This method achieves the similar functionality as var_dump and print_r + * but is more robust when handling complex objects such as Yii controllers. + * @param mixed $var variable to be dumped + * @param integer $depth maximum depth that the dumper should go into the variable. Defaults to 10. + * @param boolean $highlight whether the result should be syntax-highlighted + */ + public static function dump($var, $depth = 10, $highlight = false) + { + echo self::dumpAsString($var, $depth, $highlight); + } + + /** + * Dumps a variable in terms of a string. + * This method achieves the similar functionality as var_dump and print_r + * but is more robust when handling complex objects such as Yii controllers. + * @param mixed $var variable to be dumped + * @param integer $depth maximum depth that the dumper should go into the variable. Defaults to 10. + * @param boolean $highlight whether the result should be syntax-highlighted + * @return string the string representation of the variable + */ + public static function dumpAsString($var, $depth = 10, $highlight = false) + { + self::$_output = ''; + self::$_objects = array(); + self::$_depth = $depth; + self::dumpInternal($var, 0); + if ($highlight) { + $result = highlight_string("/', '', $result, 1); + } + return self::$_output; + } + + /** + * @param mixed $var variable to be dumped + * @param integer $level depth level + */ + private static function dumpInternal($var, $level) + { + switch (gettype($var)) { + case 'boolean': + self::$_output .= $var ? 'true' : 'false'; + break; + case 'integer': + self::$_output .= "$var"; + break; + case 'double': + self::$_output .= "$var"; + break; + case 'string': + self::$_output .= "'" . addslashes($var) . "'"; + break; + case 'resource': + self::$_output .= '{resource}'; + break; + case 'NULL': + self::$_output .= "null"; + break; + case 'unknown type': + self::$_output .= '{unknown}'; + break; + case 'array': + if (self::$_depth <= $level) { + self::$_output .= 'array(...)'; + } elseif (empty($var)) { + self::$_output .= 'array()'; + } else { + $keys = array_keys($var); + $spaces = str_repeat(' ', $level * 4); + self::$_output .= "array\n" . $spaces . '('; + foreach ($keys as $key) { + self::$_output .= "\n" . $spaces . ' '; + self::dumpInternal($key, 0); + self::$_output .= ' => '; + self::dumpInternal($var[$key], $level + 1); + } + self::$_output .= "\n" . $spaces . ')'; + } + break; + case 'object': + if (($id = array_search($var, self::$_objects, true)) !== false) { + self::$_output .= get_class($var) . '#' . ($id + 1) . '(...)'; + } elseif (self::$_depth <= $level) { + self::$_output .= get_class($var) . '(...)'; + } else { + $id = self::$_objects[] = $var; + $className = get_class($var); + $members = (array)$var; + $spaces = str_repeat(' ', $level * 4); + self::$_output .= "$className#$id\n" . $spaces . '('; + foreach ($members as $key => $value) { + $keyDisplay = strtr(trim($key), array("\0" => ':')); + self::$_output .= "\n" . $spaces . " [$keyDisplay] => "; + self::dumpInternal($value, $level + 1); + } + self::$_output .= "\n" . $spaces . ')'; + } + break; + } + } +} \ No newline at end of file diff --git a/framework/helpers/mimeTypes.php b/framework/helpers/base/mimeTypes.php similarity index 100% rename from framework/helpers/mimeTypes.php rename to framework/helpers/base/mimeTypes.php diff --git a/framework/i18n/I18N.php b/framework/i18n/I18N.php index 0409da3..8667abc 100644 --- a/framework/i18n/I18N.php +++ b/framework/i18n/I18N.php @@ -1,11 +1,23 @@ + * @since 2.0 + */ class I18N extends Component { /** @@ -13,11 +25,36 @@ class I18N extends Component * categories, and the array values are the corresponding [[MessageSource]] objects or the configurations * for creating the [[MessageSource]] objects. The message categories can contain the wildcard '*' at the end * to match multiple categories with the same prefix. For example, 'app\*' matches both 'app\cat1' and 'app\cat2'. + * + * This property may be modified on the fly by extensions who want to have their own message sources + * registered under their own namespaces. + * + * The category "yii" and "app" are always defined. The former refers to the messages used in the Yii core + * framework code, while the latter refers to the default message category for custom application code. + * By default, both of these categories use [[PhpMessageSource]] and the corresponding message files are + * stored under "@yii/messages" and "@app/messages", respectively. + * + * You may override the configuration of both categories. */ public $translations; + /** + * @var string the path or path alias of the file that contains the plural rules. + * By default, this refers to a file shipped with the Yii distribution. The file is obtained + * by converting from the data file in the CLDR project. + * + * If the default rule file does not contain the expected rules, you may copy and modify it + * for your application, and then configure this property to point to your modified copy. + * + * @see http://www.unicode.org/cldr/charts/supplemental/language_plural_rules.html + */ + public $pluralRuleFile = '@yii/i18n/data/plurals.php'; + /** + * Initializes the component by configuring the default message categories. + */ public function init() { + parent::init(); if (!isset($this->translations['yii'])) { $this->translations['yii'] = array( 'class' => 'yii\i18n\PhpMessageSource', @@ -34,6 +71,16 @@ class I18N extends Component } } + /** + * Translates a message to the specified language. + * If the first parameter in `$params` is a number and it is indexed by 0, appropriate plural rules + * will be applied to the translated message. + * @param string $message the message to be translated. + * @param array $params the parameters that will be used to replace the corresponding placeholders in the message. + * @param string $language the language code (e.g. `en_US`, `en`). If this is null, the current + * [[\yii\base\Application::language|application language]] will be used. + * @return string the translated message. + */ public function translate($message, $params = array(), $language = null) { if ($language === null) { @@ -55,7 +102,7 @@ class I18N extends Component } if (isset($params[0])) { - $message = $this->getPluralForm($message, $params[0], $language); + $message = $this->applyPluralRules($message, $params[0], $language); if (!isset($params['{n}'])) { $params['{n}'] = $params[0]; } @@ -65,6 +112,12 @@ class I18N extends Component return $params === array() ? $message : strtr($message, $params); } + /** + * Returns the message source for the given category. + * @param string $category the category name. + * @return MessageSource the message source for the given category. + * @throws InvalidConfigException if there is no message source available for the specified category. + */ public function getMessageSource($category) { if (isset($this->translations[$category])) { @@ -85,18 +138,21 @@ class I18N extends Component } } - public function getLocale($language) - { - - } - - protected function getPluralForm($message, $number, $language) + /** + * Applies appropriate plural rules to the given message. + * @param string $message the message to be applied with plural rules + * @param mixed $number the number by which plural rules will be applied + * @param string $language the language code that determines which set of plural rules to be applied. + * @return string the message that has applied plural rules + */ + protected function applyPluralRules($message, $number, $language) { if (strpos($message, '|') === false) { return $message; } $chunks = explode('|', $message); - $rules = $this->getLocale($language)->getPluralRules(); + + $rules = $this->getPluralRules($language); foreach ($rules as $i => $rule) { if (isset($chunks[$i]) && $this->evaluate($rule, $number)) { return $chunks[$i]; @@ -106,6 +162,29 @@ class I18N extends Component return isset($chunks[$n]) ? $chunks[$n] : $chunks[0]; } + private $_pluralRules = array(); // language => rule set + + /** + * Returns the plural rules for the given language code. + * @param string $language the language code (e.g. `en_US`, `en`). + * @return array the plural rules + * @throws InvalidParamException if the language code is invalid. + */ + protected function getPluralRules($language) + { + if (isset($this->_pluralRules[$language])) { + return $this->_pluralRules; + } + $allRules = require(Yii::getAlias($this->pluralRuleFile)); + if (isset($allRules[$language])) { + return $this->_pluralRules[$language] = $allRules[$language]; + } elseif (preg_match('/^[a-z]+/', strtolower($language), $matches)) { + return $this->_pluralRules[$language] = isset($allRules[$matches[0]]) ? $allRules[$matches[0]] : array(); + } else { + throw new InvalidParamException("Invalid language code: $language"); + } + } + /** * Evaluates a PHP expression with the given number value. * @param string $expression the PHP expression @@ -114,6 +193,6 @@ class I18N extends Component */ protected function evaluate($expression, $n) { - return @eval("return $expression;"); + return eval("return $expression;"); } } diff --git a/framework/i18n/PhpMessageSource.php b/framework/i18n/PhpMessageSource.php index 6b12353..1ada44a 100644 --- a/framework/i18n/PhpMessageSource.php +++ b/framework/i18n/PhpMessageSource.php @@ -72,7 +72,7 @@ class PhpMessageSource extends MessageSource } return $messages; } else { - Yii::error("The message file for category '$category' does not exist: $messageFile", __CLASS__); + Yii::error("The message file for category '$category' does not exist: $messageFile", __METHOD__); return array(); } } diff --git a/framework/i18n/data/plurals.php b/framework/i18n/data/plurals.php new file mode 100644 index 0000000..52c733b --- /dev/null +++ b/framework/i18n/data/plurals.php @@ -0,0 +1,627 @@ + + array ( + 0 => '$n==0', + 1 => '$n==1', + 2 => '$n==2', + 3 => 'in_array(fmod($n,100),range(3,10))', + 4 => 'in_array(fmod($n,100),range(11,99))', + ), + 'asa' => + array ( + 0 => '$n==1', + ), + 'af' => + array ( + 0 => '$n==1', + ), + 'bem' => + array ( + 0 => '$n==1', + ), + 'bez' => + array ( + 0 => '$n==1', + ), + 'bg' => + array ( + 0 => '$n==1', + ), + 'bn' => + array ( + 0 => '$n==1', + ), + 'brx' => + array ( + 0 => '$n==1', + ), + 'ca' => + array ( + 0 => '$n==1', + ), + 'cgg' => + array ( + 0 => '$n==1', + ), + 'chr' => + array ( + 0 => '$n==1', + ), + 'da' => + array ( + 0 => '$n==1', + ), + 'de' => + array ( + 0 => '$n==1', + ), + 'dv' => + array ( + 0 => '$n==1', + ), + 'ee' => + array ( + 0 => '$n==1', + ), + 'el' => + array ( + 0 => '$n==1', + ), + 'en' => + array ( + 0 => '$n==1', + ), + 'eo' => + array ( + 0 => '$n==1', + ), + 'es' => + array ( + 0 => '$n==1', + ), + 'et' => + array ( + 0 => '$n==1', + ), + 'eu' => + array ( + 0 => '$n==1', + ), + 'fi' => + array ( + 0 => '$n==1', + ), + 'fo' => + array ( + 0 => '$n==1', + ), + 'fur' => + array ( + 0 => '$n==1', + ), + 'fy' => + array ( + 0 => '$n==1', + ), + 'gl' => + array ( + 0 => '$n==1', + ), + 'gsw' => + array ( + 0 => '$n==1', + ), + 'gu' => + array ( + 0 => '$n==1', + ), + 'ha' => + array ( + 0 => '$n==1', + ), + 'haw' => + array ( + 0 => '$n==1', + ), + 'he' => + array ( + 0 => '$n==1', + ), + 'is' => + array ( + 0 => '$n==1', + ), + 'it' => + array ( + 0 => '$n==1', + ), + 'jmc' => + array ( + 0 => '$n==1', + ), + 'kaj' => + array ( + 0 => '$n==1', + ), + 'kcg' => + array ( + 0 => '$n==1', + ), + 'kk' => + array ( + 0 => '$n==1', + ), + 'kl' => + array ( + 0 => '$n==1', + ), + 'ksb' => + array ( + 0 => '$n==1', + ), + 'ku' => + array ( + 0 => '$n==1', + ), + 'lb' => + array ( + 0 => '$n==1', + ), + 'lg' => + array ( + 0 => '$n==1', + ), + 'mas' => + array ( + 0 => '$n==1', + ), + 'ml' => + array ( + 0 => '$n==1', + ), + 'mn' => + array ( + 0 => '$n==1', + ), + 'mr' => + array ( + 0 => '$n==1', + ), + 'nah' => + array ( + 0 => '$n==1', + ), + 'nb' => + array ( + 0 => '$n==1', + ), + 'nd' => + array ( + 0 => '$n==1', + ), + 'ne' => + array ( + 0 => '$n==1', + ), + 'nl' => + array ( + 0 => '$n==1', + ), + 'nn' => + array ( + 0 => '$n==1', + ), + 'no' => + array ( + 0 => '$n==1', + ), + 'nr' => + array ( + 0 => '$n==1', + ), + 'ny' => + array ( + 0 => '$n==1', + ), + 'nyn' => + array ( + 0 => '$n==1', + ), + 'om' => + array ( + 0 => '$n==1', + ), + 'or' => + array ( + 0 => '$n==1', + ), + 'pa' => + array ( + 0 => '$n==1', + ), + 'pap' => + array ( + 0 => '$n==1', + ), + 'ps' => + array ( + 0 => '$n==1', + ), + 'pt' => + array ( + 0 => '$n==1', + ), + 'rof' => + array ( + 0 => '$n==1', + ), + 'rm' => + array ( + 0 => '$n==1', + ), + 'rwk' => + array ( + 0 => '$n==1', + ), + 'saq' => + array ( + 0 => '$n==1', + ), + 'seh' => + array ( + 0 => '$n==1', + ), + 'sn' => + array ( + 0 => '$n==1', + ), + 'so' => + array ( + 0 => '$n==1', + ), + 'sq' => + array ( + 0 => '$n==1', + ), + 'ss' => + array ( + 0 => '$n==1', + ), + 'ssy' => + array ( + 0 => '$n==1', + ), + 'st' => + array ( + 0 => '$n==1', + ), + 'sv' => + array ( + 0 => '$n==1', + ), + 'sw' => + array ( + 0 => '$n==1', + ), + 'syr' => + array ( + 0 => '$n==1', + ), + 'ta' => + array ( + 0 => '$n==1', + ), + 'te' => + array ( + 0 => '$n==1', + ), + 'teo' => + array ( + 0 => '$n==1', + ), + 'tig' => + array ( + 0 => '$n==1', + ), + 'tk' => + array ( + 0 => '$n==1', + ), + 'tn' => + array ( + 0 => '$n==1', + ), + 'ts' => + array ( + 0 => '$n==1', + ), + 'ur' => + array ( + 0 => '$n==1', + ), + 'wae' => + array ( + 0 => '$n==1', + ), + 've' => + array ( + 0 => '$n==1', + ), + 'vun' => + array ( + 0 => '$n==1', + ), + 'xh' => + array ( + 0 => '$n==1', + ), + 'xog' => + array ( + 0 => '$n==1', + ), + 'zu' => + array ( + 0 => '$n==1', + ), + 'ak' => + array ( + 0 => '($n==0||$n==1)', + ), + 'am' => + array ( + 0 => '($n==0||$n==1)', + ), + 'bh' => + array ( + 0 => '($n==0||$n==1)', + ), + 'fil' => + array ( + 0 => '($n==0||$n==1)', + ), + 'tl' => + array ( + 0 => '($n==0||$n==1)', + ), + 'guw' => + array ( + 0 => '($n==0||$n==1)', + ), + 'hi' => + array ( + 0 => '($n==0||$n==1)', + ), + 'ln' => + array ( + 0 => '($n==0||$n==1)', + ), + 'mg' => + array ( + 0 => '($n==0||$n==1)', + ), + 'nso' => + array ( + 0 => '($n==0||$n==1)', + ), + 'ti' => + array ( + 0 => '($n==0||$n==1)', + ), + 'wa' => + array ( + 0 => '($n==0||$n==1)', + ), + 'ff' => + array ( + 0 => '($n>=0&&$n<=2)&&$n!=2', + ), + 'fr' => + array ( + 0 => '($n>=0&&$n<=2)&&$n!=2', + ), + 'kab' => + array ( + 0 => '($n>=0&&$n<=2)&&$n!=2', + ), + 'lv' => + array ( + 0 => '$n==0', + 1 => 'fmod($n,10)==1&&fmod($n,100)!=11', + ), + 'iu' => + array ( + 0 => '$n==1', + 1 => '$n==2', + ), + 'kw' => + array ( + 0 => '$n==1', + 1 => '$n==2', + ), + 'naq' => + array ( + 0 => '$n==1', + 1 => '$n==2', + ), + 'se' => + array ( + 0 => '$n==1', + 1 => '$n==2', + ), + 'sma' => + array ( + 0 => '$n==1', + 1 => '$n==2', + ), + 'smi' => + array ( + 0 => '$n==1', + 1 => '$n==2', + ), + 'smj' => + array ( + 0 => '$n==1', + 1 => '$n==2', + ), + 'smn' => + array ( + 0 => '$n==1', + 1 => '$n==2', + ), + 'sms' => + array ( + 0 => '$n==1', + 1 => '$n==2', + ), + 'ga' => + array ( + 0 => '$n==1', + 1 => '$n==2', + 2 => 'in_array($n,array(3,4,5,6))', + 3 => 'in_array($n,array(7,8,9,10))', + ), + 'ro' => + array ( + 0 => '$n==1', + 1 => '$n==0||$n!=1&&in_array(fmod($n,100),range(1,19))', + ), + 'mo' => + array ( + 0 => '$n==1', + 1 => '$n==0||$n!=1&&in_array(fmod($n,100),range(1,19))', + ), + 'lt' => + array ( + 0 => 'fmod($n,10)==1&&!in_array(fmod($n,100),range(11,19))', + 1 => 'in_array(fmod($n,10),range(2,9))&&!in_array(fmod($n,100),range(11,19))', + ), + 'be' => + array ( + 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', + 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', + 2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))', + ), + 'bs' => + array ( + 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', + 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', + 2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))', + ), + 'hr' => + array ( + 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', + 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', + 2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))', + ), + 'ru' => + array ( + 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', + 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', + 2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))', + ), + 'sh' => + array ( + 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', + 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', + 2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))', + ), + 'sr' => + array ( + 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', + 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', + 2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))', + ), + 'uk' => + array ( + 0 => 'fmod($n,10)==1&&fmod($n,100)!=11', + 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', + 2 => 'fmod($n,10)==0||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(11,12,13,14))', + ), + 'cs' => + array ( + 0 => '$n==1', + 1 => 'in_array($n,array(2,3,4))', + ), + 'sk' => + array ( + 0 => '$n==1', + 1 => 'in_array($n,array(2,3,4))', + ), + 'pl' => + array ( + 0 => '$n==1', + 1 => 'in_array(fmod($n,10),array(2,3,4))&&!in_array(fmod($n,100),array(12,13,14))', + 2 => '$n!=1&&in_array(fmod($n,10),array(0,1))||in_array(fmod($n,10),array(5,6,7,8,9))||in_array(fmod($n,100),array(12,13,14))', + ), + 'sl' => + array ( + 0 => 'fmod($n,100)==1', + 1 => 'fmod($n,100)==2', + 2 => 'in_array(fmod($n,100),array(3,4))', + ), + 'mt' => + array ( + 0 => '$n==1', + 1 => '$n==0||in_array(fmod($n,100),range(2,10))', + 2 => 'in_array(fmod($n,100),range(11,19))', + ), + 'mk' => + array ( + 0 => 'fmod($n,10)==1&&$n!=11', + ), + 'cy' => + array ( + 0 => '$n==0', + 1 => '$n==1', + 2 => '$n==2', + 3 => '$n==3', + 4 => '$n==6', + ), + 'lag' => + array ( + 0 => '$n==0', + 1 => '($n>=0&&$n<=2)&&$n!=0&&$n!=2', + ), + 'shi' => + array ( + 0 => '($n>=0&&$n<=1)', + 1 => 'in_array($n,range(2,10))', + ), + 'br' => + array ( + 0 => 'fmod($n,10)==1&&!in_array(fmod($n,100),array(11,71,91))', + 1 => 'fmod($n,10)==2&&!in_array(fmod($n,100),array(12,72,92))', + 2 => 'in_array(fmod($n,10),array(3,4,9))&&!in_array(fmod($n,100),array_merge(range(10,19),range(70,79),range(90,99)))', + 3 => 'fmod($n,1000000)==0&&$n!=0', + ), + 'ksh' => + array ( + 0 => '$n==0', + 1 => '$n==1', + ), + 'tzm' => + array ( + 0 => '($n==0||$n==1)||in_array($n,range(11,99))', + ), + 'gv' => + array ( + 0 => 'in_array(fmod($n,10),array(1,2))||fmod($n,20)==0', + ), +); \ No newline at end of file diff --git a/framework/i18n/data/plurals.xml b/framework/i18n/data/plurals.xml new file mode 100644 index 0000000..9227dc6 --- /dev/null +++ b/framework/i18n/data/plurals.xml @@ -0,0 +1,109 @@ + + + + + + + + + + n is 0 + n is 1 + n is 2 + n mod 100 in 3..10 + n mod 100 in 11..99 + + + n is 1 + + + n in 0..1 + + + n within 0..2 and n is not 2 + + + n is 0 + n mod 10 is 1 and n mod 100 is not 11 + + + n is 1 + n is 2 + + + n is 1 + n is 2 + n in 3..6 + n in 7..10 + + + n is 1 + n is 0 OR n is not 1 AND n mod 100 in 1..19 + + + n mod 10 is 1 and n mod 100 not in 11..19 + n mod 10 in 2..9 and n mod 100 not in 11..19 + + + n mod 10 is 1 and n mod 100 is not 11 + n mod 10 in 2..4 and n mod 100 not in 12..14 + n mod 10 is 0 or n mod 10 in 5..9 or n mod 100 in 11..14 + + + + n is 1 + n in 2..4 + + + n is 1 + n mod 10 in 2..4 and n mod 100 not in 12..14 + n is not 1 and n mod 10 in 0..1 or n mod 10 in 5..9 or n mod 100 in 12..14 + + + + + n mod 100 is 1 + n mod 100 is 2 + n mod 100 in 3..4 + + + n is 1 + n is 0 or n mod 100 in 2..10 + n mod 100 in 11..19 + + + n mod 10 is 1 and n is not 11 + + + n is 0 + n is 1 + n is 2 + n is 3 + n is 6 + + + n is 0 + n within 0..2 and n is not 0 and n is not 2 + + + n within 0..1 + n in 2..10 + + + n mod 10 is 1 and n mod 100 not in 11,71,91 + n mod 10 is 2 and n mod 100 not in 12,72,92 + n mod 10 in 3..4,9 and n mod 100 not in 10..19,70..79,90..99 + n mod 1000000 is 0 and n is not 0 + + + n is 0 + n is 1 + + + n in 0..1 or n in 11..99 + + + n mod 10 in 1..2 or n mod 20 is 0 + + + diff --git a/framework/logging/DbTarget.php b/framework/logging/DbTarget.php index e4e30ce..ce9d843 100644 --- a/framework/logging/DbTarget.php +++ b/framework/logging/DbTarget.php @@ -78,7 +78,8 @@ class DbTarget extends Target public function export($messages) { $tableName = $this->db->quoteTableName($this->logTable); - $sql = "INSERT INTO $tableName (level, category, log_time, message) VALUES (:level, :category, :log_time, :message)"; + $sql = "INSERT INTO $tableName ([[level]], [[category]], [[log_time]], [[message]]) + VALUES (:level, :category, :log_time, :message)"; $command = $this->db->createCommand($sql); foreach ($messages as $message) { $command->bindValues(array( diff --git a/framework/logging/FileTarget.php b/framework/logging/FileTarget.php index c3f4031..69799cd 100644 --- a/framework/logging/FileTarget.php +++ b/framework/logging/FileTarget.php @@ -6,6 +6,8 @@ */ namespace yii\logging; + +use Yii; use yii\base\InvalidConfigException; /** @@ -23,15 +25,14 @@ use yii\base\InvalidConfigException; class FileTarget extends Target { /** - * @var string log file path or path alias. If not set, it means the 'application.log' file under - * the application runtime directory. Please make sure the directory containing - * the log file is writable by the Web server process. + * @var string log file path or path alias. If not set, it will use the "runtime/logs/app.log" file. + * The directory containing the log files will be automatically created if not existing. */ public $logFile; /** - * @var integer maximum log file size, in kilo-bytes. Defaults to 1024, meaning 1MB. + * @var integer maximum log file size, in kilo-bytes. Defaults to 10240, meaning 10MB. */ - public $maxFileSize = 1024; // in KB + public $maxFileSize = 10240; // in KB /** * @var integer number of log files used for rotation. Defaults to 5. */ @@ -46,13 +47,13 @@ class FileTarget extends Target { parent::init(); if ($this->logFile === null) { - $this->logFile = \Yii::$app->getRuntimePath() . DIRECTORY_SEPARATOR . 'application.log'; + $this->logFile = Yii::$app->getRuntimePath() . '/logs/app.log'; } else { - $this->logFile = \Yii::getAlias($this->logFile); + $this->logFile = Yii::getAlias($this->logFile); } $logPath = dirname($this->logFile); - if (!is_dir($logPath) || !is_writable($logPath)) { - throw new InvalidConfigException("Directory '$logPath' does not exist or is not writable."); + if (!is_dir($logPath)) { + @mkdir($logPath, 0777, true); } if ($this->maxLogFiles < 1) { $this->maxLogFiles = 1; @@ -66,6 +67,7 @@ class FileTarget extends Target * Sends log messages to specified email addresses. * @param array $messages the messages to be exported. See [[Logger::messages]] for the structure * of each message. + * @throws InvalidConfigException if unable to open the log file for writing */ public function export($messages) { @@ -73,7 +75,9 @@ class FileTarget extends Target foreach ($messages as $message) { $text .= $this->formatMessage($message); } - $fp = @fopen($this->logFile, 'a'); + if (($fp = @fopen($this->logFile, 'a')) === false) { + throw new InvalidConfigException("Unable to append to log file: {$this->logFile}"); + } @flock($fp, LOCK_EX); if (@filesize($this->logFile) > $this->maxFileSize * 1024) { $this->rotateFiles(); diff --git a/framework/logging/Target.php b/framework/logging/Target.php index b88e78d..e76e8ac 100644 --- a/framework/logging/Target.php +++ b/framework/logging/Target.php @@ -238,6 +238,7 @@ abstract class Target extends \yii\base\Component if (!is_string($text)) { $text = var_export($text, true); } - return date('Y/m/d H:i:s', $timestamp) . " [$level] [$category] $text\n"; + $ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '127.0.0.1'; + return date('Y/m/d H:i:s', $timestamp) . " [$ip] [$level] [$category] $text\n"; } } diff --git a/framework/validators/BooleanValidator.php b/framework/validators/BooleanValidator.php index 427fa44..6d2c671 100644 --- a/framework/validators/BooleanValidator.php +++ b/framework/validators/BooleanValidator.php @@ -7,6 +7,8 @@ namespace yii\validators; +use Yii; + /** * BooleanValidator checks if the attribute value is a boolean value. * @@ -32,11 +34,17 @@ class BooleanValidator extends Validator * Defaults to false, meaning only the value needs to be matched. */ public $strict = false; + /** - * @var boolean whether the attribute value can be null or empty. Defaults to true, - * meaning that if the attribute is empty, it is considered valid. + * Initializes the validator. */ - public $allowEmpty = true; + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = Yii::t('yii|{attribute} must be either "{true}" or "{false}".'); + } + } /** * Validates the attribute of the object. @@ -47,13 +55,8 @@ class BooleanValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { - return; - } - if (!$this->strict && $value != $this->trueValue && $value != $this->falseValue - || $this->strict && $value !== $this->trueValue && $value !== $this->falseValue) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} must be either {true} or {false}.'); - $this->addError($object, $attribute, $message, array( + if (!$this->validateValue($value)) { + $this->addError($object, $attribute, $this->message, array( '{true}' => $this->trueValue, '{false}' => $this->falseValue, )); @@ -61,6 +64,17 @@ class BooleanValidator extends Validator } /** + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. + */ + public function validateValue($value) + { + return $this->strict && ($value == $this->trueValue || $value == $this->falseValue) + || !$this->strict && ($value === $this->trueValue || $value === $this->falseValue); + } + + /** * Returns the JavaScript needed for performing client-side validation. * @param \yii\base\Model $object the data object being validated * @param string $attribute the name of the attribute to be validated. @@ -68,15 +82,14 @@ class BooleanValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} must be either {true} or {false}.'); - $message = strtr($message, array( + $message = strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, '{true}' => $this->trueValue, '{false}' => $this->falseValue, )); return " -if(" . ($this->allowEmpty ? "$.trim(value)!='' && " : '') . "value!=" . json_encode($this->trueValue) . " && value!=" . json_encode($this->falseValue) . ") { +if(" . ($this->skipOnEmpty ? "$.trim(value)!='' && " : '') . "value!=" . json_encode($this->trueValue) . " && value!=" . json_encode($this->falseValue) . ") { messages.push(" . json_encode($message) . "); } "; diff --git a/framework/validators/CaptchaValidator.php b/framework/validators/CaptchaValidator.php index 3f31f77..ebb0039 100644 --- a/framework/validators/CaptchaValidator.php +++ b/framework/validators/CaptchaValidator.php @@ -7,6 +7,9 @@ namespace yii\validators; +use Yii; +use yii\base\InvalidConfigException; + /** * CaptchaValidator validates that the attribute value is the same as the verification code displayed in the CAPTCHA. * @@ -22,16 +25,21 @@ class CaptchaValidator extends Validator */ public $caseSensitive = false; /** - * @var string the ID of the action that renders the CAPTCHA image. Defaults to 'captcha', - * meaning the `captcha` action declared in the current controller. - * This can also be a route consisting of controller ID and action ID (e.g. 'site/captcha'). + * @var string the route of the controller action that renders the CAPTCHA image. */ - public $captchaAction = 'captcha'; + public $captchaAction = 'site/captcha'; + + /** - * @var boolean whether the attribute value can be null or empty. - * Defaults to false, meaning the attribute is invalid if it is empty. + * Initializes the validator. */ - public $allowEmpty = false; + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = Yii::t('yii|The verification code is incorrect.'); + } + } /** * Validates the attribute of the object. @@ -42,36 +50,39 @@ class CaptchaValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { - return; + if (!$this->validateValue($value)) { + $this->addError($object, $attribute, $this->message); } + } + + /** + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. + */ + public function validateValue($value) + { $captcha = $this->getCaptchaAction(); - if (!$captcha->validate($value, $this->caseSensitive)) { - $message = $this->message !== null ? $this->message : \Yii::t('yii|The verification code is incorrect.'); - $this->addError($object, $attribute, $message); - } + return !is_array($value) && $captcha->validate($value, $this->caseSensitive); } /** * Returns the CAPTCHA action object. - * @return CCaptchaAction the action object + * @throws InvalidConfigException + * @return CaptchaAction the action object */ public function getCaptchaAction() { - if (strpos($this->captchaAction, '/') !== false) { // contains controller or module - $ca = \Yii::$app->createController($this->captchaAction); - if ($ca !== null) { - list($controller, $actionID) = $ca; - $action = $controller->createAction($actionID); + $ca = Yii::$app->createController($this->captchaAction); + if ($ca !== false) { + /** @var \yii\base\Controller $controller */ + list($controller, $actionID) = $ca; + $action = $controller->createAction($actionID); + if ($action !== null) { + return $action; } - } else { - $action = \Yii::$app->getController()->createAction($this->captchaAction); - } - - if ($action === null) { - throw new \yii\base\Exception('Invalid captcha action ID: ' . $this->captchaAction); } - return $action; + throw new InvalidConfigException('Invalid CAPTCHA action ID: ' . $this->captchaAction); } /** @@ -83,8 +94,7 @@ class CaptchaValidator extends Validator public function clientValidateAttribute($object, $attribute) { $captcha = $this->getCaptchaAction(); - $message = $this->message !== null ? $this->message : \Yii::t('yii|The verification code is incorrect.'); - $message = strtr($message, array( + $message = strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, )); @@ -102,7 +112,7 @@ if(h != hash) { } "; - if ($this->allowEmpty) { + if ($this->skipOnEmpty) { $js = " if($.trim(value)!='') { $js diff --git a/framework/validators/CompareValidator.php b/framework/validators/CompareValidator.php index 43f2edf..1df09c4 100644 --- a/framework/validators/CompareValidator.php +++ b/framework/validators/CompareValidator.php @@ -6,6 +6,7 @@ */ namespace yii\validators; + use Yii; use yii\base\InvalidConfigException; @@ -45,23 +46,12 @@ class CompareValidator extends Validator */ public $compareValue; /** - * @var boolean whether the comparison is strict (both value and type must be the same.) - * Defaults to false. - */ - public $strict = false; - /** - * @var boolean whether the attribute value can be null or empty. Defaults to false. - * If this is true, it means the attribute is considered valid when it is empty. - */ - public $allowEmpty = false; - /** - * @var string the operator for comparison. Defaults to '='. - * The followings are valid operators: - * - * - `=` or `==`: validates to see if the two values are equal. If [[strict]] is true, the comparison - * will be done in strict mode (i.e. checking value type as well). - * - `!=`: validates to see if the two values are NOT equal. If [[strict]] is true, the comparison - * will be done in strict mode (i.e. checking value type as well). + * @var string the operator for comparison. The following operators are supported: + * + * - '==': validates to see if the two values are equal. The comparison is done is non-strict mode. + * - '===': validates to see if the two values are equal. The comparison is done is strict mode. + * - '!=': validates to see if the two values are NOT equal. The comparison is done is non-strict mode. + * - '!==': validates to see if the two values are NOT equal. The comparison is done is strict mode. * - `>`: validates to see if the value being validated is greater than the value being compared with. * - `>=`: validates to see if the value being validated is greater than or equal to the value being compared with. * - `<`: validates to see if the value being validated is less than the value being compared with. @@ -69,6 +59,45 @@ class CompareValidator extends Validator */ public $operator = '='; + + /** + * Initializes the validator. + */ + public function init() + { + parent::init(); + if ($this->message === null) { + switch ($this->operator) { + case '==': + $this->message = Yii::t('yii|{attribute} must be repeated exactly.'); + break; + case '===': + $this->message = Yii::t('yii|{attribute} must be repeated exactly.'); + break; + case '!=': + $this->message = Yii::t('yii|{attribute} must not be equal to "{compareValue}".'); + break; + case '!==': + $this->message = Yii::t('yii|{attribute} must not be equal to "{compareValue}".'); + break; + case '>': + $this->message = Yii::t('yii|{attribute} must be greater than "{compareValue}".'); + break; + case '>=': + $this->message = Yii::t('yii|{attribute} must be greater than or equal to "{compareValue}".'); + break; + case '<': + $this->message = Yii::t('yii|{attribute} must be less than "{compareValue}".'); + break; + case '<=': + $this->message = Yii::t('yii|{attribute} must be less than or equal to "{compareValue}".'); + break; + default: + throw new InvalidConfigException("Unknown operator: {$this->operator}"); + } + } + } + /** * Validates the attribute of the object. * If there is any error, the error message is added to the object. @@ -79,7 +108,8 @@ class CompareValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { + if (is_array($value)) { + $this->addError($object, $attribute, Yii::t('yii|{attribute} is invalid.')); return; } if ($this->compareValue !== null) { @@ -91,45 +121,45 @@ class CompareValidator extends Validator } switch ($this->operator) { - case '=': - case '==': - if (($this->strict && $value !== $compareValue) || (!$this->strict && $value != $compareValue)) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be repeated exactly.'); - $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel)); - } - break; - case '!=': - if (($this->strict && $value === $compareValue) || (!$this->strict && $value == $compareValue)) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must not be equal to "{compareValue}".'); - $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); - } - break; - case '>': - if ($value <= $compareValue) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be greater than "{compareValue}".'); - $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); - } - break; - case '>=': - if ($value < $compareValue) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be greater than or equal to "{compareValue}".'); - $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); - } - break; - case '<': - if ($value >= $compareValue) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be less than "{compareValue}".'); - $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); - } - break; - case '<=': - if ($value > $compareValue) { - $message = ($this->message !== null) ? $this->message : Yii::t('yii|{attribute} must be less than or equal to "{compareValue}".'); - $this->addError($object, $attribute, $message, array('{compareAttribute}' => $compareLabel, '{compareValue}' => $compareValue)); - } - break; - default: - throw new InvalidConfigException("Unknown operator: {$this->operator}"); + case '==': $valid = $value == $compareValue; break; + case '===': $valid = $value === $compareValue; break; + case '!=': $valid = $value != $compareValue; break; + case '!==': $valid = $value !== $compareValue; break; + case '>': $valid = $value > $compareValue; break; + case '>=': $valid = $value >= $compareValue; break; + case '<': $valid = $value < $compareValue; break; + case '<=': $valid = $value <= $compareValue; break; + default: $valid = false; break; + } + if (!$valid) { + $this->addError($object, $attribute, $this->message, array( + '{compareAttribute}' => $compareLabel, + '{compareValue}' => $compareValue, + )); + } + } + + /** + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. + * @throws InvalidConfigException if [[compareValue]] is not set. + */ + public function validateValue($value) + { + if ($this->compareValue === null) { + throw new InvalidConfigException('CompareValidator::compareValue must be set.'); + } + + switch ($this->operator) { + case '==': return $value == $this->compareValue; + case '===': return $value === $this->compareValue; + case '!=': return $value != $this->compareValue; + case '!==': return $value !== $this->compareValue; + case '>': return $value > $this->compareValue; + case '>=': return $value >= $this->compareValue; + case '<': return $value < $this->compareValue; + case '<=': return $value <= $this->compareValue; } } @@ -150,57 +180,14 @@ class CompareValidator extends Validator $compareValue = "\$('#" . (CHtml::activeId($object, $compareAttribute)) . "').val()"; $compareLabel = $object->getAttributeLabel($compareAttribute); } - - $message = $this->message; - switch ($this->operator) { - case '=': - case '==': - if ($message === null) { - $message = Yii::t('yii|{attribute} must be repeated exactly.'); - } - $condition = 'value!=' . $compareValue; - break; - case '!=': - if ($message === null) { - $message = Yii::t('yii|{attribute} must not be equal to "{compareValue}".'); - } - $condition = 'value==' . $compareValue; - break; - case '>': - if ($message === null) { - $message = Yii::t('yii|{attribute} must be greater than "{compareValue}".'); - } - $condition = 'value<=' . $compareValue; - break; - case '>=': - if ($message === null) { - $message = Yii::t('yii|{attribute} must be greater than or equal to "{compareValue}".'); - } - $condition = 'value<' . $compareValue; - break; - case '<': - if ($message === null) { - $message = Yii::t('yii|{attribute} must be less than "{compareValue}".'); - } - $condition = 'value>=' . $compareValue; - break; - case '<=': - if ($message === null) { - $message = Yii::t('yii|{attribute} must be less than or equal to "{compareValue}".'); - } - $condition = 'value>' . $compareValue; - break; - default: - throw new InvalidConfigException("Unknown operator: {$this->operator}"); - } - - $message = strtr($message, array( + $condition = "value {$this->operator} $compareValue"; + $message = strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{compareValue}' => $compareLabel, )); return " -if (" . ($this->allowEmpty ? "$.trim(value)!='' && " : '') . $condition . ") { +if (" . ($this->skipOnEmpty ? "$.trim(value)!='' && " : '') . $condition . ") { messages.push(" . json_encode($message) . "); } "; diff --git a/framework/validators/DateValidator.php b/framework/validators/DateValidator.php index 7899c95..7c3b181 100644 --- a/framework/validators/DateValidator.php +++ b/framework/validators/DateValidator.php @@ -7,11 +7,11 @@ namespace yii\validators; +use Yii; +use DateTime; + /** - * DateValidator verifies if the attribute represents a date, time or datetime. - * - * By setting the {@link format} property, one can specify what format the date value - * must be in. If the given date value doesn't follow the format, the attribute is considered as invalid. + * DateValidator verifies if the attribute represents a date, time or datetime in a proper format. * * @author Qiang Xue * @since 2.0 @@ -19,17 +19,11 @@ namespace yii\validators; class DateValidator extends Validator { /** - * @var mixed the format pattern that the date value should follow. - * This can be either a string or an array representing multiple formats. - * Defaults to 'MM/dd/yyyy'. Please see {@link CDateTimeParser} for details - * about how to specify a date format. - */ - public $format = 'MM/dd/yyyy'; - /** - * @var boolean whether the attribute value can be null or empty. Defaults to true, - * meaning that if the attribute is empty, it is considered valid. + * @var string the date format that the value being validated should follow. + * Please refer to [[http://www.php.net/manual/en/datetime.createfromformat.php]] on + * supported formats. */ - public $allowEmpty = true; + public $format = 'Y-m-d'; /** * @var string the name of the attribute to receive the parsing result. * When this property is not null and the validation is successful, the named attribute will @@ -38,6 +32,17 @@ class DateValidator extends Validator public $timestampAttribute; /** + * Initializes the validator. + */ + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = Yii::t('yii|The format of {attribute} is invalid.'); + } + } + + /** * Validates the attribute of the object. * If there is any error, the error message is added to the object. * @param \yii\base\Model $object the object being validated @@ -46,27 +51,26 @@ class DateValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { + if (is_array($value)) { + $this->addError($object, $attribute, $this->message); return; } - - $formats = is_string($this->format) ? array($this->format) : $this->format; - $valid = false; - foreach ($formats as $format) { - $timestamp = CDateTimeParser::parse($value, $format, array('month' => 1, 'day' => 1, 'hour' => 0, 'minute' => 0, 'second' => 0)); - if ($timestamp !== false) { - $valid = true; - if ($this->timestampAttribute !== null) { - $object-> {$this->timestampAttribute} = $timestamp; - } - break; - } + $date = DateTime::createFromFormat($this->format, $value); + if ($date === false) { + $this->addError($object, $attribute, $this->message); + } elseif ($this->timestampAttribute !== false) { + $object->{$this->timestampAttribute} = $date->getTimestamp(); } + } - if (!$valid) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|The format of {attribute} is invalid.'); - $this->addError($object, $attribute, $message); - } + /** + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. + */ + public function validateValue($value) + { + return DateTime::createFromFormat($this->format, $value) !== false; } } diff --git a/framework/validators/DefaultValueValidator.php b/framework/validators/DefaultValueValidator.php index be06768..185dbd4 100644 --- a/framework/validators/DefaultValueValidator.php +++ b/framework/validators/DefaultValueValidator.php @@ -10,12 +10,8 @@ namespace yii\validators; /** * DefaultValueValidator sets the attribute to be the specified default value. * - * By default, when the attribute being validated is [[isEmpty|empty]], the validator - * will assign a default [[value]] to it. However, if [[setOnEmpty]] is false, the validator - * will always assign the default [[value]] to the attribute, no matter it is empty or not. - * * DefaultValueValidator is not really a validator. It is provided mainly to allow - * specifying attribute default values in a dynamic way. + * specifying attribute default values when they are empty. * * @author Qiang Xue * @since 2.0 @@ -27,11 +23,10 @@ class DefaultValueValidator extends Validator */ public $value; /** - * @var boolean whether to set the default [[value]] only when the attribute is [[isEmpty|empty]]. - * Defaults to true. If false, the attribute will always be assigned with the default [[value]], - * no matter it is empty or not. + * @var boolean this property is overwritten to be false so that this validator will + * be applied when the value being validated is empty. */ - public $setOnEmpty = true; + public $skipOnEmpty = false; /** * Validates the attribute of the object. @@ -40,7 +35,7 @@ class DefaultValueValidator extends Validator */ public function validateAttribute($object, $attribute) { - if (!$this->setOnEmpty || $this->isEmpty($object->$attribute)) { + if ($this->isEmpty($object->$attribute)) { $object->$attribute = $this->value; } } diff --git a/framework/validators/EmailValidator.php b/framework/validators/EmailValidator.php index d1d2257..e498975 100644 --- a/framework/validators/EmailValidator.php +++ b/framework/validators/EmailValidator.php @@ -7,6 +7,8 @@ namespace yii\validators; +use Yii; + /** * EmailValidator validates that the attribute value is a valid email address. * @@ -42,11 +44,17 @@ class EmailValidator extends Validator * Defaults to false. */ public $checkPort = false; + /** - * @var boolean whether the attribute value can be null or empty. Defaults to true, - * meaning that if the attribute is empty, it is considered valid. + * Initializes the validator. */ - public $allowEmpty = true; + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = Yii::t('yii|{attribute} is not a valid email address.'); + } + } /** * Validates the attribute of the object. @@ -57,21 +65,15 @@ class EmailValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { - return; - } if (!$this->validateValue($value)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is not a valid email address.'); - $this->addError($object, $attribute, $message); + $this->addError($object, $attribute, $this->message); } } /** - * Validates a static value to see if it is a valid email. - * Note that this method does not respect [[allowEmpty]] property. - * This method is provided so that you can call it directly without going through the model validation rule mechanism. - * @param mixed $value the value to be validated - * @return boolean whether the value is a valid email + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. */ public function validateValue($value) { @@ -98,8 +100,7 @@ class EmailValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is not a valid email address.'); - $message = strtr($message, array( + $message = strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, )); @@ -110,7 +111,7 @@ class EmailValidator extends Validator } return " -if(" . ($this->allowEmpty ? "$.trim(value)!='' && " : '') . $condition . ") { +if(" . ($this->skipOnEmpty ? "$.trim(value)!='' && " : '') . $condition . ") { messages.push(" . json_encode($message) . "); } "; diff --git a/framework/validators/ExistValidator.php b/framework/validators/ExistValidator.php index 8df3e19..7aa434c 100644 --- a/framework/validators/ExistValidator.php +++ b/framework/validators/ExistValidator.php @@ -6,6 +6,8 @@ */ namespace yii\validators; + +use Yii; use yii\base\InvalidConfigException; /** @@ -34,11 +36,18 @@ class ExistValidator extends Validator * @see className */ public $attributeName; + + /** - * @var boolean whether the attribute value can be null or empty. Defaults to true, - * meaning that if the attribute is empty, it is considered valid. + * Initializes the validator. */ - public $allowEmpty = true; + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = Yii::t('yii|{attribute} is invalid.'); + } + } /** * Validates the attribute of the object. @@ -46,29 +55,48 @@ class ExistValidator extends Validator * * @param \yii\db\ActiveRecord $object the object being validated * @param string $attribute the attribute being validated - * @throws InvalidConfigException if table doesn't have column specified */ public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { + + if (is_array($value)) { + $this->addError($object, $attribute, $this->message); return; } /** @var $className \yii\db\ActiveRecord */ - $className = ($this->className === null) ? get_class($object) : \Yii::import($this->className); - $attributeName = ($this->attributeName === null) ? $attribute : $this->attributeName; - $table = $className::getTableSchema(); - if (($column = $table->getColumn($attributeName)) === null) { - throw new InvalidConfigException('Table "' . $table->name . '" does not have a column named "' . $attributeName . '"'); - } - + $className = $this->className === null ? get_class($object) : Yii::import($this->className); + $attributeName = $this->attributeName === null ? $attribute : $this->attributeName; $query = $className::find(); - $query->where(array($column->name => $value)); + $query->where(array($attributeName => $value)); if (!$query->exists()) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} "{value}" is invalid.'); - $this->addError($object, $attribute, $message); + $this->addError($object, $attribute, $this->message); } } + + /** + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. + * @throws InvalidConfigException if either [[className]] or [[attributeName]] is not set. + */ + public function validateValue($value) + { + if (is_array($value)) { + return false; + } + if ($this->className === null) { + throw new InvalidConfigException('The "className" property must be set.'); + } + if ($this->attributeName === null) { + throw new InvalidConfigException('The "attributeName" property must be set.'); + } + /** @var $className \yii\db\ActiveRecord */ + $className = $this->className; + $query = $className::find(); + $query->where(array($this->attributeName => $value)); + return $query->exists(); + } } diff --git a/framework/validators/FileValidator.php b/framework/validators/FileValidator.php index b05ac2a..b3de0b2 100644 --- a/framework/validators/FileValidator.php +++ b/framework/validators/FileValidator.php @@ -7,47 +7,19 @@ namespace yii\validators; +use Yii; +use yii\helpers\FileHelper; +use yii\web\UploadedFile; + /** - * CFileValidator verifies if an attribute is receiving a valid uploaded file. - * - * It uses the model class and attribute name to retrieve the information - * about the uploaded file. It then checks if a file is uploaded successfully, - * if the file size is within the limit and if the file type is allowed. - * - * This validator will attempt to fetch uploaded data if attribute is not - * previously set. Please note that this cannot be done if input is tabular: - *
- *  foreach($models as $i=>$model)
- *     $model->attribute = CUploadedFile::getInstance($model, "[$i]attribute");
- * 
- * Please note that you must use {@link CUploadedFile::getInstances} for multiple - * file uploads. - * - * When using CFileValidator with an active record, the following code is often used: - *
- *  if($model->save())
- *  {
- *     // single upload
- *     $model->attribute->saveAs($path);
- *     // multiple upload
- *     foreach($model->attribute as $file)
- *        $file->saveAs($path);
- *  }
- * 
- * - * You can use {@link CFileValidator} to validate the file attribute. + * FileValidator verifies if an attribute is receiving a valid uploaded file. * * @author Qiang Xue * @since 2.0 */ -class CFileValidator extends Validator +class FileValidator extends Validator { /** - * @var boolean whether the attribute requires a file to be uploaded or not. - * Defaults to false, meaning a file is required to be uploaded. - */ - public $allowEmpty = false; - /** * @var mixed a list of file name extensions that are allowed to be uploaded. * This can be either an array or a string consisting of file extension names * separated by space or comma (e.g. "gif, jpg"). @@ -66,136 +38,179 @@ class CFileValidator extends Validator * Defaults to null, meaning no limit. * Note, the size limit is also affected by 'upload_max_filesize' INI setting * and the 'MAX_FILE_SIZE' hidden field value. - * @see tooLarge + * @see tooBig */ public $maxSize; /** + * @var integer the maximum file count the given attribute can hold. + * It defaults to 1, meaning single file upload. By defining a higher number, + * multiple uploads become possible. + */ + public $maxFiles = 1; + /** + * @var string the error message used when a file is not uploaded correctly. + */ + public $message; + /** + * @var string the error message used when no file is uploaded. + */ + public $uploadRequired; + /** * @var string the error message used when the uploaded file is too large. - * @see maxSize + * You may use the following tokens in the message: + * + * - {attribute}: the attribute name + * - {file}: the uploaded file name + * - {limit}: the maximum size allowed (see [[getSizeLimit()]]) */ - public $tooLarge; + public $tooBig; /** * @var string the error message used when the uploaded file is too small. - * @see minSize + * You may use the following tokens in the message: + * + * - {attribute}: the attribute name + * - {file}: the uploaded file name + * - {limit}: the value of [[minSize]] */ public $tooSmall; /** * @var string the error message used when the uploaded file has an extension name - * that is not listed among {@link extensions}. + * that is not listed in [[extensions]]. You may use the following tokens in the message: + * + * - {attribute}: the attribute name + * - {extensions}: the list of the allowed extensions. */ public $wrongType; /** - * @var integer the maximum file count the given attribute can hold. - * It defaults to 1, meaning single file upload. By defining a higher number, - * multiple uploads become possible. - */ - public $maxFiles = 1; - /** - * @var string the error message used if the count of multiple uploads exceeds - * limit. + * @var string the error message used if the count of multiple uploads exceeds limit. + * You may use the following tokens in the message: + * + * - {attribute}: the attribute name + * - {file}: the uploaded file name + * - {limit}: the value of [[maxFiles]] */ public $tooMany; /** - * Set the attribute and then validates using {@link validateFile}. - * If there is any error, the error message is added to the object. - * @param \yii\base\Model $object the object being validated - * @param string $attribute the attribute being validated + * Initializes the validator. */ - public function validateAttribute($object, $attribute) + public function init() { - if ($this->maxFiles > 1) - { - $files = $object->$attribute; - if (!is_array($files) || !isset($files[0]) || !$files[0] instanceof CUploadedFile) - $files = CUploadedFile::getInstances($object, $attribute); - if (array() === $files) - return $this->emptyAttribute($object, $attribute); - if (count($files) > $this->maxFiles) - { - $message = $this->tooMany !== null ? $this->tooMany : \Yii::t('yii|{attribute} cannot accept more than {limit} files.'); - $this->addError($object, $attribute, $message, array('{attribute}' => $attribute, '{limit}' => $this->maxFiles)); - } else - foreach ($files as $file) - $this->validateFile($object, $attribute, $file); - } else - { - $file = $object->$attribute; - if (!$file instanceof CUploadedFile) - { - $file = CUploadedFile::getInstance($object, $attribute); - if (null === $file) - return $this->emptyAttribute($object, $attribute); - } - $this->validateFile($object, $attribute, $file); + parent::init(); + if ($this->message === null) { + $this->message = Yii::t('yii|File upload failed.'); + } + if ($this->uploadRequired === null) { + $this->uploadRequired = Yii::t('yii|Please upload a file.'); } + if ($this->tooMany === null) { + $this->tooMany = Yii::t('yii|You can upload at most {limit} files.'); + } + if ($this->wrongType === null) { + $this->wrongType = Yii::t('yii|Only files with these extensions are allowed: {extensions}.'); + } + if ($this->tooBig === null) { + $this->tooBig = Yii::t('yii|The file "{file}" is too big. Its size cannot exceed {limit} bytes.'); + } + if ($this->tooSmall === null) { + $this->tooSmall = Yii::t('yii|The file "{file}" is too small. Its size cannot be smaller than {limit} bytes.'); + } + if (!is_array($this->types)) { + $this->types = preg_split('/[\s,]+/', strtolower($this->types), -1, PREG_SPLIT_NO_EMPTY); + } } /** - * Internally validates a file object. + * Validates the attribute. * @param \yii\base\Model $object the object being validated * @param string $attribute the attribute being validated - * @param CUploadedFile $file uploaded file passed to check against a set of rules */ - public function validateFile($object, $attribute, $file) + public function validateAttribute($object, $attribute) { - if (null === $file || ($error = $file->getError()) == UPLOAD_ERR_NO_FILE) - return $this->emptyAttribute($object, $attribute); - elseif ($error == UPLOAD_ERR_INI_SIZE || $error == UPLOAD_ERR_FORM_SIZE || $this->maxSize !== null && $file->getSize() > $this->maxSize) - { - $message = $this->tooLarge !== null ? $this->tooLarge : \Yii::t('yii|The file "{file}" is too large. Its size cannot exceed {limit} bytes.'); - $this->addError($object, $attribute, $message, array('{file}' => $file->getName(), '{limit}' => $this->getSizeLimit())); - } elseif ($error == UPLOAD_ERR_PARTIAL) - throw new CException(\Yii::t('yii|The file "{file}" was only partially uploaded.', array('{file}' => $file->getName()))); - elseif ($error == UPLOAD_ERR_NO_TMP_DIR) - throw new CException(\Yii::t('yii|Missing the temporary folder to store the uploaded file "{file}".', array('{file}' => $file->getName()))); - elseif ($error == UPLOAD_ERR_CANT_WRITE) - throw new CException(\Yii::t('yii|Failed to write the uploaded file "{file}" to disk.', array('{file}' => $file->getName()))); - elseif (defined('UPLOAD_ERR_EXTENSION') && $error == UPLOAD_ERR_EXTENSION) // available for PHP 5.2.0 or above - throw new CException(\Yii::t('yii|File upload was stopped by extension.')); - - if ($this->minSize !== null && $file->getSize() < $this->minSize) - { - $message = $this->tooSmall !== null ? $this->tooSmall : \Yii::t('yii|The file "{file}" is too small. Its size cannot be smaller than {limit} bytes.'); - $this->addError($object, $attribute, $message, array('{file}' => $file->getName(), '{limit}' => $this->minSize)); - } - - if ($this->types !== null) - { - if (is_string($this->types)) - $types = preg_split('/[\s,]+/', strtolower($this->types), -1, PREG_SPLIT_NO_EMPTY); - else - $types = $this->types; - if (!in_array(strtolower($file->getExtensionName()), $types)) - { - $message = $this->wrongType !== null ? $this->wrongType : \Yii::t('yii|The file "{file}" cannot be uploaded. Only files with these extensions are allowed: {extensions}.'); - $this->addError($object, $attribute, $message, array('{file}' => $file->getName(), '{extensions}' => implode(', ', $types))); + if ($this->maxFiles > 1) { + $files = $object->$attribute; + if (!is_array($files)) { + $this->addError($object, $attribute, $this->uploadRequired); + return; + } + foreach ($files as $i => $file) { + if (!$file instanceof UploadedFile || $file->getError() == UPLOAD_ERR_NO_FILE) { + unset($files[$i]); + } + } + $object->$attribute = array_values($files); + if ($files === array()) { + $this->addError($object, $attribute, $this->uploadRequired); + } + if (count($files) > $this->maxFiles) { + $this->addError($object, $attribute, $this->tooMany, array('{attribute}' => $attribute, '{limit}' => $this->maxFiles)); + } else { + foreach ($files as $file) { + $this->validateFile($object, $attribute, $file); + } + } + } else { + $file = $object->$attribute; + if ($file instanceof UploadedFile && $file->getError() != UPLOAD_ERR_NO_FILE) { + $this->validateFile($object, $attribute, $file); + } else { + $this->addError($object, $attribute, $this->uploadRequired); } } } /** - * Raises an error to inform end user about blank attribute. + * Internally validates a file object. * @param \yii\base\Model $object the object being validated * @param string $attribute the attribute being validated + * @param UploadedFile $file uploaded file passed to check against a set of rules */ - public function emptyAttribute($object, $attribute) + protected function validateFile($object, $attribute, $file) { - if (!$this->allowEmpty) - { - $message = $this->message !== null ? $this->message : \Yii::t('yii|{attribute} cannot be blank.'); - $this->addError($object, $attribute, $message); + switch ($file->getError()) { + case UPLOAD_ERR_OK: + if ($this->maxSize !== null && $file->getSize() > $this->maxSize) { + $this->addError($object, $attribute, $this->tooBig, array('{file}' => $file->getName(), '{limit}' => $this->getSizeLimit())); + } + if ($this->minSize !== null && $file->getSize() < $this->minSize) { + $this->addError($object, $attribute, $this->tooSmall, array('{file}' => $file->getName(), '{limit}' => $this->minSize)); + } + if (!empty($this->types) && !in_array(strtolower(pathinfo($file->getName(), PATHINFO_EXTENSION)), $this->types, true)) { + $this->addError($object, $attribute, $this->wrongType, array('{file}' => $file->getName(), '{extensions}' => implode(', ', $this->types))); + } + break; + case UPLOAD_ERR_INI_SIZE: + case UPLOAD_ERR_FORM_SIZE: + $this->addError($object, $attribute, $this->tooBig, array('{file}' => $file->getName(), '{limit}' => $this->getSizeLimit())); + break; + case UPLOAD_ERR_PARTIAL: + $this->addError($object, $attribute, $this->message); + Yii::warning('File was only partially uploaded: ' . $file->getName(), __METHOD__); + break; + case UPLOAD_ERR_NO_TMP_DIR: + $this->addError($object, $attribute, $this->message); + Yii::warning('Missing the temporary folder to store the uploaded file: ' . $file->getName(), __METHOD__); + break; + case UPLOAD_ERR_CANT_WRITE: + $this->addError($object, $attribute, $this->message); + Yii::warning('Failed to write the uploaded file to disk: ', $file->getName(), __METHOD__); + break; + case UPLOAD_ERR_EXTENSION: + $this->addError($object, $attribute, $this->message); + Yii::warning('File upload was stopped by some PHP extension: ', $file->getName(), __METHOD__); + break; + default: + break; } } /** * Returns the maximum size allowed for uploaded files. * This is determined based on three factors: - *
    - *
  • 'upload_max_filesize' in php.ini
  • - *
  • 'MAX_FILE_SIZE' hidden field
  • - *
  • {@link maxSize}
  • - *
+ * + * - 'upload_max_filesize' in php.ini + * - 'MAX_FILE_SIZE' hidden field + * - [[maxSize]] * * @return integer the size limit for uploaded files. */ @@ -203,10 +218,12 @@ class CFileValidator extends Validator { $limit = ini_get('upload_max_filesize'); $limit = $this->sizeToBytes($limit); - if ($this->maxSize !== null && $limit > 0 && $this->maxSize < $limit) + if ($this->maxSize !== null && $limit > 0 && $this->maxSize < $limit) { $limit = $this->maxSize; - if (isset($_POST['MAX_FILE_SIZE']) && $_POST['MAX_FILE_SIZE'] > 0 && $_POST['MAX_FILE_SIZE'] < $limit) - $limit = $_POST['MAX_FILE_SIZE']; + } + if (isset($_POST['MAX_FILE_SIZE']) && $_POST['MAX_FILE_SIZE'] > 0 && $_POST['MAX_FILE_SIZE'] < $limit) { + $limit = (int)$_POST['MAX_FILE_SIZE']; + } return $limit; } @@ -218,12 +235,18 @@ class CFileValidator extends Validator */ private function sizeToBytes($sizeStr) { - switch (substr($sizeStr, -1)) - { - case 'M': case 'm': return (int)$sizeStr * 1048576; - case 'K': case 'k': return (int)$sizeStr * 1024; - case 'G': case 'g': return (int)$sizeStr * 1073741824; - default: return (int)$sizeStr; + switch (substr($sizeStr, -1)) { + case 'M': + case 'm': + return (int)$sizeStr * 1048576; + case 'K': + case 'k': + return (int)$sizeStr * 1024; + case 'G': + case 'g': + return (int)$sizeStr * 1073741824; + default: + return (int)$sizeStr; } } } \ No newline at end of file diff --git a/framework/validators/FilterValidator.php b/framework/validators/FilterValidator.php index c891979..72a9a9d 100644 --- a/framework/validators/FilterValidator.php +++ b/framework/validators/FilterValidator.php @@ -38,6 +38,23 @@ class FilterValidator extends Validator * ~~~ */ public $filter; + /** + * @var boolean this property is overwritten to be false so that this validator will + * be applied when the value being validated is empty. + */ + public $skipOnEmpty = false; + + /** + * Initializes the validator. + * @throws InvalidConfigException if [[filter]] is not set. + */ + public function init() + { + parent::init(); + if ($this->filter === null) { + throw new InvalidConfigException('The "filter" property must be set.'); + } + } /** * Validates the attribute of the object. @@ -48,9 +65,6 @@ class FilterValidator extends Validator */ public function validateAttribute($object, $attribute) { - if ($this->filter === null) { - throw new InvalidConfigException('The "filter" property must be specified with a valid callback.'); - } $object->$attribute = call_user_func($this->filter, $object->$attribute); } } diff --git a/framework/validators/InlineValidator.php b/framework/validators/InlineValidator.php index 5c12d52..3689a2f 100644 --- a/framework/validators/InlineValidator.php +++ b/framework/validators/InlineValidator.php @@ -25,8 +25,9 @@ namespace yii\validators; class InlineValidator extends Validator { /** - * @var string the name of the validation method defined in the - * \yii\base\Model class + * @var string|\Closure an anonymous function or the name of a model class method that will be + * called to perform the actual validation. Note that if you use anonymous function, you cannot + * use `$this` in it unless you are using PHP 5.4 or above. */ public $method; /** @@ -34,8 +35,8 @@ class InlineValidator extends Validator */ public $params; /** - * @var string the name of the method that returns the client validation code (see [[clientValidateAttribute()]] - * for details on how to return client validation code). The signature of the method should be like the following: + * @var string|\Closure an anonymous function or the name of a model class method that returns the client validation code. + * The signature of the method should be like the following: * * ~~~ * function foo($attribute) @@ -45,6 +46,8 @@ class InlineValidator extends Validator * ~~~ * * where `$attribute` refers to the attribute name to be validated. + * + * Please refer to [[clientValidateAttribute()]] for details on how to return client validation code. */ public $clientValidate; @@ -56,7 +59,10 @@ class InlineValidator extends Validator public function validateAttribute($object, $attribute) { $method = $this->method; - $object->$method($attribute, $this->params); + if (is_string($method)) { + $method = array($object, $method); + } + call_user_func($method, $attribute, $this->params); } /** @@ -82,7 +88,10 @@ class InlineValidator extends Validator { if ($this->clientValidate !== null) { $method = $this->clientValidate; - return $object->$method($attribute); + if (is_string($method)) { + $method = array($object, $method); + } + return call_user_func($method, $attribute); } else { return null; } diff --git a/framework/validators/NumberValidator.php b/framework/validators/NumberValidator.php index 89363fb..915419e 100644 --- a/framework/validators/NumberValidator.php +++ b/framework/validators/NumberValidator.php @@ -26,11 +26,6 @@ class NumberValidator extends Validator */ public $integerOnly = false; /** - * @var boolean whether the attribute value can be null or empty. Defaults to true, - * meaning that if the attribute is empty, it is considered valid. - */ - public $allowEmpty = true; - /** * @var integer|float upper limit of the number. Defaults to null, meaning no upper limit. */ public $max; @@ -58,6 +53,24 @@ class NumberValidator extends Validator /** + * Initializes the validator. + */ + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = $this->integerOnly ? Yii::t('yii|{attribute} must be an integer.') + : Yii::t('yii|{attribute} must be a number.'); + } + if ($this->min !== null && $this->tooSmall === null) { + $this->tooSmall = Yii::t('yii|{attribute} must be no less than {min}.'); + } + if ($this->max !== null && $this->tooBig === null) { + $this->tooBig = Yii::t('yii|{attribute} must be no greater than {max}.'); + } + } + + /** * Validates the attribute of the object. * If there is any error, the error message is added to the object. * @param \yii\base\Model $object the object being validated @@ -66,31 +79,35 @@ class NumberValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { + if (is_array($value)) { + $this->addError($object, $attribute, Yii::t('yii|{attribute} is invalid.')); return; } - if ($this->integerOnly) { - if (!preg_match($this->integerPattern, "$value")) { - $message = $this->message !== null ? $this->message : Yii::t('yii|{attribute} must be an integer.'); - $this->addError($object, $attribute, $message); - } - } else { - if (!preg_match($this->numberPattern, "$value")) { - $message = $this->message !== null ? $this->message : Yii::t('yii|{attribute} must be a number.'); - $this->addError($object, $attribute, $message); - } + $pattern = $this->integerOnly ? $this->integerPattern : $this->numberPattern; + if (!preg_match($pattern, "$value")) { + $this->addError($object, $attribute, $this->message); } if ($this->min !== null && $value < $this->min) { - $message = $this->tooSmall !== null ? $this->tooSmall : Yii::t('yii|{attribute} is too small (minimum is {min}).'); - $this->addError($object, $attribute, $message, array('{min}' => $this->min)); + $this->addError($object, $attribute, $this->tooSmall, array('{min}' => $this->min)); } if ($this->max !== null && $value > $this->max) { - $message = $this->tooBig !== null ? $this->tooBig : Yii::t('yii|{attribute} is too big (maximum is {max}).'); - $this->addError($object, $attribute, $message, array('{max}' => $this->max)); + $this->addError($object, $attribute, $this->tooBig, array('{max}' => $this->max)); } } /** + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. + */ + public function validateValue($value) + { + return preg_match($this->integerOnly ? $this->integerPattern : $this->numberPattern, "$value") + && ($this->min === null || $value >= $this->min) + && ($this->max === null || $value <= $this->max); + } + + /** * Returns the JavaScript needed for performing client-side validation. * @param \yii\base\Model $object the data object being validated * @param string $attribute the name of the attribute to be validated. @@ -99,12 +116,7 @@ class NumberValidator extends Validator public function clientValidateAttribute($object, $attribute) { $label = $object->getAttributeLabel($attribute); - - if (($message = $this->message) === null) { - $message = $this->integerOnly ? Yii::t('yii|{attribute} must be an integer.') - : Yii::t('yii|{attribute} must be a number.'); - } - $message = strtr($message, array( + $message = strtr($this->message, array( '{attribute}' => $label, )); @@ -115,10 +127,7 @@ if(!value.match($pattern)) { } "; if ($this->min !== null) { - if (($tooSmall = $this->tooSmall) === null) { - $tooSmall = Yii::t('yii|{attribute} is too small (minimum is {min}).'); - } - $tooSmall = strtr($tooSmall, array( + $tooSmall = strtr($this->tooSmall, array( '{attribute}' => $label, '{min}' => $this->min, )); @@ -130,10 +139,7 @@ if(value<{$this->min}) { "; } if ($this->max !== null) { - if (($tooBig = $this->tooBig) === null) { - $tooBig = Yii::t('yii|{attribute} is too big (maximum is {max}).'); - } - $tooBig = strtr($tooBig, array( + $tooBig = strtr($this->tooBig, array( '{attribute}' => $label, '{max}' => $this->max, )); @@ -144,7 +150,7 @@ if(value>{$this->max}) { "; } - if ($this->allowEmpty) { + if ($this->skipOnEmpty) { $js = " if(jQuery.trim(value)!='') { $js diff --git a/framework/validators/RangeValidator.php b/framework/validators/RangeValidator.php index e23567c..18742ae 100644 --- a/framework/validators/RangeValidator.php +++ b/framework/validators/RangeValidator.php @@ -6,6 +6,8 @@ */ namespace yii\validators; + +use Yii; use yii\base\InvalidConfigException; /** @@ -29,58 +31,62 @@ class RangeValidator extends Validator */ public $strict = false; /** - * @var boolean whether the attribute value can be null or empty. Defaults to true, - * meaning that if the attribute is empty, it is considered valid. - */ - public $allowEmpty = true; - /** * @var boolean whether to invert the validation logic. Defaults to false. If set to true, * the attribute value should NOT be among the list of values defined via [[range]]. **/ public $not = false; /** + * Initializes the validator. + * @throws InvalidConfigException if [[range]] is not set. + */ + public function init() + { + parent::init(); + if (!is_array($this->range)) { + throw new InvalidConfigException('The "range" property must be set.'); + } + if ($this->message === null) { + $this->message = Yii::t('yii|{attribute} is invalid.'); + } + } + + /** * Validates the attribute of the object. * If there is any error, the error message is added to the object. * @param \yii\base\Model $object the object being validated * @param string $attribute the attribute being validated - * @throws InvalidConfigException if the "range" property is not an array */ public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { - return; - } - if (!is_array($this->range)) { - throw new InvalidConfigException('The "range" property must be specified as an array.'); - } if (!$this->not && !in_array($value, $this->range, $this->strict)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} should be in the list.'); - $this->addError($object, $attribute, $message); + $this->addError($object, $attribute, $this->message); } elseif ($this->not && in_array($value, $this->range, $this->strict)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} should NOT be in the list.'); - $this->addError($object, $attribute, $message); + $this->addError($object, $attribute, $this->message); } } /** + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. + */ + public function validateValue($value) + { + return !$this->not && in_array($value, $this->range, $this->strict) + || $this->not && !in_array($value, $this->range, $this->strict); + } + + /** * Returns the JavaScript needed for performing client-side validation. * @param \yii\base\Model $object the data object being validated * @param string $attribute the name of the attribute to be validated. * @return string the client-side validation script. - * @throws InvalidConfigException if the "range" property is not an array */ public function clientValidateAttribute($object, $attribute) { - if (!is_array($this->range)) { - throw new InvalidConfigException('The "range" property must be specified as an array.'); - } - - if (($message = $this->message) === null) { - $message = $this->not ? \Yii::t('yii|{attribute} should NOT be in the list.') : \Yii::t('yii|{attribute} should be in the list.'); - } - $message = strtr($message, array( + $message = strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, )); @@ -92,7 +98,7 @@ class RangeValidator extends Validator $range = json_encode($range); return " -if (" . ($this->allowEmpty ? "$.trim(value)!='' && " : '') . ($this->not ? "$.inArray(value, $range)>=0" : "$.inArray(value, $range)<0") . ") { +if (" . ($this->skipOnEmpty ? "$.trim(value)!='' && " : '') . ($this->not ? "$.inArray(value, $range)>=0" : "$.inArray(value, $range)<0") . ") { messages.push(" . json_encode($message) . "); } "; diff --git a/framework/validators/RegularExpressionValidator.php b/framework/validators/RegularExpressionValidator.php index df2b657..6c69be3 100644 --- a/framework/validators/RegularExpressionValidator.php +++ b/framework/validators/RegularExpressionValidator.php @@ -7,6 +7,9 @@ namespace yii\validators; +use Yii; +use yii\base\InvalidConfigException; + /** * RegularExpressionValidator validates that the attribute value matches the specified [[pattern]]. * @@ -22,53 +25,63 @@ class RegularExpressionValidator extends Validator */ public $pattern; /** - * @var boolean whether the attribute value can be null or empty. Defaults to true, - * meaning that if the attribute is empty, it is considered valid. - */ - public $allowEmpty = true; - /** * @var boolean whether to invert the validation logic. Defaults to false. If set to true, * the regular expression defined via [[pattern]] should NOT match the attribute value. + * @throws InvalidConfigException if the "pattern" is not a valid regular expression **/ public $not = false; /** + * Initializes the validator. + * @throws InvalidConfigException if [[pattern]] is not set. + */ + public function init() + { + parent::init(); + if ($this->pattern === null) { + throw new InvalidConfigException('The "pattern" property must be set.'); + } + if ($this->message === null) { + $this->message = Yii::t('yii|{attribute} is invalid.'); + } + } + + /** * Validates the attribute of the object. * If there is any error, the error message is added to the object. * @param \yii\base\Model $object the object being validated * @param string $attribute the attribute being validated - * @throws \yii\base\Exception if the "pattern" is not a valid regular expression */ public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { - return; - } - if ($this->pattern === null) { - throw new \yii\base\Exception('The "pattern" property must be specified with a valid regular expression.'); - } - if ((!$this->not && !preg_match($this->pattern, $value)) || ($this->not && preg_match($this->pattern, $value))) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is invalid.'); - $this->addError($object, $attribute, $message); + if (!$this->validateValue($value)) { + $this->addError($object, $attribute, $this->message); } } /** + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. + */ + public function validateValue($value) + { + return !is_array($value) && + (!$this->not && preg_match($this->pattern, $value) + || $this->not && !preg_match($this->pattern, $value)); + } + + /** * Returns the JavaScript needed for performing client-side validation. * @param \yii\base\Model $object the data object being validated * @param string $attribute the name of the attribute to be validated. * @return string the client-side validation script. - * @throws \yii\base\Exception if the "pattern" is not a valid regular expression + * @throws InvalidConfigException if the "pattern" is not a valid regular expression */ public function clientValidateAttribute($object, $attribute) { - if ($this->pattern === null) { - throw new \yii\base\Exception('The "pattern" property must be specified with a valid regular expression.'); - } - - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is invalid.'); - $message = strtr($message, array( + $message = strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, )); @@ -88,7 +101,7 @@ class RegularExpressionValidator extends Validator } return " -if (" . ($this->allowEmpty ? "$.trim(value)!='' && " : '') . ($this->not ? '' : '!') . "value.match($pattern)) { +if (" . ($this->skipOnEmpty ? "$.trim(value)!='' && " : '') . ($this->not ? '' : '!') . "value.match($pattern)) { messages.push(" . json_encode($message) . "); } "; diff --git a/framework/validators/RequiredValidator.php b/framework/validators/RequiredValidator.php index 66b9c3c..3b13eb3 100644 --- a/framework/validators/RequiredValidator.php +++ b/framework/validators/RequiredValidator.php @@ -7,6 +7,8 @@ namespace yii\validators; +use Yii; + /** * RequiredValidator validates that the specified attribute does not have null or empty value. * @@ -16,6 +18,10 @@ namespace yii\validators; class RequiredValidator extends Validator { /** + * @var boolean whether to skip this validator if the value being validated is empty. + */ + public $skipOnEmpty = false; + /** * @var mixed the desired value that the attribute must have. * If this is null, the validator will validate that the specified attribute is not empty. * If this is set as a value that is not null, the validator will validate that @@ -35,6 +41,18 @@ class RequiredValidator extends Validator public $strict = false; /** + * Initializes the validator. + */ + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = $this->requiredValue === null ? Yii::t('yii|{attribute} is invalid.') + : Yii::t('yii|{attribute} must be "{requiredValue}".'); + } + } + + /** * Validates the attribute of the object. * If there is any error, the error message is added to the object. * @param \yii\base\Model $object the object being validated @@ -45,13 +63,11 @@ class RequiredValidator extends Validator $value = $object->$attribute; if ($this->requiredValue === null) { if ($this->strict && $value === null || !$this->strict && $this->isEmpty($value, true)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} cannot be blank.'); - $this->addError($object, $attribute, $message); + $this->addError($object, $attribute, $this->message); } } else { if (!$this->strict && $value != $this->requiredValue || $this->strict && $value !== $this->requiredValue) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} must be "{requiredValue}".'); - $this->addError($object, $attribute, $message, array( + $this->addError($object, $attribute, $this->message, array( '{requiredValue}' => $this->requiredValue, )); } @@ -59,6 +75,23 @@ class RequiredValidator extends Validator } /** + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. + */ + public function validateValue($value) + { + if ($this->requiredValue === null) { + if ($this->strict && $value !== null || !$this->strict && !$this->isEmpty($value, true)) { + return true; + } + } elseif (!$this->strict && $value == $this->requiredValue || $this->strict && $value === $this->requiredValue) { + return true; + } + return false; + } + + /** * Returns the JavaScript needed for performing client-side validation. * @param \yii\base\Model $object the data object being validated * @param string $attribute the name of the attribute to be validated. @@ -66,12 +99,8 @@ class RequiredValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { - $message = $this->message; if ($this->requiredValue !== null) { - if ($message === null) { - $message = \Yii::t('yii|{attribute} must be "{requiredValue}".'); - } - $message = strtr($message, array( + $message = strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, '{requiredValue}' => $this->requiredValue, @@ -82,10 +111,7 @@ if (value != " . json_encode($this->requiredValue) . ") { } "; } else { - if ($message === null) { - $message = \Yii::t('yii|{attribute} cannot be blank.'); - } - $message = strtr($message, array( + $message = strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, )); diff --git a/framework/validators/StringValidator.php b/framework/validators/StringValidator.php index 9135b9e..8b8c73b 100644 --- a/framework/validators/StringValidator.php +++ b/framework/validators/StringValidator.php @@ -7,6 +7,8 @@ namespace yii\validators; +use Yii; + /** * StringValidator validates that the attribute value is of certain length. * @@ -46,19 +48,34 @@ class StringValidator extends Validator */ public $notEqual; /** - * @var boolean whether the attribute value can be null or empty. Defaults to true, - * meaning that if the attribute is empty, it is considered valid. + * @var string the encoding of the string value to be validated (e.g. 'UTF-8'). + * If this property is not set, [[\yii\base\Application::charset]] will be used. */ - public $allowEmpty = true; + public $encoding; + + /** - * @var mixed the encoding of the string value to be validated (e.g. 'UTF-8'). - * This property is used only when mbstring PHP extension is enabled. - * The value of this property will be used as the 2nd parameter of the - * mb_strlen() function. If this property is not set, the application charset - * will be used. If this property is set false, then strlen() will be used even - * if mbstring is enabled. + * Initializes the validator. */ - public $encoding; + public function init() + { + parent::init(); + if ($this->encoding === null) { + $this->encoding = Yii::$app->charset; + } + if ($this->message === null) { + $this->message = Yii::t('yii|{attribute} must be a string.'); + } + if ($this->min !== null && $this->tooShort === null) { + $this->tooShort = Yii::t('yii|{attribute} should contain at least {min} characters.'); + } + if ($this->max !== null && $this->tooLong === null) { + $this->tooLong = Yii::t('yii|{attribute} should contain at most {max} characters.'); + } + if ($this->is !== null && $this->notEqual === null) { + $this->notEqual = Yii::t('yii|{attribute} should contain {length} characters.'); + } + } /** * Validates the attribute of the object. @@ -69,34 +86,39 @@ class StringValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { - return; - } if (!is_string($value)) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} must be a string.'); - $this->addError($object, $attribute, $message); + $this->addError($object, $attribute, $this->message); return; } - if (function_exists('mb_strlen') && $this->encoding !== false) { - $length = mb_strlen($value, $this->encoding ? $this->encoding : \Yii::$app->charset); - } else { - $length = strlen($value); - } + $length = mb_strlen($value, $this->encoding); if ($this->min !== null && $length < $this->min) { - $message = ($this->tooShort !== null) ? $this->tooShort : \Yii::t('yii|{attribute} is too short (minimum is {min} characters).'); - $this->addError($object, $attribute, $message, array('{min}' => $this->min)); + $this->addError($object, $attribute, $this->tooShort, array('{min}' => $this->min)); } if ($this->max !== null && $length > $this->max) { - $message = ($this->tooLong !== null) ? $this->tooLong : \Yii::t('yii|{attribute} is too long (maximum is {max} characters).'); - $this->addError($object, $attribute, $message, array('{max}' => $this->max)); + $this->addError($object, $attribute, $this->tooLong, array('{max}' => $this->max)); } if ($this->is !== null && $length !== $this->is) { - $message = ($this->notEqual !== null) ? $this->notEqual : \Yii::t('yii|{attribute} is of the wrong length (should be {length} characters).'); - $this->addError($object, $attribute, $message, array('{length}' => $this->is)); + $this->addError($object, $attribute, $this->notEqual, array('{length}' => $this->is)); + } + } + + /** + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. + */ + public function validateValue($value) + { + if (!is_string($value)) { + return false; } + $length = mb_strlen($value, $this->encoding); + return ($this->min === null || $length >= $this->min) + && ($this->max === null || $length <= $this->max) + && ($this->is === null || $length === $this->is); } /** @@ -110,28 +132,19 @@ class StringValidator extends Validator $label = $object->getAttributeLabel($attribute); $value = $object->$attribute; - if (($notEqual = $this->notEqual) === null) { - $notEqual = \Yii::t('yii|{attribute} is of the wrong length (should be {length} characters).'); - } - $notEqual = strtr($notEqual, array( + $notEqual = strtr($this->notEqual, array( '{attribute}' => $label, '{value}' => $value, '{length}' => $this->is, )); - if (($tooShort = $this->tooShort) === null) { - $tooShort = \Yii::t('yii|{attribute} is too short (minimum is {min} characters).'); - } - $tooShort = strtr($tooShort, array( + $tooShort = strtr($this->tooShort, array( '{attribute}' => $label, '{value}' => $value, '{min}' => $this->min, )); - if (($tooLong = $this->tooLong) === null) { - $tooLong = \Yii::t('yii|{attribute} is too long (maximum is {max} characters).'); - } - $tooLong = strtr($tooLong, array( + $tooLong = strtr($this->tooLong, array( '{attribute}' => $label, '{value}' => $value, '{max}' => $this->max, @@ -160,7 +173,7 @@ if(value.length!= {$this->is}) { "; } - if ($this->allowEmpty) { + if ($this->skipOnEmpty) { $js = " if($.trim(value)!='') { $js diff --git a/framework/validators/UniqueValidator.php b/framework/validators/UniqueValidator.php index bc12f5a..2240e0a 100644 --- a/framework/validators/UniqueValidator.php +++ b/framework/validators/UniqueValidator.php @@ -6,6 +6,8 @@ */ namespace yii\validators; + +use Yii; use yii\base\InvalidConfigException; /** @@ -17,11 +19,6 @@ use yii\base\InvalidConfigException; class UniqueValidator extends Validator { /** - * @var boolean whether the attribute value can be null or empty. Defaults to true, - * meaning that if the attribute is empty, it is considered valid. - */ - public $allowEmpty = true; - /** * @var string the ActiveRecord class name or alias of the class * that should be used to look for the attribute value being validated. * Defaults to null, meaning using the ActiveRecord class of the attribute being validated. @@ -36,6 +33,17 @@ class UniqueValidator extends Validator public $attributeName; /** + * Initializes the validator. + */ + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = Yii::t('yii|{attribute} "{value}" has already been taken.'); + } + } + + /** * Validates the attribute of the object. * If there is any error, the error message is added to the object. * @param \yii\db\ActiveRecord $object the object being validated @@ -45,17 +53,19 @@ class UniqueValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { + + if (is_array($value)) { + $this->addError($object, $attribute, Yii::t('yii|{attribute} is invalid.')); return; } /** @var $className \yii\db\ActiveRecord */ - $className = $this->className === null ? get_class($object) : \Yii::import($this->className); + $className = $this->className === null ? get_class($object) : Yii::import($this->className); $attributeName = $this->attributeName === null ? $attribute : $this->attributeName; $table = $className::getTableSchema(); if (($column = $table->getColumn($attributeName)) === null) { - throw new InvalidConfigException('Table "' . $table->name . '" does not have a column named "' . $attributeName . '"'); + throw new InvalidConfigException("Table '{$table->name}' does not have a column named '$attributeName'."); } $query = $className::find(); @@ -84,8 +94,7 @@ class UniqueValidator extends Validator } if ($exists) { - $message = $this->message !== null ? $this->message : \Yii::t('yii|{attribute} "{value}" has already been taken.'); - $this->addError($object, $attribute, $message); + $this->addError($object, $attribute, $this->message); } } } \ No newline at end of file diff --git a/framework/validators/UrlValidator.php b/framework/validators/UrlValidator.php index 0ba039b..cd6bfef 100644 --- a/framework/validators/UrlValidator.php +++ b/framework/validators/UrlValidator.php @@ -7,6 +7,8 @@ namespace yii\validators; +use Yii; + /** * UrlValidator validates that the attribute value is a valid http or https URL. * @@ -32,11 +34,18 @@ class UrlValidator extends Validator * contain the scheme part. **/ public $defaultScheme; + + /** - * @var boolean whether the attribute value can be null or empty. Defaults to true, - * meaning that if the attribute is empty, it is considered valid. + * Initializes the validator. */ - public $allowEmpty = true; + public function init() + { + parent::init(); + if ($this->message === null) { + $this->message = Yii::t('yii|{attribute} is not a valid URL.'); + } + } /** * Validates the attribute of the object. @@ -47,23 +56,19 @@ class UrlValidator extends Validator public function validateAttribute($object, $attribute) { $value = $object->$attribute; - if ($this->allowEmpty && $this->isEmpty($value)) { - return; - } - if (($value = $this->validateValue($value)) !== false) { - $object->$attribute = $value; + if ($this->validateValue($value)) { + if ($this->defaultScheme !== null && strpos($value, '://') === false) { + $object->$attribute = $this->defaultScheme . '://' . $value; + } } else { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is not a valid URL.'); - $this->addError($object, $attribute, $message); + $this->addError($object, $attribute, $this->message); } } /** - * Validates a static value to see if it is a valid URL. - * Note that this method does not respect [[allowEmpty]] property. - * This method is provided so that you can call it directly without going through the model validation rule mechanism. - * @param mixed $value the value to be validated - * @return mixed false if the the value is not a valid URL, otherwise the possibly modified value ({@see defaultScheme}) + * Validates the given value. + * @param mixed $value the value to be validated. + * @return boolean whether the value is valid. */ public function validateValue($value) { @@ -80,7 +85,7 @@ class UrlValidator extends Validator } if (preg_match($pattern, $value)) { - return $value; + return true; } } return false; @@ -95,8 +100,7 @@ class UrlValidator extends Validator */ public function clientValidateAttribute($object, $attribute) { - $message = ($this->message !== null) ? $this->message : \Yii::t('yii|{attribute} is not a valid URL.'); - $message = strtr($message, array( + $message = strtr($this->message, array( '{attribute}' => $object->getAttributeLabel($attribute), '{value}' => $object->$attribute, )); @@ -121,7 +125,7 @@ $js "; } - if ($this->allowEmpty) { + if ($this->skipOnEmpty) { $js = " if($.trim(value)!='') { $js diff --git a/framework/validators/Validator.php b/framework/validators/Validator.php index b688f32..5ab8dfe 100644 --- a/framework/validators/Validator.php +++ b/framework/validators/Validator.php @@ -7,7 +7,9 @@ namespace yii\validators; +use Yii; use yii\base\Component; +use yii\base\NotSupportedException; /** * Validator is the base class for all validators. @@ -81,7 +83,7 @@ abstract class Validator extends Component */ public $message; /** - * @var array list of scenarios that the validator should be applied. + * @var array list of scenarios that the validator can be applied to. */ public $on = array(); /** @@ -94,6 +96,12 @@ abstract class Validator extends Component */ public $skipOnError = true; /** + * @var boolean whether this validation rule should be skipped if the attribute value + * is null or an empty string. + */ + public $skipOnEmpty = true; + + /** * @var boolean whether to enable client-side validation. Defaults to null, meaning * its actual value inherits from that of [[\yii\web\ActiveForm::enableClientValidation]]. */ @@ -149,7 +157,7 @@ abstract class Validator extends Component } } - return \Yii::createObject($params); + return Yii::createObject($params); } /** @@ -168,13 +176,26 @@ abstract class Validator extends Component $attributes = $this->attributes; } foreach ($attributes as $attribute) { - if (!($this->skipOnError && $object->hasErrors($attribute))) { + $skip = $this->skipOnError && $object->hasErrors($attribute) + || $this->skipOnEmpty && $this->isEmpty($object->$attribute); + if (!$skip) { $this->validateAttribute($object, $attribute); } } } /** + * Validates a value. + * A validator class can implement this method to support data validation out of the context of a data model. + * @param mixed $value the data value to be validated. + * @throws NotSupportedException if data validation without a model is not supported + */ + public function validateValue($value) + { + throw new NotSupportedException(get_class($this) . ' does not support validateValue().'); + } + + /** * Returns the JavaScript needed for performing client-side validation. * * You may override this method to return the JavaScript validation code if @@ -224,8 +245,9 @@ abstract class Validator extends Component */ public function addError($object, $attribute, $message, $params = array()) { + $value = $object->$attribute; $params['{attribute}'] = $object->getAttributeLabel($attribute); - $params['{value}'] = $object->$attribute; + $params['{value}'] = is_array($value) ? 'array()' : $value; $object->addError($attribute, strtr($message, $params)); } diff --git a/framework/views/error.php b/framework/views/error.php index 893640a..548d04b 100644 --- a/framework/views/error.php +++ b/framework/views/error.php @@ -4,7 +4,7 @@ * @var \yii\base\ErrorHandler $context */ $context = $this->context; -$title = $context->htmlEncode($exception instanceof \yii\base\Exception || $exception instanceof \yii\base\ErrorException ? $exception->getName() : get_class($exception)); +$title = $context->htmlEncode($exception instanceof \yii\base\Exception ? $exception->getName() : get_class($exception)); ?> diff --git a/framework/views/exception.php b/framework/views/exception.php index db29302..f2aced0 100644 --- a/framework/views/exception.php +++ b/framework/views/exception.php @@ -4,7 +4,7 @@ * @var \yii\base\ErrorHandler $context */ $context = $this->context; -$title = $context->htmlEncode($exception instanceof \yii\base\Exception || $exception instanceof \yii\base\ErrorException ? $exception->getName().' ('.get_class($exception).')' : get_class($exception)); +$title = $context->htmlEncode($exception instanceof \yii\base\Exception ? $exception->getName().' ('.get_class($exception).')' : get_class($exception)); ?> diff --git a/framework/web/Application.php b/framework/web/Application.php index 2533f04..3387044 100644 --- a/framework/web/Application.php +++ b/framework/web/Application.php @@ -7,7 +7,7 @@ namespace yii\web; -use yii\base\InvalidParamException; +use Yii; /** * Application is the base class for all application classes. @@ -23,22 +23,42 @@ class Application extends \yii\base\Application public $defaultRoute = 'site'; /** - * Sets default path aliases. + * Processes the request. + * @return integer the exit status of the controller action (0 means normal, non-zero values mean abnormal) */ - public function registerDefaultAliases() + public function processRequest() { - parent::registerDefaultAliases(); - \Yii::$aliases['@webroot'] = dirname($_SERVER['SCRIPT_FILENAME']); + $request = $this->getRequest(); + Yii::setAlias('@wwwroot', dirname($request->getScriptFile())); + Yii::setAlias('@www', $request->getBaseUrl()); + list ($route, $params) = $request->resolve(); + return $this->runAction($route, $params); } + private $_homeUrl; + /** - * Processes the request. - * @return integer the exit status of the controller action (0 means normal, non-zero values mean abnormal) + * @return string the homepage URL */ - public function processRequest() + public function getHomeUrl() { - list ($route, $params) = $this->getRequest()->resolve(); - return $this->runAction($route, $params); + if ($this->_homeUrl === null) { + if ($this->getUrlManager()->showScriptName) { + return $this->getRequest()->getScriptUrl(); + } else { + return $this->getRequest()->getBaseUrl() . '/'; + } + } else { + return $this->_homeUrl; + } + } + + /** + * @param string $value the homepage URL + */ + public function setHomeUrl($value) + { + $this->_homeUrl = $value; } /** @@ -78,6 +98,15 @@ class Application extends \yii\base\Application } /** + * Returns the asset manager. + * @return AssetManager the asset manager component + */ + public function getAssetManager() + { + return $this->getComponent('assetManager'); + } + + /** * Registers the core application components. * @see setComponents */ @@ -97,6 +126,9 @@ class Application extends \yii\base\Application 'user' => array( 'class' => 'yii\web\User', ), + 'assetManager' => array( + 'class' => 'yii\web\AssetManager', + ), )); } } diff --git a/framework/web/AssetBundle.php b/framework/web/AssetBundle.php new file mode 100644 index 0000000..4e1eb59 --- /dev/null +++ b/framework/web/AssetBundle.php @@ -0,0 +1,176 @@ + + * @since 2.0 + */ +class AssetBundle extends Object +{ + /** + * @var string the root directory of the source asset files. A source asset file + * is a file that is part of your source code repository of your Web application. + * + * You must set this property if the directory containing the source asset files + * is not Web accessible (this is usually the case for extensions). + * + * By setting this property, the asset manager will publish the source asset files + * to a Web-accessible directory [[basePath]]. + * + * You can use either a directory or an alias of the directory. + */ + public $sourcePath; + /** + * @var string the Web-accessible directory that contains the asset files in this bundle. + * + * If [[sourcePath]] is set, this property will be *overwritten* by [[AssetManager]] + * when it publishes the asset files from [[sourcePath]]. + * + * If the bundle contains any assets that are specified in terms of relative file path, + * then this property must be set either manually or automatically (by asset manager via + * asset publishing). + * + * You can use either a directory or an alias of the directory. + */ + public $basePath; + /** + * @var string the base URL that will be prefixed to the asset files for them to + * be accessed via Web server. + * + * If [[sourcePath]] is set, this property will be *overwritten* by [[AssetManager]] + * when it publishes the asset files from [[sourcePath]]. + * + * If the bundle contains any assets that are specified in terms of relative file path, + * then this property must be set either manually or automatically (by asset manager via + * asset publishing). + * + * You can use either a URL or an alias of the URL. + */ + public $baseUrl; + /** + * @var array list of the bundle names that this bundle depends on + */ + public $depends = array(); + /** + * @var array list of JavaScript files that this bundle contains. Each JavaScript file can + * be either a file path (without leading slash) relative to [[basePath]] or a URL representing + * an external JavaScript file. + * + * Note that only forward slash "/" can be used as directory separator. + */ + public $js = array(); + /** + * @var array list of CSS files that this bundle contains. Each CSS file can + * be either a file path (without leading slash) relative to [[basePath]] or a URL representing + * an external CSS file. + * + * Note that only forward slash "/" can be used as directory separator. + */ + public $css = array(); + /** + * @var array the options that will be passed to [[\yii\base\View::registerJsFile()]] + * when registering the JS files in this bundle. + */ + public $jsOptions = array(); + /** + * @var array the options that will be passed to [[\yii\base\View::registerCssFile()]] + * when registering the CSS files in this bundle. + */ + public $cssOptions = array(); + /** + * @var array the options to be passed to [[AssetManager::publish()]] when the asset bundle + * is being published. + */ + public $publishOptions = array(); + + /** + * Initializes the bundle. + */ + public function init() + { + if ($this->sourcePath !== null) { + $this->sourcePath = rtrim(Yii::getAlias($this->sourcePath), '/\\'); + } + if ($this->basePath !== null) { + $this->basePath = rtrim(Yii::getAlias($this->basePath), '/\\'); + } + if ($this->baseUrl !== null) { + $this->baseUrl = rtrim(Yii::getAlias($this->baseUrl), '/'); + } + } + + /** + * Registers the CSS and JS files with the given view. + * This method will first register all dependent asset bundles. + * It will then try to convert non-CSS or JS files (e.g. LESS, Sass) into the corresponding + * CSS or JS files using [[AssetManager::converter|asset converter]]. + * @param \yii\base\View $view the view that the asset files to be registered with. + * @throws InvalidConfigException if [[baseUrl]] or [[basePath]] is not set when the bundle + * contains internal CSS or JS files. + */ + public function registerAssets($view) + { + foreach ($this->depends as $name) { + $view->registerAssetBundle($name); + } + + $this->publish($view->getAssetManager()); + + foreach ($this->js as $js) { + $view->registerJsFile($js, $this->jsOptions); + } + foreach ($this->css as $css) { + $view->registerCssFile($css, $this->cssOptions); + } + } + + /** + * Publishes the asset bundle if its source code is not under Web-accessible directory. + * @param AssetManager $am the asset manager to perform the asset publishing + * @throws InvalidConfigException if [[baseUrl]] or [[basePath]] is not set when the bundle + * contains internal CSS or JS files. + */ + public function publish($am) + { + if ($this->sourcePath !== null) { + list ($this->basePath, $this->baseUrl) = $am->publish($this->sourcePath, $this->publishOptions); + } + $converter = $am->getConverter(); + foreach ($this->js as $i => $js) { + if (strpos($js, '/') !== 0 && strpos($js, '://') === false) { + if (isset($this->basePath, $this->baseUrl)) { + $this->js[$i] = $converter->convert($js, $this->basePath, $this->baseUrl); + } else { + throw new InvalidConfigException('Both of the "baseUrl" and "basePath" properties must be set.'); + } + } + } + foreach ($this->css as $i => $css) { + if (strpos($css, '/') !== 0 && strpos($css, '://') === false) { + if (isset($this->basePath, $this->baseUrl)) { + $this->css[$i] = $converter->convert($css, $this->basePath, $this->baseUrl); + } else { + throw new InvalidConfigException('Both of the "baseUrl" and "basePath" properties must be set.'); + } + } + } + } +} \ No newline at end of file diff --git a/framework/web/AssetConverter.php b/framework/web/AssetConverter.php new file mode 100644 index 0000000..8340be5 --- /dev/null +++ b/framework/web/AssetConverter.php @@ -0,0 +1,62 @@ + + * @since 2.0 + */ +class AssetConverter extends Component implements IAssetConverter +{ + /** + * @var array the commands that are used to perform the asset conversion. + * The keys are the asset file extension names, and the values are the corresponding + * target script types (either "css" or "js") and the commands used for the conversion. + */ + public $commands = array( + 'less' => array('css', 'lessc {from} {to}'), + 'scss' => array('css', 'sass {from} {to}'), + 'sass' => array('css', 'sass {from} {to}'), + 'styl' => array('js', 'stylus < {from} > {to}'), + ); + + /** + * Converts a given asset file into a CSS or JS file. + * @param string $asset the asset file path, relative to $basePath + * @param string $basePath the directory the $asset is relative to. + * @param string $baseUrl the URL corresponding to $basePath + * @return string the URL to the converted asset file. + */ + public function convert($asset, $basePath, $baseUrl) + { + $pos = strrpos($asset, '.'); + if ($pos !== false) { + $ext = substr($asset, $pos + 1); + if (isset($this->commands[$ext])) { + list ($ext, $command) = $this->commands[$ext]; + $result = substr($asset, 0, $pos + 1) . $ext; + if (@filemtime("$basePath/$result") < filemtime("$basePath/$asset")) { + $output = array(); + $command = strtr($command, array( + '{from}' => "$basePath/$asset", + '{to}' => "$basePath/$result", + )); + exec($command, $output); + Yii::info("Converted $asset into $result: " . implode("\n", $output), __METHOD__); + return "$baseUrl/$result"; + } + } + } + return "$baseUrl/$asset"; + } +} \ No newline at end of file diff --git a/framework/web/AssetManager.php b/framework/web/AssetManager.php index 60f4c07..95dcbd2 100644 --- a/framework/web/AssetManager.php +++ b/framework/web/AssetManager.php @@ -1,153 +1,188 @@ * @link http://www.yiiframework.com/ - * @copyright Copyright © 2008-2011 Yii Software LLC + * @copyright Copyright (c) 2008 Yii Software LLC * @license http://www.yiiframework.com/license/ */ +namespace yii\web; + +use Yii; +use yii\base\Component; +use yii\base\InvalidConfigException; +use yii\base\InvalidParamException; +use yii\helpers\FileHelper; /** - * CAssetManager is a Web application component that manages private files (called assets) and makes them accessible by Web clients. - * - * It achieves this goal by copying assets to a Web-accessible directory - * and returns the corresponding URL for accessing them. - * - * To publish an asset, simply call {@link publish()}. - * - * The Web-accessible directory holding the published files is specified - * by {@link setBasePath basePath}, which defaults to the "assets" directory - * under the directory containing the application entry script file. - * The property {@link setBaseUrl baseUrl} refers to the URL for accessing - * the {@link setBasePath basePath}. - * - * @property string $basePath The root directory storing the published asset files. Defaults to 'WebRoot/assets'. - * @property string $baseUrl The base url that the published asset files can be accessed. - * Note, the ending slashes are stripped off. Defaults to '/AppBaseUrl/assets'. + * AssetManager manages asset bundles and asset publishing. * * @author Qiang Xue - * @version $Id$ - * @package system.web - * @since 1.0 + * @since 2.0 */ -class CAssetManager extends CApplicationComponent +class AssetManager extends Component { /** - * Default web accessible base path for storing private files + * @var array list of available asset bundles. The keys are the bundle names, and the values are the configuration + * arrays for creating the [[AssetBundle]] objects. + */ + public $bundles; + /** + * @return string the root directory storing the published asset files. */ - const DEFAULT_BASEPATH='assets'; + public $basePath = '@wwwroot/assets'; + /** + * @return string the base URL through which the published asset files can be accessed. + */ + public $baseUrl = '@www/assets'; /** * @var boolean whether to use symbolic link to publish asset files. Defaults to false, meaning - * asset files are copied to public folders. Using symbolic links has the benefit that the published - * assets will always be consistent with the source assets. This is especially useful during development. + * asset files are copied to [[basePath]]. Using symbolic links has the benefit that the published + * assets will always be consistent with the source assets and there is no copy operation required. + * This is especially useful during development. * * However, there are special requirements for hosting environments in order to use symbolic links. * In particular, symbolic links are supported only on Linux/Unix, and Windows Vista/2008 or greater. - * The latter requires PHP 5.3 or greater. * * Moreover, some Web servers need to be properly configured so that the linked assets are accessible * to Web users. For example, for Apache Web server, the following configuration directive should be added * for the Web folder: - *
-	 * Options FollowSymLinks
-	 * 
* - * @since 1.1.5 + * ~~~ + * Options FollowSymLinks + * ~~~ */ - public $linkAssets=false; + public $linkAssets = false; /** - * @var array list of directories and files which should be excluded from the publishing process. - * Defaults to exclude '.svn' and '.gitignore' files only. This option has no effect if {@link linkAssets} is enabled. - * @since 1.1.6 - **/ - public $excludeFiles=array('.svn','.gitignore'); - /** - * @var integer the permission to be set for newly generated asset files. - * This value will be used by PHP chmod function. - * Defaults to 0666, meaning the file is read-writable by all users. - * @since 1.1.8 + * @var integer the permission to be set for newly published asset files. + * This value will be used by PHP chmod() function. + * If not set, the permission will be determined by the current environment. */ - public $newFileMode=0666; + public $fileMode; /** * @var integer the permission to be set for newly generated asset directories. - * This value will be used by PHP chmod function. + * This value will be used by PHP chmod() function. * Defaults to 0777, meaning the directory can be read, written and executed by all users. - * @since 1.1.8 - */ - public $newDirMode=0777; - /** - * @var string base web accessible path for storing private files */ - private $_basePath; - /** - * @var string base URL for accessing the publishing directory. - */ - private $_baseUrl; - /** - * @var array published assets - */ - private $_published=array(); + public $dirMode = 0777; /** - * @return string the root directory storing the published asset files. Defaults to 'WebRoot/assets'. + * Initializes the component. + * @throws InvalidConfigException if [[basePath]] is invalid */ - public function getBasePath() + public function init() { - if($this->_basePath===null) - { - $request=\Yii::$app->getRequest(); - $this->setBasePath(dirname($request->getScriptFile()).DIRECTORY_SEPARATOR.self::DEFAULT_BASEPATH); + parent::init(); + $this->basePath = Yii::getAlias($this->basePath); + if (!is_dir($this->basePath)) { + throw new InvalidConfigException("The directory does not exist: {$this->basePath}"); + } elseif (!is_writable($this->basePath)) { + throw new InvalidConfigException("The directory is not writable by the Web process: {$this->basePath}"); + } else { + $this->basePath = realpath($this->basePath); + } + $this->baseUrl = rtrim(Yii::getAlias($this->baseUrl), '/'); + + foreach (require(YII_PATH . '/assets.php') as $name => $bundle) { + if (!isset($this->bundles[$name])) { + $this->bundles[$name] = $bundle; + } } - return $this->_basePath; } /** - * Sets the root directory storing published asset files. - * @param string $value the root directory storing published asset files - * @throws CException if the base path is invalid + * Returns the named bundle. + * This method will first look for the bundle in [[bundles]]. If not found, + * it will attempt to find the bundle from an installed extension using the following procedure: + * + * 1. Convert the bundle into a path alias; + * 2. Determine the root alias and use it to locate the bundle manifest file "assets.php"; + * 3. Look for the bundle in the manifest file. + * + * For example, given the bundle name "foo/button", the method will first convert it + * into the path alias "@foo/button"; since "@foo" is the root alias, it will look + * for the bundle manifest file "@foo/assets.php". The manifest file should return an array + * that lists the bundles used by the "foo/button" extension. The array format is the same as [[bundles]]. + * + * @param string $name the bundle name + * @return AssetBundle the loaded bundle object. Null is returned if the bundle does not exist. */ - public function setBasePath($value) + public function getBundle($name) { - if(($basePath=realpath($value))!==false && is_dir($basePath) && is_writable($basePath)) - $this->_basePath=$basePath; - else - throw new CException(Yii::t('yii|CAssetManager.basePath "{path}" is invalid. Please make sure the directory exists and is writable by the Web server process.', - array('{path}'=>$value))); + if (!isset($this->bundles[$name])) { + $rootAlias = Yii::getRootAlias("@$name"); + if ($rootAlias !== false) { + $manifest = Yii::getAlias("$rootAlias/assets.php", false); + if ($manifest !== false && is_file($manifest)) { + foreach (require($manifest) as $bn => $config) { + $this->bundles[$bn] = $config; + } + } + } + if (!isset($this->bundles[$name])) { + return null; + } + } + if (is_array($this->bundles[$name])) { + $config = $this->bundles[$name]; + if (!isset($config['class'])) { + $config['class'] = 'yii\\web\\AssetBundle'; + $this->bundles[$name] = Yii::createObject($config); + } + } + + return $this->bundles[$name]; } + private $_converter; + /** - * @return string the base url that the published asset files can be accessed. - * Note, the ending slashes are stripped off. Defaults to '/AppBaseUrl/assets'. + * Returns the asset converter. + * @return IAssetConverter the asset converter. */ - public function getBaseUrl() + public function getConverter() { - if($this->_baseUrl===null) - { - $request=\Yii::$app->getRequest(); - $this->setBaseUrl($request->getBaseUrl().'/'.self::DEFAULT_BASEPATH); + if ($this->_converter === null) { + $this->_converter = Yii::createObject(array( + 'class' => 'yii\\web\\AssetConverter', + )); + } elseif (is_array($this->_converter) || is_string($this->_converter)) { + $this->_converter = Yii::createObject($this->_converter); } - return $this->_baseUrl; + return $this->_converter; } /** - * @param string $value the base url that the published asset files can be accessed + * Sets the asset converter. + * @param array|IAssetConverter $value the asset converter. This can be either + * an object implementing the [[IAssetConverter]] interface, or a configuration + * array that can be used to create the asset converter object. */ - public function setBaseUrl($value) + public function setConverter($value) { - $this->_baseUrl=rtrim($value,'/'); + $this->_converter = $value; } /** + * @var array published assets + */ + private $_published = array(); + + /** * Publishes a file or a directory. - * This method will copy the specified asset to a web accessible directory - * and return the URL for accessing the published asset. - *
    - *
  • If the asset is a file, its file modification time will be checked - * to avoid unnecessary file copying;
  • - *
  • If the asset is a directory, all files and subdirectories under it will - * be published recursively. Note, in case $forceCopy is false the method only checks the - * existence of the target directory to avoid repetitive copying.
  • - *
+ * + * This method will copy the specified file or directory to [[basePath]] so that + * it can be accessed via the Web server. + * + * If the asset is a file, its file modification time will be checked to avoid + * unnecessary file copying. + * + * If the asset is a directory, all files and subdirectories under it will be published recursively. + * Note, in case $forceCopy is false the method only checks the existence of the target + * directory to avoid repetitive copying (which is very expensive). + * + * By default, when publishing a directory, subdirectories and files whose name starts with a dot "." + * will NOT be published. If you want to change this behavior, you may specify the "beforeCopy" option + * as explained in the `$options` parameter. * * Note: On rare scenario, a race condition can develop that will lead to a * one-time-manifestation of a non-critical problem in the creation of the directory @@ -157,85 +192,85 @@ class CAssetManager extends CApplicationComponent * discussion: http://code.google.com/p/yii/issues/detail?id=2579 * * @param string $path the asset (file or directory) to be published - * @param boolean $hashByName whether the published directory should be named as the hashed basename. - * If false, the name will be the hash taken from dirname of the path being published and path mtime. - * Defaults to false. Set true if the path being published is shared among - * different extensions. - * @param integer $level level of recursive copying when the asset is a directory. - * Level -1 means publishing all subdirectories and files; - * Level 0 means publishing only the files DIRECTLY under the directory; - * level N means copying those directories that are within N levels. - * @param boolean $forceCopy whether we should copy the asset file or directory even if it is already published before. - * This parameter is set true mainly during development stage when the original - * assets are being constantly changed. The consequence is that the performance - * is degraded, which is not a concern during development, however. - * This parameter has been available since version 1.1.2. - * @return string an absolute URL to the published asset - * @throws CException if the asset to be published does not exist. + * @param array $options the options to be applied when publishing a directory. + * The following options are supported: + * + * - beforeCopy: callback, a PHP callback that is called before copying each sub-directory or file. + * This option is used only when publishing a directory. If the callback returns false, the copy + * operation for the sub-directory or file will be cancelled. + * 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. + * This option is used only when publishing a directory. The signature of the callback is similar to that + * of `beforeCopy`. + * - forceCopy: boolean, whether the directory being published should be copied even if + * it is found in the target directory. This option is used only when publishing a directory. + * You may want to set this to be true during the development stage to make sure the published + * directory is always up-to-date. Do not set this to true on production servers as it will + * significantly degrade the performance. + * @return array the path (directory or file path) and the URL that the asset is published as. + * @throws InvalidParamException if the asset to be published does not exist. */ - public function publish($path,$hashByName=false,$level=-1,$forceCopy=false) + public function publish($path, $options = array()) { - if(isset($this->_published[$path])) + if (isset($this->_published[$path])) { return $this->_published[$path]; - else if(($src=realpath($path))!==false) - { - if(is_file($src)) - { - $dir=$this->hash($hashByName ? basename($src) : dirname($src).filemtime($src)); - $fileName=basename($src); - $dstDir=$this->getBasePath().DIRECTORY_SEPARATOR.$dir; - $dstFile=$dstDir.DIRECTORY_SEPARATOR.$fileName; + } - if($this->linkAssets) - { - if(!is_file($dstFile)) - { - if(!is_dir($dstDir)) - { - mkdir($dstDir); - @chmod($dstDir, $this->newDirMode); - } - symlink($src,$dstFile); - } - } - else if(@filemtime($dstFile)<@filemtime($src)) - { - if(!is_dir($dstDir)) - { - mkdir($dstDir); - @chmod($dstDir, $this->newDirMode); - } - copy($src,$dstFile); - @chmod($dstFile, $this->newFileMode); - } + $src = realpath($path); + if ($src === false) { + throw new InvalidParamException("The file or directory to be published does not exist: $path"); + } - return $this->_published[$path]=$this->getBaseUrl()."/$dir/$fileName"; + if (is_file($src)) { + $dir = $this->hash(dirname($src) . filemtime($src)); + $fileName = basename($src); + $dstDir = $this->basePath . DIRECTORY_SEPARATOR . $dir; + $dstFile = $dstDir . DIRECTORY_SEPARATOR . $fileName; + + if (!is_dir($dstDir)) { + mkdir($dstDir, $this->dirMode, true); } - else if(is_dir($src)) - { - $dir=$this->hash($hashByName ? basename($src) : $src.filemtime($src)); - $dstDir=$this->getBasePath().DIRECTORY_SEPARATOR.$dir; - if($this->linkAssets) - { - if(!is_dir($dstDir)) - symlink($src,$dstDir); + if ($this->linkAssets) { + if (!is_file($dstFile)) { + symlink($src, $dstFile); } - else if(!is_dir($dstDir) || $forceCopy) - { - CFileHelper::copyDirectory($src,$dstDir,array( - 'exclude'=>$this->excludeFiles, - 'level'=>$level, - 'newDirMode'=>$this->newDirMode, - 'newFileMode'=>$this->newFileMode, - )); + } elseif (@filemtime($dstFile) < @filemtime($src)) { + copy($src, $dstFile); + if ($this->fileMode !== null) { + @chmod($dstFile, $this->fileMode); } + } - return $this->_published[$path]=$this->getBaseUrl().'/'.$dir; + return $this->_published[$path] = array($dstFile, $this->baseUrl . "/$dir/$fileName"); + } else { + $dir = $this->hash($src . filemtime($src)); + $dstDir = $this->basePath . DIRECTORY_SEPARATOR . $dir; + if ($this->linkAssets) { + if (!is_dir($dstDir)) { + symlink($src, $dstDir); + } + } elseif (!is_dir($dstDir) || !empty($options['forceCopy'])) { + $opts = array( + 'dirMode' => $this->dirMode, + 'fileMode' => $this->fileMode, + ); + if (isset($options['beforeCopy'])) { + $opts['beforeCopy'] = $options['beforeCopy']; + } else { + $opts['beforeCopy'] = function ($from, $to) { + return strncmp(basename($from), '.', 1) !== 0; + }; + } + if (isset($options['afterCopy'])) { + $opts['afterCopy'] = $options['afterCopy']; + } + FileHelper::copyDirectory($src, $dstDir, $opts); } + + return $this->_published[$path] = array($dstDir, $this->baseUrl . '/' . $dir); } - throw new CException(Yii::t('yii|The asset "{asset}" to be published does not exist.', - array('{asset}'=>$path))); } /** @@ -243,24 +278,20 @@ class CAssetManager extends CApplicationComponent * This method does not perform any publishing. It merely tells you * if the file or directory is published, where it will go. * @param string $path directory or file path being published - * @param boolean $hashByName whether the published directory should be named as the hashed basename. - * If false, the name will be the hash taken from dirname of the path being published and path mtime. - * Defaults to false. Set true if the path being published is shared among - * different extensions. * @return string the published file path. False if the file or directory does not exist */ - public function getPublishedPath($path,$hashByName=false) + public function getPublishedPath($path) { - if(($path=realpath($path))!==false) - { - $base=$this->getBasePath().DIRECTORY_SEPARATOR; - if(is_file($path)) - return $base . $this->hash($hashByName ? basename($path) : dirname($path).filemtime($path)) . DIRECTORY_SEPARATOR . basename($path); - else - return $base . $this->hash($hashByName ? basename($path) : $path.filemtime($path)); - } - else + if (($path = realpath($path)) !== false) { + $base = $this->basePath . DIRECTORY_SEPARATOR; + if (is_file($path)) { + return $base . $this->hash(dirname($path) . filemtime($path)) . DIRECTORY_SEPARATOR . basename($path); + } else { + return $base . $this->hash($path . filemtime($path)); + } + } else { return false; + } } /** @@ -268,25 +299,22 @@ class CAssetManager extends CApplicationComponent * This method does not perform any publishing. It merely tells you * if the file path is published, what the URL will be to access it. * @param string $path directory or file path being published - * @param boolean $hashByName whether the published directory should be named as the hashed basename. - * If false, the name will be the hash taken from dirname of the path being published and path mtime. - * Defaults to false. Set true if the path being published is shared among - * different extensions. * @return string the published URL for the file or directory. False if the file or directory does not exist. */ - public function getPublishedUrl($path,$hashByName=false) + public function getPublishedUrl($path) { - if(isset($this->_published[$path])) + if (isset($this->_published[$path])) { return $this->_published[$path]; - if(($path=realpath($path))!==false) - { - if(is_file($path)) - return $this->getBaseUrl().'/'.$this->hash($hashByName ? basename($path) : dirname($path).filemtime($path)).'/'.basename($path); - else - return $this->getBaseUrl().'/'.$this->hash($hashByName ? basename($path) : $path.filemtime($path)); } - else + if (($path = realpath($path)) !== false) { + if (is_file($path)) { + return $this->baseUrl . '/' . $this->hash(dirname($path) . filemtime($path)) . '/' . basename($path); + } else { + return $this->baseUrl . '/' . $this->hash($path . filemtime($path)); + } + } else { return false; + } } /** @@ -297,6 +325,6 @@ class CAssetManager extends CApplicationComponent */ protected function hash($path) { - return sprintf('%x',crc32($path.Yii::getVersion())); + return sprintf('%x', crc32($path . Yii::getVersion())); } } diff --git a/framework/web/Controller.php b/framework/web/Controller.php index 93b74aa..8049299 100644 --- a/framework/web/Controller.php +++ b/framework/web/Controller.php @@ -8,6 +8,7 @@ namespace yii\web; use Yii; +use yii\helpers\Html; /** * Controller is the base class of Web controllers. @@ -40,5 +41,4 @@ class Controller extends \yii\base\Controller } return Yii::$app->getUrlManager()->createUrl($route, $params); } - } \ No newline at end of file diff --git a/framework/web/DbSession.php b/framework/web/DbSession.php index d3afc76..2910b40 100644 --- a/framework/web/DbSession.php +++ b/framework/web/DbSession.php @@ -144,7 +144,7 @@ class DbSession extends Session $query = new Query; $data = $query->select(array('data')) ->from($this->sessionTable) - ->where('expire>:expire AND id=:id', array(':expire' => time(), ':id' => $id)) + ->where('[[expire]]>:expire AND [[id]]=:id', array(':expire' => time(), ':id' => $id)) ->createCommand($this->db) ->queryScalar(); return $data === false ? '' : $data; @@ -214,7 +214,7 @@ class DbSession extends Session public function gcSession($maxLifetime) { $this->db->createCommand() - ->delete($this->sessionTable, 'expire<:expire', array(':expire' => time())) + ->delete($this->sessionTable, '[[expire]]<:expire', array(':expire' => time())) ->execute(); return true; } diff --git a/framework/web/IAssetConverter.php b/framework/web/IAssetConverter.php new file mode 100644 index 0000000..4334d3e --- /dev/null +++ b/framework/web/IAssetConverter.php @@ -0,0 +1,27 @@ + + * @since 2.0 + */ +interface IAssetConverter +{ + /** + * Converts a given asset file into a CSS or JS file. + * @param string $asset the asset file path, relative to $basePath + * @param string $basePath the directory the $asset is relative to. + * @param string $baseUrl the URL corresponding to $basePath + * @return string the URL to the converted asset file. If the given asset does not + * need conversion, "$baseUrl/$asset" should be returned. + */ + public function convert($asset, $basePath, $baseUrl); +} \ No newline at end of file diff --git a/framework/web/Identity.php b/framework/web/Identity.php index 4668337..6d67bc0 100644 --- a/framework/web/Identity.php +++ b/framework/web/Identity.php @@ -8,6 +8,35 @@ namespace yii\web; /** + * Identity is the interface that should be implemented by a class providing identity information. + * + * This interface can typically be implemented by a user model class. For example, the following + * code shows how to implement this interface by a User ActiveRecord class: + * + * ~~~ + * class User extends ActiveRecord implements Identity + * { + * public static function findIdentity($id) + * { + * return static::find($id); + * } + * + * public function getId() + * { + * return $this->id; + * } + * + * public function getAuthKey() + * { + * return $this->authKey; + * } + * + * public function validateAuthKey($authKey) + * { + * return $this->authKey === $authKey; + * } + * } + * ~~~ * * @author Qiang Xue * @since 2.0 @@ -15,31 +44,38 @@ namespace yii\web; interface Identity { /** + * Finds an identity by the given ID. + * @param string|integer $id the ID to be looked for + * @return Identity the identity object that matches the given ID. + * Null should be returned if such an identity cannot be found + * or the identity is not in an active state (disabled, deleted, etc.) + */ + public static function findIdentity($id); + /** * Returns an ID that can uniquely identify a user identity. - * The returned ID can be a string, an integer, or any serializable data. - * @return mixed an ID that uniquely identifies a user identity. + * @return string|integer an ID that uniquely identifies a user identity. */ public function getId(); /** * Returns a key that can be used to check the validity of a given identity ID. - * The space of such keys should be big and random enough to defeat potential identity attacks. - * The returned key can be a string, an integer, or any serializable data. - * @return mixed a key that is used to check the validity of a given identity ID. + * + * The key should be unique for each individual user, and should be persistent + * so that it can be used to check the validity of the user identity. + * + * The space of such keys should be big enough to defeat potential identity attacks. + * + * This is required if [[User::enableAutoLogin]] is enabled. + * @return string a key that is used to check the validity of a given identity ID. * @see validateAuthKey() */ public function getAuthKey(); /** * Validates the given auth key. + * + * This is required if [[User::enableAutoLogin]] is enabled. * @param string $authKey the given auth key * @return boolean whether the given auth key is valid. * @see getAuthKey() */ public function validateAuthKey($authKey); - /** - * Finds an identity by the given ID. - * @param mixed $id the ID to be looked for - * @return Identity the identity object that matches the given ID. - * Null should be returned if such an identity cannot be found. - */ - public static function findIdentity($id); } \ No newline at end of file diff --git a/framework/web/PageCache.php b/framework/web/PageCache.php index 29c8cc8..5a50825 100644 --- a/framework/web/PageCache.php +++ b/framework/web/PageCache.php @@ -25,11 +25,6 @@ class PageCache extends ActionFilter */ public $varyByRoute = true; /** - * @var View the view object that is used to create the fragment cache widget to implement page caching. - * If not set, the view registered with the application will be used. - */ - public $view; - /** * @var string the application component ID of the [[\yii\caching\Cache|cache]] object. */ public $cache = 'cache'; diff --git a/framework/web/Request.php b/framework/web/Request.php index 093a394..369fa0c 100644 --- a/framework/web/Request.php +++ b/framework/web/Request.php @@ -43,8 +43,6 @@ class Request extends \yii\base\Request */ public function resolve() { - Yii::setAlias('@www', $this->getBaseUrl()); - $result = Yii::$app->getUrlManager()->parseRequest($this); if ($result !== false) { list ($route, $params) = $result; @@ -301,7 +299,8 @@ class Request extends \yii\base\Request public function getScriptUrl() { if ($this->_scriptUrl === null) { - $scriptName = basename($_SERVER['SCRIPT_FILENAME']); + $scriptFile = $this->getScriptFile(); + $scriptName = basename($scriptFile); if (basename($_SERVER['SCRIPT_NAME']) === $scriptName) { $this->_scriptUrl = $_SERVER['SCRIPT_NAME']; } elseif (basename($_SERVER['PHP_SELF']) === $scriptName) { @@ -310,8 +309,8 @@ class Request extends \yii\base\Request $this->_scriptUrl = $_SERVER['ORIG_SCRIPT_NAME']; } elseif (($pos = strpos($_SERVER['PHP_SELF'], '/' . $scriptName)) !== false) { $this->_scriptUrl = substr($_SERVER['SCRIPT_NAME'], 0, $pos) . '/' . $scriptName; - } elseif (isset($_SERVER['DOCUMENT_ROOT']) && strpos($_SERVER['SCRIPT_FILENAME'], $_SERVER['DOCUMENT_ROOT']) === 0) { - $this->_scriptUrl = str_replace('\\', '/', str_replace($_SERVER['DOCUMENT_ROOT'], '', $_SERVER['SCRIPT_FILENAME'])); + } elseif (isset($_SERVER['DOCUMENT_ROOT']) && strpos($scriptFile, $_SERVER['DOCUMENT_ROOT']) === 0) { + $this->_scriptUrl = str_replace('\\', '/', str_replace($_SERVER['DOCUMENT_ROOT'], '', $scriptFile)); } else { throw new InvalidConfigException('Unable to determine the entry script URL.'); } @@ -330,6 +329,30 @@ class Request extends \yii\base\Request $this->_scriptUrl = '/' . trim($value, '/'); } + private $_scriptFile; + + /** + * Returns the entry script file path. + * The default implementation will simply return `$_SERVER['SCRIPT_FILENAME']`. + * @return string the entry script file path + */ + public function getScriptFile() + { + return isset($this->_scriptFile) ? $this->_scriptFile : $_SERVER['SCRIPT_FILENAME']; + } + + /** + * Sets the entry script file path. + * The entry script file path normally can be obtained from `$_SERVER['SCRIPT_FILENAME']`. + * If your server configuration does not return the correct value, you may configure + * this property to make it right. + * @param string $value the entry script file path. + */ + public function setScriptFile($value) + { + $this->_scriptFile = $value; + } + private $_pathInfo; /** diff --git a/framework/web/Response.php b/framework/web/Response.php index d23c5b9..1d604e9 100644 --- a/framework/web/Response.php +++ b/framework/web/Response.php @@ -9,6 +9,7 @@ namespace yii\web; use Yii; use yii\helpers\FileHelper; +use yii\helpers\Html; /** * @author Qiang Xue @@ -17,6 +18,14 @@ use yii\helpers\FileHelper; class Response extends \yii\base\Response { /** + * @var integer the HTTP status code that should be used when redirecting in AJAX mode. + * This is used by [[redirect()]]. A 2xx code should normally be used for this purpose + * so that the AJAX handler will treat the response as a success. + * @see redirect + */ + public $ajaxRedirectCode = 278; + + /** * Sends a file to user. * @param string $fileName file name * @param string $content content to be set. @@ -103,61 +112,89 @@ class Response extends \yii\base\Response *
  • mimeType: mime type of the file, if not set it will be guessed automatically based on the file name, if set to null no content-type header will be sent.
  • *
  • xHeader: appropriate x-sendfile header, defaults to "X-Sendfile"
  • *
  • terminate: whether to terminate the current application after calling this method, defaults to true
  • - *
  • forceDownload: specifies whether the file will be downloaded or shown inline, defaults to true. (Since version 1.1.9.)
  • - *
  • addHeaders: an array of additional http headers in header-value pairs (available since version 1.1.10)
  • + *
  • forceDownload: specifies whether the file will be downloaded or shown inline, defaults to true
  • + *
  • addHeaders: an array of additional http headers in header-value pairs
  • * */ - public function xSendFile($filePath, $options=array()) + public function xSendFile($filePath, $options = array()) { - if(!isset($options['forceDownload']) || $options['forceDownload']) - $disposition='attachment'; - else - $disposition='inline'; + if (!isset($options['forceDownload']) || $options['forceDownload']) { + $disposition = 'attachment'; + } else { + $disposition = 'inline'; + } - if(!isset($options['saveName'])) - $options['saveName']=basename($filePath); + if (!isset($options['saveName'])) { + $options['saveName'] = basename($filePath); + } - if(!isset($options['mimeType'])) - { - if(($options['mimeType']=CFileHelper::getMimeTypeByExtension($filePath))===null) - $options['mimeType']='text/plain'; + if (!isset($options['mimeType'])) { + if (($options['mimeType'] = CFileHelper::getMimeTypeByExtension($filePath)) === null) { + $options['mimeType'] = 'text/plain'; + } } - if(!isset($options['xHeader'])) - $options['xHeader']='X-Sendfile'; + if (!isset($options['xHeader'])) { + $options['xHeader'] = 'X-Sendfile'; + } - 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); + if ($options['mimeType'] !== null) { + header('Content-type: ' . $options['mimeType']); } - header(trim($options['xHeader']).': '.$filePath); + header('Content-Disposition: ' . $disposition . '; filename="' . $options['saveName'] . '"'); + if (isset($options['addHeaders'])) { + foreach ($options['addHeaders'] as $header => $value) { + header($header . ': ' . $value); + } + } + header(trim($options['xHeader']) . ': ' . $filePath); - if(!isset($options['terminate']) || $options['terminate']) - Yii::app()->end(); + if (!isset($options['terminate']) || $options['terminate']) { + Yii::$app->end(); + } } /** * Redirects the browser to the specified URL. - * @param string $url URL to be redirected to. Note that when URL is not - * absolute (not starting with "/") it will be relative to current request URL. + * This method will send out a "Location" header to achieve the redirection. + * In AJAX mode, this normally will not work as expected unless there are some + * client-side JavaScript code handling the redirection. To help achieve this goal, + * this method will use [[ajaxRedirectCode]] as the HTTP status code when performing + * redirection in AJAX mode. The following JavaScript code may be used on the client + * side to handle the redirection response: + * + * ~~~ + * $(document).ajaxSuccess(function(event, xhr, settings) { + * if (xhr.status == 278) { + * window.location = xhr.getResponseHeader('Location'); + * } + * }); + * ~~~ + * + * @param array|string $url the URL to be redirected to. [[\yii\helpers\Html::url()]] + * will be used to normalize the URL. If the resulting URL is still a relative URL + * (one without host info), the current request host info will be used. * @param boolean $terminate whether to terminate the current application - * @param integer $statusCode the HTTP status code. Defaults to 302. See {@link http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html} + * @param integer $statusCode the HTTP status code. Defaults to 302. + * See [[http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html]] * for details about HTTP status code. + * Note that if the request is an AJAX request, [[ajaxRedirectCode]] will be used instead. */ - public function redirect($url,$terminate=true,$statusCode=302) + public function redirect($url, $terminate = true, $statusCode = 302) { - if(strpos($url,'/')===0 && strpos($url,'//')!==0) - $url=$this->getHostInfo().$url; - header('Location: '.$url, true, $statusCode); - if($terminate) - Yii::app()->end(); + $url = Html::url($url); + if (strpos($url, '/') === 0 && strpos($url, '//') !== 0) { + $url = Yii::$app->getRequest()->getHostInfo() . $url; + } + if (Yii::$app->getRequest()->getIsAjaxRequest()) { + $statusCode = $this->ajaxRedirectCode; + } + header('Location: ' . $url, true, $statusCode); + if ($terminate) { + Yii::$app->end(); + } } - /** * Returns the cookie collection. * Through the returned cookie collection, you add or remove cookies as follows, diff --git a/framework/web/Session.php b/framework/web/Session.php index c289db2..4c0505f 100644 --- a/framework/web/Session.php +++ b/framework/web/Session.php @@ -60,6 +60,13 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co public $flashVar = '__flash'; /** + * @var array parameter-value pairs to override default session cookie parameters + */ + public $cookieParams = array( + 'httponly' => true + ); + + /** * Initializes the application component. * This method is required by IApplicationComponent and is invoked by application. */ @@ -111,13 +118,15 @@ class Session extends Component implements \IteratorAggregate, \ArrayAccess, \Co ); } + $this->setCookieParams($this->cookieParams); + @session_start(); if (session_id() == '') { $this->_opened = false; $error = error_get_last(); $message = isset($error['message']) ? $error['message'] : 'Failed to start session.'; - Yii::error($message, __CLASS__); + Yii::error($message, __METHOD__); } else { $this->_opened = true; $this->updateFlashCounters(); diff --git a/framework/web/Sort.php b/framework/web/Sort.php index 7cfeeca..e5c2451 100644 --- a/framework/web/Sort.php +++ b/framework/web/Sort.php @@ -216,7 +216,7 @@ class Sort extends \yii\base\Object $url = $this->createUrl($attribute); - return Html::link($label, $url, $htmlOptions); + return Html::a($label, $url, $htmlOptions); } private $_attributeOrders; diff --git a/framework/web/UploadedFile.php b/framework/web/UploadedFile.php new file mode 100644 index 0000000..c67281c --- /dev/null +++ b/framework/web/UploadedFile.php @@ -0,0 +1,246 @@ + + * @since 2.0 + */ +class UploadedFile extends \yii\base\Object +{ + private static $_files; + private $_name; + private $_tempName; + private $_type; + private $_size; + private $_error; + + + /** + * Constructor. + * Instead of using the constructor to create a new instance, + * you should normally call [[getInstance()]] or [[getInstances()]] + * to obtain new instances. + * @param string $name the original name of the file being uploaded + * @param string $tempName the path of the uploaded file on the server. + * @param string $type the MIME-type of the uploaded file (such as "image/gif"). + * @param integer $size the actual size of the uploaded file in bytes + * @param integer $error the error code + */ + public function __construct($name, $tempName, $type, $size, $error) + { + $this->_name = $name; + $this->_tempName = $tempName; + $this->_type = $type; + $this->_size = $size; + $this->_error = $error; + } + + /** + * String output. + * This is PHP magic method that returns string representation of an object. + * The implementation here returns the uploaded file's name. + * @return string the string representation of the object + */ + public function __toString() + { + return $this->_name; + } + + /** + * Returns an uploaded file for the given model attribute. + * The file should be uploaded using [[ActiveForm::fileInput()]]. + * @param \yii\base\Model $model the data model + * @param string $attribute the attribute name. The attribute name may contain array indexes. + * For example, '[1]file' for tabular file uploading; and 'file[1]' for an element in a file array. + * @return UploadedFile the instance of the uploaded file. + * Null is returned if no file is uploaded for the specified model attribute. + * @see getInstanceByName + */ + public static function getInstance($model, $attribute) + { + $name = ActiveForm::getInputName($model, $attribute); + return static::getInstanceByName($name); + } + + /** + * Returns all uploaded files for the given model attribute. + * @param \yii\base\Model $model the data model + * @param string $attribute the attribute name. The attribute name may contain array indexes + * for tabular file uploading, e.g. '[1]file'. + * @return UploadedFile[] array of UploadedFile objects. + * Empty array is returned if no available file was found for the given attribute. + */ + public static function getInstances($model, $attribute) + { + $name = ActiveForm::getInputName($model, $attribute); + return static::getInstancesByName($name); + } + + /** + * Returns an uploaded file according to the given file input name. + * The name can be a plain string or a string like an array element (e.g. 'Post[imageFile]', or 'Post[0][imageFile]'). + * @param string $name the name of the file input field. + * @return UploadedFile the instance of the uploaded file. + * Null is returned if no file is uploaded for the specified name. + */ + public static function getInstanceByName($name) + { + $files = static::loadFiles(); + return isset($files[$name]) ? $files[$name] : null; + } + + /** + * Returns an array of uploaded files corresponding to the specified file input name. + * This is mainly used when multiple files were uploaded and saved as 'files[0]', 'files[1]', + * 'files[n]'..., and you can retrieve them all by passing 'files' as the name. + * @param string $name the name of the array of files + * @return UploadedFile[] the array of CUploadedFile objects. Empty array is returned + * if no adequate upload was found. Please note that this array will contain + * all files from all sub-arrays regardless how deeply nested they are. + */ + public static function getInstancesByName($name) + { + $files = static::loadFiles(); + if (isset($files[$name])) { + return array($files[$name]); + } + $results = array(); + foreach ($files as $key => $file) { + if (strpos($key, "{$name}[") === 0) { + $results[] = self::$_files[$key]; + } + } + return $results; + } + + /** + * Cleans up the loaded UploadedFile instances. + * This method is mainly used by test scripts to set up a fixture. + */ + public static function reset() + { + self::$_files = null; + } + + /** + * Saves the uploaded file. + * Note that this method uses php's move_uploaded_file() method. If the target file `$file` + * already exists, it will be overwritten. + * @param string $file the file path used to save the uploaded file + * @param boolean $deleteTempFile whether to delete the temporary file after saving. + * If true, you will not be able to save the uploaded file again in the current request. + * @return boolean true whether the file is saved successfully + * @see error + */ + public function saveAs($file, $deleteTempFile = true) + { + if ($this->_error == UPLOAD_ERR_OK) { + if ($deleteTempFile) { + return move_uploaded_file($this->_tempName, $file); + } elseif (is_uploaded_file($this->_tempName)) { + return copy($this->_tempName, $file); + } + } + return false; + } + + /** + * @return string the original name of the file being uploaded + */ + public function getName() + { + return $this->_name; + } + + /** + * @return string the path of the uploaded file on the server. + * Note, this is a temporary file which will be automatically deleted by PHP + * after the current request is processed. + */ + public function getTempName() + { + return $this->_tempName; + } + + /** + * @return string the MIME-type of the uploaded file (such as "image/gif"). + * Since this MIME type is not checked on the server side, do not take this value for granted. + * Instead, use [[FileHelper::getMimeType()]] to determine the exact MIME type. + */ + public function getType() + { + return $this->_type; + } + + /** + * @return integer the actual size of the uploaded file in bytes + */ + public function getSize() + { + return $this->_size; + } + + /** + * Returns an error code describing the status of this file uploading. + * @return integer the error code + * @see http://www.php.net/manual/en/features.file-upload.errors.php + */ + public function getError() + { + return $this->_error; + } + + /** + * @return boolean whether there is an error with the uploaded file. + * Check [[error]] for detailed error code information. + */ + public function getHasError() + { + return $this->_error != UPLOAD_ERR_OK; + } + + /** + * Creates UploadedFile instances from $_FILE. + * @return array the UploadedFile instances + */ + private static function loadFiles() + { + if (self::$_files === null) { + self::$_files = array(); + if (isset($_FILES) && is_array($_FILES)) { + foreach ($_FILES as $class => $info) { + self::loadFilesRecursive($class, $info['name'], $info['tmp_name'], $info['type'], $info['size'], $info['error']); + } + } + } + return self::$_files; + } + + /** + * Creates UploadedFile instances from $_FILE recursively. + * @param string $key key for identifying uploaded file: class name and sub-array indexes + * @param mixed $names file names provided by PHP + * @param mixed $tempNames temporary file names provided by PHP + * @param mixed $types file types provided by PHP + * @param mixed $sizes file sizes provided by PHP + * @param mixed $errors uploading issues provided by PHP + */ + private static function loadFilesRecursive($key, $names, $tempNames, $types, $sizes, $errors) + { + if (is_array($names)) { + foreach ($names as $i => $name) { + self::loadFilesRecursive($key . '[' . $i . ']', $name, $tempNames[$i], $types[$i], $sizes[$i], $errors[$i]); + } + } else { + self::$_files[$key] = new self($names, $tempNames, $types, $sizes, $errors); + } + } +} diff --git a/framework/web/UrlManager.php b/framework/web/UrlManager.php index 459e8e8..755d644 100644 --- a/framework/web/UrlManager.php +++ b/framework/web/UrlManager.php @@ -74,9 +74,6 @@ class UrlManager extends Component public function init() { parent::init(); - if (is_string($this->cache)) { - $this->cache = Yii::$app->getComponent($this->cache); - } $this->compileRules(); } @@ -88,6 +85,9 @@ class UrlManager extends Component if (!$this->enablePrettyUrl || $this->rules === array()) { return; } + if (is_string($this->cache)) { + $this->cache = Yii::$app->getComponent($this->cache); + } if ($this->cache instanceof Cache) { $key = $this->cache->buildKey(__CLASS__); $hash = md5(json_encode($this->rules)); @@ -104,7 +104,7 @@ class UrlManager extends Component $this->rules[$i] = Yii::createObject($rule); } - if ($this->cache instanceof Cache) { + if (isset($key, $hash)) { $this->cache->set($key, array($this->rules, $hash)); } } diff --git a/framework/web/User.php b/framework/web/User.php index 2326a10..435b606 100644 --- a/framework/web/User.php +++ b/framework/web/User.php @@ -9,24 +9,30 @@ namespace yii\web; use Yii; use yii\base\Component; +use yii\base\HttpException; use yii\base\InvalidConfigException; /** + * User is the class for the "user" application component that manages the user authentication status. + * + * In particular, [[User::isGuest]] returns a value indicating whether the current user is a guest or not. + * Through methods [[login()]] and [[logout()]], you can change the user authentication status. + * + * User works with a class implementing the [[Identity]] interface. This class implements + * the actual user authentication logic and is often backed by a user database table. + * * @author Qiang Xue * @since 2.0 */ class User extends Component { - const ID_VAR = '__id'; - const AUTH_EXPIRE_VAR = '__expire'; - const EVENT_BEFORE_LOGIN = 'beforeLogin'; const EVENT_AFTER_LOGIN = 'afterLogin'; const EVENT_BEFORE_LOGOUT = 'beforeLogout'; const EVENT_AFTER_LOGOUT = 'afterLogout'; /** - * @var string the class name of the [[identity]] object. + * @var string the class name or alias of the [[identity]] object. */ public $identityClass; /** @@ -50,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'); + 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 @@ -59,29 +65,27 @@ class User extends Component public $authTimeout; /** * @var boolean whether to automatically renew the identity cookie each time a page is requested. - * Defaults to false. This property is effective only when {@link enableAutoLogin} is true. + * This property is effective only when [[enableAutoLogin]] is true. * When this is false, the identity cookie will expire after the specified duration since the user * is initially logged in. When this is true, the identity cookie will expire after the specified duration * since the user visits the site the last time. * @see enableAutoLogin - * @since 1.1.0 */ - public $autoRenewCookie = false; + public $autoRenewCookie = true; /** - * @var string value that will be echoed in case that user session has expired during an ajax call. - * When a request is made and user session has expired, {@link loginRequired} redirects to {@link loginUrl} for login. - * If that happens during an ajax call, the complete HTML login page is returned as the result of that ajax call. That could be - * a problem if the ajax call expects the result to be a json array or a predefined string, as the login page is ignored in that case. - * To solve this, set this property to the desired return value. - * - * If this property is set, this value will be returned as the result of the ajax call in case that the user session has expired. - * @since 1.1.9 - * @see loginRequired + * @var string the session variable name used to store the value of [[id]]. + */ + public $idVar = '__id'; + /** + * @var string the session variable name used to store the value of expiration timestamp of the authenticated state. + * This is used when [[authTimeout]] is set. */ - public $loginRequiredAjaxResponse; - + public $authTimeoutVar = '__expire'; + /** + * @var string the session variable name used to store the value of [[returnUrl]]. + */ + public $returnUrlVar = '__returnUrl'; - public $stateVar = '__states'; /** * Initializes the application component. @@ -90,6 +94,9 @@ class User extends Component { parent::init(); + if ($this->identityClass === null) { + throw new InvalidConfigException('User::identityClass must be set.'); + } if ($this->enableAutoLogin && !isset($this->identityCookie['name'])) { throw new InvalidConfigException('User::identityCookie must contain the "name" element.'); } @@ -107,11 +114,15 @@ class User extends Component } } - /** - * @var Identity the identity object associated with the currently logged user. - */ private $_identity = false; + /** + * Returns the identity object associated with the currently logged user. + * @return Identity the identity object associated with the currently logged user. + * Null is returned if the user is not logged in (not authenticated). + * @see login + * @see logout + */ public function getIdentity() { if ($this->_identity === false) { @@ -120,53 +131,56 @@ class User extends Component $this->_identity = null; } else { /** @var $class Identity */ - $class = $this->identityClass; - $this->_identity = $class::findIdentity($this->getId()); + $class = Yii::import($this->identityClass); + $this->_identity = $class::findIdentity($id); } } return $this->_identity; } + /** + * Sets the identity object. + * This method should be mainly be used by the User component or its child class + * to maintain the identity object. + * + * You should normally update the user identity via methods [[login()]], [[logout()]] + * or [[switchIdentity()]]. + * + * @param Identity $identity the identity object associated with the currently logged user. + */ public function setIdentity($identity) { - $this->switchIdentity($identity); + $this->_identity = $identity; } /** * Logs in a user. * - * The user identity information will be saved in storage that is - * persistent during the user session. By default, the storage is simply - * the session storage. If the duration parameter is greater than 0, - * a cookie will be sent to prepare for cookie-based login in future. - * - * Note, you have to set {@link enableAutoLogin} to true - * if you want to allow user to be authenticated based on the cookie information. + * This method stores the necessary session information to keep track + * of the user identity information. If `$duration` is greater than 0 + * and [[enableAutoLogin]] is true, it will also send out an identity + * cookie to support cookie-based login. * * @param Identity $identity the user identity (which should already be authenticated) - * @param integer $duration number of seconds that the user can remain in logged-in status. Defaults to 0, meaning login till the user closes the browser. - * If greater than 0, cookie-based login will be used. In this case, {@link enableAutoLogin} - * must be set true, otherwise an exception will be thrown. + * @param integer $duration number of seconds that the user can remain in logged-in status. + * Defaults to 0, meaning login till the user closes the browser or the session is manually destroyed. + * If greater than 0 and [[enableAutoLogin]] is true, cookie-based login will be supported. * @return boolean whether the user is logged in */ public function login($identity, $duration = 0) { if ($this->beforeLogin($identity, false)) { - $this->switchIdentity($identity); - if ($duration > 0 && $this->enableAutoLogin) { - $this->saveIdentityCookie($identity, $duration); - } + $this->switchIdentity($identity, $duration); $this->afterLogin($identity, false); } return !$this->getIsGuest(); } /** - * Populates the current user object with the information obtained from cookie. - * This method is used when automatic login ({@link enableAutoLogin}) is enabled. - * The user identity information is recovered from cookie. - * Sufficient security measures are used to prevent cookie data from being tampered. - * @see saveIdentityCookie + * Logs in a user by cookie. + * + * This method attempts to log in a user using the ID and authKey information + * provided by the given cookie. */ protected function loginByCookie() { @@ -179,12 +193,13 @@ class User extends Component /** @var $class Identity */ $class = $this->identityClass; $identity = $class::findIdentity($id); - if ($identity !== null && $identity->validateAuthKey($authKey) && $this->beforeLogin($identity, true)) { - $this->switchIdentity($identity); - if ($this->autoRenewCookie) { - $this->saveIdentityCookie($identity, $duration); + if ($identity !== null && $identity->validateAuthKey($authKey)) { + if ($this->beforeLogin($identity, true)) { + $this->switchIdentity($identity, $this->autoRenewCookie ? $duration : 0); + $this->afterLogin($identity, true); } - $this->afterLogin($identity, true); + } elseif ($identity !== null) { + Yii::warning("Invalid auth key attempted for user '$id': $authKey", __METHOD__); } } } @@ -193,18 +208,14 @@ class User extends Component /** * Logs out the current user. * This will remove authentication-related session data. - * If the parameter is true, the whole session will be destroyed as well. - * @param boolean $destroySession whether to destroy the whole session. Defaults to true. If false, - * then {@link clearStates} will be called, which removes only the data stored via {@link setState}. + * If `$destroySession` is true, all session data will be removed. + * @param boolean $destroySession whether to destroy the whole session. Defaults to true. */ public function logout($destroySession = true) { $identity = $this->getIdentity(); if ($identity !== null && $this->beforeLogout($identity)) { $this->switchIdentity(null); - if ($this->enableAutoLogin) { - Yii::$app->getResponse()->getCookies()->remove(new Cookie($this->identityCookie)); - } if ($destroySession) { Yii::$app->getSession()->destroy(); } @@ -223,97 +234,74 @@ class User extends Component /** * Returns a value that uniquely represents the user. - * @return mixed the unique identifier for the user. If null, it means the user is a guest. + * @return string|integer the unique identifier for the user. If null, it means the user is a guest. */ public function getId() { - return $this->getState(static::ID_VAR); - } - - /** - * @param mixed $value the unique identifier for the user. If null, it means the user is a guest. - */ - public function setId($value) - { - $this->setState(static::ID_VAR, $value); + return Yii::$app->getSession()->get($this->idVar); } /** * Returns the URL that the user should be redirected to after successful login. * This property is usually used by the login action. If the login is successful, * the action should read this property and use it to redirect the user browser. - * @param string $defaultUrl the default return URL in case it was not set previously. If this is null, - * the application entry URL will be considered as the default return URL. + * @param string|array $defaultUrl the default return URL in case it was not set previously. + * If this is null, it means [[Application::homeUrl]] will be redirected to. + * Please refer to [[\yii\helpers\Html::url()]] on acceptable URL formats. * @return string the URL that the user should be redirected to after login. * @see loginRequired */ public function getReturnUrl($defaultUrl = null) { - if ($defaultUrl === null) { - $defaultReturnUrl = Yii::app()->getUrlManager()->showScriptName ? Yii::app()->getRequest()->getScriptUrl() : Yii::app()->getRequest()->getBaseUrl() . '/'; - } else { - $defaultReturnUrl = CHtml::normalizeUrl($defaultUrl); - } - return $this->getState('__returnUrl', $defaultReturnUrl); + $url = Yii::$app->getSession()->get($this->returnUrlVar, $defaultUrl); + return $url === null ? Yii::$app->getHomeUrl() : $url; } /** - * @param string $value the URL that the user should be redirected to after login. + * @param string|array $url the URL that the user should be redirected to after login. + * Please refer to [[\yii\helpers\Html::url()]] on acceptable URL formats. */ - public function setReturnUrl($value) + public function setReturnUrl($url) { - $this->setState('__returnUrl', $value); + Yii::$app->getSession()->set($this->returnUrlVar, $url); } /** * Redirects the user browser to the login page. * Before the redirection, the current URL (if it's not an AJAX url) will be - * kept in {@link returnUrl} so that the user browser may be redirected back - * to the current page after successful login. Make sure you set {@link loginUrl} + * kept as [[returnUrl]] so that the user browser may be redirected back + * to the current page after successful login. Make sure you set [[loginUrl]] * so that the user browser can be redirected to the specified login URL after * calling this method. * After calling this method, the current request processing will be terminated. */ public function loginRequired() { - $app = Yii::app(); - $request = $app->getRequest(); - + $request = Yii::$app->getRequest(); if (!$request->getIsAjaxRequest()) { $this->setReturnUrl($request->getUrl()); - } elseif (isset($this->loginRequiredAjaxResponse)) { - echo $this->loginRequiredAjaxResponse; - Yii::app()->end(); } - - if (($url = $this->loginUrl) !== null) { - if (is_array($url)) { - $route = isset($url[0]) ? $url[0] : $app->defaultController; - $url = $app->createUrl($route, array_splice($url, 1)); - } - $request->redirect($url); + if ($this->loginUrl !== null) { + Yii::$app->getResponse()->redirect($this->loginUrl); } else { - throw new CHttpException(403, Yii::t('yii', 'Login Required')); + throw new HttpException(403, Yii::t('yii|Login Required')); } } /** * This method is called before logging in a user. - * You may override this method to provide additional security check. - * For example, when the login is cookie-based, you may want to verify - * that the user ID together with a random token in the states can be found - * in the database. This will prevent hackers from faking arbitrary - * identity cookies even if they crack down the server private key. - * @param mixed $id the user ID. This is the same as returned by {@link getId()}. - * @param array $states a set of name-value pairs that are provided by the user identity. - * @param boolean $fromCookie whether the login is based on cookie - * @return boolean whether the user should be logged in - */ - protected function beforeLogin($identity, $fromCookie) + * The default implementation will trigger the [[EVENT_BEFORE_LOGIN]] event. + * If you override this method, make sure you call the parent implementation + * so that the event is triggered. + * @param Identity $identity the user identity information + * @param boolean $cookieBased whether the login is cookie-based + * @return boolean whether the user should continue to be logged in + */ + protected function beforeLogin($identity, $cookieBased) { $event = new UserEvent(array( 'identity' => $identity, - 'fromCookie' => $fromCookie, + 'cookieBased' => $cookieBased, )); $this->trigger(self::EVENT_BEFORE_LOGIN, $event); return $event->isValid; @@ -321,24 +309,27 @@ class User extends Component /** * This method is called after the user is successfully logged in. - * You may override this method to do some postprocessing (e.g. log the user - * login IP and time; load the user permission information). - * @param boolean $fromCookie whether the login is based on cookie. + * The default implementation will trigger the [[EVENT_AFTER_LOGIN]] event. + * If you override this method, make sure you call the parent implementation + * so that the event is triggered. + * @param Identity $identity the user identity information + * @param boolean $cookieBased whether the login is cookie-based */ - protected function afterLogin($identity, $fromCookie) + protected function afterLogin($identity, $cookieBased) { $this->trigger(self::EVENT_AFTER_LOGIN, new UserEvent(array( 'identity' => $identity, - 'fromCookie' => $fromCookie, + 'cookieBased' => $cookieBased, ))); } /** - * This method is invoked when calling {@link logout} to log out a user. - * If this method return false, the logout action will be cancelled. - * You may override this method to provide additional check before - * logging out a user. - * @return boolean whether to log out the user + * This method is invoked when calling [[logout()]] to log out a user. + * The default implementation will trigger the [[EVENT_BEFORE_LOGOUT]] event. + * If you override this method, make sure you call the parent implementation + * so that the event is triggered. + * @param Identity $identity the user identity information + * @return boolean whether the user should continue to be logged out */ protected function beforeLogout($identity) { @@ -350,8 +341,11 @@ class User extends Component } /** - * This method is invoked right after a user is logged out. - * You may override this method to do some extra cleanup work for the user. + * This method is invoked right after a user is logged out via [[logout()]]. + * The default implementation will trigger the [[EVENT_AFTER_LOGOUT]] event. + * If you override this method, make sure you call the parent implementation + * so that the event is triggered. + * @param Identity $identity the user identity information */ protected function afterLogout($identity) { @@ -360,7 +354,6 @@ class User extends Component ))); } - /** * Renews the identity cookie. * This method will set the expiration time of the identity cookie to be the current time @@ -382,15 +375,15 @@ class User extends Component } /** - * Saves necessary user data into a cookie. - * This method is used when automatic login ({@link enableAutoLogin}) is enabled. - * This method saves user ID, username, other identity states and a validation key to cookie. - * These information are used to do authentication next time when user visits the application. + * Sends an identity cookie. + * This method is used when [[enableAutoLogin]] is true. + * It saves [[id]], [[Identity::getAuthKey()|auth key]], and the duration of cookie-based login + * information in the cookie. * @param Identity $identity - * @param integer $duration number of seconds that the user can remain in logged-in status. Defaults to 0, meaning login till the user closes the browser. + * @param integer $duration number of seconds that the user can remain in logged-in status. * @see loginByCookie */ - protected function saveIdentityCookie($identity, $duration) + protected function sendIdentityCookie($identity, $duration) { $cookie = new Cookie($this->identityCookie); $cookie->value = json_encode(array( @@ -403,145 +396,55 @@ class User extends Component } /** - * Changes the current user with the specified identity information. - * This method is called by {@link login} and {@link restoreFromCookie} - * when the current user needs to be populated with the corresponding - * identity information. Derived classes may override this method - * by retrieving additional user-related information. Make sure the - * parent implementation is called first. - * @param Identity $identity a unique identifier for the user + * Switches to a new identity for the current user. + * + * This method will save necessary session information to keep track of the user authentication status. + * If `$duration` is provided, it will also send out appropriate identity cookie + * to support cookie-based login. + * + * This method is mainly called by [[login()]], [[logout()]] and [[loginByCookie()]] + * when the current user needs to be associated with the corresponding identity information. + * + * @param Identity $identity the identity information to be associated with the current user. + * If null, it means switching to be a guest. + * @param integer $duration number of seconds that the user can remain in logged-in status. + * This parameter is used only when `$identity` is not null. */ - protected function switchIdentity($identity) + public function switchIdentity($identity, $duration = 0) { - Yii::$app->getSession()->regenerateID(true); + $session = Yii::$app->getSession(); + $session->regenerateID(true); $this->setIdentity($identity); + $session->remove($this->idVar); + $session->remove($this->authTimeoutVar); if ($identity instanceof Identity) { - $this->setId($identity->getId()); + $session->set($this->idVar, $identity->getId()); if ($this->authTimeout !== null) { - $this->setState(self::AUTH_EXPIRE_VAR, time() + $this->authTimeout); + $session->set($this->authTimeoutVar, time() + $this->authTimeout); } - } else { - $this->removeAllStates(); + if ($duration > 0 && $this->enableAutoLogin) { + $this->sendIdentityCookie($identity, $duration); + } + } elseif ($this->enableAutoLogin) { + Yii::$app->getResponse()->getCookies()->remove(new Cookie($this->identityCookie)); } } /** - * Updates the authentication status according to {@link authTimeout}. - * If the user has been inactive for {@link authTimeout} seconds, - * he will be automatically logged out. + * Updates the authentication status according to [[authTimeout]]. + * This method is called during [[init()]]. + * It will update the user's authentication status if it has not outdated yet. + * Otherwise, it will logout the user. */ protected function renewAuthStatus() { if ($this->authTimeout !== null && !$this->getIsGuest()) { - $expire = $this->getState(self::AUTH_EXPIRE_VAR); + $expire = Yii::$app->getSession()->get($this->authTimeoutVar); if ($expire !== null && $expire < time()) { $this->logout(false); } else { - $this->setState(self::AUTH_EXPIRE_VAR, time() + $this->authTimeout); - } - } - } - - /** - * Returns a user state. - * A user state is a session data item associated with the current user. - * If the user logs out, all his/her user states will be removed. - * @param string $key the key identifying the state - * @param mixed $defaultValue value to be returned if the state does not exist. - * @return mixed the state - */ - public function getState($key, $defaultValue = null) - { - $manifest = isset($_SESSION[$this->stateVar]) ? $_SESSION[$this->stateVar] : null; - if (is_array($manifest) && isset($manifest[$key], $_SESSION[$key])) { - return $_SESSION[$key]; - } else { - return $defaultValue; - } - } - - /** - * Returns all user states. - * @return array states (key => state). - */ - public function getAllStates() - { - $manifest = isset($_SESSION[$this->stateVar]) ? $_SESSION[$this->stateVar] : null; - $states = array(); - if (is_array($manifest)) { - foreach (array_keys($manifest) as $key) { - if (isset($_SESSION[$key])) { - $states[$key] = $_SESSION[$key]; - } + Yii::$app->getSession()->set($this->authTimeoutVar, time() + $this->authTimeout); } } - return $states; - } - - /** - * Stores a user state. - * A user state is a session data item associated with the current user. - * If the user logs out, all his/her user states will be removed. - * @param string $key the key identifying the state. Note that states - * and normal session variables share the same name space. If you have a normal - * session variable using the same name, its value will be overwritten by this method. - * @param mixed $value state - */ - public function setState($key, $value) - { - $manifest = isset($_SESSION[$this->stateVar]) ? $_SESSION[$this->stateVar] : array(); - $manifest[$value] = true; - $_SESSION[$key] = $value; - $_SESSION[$this->stateVar] = $manifest; - } - - /** - * Removes a user state. - * If the user logs out, all his/her user states will be removed automatically. - * @param string $key the key identifying the state. Note that states - * and normal session variables share the same name space. If you have a normal - * session variable using the same name, it will be removed by this method. - * @return mixed the removed state. Null if the state does not exist. - */ - public function removeState($key) - { - $manifest = isset($_SESSION[$this->stateVar]) ? $_SESSION[$this->stateVar] : null; - if (is_array($manifest) && isset($manifest[$key], $_SESSION[$key])) { - $value = $_SESSION[$key]; - } else { - $value = null; - } - unset($_SESSION[$this->stateVar][$key], $_SESSION[$key]); - return $value; - } - - /** - * Removes all states. - * If the user logs out, all his/her user states will be removed automatically - * without the need to call this method manually. - * - * Note that states and normal session variables share the same name space. - * If you have a normal session variable using the same name, it will be removed - * by this method. - */ - public function removeAllStates() - { - $manifest = isset($_SESSION[$this->stateVar]) ? $_SESSION[$this->stateVar] : null; - if (is_array($manifest)) { - foreach (array_keys($manifest) as $key) { - unset($_SESSION[$key]); - } - } - unset($_SESSION[$this->stateVar]); - } - - /** - * Returns a value indicating whether there is a state associated with the specified key. - * @param string $key key identifying the state - * @return boolean whether the specified state exists - */ - public function hasState($key) - { - return $this->getState($key) !== null; } } diff --git a/framework/web/UserEvent.php b/framework/web/UserEvent.php index 3a8723a..7a5d23d 100644 --- a/framework/web/UserEvent.php +++ b/framework/web/UserEvent.php @@ -24,11 +24,11 @@ class UserEvent extends Event * @var boolean whether the login is cookie-based. This property is only meaningful * for [[User::EVENT_BEFORE_LOGIN]] and [[User::EVENT_AFTER_LOGIN]] events. */ - public $fromCookie; + public $cookieBased; /** * @var boolean whether the login or logout should proceed. * Event handlers may modify this property to determine whether the login or logout should proceed. * This property is only meaningful for [[User::EVENT_BEFORE_LOGIN]] and [[User::EVENT_BEFORE_LOGOUT]] events. */ - public $isValid; + public $isValid = true; } \ No newline at end of file diff --git a/framework/widgets/ActiveForm.php b/framework/widgets/ActiveForm.php index 2c965e7..83506dd 100644 --- a/framework/widgets/ActiveForm.php +++ b/framework/widgets/ActiveForm.php @@ -39,10 +39,16 @@ class ActiveForm extends Widget public $errorMessageClass = 'yii-error-message'; /** * @var string the default CSS class that indicates an input has error. - * This is */ public $errorClass = 'yii-error'; + /** + * @var string the default CSS class that indicates an input validated successfully. + */ public $successClass = 'yii-success'; + + /** + * @var string the default CSS class that indicates an input is currently being validated. + */ public $validatingClass = 'yii-validating'; /** * @var boolean whether to enable client-side data validation. Defaults to false. @@ -52,10 +58,6 @@ class ActiveForm extends Widget public $enableClientValidation = false; public $options = array(); - /** - * @var array model-class mapped to name prefix - */ - public $modelMap; /** * @param Model|Model[] $models @@ -68,7 +70,7 @@ class ActiveForm extends Widget $models = array($models); } - $showAll = isset($options['showAll']) && $options['showAll']; + $showAll = !empty($options['showAll']); $lines = array(); /** @var $model Model */ foreach ($models as $model) { @@ -110,8 +112,7 @@ class ActiveForm extends Widget */ public function error($model, $attribute, $options = array()) { - $attribute = $this->normalizeAttributeName($attribute); - $this->getInputName($model, $attribute); + $attribute = $this->getAttributeName($attribute); $tag = isset($options['tag']) ? $options['tag'] : 'div'; unset($options['tag']); $error = $model->getFirstError($attribute); @@ -126,15 +127,27 @@ class ActiveForm extends Widget */ public function label($model, $attribute, $options = array()) { - $attribute = $this->normalizeAttributeName($attribute); - $label = $model->getAttributeLabel($attribute); - return Html::label(Html::encode($label), isset($options['for']) ? $options['for'] : null, $options); + $attribute = $this->getAttributeName($attribute); + $label = isset($options['label']) ? $options['label'] : Html::encode($model->getAttributeLabel($attribute)); + $for = array_key_exists('for', $options) ? $options['for'] : $this->getInputId($model, $attribute); + return Html::label($label, $for, $options); } + /** + * @param string $type + * @param Model $model + * @param string $attribute + * @param array $options + * + * @return string + */ public function input($type, $model, $attribute, $options = array()) { $value = $this->getAttributeValue($model, $attribute); $name = $this->getInputName($model, $attribute); + if (!array_key_exists('id', $options)) { + $options['id'] = $this->getInputId($model, $attribute); + } return Html::input($type, $name, $value, $options); } @@ -162,6 +175,9 @@ class ActiveForm extends Widget { $value = $this->getAttributeValue($model, $attribute); $name = $this->getInputName($model, $attribute); + if (!array_key_exists('id', $options)) { + $options['id'] = $this->getInputId($model, $attribute); + } return Html::textarea($name, $value, $options); } @@ -172,6 +188,9 @@ class ActiveForm extends Widget if (!array_key_exists('uncheck', $options)) { $options['unchecked'] = '0'; } + if (!array_key_exists('id', $options)) { + $options['id'] = $this->getInputId($model, $attribute); + } return Html::radio($name, $checked, $value, $options); } @@ -182,6 +201,9 @@ class ActiveForm extends Widget if (!array_key_exists('uncheck', $options)) { $options['unchecked'] = '0'; } + if (!array_key_exists('id', $options)) { + $options['id'] = $this->getInputId($model, $attribute); + } return Html::checkbox($name, $checked, $value, $options); } @@ -189,6 +211,9 @@ class ActiveForm extends Widget { $checked = $this->getAttributeValue($model, $attribute); $name = $this->getInputName($model, $attribute); + if (!array_key_exists('id', $options)) { + $options['id'] = $this->getInputId($model, $attribute); + } return Html::dropDownList($name, $checked, $items, $options); } @@ -199,6 +224,9 @@ class ActiveForm extends Widget if (!array_key_exists('unselect', $options)) { $options['unselect'] = '0'; } + if (!array_key_exists('id', $options)) { + $options['id'] = $this->getInputId($model, $attribute); + } return Html::listBox($name, $checked, $items, $options); } @@ -222,29 +250,6 @@ class ActiveForm extends Widget return Html::radioList($name, $checked, $items, $options); } - public function getInputName($model, $attribute) - { - $class = get_class($model); - if (isset($this->modelMap[$class])) { - $class = $this->modelMap[$class]; - } elseif (($pos = strrpos($class, '\\')) !== false) { - $class = substr($class, $pos); - } - if (!preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) { - throw new InvalidParamException('Attribute name must contain word characters only.'); - } - $prefix = $matches[1]; - $attribute = $matches[2]; - $suffix = $matches[3]; - if ($class === '' && $prefix === '') { - return $attribute . $suffix; - } elseif ($class !== '') { - return $class . $prefix . "[$attribute]" . $suffix; - } else { - throw new InvalidParamException('Model name cannot be mapped to empty for tabular inputs.'); - } - } - public function getAttributeValue($model, $attribute) { if (!preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) { @@ -267,7 +272,7 @@ class ActiveForm extends Widget } } - public function normalizeAttributeName($attribute) + public function getAttributeName($attribute) { if (preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) { return $matches[2]; @@ -275,4 +280,34 @@ class ActiveForm extends Widget throw new InvalidParamException('Attribute name must contain word characters only.'); } } + + /** + * @param Model $model + * @param string $attribute + * @return string + * @throws \yii\base\InvalidParamException + */ + public static function getInputName($model, $attribute) + { + $formName = $model->formName(); + if (!preg_match('/(^|.*\])(\w+)(\[.*|$)/', $attribute, $matches)) { + throw new InvalidParamException('Attribute name must contain word characters only.'); + } + $prefix = $matches[1]; + $attribute = $matches[2]; + $suffix = $matches[3]; + if ($formName === '' && $prefix === '') { + return $attribute . $suffix; + } elseif ($formName !== '') { + return $formName . $prefix . "[$attribute]" . $suffix; + } else { + throw new InvalidParamException(get_class($model) . '::formName() cannot be empty for tabular inputs.'); + } + } + + public static function getInputId($model, $attribute) + { + $name = static::getInputName($model, $attribute); + return str_replace(array('[]', '][', '[', ']', ' '), array('', '-', '-', '', '-'), $name); + } } diff --git a/framework/widgets/Block.php b/framework/widgets/Block.php new file mode 100644 index 0000000..d6f7317 --- /dev/null +++ b/framework/widgets/Block.php @@ -0,0 +1,49 @@ + + * @since 2.0 + */ +class Block extends Widget +{ + /** + * @var string the ID of this block. + */ + public $id; + /** + * @var boolean whether to render the block content in place. Defaults to false, + * meaning the captured block content will not be displayed. + */ + public $renderInPlace = false; + + /** + * Starts recording a block. + */ + public function init() + { + ob_start(); + ob_implicit_flush(false); + } + + /** + * Ends recording a block. + * This method stops output buffering and saves the rendering result as a named block in the controller. + */ + public function run() + { + $block = ob_get_clean(); + if ($this->renderInPlace) { + echo $block; + } + $this->view->blocks[$this->id] = $block; + } +} \ No newline at end of file diff --git a/framework/widgets/Clip.php b/framework/widgets/Clip.php deleted file mode 100644 index d540b24..0000000 --- a/framework/widgets/Clip.php +++ /dev/null @@ -1,57 +0,0 @@ - - * @since 2.0 - */ -class Clip extends Widget -{ - /** - * @var string the ID of this clip. - */ - public $id; - /** - * @var View the view object for keeping the clip. If not set, the view registered with the application - * will be used. - */ - public $view; - /** - * @var boolean whether to render the clip content in place. Defaults to false, - * meaning the captured clip will not be displayed. - */ - public $renderInPlace = false; - - /** - * Starts recording a clip. - */ - public function init() - { - ob_start(); - ob_implicit_flush(false); - } - - /** - * Ends recording a clip. - * This method stops output buffering and saves the rendering result as a named clip in the controller. - */ - public function run() - { - $clip = ob_get_clean(); - if ($this->renderClip) { - echo $clip; - } - $view = $this->view !== null ? $this->view : Yii::$app->getView(); - $view->clips[$this->id] = $clip; - } -} \ No newline at end of file diff --git a/framework/widgets/ContentDecorator.php b/framework/widgets/ContentDecorator.php index 4c3ae70..3f63621 100644 --- a/framework/widgets/ContentDecorator.php +++ b/framework/widgets/ContentDecorator.php @@ -7,10 +7,8 @@ namespace yii\widgets; -use Yii; use yii\base\InvalidConfigException; use yii\base\Widget; -use yii\base\View; /** * @author Qiang Xue @@ -19,15 +17,10 @@ use yii\base\View; class ContentDecorator extends Widget { /** - * @var View the view object for rendering [[viewName]]. If not set, the view registered with the application - * will be used. + * @var string the view file that will be used to decorate the content enclosed by this widget. + * This can be specified as either the view file path or path alias. */ - public $view; - /** - * @var string the name of the view that will be used to decorate the content enclosed by this widget. - * Please refer to [[View::findViewFile()]] on how to set this property. - */ - public $viewName; + public $viewFile; /** * @var array the parameters (name=>value) to be extracted and made available in the decorative view. */ @@ -38,8 +31,8 @@ class ContentDecorator extends Widget */ public function init() { - if ($this->viewName === null) { - throw new InvalidConfigException('ContentDecorator::viewName must be set.'); + if ($this->viewFile === null) { + throw new InvalidConfigException('ContentDecorator::viewFile must be set.'); } ob_start(); ob_implicit_flush(false); @@ -53,7 +46,7 @@ class ContentDecorator extends Widget { $params = $this->params; $params['content'] = ob_get_clean(); - $view = $this->view !== null ? $this->view : Yii::$app->getView(); - echo $view->render($this->viewName, $params); + // render under the existing context + echo $this->view->renderFile($this->viewFile, $params); } } diff --git a/framework/widgets/FragmentCache.php b/framework/widgets/FragmentCache.php index 65bb86b..637d115 100644 --- a/framework/widgets/FragmentCache.php +++ b/framework/widgets/FragmentCache.php @@ -8,7 +8,6 @@ namespace yii\widgets; use Yii; -use yii\base\InvalidConfigException; use yii\base\Widget; use yii\caching\Cache; use yii\caching\Dependency; @@ -64,11 +63,6 @@ class FragmentCache extends Widget */ public $enabled = true; /** - * @var \yii\base\View the view object within which this widget is used. If not set, - * the view registered with the application will be used. This is mainly used by dynamic content feature. - */ - public $view; - /** * @var array a list of placeholders for embedding dynamic contents. This property * is used internally to implement the content caching feature. Do not modify it. */ @@ -81,10 +75,6 @@ class FragmentCache extends Widget { parent::init(); - if ($this->view === null) { - $this->view = Yii::$app->getView(); - } - if (!$this->enabled) { $this->cache = null; } elseif (is_string($this->cache)) { diff --git a/framework/yiic.php b/framework/yiic.php index 0db69bb..3872e2f 100644 --- a/framework/yiic.php +++ b/framework/yiic.php @@ -14,10 +14,9 @@ defined('STDIN') or define('STDIN', fopen('php://stdin', 'r')); require(__DIR__ . '/yii.php'); -$id = 'yiic'; -$basePath = __DIR__ . '/console'; - -$application = new yii\console\Application($id, $basePath, array( +$application = new yii\console\Application(array( + 'id' => 'yiic', + 'basePath' => __DIR__ . '/console', 'controllerPath' => '@yii/console/controllers', )); $application->run(); diff --git a/tests/unit/MysqlTestCase.php b/tests/unit/MysqlTestCase.php index d62f95e..e1a1f7e 100644 --- a/tests/unit/MysqlTestCase.php +++ b/tests/unit/MysqlTestCase.php @@ -4,7 +4,7 @@ namespace yiiunit; class MysqlTestCase extends TestCase { - function __construct() + protected function setUp() { if (!extension_loaded('pdo') || !extension_loaded('pdo_mysql')) { $this->markTestSkipped('pdo and pdo_mysql extensions are required.'); @@ -15,7 +15,7 @@ class MysqlTestCase extends TestCase * @param bool $reset whether to clean up the test database * @return \yii\db\Connection */ - function getConnection($reset = true) + public function getConnection($reset = true) { $params = $this->getParam('mysql'); $db = new \yii\db\Connection; diff --git a/tests/unit/bootstrap.php b/tests/unit/bootstrap.php index 4a388c6..8290bbe 100644 --- a/tests/unit/bootstrap.php +++ b/tests/unit/bootstrap.php @@ -9,4 +9,6 @@ require_once(__DIR__ . '/../../framework/yii.php'); Yii::setAlias('@yiiunit', __DIR__); +new \yii\web\Application(array('id' => 'testapp', 'basePath' => __DIR__)); + require_once(__DIR__ . '/TestCase.php'); diff --git a/tests/unit/framework/YiiBaseTest.php b/tests/unit/framework/YiiBaseTest.php index df12bf9..47474f2 100644 --- a/tests/unit/framework/YiiBaseTest.php +++ b/tests/unit/framework/YiiBaseTest.php @@ -1,6 +1,7 @@ aliases = Yii::$aliases; + } + + public function tearDown() + { + Yii::$aliases = $this->aliases; + } + public function testAlias() { + $this->assertEquals(YII_PATH, Yii::getAlias('@yii')); + + Yii::$aliases = array(); + $this->assertFalse(Yii::getAlias('@yii', false)); + + Yii::setAlias('@yii', '/yii/framework'); + $this->assertEquals('/yii/framework', Yii::getAlias('@yii')); + $this->assertEquals('/yii/framework/test/file', Yii::getAlias('@yii/test/file')); + Yii::setAlias('@yii/gii', '/yii/gii'); + $this->assertEquals('/yii/framework', Yii::getAlias('@yii')); + $this->assertEquals('/yii/framework/test/file', Yii::getAlias('@yii/test/file')); + $this->assertEquals('/yii/gii', Yii::getAlias('@yii/gii')); + $this->assertEquals('/yii/gii/file', Yii::getAlias('@yii/gii/file')); + + Yii::setAlias('@tii', '@yii/test'); + $this->assertEquals('/yii/framework/test', Yii::getAlias('@tii')); + Yii::setAlias('@yii', null); + $this->assertFalse(Yii::getAlias('@yii', false)); + $this->assertEquals('/yii/gii/file', Yii::getAlias('@yii/gii/file')); } public function testGetVersion() { - echo \Yii::getVersion(); + echo Yii::getVersion(); $this->assertTrue((boolean)preg_match('~\d+\.\d+(?:\.\d+)?(?:-\w+)?~', \Yii::getVersion())); } public function testPowered() { - $this->assertTrue(is_string(\Yii::powered())); + $this->assertTrue(is_string(Yii::powered())); } } diff --git a/tests/unit/framework/base/ComponentTest.php b/tests/unit/framework/base/ComponentTest.php index 97b0116..74b6e9a 100644 --- a/tests/unit/framework/base/ComponentTest.php +++ b/tests/unit/framework/base/ComponentTest.php @@ -41,12 +41,12 @@ class ComponentTest extends TestCase $component->attachBehavior('a', $behavior); $this->assertSame($behavior, $component->getBehavior('a')); $component->on('test', 'fake'); - $this->assertEquals(1, $component->getEventHandlers('test')->count); + $this->assertTrue($component->hasEventHandlers('test')); $clone = clone $component; $this->assertNotSame($component, $clone); $this->assertNull($clone->getBehavior('a')); - $this->assertEquals(0, $clone->getEventHandlers('test')->count); + $this->assertFalse($clone->hasEventHandlers('test')); } public function testHasProperty() @@ -151,34 +151,32 @@ class ComponentTest extends TestCase public function testOn() { - $this->assertEquals(0, $this->component->getEventHandlers('click')->getCount()); + $this->assertFalse($this->component->hasEventHandlers('click')); $this->component->on('click', 'foo'); - $this->assertEquals(1, $this->component->getEventHandlers('click')->getCount()); - $this->component->on('click', 'bar'); - $this->assertEquals(2, $this->component->getEventHandlers('click')->getCount()); - $p = 'on click'; - $this->component->$p = 'foo2'; - $this->assertEquals(3, $this->component->getEventHandlers('click')->getCount()); + $this->assertTrue($this->component->hasEventHandlers('click')); - $this->component->getEventHandlers('click')->add('test'); - $this->assertEquals(4, $this->component->getEventHandlers('click')->getCount()); + $this->assertFalse($this->component->hasEventHandlers('click2')); + $p = 'on click2'; + $this->component->$p = 'foo2'; + $this->assertTrue($this->component->hasEventHandlers('click2')); } public function testOff() { + $this->assertFalse($this->component->hasEventHandlers('click')); $this->component->on('click', 'foo'); - $this->component->on('click', array($this->component, 'myEventHandler')); - $this->assertEquals(2, $this->component->getEventHandlers('click')->getCount()); - - $result = $this->component->off('click', 'foo'); - $this->assertTrue($result); - $this->assertEquals(1, $this->component->getEventHandlers('click')->getCount()); - $result = $this->component->off('click', 'foo'); - $this->assertFalse($result); - $this->assertEquals(1, $this->component->getEventHandlers('click')->getCount()); - $result = $this->component->off('click', array($this->component, 'myEventHandler')); - $this->assertTrue($result); - $this->assertEquals(0, $this->component->getEventHandlers('click')->getCount()); + $this->assertTrue($this->component->hasEventHandlers('click')); + $this->component->off('click', 'foo'); + $this->assertFalse($this->component->hasEventHandlers('click')); + + $this->component->on('click2', 'foo'); + $this->component->on('click2', 'foo2'); + $this->component->on('click2', 'foo3'); + $this->assertTrue($this->component->hasEventHandlers('click2')); + $this->component->off('click2', 'foo3'); + $this->assertTrue($this->component->hasEventHandlers('click2')); + $this->component->off('click2'); + $this->assertFalse($this->component->hasEventHandlers('click2')); } public function testTrigger() diff --git a/tests/unit/framework/base/ModelTest.php b/tests/unit/framework/base/ModelTest.php index aa15230..f04e550 100644 --- a/tests/unit/framework/base/ModelTest.php +++ b/tests/unit/framework/base/ModelTest.php @@ -195,7 +195,7 @@ class ModelTest extends TestCase public function testCreateValidators() { - $this->setExpectedException('yii\base\InvalidConfigException', 'Invalid validation rule: a rule must be an array specifying both attribute names and validator type.'); + $this->setExpectedException('yii\base\InvalidConfigException', 'Invalid validation rule: a rule must specify both attribute names and validator type.'); $invalid = new InvalidRulesModel(); $invalid->createValidators(); diff --git a/tests/unit/framework/caching/CacheTest.php b/tests/unit/framework/caching/CacheTest.php index ad2fcf5..f9db4f4 100644 --- a/tests/unit/framework/caching/CacheTest.php +++ b/tests/unit/framework/caching/CacheTest.php @@ -16,9 +16,9 @@ abstract class CacheTest extends TestCase public function testSet() { $cache = $this->getCacheInstance(); - $cache->set('string_test', 'string_test'); - $cache->set('number_test', 42); - $cache->set('array_test', array('array_test' => 'array_test')); + $this->assertTrue($cache->set('string_test', 'string_test')); + $this->assertTrue($cache->set('number_test', 42)); + $this->assertTrue($cache->set('array_test', array('array_test' => 'array_test'))); $cache['arrayaccess_test'] = new \stdClass(); } @@ -45,7 +45,7 @@ abstract class CacheTest extends TestCase public function testExpire() { $cache = $this->getCacheInstance(); - $cache->set('expire_test', 'expire_test', 2); + $this->assertTrue($cache->set('expire_test', 'expire_test', 2)); sleep(1); $this->assertEquals('expire_test', $cache->get('expire_test')); sleep(2); @@ -57,11 +57,11 @@ abstract class CacheTest extends TestCase $cache = $this->getCacheInstance(); // should not change existing keys - $cache->add('number_test', 13); + $this->assertFalse($cache->add('number_test', 13)); $this->assertEquals(42, $cache->get('number_test')); // should store data is it's not there yet - $cache->add('add_test', 13); + $this->assertTrue($cache->add('add_test', 13)); $this->assertEquals(13, $cache->get('add_test')); } @@ -69,14 +69,14 @@ abstract class CacheTest extends TestCase { $cache = $this->getCacheInstance(); - $cache->delete('number_test'); + $this->assertTrue($cache->delete('number_test')); $this->assertEquals(null, $cache->get('number_test')); } public function testFlush() { $cache = $this->getCacheInstance(); - $cache->flush(); + $this->assertTrue($cache->flush()); $this->assertEquals(null, $cache->get('add_test')); } } diff --git a/tests/unit/framework/caching/DbCacheTest.php b/tests/unit/framework/caching/DbCacheTest.php index 3977ee8..594e946 100644 --- a/tests/unit/framework/caching/DbCacheTest.php +++ b/tests/unit/framework/caching/DbCacheTest.php @@ -11,7 +11,7 @@ class DbCacheTest extends CacheTest private $_cacheInstance; private $_connection; - function __construct() + protected function setUp() { if (!extension_loaded('pdo') || !extension_loaded('pdo_mysql')) { $this->markTestSkipped('pdo and pdo_mysql extensions are required.'); diff --git a/tests/unit/framework/db/ConnectionTest.php b/tests/unit/framework/db/ConnectionTest.php index afb4f20..256c5a9 100644 --- a/tests/unit/framework/db/ConnectionTest.php +++ b/tests/unit/framework/db/ConnectionTest.php @@ -59,7 +59,6 @@ class ConnectionTest extends \yiiunit\MysqlTestCase $this->assertEquals('`table`', $connection->quoteTableName('`table`')); $this->assertEquals('`schema`.`table`', $connection->quoteTableName('schema.table')); $this->assertEquals('`schema`.`table`', $connection->quoteTableName('schema.`table`')); - $this->assertEquals('[[table]]', $connection->quoteTableName('[[table]]')); $this->assertEquals('{{table}}', $connection->quoteTableName('{{table}}')); $this->assertEquals('(table)', $connection->quoteTableName('(table)')); } diff --git a/tests/unit/framework/util/ArrayHelperTest.php b/tests/unit/framework/helpers/ArrayHelperTest.php similarity index 97% rename from tests/unit/framework/util/ArrayHelperTest.php rename to tests/unit/framework/helpers/ArrayHelperTest.php index 117c702..187217f 100644 --- a/tests/unit/framework/util/ArrayHelperTest.php +++ b/tests/unit/framework/helpers/ArrayHelperTest.php @@ -1,6 +1,6 @@ 'test', + 'basePath' => '@yiiunit/runtime', 'components' => array( 'request' => array( 'class' => 'yii\web\Request', @@ -20,6 +22,14 @@ class HtmlTest extends \yii\test\TestCase )); } + public function assertEqualsWithoutLE($expected, $actual) + { + $expected = str_replace("\r\n", "\n", $expected); + $actual = str_replace("\r\n", "\n", $actual); + + $this->assertEquals($expected, $actual); + } + public function tearDown() { Yii::$app = null; @@ -238,21 +248,21 @@ class HtmlTest extends \yii\test\TestCase EOD; - $this->assertEquals($expected, Html::dropDownList('test')); + $this->assertEqualsWithoutLE($expected, Html::dropDownList('test')); $expected = << EOD; - $this->assertEquals($expected, Html::dropDownList('test', null, $this->getDataItems())); + $this->assertEqualsWithoutLE($expected, Html::dropDownList('test', null, $this->getDataItems())); $expected = << EOD; - $this->assertEquals($expected, Html::dropDownList('test', 'value2', $this->getDataItems())); + $this->assertEqualsWithoutLE($expected, Html::dropDownList('test', 'value2', $this->getDataItems())); } public function testListBox() @@ -262,48 +272,48 @@ EOD; EOD; - $this->assertEquals($expected, Html::listBox('test')); + $this->assertEqualsWithoutLE($expected, Html::listBox('test')); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems(), array('size' => 5))); + $this->assertEqualsWithoutLE($expected, Html::listBox('test', null, $this->getDataItems(), array('size' => 5))); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', null, $this->getDataItems2())); + $this->assertEqualsWithoutLE($expected, Html::listBox('test', null, $this->getDataItems2())); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', 'value2', $this->getDataItems())); + $this->assertEqualsWithoutLE($expected, Html::listBox('test', 'value2', $this->getDataItems())); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', array('value1', 'value2'), $this->getDataItems())); + $this->assertEqualsWithoutLE($expected, Html::listBox('test', array('value1', 'value2'), $this->getDataItems())); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', null, array(), array('multiple' => true))); + $this->assertEqualsWithoutLE($expected, Html::listBox('test', null, array(), array('multiple' => true))); $expected = << EOD; - $this->assertEquals($expected, Html::listBox('test', '', array(), array('unselect' => '0'))); + $this->assertEqualsWithoutLE($expected, Html::listBox('test', '', array(), array('unselect' => '0'))); } public function testCheckboxList() @@ -314,19 +324,19 @@ EOD; EOD; - $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems())); + $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', array('value2'), $this->getDataItems())); $expected = << text1<> EOD; - $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems2())); + $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', array('value2'), $this->getDataItems2())); $expected = <<
    EOD; - $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( + $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( 'separator' => "
    \n", 'unselect' => '0', ))); @@ -335,7 +345,7 @@ EOD; 0 1 EOD; - $this->assertEquals($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( + $this->assertEqualsWithoutLE($expected, Html::checkboxList('test', array('value2'), $this->getDataItems(), array( 'item' => function ($index, $label, $name, $checked, $value) { return $index . Html::label($label . ' ' . Html::checkbox($name, $checked, $value)); } @@ -350,19 +360,19 @@ EOD; EOD; - $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems())); + $this->assertEqualsWithoutLE($expected, Html::radioList('test', array('value2'), $this->getDataItems())); $expected = << text1<> EOD; - $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems2())); + $this->assertEqualsWithoutLE($expected, Html::radioList('test', array('value2'), $this->getDataItems2())); $expected = <<
    EOD; - $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( + $this->assertEqualsWithoutLE($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( 'separator' => "
    \n", 'unselect' => '0', ))); @@ -371,7 +381,7 @@ EOD; 0 1 EOD; - $this->assertEquals($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( + $this->assertEqualsWithoutLE($expected, Html::radioList('test', array('value2'), $this->getDataItems(), array( 'item' => function ($index, $label, $name, $checked, $value) { return $index . Html::label($label . ' ' . Html::radio($name, $checked, $value)); } @@ -418,7 +428,7 @@ EOD; 'group12' => array('class' => 'group'), ), ); - $this->assertEquals($expected, Html::renderSelectOptions(array('value111', 'value1'), $data, $attributes)); + $this->assertEqualsWithoutLE($expected, Html::renderSelectOptions(array('value111', 'value1'), $data, $attributes)); } public function testRenderAttributes() diff --git a/tests/unit/framework/helpers/StringHelperTest.php b/tests/unit/framework/helpers/StringHelperTest.php new file mode 100644 index 0000000..4e1266f --- /dev/null +++ b/tests/unit/framework/helpers/StringHelperTest.php @@ -0,0 +1,73 @@ +assertEquals(4, StringHelper::strlen('this')); + $this->assertEquals(6, StringHelper::strlen('это')); + } + + public function testSubstr() + { + $this->assertEquals('th', StringHelper::substr('this', 0, 2)); + $this->assertEquals('э', StringHelper::substr('это', 0, 2)); + } + + public function testPluralize() + { + $testData = array( + 'move' => 'moves', + 'foot' => 'feet', + 'child' => 'children', + 'human' => 'humans', + 'man' => 'men', + 'staff' => 'staff', + 'tooth' => 'teeth', + 'person' => 'people', + 'mouse' => 'mice', + 'touch' => 'touches', + 'hash' => 'hashes', + 'shelf' => 'shelves', + 'potato' => 'potatoes', + 'bus' => 'buses', + 'test' => 'tests', + 'car' => 'cars', + ); + + foreach($testData as $testIn => $testOut) { + $this->assertEquals($testOut, StringHelper::pluralize($testIn)); + $this->assertEquals(ucfirst($testOut), ucfirst(StringHelper::pluralize($testIn))); + } + } + + public function testCamel2words() + { + $this->assertEquals('Camel Case', StringHelper::camel2words('camelCase')); + $this->assertEquals('Lower Case', StringHelper::camel2words('lower_case')); + $this->assertEquals('Tricky Stuff It Is Testing', StringHelper::camel2words(' tricky_stuff.it-is testing... ')); + } + + public function testCamel2id() + { + $this->assertEquals('post-tag', StringHelper::camel2id('PostTag')); + $this->assertEquals('post_tag', StringHelper::camel2id('PostTag', '_')); + + $this->assertEquals('post-tag', StringHelper::camel2id('postTag')); + $this->assertEquals('post_tag', StringHelper::camel2id('postTag', '_')); + } + + public function testId2camel() + { + $this->assertEquals('PostTag', StringHelper::id2camel('post-tag')); + $this->assertEquals('PostTag', StringHelper::id2camel('post_tag', '_')); + + $this->assertEquals('PostTag', StringHelper::id2camel('post-tag')); + $this->assertEquals('PostTag', StringHelper::id2camel('post_tag', '_')); + } +} \ No newline at end of file