diff --git a/.gitignore b/.gitignore index 9291d3e..832a890 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,4 @@ nbproject Thumbs.db # composer vendor dir -vendor \ No newline at end of file +/yii/vendor \ No newline at end of file diff --git a/apps/bootstrap/index.php b/apps/bootstrap/index.php index e3188c1..a0488ca 100644 --- a/apps/bootstrap/index.php +++ b/apps/bootstrap/index.php @@ -1,8 +1,13 @@ 'hello', 'basePath' => dirname(__DIR__), + 'preload' => array('log'), + 'modules' => array( + 'debug' => array( + 'class' => 'yii\debug\Module', + ) + ), 'components' => array( 'cache' => array( 'class' => 'yii\caching\FileCache', @@ -14,6 +20,15 @@ return array( 'assetManager' => array( 'bundles' => require(__DIR__ . '/assets.php'), ), + 'log' => array( + 'class' => 'yii\logging\Router', + 'targets' => array( + 'file' => array( + 'class' => 'yii\logging\FileTarget', + 'levels' => array('error', 'warning'), + ), + ), + ), ), 'params' => array( 'adminEmail' => 'admin@example.com', diff --git a/apps/bootstrap/protected/controllers/SiteController.php b/apps/bootstrap/protected/controllers/SiteController.php index d1186f6..b06ed06 100644 --- a/apps/bootstrap/protected/controllers/SiteController.php +++ b/apps/bootstrap/protected/controllers/SiteController.php @@ -6,6 +6,15 @@ use app\models\ContactForm; class SiteController extends Controller { + public function actions() + { + return array( + 'captcha' => array( + 'class' => 'yii\web\CaptchaAction', + ), + ); + } + public function actionIndex() { echo $this->render('index'); diff --git a/apps/bootstrap/protected/models/ContactForm.php b/apps/bootstrap/protected/models/ContactForm.php index 5124b2c..7b713a1 100644 --- a/apps/bootstrap/protected/models/ContactForm.php +++ b/apps/bootstrap/protected/models/ContactForm.php @@ -26,7 +26,7 @@ class ContactForm extends Model // email has to be a valid email address array('email', 'email'), // verifyCode needs to be entered correctly - //array('verifyCode', 'captcha', 'allowEmpty' => !Captcha::checkRequirements()), + array('verifyCode', 'captcha'), ); } diff --git a/apps/bootstrap/protected/views/layouts/main.php b/apps/bootstrap/protected/views/layouts/main.php index 1240053..a81f983 100644 --- a/apps/bootstrap/protected/views/layouts/main.php +++ b/apps/bootstrap/protected/views/layouts/main.php @@ -1,9 +1,11 @@ registerAssetBundle('app'); ?> beginPage(); ?> @@ -23,16 +25,17 @@ $this->registerAssetBundle('app'); @@ -55,6 +58,7 @@ $this->registerAssetBundle('app'); endBody(); ?> +widget('yii\debug\Toolbar'); ?> endPage(); ?> diff --git a/apps/bootstrap/protected/views/site/contact.php b/apps/bootstrap/protected/views/site/contact.php index 5cb5a8e..bee1ede 100644 --- a/apps/bootstrap/protected/views/site/contact.php +++ b/apps/bootstrap/protected/views/site/contact.php @@ -1,5 +1,8 @@ params['breadcrumbs'][] = $this->title; If you have business inquiries or other questions, please fill out the following form to contact us. Thank you.

-beginWidget('yii\widgets\ActiveForm', array( +beginWidget(ActiveForm::className(), array( 'options' => array('class' => 'form-horizontal'), 'fieldConfig' => array('inputOptions' => array('class' => 'input-xlarge')), )); ?> @@ -28,6 +31,15 @@ $this->params['breadcrumbs'][] = $this->title; field($model, 'email')->textInput(); ?> field($model, 'subject')->textInput(); ?> field($model, 'body')->textArea(array('rows' => 6)); ?> + field($model, 'verifyCode'); + echo $field->begin(); + echo $field->label(); + $this->widget(Captcha::className()); + echo Html::activeTextInput($model, 'verifyCode', array('class' => 'input-medium')); + echo $field->error(); + echo $field->end(); + ?>
'btn btn-primary')); ?>
diff --git a/apps/bootstrap/protected/views/site/login.php b/apps/bootstrap/protected/views/site/login.php index f7f842e..65dc7d1 100644 --- a/apps/bootstrap/protected/views/site/login.php +++ b/apps/bootstrap/protected/views/site/login.php @@ -1,5 +1,7 @@ params['breadcrumbs'][] = $this->title;

Please fill out the following fields to login:

-beginWidget('yii\widgets\ActiveForm', array('options' => array('class' => 'form-horizontal'))); ?> +beginWidget(ActiveForm::className(), array('options' => array('class' => 'form-horizontal'))); ?> field($model, 'username')->textInput(); ?> field($model, 'password')->passwordInput(); ?> field($model, 'rememberMe')->checkbox(); ?> diff --git a/build/build b/build/build index 691eba9..95b51e4 100755 --- a/build/build +++ b/build/build @@ -11,7 +11,7 @@ // fcgi doesn't have STDIN defined by default defined('STDIN') or define('STDIN', fopen('php://stdin', 'r')); -require(__DIR__ . '/../framework/yii.php'); +require(__DIR__ . '/../framework/Yii.php'); $id = 'yiic-build'; $basePath = __DIR__; diff --git a/composer.json b/composer.json index 6a5c383..e9c3927 100644 --- a/composer.json +++ b/composer.json @@ -63,13 +63,17 @@ "irc": "irc://irc.freenode.net/yii", "source": "https://github.com/yiisoft/yii2" }, + "config": { + "vendor-dir": "yii/vendor" + }, "bin": [ - "framework/yiic" + "yii/yiic" ], "require": { "php": ">=5.3.0", "michelf/php-markdown": "1.3", "twig/twig": "1.12.*", - "smarty/smarty": "3.1.*" + "smarty/smarty": "3.1.*", + "ezyang/htmlpurifier": "v4.5.0" } } diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..1cae3d4 --- /dev/null +++ b/composer.lock @@ -0,0 +1,212 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" + ], + "hash": "7d46ce9c4d8d5f4ecae1611ea8f0b49c", + "packages": [ + { + "name": "ezyang/htmlpurifier", + "version": "v4.5.0", + "source": { + "type": "git", + "url": "https://github.com/ezyang/htmlpurifier.git", + "reference": "v4.5.0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/v4.5.0", + "reference": "v4.5.0", + "shasum": "" + }, + "require": { + "php": ">=5.2" + }, + "type": "library", + "autoload": { + "psr-0": { + "HTMLPurifier": "library/" + }, + "files": [ + "library/HTMLPurifier.composer.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL" + ], + "authors": [ + { + "name": "Edward Z. Yang", + "email": "admin@htmlpurifier.org", + "homepage": "http://ezyang.com" + } + ], + "description": "Standards compliant HTML filter written in PHP", + "homepage": "http://htmlpurifier.org/", + "keywords": [ + "html" + ], + "time": "2013-02-18 00:04:08" + }, + { + "name": "michelf/php-markdown", + "version": "1.3", + "source": { + "type": "git", + "url": "https://github.com/michelf/php-markdown.git", + "reference": "1.3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/michelf/php-markdown/zipball/1.3", + "reference": "1.3", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-lib": "1.3.x-dev" + } + }, + "autoload": { + "psr-0": { + "Michelf": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Michel Fortin", + "email": "michel.fortin@michelf.ca", + "homepage": "http://michelf.ca/", + "role": "Developer" + }, + { + "name": "John Gruber", + "homepage": "http://daringfireball.net/" + } + ], + "description": "PHP Markdown", + "homepage": "http://michelf.ca/projects/php-markdown/", + "keywords": [ + "markdown" + ], + "time": "2013-04-11 18:53:11" + }, + { + "name": "smarty/smarty", + "version": "v3.1.13", + "source": { + "type": "svn", + "url": "http://smarty-php.googlecode.com/svn", + "reference": "/tags/v3.1.13/@4699" + }, + "require": { + "php": ">=5.2" + }, + "type": "library", + "autoload": { + "classmap": [ + "distribution/libs/Smarty.class.php", + "distribution/libs/SmartyBC.class.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "authors": [ + { + "name": "Monte Ohrt", + "email": "monte@ohrt.com" + }, + { + "name": "Uwe Tews", + "email": "uwe.tews@googlemail.com" + }, + { + "name": "Rodney Rehm", + "email": "rodney.rehm@medialize.de" + } + ], + "description": "Smarty - the compiling PHP template engine", + "homepage": "http://www.smarty.net", + "keywords": [ + "templating" + ], + "time": "2013-01-26 12:03:52" + }, + { + "name": "twig/twig", + "version": "v1.12.3", + "source": { + "type": "git", + "url": "https://github.com/fabpot/Twig.git", + "reference": "v1.12.3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fabpot/Twig/zipball/v1.12.3", + "reference": "v1.12.3", + "shasum": "" + }, + "require": { + "php": ">=5.2.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.12-dev" + } + }, + "autoload": { + "psr-0": { + "Twig_": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Armin Ronacher", + "email": "armin.ronacher@active-4.com" + } + ], + "description": "Twig, the flexible, fast, and secure template language for PHP", + "homepage": "http://twig.sensiolabs.org", + "keywords": [ + "templating" + ], + "time": "2013-04-08 12:40:11" + } + ], + "packages-dev": [ + + ], + "aliases": [ + + ], + "minimum-stability": "stable", + "stability-flags": [ + + ], + "platform": { + "php": ">=5.3.0" + }, + "platform-dev": [ + + ] +} diff --git a/docs/internals/exception_hierarchy.png b/docs/internals/exception_hierarchy.png new file mode 100644 index 0000000..2c43e03 Binary files /dev/null and b/docs/internals/exception_hierarchy.png differ diff --git a/docs/internals/exception_hierarchy.vsd b/docs/internals/exception_hierarchy.vsd new file mode 100644 index 0000000..d044ea9 Binary files /dev/null and b/docs/internals/exception_hierarchy.vsd differ diff --git a/framework/helpers/base/ConsoleColor.php b/framework/helpers/base/ConsoleColor.php deleted file mode 100644 index 5e7f577..0000000 --- a/framework/helpers/base/ConsoleColor.php +++ /dev/null @@ -1,470 +0,0 @@ - - * @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 'getController()->getRoute(); - $this->items = $this->normalizeItems($this->items, $route, $hasActiveChild); - } - - /** - * Calls {@link renderMenu} to render the menu. - */ - public function run() - { - if (count($this->items)) { - echo Html::beginTag('ul', $this->options) . "\n"; - $this->renderItems($this->items); - echo Html::endTag('ul'); - } - } - - /** - * Recursively renders the menu items. - * @param array $items the menu items to be rendered recursively - */ - protected function renderItems($items) - { - $count = 0; - $n = count($items); - foreach ($items as $item) { - $count++; - $options = isset($item['itemOptions']) ? $item['itemOptions'] : array(); - $class = array(); - if ($item['active'] && $this->activeCssClass != '') { - $class[] = $this->activeCssClass; - } - if ($count === 1 && $this->firstItemCssClass !== null) { - $class[] = $this->firstItemCssClass; - } - if ($count === $n && $this->lastItemCssClass !== null) { - $class[] = $this->lastItemCssClass; - } - if ($this->itemCssClass !== null) { - $class[] = $this->itemCssClass; - } - if ($class !== array()) { - if (empty($options['class'])) { - $options['class'] = implode(' ', $class); - } else { - $options['class'] .= ' ' . implode(' ', $class); - } - } - - echo Html::beginTag('li', $options); - - $menu = $this->renderItem($item); - if (isset($this->itemTemplate) || isset($item['template'])) { - $template = isset($item['template']) ? $item['template'] : $this->itemTemplate; - echo strtr($template, array('{menu}' => $menu)); - } else { - echo $menu; - } - - if (isset($item['items']) && count($item['items'])) { - echo "\n" . Html::beginTag('ul', isset($item['submenuOptions']) ? $item['submenuOptions'] : $this->submenuHtmlOptions) . "\n"; - $this->renderItems($item['items']); - echo Html::endTag('ul') . "\n"; - } - - echo Html::endTag('li') . "\n"; - } - } - - /** - * Renders the content of a menu item. - * Note that the container and the sub-menus are not rendered here. - * @param array $item the menu item to be rendered. Please see {@link items} on what data might be in the item. - * @return string - * @since 1.1.6 - */ - protected function renderItem($item) - { - if (isset($item['url'])) { - $label = $this->linkLabelWrapper === null ? $item['label'] : Html::tag($this->linkLabelWrapper, $this->linkLabelWrapperHtmlOptions, $item['label']); - return Html::a($label, $item['url'], isset($item['linkOptions']) ? $item['linkOptions'] : array()); - } else { - return Html::tag('span', isset($item['linkOptions']) ? $item['linkOptions'] : array(), $item['label']); - } - } - - /** - * Normalizes the {@link items} property so that the 'active' state is properly identified for every menu item. - * @param array $items the items to be normalized. - * @param string $route the route of the current request. - * @param boolean $active whether there is an active child menu item. - * @return array the normalized menu items - */ - protected function normalizeItems($items, $route, &$active) - { - foreach ($items as $i => $item) { - if (isset($item['visible']) && !$item['visible']) { - unset($items[$i]); - continue; - } - if (!isset($item['label'])) { - $item['label'] = ''; - } - if ($this->encodeLabel) { - $items[$i]['label'] = Html::encode($item['label']); - } - $hasActiveChild = false; - if (isset($item['items'])) { - $items[$i]['items'] = $this->normalizeItems($item['items'], $route, $hasActiveChild); - if (empty($items[$i]['items']) && $this->hideEmptyItems) { - unset($items[$i]['items']); - if (!isset($item['url'])) { - unset($items[$i]); - continue; - } - } - } - if (!isset($item['active'])) { - if ($this->activateParents && $hasActiveChild || $this->activateItems && $this->isItemActive($item, $route)) { - $active = $items[$i]['active'] = true; - } else { - $items[$i]['active'] = false; - } - } elseif ($item['active']) { - $active = true; - } - } - return array_values($items); - } - - /** - * Checks whether a menu item is active. - * This is done by checking if the currently requested URL is generated by the 'url' option - * of the menu item. Note that the GET parameters not specified in the 'url' option will be ignored. - * @param array $item the menu item to be checked - * @param string $route the route of the current request - * @return boolean whether the menu item is active - */ - protected function isItemActive($item, $route) - { - if (isset($item['url']) && is_array($item['url']) && !strcasecmp(trim($item['url'][0], '/'), $route)) { - unset($item['url']['#']); - if (count($item['url']) > 1) { - foreach (array_splice($item['url'], 1) as $name => $value) { - if (!isset($_GET[$name]) || $_GET[$name] != $value) { - return false; - } - } - } - return true; - } - return false; - } - -} diff --git a/tests/unit/DatabaseTestCase.php b/tests/unit/DatabaseTestCase.php new file mode 100644 index 0000000..b39c2e5 --- /dev/null +++ b/tests/unit/DatabaseTestCase.php @@ -0,0 +1,50 @@ +getParam('databases'); + $this->database = $databases[$this->driverName]; + $pdo_database = 'pdo_'.$this->driverName; + + if (!extension_loaded('pdo') || !extension_loaded($pdo_database)) { + $this->markTestSkipped('pdo and pdo_'.$pdo_database.' extension are required.'); + } + } + + /** + * @param bool $reset whether to clean up the test database + * @param bool $open whether to open and populate test database + * @return \yii\db\Connection + */ + public function getConnection($reset = true, $open = true) + { + if (!$reset && $this->db) { + return $this->db; + } + $db = new \yii\db\Connection; + $db->dsn = $this->database['dsn']; + if (isset($this->database['username'])) { + $db->username = $this->database['username']; + $db->password = $this->database['password']; + } + if ($open) { + $db->open(); + $lines = explode(';', file_get_contents($this->database['fixture'])); + foreach ($lines as $line) { + if (trim($line) !== '') { + $db->pdo->exec($line); + } + } + } + $this->db = $db; + return $db; + } +} diff --git a/tests/unit/bootstrap.php b/tests/unit/bootstrap.php index 8290bbe..285b55b 100644 --- a/tests/unit/bootstrap.php +++ b/tests/unit/bootstrap.php @@ -5,7 +5,7 @@ define('YII_DEBUG', true); $_SERVER['SCRIPT_NAME'] = '/' . __DIR__; $_SERVER['SCRIPT_FILENAME'] = __FILE__; -require_once(__DIR__ . '/../../framework/yii.php'); +require_once(__DIR__ . '/../../yii/Yii.php'); Yii::setAlias('@yiiunit', __DIR__); diff --git a/tests/unit/data/config.php b/tests/unit/data/config.php index 238a7cc..c980c57 100644 --- a/tests/unit/data/config.php +++ b/tests/unit/data/config.php @@ -1,10 +1,16 @@ array( - 'dsn' => 'mysql:host=127.0.0.1;dbname=yiitest', - 'username' => 'travis', - 'password' => '', - 'fixture' => __DIR__ . '/mysql.sql', - ), + 'databases' => array( + 'mysql' => array( + 'dsn' => 'mysql:host=127.0.0.1;dbname=yiitest', + 'username' => 'travis', + 'password' => '', + 'fixture' => __DIR__ . '/mysql.sql', + ), + 'sqlite' => array( + 'dsn' => 'sqlite::memory:', + 'fixture' => __DIR__ . '/sqlite.sql', + ), + ) ); diff --git a/tests/unit/data/i18n/test.mo b/tests/unit/data/i18n/test.mo new file mode 100644 index 0000000..d5f94f1 Binary files /dev/null and b/tests/unit/data/i18n/test.mo differ diff --git a/tests/unit/data/i18n/test.po b/tests/unit/data/i18n/test.po new file mode 100644 index 0000000..fed95c9 --- /dev/null +++ b/tests/unit/data/i18n/test.po @@ -0,0 +1,64 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: resurtm \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 1.5.5\n" + +msgctxt "context1" +msgid "" +"Aliquam tempus elit vel purus molestie placerat. In sollicitudin tincidunt\n" +"aliquet. Integer tincidunt gravida tempor. In convallis blandit dui vel " +"malesuada.\n" +"Nunc vel sapien nunc, a pretium nulla." +msgstr "" +"Олицетворение однократно. Представленный лексико-семантический анализ " +"является\n" +"психолингвистическим в своей основе, но механизм сочленений полидисперсен. " +"Впечатление\n" +"однократно. Различное расположение выбирает сюжетный механизм сочленений." + +msgctxt "context1" +msgid "String number two." +msgstr "Строка номер два." + +msgctxt "context2" +msgid "" +"The other\n" +"\n" +"context.\n" +msgstr "" +"Другой\n" +"\n" +"контекст.\n" + +msgctxt "context1" +msgid "" +"Missing\n" +"\r\t\"translation." +msgstr "" + +msgctxt "context1" +msgid "" +"Nunc vel sapien nunc, a pretium nulla.\n" +"Pellentesque habitant morbi tristique senectus et netus et malesuada fames " +"ac turpis egestas." +msgstr "Короткий перевод." + +msgid "contextless" +msgstr "" + +msgctxt "context2" +msgid "" +"test1\\ntest2\n" +"\\\n" +"test3" +msgstr "" +"тест1\\nтест2\n" +"\\\n" +"тест3" diff --git a/tests/unit/data/mysql.sql b/tests/unit/data/mysql.sql index 8223f59..1bb5558 100644 --- a/tests/unit/data/mysql.sql +++ b/tests/unit/data/mysql.sql @@ -1,10 +1,6 @@ /** * This is the database schema for testing MySQL support of Yii DAO and Active Record. - * The following database setup is required to perform then relevant tests: - * Database name: yiitest - * username: test - * password: test - * charset: utf8 + * The database setup in config.php is required to perform then relevant tests: */ DROP TABLE IF EXISTS tbl_order_item CASCADE; diff --git a/tests/unit/data/sqlite.sql b/tests/unit/data/sqlite.sql index affeca9..f75bfa6 100644 --- a/tests/unit/data/sqlite.sql +++ b/tests/unit/data/sqlite.sql @@ -1,262 +1,88 @@ -CREATE TABLE users -( - id INTEGER NOT NULL PRIMARY KEY, - username VARCHAR(128) NOT NULL, - password VARCHAR(128) NOT NULL, - email VARCHAR(128) NOT NULL -); - -INSERT INTO users(id,username,password,email) VALUES (1,'user1','pass1','email1'); -INSERT INTO users(id,username,password,email) VALUES (2,'user2','pass2','email2'); -INSERT INTO users(id,username,password,email) VALUES (3,'user3','pass3','email3'); -INSERT INTO users(id,username,password,email) VALUES (4,'user4','pass4','email4'); - -CREATE TABLE groups -( - id INTEGER NOT NULL PRIMARY KEY, - name VARCHAR(128) NOT NULL -); - -INSERT INTO groups(id,name) VALUES (1,'group1'); -INSERT INTO groups(id,name) VALUES (2,'group2'); -INSERT INTO groups(id,name) VALUES (3,'group3'); -INSERT INTO groups(id,name) VALUES (4,'group4'); -INSERT INTO groups(id,name) VALUES (5,'group5'); -INSERT INTO groups(id,name) VALUES (6,'group6'); - -CREATE TABLE groups_descriptions -( - group_id INTEGER NOT NULL PRIMARY KEY, - name VARCHAR(128) NOT NULL -); - -INSERT INTO groups_descriptions(group_id,name) VALUES (1,'room1'); -INSERT INTO groups_descriptions(group_id,name) VALUES (2,'room2'); -INSERT INTO groups_descriptions(group_id,name) VALUES (3,'room3'); -INSERT INTO groups_descriptions(group_id,name) VALUES (4,'room4'); - -CREATE TABLE roles -( - user_id INTEGER NOT NULL, - group_id INTEGER NOT NULL, - name VARCHAR(128) NOT NULL, - PRIMARY KEY(user_id,group_id) -); - -INSERT INTO roles(user_id,group_id,name) VALUES (1,1,'dev'); -INSERT INTO roles(user_id,group_id,name) VALUES (1,2,'user'); -INSERT INTO roles(user_id,group_id,name) VALUES (2,1,'dev'); -INSERT INTO roles(user_id,group_id,name) VALUES (2,3,'user'); - -CREATE TABLE mentorships -( - teacher_id INTEGER NOT NULL, - student_id INTEGER NOT NULL, - progress VARCHAR(128) NOT NULL, - PRIMARY KEY(teacher_id,student_id) -); - -INSERT INTO mentorships(teacher_id,student_id,progress) VALUES (1,3,'good'); -INSERT INTO mentorships(teacher_id,student_id,progress) VALUES (2,4,'average'); - -CREATE TABLE profiles -( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - first_name VARCHAR(128) NOT NULL, - last_name VARCHAR(128) NOT NULL, - user_id INTEGER NOT NULL, - CONSTRAINT FK_profile_user FOREIGN KEY (user_id) - REFERENCES users (id) ON DELETE CASCADE ON UPDATE RESTRICT -); - -INSERT INTO profiles (first_name, last_name, user_id) VALUES ('first 1','last 1',1); -INSERT INTO profiles (first_name, last_name, user_id) VALUES ('first 2','last 2',2); - -CREATE TABLE posts -( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - title VARCHAR(128) NOT NULL, - create_time TIMESTAMP NOT NULL, - author_id INTEGER NOT NULL, - content TEXT, - CONSTRAINT FK_post_author FOREIGN KEY (author_id) - REFERENCES users (id) ON DELETE CASCADE ON UPDATE RESTRICT -); - -INSERT INTO posts (title, create_time, author_id, content) VALUES ('post 1',100000,1,'content 1'); -INSERT INTO posts (title, create_time, author_id, content) VALUES ('post 2',100001,2,'content 2'); -INSERT INTO posts (title, create_time, author_id, content) VALUES ('post 3',100002,2,'content 3'); -INSERT INTO posts (title, create_time, author_id, content) VALUES ('post 4',100003,2,'content 4'); -INSERT INTO posts (title, create_time, author_id, content) VALUES ('post 5',100004,3,'content 5'); - - -CREATE TABLE posts_nofk -( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - title VARCHAR(128) NOT NULL, - create_time TIMESTAMP NOT NULL, - author_id INTEGER NOT NULL, - content TEXT -); - -INSERT INTO posts_nofk (title, create_time, author_id, content) VALUES ('post 1',100000,1,'content 1'); -INSERT INTO posts_nofk (title, create_time, author_id, content) VALUES ('post 2',100001,2,'content 2'); -INSERT INTO posts_nofk (title, create_time, author_id, content) VALUES ('post 3',100002,2,'content 3'); -INSERT INTO posts_nofk (title, create_time, author_id, content) VALUES ('post 4',100003,2,'content 4'); -INSERT INTO posts_nofk (title, create_time, author_id, content) VALUES ('post 5',100004,3,'content 5'); - - -CREATE TABLE comments -( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - content TEXT NOT NULL, - post_id INTEGER NOT NULL, - author_id INTEGER NOT NULL, - CONSTRAINT FK_post_comment FOREIGN KEY (post_id) - REFERENCES posts (id) ON DELETE CASCADE ON UPDATE RESTRICT, - CONSTRAINT FK_user_comment FOREIGN KEY (author_id) - REFERENCES users (id) ON DELETE CASCADE ON UPDATE RESTRICT -); - -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 1',1, 2); -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 2',1, 2); -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 3',1, 2); -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 4',2, 2); -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 5',2, 2); -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 6',3, 2); -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 7',3, 2); -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 8',3, 2); -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 9',3, 2); -INSERT INTO comments (content, post_id, author_id) VALUES ('comment 10',5, 3); - -CREATE TABLE categories -( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - name VARCHAR(128) NOT NULL, - parent_id INTEGER, - CONSTRAINT FK_category_category FOREIGN KEY (parent_id) - REFERENCES categories (id) ON DELETE CASCADE ON UPDATE RESTRICT -); - -INSERT INTO categories (name, parent_id) VALUES ('cat 1',NULL); -INSERT INTO categories (name, parent_id) VALUES ('cat 2',NULL); -INSERT INTO categories (name, parent_id) VALUES ('cat 3',NULL); -INSERT INTO categories (name, parent_id) VALUES ('cat 4',1); -INSERT INTO categories (name, parent_id) VALUES ('cat 5',1); -INSERT INTO categories (name, parent_id) VALUES ('cat 6',5); -INSERT INTO categories (name, parent_id) VALUES ('cat 7',5); - -CREATE TABLE post_category -( - category_id INTEGER NOT NULL, - post_id INTEGER NOT NULL, - PRIMARY KEY (category_id, post_id), - CONSTRAINT FK_post_category_post FOREIGN KEY (post_id) - REFERENCES posts (id) ON DELETE CASCADE ON UPDATE RESTRICT, - CONSTRAINT FK_post_category_category FOREIGN KEY (category_id) - REFERENCES categories (id) ON DELETE CASCADE ON UPDATE RESTRICT -); - -INSERT INTO post_category (category_id, post_id) VALUES (1,1); -INSERT INTO post_category (category_id, post_id) VALUES (2,1); -INSERT INTO post_category (category_id, post_id) VALUES (3,1); -INSERT INTO post_category (category_id, post_id) VALUES (4,2); -INSERT INTO post_category (category_id, post_id) VALUES (1,2); -INSERT INTO post_category (category_id, post_id) VALUES (1,3); - -CREATE TABLE orders -( - key1 INTEGER NOT NULL, - key2 INTEGER NOT NULL, - name VARCHAR(128), - PRIMARY KEY (key1, key2) -); - -INSERT INTO orders (key1,key2,name) VALUES (1,2,'order 12'); -INSERT INTO orders (key1,key2,name) VALUES (1,3,'order 13'); -INSERT INTO orders (key1,key2,name) VALUES (2,1,'order 21'); -INSERT INTO orders (key1,key2,name) VALUES (2,2,'order 22'); - -CREATE TABLE items -( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - name VARCHAR(128), - col1 INTEGER NOT NULL, - col2 INTEGER NOT NULL, - CONSTRAINT FK_order_item FOREIGN KEY (col1,col2) - REFERENCES orders (key1,key2) ON DELETE CASCADE ON UPDATE RESTRICT -); - -INSERT INTO items (name,col1,col2) VALUES ('item 1',1,2); -INSERT INTO items (name,col1,col2) VALUES ('item 2',1,2); -INSERT INTO items (name,col1,col2) VALUES ('item 3',1,3); -INSERT INTO items (name,col1,col2) VALUES ('item 4',2,2); -INSERT INTO items (name,col1,col2) VALUES ('item 5',2,2); - -CREATE TABLE types -( - int_col INT NOT NULL, - int_col2 INTEGER DEFAULT 1, - char_col CHAR(100) NOT NULL, - char_col2 VARCHAR(100) DEFAULT 'something', - char_col3 TEXT, - float_col REAL(4,3) NOT NULL, - float_col2 DOUBLE DEFAULT 1.23, - blob_col BLOB, - numeric_col NUMERIC(5,2) DEFAULT 33.22, - time TIMESTAMP DEFAULT 123, - bool_col BOOL NOT NULL, - bool_col2 BOOLEAN DEFAULT 1, - null_col INTEGER DEFAULT NULL -); - -CREATE TABLE Content -( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - class VARCHAR(128), - parentID INTEGER NOT NULL, - ownerID INTEGER NOT NULL, - title VARCHAR(100), - CONSTRAINT FK_content_user FOREIGN KEY (ownerID) - REFERENCES users (id) ON DELETE CASCADE ON UPDATE RESTRICT - CONSTRAINT FK_content_parent FOREIGN KEY (parentID) - REFERENCES Content (id) ON DELETE CASCADE ON UPDATE RESTRICT -); - -INSERT INTO Content (class,parentID,ownerID,title) VALUES ('Article',-1,1,'article 1'); -INSERT INTO Content (class,parentID,ownerID,title) VALUES ('Article',-1,2,'article 2'); -INSERT INTO Content (class,parentID,ownerID,title) VALUES ('Comment',1,1,'comment 1'); -INSERT INTO Content (class,parentID,ownerID,title) VALUES ('Article',-1,2,'article 3'); -INSERT INTO Content (class,parentID,ownerID,title) VALUES ('Comment',4,2,'comment 2'); -INSERT INTO Content (class,parentID,ownerID,title) VALUES ('Comment',4,1,'comment 3'); - -CREATE TABLE Article -( - id INTEGER NOT NULL PRIMARY KEY, - authorID INTEGER NOT NULL, - body TEXT, - CONSTRAINT FK_article_content FOREIGN KEY (id) - REFERENCES Content (id) ON DELETE CASCADE ON UPDATE RESTRICT - CONSTRAINT FK_article_author FOREIGN KEY (authorID) - REFERENCES users (id) ON DELETE CASCADE ON UPDATE RESTRICT -); - -INSERT INTO Article (id,authorID,body) VALUES (1,1,'content for article 1'); -INSERT INTO Article (id,authorID,body) VALUES (2,2,'content for article 2'); -INSERT INTO Article (id,authorID,body) VALUES (4,1,'content for article 3'); - -CREATE TABLE Comment -( - id INTEGER NOT NULL PRIMARY KEY, - authorID INTEGER NOT NULL, - body TEXT, - CONSTRAINT FK_comment_content FOREIGN KEY (id) - REFERENCES Content (id) ON DELETE CASCADE ON UPDATE RESTRICT - CONSTRAINT FK_article_author FOREIGN KEY (authorID) - REFERENCES users (id) ON DELETE CASCADE ON UPDATE RESTRICT -); - -INSERT INTO Comment (id,authorID,body) VALUES (3,1,'content for comment 1'); -INSERT INTO Comment (id,authorID,body) VALUES (5,1,'content for comment 2'); -INSERT INTO Comment (id,authorID,body) VALUES (6,1,'content for comment 3'); - +/** + * This is the database schema for testing Sqlite support of Yii DAO and Active Record. + * The database setup in config.php is required to perform then relevant tests: + */ + +DROP TABLE IF EXISTS tbl_order_item; +DROP TABLE IF EXISTS tbl_item; +DROP TABLE IF EXISTS tbl_order; +DROP TABLE IF EXISTS tbl_category; +DROP TABLE IF EXISTS tbl_customer; +DROP TABLE IF EXISTS tbl_type; + +CREATE TABLE tbl_customer ( + id INTEGER NOT NULL, + email varchar(128) NOT NULL, + name varchar(128) NOT NULL, + address text, + status INTEGER DEFAULT 0, + PRIMARY KEY (id) +); + +CREATE TABLE tbl_category ( + id INTEGER NOT NULL, + name varchar(128) NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE tbl_item ( + id INTEGER NOT NULL, + name varchar(128) NOT NULL, + category_id INTEGER NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE tbl_order ( + id INTEGER NOT NULL, + customer_id INTEGER NOT NULL, + create_time INTEGER NOT NULL, + total decimal(10,0) NOT NULL, + PRIMARY KEY (id) +); + +CREATE TABLE tbl_order_item ( + order_id INTEGER NOT NULL, + item_id INTEGER NOT NULL, + quantity INTEGER NOT NULL, + subtotal decimal(10,0) NOT NULL, + PRIMARY KEY (order_id, item_id) +); + +CREATE TABLE tbl_type ( + int_col INTEGER NOT NULL, + int_col2 INTEGER DEFAULT '1', + char_col char(100) NOT NULL, + char_col2 varchar(100) DEFAULT 'something', + char_col3 text, + float_col double(4,3) NOT NULL, + float_col2 double DEFAULT '1.23', + blob_col blob, + numeric_col decimal(5,2) DEFAULT '33.22', + time timestamp NOT NULL DEFAULT '2002-01-01 00:00:00', + bool_col tinyint(1) NOT NULL, + bool_col2 tinyint(1) DEFAULT '1' +); + +INSERT INTO tbl_customer (email, name, address, status) VALUES ('user1@example.com', 'user1', 'address1', 1); +INSERT INTO tbl_customer (email, name, address, status) VALUES ('user2@example.com', 'user2', 'address2', 1); +INSERT INTO tbl_customer (email, name, address, status) VALUES ('user3@example.com', 'user3', 'address3', 2); + +INSERT INTO tbl_category (name) VALUES ('Books'); +INSERT INTO tbl_category (name) VALUES ('Movies'); + +INSERT INTO tbl_item (name, category_id) VALUES ('Agile Web Application Development with Yii1.1 and PHP5', 1); +INSERT INTO tbl_item (name, category_id) VALUES ('Yii 1.1 Application Development Cookbook', 1); +INSERT INTO tbl_item (name, category_id) VALUES ('Ice Age', 2); +INSERT INTO tbl_item (name, category_id) VALUES ('Toy Story', 2); +INSERT INTO tbl_item (name, category_id) VALUES ('Cars', 2); + +INSERT INTO tbl_order (customer_id, create_time, total) VALUES (1, 1325282384, 110.0); +INSERT INTO tbl_order (customer_id, create_time, total) VALUES (2, 1325334482, 33.0); +INSERT INTO tbl_order (customer_id, create_time, total) VALUES (2, 1325502201, 40.0); + +INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (1, 1, 1, 30.0); +INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (1, 2, 2, 40.0); +INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (2, 4, 1, 10.0); +INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (2, 5, 1, 15.0); +INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (2, 3, 1, 8.0); +INSERT INTO tbl_order_item (order_id, item_id, quantity, subtotal) VALUES (3, 2, 1, 40.0); \ No newline at end of file diff --git a/tests/unit/framework/YiiBaseTest.php b/tests/unit/framework/YiiBaseTest.php index 47474f2..4ebf45c 100644 --- a/tests/unit/framework/YiiBaseTest.php +++ b/tests/unit/framework/YiiBaseTest.php @@ -47,7 +47,6 @@ class YiiBaseTest extends TestCase public function testGetVersion() { - echo Yii::getVersion(); $this->assertTrue((boolean)preg_match('~\d+\.\d+(?:\.\d+)?(?:-\w+)?~', \Yii::getVersion())); } diff --git a/tests/unit/framework/caching/ApcCacheTest.php b/tests/unit/framework/caching/ApcCacheTest.php index 859f6a6..6018ce7 100644 --- a/tests/unit/framework/caching/ApcCacheTest.php +++ b/tests/unit/framework/caching/ApcCacheTest.php @@ -15,13 +15,13 @@ class ApcCacheTest extends CacheTest */ protected function getCacheInstance() { - if(!extension_loaded("apc")) { + if (!extension_loaded("apc")) { $this->markTestSkipped("APC not installed. Skipping."); - } else if ('cli' === PHP_SAPI && !ini_get('apc.enable_cli')) { + } elseif ('cli' === PHP_SAPI && !ini_get('apc.enable_cli')) { $this->markTestSkipped("APC cli is not enabled. Skipping."); } - if($this->_cacheInstance === null) { + if ($this->_cacheInstance === null) { $this->_cacheInstance = new ApcCache(); } return $this->_cacheInstance; diff --git a/tests/unit/framework/caching/DbCacheTest.php b/tests/unit/framework/caching/DbCacheTest.php index a41667c..253240d 100644 --- a/tests/unit/framework/caching/DbCacheTest.php +++ b/tests/unit/framework/caching/DbCacheTest.php @@ -35,7 +35,8 @@ class DbCacheTest extends CacheTest function getConnection($reset = true) { if($this->_connection === null) { - $params = $this->getParam('mysql'); + $databases = $this->getParam('databases'); + $params = $databases['mysql']; $db = new \yii\db\Connection; $db->dsn = $params['dsn']; $db->username = $params['username']; diff --git a/tests/unit/framework/db/ActiveRecordTest.php b/tests/unit/framework/db/ActiveRecordTest.php index f0ea968..e337a5d 100644 --- a/tests/unit/framework/db/ActiveRecordTest.php +++ b/tests/unit/framework/db/ActiveRecordTest.php @@ -10,10 +10,11 @@ use yiiunit\data\ar\OrderItem; use yiiunit\data\ar\Order; use yiiunit\data\ar\Item; -class ActiveRecordTest extends \yiiunit\MysqlTestCase +class ActiveRecordTest extends \yiiunit\DatabaseTestCase { public function setUp() { + parent::setUp(); ActiveRecord::$db = $this->getConnection(); } diff --git a/tests/unit/framework/db/CommandTest.php b/tests/unit/framework/db/CommandTest.php index 2576e78..0d9652b 100644 --- a/tests/unit/framework/db/CommandTest.php +++ b/tests/unit/framework/db/CommandTest.php @@ -7,7 +7,7 @@ use yii\db\Command; use yii\db\Query; use yii\db\DataReader; -class CommandTest extends \yiiunit\MysqlTestCase +class CommandTest extends \yiiunit\DatabaseTestCase { function testConstruct() { diff --git a/tests/unit/framework/db/ConnectionTest.php b/tests/unit/framework/db/ConnectionTest.php index 256c5a9..c837b95 100644 --- a/tests/unit/framework/db/ConnectionTest.php +++ b/tests/unit/framework/db/ConnectionTest.php @@ -4,12 +4,12 @@ namespace yiiunit\framework\db; use yii\db\Connection; -class ConnectionTest extends \yiiunit\MysqlTestCase +class ConnectionTest extends \yiiunit\DatabaseTestCase { function testConstruct() { $connection = $this->getConnection(false); - $params = $this->getParam('mysql'); + $params = $this->database; $this->assertEquals($params['dsn'], $connection->dsn); $this->assertEquals($params['username'], $connection->username); @@ -18,7 +18,7 @@ class ConnectionTest extends \yiiunit\MysqlTestCase function testOpenClose() { - $connection = $this->getConnection(false); + $connection = $this->getConnection(false, false); $this->assertFalse($connection->isActive); $this->assertEquals(null, $connection->pdo); @@ -39,9 +39,8 @@ class ConnectionTest extends \yiiunit\MysqlTestCase function testGetDriverName() { - $connection = $this->getConnection(false); - $this->assertEquals('mysql', $connection->driverName); - $this->assertFalse($connection->isActive); + $connection = $this->getConnection(false, false); + $this->assertEquals($this->driverName, $connection->driverName); } function testQuoteValue() diff --git a/tests/unit/framework/db/QueryTest.php b/tests/unit/framework/db/QueryTest.php index 1c730cd..398d0d9 100644 --- a/tests/unit/framework/db/QueryTest.php +++ b/tests/unit/framework/db/QueryTest.php @@ -7,7 +7,7 @@ use yii\db\Command; use yii\db\Query; use yii\db\DataReader; -class QueryTest extends \yiiunit\MysqlTestCase +class QueryTest extends \yiiunit\DatabaseTestCase { function testSelect() { diff --git a/tests/unit/framework/db/sqlite/SqliteActiveRecordTest.php b/tests/unit/framework/db/sqlite/SqliteActiveRecordTest.php new file mode 100644 index 0000000..539e879 --- /dev/null +++ b/tests/unit/framework/db/sqlite/SqliteActiveRecordTest.php @@ -0,0 +1,12 @@ +driverName = 'sqlite'; + parent::setUp(); + } +} diff --git a/tests/unit/framework/db/sqlite/SqliteCommandTest.php b/tests/unit/framework/db/sqlite/SqliteCommandTest.php new file mode 100644 index 0000000..4110cd3 --- /dev/null +++ b/tests/unit/framework/db/sqlite/SqliteCommandTest.php @@ -0,0 +1,21 @@ +driverName = 'sqlite'; + parent::setUp(); + } + + function testAutoQuoting() + { + $db = $this->getConnection(false); + + $sql = 'SELECT [[id]], [[t.name]] FROM {{tbl_customer}} t'; + $command = $db->createCommand($sql); + $this->assertEquals("SELECT \"id\", 't'.\"name\" FROM 'tbl_customer' t", $command->sql); + } +} diff --git a/tests/unit/framework/db/sqlite/SqliteConnectionTest.php b/tests/unit/framework/db/sqlite/SqliteConnectionTest.php new file mode 100644 index 0000000..eeb5aae --- /dev/null +++ b/tests/unit/framework/db/sqlite/SqliteConnectionTest.php @@ -0,0 +1,47 @@ +driverName = 'sqlite'; + parent::setUp(); + } + + function testConstruct() + { + $connection = $this->getConnection(false); + $params = $this->database; + + $this->assertEquals($params['dsn'], $connection->dsn); + } + + function testQuoteValue() + { + $connection = $this->getConnection(false); + $this->assertEquals(123, $connection->quoteValue(123)); + $this->assertEquals("'string'", $connection->quoteValue('string')); + $this->assertEquals("'It''s interesting'", $connection->quoteValue("It's interesting")); + } + + function testQuoteTableName() + { + $connection = $this->getConnection(false); + $this->assertEquals("'table'", $connection->quoteTableName('table')); + $this->assertEquals("'schema'.'table'", $connection->quoteTableName('schema.table')); + $this->assertEquals('{{table}}', $connection->quoteTableName('{{table}}')); + $this->assertEquals('(table)', $connection->quoteTableName('(table)')); + } + + function testQuoteColumnName() + { + $connection = $this->getConnection(false); + $this->assertEquals('"column"', $connection->quoteColumnName('column')); + $this->assertEquals("'table'.\"column\"", $connection->quoteColumnName('table.column')); + $this->assertEquals('[[column]]', $connection->quoteColumnName('[[column]]')); + $this->assertEquals('{{column}}', $connection->quoteColumnName('{{column}}')); + $this->assertEquals('(column)', $connection->quoteColumnName('(column)')); + } +} diff --git a/tests/unit/framework/db/sqlite/SqliteQueryTest.php b/tests/unit/framework/db/sqlite/SqliteQueryTest.php new file mode 100644 index 0000000..f00e59e --- /dev/null +++ b/tests/unit/framework/db/sqlite/SqliteQueryTest.php @@ -0,0 +1,20 @@ +driverName = 'sqlite'; + parent::setUp(); + } +} diff --git a/tests/unit/framework/helpers/ArrayHelperTest.php b/tests/unit/framework/helpers/ArrayHelperTest.php index b3ffabf..8c83278 100644 --- a/tests/unit/framework/helpers/ArrayHelperTest.php +++ b/tests/unit/framework/helpers/ArrayHelperTest.php @@ -12,6 +12,16 @@ class ArrayHelperTest extends \yii\test\TestCase } + public function testRemove() + { + $array = array('name' => 'b', 'age' => 3); + $name = ArrayHelper::remove($array, 'name'); + + $this->assertEquals($name, 'b'); + $this->assertEquals($array, array('age' => 3)); + } + + public function testMultisort() { // single key diff --git a/tests/unit/framework/helpers/StringHelperTest.php b/tests/unit/framework/helpers/StringHelperTest.php index c37aafd..64811d1 100644 --- a/tests/unit/framework/helpers/StringHelperTest.php +++ b/tests/unit/framework/helpers/StringHelperTest.php @@ -70,4 +70,48 @@ class StringHelperTest extends \yii\test\TestCase $this->assertEquals('PostTag', StringHelper::id2camel('post-tag')); $this->assertEquals('PostTag', StringHelper::id2camel('post_tag', '_')); } + + public function testBasename() + { + $this->assertEquals('', StringHelper::basename('')); + + $this->assertEquals('file', StringHelper::basename('file')); + $this->assertEquals('file.test', StringHelper::basename('file.test', '.test2')); + $this->assertEquals('file', StringHelper::basename('file.test', '.test')); + + $this->assertEquals('file', StringHelper::basename('/file')); + $this->assertEquals('file.test', StringHelper::basename('/file.test', '.test2')); + $this->assertEquals('file', StringHelper::basename('/file.test', '.test')); + + $this->assertEquals('file', StringHelper::basename('/path/to/file')); + $this->assertEquals('file.test', StringHelper::basename('/path/to/file.test', '.test2')); + $this->assertEquals('file', StringHelper::basename('/path/to/file.test', '.test')); + + $this->assertEquals('file', StringHelper::basename('\file')); + $this->assertEquals('file.test', StringHelper::basename('\file.test', '.test2')); + $this->assertEquals('file', StringHelper::basename('\file.test', '.test')); + + $this->assertEquals('file', StringHelper::basename('C:\file')); + $this->assertEquals('file.test', StringHelper::basename('C:\file.test', '.test2')); + $this->assertEquals('file', StringHelper::basename('C:\file.test', '.test')); + + $this->assertEquals('file', StringHelper::basename('C:\path\to\file')); + $this->assertEquals('file.test', StringHelper::basename('C:\path\to\file.test', '.test2')); + $this->assertEquals('file', StringHelper::basename('C:\path\to\file.test', '.test')); + + // mixed paths + $this->assertEquals('file.test', StringHelper::basename('/path\to/file.test')); + $this->assertEquals('file.test', StringHelper::basename('/path/to\file.test')); + $this->assertEquals('file.test', StringHelper::basename('\path/to\file.test')); + + // \ and / in suffix + $this->assertEquals('file', StringHelper::basename('/path/to/filete/st', 'te/st')); + $this->assertEquals('st', StringHelper::basename('/path/to/filete/st', 'te\st')); + $this->assertEquals('file', StringHelper::basename('/path/to/filete\st', 'te\st')); + $this->assertEquals('st', StringHelper::basename('/path/to/filete\st', 'te/st')); + + // http://www.php.net/manual/en/function.basename.php#72254 + $this->assertEquals('foo', StringHelper::basename('/bar/foo/')); + $this->assertEquals('foo', StringHelper::basename('\\bar\\foo\\')); + } } diff --git a/tests/unit/framework/i18n/GettextMessageSourceTest.php b/tests/unit/framework/i18n/GettextMessageSourceTest.php new file mode 100644 index 0000000..7b499f4 --- /dev/null +++ b/tests/unit/framework/i18n/GettextMessageSourceTest.php @@ -0,0 +1,14 @@ +markTestSkipped(); + } +} diff --git a/tests/unit/framework/i18n/GettextMoFileTest.php b/tests/unit/framework/i18n/GettextMoFileTest.php new file mode 100644 index 0000000..0aa22da --- /dev/null +++ b/tests/unit/framework/i18n/GettextMoFileTest.php @@ -0,0 +1,95 @@ +load($moFilePath, 'context1'); + $context2 = $moFile->load($moFilePath, 'context2'); + + // item count + $this->assertCount(3, $context1); + $this->assertCount(2, $context2); + + // original messages + $this->assertArrayNotHasKey("Missing\n\r\t\"translation.", $context1); + $this->assertArrayHasKey("Aliquam tempus elit vel purus molestie placerat. In sollicitudin tincidunt\naliquet. Integer tincidunt gravida tempor. In convallis blandit dui vel malesuada.\nNunc vel sapien nunc, a pretium nulla.", $context1); + $this->assertArrayHasKey("String number two.", $context1); + $this->assertArrayHasKey("Nunc vel sapien nunc, a pretium nulla.\nPellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.", $context1); + + $this->assertArrayHasKey("The other\n\ncontext.\n", $context2); + $this->assertArrayHasKey("test1\\ntest2\n\\\ntest3", $context2); + + // translated messages + $this->assertFalse(in_array("", $context1)); + $this->assertTrue(in_array("Олицетворение однократно. Представленный лексико-семантический анализ является\nпсихолингвистическим в своей основе, но механизм сочленений полидисперсен. Впечатление\nоднократно. Различное расположение выбирает сюжетный механизм сочленений.", $context1)); + $this->assertTrue(in_array('Строка номер два.', $context1)); + $this->assertTrue(in_array('Короткий перевод.', $context1)); + + $this->assertTrue(in_array("Другой\n\nконтекст.\n", $context2)); + $this->assertTrue(in_array("тест1\\nтест2\n\\\nтест3", $context2)); + } + + public function testSave() + { + // initial data + $s = chr(4); + $messages = array( + 'Hello!' => 'Привет!', + "context1{$s}Hello?" => 'Привет?', + 'Hello!?' => '', + "context1{$s}Hello!?!" => '', + "context2{$s}\"Quotes\"" => '"Кавычки"', + "context2{$s}\nNew lines\n" => "\nПереносы строк\n", + "context2{$s}\tTabs\t" => "\tТабы\t", + "context2{$s}\rCarriage returns\r" => "\rВозвраты кареток\r", + ); + + // create temporary directory and dump messages + $poFileDirectory = __DIR__ . '/../../runtime/i18n'; + if (!is_dir($poFileDirectory)) { + mkdir($poFileDirectory); + } + if (is_file($poFileDirectory . '/test.mo')) { + unlink($poFileDirectory . '/test.mo'); + } + + $moFile = new GettextMoFile(); + $moFile->save($poFileDirectory . '/test.mo', $messages); + + // load messages + $context1 = $moFile->load($poFileDirectory . '/test.mo', 'context1'); + $context2 = $moFile->load($poFileDirectory . '/test.mo', 'context2'); + + // context1 + $this->assertCount(2, $context1); + + $this->assertArrayHasKey('Hello?', $context1); + $this->assertTrue(in_array('Привет?', $context1)); + + $this->assertArrayHasKey('Hello!?!', $context1); + $this->assertTrue(in_array('', $context1)); + + // context2 + $this->assertCount(4, $context2); + + $this->assertArrayHasKey("\"Quotes\"", $context2); + $this->assertTrue(in_array('"Кавычки"', $context2)); + + $this->assertArrayHasKey("\nNew lines\n", $context2); + $this->assertTrue(in_array("\nПереносы строк\n", $context2)); + + $this->assertArrayHasKey("\tTabs\t", $context2); + $this->assertTrue(in_array("\tТабы\t", $context2)); + + $this->assertArrayHasKey("\rCarriage returns\r", $context2); + $this->assertTrue(in_array("\rВозвраты кареток\r", $context2)); + } +} diff --git a/tests/unit/framework/i18n/GettextPoFileTest.php b/tests/unit/framework/i18n/GettextPoFileTest.php new file mode 100644 index 0000000..8dddb40 --- /dev/null +++ b/tests/unit/framework/i18n/GettextPoFileTest.php @@ -0,0 +1,95 @@ +load($poFilePath, 'context1'); + $context2 = $poFile->load($poFilePath, 'context2'); + + // item count + $this->assertCount(4, $context1); + $this->assertCount(2, $context2); + + // original messages + $this->assertArrayHasKey("Missing\n\r\t\"translation.", $context1); + $this->assertArrayHasKey("Aliquam tempus elit vel purus molestie placerat. In sollicitudin tincidunt\naliquet. Integer tincidunt gravida tempor. In convallis blandit dui vel malesuada.\nNunc vel sapien nunc, a pretium nulla.", $context1); + $this->assertArrayHasKey("String number two.", $context1); + $this->assertArrayHasKey("Nunc vel sapien nunc, a pretium nulla.\nPellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.", $context1); + + $this->assertArrayHasKey("The other\n\ncontext.\n", $context2); + $this->assertArrayHasKey("test1\\\ntest2\n\\\\\ntest3", $context2); + + // translated messages + $this->assertTrue(in_array("", $context1)); + $this->assertTrue(in_array("Олицетворение однократно. Представленный лексико-семантический анализ является\nпсихолингвистическим в своей основе, но механизм сочленений полидисперсен. Впечатление\nоднократно. Различное расположение выбирает сюжетный механизм сочленений.", $context1)); + $this->assertTrue(in_array('Строка номер два.', $context1)); + $this->assertTrue(in_array('Короткий перевод.', $context1)); + + $this->assertTrue(in_array("Другой\n\nконтекст.\n", $context2)); + $this->assertTrue(in_array("тест1\\\nтест2\n\\\\\nтест3", $context2)); + } + + public function testSave() + { + // initial data + $s = chr(4); + $messages = array( + 'Hello!' => 'Привет!', + "context1{$s}Hello?" => 'Привет?', + 'Hello!?' => '', + "context1{$s}Hello!?!" => '', + "context2{$s}\"Quotes\"" => '"Кавычки"', + "context2{$s}\nNew lines\n" => "\nПереносы строк\n", + "context2{$s}\tTabs\t" => "\tТабы\t", + "context2{$s}\rCarriage returns\r" => "\rВозвраты кареток\r", + ); + + // create temporary directory and dump messages + $poFileDirectory = __DIR__ . '/../../runtime/i18n'; + if (!is_dir($poFileDirectory)) { + mkdir($poFileDirectory); + } + if (is_file($poFileDirectory . '/test.po')) { + unlink($poFileDirectory . '/test.po'); + } + + $poFile = new GettextPoFile(); + $poFile->save($poFileDirectory . '/test.po', $messages); + + // load messages + $context1 = $poFile->load($poFileDirectory . '/test.po', 'context1'); + $context2 = $poFile->load($poFileDirectory . '/test.po', 'context2'); + + // context1 + $this->assertCount(2, $context1); + + $this->assertArrayHasKey('Hello?', $context1); + $this->assertTrue(in_array('Привет?', $context1)); + + $this->assertArrayHasKey('Hello!?!', $context1); + $this->assertTrue(in_array('', $context1)); + + // context2 + $this->assertCount(4, $context2); + + $this->assertArrayHasKey("\"Quotes\"", $context2); + $this->assertTrue(in_array('"Кавычки"', $context2)); + + $this->assertArrayHasKey("\nNew lines\n", $context2); + $this->assertTrue(in_array("\nПереносы строк\n", $context2)); + + $this->assertArrayHasKey("\tTabs\t", $context2); + $this->assertTrue(in_array("\tТабы\t", $context2)); + + $this->assertArrayHasKey("\rCarriage returns\r", $context2); + $this->assertTrue(in_array("\rВозвраты кареток\r", $context2)); + } +} diff --git a/tests/unit/framework/rbac/ManagerTestBase.php b/tests/unit/framework/rbac/ManagerTestBase.php new file mode 100644 index 0000000..03d5354 --- /dev/null +++ b/tests/unit/framework/rbac/ManagerTestBase.php @@ -0,0 +1,248 @@ +auth->createItem($name, $type, $description, $bizRule, $data); + $this->assertTrue($item instanceof Item); + $this->assertEquals($item->type, $type); + $this->assertEquals($item->name, $name); + $this->assertEquals($item->description, $description); + $this->assertEquals($item->bizRule, $bizRule); + $this->assertEquals($item->data, $data); + + // test shortcut + $name2 = 'createUser'; + $item2 = $this->auth->createRole($name2, $description, $bizRule, $data); + $this->assertEquals($item2->type, Item::TYPE_ROLE); + + // test adding an item with the same name + $this->setExpectedException('Exception'); + $this->auth->createItem($name, $type, $description, $bizRule, $data); + } + + public function testGetItem() + { + $this->assertTrue($this->auth->getItem('readPost') instanceof Item); + $this->assertTrue($this->auth->getItem('reader') instanceof Item); + $this->assertNull($this->auth->getItem('unknown')); + } + + public function testRemoveAuthItem() + { + $this->assertTrue($this->auth->getItem('updatePost') instanceof Item); + $this->assertTrue($this->auth->removeItem('updatePost')); + $this->assertNull($this->auth->getItem('updatePost')); + $this->assertFalse($this->auth->removeItem('updatePost')); + } + + public function testChangeItemName() + { + $item = $this->auth->getItem('readPost'); + $this->assertTrue($item instanceof Item); + $this->assertTrue($this->auth->hasItemChild('reader', 'readPost')); + $item->name = 'readPost2'; + $this->assertNull($this->auth->getItem('readPost')); + $this->assertEquals($this->auth->getItem('readPost2'), $item); + $this->assertFalse($this->auth->hasItemChild('reader', 'readPost')); + $this->assertTrue($this->auth->hasItemChild('reader', 'readPost2')); + } + + public function testAddItemChild() + { + $this->auth->addItemChild('createPost', 'updatePost'); + + // test adding upper level item to lower one + $this->setExpectedException('Exception'); + $this->auth->addItemChild('readPost', 'reader'); + } + + public function testAddItemChild2() + { + // test adding inexistent items + $this->setExpectedException('Exception'); + $this->assertFalse($this->auth->addItemChild('createPost2', 'updatePost')); + } + + public function testRemoveItemChild() + { + $this->assertTrue($this->auth->hasItemChild('reader', 'readPost')); + $this->assertTrue($this->auth->removeItemChild('reader', 'readPost')); + $this->assertFalse($this->auth->hasItemChild('reader', 'readPost')); + $this->assertFalse($this->auth->removeItemChild('reader', 'readPost')); + } + + public function testGetItemChildren() + { + $this->assertEquals(array(), $this->auth->getItemChildren('readPost')); + $children = $this->auth->getItemChildren('author'); + $this->assertEquals(3, count($children)); + $this->assertTrue(reset($children) instanceof Item); + } + + public function testAssign() + { + $auth = $this->auth->assign('new user', 'createPost', 'rule', 'data'); + $this->assertTrue($auth instanceof Assignment); + $this->assertEquals($auth->userId, 'new user'); + $this->assertEquals($auth->itemName, 'createPost'); + $this->assertEquals($auth->bizRule, 'rule'); + $this->assertEquals($auth->data, 'data'); + + $this->setExpectedException('Exception'); + $this->auth->assign('new user', 'createPost2', 'rule', 'data'); + } + + public function testRevoke() + { + $this->assertTrue($this->auth->isAssigned('author B', 'author')); + $auth = $this->auth->getAssignment('author B', 'author'); + $this->assertTrue($auth instanceof Assignment); + $this->assertTrue($this->auth->revoke('author B', 'author')); + $this->assertFalse($this->auth->isAssigned('author B', 'author')); + $this->assertFalse($this->auth->revoke('author B', 'author')); + } + + public function testGetAssignments() + { + $this->auth->assign('author B', 'deletePost'); + $auths = $this->auth->getAssignments('author B'); + $this->assertEquals(2, count($auths)); + $this->assertTrue(reset($auths) instanceof Assignment); + } + + public function testGetItems() + { + $this->assertEquals(count($this->auth->getRoles()), 4); + $this->assertEquals(count($this->auth->getOperations()), 4); + $this->assertEquals(count($this->auth->getTasks()), 1); + $this->assertEquals(count($this->auth->getItems()), 9); + + $this->assertEquals(count($this->auth->getItems('author B', null)), 1); + $this->assertEquals(count($this->auth->getItems('author C', null)), 0); + $this->assertEquals(count($this->auth->getItems('author B', Item::TYPE_ROLE)), 1); + $this->assertEquals(count($this->auth->getItems('author B', Item::TYPE_OPERATION)), 0); + } + + public function testClearAll() + { + $this->auth->clearAll(); + $this->assertEquals(count($this->auth->getRoles()), 0); + $this->assertEquals(count($this->auth->getOperations()), 0); + $this->assertEquals(count($this->auth->getTasks()), 0); + $this->assertEquals(count($this->auth->getItems()), 0); + $this->assertEquals(count($this->auth->getAssignments('author B')), 0); + } + + public function testClearAssignments() + { + $this->auth->clearAssignments(); + $this->assertEquals(count($this->auth->getAssignments('author B')), 0); + } + + public function testDetectLoop() + { + $this->setExpectedException('Exception'); + $this->auth->addItemChild('readPost', 'readPost'); + } + + public function testExecuteBizRule() + { + $this->assertTrue($this->auth->executeBizRule(null, array(), null)); + $this->assertTrue($this->auth->executeBizRule('return 1==true;', array(), null)); + $this->assertTrue($this->auth->executeBizRule('return $params[0]==$params[1];', array(1, '1'), null)); + $this->assertFalse($this->auth->executeBizRule('invalid', array(), null)); + } + + public function testCheckAccess() + { + $results = array( + 'reader A' => array( + 'createPost' => false, + 'readPost' => true, + 'updatePost' => false, + 'updateOwnPost' => false, + 'deletePost' => false, + ), + 'author B' => array( + 'createPost' => true, + 'readPost' => true, + 'updatePost' => true, + 'updateOwnPost' => true, + 'deletePost' => false, + ), + 'editor C' => array( + 'createPost' => false, + 'readPost' => true, + 'updatePost' => true, + 'updateOwnPost' => false, + 'deletePost' => false, + ), + 'admin D' => array( + 'createPost' => true, + 'readPost' => true, + 'updatePost' => true, + 'updateOwnPost' => false, + 'deletePost' => true, + ), + ); + + $params = array('authorID' => 'author B'); + + foreach (array('reader A', 'author B', 'editor C', 'admin D') as $user) { + $params['userID'] = $user; + foreach (array('createPost', 'readPost', 'updatePost', 'updateOwnPost', 'deletePost') as $operation) { + $result = $this->auth->checkAccess($user, $operation, $params); + $this->assertEquals($results[$user][$operation], $result); + } + } + } + + protected function prepareData() + { + $this->auth->createOperation('createPost', 'create a post'); + $this->auth->createOperation('readPost', 'read a post'); + $this->auth->createOperation('updatePost', 'update a post'); + $this->auth->createOperation('deletePost', 'delete a post'); + + $task = $this->auth->createTask('updateOwnPost', 'update a post by author himself', 'return $params["authorID"]==$params["userID"];'); + $task->addChild('updatePost'); + + $role = $this->auth->createRole('reader'); + $role->addChild('readPost'); + + $role = $this->auth->createRole('author'); + $role->addChild('reader'); + $role->addChild('createPost'); + $role->addChild('updateOwnPost'); + + $role = $this->auth->createRole('editor'); + $role->addChild('reader'); + $role->addChild('updatePost'); + + $role = $this->auth->createRole('admin'); + $role->addChild('editor'); + $role->addChild('author'); + $role->addChild('deletePost'); + + $this->auth->assign('reader A', 'reader'); + $this->auth->assign('author B', 'author'); + $this->auth->assign('editor C', 'editor'); + $this->auth->assign('admin D', 'admin'); + } +} diff --git a/tests/unit/framework/rbac/PhpManagerTest.php b/tests/unit/framework/rbac/PhpManagerTest.php new file mode 100644 index 0000000..69fdd41 --- /dev/null +++ b/tests/unit/framework/rbac/PhpManagerTest.php @@ -0,0 +1,34 @@ +getRuntimePath() . '/rbac.php'; + @unlink($authFile); + $this->auth = new PhpManager; + $this->auth->authFile = $authFile; + $this->auth->init(); + $this->prepareData(); + } + + public function tearDown() + { + @unlink($this->auth->authFile); + } + + public function testSaveLoad() + { + $this->auth->save(); + $this->auth->clearAll(); + $this->auth->load(); + $this->testCheckAccess(); + } +} diff --git a/tests/web/app/index.php b/tests/web/app/index.php index 4cfa1ab..7096665 100644 --- a/tests/web/app/index.php +++ b/tests/web/app/index.php @@ -1,6 +1,6 @@ run(); diff --git a/framework/.htaccess b/yii/.htaccess similarity index 100% rename from framework/.htaccess rename to yii/.htaccess diff --git a/framework/yii.php b/yii/Yii.php similarity index 100% rename from framework/yii.php rename to yii/Yii.php diff --git a/framework/YiiBase.php b/yii/YiiBase.php similarity index 98% rename from framework/YiiBase.php rename to yii/YiiBase.php index c911f78..1a3f50c 100644 --- a/framework/YiiBase.php +++ b/yii/YiiBase.php @@ -158,8 +158,8 @@ class YiiBase { foreach ($namespaces as $name => $path) { if ($name !== '') { - $name = '@' . str_replace('\\', '/', $name); - static::setAlias($name, $path); + $name = trim(strtr($name, array('\\' => '/', '_' => '/')), '/'); + static::setAlias('@' . $name, rtrim($path, '/\\') . '/' . $name); } } } @@ -370,7 +370,8 @@ class YiiBase include($classFile); - if (class_exists($className, false) || interface_exists($className, false)) { + if (class_exists($className, false) || interface_exists($className, false) || + function_exists('trait_exists') && trait_exists($className, false)) { return true; } else { throw new UnknownClassException("Unable to find '$className' in file: $classFile"); @@ -451,12 +452,12 @@ class YiiBase } $args = func_get_args(); array_shift($args); // remove $config - if ($config !== array()) { + if (!empty($config)) { $args[] = $config; } return $reflection->newInstanceArgs($args); } else { - return $config === array() ? new $class : new $class($config); + return empty($config) ? new $class : new $class($config); } } diff --git a/framework/assets.php b/yii/assets.php similarity index 66% rename from framework/assets.php rename to yii/assets.php index 919011b..7ee177d 100644 --- a/framework/assets.php +++ b/yii/assets.php @@ -28,4 +28,18 @@ return array( ), 'depends' => array('yii', 'yii/validation'), ), + 'yii/captcha' => array( + 'sourcePath' => __DIR__ . '/assets', + 'js' => array( + 'yii.captcha.js', + ), + 'depends' => array('yii'), + ), + 'yii/debug' => array( + 'sourcePath' => __DIR__ . '/assets', + 'js' => array( + 'yii.debug.js', + ), + 'depends' => array('yii'), + ), ); diff --git a/framework/assets/jquery.min.js b/yii/assets/jquery.min.js similarity index 100% rename from framework/assets/jquery.min.js rename to yii/assets/jquery.min.js diff --git a/framework/assets/yii.activeForm.js b/yii/assets/yii.activeForm.js similarity index 99% rename from framework/assets/yii.activeForm.js rename to yii/assets/yii.activeForm.js index 158ea74..d987879 100644 --- a/framework/assets/yii.activeForm.js +++ b/yii/assets/yii.activeForm.js @@ -116,8 +116,8 @@ }); }, - options: function() { - return this.data('yiiActiveForm').settings; + data: function() { + return this.data('yiiActiveForm'); }, submitForm: function () { @@ -384,4 +384,4 @@ } }; -})(window.jQuery); \ No newline at end of file +})(window.jQuery); diff --git a/yii/assets/yii.captcha.js b/yii/assets/yii.captcha.js new file mode 100644 index 0000000..9211edb --- /dev/null +++ b/yii/assets/yii.captcha.js @@ -0,0 +1,72 @@ +/** + * Yii Captcha widget. + * + * This is the JavaScript widget used by the yii\widgets\Captcha widget. + * + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + * @author Qiang Xue + * @since 2.0 + */ +(function ($) { + $.fn.yiiCaptcha = function (method) { + if (methods[method]) { + return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); + } else if (typeof method === 'object' || !method) { + return methods.init.apply(this, arguments); + } else { + $.error('Method ' + method + ' does not exist on jQuery.yiiCaptcha'); + return false; + } + }; + + var defaults = { + refreshUrl: undefined, + hashKey: undefined + }; + + var methods = { + init: function (options) { + return this.each(function () { + var $e = $(this); + var settings = $.extend({}, defaults, options || {}); + $e.data('yiiCaptcha', { + settings: settings + }); + + $e.on('click.yiiCaptcha', function() { + methods.refresh.apply($e); + return false; + }); + + }); + }, + + refresh: function () { + var $e = this, + settings = this.data('yiiCaptcha').settings; + $.ajax({ + url: $e.data('yiiCaptcha').settings.refreshUrl, + dataType: 'json', + cache: false, + success: function(data) { + $e.attr('src', data['url']); + $('body').data(settings.hashKey, [data['hash1'], data['hash2']]); + } + }); + }, + + destroy: function () { + return this.each(function () { + $(window).unbind('.yiiCaptcha'); + $(this).removeData('yiiCaptcha'); + }); + }, + + data: function() { + return this.data('yiiCaptcha'); + } + }; +})(window.jQuery); + diff --git a/yii/assets/yii.debug.js b/yii/assets/yii.debug.js new file mode 100644 index 0000000..4e32d89 --- /dev/null +++ b/yii/assets/yii.debug.js @@ -0,0 +1,26 @@ +/** + * Yii debug module. + * + * This JavaScript module provides the functions needed by the Yii debug toolbar. + * + * @link http://www.yiiframework.com/ + * @copyright Copyright (c) 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + * @author Qiang Xue + * @since 2.0 + */ + +yii.debug = (function ($) { + return { + load: function (id, url) { + $.ajax({ + url: url, + //dataType: 'json', + success: function(data) { + var $e = $('#' + id); + $e.html(data); + } + }); + } + }; +})(jQuery); diff --git a/framework/assets/yii.js b/yii/assets/yii.js similarity index 100% rename from framework/assets/yii.js rename to yii/assets/yii.js diff --git a/framework/assets/yii.validation.js b/yii/assets/yii.validation.js similarity index 99% rename from framework/assets/yii.validation.js rename to yii/assets/yii.validation.js index fd098be..5fa8492 100644 --- a/framework/assets/yii.validation.js +++ b/yii/assets/yii.validation.js @@ -1,7 +1,7 @@ /** * Yii validation module. * - * This JavaScript module provides the validation methods for the built-in validaotrs. + * This JavaScript module provides the validation methods for the built-in validators. * * @link http://www.yiiframework.com/ * @copyright Copyright (c) 2008 Yii Software LLC diff --git a/framework/base/Action.php b/yii/base/Action.php similarity index 100% rename from framework/base/Action.php rename to yii/base/Action.php diff --git a/framework/base/ActionEvent.php b/yii/base/ActionEvent.php similarity index 100% rename from framework/base/ActionEvent.php rename to yii/base/ActionEvent.php diff --git a/framework/base/ActionFilter.php b/yii/base/ActionFilter.php similarity index 100% rename from framework/base/ActionFilter.php rename to yii/base/ActionFilter.php diff --git a/framework/base/Application.php b/yii/base/Application.php similarity index 96% rename from framework/base/Application.php rename to yii/base/Application.php index 5b92f76..fb9a6c1 100644 --- a/framework/base/Application.php +++ b/yii/base/Application.php @@ -8,7 +8,6 @@ namespace yii\base; use Yii; -use yii\helpers\FileHelper; /** * Application is the base class for all application classes. @@ -85,6 +84,13 @@ class Application extends Module } else { throw new InvalidConfigException('The "basePath" configuration is required.'); } + + if (isset($config['timeZone'])) { + $this->setTimeZone($config['timeZone']); + unset($config['timeZone']); + } elseif (!ini_get('date.timezone')) { + $this->setTimeZone('UTC'); + } $this->registerErrorHandlers(); $this->registerCoreComponents(); @@ -223,6 +229,8 @@ class Application extends Module /** * Returns the time zone used by this application. * This is a simple wrapper of PHP function date_default_timezone_get(). + * If time zone is not configured in php.ini or application config, + * it will be set to UTC by default. * @return string the time zone used by this application. * @see http://php.net/manual/en/function.date-default-timezone-get.php */ @@ -306,12 +314,12 @@ class Application extends Module } /** - * @return null|Component - * @todo + * Returns the auth manager for this application. + * @return \yii\rbac\Manager the auth manager for this application. */ public function getAuthManager() { - return $this->getComponent('auth'); + return $this->getComponent('authManager'); } /** diff --git a/framework/base/Behavior.php b/yii/base/Behavior.php similarity index 100% rename from framework/base/Behavior.php rename to yii/base/Behavior.php diff --git a/framework/base/Component.php b/yii/base/Component.php similarity index 100% rename from framework/base/Component.php rename to yii/base/Component.php diff --git a/framework/base/Controller.php b/yii/base/Controller.php similarity index 99% rename from framework/base/Controller.php rename to yii/base/Controller.php index 583de60..3b16a93 100644 --- a/framework/base/Controller.php +++ b/yii/base/Controller.php @@ -8,7 +8,6 @@ namespace yii\base; use Yii; -use yii\helpers\FileHelper; use yii\helpers\StringHelper; /** @@ -183,7 +182,7 @@ class Controller extends Component } } - if ($missing !== array()) { + if (!empty($missing)) { throw new InvalidRequestException(Yii::t('yii|Missing required parameters: {params}', array( '{params}' => implode(', ', $missing), ))); @@ -204,7 +203,7 @@ class Controller extends Component public function forward($route, $params = array()) { $status = $this->run($route, $params); - exit($status); + Yii::$app->end($status); } /** diff --git a/framework/base/Dictionary.php b/yii/base/Dictionary.php similarity index 99% rename from framework/base/Dictionary.php rename to yii/base/Dictionary.php index 52262cb..5807d93 100644 --- a/framework/base/Dictionary.php +++ b/yii/base/Dictionary.php @@ -51,7 +51,7 @@ class Dictionary extends Object implements \IteratorAggregate, \ArrayAccess, \Co */ public function __construct($data = array(), $config = array()) { - if ($data !== array()) { + if (!empty($data)) { $this->copyFrom($data); } parent::__construct($config); @@ -187,7 +187,7 @@ class Dictionary extends Object implements \IteratorAggregate, \ArrayAccess, \Co public function copyFrom($data) { if (is_array($data) || $data instanceof \Traversable) { - if ($this->_d !== array()) { + if (!empty($this->_d)) { $this->removeAll(); } if ($data instanceof self) { diff --git a/framework/base/DictionaryIterator.php b/yii/base/DictionaryIterator.php similarity index 100% rename from framework/base/DictionaryIterator.php rename to yii/base/DictionaryIterator.php diff --git a/framework/base/ErrorException.php b/yii/base/ErrorException.php similarity index 100% rename from framework/base/ErrorException.php rename to yii/base/ErrorException.php diff --git a/framework/base/ErrorHandler.php b/yii/base/ErrorHandler.php similarity index 100% rename from framework/base/ErrorHandler.php rename to yii/base/ErrorHandler.php diff --git a/framework/base/Event.php b/yii/base/Event.php similarity index 100% rename from framework/base/Event.php rename to yii/base/Event.php diff --git a/framework/base/Exception.php b/yii/base/Exception.php similarity index 100% rename from framework/base/Exception.php rename to yii/base/Exception.php diff --git a/framework/base/HttpException.php b/yii/base/HttpException.php similarity index 100% rename from framework/base/HttpException.php rename to yii/base/HttpException.php diff --git a/framework/base/InlineAction.php b/yii/base/InlineAction.php similarity index 100% rename from framework/base/InlineAction.php rename to yii/base/InlineAction.php diff --git a/framework/base/InvalidCallException.php b/yii/base/InvalidCallException.php similarity index 100% rename from framework/base/InvalidCallException.php rename to yii/base/InvalidCallException.php diff --git a/framework/base/InvalidConfigException.php b/yii/base/InvalidConfigException.php similarity index 100% rename from framework/base/InvalidConfigException.php rename to yii/base/InvalidConfigException.php diff --git a/framework/base/InvalidParamException.php b/yii/base/InvalidParamException.php similarity index 100% rename from framework/base/InvalidParamException.php rename to yii/base/InvalidParamException.php diff --git a/framework/base/InvalidRequestException.php b/yii/base/InvalidRequestException.php similarity index 100% rename from framework/base/InvalidRequestException.php rename to yii/base/InvalidRequestException.php diff --git a/framework/base/InvalidRouteException.php b/yii/base/InvalidRouteException.php similarity index 100% rename from framework/base/InvalidRouteException.php rename to yii/base/InvalidRouteException.php diff --git a/framework/base/Model.php b/yii/base/Model.php similarity index 100% rename from framework/base/Model.php rename to yii/base/Model.php diff --git a/framework/base/ModelEvent.php b/yii/base/ModelEvent.php similarity index 100% rename from framework/base/ModelEvent.php rename to yii/base/ModelEvent.php diff --git a/framework/base/Module.php b/yii/base/Module.php similarity index 100% rename from framework/base/Module.php rename to yii/base/Module.php diff --git a/framework/base/NotSupportedException.php b/yii/base/NotSupportedException.php similarity index 100% rename from framework/base/NotSupportedException.php rename to yii/base/NotSupportedException.php diff --git a/framework/base/Object.php b/yii/base/Object.php similarity index 98% rename from framework/base/Object.php rename to yii/base/Object.php index a547990..a90a231 100644 --- a/framework/base/Object.php +++ b/yii/base/Object.php @@ -15,6 +15,14 @@ namespace yii\base; class Object { /** + * @return string the fully qualified name of this class. + */ + public static function className() + { + return get_called_class(); + } + + /** * Constructor. * The default implementation does two things: * diff --git a/framework/base/Request.php b/yii/base/Request.php similarity index 100% rename from framework/base/Request.php rename to yii/base/Request.php diff --git a/framework/base/Response.php b/yii/base/Response.php similarity index 100% rename from framework/base/Response.php rename to yii/base/Response.php diff --git a/framework/base/Theme.php b/yii/base/Theme.php similarity index 100% rename from framework/base/Theme.php rename to yii/base/Theme.php diff --git a/framework/base/UnknownClassException.php b/yii/base/UnknownClassException.php similarity index 100% rename from framework/base/UnknownClassException.php rename to yii/base/UnknownClassException.php diff --git a/framework/base/UnknownMethodException.php b/yii/base/UnknownMethodException.php similarity index 100% rename from framework/base/UnknownMethodException.php rename to yii/base/UnknownMethodException.php diff --git a/framework/base/UnknownPropertyException.php b/yii/base/UnknownPropertyException.php similarity index 100% rename from framework/base/UnknownPropertyException.php rename to yii/base/UnknownPropertyException.php diff --git a/framework/base/UserException.php b/yii/base/UserException.php similarity index 100% rename from framework/base/UserException.php rename to yii/base/UserException.php diff --git a/framework/base/Vector.php b/yii/base/Vector.php similarity index 99% rename from framework/base/Vector.php rename to yii/base/Vector.php index 7d43fdb..6418077 100644 --- a/framework/base/Vector.php +++ b/yii/base/Vector.php @@ -58,7 +58,7 @@ class Vector extends Object implements \IteratorAggregate, \ArrayAccess, \Counta */ public function __construct($data = array(), $config = array()) { - if ($data !== array()) { + if (!empty($data)) { $this->copyFrom($data); } parent::__construct($config); diff --git a/framework/base/VectorIterator.php b/yii/base/VectorIterator.php similarity index 100% rename from framework/base/VectorIterator.php rename to yii/base/VectorIterator.php diff --git a/framework/base/View.php b/yii/base/View.php similarity index 98% rename from framework/base/View.php rename to yii/base/View.php index af1f0d5..af65c49 100644 --- a/framework/base/View.php +++ b/yii/base/View.php @@ -23,6 +23,14 @@ use yii\helpers\Html; class View extends Component { /** + * @event ViewEvent an event that is triggered by [[beginPage()]]. + */ + const EVENT_BEGIN_PAGE = 'beginPage'; + /** + * @event ViewEvent an event that is triggered by [[endPage()]]. + */ + const EVENT_END_PAGE = 'endPage'; + /** * @event ViewEvent an event that is triggered by [[renderFile()]] right before it renders a view file. */ const EVENT_BEFORE_RENDER = 'beforeRender'; @@ -555,6 +563,8 @@ class View extends Component { ob_start(); ob_implicit_flush(false); + + $this->trigger(self::EVENT_BEGIN_PAGE); } /** @@ -562,6 +572,8 @@ class View extends Component */ public function endPage() { + $this->trigger(self::EVENT_END_PAGE); + $content = ob_get_clean(); echo strtr($content, array( self::PL_HEAD => $this->renderHeadHtml(), diff --git a/framework/base/ViewEvent.php b/yii/base/ViewEvent.php similarity index 100% rename from framework/base/ViewEvent.php rename to yii/base/ViewEvent.php diff --git a/framework/base/ViewRenderer.php b/yii/base/ViewRenderer.php similarity index 100% rename from framework/base/ViewRenderer.php rename to yii/base/ViewRenderer.php diff --git a/framework/base/Widget.php b/yii/base/Widget.php similarity index 100% rename from framework/base/Widget.php rename to yii/base/Widget.php diff --git a/framework/caching/ApcCache.php b/yii/caching/ApcCache.php similarity index 100% rename from framework/caching/ApcCache.php rename to yii/caching/ApcCache.php diff --git a/framework/caching/Cache.php b/yii/caching/Cache.php similarity index 100% rename from framework/caching/Cache.php rename to yii/caching/Cache.php diff --git a/framework/caching/ChainedDependency.php b/yii/caching/ChainedDependency.php similarity index 100% rename from framework/caching/ChainedDependency.php rename to yii/caching/ChainedDependency.php diff --git a/framework/caching/DbCache.php b/yii/caching/DbCache.php similarity index 100% rename from framework/caching/DbCache.php rename to yii/caching/DbCache.php diff --git a/framework/caching/DbDependency.php b/yii/caching/DbDependency.php similarity index 100% rename from framework/caching/DbDependency.php rename to yii/caching/DbDependency.php diff --git a/framework/caching/Dependency.php b/yii/caching/Dependency.php similarity index 100% rename from framework/caching/Dependency.php rename to yii/caching/Dependency.php diff --git a/framework/caching/DummyCache.php b/yii/caching/DummyCache.php similarity index 100% rename from framework/caching/DummyCache.php rename to yii/caching/DummyCache.php diff --git a/framework/caching/ExpressionDependency.php b/yii/caching/ExpressionDependency.php similarity index 100% rename from framework/caching/ExpressionDependency.php rename to yii/caching/ExpressionDependency.php diff --git a/framework/caching/FileCache.php b/yii/caching/FileCache.php similarity index 100% rename from framework/caching/FileCache.php rename to yii/caching/FileCache.php diff --git a/framework/caching/FileDependency.php b/yii/caching/FileDependency.php similarity index 100% rename from framework/caching/FileDependency.php rename to yii/caching/FileDependency.php diff --git a/framework/caching/MemCache.php b/yii/caching/MemCache.php similarity index 100% rename from framework/caching/MemCache.php rename to yii/caching/MemCache.php diff --git a/framework/caching/MemCacheServer.php b/yii/caching/MemCacheServer.php similarity index 100% rename from framework/caching/MemCacheServer.php rename to yii/caching/MemCacheServer.php diff --git a/framework/caching/WinCache.php b/yii/caching/WinCache.php similarity index 100% rename from framework/caching/WinCache.php rename to yii/caching/WinCache.php diff --git a/framework/caching/XCache.php b/yii/caching/XCache.php similarity index 100% rename from framework/caching/XCache.php rename to yii/caching/XCache.php diff --git a/framework/caching/ZendDataCache.php b/yii/caching/ZendDataCache.php similarity index 100% rename from framework/caching/ZendDataCache.php rename to yii/caching/ZendDataCache.php diff --git a/framework/console/Application.php b/yii/console/Application.php similarity index 100% rename from framework/console/Application.php rename to yii/console/Application.php diff --git a/framework/console/Controller.php b/yii/console/Controller.php similarity index 53% rename from framework/console/Controller.php rename to yii/console/Controller.php index 2eaf4b8..c07d92d 100644 --- a/framework/console/Controller.php +++ b/yii/console/Controller.php @@ -11,6 +11,7 @@ use Yii; use yii\base\Action; use yii\base\InlineAction; use yii\base\InvalidRouteException; +use yii\helpers\Console; /** * Controller is the base class of console command classes. @@ -35,6 +36,14 @@ class Controller extends \yii\base\Controller public $interactive = true; /** + * @var bool whether to enable ANSI style in output. + * Setting this will affect [[ansiFormat()]], [[stdout()]] and [[stderr()]]. + * If not set it will be auto detected using [[yii\helpers\Console::streamSupportsAnsiColors()]] with STDOUT + * for [[ansiFormat()]] and [[stdout()]] and STDERR for [[stderr()]]. + */ + public $colors; + + /** * Runs an action with the specified action ID and parameters. * If the action ID is empty, the method will use [[defaultAction]]. * @param string $id the ID of the action to be executed. @@ -45,7 +54,7 @@ class Controller extends \yii\base\Controller */ public function runAction($id, $params = array()) { - if ($params !== array()) { + if (!empty($params)) { $options = $this->globalOptions(); foreach ($params as $name => $value) { if (in_array($name, $options, true)) { @@ -69,7 +78,7 @@ class Controller extends \yii\base\Controller */ public function bindActionParams($action, $params) { - if ($params !== array()) { + if (!empty($params)) { $options = $this->globalOptions(); foreach ($params as $name => $value) { if (in_array($name, $options, true)) { @@ -81,7 +90,7 @@ class Controller extends \yii\base\Controller $args = isset($params[Request::ANONYMOUS_PARAMS]) ? $params[Request::ANONYMOUS_PARAMS] : array(); unset($params[Request::ANONYMOUS_PARAMS]); - if ($params !== array()) { + if (!empty($params)) { throw new Exception(Yii::t('yii|Unknown options: {params}', array( '{params}' => implode(', ', array_keys($params)), ))); @@ -105,7 +114,7 @@ class Controller extends \yii\base\Controller } } - if ($missing !== array()) { + if (!empty($missing)) { throw new Exception(Yii::t('yii|Missing required arguments: {params}', array( '{params}' => implode(', ', $missing), ))); @@ -115,6 +124,99 @@ class Controller extends \yii\base\Controller } /** + * Formats a string with ANSI codes + * + * You may pass additional parameters using the constants defined in [[yii\helpers\base\Console]]. + * + * Example: + * ~~~ + * $this->ansiFormat('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE); + * ~~~ + * + * @param string $string the string to be formatted + * @return string + */ + public function ansiFormat($string) + { + if ($this->ansi === true || $this->ansi === null && Console::streamSupportsAnsiColors(STDOUT)) { + $args = func_get_args(); + array_shift($args); + $string = Console::ansiFormat($string, $args); + } + return $string; + } + + /** + * Prints a string to STDOUT + * + * You may optionally format the string with ANSI codes by + * passing additional parameters using the constants defined in [[yii\helpers\base\Console]]. + * + * Example: + * ~~~ + * $this->stdout('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE); + * ~~~ + * + * @param string $string the string to print + * @return int|boolean Number of bytes printed or false on error + */ + public function stdout($string) + { + if ($this->ansi === true || $this->ansi === null && Console::streamSupportsAnsiColors(STDOUT)) { + $args = func_get_args(); + array_shift($args); + $string = Console::ansiFormat($string, $args); + } + return Console::stdout($string); + } + + /** + * Prints a string to STDERR + * + * You may optionally format the string with ANSI codes by + * passing additional parameters using the constants defined in [[yii\helpers\base\Console]]. + * + * Example: + * ~~~ + * $this->stderr('This will be red and underlined.', Console::FG_RED, Console::UNDERLINE); + * ~~~ + * + * @param string $string the string to print + * @return int|boolean Number of bytes printed or false on error + */ + public function stderr($string) + { + if ($this->ansi === true || $this->ansi === null && Console::streamSupportsAnsiColors(STDERR)) { + $args = func_get_args(); + array_shift($args); + $string = Console::ansiFormat($string, $args); + } + return fwrite(STDERR, $string); + } + + /** + * Prompts the user for input and validates it + * + * @param string $text prompt string + * @param array $options the options to validate the input: + * - required: whether it is required or not + * - default: default value if no input is inserted by the user + * - pattern: regular expression pattern to validate user input + * - validator: a callable function to validate input. The function must accept two parameters: + * - $input: the user input to validate + * - $error: the error value passed by reference if validation failed. + * @return string the user input + */ + public function prompt($text, $options = array()) + { + if ($this->interactive) { + return Console::prompt($text, $options); + } else { + return isset($options['default']) ? $options['default'] : ''; + } + } + + /** * Asks user to confirm by typing y or n. * * @param string $message to echo out before waiting for user input @@ -124,15 +226,27 @@ class Controller extends \yii\base\Controller public function confirm($message, $default = false) { if ($this->interactive) { - echo $message . ' (yes|no) [' . ($default ? 'yes' : 'no') . ']:'; - $input = trim(fgets(STDIN)); - return empty($input) ? $default : !strncasecmp($input, 'y', 1); + return Console::confirm($message, $default); } else { return true; } } /** + * Gives the user an option to choose from. Giving '?' as an input will show + * a list of options to choose from and their explanations. + * + * @param string $prompt the prompt message + * @param array $options Key-value array of options to choose from + * + * @return string An option character the user chose + */ + public function select($prompt, $options = array()) + { + return Console::select($prompt, $options); + } + + /** * Returns the names of the global options for this command. * A global option requires the existence of a public member variable whose * name is the option name. diff --git a/framework/console/Exception.php b/yii/console/Exception.php similarity index 100% rename from framework/console/Exception.php rename to yii/console/Exception.php diff --git a/framework/console/Request.php b/yii/console/Request.php similarity index 100% rename from framework/console/Request.php rename to yii/console/Request.php diff --git a/framework/console/controllers/AppController.php b/yii/console/controllers/AppController.php similarity index 100% rename from framework/console/controllers/AppController.php rename to yii/console/controllers/AppController.php diff --git a/framework/console/controllers/AssetController.php b/yii/console/controllers/AssetController.php similarity index 100% rename from framework/console/controllers/AssetController.php rename to yii/console/controllers/AssetController.php diff --git a/framework/console/controllers/CacheController.php b/yii/console/controllers/CacheController.php similarity index 100% rename from framework/console/controllers/CacheController.php rename to yii/console/controllers/CacheController.php diff --git a/framework/console/controllers/HelpController.php b/yii/console/controllers/HelpController.php similarity index 99% rename from framework/console/controllers/HelpController.php rename to yii/console/controllers/HelpController.php index 82bd6fe..6a66fd0 100644 --- a/framework/console/controllers/HelpController.php +++ b/yii/console/controllers/HelpController.php @@ -142,7 +142,7 @@ class HelpController extends Controller protected function getHelp() { $commands = $this->getCommands(); - if ($commands !== array()) { + if (!empty($commands)) { echo "The following commands are available:\n\n"; foreach ($commands as $command) { echo "* $command\n"; @@ -172,7 +172,7 @@ class HelpController extends Controller } $actions = $this->getActions($controller); - if ($actions !== array()) { + if (!empty($actions)) { echo "\nSUB-COMMANDS\n\n"; $prefix = $controller->getUniqueId(); foreach ($actions as $action) { @@ -280,7 +280,7 @@ class HelpController extends Controller } $options = $this->getOptionHelps($controller); - if ($options !== array()) { + if (!empty($options)) { echo "\nOPTIONS\n\n"; echo implode("\n\n", $options) . "\n\n"; } diff --git a/framework/console/controllers/MessageController.php b/yii/console/controllers/MessageController.php similarity index 100% rename from framework/console/controllers/MessageController.php rename to yii/console/controllers/MessageController.php diff --git a/framework/console/controllers/MigrateController.php b/yii/console/controllers/MigrateController.php similarity index 95% rename from framework/console/controllers/MigrateController.php rename to yii/console/controllers/MigrateController.php index 3f816f1..fb06c66 100644 --- a/framework/console/controllers/MigrateController.php +++ b/yii/console/controllers/MigrateController.php @@ -144,7 +144,8 @@ class MigrateController extends Controller */ public function actionUp($limit = 0) { - if (($migrations = $this->getNewMigrations()) === array()) { + $migrations = $this->getNewMigrations(); + if (empty($migrations)) { echo "No new migration found. Your system is up-to-date.\n"; Yii::$app->end(); } @@ -198,7 +199,8 @@ class MigrateController extends Controller throw new Exception("The step argument must be greater than 0."); } - if (($migrations = $this->getMigrationHistory($limit)) === array()) { + $migrations = $this->getMigrationHistory($limit); + if (empty($migrations)) { echo "No migration has been done before.\n"; return; } @@ -244,7 +246,8 @@ class MigrateController extends Controller throw new Exception("The step argument must be greater than 0."); } - if (($migrations = $this->getMigrationHistory($limit)) === array()) { + $migrations = $this->getMigrationHistory($limit); + if (empty($migrations)) { echo "No migration has been done before.\n"; return; } @@ -407,7 +410,7 @@ class MigrateController extends Controller { $limit = (int)$limit; $migrations = $this->getMigrationHistory($limit); - if ($migrations === array()) { + if (empty($migrations)) { echo "No migration has been done before.\n"; } else { $n = count($migrations); @@ -441,7 +444,7 @@ class MigrateController extends Controller { $limit = (int)$limit; $migrations = $this->getNewMigrations(); - if ($migrations === array()) { + if (empty($migrations)) { echo "No new migrations found. Your system is up-to-date.\n"; } else { $n = count($migrations); diff --git a/framework/console/runtime/.gitignore b/yii/console/runtime/.gitignore similarity index 100% rename from framework/console/runtime/.gitignore rename to yii/console/runtime/.gitignore diff --git a/framework/db/ActiveQuery.php b/yii/db/ActiveQuery.php similarity index 99% rename from framework/db/ActiveQuery.php rename to yii/db/ActiveQuery.php index 3999600..dac94c8 100644 --- a/framework/db/ActiveQuery.php +++ b/yii/db/ActiveQuery.php @@ -103,7 +103,7 @@ class ActiveQuery extends Query { $command = $this->createCommand(); $rows = $command->queryAll(); - if ($rows !== array()) { + if (!empty($rows)) { $models = $this->createModels($rows); if (!empty($this->with)) { $this->populateRelations($models, $this->with); diff --git a/framework/db/ActiveRecord.php b/yii/db/ActiveRecord.php similarity index 95% rename from framework/db/ActiveRecord.php rename to yii/db/ActiveRecord.php index 709d139..356819d 100644 --- a/framework/db/ActiveRecord.php +++ b/yii/db/ActiveRecord.php @@ -28,7 +28,7 @@ use yii\helpers\StringHelper; * @property TableSchema $tableSchema the schema information of the DB table associated with this AR class. * @property array $oldAttributes the old attribute values (name-value pairs). * @property array $dirtyAttributes the changed attribute values (name-value pairs). - * @property boolean $isPrimaryKey whether the record is new and should be inserted when calling [[save()]]. + * @property boolean $isNewRecord whether the record is new and should be inserted when calling [[save()]]. * @property mixed $primaryKey the primary key value. * @property mixed $oldPrimaryKey the old primary key value. * @@ -252,7 +252,7 @@ class ActiveRecord extends Model */ public static function tableName() { - return 'tbl_' . StringHelper::camel2id(basename(get_called_class()), '_'); + return 'tbl_' . StringHelper::camel2id(StringHelper::basename(get_called_class()), '_'); } /** @@ -667,36 +667,33 @@ class ActiveRecord extends Model */ public function insert($runValidation = true, $attributes = null) { - if ($runValidation && !$this->validate($attributes)) { + if ($runValidation && !$this->validate($attributes) || !$this->beforeSave(true)) { return false; } - if ($this->beforeSave(true)) { - $values = $this->getDirtyAttributes($attributes); - if ($values === array()) { - foreach ($this->primaryKey() as $key) { - $values[$key] = isset($this->_attributes[$key]) ? $this->_attributes[$key] : null; - } + $values = $this->getDirtyAttributes($attributes); + if (empty($values)) { + foreach ($this->primaryKey() as $key) { + $values[$key] = isset($this->_attributes[$key]) ? $this->_attributes[$key] : null; } - $db = static::getDb(); - $command = $db->createCommand()->insert($this->tableName(), $values); - if ($command->execute()) { - $table = $this->getTableSchema(); - if ($table->sequenceName !== null) { - foreach ($table->primaryKey as $name) { - if (!isset($this->_attributes[$name])) { - $this->_oldAttributes[$name] = $this->_attributes[$name] = $db->getLastInsertID($table->sequenceName); - break; - } + } + $db = static::getDb(); + $command = $db->createCommand()->insert($this->tableName(), $values); + if ($command->execute()) { + $table = $this->getTableSchema(); + if ($table->sequenceName !== null) { + foreach ($table->primaryKey as $name) { + if (!isset($this->_attributes[$name])) { + $this->_oldAttributes[$name] = $this->_attributes[$name] = $db->getLastInsertID($table->sequenceName); + break; } } - foreach ($values as $name => $value) { - $this->_oldAttributes[$name] = $value; - } - $this->afterSave(true); - return true; } + foreach ($values as $name => $value) { + $this->_oldAttributes[$name] = $value; + } + $this->afterSave(true); + return true; } - return false; } /** @@ -750,39 +747,35 @@ class ActiveRecord extends Model */ public function update($runValidation = true, $attributes = null) { - if ($runValidation && !$this->validate($attributes)) { + if ($runValidation && !$this->validate($attributes) || !$this->beforeSave(false)) { return false; } - 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. - $rows = $this->updateAll($values, $condition); - - if ($lock !== null && !$rows) { - throw new StaleObjectException('The object being updated is outdated.'); + $values = $this->getDirtyAttributes($attributes); + if (!empty($values)) { + $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. + $rows = $this->updateAll($values, $condition); - foreach ($values as $name => $value) { - $this->_oldAttributes[$name] = $this->_attributes[$name]; - } + if ($lock !== null && !$rows) { + throw new StaleObjectException('The object being updated is outdated.'); + } - $this->afterSave(false); - return $rows; - } else { - return 0; + foreach ($values as $name => $value) { + $this->_oldAttributes[$name] = $this->_attributes[$name]; } + + $this->afterSave(false); + return $rows; } else { - return false; + return 0; } } @@ -1131,8 +1124,8 @@ class ActiveRecord extends Model return $relation; } } catch (UnknownMethodException $e) { + throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".'); } - throw new InvalidParamException(get_class($this) . ' has no relation named "' . $name . '".'); } /** diff --git a/framework/db/ActiveRelation.php b/yii/db/ActiveRelation.php similarity index 99% rename from framework/db/ActiveRelation.php rename to yii/db/ActiveRelation.php index 97e2a8c..42ae0e7 100644 --- a/framework/db/ActiveRelation.php +++ b/yii/db/ActiveRelation.php @@ -131,7 +131,7 @@ class ActiveRelation extends ActiveQuery /** * Finds the related records and populates them into the primary models. - * This method is internally by [[ActiveQuery]]. Do not call it directly. + * This method is internally used by [[ActiveQuery]]. Do not call it directly. * @param string $name the relation name * @param array $primaryModels primary models * @return array the related models diff --git a/framework/db/ColumnSchema.php b/yii/db/ColumnSchema.php similarity index 100% rename from framework/db/ColumnSchema.php rename to yii/db/ColumnSchema.php diff --git a/framework/db/Command.php b/yii/db/Command.php similarity index 99% rename from framework/db/Command.php rename to yii/db/Command.php index dc6c972..a117685 100644 --- a/framework/db/Command.php +++ b/yii/db/Command.php @@ -106,7 +106,7 @@ class Command extends \yii\base\Component */ public function getRawSql() { - if ($this->_params === array()) { + if (empty($this->_params)) { return $this->_sql; } else { $params = array(); diff --git a/framework/db/Connection.php b/yii/db/Connection.php similarity index 99% rename from framework/db/Connection.php rename to yii/db/Connection.php index 03b10a8..37a5307 100644 --- a/framework/db/Connection.php +++ b/yii/db/Connection.php @@ -242,7 +242,7 @@ class Connection extends Component '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 + 'mssql' => 'yii\db\dao\mssql\Schema', // Mssql driver on windows hosts '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 diff --git a/framework/db/DataReader.php b/yii/db/DataReader.php similarity index 100% rename from framework/db/DataReader.php rename to yii/db/DataReader.php diff --git a/framework/db/Exception.php b/yii/db/Exception.php similarity index 100% rename from framework/db/Exception.php rename to yii/db/Exception.php diff --git a/framework/db/Expression.php b/yii/db/Expression.php similarity index 100% rename from framework/db/Expression.php rename to yii/db/Expression.php diff --git a/framework/db/Migration.php b/yii/db/Migration.php similarity index 100% rename from framework/db/Migration.php rename to yii/db/Migration.php diff --git a/framework/db/Query.php b/yii/db/Query.php similarity index 99% rename from framework/db/Query.php rename to yii/db/Query.php index 6f76265..b1fc718 100644 --- a/framework/db/Query.php +++ b/yii/db/Query.php @@ -589,7 +589,7 @@ class Query extends \yii\base\Component */ public function addParams($params) { - if ($params !== array()) { + if (!empty($params)) { if ($this->params === null) { $this->params = $params; } else { diff --git a/framework/db/QueryBuilder.php b/yii/db/QueryBuilder.php similarity index 99% rename from framework/db/QueryBuilder.php rename to yii/db/QueryBuilder.php index 441d287..a4e30ea 100644 --- a/framework/db/QueryBuilder.php +++ b/yii/db/QueryBuilder.php @@ -472,8 +472,8 @@ class QueryBuilder extends \yii\base\Object if (isset($this->typeMap[$type])) { return $this->typeMap[$type]; } elseif (preg_match('/^(\w+)\s+/', $type, $matches)) { - if (isset($this->typeMap[$matches[0]])) { - return preg_replace('/^\w+/', $this->typeMap[$matches[0]], $type); + if (isset($this->typeMap[$matches[1]])) { + return preg_replace('/^\w+/', $this->typeMap[$matches[1]], $type); } } return $type; @@ -722,7 +722,7 @@ class QueryBuilder extends \yii\base\Object if (!is_array($condition)) { return (string)$condition; - } elseif ($condition === array()) { + } elseif (empty($condition)) { return ''; } if (isset($condition[0])) { // operator format: operator, operand 1, operand 2, ... @@ -777,7 +777,7 @@ class QueryBuilder extends \yii\base\Object $parts[] = $operand; } } - if ($parts !== array()) { + if (!empty($parts)) { return '(' . implode(") $operator (", $parts) . ')'; } else { return ''; @@ -813,7 +813,7 @@ class QueryBuilder extends \yii\base\Object $values = (array)$values; - if ($values === array() || $column === array()) { + if (empty($values) || $column === array()) { return $operator === 'IN' ? '0=1' : ''; } @@ -885,7 +885,7 @@ class QueryBuilder extends \yii\base\Object $values = (array)$values; - if ($values === array()) { + if (empty($values)) { return $operator === 'LIKE' || $operator === 'OR LIKE' ? '0=1' : ''; } diff --git a/framework/db/Schema.php b/yii/db/Schema.php similarity index 100% rename from framework/db/Schema.php rename to yii/db/Schema.php diff --git a/framework/db/StaleObjectException.php b/yii/db/StaleObjectException.php similarity index 100% rename from framework/db/StaleObjectException.php rename to yii/db/StaleObjectException.php diff --git a/framework/db/TableSchema.php b/yii/db/TableSchema.php similarity index 100% rename from framework/db/TableSchema.php rename to yii/db/TableSchema.php diff --git a/framework/db/Transaction.php b/yii/db/Transaction.php similarity index 100% rename from framework/db/Transaction.php rename to yii/db/Transaction.php diff --git a/framework/db/mysql/QueryBuilder.php b/yii/db/mysql/QueryBuilder.php similarity index 100% rename from framework/db/mysql/QueryBuilder.php rename to yii/db/mysql/QueryBuilder.php diff --git a/framework/db/mysql/Schema.php b/yii/db/mysql/Schema.php similarity index 100% rename from framework/db/mysql/Schema.php rename to yii/db/mysql/Schema.php diff --git a/framework/db/sqlite/QueryBuilder.php b/yii/db/sqlite/QueryBuilder.php similarity index 100% rename from framework/db/sqlite/QueryBuilder.php rename to yii/db/sqlite/QueryBuilder.php diff --git a/framework/db/sqlite/Schema.php b/yii/db/sqlite/Schema.php similarity index 98% rename from framework/db/sqlite/Schema.php rename to yii/db/sqlite/Schema.php index 45f8392..d4fb245 100644 --- a/framework/db/sqlite/Schema.php +++ b/yii/db/sqlite/Schema.php @@ -169,7 +169,7 @@ class Schema extends \yii\db\Schema } } } - $column->phpType = $this->getColumnPhpType($this->type); + $column->phpType = $this->getColumnPhpType($column); $value = $info['dflt_value']; if ($column->type === 'string') { diff --git a/yii/debug/Module.php b/yii/debug/Module.php new file mode 100644 index 0000000..3421d95 --- /dev/null +++ b/yii/debug/Module.php @@ -0,0 +1,17 @@ + + * @since 2.0 + */ +class Module extends \yii\base\Module +{ + public $controllerNamespace = 'yii\debug\controllers'; +} \ No newline at end of file diff --git a/yii/debug/Toolbar.php b/yii/debug/Toolbar.php new file mode 100644 index 0000000..84b55c8 --- /dev/null +++ b/yii/debug/Toolbar.php @@ -0,0 +1,38 @@ + + * @since 2.0 + */ +class Toolbar extends Widget +{ + public $debugAction = 'debug'; + public $enabled = YII_DEBUG; + + public function run() + { + if ($this->enabled) { + $id = 'yii-debug-toolbar'; + $url = Yii::$app->getUrlManager()->createUrl($this->debugAction, array( + 'tag' => Yii::getLogger()->tag, + )); + $this->view->registerJs("yii.debug.load('$id', '$url');"); + $this->view->registerAssetBundle('yii/debug'); + echo Html::tag('div', '', array( + 'id' => $id, + 'style' => 'display: none', + )); + } + } +} diff --git a/yii/debug/controllers/DefaultController.php b/yii/debug/controllers/DefaultController.php new file mode 100644 index 0000000..ca90920 --- /dev/null +++ b/yii/debug/controllers/DefaultController.php @@ -0,0 +1,22 @@ + + * @since 2.0 + */ +class DefaultController extends Controller +{ + public function actionIndex($tag) + { + echo $tag; + } +} \ No newline at end of file diff --git a/framework/helpers/ArrayHelper.php b/yii/helpers/ArrayHelper.php similarity index 100% rename from framework/helpers/ArrayHelper.php rename to yii/helpers/ArrayHelper.php diff --git a/framework/helpers/ConsoleColor.php b/yii/helpers/Console.php similarity index 77% rename from framework/helpers/ConsoleColor.php rename to yii/helpers/Console.php index 794b9c8..0055107 100644 --- a/framework/helpers/ConsoleColor.php +++ b/yii/helpers/Console.php @@ -7,9 +7,8 @@ namespace yii\helpers; -// todo test this on all kinds of terminals, especially windows (check out lib ncurses) - /** + * TODO adjust phpdoc * Console View is the base class for console view components * * A console view provides functionality to create rich console application by allowing to format output @@ -18,6 +17,6 @@ namespace yii\helpers; * @author Carsten Brandt * @since 2.0 */ -class ConsoleColor extends base\ConsoleColor +class Console extends base\Console { } diff --git a/framework/helpers/FileHelper.php b/yii/helpers/FileHelper.php similarity index 100% rename from framework/helpers/FileHelper.php rename to yii/helpers/FileHelper.php diff --git a/framework/helpers/Html.php b/yii/helpers/Html.php similarity index 100% rename from framework/helpers/Html.php rename to yii/helpers/Html.php diff --git a/framework/helpers/Json.php b/yii/helpers/Json.php similarity index 100% rename from framework/helpers/Json.php rename to yii/helpers/Json.php diff --git a/yii/helpers/Markdown.php b/yii/helpers/Markdown.php new file mode 100644 index 0000000..3f6c98e --- /dev/null +++ b/yii/helpers/Markdown.php @@ -0,0 +1,33 @@ + 'footnote_', + * )); + * ``` + * + * For more details please refer to [PHP Markdown library documentation](http://michelf.ca/projects/php-markdown/). + * @author Alexander Makarov + * @since 2.0 + */ +class Markdown extends base\Markdown +{ +} diff --git a/yii/helpers/Purifier.php b/yii/helpers/Purifier.php new file mode 100644 index 0000000..b659531 --- /dev/null +++ b/yii/helpers/Purifier.php @@ -0,0 +1,34 @@ + true, + * )); + * ``` + * + * For more details please refer to HTMLPurifier documentation](http://htmlpurifier.org/). + * + * @author Alexander Makarov + * @since 2.0 + */ +class Purifier extends base\Purifier +{ +} diff --git a/framework/helpers/SecurityHelper.php b/yii/helpers/SecurityHelper.php similarity index 100% rename from framework/helpers/SecurityHelper.php rename to yii/helpers/SecurityHelper.php diff --git a/framework/helpers/StringHelper.php b/yii/helpers/StringHelper.php similarity index 100% rename from framework/helpers/StringHelper.php rename to yii/helpers/StringHelper.php diff --git a/framework/helpers/VarDumper.php b/yii/helpers/VarDumper.php similarity index 100% rename from framework/helpers/VarDumper.php rename to yii/helpers/VarDumper.php diff --git a/framework/helpers/base/ArrayHelper.php b/yii/helpers/base/ArrayHelper.php similarity index 92% rename from framework/helpers/base/ArrayHelper.php rename to yii/helpers/base/ArrayHelper.php index 86445d7..e482883 100644 --- a/framework/helpers/base/ArrayHelper.php +++ b/yii/helpers/base/ArrayHelper.php @@ -36,7 +36,7 @@ class ArrayHelper { $args = func_get_args(); $res = array_shift($args); - while ($args !== array()) { + while (!empty($args)) { $next = array_shift($args); foreach ($next as $k => $v) { if (is_integer($k)) { @@ -87,6 +87,35 @@ class ArrayHelper } /** + * Removes an item from an array and returns the value. If the key does not exist in the array, the default value + * will be returned instead. + * + * Usage examples, + * + * ~~~ + * // $array = array('type'=>'A', 'options'=>array(1,2)); + * // working with array + * $type = \yii\helpers\ArrayHelper::remove($array, 'type'); + * // $array content + * // $array = array('options'=>array(1,2)); + * ~~~ + * + * @param array $array the array to extract value from + * @param string $key key name of the array element + * @param mixed $default the default value to be returned if the specified key does not exist + * @return mixed|null the value of the element if found, default value otherwise + */ + public static function remove(&$array, $key, $default = null) + { + if (is_array($array) && (isset($array[$key]) || array_key_exists($key, $array))) { + $value = $array[$key]; + unset($array[$key]); + return $value; + } + return $default; + } + + /** * Indexes an array according to a specified key. * The input array should be multidimensional or an array of objects. * @@ -284,7 +313,7 @@ class ArrayHelper $args[] = $column; } } else { - $args[] = static::getColumn($array, $key); + $args[] = static::getColumn($array, $key); } $args[] = $ascending[$i] ? SORT_ASC : SORT_DESC; $args[] = $flag; diff --git a/yii/helpers/base/Console.php b/yii/helpers/base/Console.php new file mode 100644 index 0000000..b611919 --- /dev/null +++ b/yii/helpers/base/Console.php @@ -0,0 +1,781 @@ + + * @since 2.0 + */ +class Console +{ + 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 NORMAL = 0; + 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"; + } + + /** + * Sets the ANSI format for any text that is printed afterwards. + * + * You can pass any of the FG_*, BG_* and TEXT_* constants and also [[xterm256ColorFg]] and [[xterm256ColorBg]]. + * TODO: documentation + */ + public static function ansiFormatBegin() + { + echo "\033[" . implode(';', func_get_args()) . 'm'; + } + + /** + * Resets any ANSI format set by previous method [[ansiFormatBegin()]] + * Any output after this is will have default text style. + */ + public static function ansiFormatReset() + { + echo "\033[0m"; + } + + /** + * Returns the ANSI format code. + * + * You can pass any of the FG_*, BG_* and TEXT_* constants and also [[xterm256ColorFg]] and [[xterm256ColorBg]]. + * TODO: documentation + */ + public static function ansiFormatCode($format) + { + return "\033[" . implode(';', $format) . 'm'; + } + + /** + * Will return a string formatted with the given ANSI style + * + * @param string $string the string to be formatted + * @param array $format array containing formatting values. + * You can pass any of the FG_*, BG_* and TEXT_* constants and also [[xterm256ColorFg]] and [[xterm256ColorBg]]. + * @return string + */ + public static function ansiFormat($string, $format=array()) + { + $code = implode(';', $format); + 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; + } + + /** + * Strips ANSI control codes from a string + * + * @param string $string String to strip + * @return string + */ + public static function stripAnsiFormat($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 self::FG_BLACK: + $style = array('color' => '#000000'); + break; + case self::FG_BLUE: + $style = array('color' => '#000078'); + break; + case self::FG_CYAN: + $style = array('color' => '#007878'); + break; + case self::FG_GREEN: + $style = array('color' => '#007800'); + break; + case self::FG_GREY: + $style = array('color' => '#787878'); + break; + case self::FG_PURPLE: + $style = array('color' => '#780078'); + break; + case self::FG_RED: + $style = array('color' => '#780000'); + break; + case self::FG_YELLOW: + $style = array('color' => '#787800'); + break; + case self::BG_BLACK: + $style = array('background-color' => '#000000'); + break; + case self::BG_BLUE: + $style = array('background-color' => '#000078'); + break; + case self::BG_CYAN: + $style = array('background-color' => '#007878'); + break; + case self::BG_GREEN: + $style = array('background-color' => '#007800'); + break; + case self::BG_GREY: + $style = array('background-color' => '#787878'); + break; + case self::BG_PURPLE: + $style = array('background-color' => '#780078'); + break; + case self::BG_RED: + $style = array('background-color' => '#780000'); + break; + case self::BG_YELLOW: + $style = array('background-color' => '#787800'); + break; + case self::BOLD: + $style = array('font-weight' => 'bold'); + break; + case self::ITALIC: + $style = array('font-style' => 'italic'); + break; + case self::UNDERLINE: + $style = array('text-decoration' => array('underline')); + break; + case self::OVERLINED: + $style = array('text-decoration' => array('overline')); + break; + case self::CROSSED_OUT: + $style = array('text-decoration' => array('line-through')); + break; + case self::BLINK: + $style = array('text-decoration' => array('blink')); + break; + case self::NEGATIVE: // ??? + case self::CONCEALED: + case self::ENCIRCLED: + case self::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 ' $value) { + echo " $key - $value\n"; + } + echo " ? - Show help\n"; + goto top; + } elseif (!in_array($input, array_keys($options))) { + goto top; + } + return $input; + } + + /** + * Displays and updates a simple progress bar on screen. + * + * @param integer $done the number of items that are completed + * @param integer $total the total value of items that are to be done + * @param integer $size the size of the status bar (optional) + * @see http://snipplr.com/view/29548/ + */ + public static function showProgress($done, $total, $size = 30) + { + static $start; + + // if we go over our bound, just ignore it + if ($done > $total) { + return; + } + + if (empty($start)) { + $start = time(); + } + + $now = time(); + + $percent = (double)($done / $total); + $bar = floor($percent * $size); + + $status = "\r["; + $status .= str_repeat("=", $bar); + if ($bar < $size) { + $status .= ">"; + $status .= str_repeat(" ", $size - $bar); + } else { + $status .= "="; + } + + $display = number_format($percent * 100, 0); + + $status .= "] $display% $done/$total"; + + $rate = ($now - $start) / $done; + $left = $total - $done; + $eta = round($rate * $left, 2); + + $elapsed = $now - $start; + + $status .= " remaining: " . number_format($eta) . " sec. elapsed: " . number_format($elapsed) . " sec."; + + static::stdout("$status "); + + flush(); + + // when done, send a newline + if ($done == $total) { + echo "\n"; + } + } +} diff --git a/framework/helpers/base/FileHelper.php b/yii/helpers/base/FileHelper.php similarity index 100% rename from framework/helpers/base/FileHelper.php rename to yii/helpers/base/FileHelper.php diff --git a/framework/helpers/base/Html.php b/yii/helpers/base/Html.php similarity index 99% rename from framework/helpers/base/Html.php rename to yii/helpers/base/Html.php index 15db823..f601772 100644 --- a/framework/helpers/base/Html.php +++ b/yii/helpers/base/Html.php @@ -324,7 +324,7 @@ class Html $options['action'] = $action; $options['method'] = $method; $form = static::beginTag('form', $options); - if ($hiddenInputs !== array()) { + if (!empty($hiddenInputs)) { $form .= "\n" . implode("\n", $hiddenInputs); } @@ -618,7 +618,7 @@ class Html * 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 + * 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 @@ -647,7 +647,7 @@ class Html * 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 + * 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 @@ -694,9 +694,9 @@ class Html * - 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 + * 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()) @@ -737,9 +737,9 @@ class Html * 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 + * 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()) diff --git a/framework/helpers/base/Json.php b/yii/helpers/base/Json.php similarity index 97% rename from framework/helpers/base/Json.php rename to yii/helpers/base/Json.php index 262dd81..8de55f9 100644 --- a/framework/helpers/base/Json.php +++ b/yii/helpers/base/Json.php @@ -34,7 +34,7 @@ class Json $expressions = array(); $value = static::processData($value, $expressions); $json = json_encode($value, $options); - return $expressions === array() ? $json : strtr($json, $expressions); + return empty($expressions) ? $json : strtr($json, $expressions); } /** diff --git a/yii/helpers/base/Markdown.php b/yii/helpers/base/Markdown.php new file mode 100644 index 0000000..1efdccb --- /dev/null +++ b/yii/helpers/base/Markdown.php @@ -0,0 +1,50 @@ + 'footnote_', + * )); + * ``` + * + * For more details please refer to [PHP Markdown library documentation](http://michelf.ca/projects/php-markdown/). + * @author Alexander Makarov + * @since 2.0 + */ +class Markdown +{ + /** + * @var MarkdownExtra + */ + protected static $markdown; + + public static function process($content, $config = array()) + { + if (static::$markdown===null) { + static::$markdown = new MarkdownExtra(); + } + foreach ($config as $name => $value) { + static::$markdown->{$name} = $value; + } + return static::$markdown->transform($content); + } +} diff --git a/yii/helpers/base/Purifier.php b/yii/helpers/base/Purifier.php new file mode 100644 index 0000000..2c5d334 --- /dev/null +++ b/yii/helpers/base/Purifier.php @@ -0,0 +1,39 @@ + true, + * )); + * ``` + * + * For more details please refer to HTMLPurifier documentation](http://htmlpurifier.org/). + * + * @author Alexander Makarov + * @since 2.0 + */ +class Purifier +{ + public static function process($content, $config = null) + { + $purifier=\HTMLPurifier::instance($config); + $purifier->config->set('Cache.SerializerPath', \Yii::$app->getRuntimePath()); + return $purifier->purify($content); + } +} diff --git a/framework/helpers/base/SecurityHelper.php b/yii/helpers/base/SecurityHelper.php similarity index 100% rename from framework/helpers/base/SecurityHelper.php rename to yii/helpers/base/SecurityHelper.php diff --git a/framework/helpers/base/StringHelper.php b/yii/helpers/base/StringHelper.php similarity index 81% rename from framework/helpers/base/StringHelper.php rename to yii/helpers/base/StringHelper.php index cb4b09b..646bcbb 100644 --- a/framework/helpers/base/StringHelper.php +++ b/yii/helpers/base/StringHelper.php @@ -44,6 +44,29 @@ class StringHelper } /** + * Returns the trailing name component of a path. + * This method does the same as the php function basename() except that it will + * always use \ and / as directory separators, independent of the operating system. + * Note: basename() operates naively on the input string, and is not aware of the + * actual filesystem, or path components such as "..". + * @param string $path A path string. + * @param string $suffix If the name component ends in suffix this will also be cut off. + * @return string the trailing name component of the given path. + * @see http://www.php.net/manual/en/function.basename.php + */ + public static function basename($path, $suffix = '') + { + if (($len = mb_strlen($suffix)) > 0 && mb_substr($path, -$len) == $suffix) { + $path = mb_substr($path, 0, -$len); + } + $path = rtrim(str_replace('\\', '/', $path), '/\\'); + if (($pos = mb_strrpos($path, '/')) !== false) { + return mb_substr($path, $pos + 1); + } + return $path; + } + + /** * 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'. diff --git a/framework/helpers/base/VarDumper.php b/yii/helpers/base/VarDumper.php similarity index 100% rename from framework/helpers/base/VarDumper.php rename to yii/helpers/base/VarDumper.php diff --git a/framework/helpers/base/mimeTypes.php b/yii/helpers/base/mimeTypes.php similarity index 100% rename from framework/helpers/base/mimeTypes.php rename to yii/helpers/base/mimeTypes.php diff --git a/yii/i18n/GettextFile.php b/yii/i18n/GettextFile.php new file mode 100644 index 0000000..03eecca --- /dev/null +++ b/yii/i18n/GettextFile.php @@ -0,0 +1,37 @@ + + * @since 2.0 + */ +abstract class GettextFile extends Component +{ + /** + * Loads messages from a file. + * @param string $filePath file path + * @param string $context message context + * @return array message translations. Array keys are source messages and array values are translated messages: + * source message => translated message. + */ + abstract public function load($filePath, $context); + + /** + * Saves messages to a file. + * @param string $filePath file path + * @param array $messages message translations. Array keys are source messages and array values are + * translated messages: source message => translated message. Note if the message has a context, + * the message ID must be prefixed with the context with chr(4) as the separator. + */ + abstract public function save($filePath, $messages); +} diff --git a/yii/i18n/GettextMessageSource.php b/yii/i18n/GettextMessageSource.php new file mode 100644 index 0000000..0eb7cb3 --- /dev/null +++ b/yii/i18n/GettextMessageSource.php @@ -0,0 +1,59 @@ +basePath) . '/' . $language . '/' . $this->catalog; + if ($this->useMoFile) { + $messageFile .= static::MO_FILE_EXT; + } else { + $messageFile .= static::PO_FILE_EXT; + } + + if (is_file($messageFile)) { + if ($this->useMoFile) { + $gettextFile = new GettextMoFile(array('useBigEndian' => $this->useBigEndian)); + } else { + $gettextFile = new GettextPoFile(); + } + $messages = $gettextFile->load($messageFile, $category); + if (!is_array($messages)) { + $messages = array(); + } + return $messages; + } else { + Yii::error("The message file for category '$category' does not exist: $messageFile", __METHOD__); + return array(); + } + } +} diff --git a/yii/i18n/GettextMoFile.php b/yii/i18n/GettextMoFile.php new file mode 100644 index 0000000..bacba52 --- /dev/null +++ b/yii/i18n/GettextMoFile.php @@ -0,0 +1,267 @@ +. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @author Qiang Xue + * @since 2.0 + */ +class GettextMoFile extends GettextFile +{ + /** + * @var boolean whether to use big-endian when reading and writing an integer. + */ + public $useBigEndian = false; + + /** + * Loads messages from an MO file. + * @param string $filePath file path + * @param string $context message context + * @return array message translations. Array keys are source messages and array values are translated messages: + * source message => translated message. + */ + public function load($filePath, $context) + { + if (false === ($fileHandle = @fopen($filePath, 'rb'))) { + throw new Exception('Unable to read file "' . $filePath . '".'); + } + if (false === @flock($fileHandle, LOCK_SH)) { + throw new Exception('Unable to lock file "' . $filePath . '" for reading.'); + } + + // magic + $array = unpack('c', $this->readBytes($fileHandle, 4)); + $magic = current($array); + if ($magic == -34) { + $this->useBigEndian = false; + } elseif ($magic == -107) { + $this->useBigEndian = true; + } else { + throw new Exception('Invalid MO file: ' . $filePath . ' (magic: ' . $magic . ').'); + } + + // revision + $revision = $this->readInteger($fileHandle); + if ($revision != 0) { + throw new Exception('Invalid MO file revision: ' . $revision . '.'); + } + + $count = $this->readInteger($fileHandle); + $sourceOffset = $this->readInteger($fileHandle); + $targetOffset = $this->readInteger($fileHandle); + + $sourceLengths = array(); + $sourceOffsets = array(); + fseek($fileHandle, $sourceOffset); + for ($i = 0; $i < $count; ++$i) { + $sourceLengths[] = $this->readInteger($fileHandle); + $sourceOffsets[] = $this->readInteger($fileHandle); + } + + $targetLengths = array(); + $targetOffsets = array(); + fseek($fileHandle, $targetOffset); + for ($i = 0; $i < $count; ++$i) { + $targetLengths[] = $this->readInteger($fileHandle); + $targetOffsets[] = $this->readInteger($fileHandle); + } + + $messages = array(); + for ($i = 0; $i < $count; ++$i) { + $id = $this->readString($fileHandle, $sourceLengths[$i], $sourceOffsets[$i]); + $separatorPosition = strpos($id, chr(4)); + + if (($context && $separatorPosition !== false && substr($id, 0, $separatorPosition) === $context) || + (!$context && $separatorPosition === false)) { + if ($separatorPosition !== false) { + $id = substr($id,$separatorPosition+1); + } + + $message = $this->readString($fileHandle, $targetLengths[$i], $targetOffsets[$i]); + $messages[$id] = $message; + } + } + + @flock($fileHandle, LOCK_UN); + @fclose($fileHandle); + return $messages; + } + + /** + * Saves messages to an MO file. + * @param string $filePath file path + * @param array $messages message translations. Array keys are source messages and array values are + * translated messages: source message => translated message. Note if the message has a context, + * the message ID must be prefixed with the context with chr(4) as the separator. + */ + public function save($filePath, $messages) + { + if (false === ($fileHandle = @fopen($filePath, 'wb'))) { + throw new Exception('Unable to write file "' . $filePath . '".'); + } + if (false === @flock($fileHandle, LOCK_EX)) { + throw new Exception('Unable to lock file "' . $filePath . '" for reading.'); + } + + // magic + if ($this->useBigEndian) { + $this->writeBytes($fileHandle, pack('c*', 0x95, 0x04, 0x12, 0xde)); // -107 + } else { + $this->writeBytes($fileHandle, pack('c*', 0xde, 0x12, 0x04, 0x95)); // -34 + } + + // revision + $this->writeInteger($fileHandle, 0); + + // message count + $messageCount = count($messages); + $this->writeInteger($fileHandle, $messageCount); + + // offset of source message table + $offset = 28; + $this->writeInteger($fileHandle, $offset); + $offset += $messageCount * 8; + $this->writeInteger($fileHandle, $offset); + + // hashtable size, omitted + $this->writeInteger($fileHandle, 0); + $offset += $messageCount * 8; + $this->writeInteger($fileHandle, $offset); + + // length and offsets for source messages + foreach (array_keys($messages) as $id) { + $length = strlen($id); + $this->writeInteger($fileHandle, $length); + $this->writeInteger($fileHandle, $offset); + $offset += $length + 1; + } + + // length and offsets for target messages + foreach ($messages as $message) { + $length = strlen($message); + $this->writeInteger($fileHandle, $length); + $this->writeInteger($fileHandle, $offset); + $offset += $length + 1; + } + + // source messages + foreach (array_keys($messages) as $id) { + $this->writeString($fileHandle, $id); + } + + // target messages + foreach ($messages as $message) { + $this->writeString($fileHandle, $message); + } + + @flock($fileHandle, LOCK_UN); + @fclose($fileHandle); + } + + /** + * Reads one or several bytes. + * @param resource $fileHandle to read from + * @param integer $byteCount to be read + * @return string bytes + */ + protected function readBytes($fileHandle, $byteCount = 1) + { + if ($byteCount > 0) { + return fread($fileHandle, $byteCount); + } + } + + /** + * Write bytes. + * @param resource $fileHandle to write to + * @param string $bytes to be written + * @return integer how many bytes are written + */ + protected function writeBytes($fileHandle, $bytes) + { + return fwrite($fileHandle, $bytes); + } + + /** + * Reads a 4-byte integer. + * @param resource $fileHandle to read from + * @return integer the result + */ + protected function readInteger($fileHandle) + { + $array = unpack($this->useBigEndian ? 'N' : 'V', $this->readBytes($fileHandle, 4)); + return current($array); + } + + /** + * Writes a 4-byte integer. + * @param resource $fileHandle to write to + * @param integer $integer to be written + * @return integer how many bytes are written + */ + protected function writeInteger($fileHandle, $integer) + { + return $this->writeBytes($fileHandle, pack($this->useBigEndian ? 'N' : 'V', (int)$integer)); + } + + /** + * Reads a string. + * @param resource $fileHandle file handle + * @param integer $length of the string + * @param integer $offset of the string in the file. If null, it reads from the current position. + * @return string the result + */ + protected function readString($fileHandle, $length, $offset = null) + { + if ($offset !== null) { + fseek($fileHandle, $offset); + } + return $this->readBytes($fileHandle, $length); + } + + /** + * Writes a string. + * @param resource $fileHandle to write to + * @param string $string to be written + * @return integer how many bytes are written + */ + protected function writeString($fileHandle, $string) + { + return $this->writeBytes($fileHandle, $string. "\0"); + } +} diff --git a/yii/i18n/GettextPoFile.php b/yii/i18n/GettextPoFile.php new file mode 100644 index 0000000..cac075b --- /dev/null +++ b/yii/i18n/GettextPoFile.php @@ -0,0 +1,97 @@ + + * @since 2.0 + */ +class GettextPoFile extends GettextFile +{ + /** + * Loads messages from a PO file. + * @param string $filePath file path + * @param string $context message context + * @return array message translations. Array keys are source messages and array values are translated messages: + * source message => translated message. + */ + public function load($filePath, $context) + { + $pattern = '/(msgctxt\s+"(.*?(?decode($matches[3][$i]); + $message = $this->decode($matches[4][$i]); + $messages[$id] = $message; + } + } + return $messages; + } + + /** + * Saves messages to a PO file. + * @param string $filePath file path + * @param array $messages message translations. Array keys are source messages and array values are + * translated messages: source message => translated message. Note if the message has a context, + * the message ID must be prefixed with the context with chr(4) as the separator. + */ + public function save($filePath, $messages) + { + $content = ''; + foreach ($messages as $id => $message) { + $separatorPosition = strpos($id, chr(4)); + if ($separatorPosition !== false) { + $content .= 'msgctxt "' . substr($id, 0, $separatorPosition) . "\"\n"; + $id = substr($id, $separatorPosition + 1); + } + $content .= 'msgid "' . $this->encode($id) . "\"\n"; + $content .= 'msgstr "' . $this->encode($message) . "\"\n\n"; + } + file_put_contents($filePath, $content); + } + + /** + * Encodes special characters in a message. + * @param string $string message to be encoded + * @return string the encoded message + */ + protected function encode($string) + { + return str_replace( + array('"', "\n", "\t", "\r"), + array('\\"', '\\n', '\\t', '\\r'), + $string + ); + } + + /** + * Decodes special characters in a message. + * @param string $string message to be decoded + * @return string the decoded message + */ + protected function decode($string) + { + $string = preg_replace( + array('/"\s+"/', '/\\\\n/', '/\\\\r/', '/\\\\t/', '/\\\\"/'), + array('', "\n", "\r", "\t", '"'), + $string + ); + return substr(rtrim($string), 1, -1); + } +} diff --git a/framework/i18n/I18N.php b/yii/i18n/I18N.php similarity index 99% rename from framework/i18n/I18N.php rename to yii/i18n/I18N.php index 8667abc..7fae5b0 100644 --- a/framework/i18n/I18N.php +++ b/yii/i18n/I18N.php @@ -109,7 +109,7 @@ class I18N extends Component unset($params[0]); } - return $params === array() ? $message : strtr($message, $params); + return empty($params) ? $message : strtr($message, $params); } /** diff --git a/framework/i18n/MessageSource.php b/yii/i18n/MessageSource.php similarity index 100% rename from framework/i18n/MessageSource.php rename to yii/i18n/MessageSource.php diff --git a/framework/i18n/MissingTranslationEvent.php b/yii/i18n/MissingTranslationEvent.php similarity index 100% rename from framework/i18n/MissingTranslationEvent.php rename to yii/i18n/MissingTranslationEvent.php diff --git a/framework/i18n/PhpMessageSource.php b/yii/i18n/PhpMessageSource.php similarity index 100% rename from framework/i18n/PhpMessageSource.php rename to yii/i18n/PhpMessageSource.php diff --git a/framework/i18n/data/plurals.php b/yii/i18n/data/plurals.php similarity index 100% rename from framework/i18n/data/plurals.php rename to yii/i18n/data/plurals.php diff --git a/framework/i18n/data/plurals.xml b/yii/i18n/data/plurals.xml similarity index 100% rename from framework/i18n/data/plurals.xml rename to yii/i18n/data/plurals.xml diff --git a/framework/logging/DbTarget.php b/yii/logging/DbTarget.php similarity index 100% rename from framework/logging/DbTarget.php rename to yii/logging/DbTarget.php diff --git a/framework/logging/EmailTarget.php b/yii/logging/EmailTarget.php similarity index 100% rename from framework/logging/EmailTarget.php rename to yii/logging/EmailTarget.php diff --git a/framework/logging/FileTarget.php b/yii/logging/FileTarget.php similarity index 100% rename from framework/logging/FileTarget.php rename to yii/logging/FileTarget.php diff --git a/framework/logging/Logger.php b/yii/logging/Logger.php similarity index 95% rename from framework/logging/Logger.php rename to yii/logging/Logger.php index 607c388..b557c5e 100644 --- a/framework/logging/Logger.php +++ b/yii/logging/Logger.php @@ -6,7 +6,9 @@ */ namespace yii\logging; -use yii\base\InvalidConfigException; + +use \yii\base\Component; +use \yii\base\InvalidConfigException; /** * Logger records logged messages in memory. @@ -17,7 +19,7 @@ use yii\base\InvalidConfigException; * @author Qiang Xue * @since 2.0 */ -class Logger extends \yii\base\Component +class Logger extends Component { /** * Error message level. An error message is one that indicates the abnormal termination of the @@ -80,6 +82,11 @@ class Logger extends \yii\base\Component * @var Router the log target router registered with this logger. */ public $router; + /** + * @var string a tag that uniquely identifies the current request. This can be used + * to differentiate the log messages for different requests. + */ + public $tag; /** * Initializes the logger by registering [[flush()]] as a shutdown function. @@ -87,6 +94,7 @@ class Logger extends \yii\base\Component public function init() { parent::init(); + $this->tag = date('Ymd-His', microtime(true)); register_shutdown_function(array($this, 'flush'), true); } diff --git a/framework/logging/ProfileTarget.php b/yii/logging/ProfileTarget.php similarity index 100% rename from framework/logging/ProfileTarget.php rename to yii/logging/ProfileTarget.php diff --git a/framework/logging/Router.php b/yii/logging/Router.php similarity index 88% rename from framework/logging/Router.php rename to yii/logging/Router.php index 2f399fe..f544b72 100644 --- a/framework/logging/Router.php +++ b/yii/logging/Router.php @@ -28,16 +28,16 @@ use yii\base\Application; * 'preload' => array('log'), * 'components' => array( * 'log' => array( - * 'class' => '\yii\logging\Router', + * 'class' => 'yii\logging\Router', * 'targets' => array( * 'file' => array( - * 'class' => '\yii\logging\FileTarget', - * 'levels' => 'trace, info', - * 'categories' => 'yii\*', + * 'class' => 'yii\logging\FileTarget', + * 'levels' => array('trace', 'info'), + * 'categories' => array('yii\*'), * ), * 'email' => array( - * 'class' => '\yii\logging\EmailTarget', - * 'levels' => 'error, warning', + * 'class' => 'yii\logging\EmailTarget', + * 'levels' => array('error', 'warning'), * 'emails' => array('admin@example.com'), * ), * ), @@ -73,7 +73,6 @@ class Router extends Component public function init() { parent::init(); - foreach ($this->targets as $name => $target) { if (!$target instanceof Target) { $this->targets[$name] = Yii::createObject($target); diff --git a/framework/logging/Target.php b/yii/logging/Target.php similarity index 100% rename from framework/logging/Target.php rename to yii/logging/Target.php diff --git a/framework/logging/WebTarget.php b/yii/logging/WebTarget.php similarity index 100% rename from framework/logging/WebTarget.php rename to yii/logging/WebTarget.php diff --git a/yii/rbac/Assignment.php b/yii/rbac/Assignment.php new file mode 100644 index 0000000..5b6a607 --- /dev/null +++ b/yii/rbac/Assignment.php @@ -0,0 +1,106 @@ + + * @author Alexander Kochetov + * @since 2.0 + */ +class Assignment extends Object +{ + private $_auth; + private $_userId; + private $_itemName; + private $_bizRule; + private $_data; + + /** + * Constructor. + * @param Manager $auth the authorization manager + * @param mixed $userId user ID (see [[User::id]]) + * @param string $itemName authorization item name + * @param string $bizRule the business rule associated with this assignment + * @param mixed $data additional data for this assignment + */ + public function __construct($auth, $userId, $itemName, $bizRule = null, $data = null) + { + $this->_auth = $auth; + $this->_userId = $userId; + $this->_itemName = $itemName; + $this->_bizRule = $bizRule; + $this->_data = $data; + } + + /** + * @return mixed user ID (see [[User::id]]) + */ + public function getUserId() + { + return $this->_userId; + } + + /** + * @return string the authorization item name + */ + public function getItemName() + { + return $this->_itemName; + } + + /** + * @return string the business rule associated with this assignment + */ + public function getBizRule() + { + return $this->_bizRule; + } + + /** + * @param string $value the business rule associated with this assignment + */ + public function setBizRule($value) + { + if ($this->_bizRule !== $value) { + $this->_bizRule = $value; + $this->_auth->saveAssignment($this); + } + } + + /** + * @return mixed additional data for this assignment + */ + public function getData() + { + return $this->_data; + } + + /** + * @param mixed $value additional data for this assignment + */ + public function setData($value) + { + if ($this->_data !== $value) { + $this->_data = $value; + $this->_auth->saveAssignment($this); + } + } +} diff --git a/yii/rbac/DbManager.php b/yii/rbac/DbManager.php new file mode 100644 index 0000000..f3658b2 --- /dev/null +++ b/yii/rbac/DbManager.php @@ -0,0 +1,573 @@ + + * @author Alexander Kochetov + * @since 2.0 + */ +class DbManager extends Manager +{ + /** + * @var Connection|string the DB connection object or the application component ID of the DB connection. + * After the DbManager object is created, if you want to change this property, you should only assign it + * with a DB connection object. + */ + public $db = 'db'; + /** + * @var string the name of the table storing authorization items. Defaults to 'tbl_auth_item'. + */ + public $itemTable = 'tbl_auth_item'; + /** + * @var string the name of the table storing authorization item hierarchy. Defaults to 'tbl_auth_item_child'. + */ + public $itemChildTable = 'tbl_auth_item_child'; + /** + * @var string the name of the table storing authorization item assignments. Defaults to 'tbl_auth_assignment'. + */ + public $assignmentTable = 'tbl_auth_assignment'; + + private $_usingSqlite; + + /** + * Initializes the application component. + * This method overrides the parent implementation by establishing the database connection. + */ + public function init() + { + if (is_string($this->db)) { + $this->db = Yii::$app->getComponent($this->db); + } + if (!$this->db instanceof Connection) { + throw new InvalidConfigException("DbManager::db must be either a DB connection instance or the application component ID of a DB connection."); + } + $this->_usingSqlite = !strncmp($this->db->getDriverName(), 'sqlite', 6); + parent::init(); + } + + /** + * Performs access check for the specified user. + * @param mixed $userId the user ID. This should can be either an integer or a string representing + * the unique identifier of a user. See [[User::id]]. + * @param string $itemName the name of the operation that need access check + * @param array $params name-value pairs that would be passed to biz rules associated + * with the tasks and roles assigned to the user. A param with name 'userId' is added to this array, + * which holds the value of `$userId`. + * @return boolean whether the operations can be performed by the user. + */ + public function checkAccess($userId, $itemName, $params = array()) + { + $assignments = $this->getAssignments($userId); + return $this->checkAccessRecursive($userId, $itemName, $params, $assignments); + } + + /** + * Performs access check for the specified user. + * This method is internally called by [[checkAccess()]]. + * @param mixed $userId the user ID. This should can be either an integer or a string representing + * the unique identifier of a user. See [[User::id]]. + * @param string $itemName the name of the operation that need access check + * @param array $params name-value pairs that would be passed to biz rules associated + * with the tasks and roles assigned to the user. A param with name 'userId' is added to this array, + * which holds the value of `$userId`. + * @param Assignment[] $assignments the assignments to the specified user + * @return boolean whether the operations can be performed by the user. + */ + protected function checkAccessRecursive($userId, $itemName, $params, $assignments) + { + if (($item = $this->getItem($itemName)) === null) { + return false; + } + Yii::trace('Checking permission: ' . $item->getName(), __METHOD__); + if (!isset($params['userId'])) { + $params['userId'] = $userId; + } + if ($this->executeBizRule($item->getBizRule(), $params, $item->getData())) { + if (in_array($itemName, $this->defaultRoles)) { + return true; + } + if (isset($assignments[$itemName])) { + $assignment = $assignments[$itemName]; + if ($this->executeBizRule($assignment->getBizRule(), $params, $assignment->getData())) { + return true; + } + } + $query = new Query; + $parents = $query->select(array('parent')) + ->from($this->itemChildTable) + ->where(array('child' => $itemName)) + ->createCommand($this->db) + ->queryColumn(); + foreach ($parents as $parent) { + if ($this->checkAccessRecursive($userId, $parent, $params, $assignments)) { + return true; + } + } + } + return false; + } + + /** + * Adds an item as a child of another item. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the item is added successfully + * @throws Exception if either parent or child doesn't exist. + * @throws InvalidCallException if a loop has been detected. + */ + public function addItemChild($itemName, $childName) + { + if ($itemName === $childName) { + throw new Exception("Cannot add '$itemName' as a child of itself."); + } + $query = new Query; + $rows = $query->from($this->itemTable) + ->where(array('or', 'name=:name1', 'name=:name2'), array( + ':name1' => $itemName, + ':name2' => $childName + )) + ->createCommand($this->db) + ->queryAll(); + if (count($rows) == 2) { + if ($rows[0]['name'] === $itemName) { + $parentType = $rows[0]['type']; + $childType = $rows[1]['type']; + } else { + $childType = $rows[0]['type']; + $parentType = $rows[1]['type']; + } + $this->checkItemChildType($parentType, $childType); + if ($this->detectLoop($itemName, $childName)) { + throw new InvalidCallException("Cannot add '$childName' as a child of '$itemName'. A loop has been detected."); + } + $this->db->createCommand() + ->insert($this->itemChildTable, array( + 'parent' => $itemName, + 'child' => $childName, + )); + return true; + } else { + throw new Exception("Either '$itemName' or '$childName' does not exist."); + } + } + + /** + * Removes a child from its parent. + * Note, the child item is not deleted. Only the parent-child relationship is removed. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the removal is successful + */ + public function removeItemChild($itemName, $childName) + { + return $this->db->createCommand() + ->delete($this->itemChildTable, array( + 'parent' => $itemName, + 'child' => $childName + )) > 0; + } + + /** + * Returns a value indicating whether a child exists within a parent. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the child exists + */ + public function hasItemChild($itemName, $childName) + { + $query = new Query; + return $query->select(array('parent')) + ->from($this->itemChildTable) + ->where(array( + 'parent' => $itemName, + 'child' => $childName + )) + ->createCommand($this->db) + ->queryScalar() !== false; + } + + /** + * Returns the children of the specified item. + * @param mixed $names the parent item name. This can be either a string or an array. + * The latter represents a list of item names. + * @return Item[] all child items of the parent + */ + public function getItemChildren($names) + { + $query = new Query; + $rows = $query->select(array('name', 'type', 'description', 'bizrule', 'data')) + ->from(array( + $this->itemTable, + $this->itemChildTable + )) + ->where(array('parent' => $names, 'name' => new Expression('child'))) + ->createCommand($this->db) + ->queryAll(); + $children = array(); + foreach ($rows as $row) { + if (($data = @unserialize($row['data'])) === false) { + $data = null; + } + $children[$row['name']] = new Item($this, $row['name'], $row['type'], $row['description'], $row['bizrule'], $data); + } + return $children; + } + + /** + * Assigns an authorization item to a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called + * for this particular authorization item. + * @param mixed $data additional data associated with this assignment + * @return Assignment the authorization assignment information. + * @throws InvalidParamException if the item does not exist or if the item has already been assigned to the user + */ + public function assign($userId, $itemName, $bizRule = null, $data = null) + { + if ($this->usingSqlite() && $this->getItem($itemName) === null) { + throw new InvalidParamException("The item '$itemName' does not exist."); + } + $this->db->createCommand() + ->insert($this->assignmentTable, array( + 'user_id' => $userId, + 'item_name' => $itemName, + 'bizrule' => $bizRule, + 'data' => serialize($data) + )); + return new Assignment($this, $userId, $itemName, $bizRule, $data); + } + + /** + * Revokes an authorization assignment from a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return boolean whether removal is successful + */ + public function revoke($userId, $itemName) + { + return $this->db->createCommand() + ->delete($this->assignmentTable, array( + 'user_id' => $userId, + 'item_name' => $itemName + )) > 0; + } + + /** + * Returns a value indicating whether the item has been assigned to the user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return boolean whether the item has been assigned to the user. + */ + public function isAssigned($itemName, $userId) + { + $query = new Query; + return $query->select(array('item_name')) + ->from($this->assignmentTable) + ->where(array( + 'user_id' => $userId, + 'item_name' => $itemName + )) + ->createCommand($this->db) + ->queryScalar() !== false; + } + + /** + * Returns the item assignment information. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return Assignment the item assignment information. Null is returned if + * the item is not assigned to the user. + */ + public function getAssignment($userId, $itemName) + { + $query = new Query; + $row = $query->from($this->assignmentTable) + ->where(array( + 'user_id' => $userId, + 'item_name' => $itemName + )) + ->createCommand($this->db) + ->queryRow(); + if ($row !== false) { + if (($data = @unserialize($row['data'])) === false) { + $data = null; + } + return new Assignment($this, $row['user_id'], $row['item_name'], $row['bizrule'], $data); + } else { + return null; + } + } + + /** + * Returns the item assignments for the specified user. + * @param mixed $userId the user ID (see [[User::id]]) + * @return Assignment[] the item assignment information for the user. An empty array will be + * returned if there is no item assigned to the user. + */ + public function getAssignments($userId) + { + $query = new Query; + $rows = $query->from($this->assignmentTable) + ->where(array('user_id' => $userId)) + ->createCommand($this->db) + ->queryAll(); + $assignments = array(); + foreach ($rows as $row) { + if (($data = @unserialize($row['data'])) === false) { + $data = null; + } + $assignments[$row['item_name']] = new Assignment($this, $row['user_id'], $row['item_name'], $row['bizrule'], $data); + } + return $assignments; + } + + /** + * Saves the changes to an authorization assignment. + * @param Assignment $assignment the assignment that has been changed. + */ + public function saveAssignment($assignment) + { + $this->db->createCommand() + ->update($this->assignmentTable, array( + 'bizrule' => $assignment->getBizRule(), + 'data' => serialize($assignment->getData()), + ), array( + 'user_id' => $assignment->getUserId(), + 'item_name' => $assignment->getItemName() + )); + } + + /** + * Returns the authorization items of the specific type and user. + * @param mixed $userId the user ID. Defaults to null, meaning returning all items even if + * they are not assigned to a user. + * @param integer $type the item type (0: operation, 1: task, 2: role). Defaults to null, + * meaning returning all items regardless of their type. + * @return Item[] the authorization items of the specific type. + */ + public function getItems($userId = null, $type = null) + { + $query = new Query; + if ($userId === null && $type === null) { + $command = $query->from($this->itemTable) + ->createCommand($this->db); + } elseif ($userId === null) { + $command = $query->from($this->itemTable) + ->where(array('type' => $type)) + ->createCommand($this->db); + } elseif ($type === null) { + $command = $query->select(array('name', 'type', 'description', 't1.bizrule', 't1.data')) + ->from(array( + $this->itemTable . ' t1', + $this->assignmentTable . ' t2' + )) + ->where(array('user_id' => $userId, 'name' => new Expression('item_name'))) + ->createCommand($this->db); + } else { + $command = $query->select('name', 'type', 'description', 't1.bizrule', 't1.data') + ->from(array( + $this->itemTable . ' t1', + $this->assignmentTable . ' t2' + )) + ->where(array( + 'user_id' => $userId, + 'type' => $type, + 'name' => new Expression('item_name'), + )) + ->createCommand($this->db); + } + $items = array(); + foreach ($command->queryAll() as $row) { + if (($data = @unserialize($row['data'])) === false) { + $data = null; + } + $items[$row['name']] = new Item($this, $row['name'], $row['type'], $row['description'], $row['bizrule'], $data); + } + return $items; + } + + /** + * Creates an authorization item. + * An authorization item represents an action permission (e.g. creating a post). + * It has three types: operation, task and role. + * Authorization items form a hierarchy. Higher level items inheirt permissions representing + * by lower level items. + * @param string $name the item name. This must be a unique identifier. + * @param integer $type the item type (0: operation, 1: task, 2: role). + * @param string $description description of the item + * @param string $bizRule business rule associated with the item. This is a piece of + * PHP code that will be executed when [[checkAccess()]] is called for the item. + * @param mixed $data additional data associated with the item. + * @return Item the authorization item + * @throws Exception if an item with the same name already exists + */ + public function createItem($name, $type, $description = '', $bizRule = null, $data = null) + { + $this->db->createCommand() + ->insert($this->itemTable, array( + 'name' => $name, + 'type' => $type, + 'description' => $description, + 'bizrule' => $bizRule, + 'data' => serialize($data) + )); + return new Item($this, $name, $type, $description, $bizRule, $data); + } + + /** + * Removes the specified authorization item. + * @param string $name the name of the item to be removed + * @return boolean whether the item exists in the storage and has been removed + */ + public function removeItem($name) + { + if ($this->usingSqlite()) { + $this->db->createCommand() + ->delete($this->itemChildTable, array('or', 'parent=:name1', 'child=:name2'), array( + ':name1' => $name, + ':name2' => $name + )); + $this->db->createCommand()->delete($this->assignmentTable, array('item_name' => $name)); + } + return $this->db->createCommand()->delete($this->itemTable, array('name' => $name)) > 0; + } + + /** + * Returns the authorization item with the specified name. + * @param string $name the name of the item + * @return Item the authorization item. Null if the item cannot be found. + */ + public function getItem($name) + { + $query = new Query; + $row = $query->from($this->itemTable) + ->where(array('name' => $name)) + ->createCommand($this->db) + ->queryRow(); + + if ($row !== false) { + if (($data = @unserialize($row['data'])) === false) { + $data = null; + } + return new Item($this, $row['name'], $row['type'], $row['description'], $row['bizrule'], $data); + } else + return null; + } + + /** + * Saves an authorization item to persistent storage. + * @param Item $item the item to be saved. + * @param string $oldName the old item name. If null, it means the item name is not changed. + */ + public function saveItem($item, $oldName = null) + { + if ($this->usingSqlite() && $oldName !== null && $item->getName() !== $oldName) { + $this->db->createCommand() + ->update($this->itemChildTable, array( + 'parent' => $item->getName(), + ), array( + 'parent' => $oldName, + )); + $this->db->createCommand() + ->update($this->itemChildTable, array( + 'child' => $item->getName(), + ), array( + 'child' => $oldName, + )); + $this->db->createCommand() + ->update($this->assignmentTable, array( + 'item_name' => $item->getName(), + ), array( + 'item_name' => $oldName, + )); + } + + $this->db->createCommand() + ->update($this->itemTable, array( + 'name' => $item->getName(), + 'type' => $item->getType(), + 'description' => $item->getDescription(), + 'bizrule' => $item->getBizRule(), + 'data' => serialize($item->getData()), + ), array( + 'name' => $oldName === null ? $item->getName() : $oldName, + )); + } + + /** + * Saves the authorization data to persistent storage. + */ + public function save() + { + } + + /** + * Removes all authorization data. + */ + public function clearAll() + { + $this->clearAssignments(); + $this->db->createCommand()->delete($this->itemChildTable); + $this->db->createCommand()->delete($this->itemTable); + } + + /** + * Removes all authorization assignments. + */ + public function clearAssignments() + { + $this->db->createCommand()->delete($this->assignmentTable); + } + + /** + * Checks whether there is a loop in the authorization item hierarchy. + * @param string $itemName parent item name + * @param string $childName the name of the child item that is to be added to the hierarchy + * @return boolean whether a loop exists + */ + protected function detectLoop($itemName, $childName) + { + if ($childName === $itemName) { + return true; + } + foreach ($this->getItemChildren($childName) as $child) { + if ($this->detectLoop($itemName, $child->getName())) { + return true; + } + } + return false; + } + + /** + * @return boolean whether the database is a SQLite database + */ + protected function usingSqlite() + { + return $this->_usingSqlite; + } +} diff --git a/yii/rbac/Item.php b/yii/rbac/Item.php new file mode 100644 index 0000000..ef56a53 --- /dev/null +++ b/yii/rbac/Item.php @@ -0,0 +1,275 @@ + + * @author Alexander Kochetov + * @since 2.0 + */ +class Item extends Object +{ + const TYPE_OPERATION = 0; + const TYPE_TASK = 1; + const TYPE_ROLE = 2; + + private $_auth; + private $_type; + private $_name; + private $_description; + private $_bizRule; + private $_data; + + /** + * Constructor. + * @param Manager $auth authorization manager + * @param string $name authorization item name + * @param integer $type authorization item type. This can be 0 (operation), 1 (task) or 2 (role). + * @param string $description the description + * @param string $bizRule the business rule associated with this item + * @param mixed $data additional data for this item + */ + public function __construct($auth, $name, $type, $description = '', $bizRule = null, $data = null) + { + $this->_type = (int)$type; + $this->_auth = $auth; + $this->_name = $name; + $this->_description = $description; + $this->_bizRule = $bizRule; + $this->_data = $data; + } + + /** + * Checks to see if the specified item is within the hierarchy starting from this item. + * This method is expected to be internally used by the actual implementations + * of the [[Manager::checkAccess()]]. + * @param string $itemName the name of the item to be checked + * @param array $params the parameters to be passed to business rule evaluation + * @return boolean whether the specified item is within the hierarchy starting from this item. + */ + public function checkAccess($itemName, $params = array()) + { + Yii::trace('Checking permission: ' . $this->_name, __METHOD__); + if ($this->_auth->executeBizRule($this->_bizRule, $params, $this->_data)) { + if ($this->_name == $itemName) { + return true; + } + foreach ($this->_auth->getItemChildren($this->_name) as $item) { + if ($item->checkAccess($itemName, $params)) { + return true; + } + } + } + return false; + } + + /** + * @return Manager the authorization manager + */ + public function getManager() + { + return $this->_auth; + } + + /** + * @return integer the authorization item type. This could be 0 (operation), 1 (task) or 2 (role). + */ + public function getType() + { + return $this->_type; + } + + /** + * @return string the item name + */ + public function getName() + { + return $this->_name; + } + + /** + * @param string $value the item name + */ + public function setName($value) + { + if ($this->_name !== $value) { + $oldName = $this->_name; + $this->_name = $value; + $this->_auth->saveItem($this, $oldName); + } + } + + /** + * @return string the item description + */ + public function getDescription() + { + return $this->_description; + } + + /** + * @param string $value the item description + */ + public function setDescription($value) + { + if ($this->_description !== $value) { + $this->_description = $value; + $this->_auth->saveItem($this); + } + } + + /** + * @return string the business rule associated with this item + */ + public function getBizRule() + { + return $this->_bizRule; + } + + /** + * @param string $value the business rule associated with this item + */ + public function setBizRule($value) + { + if ($this->_bizRule !== $value) { + $this->_bizRule = $value; + $this->_auth->saveItem($this); + } + } + + /** + * @return mixed the additional data associated with this item + */ + public function getData() + { + return $this->_data; + } + + /** + * @param mixed $value the additional data associated with this item + */ + public function setData($value) + { + if ($this->_data !== $value) { + $this->_data = $value; + $this->_auth->saveItem($this); + } + } + + /** + * Adds a child item. + * @param string $name the name of the child item + * @return boolean whether the item is added successfully + * @throws \yii\base\Exception if either parent or child doesn't exist or if a loop has been detected. + * @see Manager::addItemChild + */ + public function addChild($name) + { + return $this->_auth->addItemChild($this->_name, $name); + } + + /** + * Removes a child item. + * Note, the child item is not deleted. Only the parent-child relationship is removed. + * @param string $name the child item name + * @return boolean whether the removal is successful + * @see Manager::removeItemChild + */ + public function removeChild($name) + { + return $this->_auth->removeItemChild($this->_name, $name); + } + + /** + * Returns a value indicating whether a child exists + * @param string $name the child item name + * @return boolean whether the child exists + * @see Manager::hasItemChild + */ + public function hasChild($name) + { + return $this->_auth->hasItemChild($this->_name, $name); + } + + /** + * Returns the children of this item. + * @return Item[] all child items of this item. + * @see Manager::getItemChildren + */ + public function getChildren() + { + return $this->_auth->getItemChildren($this->_name); + } + + /** + * Assigns this item to a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called + * for this particular authorization item. + * @param mixed $data additional data associated with this assignment + * @return Assignment the authorization assignment information. + * @throws \yii\base\Exception if the item has already been assigned to the user + * @see Manager::assign + */ + public function assign($userId, $bizRule = null, $data = null) + { + return $this->_auth->assign($userId, $this->_name, $bizRule, $data); + } + + /** + * Revokes an authorization assignment from a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @return boolean whether removal is successful + * @see Manager::revoke + */ + public function revoke($userId) + { + return $this->_auth->revoke($userId, $this->_name); + } + + /** + * Returns a value indicating whether this item has been assigned to the user. + * @param mixed $userId the user ID (see [[User::id]]) + * @return boolean whether the item has been assigned to the user. + * @see Manager::isAssigned + */ + public function isAssigned($userId) + { + return $this->_auth->isAssigned($userId, $this->_name); + } + + /** + * Returns the item assignment information. + * @param mixed $userId the user ID (see [[User::id]]) + * @return Assignment the item assignment information. Null is returned if + * this item is not assigned to the user. + * @see Manager::getAssignment + */ + public function getAssignment($userId) + { + return $this->_auth->getAssignment($userId, $this->_name); + } +} diff --git a/yii/rbac/Manager.php b/yii/rbac/Manager.php new file mode 100644 index 0000000..5c3c4f6 --- /dev/null +++ b/yii/rbac/Manager.php @@ -0,0 +1,312 @@ +Item). + * @property array $tasks Tasks (name=>Item). + * @property array $operations Operations (name=>Item). + * + * @author Qiang Xue + * @author Alexander Kochetov + * @since 2.0 + */ +abstract class Manager extends Component +{ + /** + * @var boolean Enable error reporting for bizRules. + */ + public $showErrors = false; + + /** + * @var array list of role names that are assigned to all users implicitly. + * These roles do not need to be explicitly assigned to any user. + * When calling [[checkAccess()]], these roles will be checked first. + * For performance reason, you should minimize the number of such roles. + * A typical usage of such roles is to define an 'authenticated' role and associate + * it with a biz rule which checks if the current user is authenticated. + * And then declare 'authenticated' in this property so that it can be applied to + * every authenticated user. + */ + public $defaultRoles = array(); + + /** + * Creates a role. + * This is a shortcut method to [[Manager::createItem()]]. + * @param string $name the item name + * @param string $description the item description. + * @param string $bizRule the business rule associated with this item + * @param mixed $data additional data to be passed when evaluating the business rule + * @return Item the authorization item + */ + public function createRole($name, $description = '', $bizRule = null, $data = null) + { + return $this->createItem($name, Item::TYPE_ROLE, $description, $bizRule, $data); + } + + /** + * Creates a task. + * This is a shortcut method to [[Manager::createItem()]]. + * @param string $name the item name + * @param string $description the item description. + * @param string $bizRule the business rule associated with this item + * @param mixed $data additional data to be passed when evaluating the business rule + * @return Item the authorization item + */ + public function createTask($name, $description = '', $bizRule = null, $data = null) + { + return $this->createItem($name, Item::TYPE_TASK, $description, $bizRule, $data); + } + + /** + * Creates an operation. + * This is a shortcut method to [[Manager::createItem()]]. + * @param string $name the item name + * @param string $description the item description. + * @param string $bizRule the business rule associated with this item + * @param mixed $data additional data to be passed when evaluating the business rule + * @return Item the authorization item + */ + public function createOperation($name, $description = '', $bizRule = null, $data = null) + { + return $this->createItem($name, Item::TYPE_OPERATION, $description, $bizRule, $data); + } + + /** + * Returns roles. + * This is a shortcut method to [[Manager::getItems()]]. + * @param mixed $userId the user ID. If not null, only the roles directly assigned to the user + * will be returned. Otherwise, all roles will be returned. + * @return Item[] roles (name=>AuthItem) + */ + public function getRoles($userId = null) + { + return $this->getItems($userId, Item::TYPE_ROLE); + } + + /** + * Returns tasks. + * This is a shortcut method to [[Manager::getItems()]]. + * @param mixed $userId the user ID. If not null, only the tasks directly assigned to the user + * will be returned. Otherwise, all tasks will be returned. + * @return Item[] tasks (name=>AuthItem) + */ + public function getTasks($userId = null) + { + return $this->getItems($userId, Item::TYPE_TASK); + } + + /** + * Returns operations. + * This is a shortcut method to [[Manager::getItems()]]. + * @param mixed $userId the user ID. If not null, only the operations directly assigned to the user + * will be returned. Otherwise, all operations will be returned. + * @return Item[] operations (name=>AuthItem) + */ + public function getOperations($userId = null) + { + return $this->getItems($userId, Item::TYPE_OPERATION); + } + + /** + * Executes the specified business rule. + * @param string $bizRule the business rule to be executed. + * @param array $params parameters passed to [[Manager::checkAccess()]]. + * @param mixed $data additional data associated with the authorization item or assignment. + * @return boolean whether the business rule returns true. + * If the business rule is empty, it will still return true. + */ + public function executeBizRule($bizRule, $params, $data) + { + return $bizRule === '' || $bizRule === null || ($this->showErrors ? eval($bizRule) != 0 : @eval($bizRule) != 0); + } + + /** + * Checks the item types to make sure a child can be added to a parent. + * @param integer $parentType parent item type + * @param integer $childType child item type + * @throws InvalidParamException if the item cannot be added as a child due to its incompatible type. + */ + protected function checkItemChildType($parentType, $childType) + { + static $types = array('operation', 'task', 'role'); + if ($parentType < $childType) { + throw new InvalidParamException("Cannot add an item of type '$types[$childType]' to an item of type '$types[$parentType]'."); + } + } + + /** + * Performs access check for the specified user. + * @param mixed $userId the user ID. This should be either an integer or a string representing + * the unique identifier of a user. See [[User::id]]. + * @param string $itemName the name of the operation that we are checking access to + * @param array $params name-value pairs that would be passed to biz rules associated + * with the tasks and roles assigned to the user. + * @return boolean whether the operations can be performed by the user. + */ + abstract public function checkAccess($userId, $itemName, $params = array()); + + /** + * Creates an authorization item. + * An authorization item represents an action permission (e.g. creating a post). + * It has three types: operation, task and role. + * Authorization items form a hierarchy. Higher level items inheirt permissions representing + * by lower level items. + * @param string $name the item name. This must be a unique identifier. + * @param integer $type the item type (0: operation, 1: task, 2: role). + * @param string $description description of the item + * @param string $bizRule business rule associated with the item. This is a piece of + * PHP code that will be executed when [[checkAccess()]] is called for the item. + * @param mixed $data additional data associated with the item. + * @throws \yii\base\Exception if an item with the same name already exists + * @return Item the authorization item + */ + abstract public function createItem($name, $type, $description = '', $bizRule = null, $data = null); + /** + * Removes the specified authorization item. + * @param string $name the name of the item to be removed + * @return boolean whether the item exists in the storage and has been removed + */ + abstract public function removeItem($name); + /** + * Returns the authorization items of the specific type and user. + * @param mixed $userId the user ID. Defaults to null, meaning returning all items even if + * they are not assigned to a user. + * @param integer $type the item type (0: operation, 1: task, 2: role). Defaults to null, + * meaning returning all items regardless of their type. + * @return Item[] the authorization items of the specific type. + */ + abstract public function getItems($userId = null, $type = null); + /** + * Returns the authorization item with the specified name. + * @param string $name the name of the item + * @return Item the authorization item. Null if the item cannot be found. + */ + abstract public function getItem($name); + /** + * Saves an authorization item to persistent storage. + * @param Item $item the item to be saved. + * @param string $oldName the old item name. If null, it means the item name is not changed. + */ + abstract public function saveItem($item, $oldName = null); + + /** + * Adds an item as a child of another item. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @throws \yii\base\Exception if either parent or child doesn't exist or if a loop has been detected. + */ + abstract public function addItemChild($itemName, $childName); + /** + * Removes a child from its parent. + * Note, the child item is not deleted. Only the parent-child relationship is removed. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the removal is successful + */ + abstract public function removeItemChild($itemName, $childName); + /** + * Returns a value indicating whether a child exists within a parent. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the child exists + */ + abstract public function hasItemChild($itemName, $childName); + /** + * Returns the children of the specified item. + * @param mixed $itemName the parent item name. This can be either a string or an array. + * The latter represents a list of item names. + * @return Item[] all child items of the parent + */ + abstract public function getItemChildren($itemName); + + /** + * Assigns an authorization item to a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called + * for this particular authorization item. + * @param mixed $data additional data associated with this assignment + * @return Assignment the authorization assignment information. + * @throws \yii\base\Exception if the item does not exist or if the item has already been assigned to the user + */ + abstract public function assign($userId, $itemName, $bizRule = null, $data = null); + /** + * Revokes an authorization assignment from a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return boolean whether removal is successful + */ + abstract public function revoke($userId, $itemName); + /** + * Returns a value indicating whether the item has been assigned to the user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return boolean whether the item has been assigned to the user. + */ + abstract public function isAssigned($userId, $itemName); + /** + * Returns the item assignment information. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return Assignment the item assignment information. Null is returned if + * the item is not assigned to the user. + */ + abstract public function getAssignment($userId, $itemName); + /** + * Returns the item assignments for the specified user. + * @param mixed $userId the user ID (see [[User::id]]) + * @return Item[] the item assignment information for the user. An empty array will be + * returned if there is no item assigned to the user. + */ + abstract public function getAssignments($userId); + /** + * Saves the changes to an authorization assignment. + * @param Assignment $assignment the assignment that has been changed. + */ + abstract public function saveAssignment($assignment); + /** + * Removes all authorization data. + */ + abstract public function clearAll(); + /** + * Removes all authorization assignments. + */ + abstract public function clearAssignments(); + /** + * Saves authorization data into persistent storage. + * If any change is made to the authorization data, please make + * sure you call this method to save the changed data into persistent storage. + */ + abstract public function save(); +} diff --git a/yii/rbac/PhpManager.php b/yii/rbac/PhpManager.php new file mode 100644 index 0000000..8a4dbec --- /dev/null +++ b/yii/rbac/PhpManager.php @@ -0,0 +1,516 @@ + + * @author Alexander Kochetov + * @since 2.0 + */ +class PhpManager extends Manager +{ + /** + * @var string the path of the PHP script that contains the authorization data. + * If not set, it will be using 'protected/data/rbac.php' as the data file. + * Make sure this file is writable by the Web server process if the authorization + * needs to be changed. + * @see loadFromFile + * @see saveToFile + */ + public $authFile; + + private $_items = array(); // itemName => item + private $_children = array(); // itemName, childName => child + private $_assignments = array(); // userId, itemName => assignment + + /** + * Initializes the application component. + * This method overrides parent implementation by loading the authorization data + * from PHP script. + */ + public function init() + { + parent::init(); + if ($this->authFile === null) { + $this->authFile = Yii::getAlias('@app/data/rbac') . '.php'; + } + $this->load(); + } + + /** + * Performs access check for the specified user. + * @param mixed $userId the user ID. This can be either an integer or a string representing + * @param string $itemName the name of the operation that need access check + * the unique identifier of a user. See [[User::id]]. + * @param array $params name-value pairs that would be passed to biz rules associated + * with the tasks and roles assigned to the user. A param with name 'userId' is added to + * this array, which holds the value of `$userId`. + * @return boolean whether the operations can be performed by the user. + */ + public function checkAccess($userId, $itemName, $params = array()) + { + if (!isset($this->_items[$itemName])) { + return false; + } + /** @var $item Item */ + $item = $this->_items[$itemName]; + Yii::trace('Checking permission: ' . $item->getName(), __METHOD__); + if (!isset($params['userId'])) { + $params['userId'] = $userId; + } + if ($this->executeBizRule($item->getBizRule(), $params, $item->getData())) { + if (in_array($itemName, $this->defaultRoles)) { + return true; + } + if (isset($this->_assignments[$userId][$itemName])) { + /** @var $assignment Assignment */ + $assignment = $this->_assignments[$userId][$itemName]; + if ($this->executeBizRule($assignment->getBizRule(), $params, $assignment->getData())) { + return true; + } + } + foreach ($this->_children as $parentName => $children) { + if (isset($children[$itemName]) && $this->checkAccess($userId, $parentName, $params)) { + return true; + } + } + } + return false; + } + + /** + * Adds an item as a child of another item. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the item is added successfully + * @throws Exception if either parent or child doesn't exist. + * @throws InvalidCallException if item already has a child with $itemName or if a loop has been detected. + */ + public function addItemChild($itemName, $childName) + { + if (!isset($this->_items[$childName], $this->_items[$itemName])) { + throw new Exception("Either '$itemName' or '$childName' does not exist."); + } + /** @var $child Item */ + $child = $this->_items[$childName]; + /** @var $item Item */ + $item = $this->_items[$itemName]; + $this->checkItemChildType($item->getType(), $child->getType()); + if ($this->detectLoop($itemName, $childName)) { + throw new InvalidCallException("Cannot add '$childName' as a child of '$itemName'. A loop has been detected."); + } + if (isset($this->_children[$itemName][$childName])) { + throw new InvalidCallException("The item '$itemName' already has a child '$childName'."); + } + $this->_children[$itemName][$childName] = $this->_items[$childName]; + return true; + } + + /** + * Removes a child from its parent. + * Note, the child item is not deleted. Only the parent-child relationship is removed. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the removal is successful + */ + public function removeItemChild($itemName, $childName) + { + if (isset($this->_children[$itemName][$childName])) { + unset($this->_children[$itemName][$childName]); + return true; + } else { + return false; + } + } + + /** + * Returns a value indicating whether a child exists within a parent. + * @param string $itemName the parent item name + * @param string $childName the child item name + * @return boolean whether the child exists + */ + public function hasItemChild($itemName, $childName) + { + return isset($this->_children[$itemName][$childName]); + } + + /** + * Returns the children of the specified item. + * @param mixed $names the parent item name. This can be either a string or an array. + * The latter represents a list of item names. + * @return Item[] all child items of the parent + */ + public function getItemChildren($names) + { + if (is_string($names)) { + return isset($this->_children[$names]) ? $this->_children[$names] : array(); + } + + $children = array(); + foreach ($names as $name) { + if (isset($this->_children[$name])) { + $children = array_merge($children, $this->_children[$name]); + } + } + return $children; + } + + /** + * Assigns an authorization item to a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @param string $bizRule the business rule to be executed when [[checkAccess()]] is called + * for this particular authorization item. + * @param mixed $data additional data associated with this assignment + * @return Assignment the authorization assignment information. + * @throws InvalidParamException if the item does not exist or if the item has already been assigned to the user + */ + public function assign($userId, $itemName, $bizRule = null, $data = null) + { + if (!isset($this->_items[$itemName])) { + throw new InvalidParamException("Unknown authorization item '$itemName'."); + } elseif (isset($this->_assignments[$userId][$itemName])) { + throw new InvalidParamException("Authorization item '$itemName' has already been assigned to user '$userId'."); + } else { + return $this->_assignments[$userId][$itemName] = new Assignment($this, $userId, $itemName, $bizRule, $data); + } + } + + /** + * Revokes an authorization assignment from a user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return boolean whether removal is successful + */ + public function revoke($userId, $itemName) + { + if (isset($this->_assignments[$userId][$itemName])) { + unset($this->_assignments[$userId][$itemName]); + return true; + } else { + return false; + } + } + + /** + * Returns a value indicating whether the item has been assigned to the user. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return boolean whether the item has been assigned to the user. + */ + public function isAssigned($userId, $itemName) + { + return isset($this->_assignments[$userId][$itemName]); + } + + /** + * Returns the item assignment information. + * @param mixed $userId the user ID (see [[User::id]]) + * @param string $itemName the item name + * @return Assignment the item assignment information. Null is returned if + * the item is not assigned to the user. + */ + public function getAssignment($userId, $itemName) + { + return isset($this->_assignments[$userId][$itemName]) ? $this->_assignments[$userId][$itemName] : null; + } + + /** + * Returns the item assignments for the specified user. + * @param mixed $userId the user ID (see [[User::id]]) + * @return Assignment[] the item assignment information for the user. An empty array will be + * returned if there is no item assigned to the user. + */ + public function getAssignments($userId) + { + return isset($this->_assignments[$userId]) ? $this->_assignments[$userId] : array(); + } + + /** + * Returns the authorization items of the specific type and user. + * @param mixed $userId the user ID. Defaults to null, meaning returning all items even if + * they are not assigned to a user. + * @param integer $type the item type (0: operation, 1: task, 2: role). Defaults to null, + * meaning returning all items regardless of their type. + * @return Item[] the authorization items of the specific type. + */ + public function getItems($userId = null, $type = null) + { + if ($userId === null && $type === null) { + return $this->_items; + } + $items = array(); + if ($userId === null) { + foreach ($this->_items as $name => $item) { + /** @var $item Item */ + if ($item->getType() == $type) { + $items[$name] = $item; + } + } + } elseif (isset($this->_assignments[$userId])) { + foreach ($this->_assignments[$userId] as $assignment) { + /** @var $assignment Assignment */ + $name = $assignment->getItemName(); + if (isset($this->_items[$name]) && ($type === null || $this->_items[$name]->getType() == $type)) { + $items[$name] = $this->_items[$name]; + } + } + } + return $items; + } + + /** + * Creates an authorization item. + * An authorization item represents an action permission (e.g. creating a post). + * It has three types: operation, task and role. + * Authorization items form a hierarchy. Higher level items inheirt permissions representing + * by lower level items. + * @param string $name the item name. This must be a unique identifier. + * @param integer $type the item type (0: operation, 1: task, 2: role). + * @param string $description description of the item + * @param string $bizRule business rule associated with the item. This is a piece of + * PHP code that will be executed when [[checkAccess()]] is called for the item. + * @param mixed $data additional data associated with the item. + * @return Item the authorization item + * @throws Exception if an item with the same name already exists + */ + public function createItem($name, $type, $description = '', $bizRule = null, $data = null) + { + if (isset($this->_items[$name])) { + throw new Exception('Unable to add an item whose name is the same as an existing item.'); + } + return $this->_items[$name] = new Item($this, $name, $type, $description, $bizRule, $data); + } + + /** + * Removes the specified authorization item. + * @param string $name the name of the item to be removed + * @return boolean whether the item exists in the storage and has been removed + */ + public function removeItem($name) + { + if (isset($this->_items[$name])) { + foreach ($this->_children as &$children) { + unset($children[$name]); + } + foreach ($this->_assignments as &$assignments) { + unset($assignments[$name]); + } + unset($this->_items[$name]); + return true; + } else { + return false; + } + } + + /** + * Returns the authorization item with the specified name. + * @param string $name the name of the item + * @return Item the authorization item. Null if the item cannot be found. + */ + public function getItem($name) + { + return isset($this->_items[$name]) ? $this->_items[$name] : null; + } + + /** + * Saves an authorization item to persistent storage. + * @param Item $item the item to be saved. + * @param string $oldName the old item name. If null, it means the item name is not changed. + * @throws InvalidParamException if an item with the same name already taken + */ + public function saveItem($item, $oldName = null) + { + if ($oldName !== null && ($newName = $item->getName()) !== $oldName) // name changed + { + if (isset($this->_items[$newName])) { + throw new InvalidParamException("Unable to change the item name. The name '$newName' is already used by another item."); + } + if (isset($this->_items[$oldName]) && $this->_items[$oldName] === $item) { + unset($this->_items[$oldName]); + $this->_items[$newName] = $item; + if (isset($this->_children[$oldName])) { + $this->_children[$newName] = $this->_children[$oldName]; + unset($this->_children[$oldName]); + } + foreach ($this->_children as &$children) { + if (isset($children[$oldName])) { + $children[$newName] = $children[$oldName]; + unset($children[$oldName]); + } + } + foreach ($this->_assignments as &$assignments) { + if (isset($assignments[$oldName])) { + $assignments[$newName] = $assignments[$oldName]; + unset($assignments[$oldName]); + } + } + } + } + } + + /** + * Saves the changes to an authorization assignment. + * @param Assignment $assignment the assignment that has been changed. + */ + public function saveAssignment($assignment) + { + } + + /** + * Saves authorization data into persistent storage. + * If any change is made to the authorization data, please make + * sure you call this method to save the changed data into persistent storage. + */ + public function save() + { + $items = array(); + foreach ($this->_items as $name => $item) { + /** @var $item Item */ + $items[$name] = array( + 'type' => $item->getType(), + 'description' => $item->getDescription(), + 'bizRule' => $item->getBizRule(), + 'data' => $item->getData(), + ); + if (isset($this->_children[$name])) { + foreach ($this->_children[$name] as $child) { + /** @var $child Item */ + $items[$name]['children'][] = $child->getName(); + } + } + } + + foreach ($this->_assignments as $userId => $assignments) { + foreach ($assignments as $name => $assignment) { + /** @var $assignment Assignment */ + if (isset($items[$name])) { + $items[$name]['assignments'][$userId] = array( + 'bizRule' => $assignment->getBizRule(), + 'data' => $assignment->getData(), + ); + } + } + } + + $this->saveToFile($items, $this->authFile); + } + + /** + * Loads authorization data. + */ + public function load() + { + $this->clearAll(); + + $items = $this->loadFromFile($this->authFile); + + foreach ($items as $name => $item) { + $this->_items[$name] = new Item($this, $name, $item['type'], $item['description'], $item['bizRule'], $item['data']); + } + + foreach ($items as $name => $item) { + if (isset($item['children'])) { + foreach ($item['children'] as $childName) { + if (isset($this->_items[$childName])) { + $this->_children[$name][$childName] = $this->_items[$childName]; + } + } + } + if (isset($item['assignments'])) { + foreach ($item['assignments'] as $userId => $assignment) { + $this->_assignments[$userId][$name] = new Assignment($this, $name, $userId, $assignment['bizRule'], $assignment['data']); + } + } + } + } + + /** + * Removes all authorization data. + */ + public function clearAll() + { + $this->clearAssignments(); + $this->_children = array(); + $this->_items = array(); + } + + /** + * Removes all authorization assignments. + */ + public function clearAssignments() + { + $this->_assignments = array(); + } + + /** + * Checks whether there is a loop in the authorization item hierarchy. + * @param string $itemName parent item name + * @param string $childName the name of the child item that is to be added to the hierarchy + * @return boolean whether a loop exists + */ + protected function detectLoop($itemName, $childName) + { + if ($childName === $itemName) { + return true; + } + if (!isset($this->_children[$childName], $this->_items[$itemName])) { + return false; + } + foreach ($this->_children[$childName] as $child) { + /** @var $child Item */ + if ($this->detectLoop($itemName, $child->getName())) { + return true; + } + } + return false; + } + + /** + * Loads the authorization data from a PHP script file. + * @param string $file the file path. + * @return array the authorization data + * @see saveToFile + */ + protected function loadFromFile($file) + { + if (is_file($file)) { + return require($file); + } else { + return array(); + } + } + + /** + * Saves the authorization data to a PHP script file. + * @param array $data the authorization data + * @param string $file the file path. + * @see loadFromFile + */ + protected function saveToFile($data, $file) + { + file_put_contents($file, " + * @author Alexander Kochetov + * @link http://www.yiiframework.com/ + * @copyright 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + * @since 2.0 + */ + +drop table if exists [tbl_auth_assignment]; +drop table if exists [tbl_auth_item_child]; +drop table if exists [tbl_auth_item]; + +create table [tbl_auth_item] +( + [name] varchar(64) not null, + [type] integer not null, + [description] text, + [bizrule] text, + [data] text, + primary key ([name]) +); + +create table [tbl_auth_item_child] +( + [parent] varchar(64) not null, + [child] varchar(64) not null, + primary key ([parent],[child]), + foreign key ([parent]) references [tbl_auth_item] ([name]) on delete cascade on update cascade, + foreign key ([child]) references [tbl_auth_item] ([name]) on delete cascade on update cascade +); + +create table [tbl_auth_assignment] +( + [item_name] varchar(64) not null, + [user_id] varchar(64) not null, + [bizrule] text, + [data] text, + primary key ([item_name],[user_id]), + foreign key ([item_name]) references [tbl_auth_item] ([name]) on delete cascade on update cascade +); diff --git a/yii/rbac/schema-mysql.sql b/yii/rbac/schema-mysql.sql new file mode 100644 index 0000000..aa8015b --- /dev/null +++ b/yii/rbac/schema-mysql.sql @@ -0,0 +1,43 @@ +/** + * Database schema required by \yii\rbac\DbManager. + * + * @author Qiang Xue + * @author Alexander Kochetov + * @link http://www.yiiframework.com/ + * @copyright 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + * @since 2.0 + */ + +drop table if exists `tbl_auth_assignment`; +drop table if exists `tbl_auth_item_child`; +drop table if exists `tbl_auth_item`; + +create table `tbl_auth_item` +( + `name` varchar(64) not null, + `type` integer not null, + `description` text, + `bizrule` text, + `data` text, + primary key (`name`) +) engine InnoDB; + +create table `tbl_auth_item_child` +( + `parent` varchar(64) not null, + `child` varchar(64) not null, + primary key (`parent`,`child`), + foreign key (`parent`) references `tbl_auth_item` (`name`) on delete cascade on update cascade, + foreign key (`child`) references `tbl_auth_item` (`name`) on delete cascade on update cascade +) engine InnoDB; + +create table `tbl_auth_assignment` +( + `item_name` varchar(64) not null, + `user_id` varchar(64) not null, + `bizrule` text, + `data` text, + primary key (`item_name`,`user_id`), + foreign key (`item_name`) references `tbl_auth_item` (`name`) on delete cascade on update cascade +) engine InnoDB; diff --git a/yii/rbac/schema-oci.sql b/yii/rbac/schema-oci.sql new file mode 100644 index 0000000..16b7964 --- /dev/null +++ b/yii/rbac/schema-oci.sql @@ -0,0 +1,43 @@ +/** + * Database schema required by \yii\rbac\DbManager. + * + * @author Qiang Xue + * @author Alexander Kochetov + * @link http://www.yiiframework.com/ + * @copyright 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + * @since 2.0 + */ + +drop table if exists "tbl_auth_assignment"; +drop table if exists "tbl_auth_item_child"; +drop table if exists "tbl_auth_item"; + +create table "tbl_auth_item" +( + "name" varchar(64) not null, + "type" integer not null, + "description" text, + "bizrule" text, + "data" text, + primary key ("name") +); + +create table "tbl_auth_item_child" +( + "parent" varchar(64) not null, + "child" varchar(64) not null, + primary key ("parent","child"), + foreign key ("parent") references "tbl_auth_item" ("name") on delete cascade on update cascade, + foreign key ("child") references "tbl_auth_item" ("name") on delete cascade on update cascade +); + +create table "tbl_auth_assignment" +( + "item_name" varchar(64) not null, + "user_id" varchar(64) not null, + "bizrule" text, + "data" text, + primary key ("item_name","user_id"), + foreign key ("item_name") references "tbl_auth_item" ("name") on delete cascade on update cascade +); diff --git a/yii/rbac/schema-pgsql.sql b/yii/rbac/schema-pgsql.sql new file mode 100644 index 0000000..16b7964 --- /dev/null +++ b/yii/rbac/schema-pgsql.sql @@ -0,0 +1,43 @@ +/** + * Database schema required by \yii\rbac\DbManager. + * + * @author Qiang Xue + * @author Alexander Kochetov + * @link http://www.yiiframework.com/ + * @copyright 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + * @since 2.0 + */ + +drop table if exists "tbl_auth_assignment"; +drop table if exists "tbl_auth_item_child"; +drop table if exists "tbl_auth_item"; + +create table "tbl_auth_item" +( + "name" varchar(64) not null, + "type" integer not null, + "description" text, + "bizrule" text, + "data" text, + primary key ("name") +); + +create table "tbl_auth_item_child" +( + "parent" varchar(64) not null, + "child" varchar(64) not null, + primary key ("parent","child"), + foreign key ("parent") references "tbl_auth_item" ("name") on delete cascade on update cascade, + foreign key ("child") references "tbl_auth_item" ("name") on delete cascade on update cascade +); + +create table "tbl_auth_assignment" +( + "item_name" varchar(64) not null, + "user_id" varchar(64) not null, + "bizrule" text, + "data" text, + primary key ("item_name","user_id"), + foreign key ("item_name") references "tbl_auth_item" ("name") on delete cascade on update cascade +); diff --git a/yii/rbac/schema-sqlite.sql b/yii/rbac/schema-sqlite.sql new file mode 100644 index 0000000..668196e --- /dev/null +++ b/yii/rbac/schema-sqlite.sql @@ -0,0 +1,43 @@ +/** + * Database schema required by \yii\rbac\DbManager. + * + * @author Qiang Xue + * @author Alexander Kochetov + * @link http://www.yiiframework.com/ + * @copyright 2008 Yii Software LLC + * @license http://www.yiiframework.com/license/ + * @since 2.0 + */ + +drop table if exists 'tbl_auth_assignment'; +drop table if exists 'tbl_auth_item_child'; +drop table if exists 'tbl_auth_item'; + +create table 'tbl_auth_item' +( + "name" varchar(64) not null, + "type" integer not null, + "description" text, + "bizrule" text, + "data" text, + primary key ("name") +); + +create table 'tbl_auth_item_child' +( + "parent" varchar(64) not null, + "child" varchar(64) not null, + primary key ("parent","child"), + foreign key ("parent") references 'tbl_auth_item' ("name") on delete cascade on update cascade, + foreign key ("child") references 'tbl_auth_item' ("name") on delete cascade on update cascade +); + +create table 'tbl_auth_assignment' +( + "item_name" varchar(64) not null, + "user_id" varchar(64) not null, + "bizrule" text, + "data" text, + primary key ("item_name","user_id"), + foreign key ("item_name") references 'tbl_auth_item' ("name") on delete cascade on update cascade +); diff --git a/framework/renderers/SmartyViewRenderer.php b/yii/renderers/SmartyViewRenderer.php similarity index 92% rename from framework/renderers/SmartyViewRenderer.php rename to yii/renderers/SmartyViewRenderer.php index 29d7b97..c6147c0 100644 --- a/framework/renderers/SmartyViewRenderer.php +++ b/yii/renderers/SmartyViewRenderer.php @@ -24,11 +24,6 @@ use yii\helpers\Html; class SmartyViewRenderer extends ViewRenderer { /** - * @var string the directory or path alias pointing to where Smarty code is located. - */ - public $smartyPath = '@app/vendors/Smarty'; - - /** * @var string the directory or path alias pointing to where Smarty cache will be stored. */ public $cachePath = '@app/runtime/Smarty/cache'; @@ -45,7 +40,6 @@ class SmartyViewRenderer extends ViewRenderer public function init() { - require_once(Yii::getAlias($this->smartyPath) . '/Smarty.class.php'); $this->smarty = new Smarty(); $this->smarty->setCompileDir(Yii::getAlias($this->compilePath)); $this->smarty->setCacheDir(Yii::getAlias($this->cachePath)); diff --git a/framework/renderers/TwigViewRenderer.php b/yii/renderers/TwigViewRenderer.php similarity index 89% rename from framework/renderers/TwigViewRenderer.php rename to yii/renderers/TwigViewRenderer.php index 79faa9f..de561d3 100644 --- a/framework/renderers/TwigViewRenderer.php +++ b/yii/renderers/TwigViewRenderer.php @@ -23,11 +23,6 @@ use yii\helpers\Html; class TwigViewRenderer extends ViewRenderer { /** - * @var string the directory or path alias pointing to where Twig code is located. - */ - public $twigPath = '@Twig'; - - /** * @var string the directory or path alias pointing to where Twig cache will be stored. */ public $cachePath = '@app/runtime/Twig/cache'; @@ -45,10 +40,6 @@ class TwigViewRenderer extends ViewRenderer public function init() { - if (!isset(Yii::$aliases['@Twig'])) { - Yii::setAlias('@Twig', $this->twigPath); - } - $loader = new \Twig_Loader_String(); $this->twig = new \Twig_Environment($loader, array_merge(array( diff --git a/framework/test/TestCase.php b/yii/test/TestCase.php similarity index 100% rename from framework/test/TestCase.php rename to yii/test/TestCase.php diff --git a/framework/test/WebTestCase.php b/yii/test/WebTestCase.php similarity index 100% rename from framework/test/WebTestCase.php rename to yii/test/WebTestCase.php diff --git a/framework/validators/BooleanValidator.php b/yii/validators/BooleanValidator.php similarity index 100% rename from framework/validators/BooleanValidator.php rename to yii/validators/BooleanValidator.php diff --git a/framework/validators/CaptchaValidator.php b/yii/validators/CaptchaValidator.php similarity index 97% rename from framework/validators/CaptchaValidator.php rename to yii/validators/CaptchaValidator.php index 4eba9df..2e58cf2 100644 --- a/framework/validators/CaptchaValidator.php +++ b/yii/validators/CaptchaValidator.php @@ -21,6 +21,7 @@ use yii\helpers\Html; */ class CaptchaValidator extends Validator { + public $skipOnEmpty = false; /** * @var boolean whether the comparison is case sensitive. Defaults to false. */ @@ -70,7 +71,7 @@ class CaptchaValidator extends Validator /** * Returns the CAPTCHA action object. * @throws InvalidConfigException - * @return CaptchaAction the action object + * @return \yii\web\CaptchaAction the action object */ public function getCaptchaAction() { diff --git a/framework/validators/CompareValidator.php b/yii/validators/CompareValidator.php similarity index 100% rename from framework/validators/CompareValidator.php rename to yii/validators/CompareValidator.php diff --git a/framework/validators/DateValidator.php b/yii/validators/DateValidator.php similarity index 100% rename from framework/validators/DateValidator.php rename to yii/validators/DateValidator.php diff --git a/framework/validators/DefaultValueValidator.php b/yii/validators/DefaultValueValidator.php similarity index 100% rename from framework/validators/DefaultValueValidator.php rename to yii/validators/DefaultValueValidator.php diff --git a/framework/validators/EmailValidator.php b/yii/validators/EmailValidator.php similarity index 100% rename from framework/validators/EmailValidator.php rename to yii/validators/EmailValidator.php diff --git a/framework/validators/ExistValidator.php b/yii/validators/ExistValidator.php similarity index 100% rename from framework/validators/ExistValidator.php rename to yii/validators/ExistValidator.php diff --git a/framework/validators/FileValidator.php b/yii/validators/FileValidator.php similarity index 99% rename from framework/validators/FileValidator.php rename to yii/validators/FileValidator.php index ebe6cad..cc36d07 100644 --- a/framework/validators/FileValidator.php +++ b/yii/validators/FileValidator.php @@ -116,7 +116,7 @@ class FileValidator extends Validator } if (!is_array($this->types)) { $this->types = preg_split('/[\s,]+/', strtolower($this->types), -1, PREG_SPLIT_NO_EMPTY); - } + } } /** @@ -138,8 +138,8 @@ class FileValidator extends Validator } } $object->$attribute = array_values($files); - if ($files === array()) { - $this->addError($object, $attribute, $this->uploadRequired); + if (empty($files)) { + $this->addError($object, $attribute, $this->uploadRequired); } if (count($files) > $this->maxFiles) { $this->addError($object, $attribute, $this->tooMany, array('{attribute}' => $attribute, '{limit}' => $this->maxFiles)); @@ -153,7 +153,7 @@ class FileValidator extends Validator if ($file instanceof UploadedFile && $file->getError() != UPLOAD_ERR_NO_FILE) { $this->validateFile($object, $attribute, $file); } else { - $this->addError($object, $attribute, $this->uploadRequired); + $this->addError($object, $attribute, $this->uploadRequired); } } } diff --git a/framework/validators/FilterValidator.php b/yii/validators/FilterValidator.php similarity index 100% rename from framework/validators/FilterValidator.php rename to yii/validators/FilterValidator.php diff --git a/framework/validators/InlineValidator.php b/yii/validators/InlineValidator.php similarity index 100% rename from framework/validators/InlineValidator.php rename to yii/validators/InlineValidator.php diff --git a/framework/validators/NumberValidator.php b/yii/validators/NumberValidator.php similarity index 100% rename from framework/validators/NumberValidator.php rename to yii/validators/NumberValidator.php diff --git a/framework/validators/RangeValidator.php b/yii/validators/RangeValidator.php similarity index 100% rename from framework/validators/RangeValidator.php rename to yii/validators/RangeValidator.php diff --git a/framework/validators/RegularExpressionValidator.php b/yii/validators/RegularExpressionValidator.php similarity index 100% rename from framework/validators/RegularExpressionValidator.php rename to yii/validators/RegularExpressionValidator.php diff --git a/framework/validators/RequiredValidator.php b/yii/validators/RequiredValidator.php similarity index 100% rename from framework/validators/RequiredValidator.php rename to yii/validators/RequiredValidator.php diff --git a/framework/validators/StringValidator.php b/yii/validators/StringValidator.php similarity index 100% rename from framework/validators/StringValidator.php rename to yii/validators/StringValidator.php diff --git a/framework/validators/UniqueValidator.php b/yii/validators/UniqueValidator.php similarity index 100% rename from framework/validators/UniqueValidator.php rename to yii/validators/UniqueValidator.php diff --git a/framework/validators/UrlValidator.php b/yii/validators/UrlValidator.php similarity index 100% rename from framework/validators/UrlValidator.php rename to yii/validators/UrlValidator.php diff --git a/framework/validators/Validator.php b/yii/validators/Validator.php similarity index 100% rename from framework/validators/Validator.php rename to yii/validators/Validator.php diff --git a/framework/views/error.php b/yii/views/error.php similarity index 100% rename from framework/views/error.php rename to yii/views/error.php diff --git a/framework/views/exception.php b/yii/views/exception.php similarity index 100% rename from framework/views/exception.php rename to yii/views/exception.php diff --git a/framework/views/migration.php b/yii/views/migration.php similarity index 100% rename from framework/views/migration.php rename to yii/views/migration.php diff --git a/framework/web/AccessControl.php b/yii/web/AccessControl.php similarity index 100% rename from framework/web/AccessControl.php rename to yii/web/AccessControl.php diff --git a/framework/web/AccessRule.php b/yii/web/AccessRule.php similarity index 100% rename from framework/web/AccessRule.php rename to yii/web/AccessRule.php diff --git a/framework/web/Application.php b/yii/web/Application.php similarity index 100% rename from framework/web/Application.php rename to yii/web/Application.php diff --git a/framework/web/AssetBundle.php b/yii/web/AssetBundle.php similarity index 100% rename from framework/web/AssetBundle.php rename to yii/web/AssetBundle.php diff --git a/framework/web/AssetConverter.php b/yii/web/AssetConverter.php similarity index 100% rename from framework/web/AssetConverter.php rename to yii/web/AssetConverter.php diff --git a/framework/web/AssetManager.php b/yii/web/AssetManager.php similarity index 100% rename from framework/web/AssetManager.php rename to yii/web/AssetManager.php diff --git a/framework/web/CacheSession.php b/yii/web/CacheSession.php similarity index 100% rename from framework/web/CacheSession.php rename to yii/web/CacheSession.php diff --git a/yii/web/CaptchaAction.php b/yii/web/CaptchaAction.php new file mode 100644 index 0000000..e3d6eaa --- /dev/null +++ b/yii/web/CaptchaAction.php @@ -0,0 +1,338 @@ + + * @since 2.0 + */ +class CaptchaAction extends Action +{ + /** + * The name of the GET parameter indicating whether the CAPTCHA image should be regenerated. + */ + const REFRESH_GET_VAR = 'refresh'; + /** + * @var integer how many times should the same CAPTCHA be displayed. Defaults to 3. + * A value less than or equal to 0 means the test is unlimited (available since version 1.1.2). + */ + public $testLimit = 3; + /** + * @var integer the width of the generated CAPTCHA image. Defaults to 120. + */ + public $width = 120; + /** + * @var integer the height of the generated CAPTCHA image. Defaults to 50. + */ + public $height = 50; + /** + * @var integer padding around the text. Defaults to 2. + */ + public $padding = 2; + /** + * @var integer the background color. For example, 0x55FF00. + * Defaults to 0xFFFFFF, meaning white color. + */ + public $backColor = 0xFFFFFF; + /** + * @var integer the font color. For example, 0x55FF00. Defaults to 0x2040A0 (blue color). + */ + public $foreColor = 0x2040A0; + /** + * @var boolean whether to use transparent background. Defaults to false. + */ + public $transparent = false; + /** + * @var integer the minimum length for randomly generated word. Defaults to 6. + */ + public $minLength = 6; + /** + * @var integer the maximum length for randomly generated word. Defaults to 7. + */ + public $maxLength = 7; + /** + * @var integer the offset between characters. Defaults to -2. You can adjust this property + * in order to decrease or increase the readability of the captcha. + **/ + public $offset = -2; + /** + * @var string the TrueType font file. This can be either a file path or path alias. + */ + public $fontFile = '@yii/web/SpicyRice.ttf'; + /** + * @var string the fixed verification code. When this is property is set, + * [[getVerifyCode()]] will always return the value of this property. + * This is mainly used in automated tests where we want to be able to reproduce + * the same verification code each time we run the tests. + * If not set, it means the verification code will be randomly generated. + */ + public $fixedVerifyCode; + + + /** + * Initializes the action. + * @throws InvalidConfigException if the font file does not exist. + */ + public function init() + { + $this->fontFile = Yii::getAlias($this->fontFile); + if (!is_file($this->fontFile)) { + throw new InvalidConfigException("The font file does not exist: {$this->fontFile}"); + } + } + + /** + * Runs the action. + */ + public function run() + { + if (isset($_GET[self::REFRESH_GET_VAR])) { + // AJAX request for regenerating code + $code = $this->getVerifyCode(true); + echo json_encode(array( + 'hash1' => $this->generateValidationHash($code), + 'hash2' => $this->generateValidationHash(strtolower($code)), + // we add a random 'v' parameter so that FireFox can refresh the image + // when src attribute of image tag is changed + 'url' => $this->controller->createUrl($this->id, array('v' => uniqid())), + )); + } else { + $this->renderImage($this->getVerifyCode()); + } + Yii::$app->end(); + } + + /** + * Generates a hash code that can be used for client side validation. + * @param string $code the CAPTCHA code + * @return string a hash code generated from the CAPTCHA code + */ + public function generateValidationHash($code) + { + for ($h = 0, $i = strlen($code) - 1; $i >= 0; --$i) { + $h += ord($code[$i]); + } + return $h; + } + + /** + * Gets the verification code. + * @param boolean $regenerate whether the verification code should be regenerated. + * @return string the verification code. + */ + public function getVerifyCode($regenerate = false) + { + if ($this->fixedVerifyCode !== null) { + return $this->fixedVerifyCode; + } + + $session = Yii::$app->session; + $session->open(); + $name = $this->getSessionKey(); + if ($session[$name] === null || $regenerate) { + $session[$name] = $this->generateVerifyCode(); + $session[$name . 'count'] = 1; + } + return $session[$name]; + } + + /** + * Validates the input to see if it matches the generated code. + * @param string $input user input + * @param boolean $caseSensitive whether the comparison should be case-sensitive + * @return boolean whether the input is valid + */ + public function validate($input, $caseSensitive) + { + $code = $this->getVerifyCode(); + $valid = $caseSensitive ? ($input === $code) : strcasecmp($input, $code) === 0; + $session = Yii::$app->session; + $session->open(); + $name = $this->getSessionKey() . 'count'; + $session[$name] = $session[$name] + 1; + if ($session[$name] > $this->testLimit && $this->testLimit > 0) { + $this->getVerifyCode(true); + } + return $valid; + } + + /** + * Generates a new verification code. + * @return string the generated verification code + */ + protected function generateVerifyCode() + { + if ($this->minLength < 3) { + $this->minLength = 3; + } + if ($this->maxLength > 20) { + $this->maxLength = 20; + } + if ($this->minLength > $this->maxLength) { + $this->maxLength = $this->minLength; + } + $length = mt_rand($this->minLength, $this->maxLength); + + $letters = 'bcdfghjklmnpqrstvwxyz'; + $vowels = 'aeiou'; + $code = ''; + for ($i = 0; $i < $length; ++$i) { + if ($i % 2 && mt_rand(0, 10) > 2 || !($i % 2) && mt_rand(0, 10) > 9) { + $code .= $vowels[mt_rand(0, 4)]; + } else { + $code .= $letters[mt_rand(0, 20)]; + } + } + + return $code; + } + + /** + * Returns the session variable name used to store verification code. + * @return string the session variable name + */ + protected function getSessionKey() + { + return '__captcha/' . $this->getUniqueId(); + } + + /** + * Renders the CAPTCHA image. + * @param string $code the verification code + */ + protected function renderImage($code) + { + if (Captcha::checkRequirements() === 'gd') { + $this->renderImageByGD($code); + } else { + $this->renderImageByImagick($code); + } + } + + /** + * Renders the CAPTCHA image based on the code using GD library. + * @param string $code the verification code + */ + protected function renderImageByGD($code) + { + $image = imagecreatetruecolor($this->width, $this->height); + + $backColor = imagecolorallocate($image, + (int)($this->backColor % 0x1000000 / 0x10000), + (int)($this->backColor % 0x10000 / 0x100), + $this->backColor % 0x100); + imagefilledrectangle($image, 0, 0, $this->width, $this->height, $backColor); + imagecolordeallocate($image, $backColor); + + if ($this->transparent) { + imagecolortransparent($image, $backColor); + } + + $foreColor = imagecolorallocate($image, + (int)($this->foreColor % 0x1000000 / 0x10000), + (int)($this->foreColor % 0x10000 / 0x100), + $this->foreColor % 0x100); + + if ($this->fontFile === null) { + $this->fontFile = dirname(__FILE__) . '/SpicyRice.ttf'; + } + + $length = strlen($code); + $box = imagettfbbox(30, 0, $this->fontFile, $code); + $w = $box[4] - $box[0] + $this->offset * ($length - 1); + $h = $box[1] - $box[5]; + $scale = min(($this->width - $this->padding * 2) / $w, ($this->height - $this->padding * 2) / $h); + $x = 10; + $y = round($this->height * 27 / 40); + for ($i = 0; $i < $length; ++$i) { + $fontSize = (int)(rand(26, 32) * $scale * 0.8); + $angle = rand(-10, 10); + $letter = $code[$i]; + $box = imagettftext($image, $fontSize, $angle, $x, $y, $foreColor, $this->fontFile, $letter); + $x = $box[2] + $this->offset; + } + + imagecolordeallocate($image, $foreColor); + + header('Pragma: public'); + header('Expires: 0'); + header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); + header('Content-Transfer-Encoding: binary'); + header("Content-type: image/png"); + imagepng($image); + imagedestroy($image); + } + + /** + * Renders the CAPTCHA image based on the code using ImageMagick library. + * @param string $code the verification code + */ + protected function renderImageByImagick($code) + { + $backColor = $this->transparent ? new \ImagickPixel('transparent') : new \ImagickPixel('#' . dechex($this->backColor)); + $foreColor = new \ImagickPixel('#' . dechex($this->foreColor)); + + $image = new \Imagick(); + $image->newImage($this->width, $this->height, $backColor); + + if ($this->fontFile === null) { + $this->fontFile = dirname(__FILE__) . '/SpicyRice.ttf'; + } + + $draw = new \ImagickDraw(); + $draw->setFont($this->fontFile); + $draw->setFontSize(30); + $fontMetrics = $image->queryFontMetrics($draw, $code); + + $length = strlen($code); + $w = (int)($fontMetrics['textWidth']) - 8 + $this->offset * ($length - 1); + $h = (int)($fontMetrics['textHeight']) - 8; + $scale = min(($this->width - $this->padding * 2) / $w, ($this->height - $this->padding * 2) / $h); + $x = 10; + $y = round($this->height * 27 / 40); + for ($i = 0; $i < $length; ++$i) { + $draw = new \ImagickDraw(); + $draw->setFont($this->fontFile); + $draw->setFontSize((int)(rand(26, 32) * $scale * 0.8)); + $draw->setFillColor($foreColor); + $image->annotateImage($draw, $x, $y, rand(-10, 10), $code[$i]); + $fontMetrics = $image->queryFontMetrics($draw, $code[$i]); + $x += (int)($fontMetrics['textWidth']) + $this->offset; + } + + header('Pragma: public'); + header('Expires: 0'); + header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); + header('Content-Transfer-Encoding: binary'); + header("Content-type: image/png"); + $image->setImageFormat('png'); + echo $image; + } +} diff --git a/framework/web/Controller.php b/yii/web/Controller.php similarity index 100% rename from framework/web/Controller.php rename to yii/web/Controller.php diff --git a/framework/web/Cookie.php b/yii/web/Cookie.php similarity index 100% rename from framework/web/Cookie.php rename to yii/web/Cookie.php diff --git a/framework/web/CookieCollection.php b/yii/web/CookieCollection.php similarity index 100% rename from framework/web/CookieCollection.php rename to yii/web/CookieCollection.php diff --git a/framework/web/DbSession.php b/yii/web/DbSession.php similarity index 99% rename from framework/web/DbSession.php rename to yii/web/DbSession.php index 2910b40..e81aa7f 100644 --- a/framework/web/DbSession.php +++ b/yii/web/DbSession.php @@ -71,13 +71,13 @@ class DbSession extends Session */ public function init() { - parent::init(); if (is_string($this->db)) { $this->db = Yii::$app->getComponent($this->db); } if (!$this->db instanceof Connection) { throw new InvalidConfigException("DbSession::db must be either a DB connection instance or the application component ID of a DB connection."); - } + } + parent::init(); } /** diff --git a/framework/web/HttpCache.php b/yii/web/HttpCache.php similarity index 100% rename from framework/web/HttpCache.php rename to yii/web/HttpCache.php diff --git a/framework/web/IAssetConverter.php b/yii/web/IAssetConverter.php similarity index 100% rename from framework/web/IAssetConverter.php rename to yii/web/IAssetConverter.php diff --git a/framework/web/Identity.php b/yii/web/Identity.php similarity index 100% rename from framework/web/Identity.php rename to yii/web/Identity.php diff --git a/framework/web/JsExpression.php b/yii/web/JsExpression.php similarity index 100% rename from framework/web/JsExpression.php rename to yii/web/JsExpression.php diff --git a/framework/web/PageCache.php b/yii/web/PageCache.php similarity index 100% rename from framework/web/PageCache.php rename to yii/web/PageCache.php diff --git a/framework/web/Pagination.php b/yii/web/Pagination.php similarity index 99% rename from framework/web/Pagination.php rename to yii/web/Pagination.php index 764dbb4..3d4e242 100644 --- a/framework/web/Pagination.php +++ b/yii/web/Pagination.php @@ -73,7 +73,7 @@ class Pagination extends \yii\base\Object * @var boolean whether to always have the page parameter in the URL created by [[createUrl()]]. * If false and [[page]] is 0, the page parameter will not be put in the URL. */ - public $forcePageVar = false; + public $forcePageVar = true; /** * @var string the route of the controller action for displaying the paged contents. * If not set, it means using the currently requested route. diff --git a/framework/web/Request.php b/yii/web/Request.php similarity index 99% rename from framework/web/Request.php rename to yii/web/Request.php index 5e2f064..d3f419b 100644 --- a/framework/web/Request.php +++ b/yii/web/Request.php @@ -69,8 +69,8 @@ class Request extends \yii\base\Request $result = Yii::$app->getUrlManager()->parseRequest($this); if ($result !== false) { list ($route, $params) = $result; - $params = array_merge($_GET, $params); - return array($route, $params); + $_GET = array_merge($_GET, $params); + return array($route, $_GET); } else { throw new HttpException(404, Yii::t('yii|Page not found.')); } diff --git a/framework/web/Response.php b/yii/web/Response.php similarity index 100% rename from framework/web/Response.php rename to yii/web/Response.php diff --git a/framework/web/Session.php b/yii/web/Session.php similarity index 100% rename from framework/web/Session.php rename to yii/web/Session.php diff --git a/framework/web/SessionIterator.php b/yii/web/SessionIterator.php similarity index 100% rename from framework/web/SessionIterator.php rename to yii/web/SessionIterator.php diff --git a/framework/web/Sort.php b/yii/web/Sort.php similarity index 99% rename from framework/web/Sort.php rename to yii/web/Sort.php index 99084c1..324e733 100644 --- a/framework/web/Sort.php +++ b/yii/web/Sort.php @@ -251,7 +251,7 @@ class Sort extends \yii\base\Object } } } - if ($this->_attributeOrders === array() && is_array($this->defaults)) { + if (empty($this->_attributeOrders) && is_array($this->defaults)) { $this->_attributeOrders = $this->defaults; } } diff --git a/yii/web/SpicyRice.md b/yii/web/SpicyRice.md new file mode 100644 index 0000000..d99f3dc --- /dev/null +++ b/yii/web/SpicyRice.md @@ -0,0 +1,11 @@ +## Spicy Rice font + +* **Author:** Brian J. Bonislawsky, Astigmatic (AOETI, Astigmatic One Eye Typographic Institute) +* **License:** SIL Open Font License (OFL), version 1.1, [notes and FAQ](http://scripts.sil.org/OFL) + +## Links + +* [Astigmatic](http://www.astigmatic.com/) +* [Google WebFonts](http://www.google.com/webfonts/specimen/Spicy+Rice) +* [fontsquirrel.com](http://www.fontsquirrel.com/fonts/spicy-rice) +* [fontspace.com](http://www.fontspace.com/astigmatic-one-eye-typographic-institute/spicy-rice) diff --git a/yii/web/SpicyRice.ttf b/yii/web/SpicyRice.ttf new file mode 100644 index 0000000..638436c Binary files /dev/null and b/yii/web/SpicyRice.ttf differ diff --git a/framework/web/UploadedFile.php b/yii/web/UploadedFile.php similarity index 100% rename from framework/web/UploadedFile.php rename to yii/web/UploadedFile.php diff --git a/framework/web/UrlManager.php b/yii/web/UrlManager.php similarity index 98% rename from framework/web/UrlManager.php rename to yii/web/UrlManager.php index a300033..aab7979 100644 --- a/framework/web/UrlManager.php +++ b/yii/web/UrlManager.php @@ -90,7 +90,7 @@ class UrlManager extends Component */ protected function compileRules() { - if (!$this->enablePrettyUrl || $this->rules === array()) { + if (!$this->enablePrettyUrl || empty($this->rules)) { return; } if (is_string($this->cache)) { @@ -190,13 +190,13 @@ class UrlManager extends Component if ($this->suffix !== null) { $route .= $this->suffix; } - if ($params !== array()) { + if (!empty($params)) { $route .= '?' . http_build_query($params); } return rtrim($baseUrl, '/') . '/' . $route . $anchor; } else { $url = $baseUrl . '?' . $this->routeVar . '=' . $route; - if ($params !== array()) { + if (!empty($params)) { $url .= '&' . http_build_query($params); } return $url; diff --git a/framework/web/UrlRule.php b/yii/web/UrlRule.php similarity index 97% rename from framework/web/UrlRule.php rename to yii/web/UrlRule.php index d9cb4fd..96cb994 100644 --- a/framework/web/UrlRule.php +++ b/yii/web/UrlRule.php @@ -125,7 +125,7 @@ class UrlRule extends Object if (isset($this->defaults[$name])) { $length = strlen($match[0][0]); $offset = $match[0][1]; - if ($this->pattern[$offset - 1] === '/' && $this->pattern[$offset + $length] === '/') { + if ($offset > 1 && $this->pattern[$offset - 1] === '/' && $this->pattern[$offset + $length] === '/') { $tr["/<$name>"] = "(/(?P<$name>$pattern))?"; } else { $tr["<$name>"] = "(?P<$name>$pattern)?"; @@ -144,7 +144,7 @@ class UrlRule extends Object $this->_template = preg_replace('/<(\w+):?([^>]+)?>/', '<$1>', $this->pattern); $this->pattern = '#^' . trim(strtr($this->_template, $tr), '/') . '$#u'; - if ($this->_routeParams !== array()) { + if (!empty($this->_routeParams)) { $this->_routeRule = '#^' . strtr($this->route, $tr2) . '$#u'; } } @@ -275,7 +275,7 @@ class UrlRule extends Object $url .= ($this->suffix === null ? $manager->suffix : $this->suffix); } - if ($params !== array()) { + if (!empty($params)) { $url .= '?' . http_build_query($params); } return $url; diff --git a/framework/web/User.php b/yii/web/User.php similarity index 91% rename from framework/web/User.php rename to yii/web/User.php index e05d88a..7d8e300 100644 --- a/framework/web/User.php +++ b/yii/web/User.php @@ -40,11 +40,11 @@ class User extends Component */ public $enableAutoLogin = false; /** - * @var string|array the URL for login when [[loginRequired()]] is called. + * @var string|array the URL for login when [[loginRequired()]] is called. * If an array is given, [[UrlManager::createUrl()]] will be called to create the corresponding URL. - * The first element of the array should be the route to the login action, and the rest of + * The first element of the array should be the route to the login action, and the rest of * the name-value pairs are GET parameters used to construct the login URL. For example, - * + * * ~~~ * array('site/login', 'ref' => 1) * ~~~ @@ -86,6 +86,8 @@ class User extends Component */ public $returnUrlVar = '__returnUrl'; + private $_access = array(); + /** * Initializes the application component. @@ -449,18 +451,33 @@ class User extends Component } /** - * Checks whether the user has access to the specified operation. - * @param $operator - * @param array $params - * @return bool - * @todo + * Performs access check for this user. + * @param string $operation the name of the operation that need access check. + * @param array $params name-value pairs that would be passed to business rules associated + * with the tasks and roles assigned to the user. A param with name 'userId' is added to + * this array, which holds the value of [[id]] when [[DbAuthManager]] or + * [[PhpAuthManager]] is used. + * @param boolean $allowCaching whether to allow caching the result of access check. + * When this parameter is true (default), if the access check of an operation was performed + * before, its result will be directly returned when calling this method to check the same + * operation. If this parameter is false, this method will always call + * [[AuthManager::checkAccess()]] to obtain the up-to-date access result. Note that this + * caching is effective only within the same request and only works when `$params = array()`. + * @return boolean whether the operations can be performed by this user. */ - public function checkAccess($operation, $params = array()) + public function checkAccess($operation, $params = array(), $allowCaching = true) { $auth = Yii::$app->getAuthManager(); - if ($auth !== null) { - return $auth->checkAccess($this->getId(), $operation, $params); + if ($auth === null) { + return false; + } + if ($allowCaching && empty($params) && isset($this->_access[$operation])) { + return $this->_access[$operation]; + } + $access = $auth->checkAccess($this->getId(), $operation, $params); + if ($allowCaching && empty($params)) { + $this->_access[$operation] = $access; } - return false; + return $access; } } diff --git a/framework/web/UserEvent.php b/yii/web/UserEvent.php similarity index 100% rename from framework/web/UserEvent.php rename to yii/web/UserEvent.php diff --git a/framework/widgets/ActiveField.php b/yii/widgets/ActiveField.php similarity index 99% rename from framework/widgets/ActiveField.php rename to yii/widgets/ActiveField.php index 0e0381f..9f3f201 100644 --- a/framework/widgets/ActiveField.php +++ b/yii/widgets/ActiveField.php @@ -104,7 +104,7 @@ class ActiveField extends Component public function begin() { $options = $this->getClientOptions(); - if ($options !== array()) { + if (!empty($options)) { $this->form->attributes[$this->attribute] = $options; } @@ -123,7 +123,7 @@ class ActiveField extends Component return Html::beginTag($this->tag, $options); } - + public function end() { return Html::endTag($this->tag); @@ -143,7 +143,7 @@ class ActiveField extends Component $validators[] = $js; } } - if ($validators !== array()) { + if (!empty($validators)) { $options['validate'] = new JsExpression("function(attribute,value,messages){" . implode('', $validators) . '}'); } } @@ -152,7 +152,7 @@ class ActiveField extends Component $options['enableAjaxValidation'] = 1; } - if ($enableClientValidation || $enableAjaxValidation) { + if ($enableClientValidation && !empty($options['validate']) || $enableAjaxValidation) { $inputID = Html::getInputId($this->model, $this->attribute); $options['name'] = $inputID; $names = array( diff --git a/framework/widgets/ActiveForm.php b/yii/widgets/ActiveForm.php similarity index 99% rename from framework/widgets/ActiveForm.php rename to yii/widgets/ActiveForm.php index 61416e2..24451b9 100644 --- a/framework/widgets/ActiveForm.php +++ b/yii/widgets/ActiveForm.php @@ -130,7 +130,7 @@ class ActiveForm extends Widget */ public function run() { - if ($this->attributes !== array()) { + if (!empty($this->attributes)) { $id = $this->options['id']; $options = Json::encode($this->getClientOptions()); $attributes = Json::encode($this->attributes); @@ -197,7 +197,7 @@ class ActiveForm extends Widget $options['class'] .= ' ' . $this->errorSummaryCssClass; } - if ($lines !== array()) { + if (!empty($lines)) { $content = "
  • " . implode("
  • \n
  • ", $lines) . "
    • "; return Html::tag('div', $header . $content . $footer, $options); } else { diff --git a/framework/widgets/Block.php b/yii/widgets/Block.php similarity index 100% rename from framework/widgets/Block.php rename to yii/widgets/Block.php diff --git a/framework/widgets/Breadcrumbs.php b/yii/widgets/Breadcrumbs.php similarity index 100% rename from framework/widgets/Breadcrumbs.php rename to yii/widgets/Breadcrumbs.php diff --git a/yii/widgets/Captcha.php b/yii/widgets/Captcha.php new file mode 100644 index 0000000..918e30c --- /dev/null +++ b/yii/widgets/Captcha.php @@ -0,0 +1,102 @@ + + * @since 2.0 + */ +class Captcha extends Widget +{ + /** + * @var string the route of the action that generates the CAPTCHA images. + * The action represented by this route must be an action of [[CaptchaAction]]. + */ + public $captchaAction = 'site/captcha'; + /** + * @var array HTML attributes to be applied to the rendered image element. + */ + public $options = array(); + + + /** + * Renders the widget. + */ + public function run() + { + $this->checkRequirements(); + + if (!isset($this->options['id'])) { + $this->options['id'] = $this->getId(); + } + $id = $this->options['id']; + $options = Json::encode($this->getClientOptions()); + $this->view->registerAssetBundle('yii/captcha'); + $this->view->registerJs("jQuery('#$id').yiiCaptcha($options);"); + $url = Yii::$app->getUrlManager()->createUrl($this->captchaAction, array('v' => uniqid())); + echo Html::img($url, $this->options); + } + + /** + * Returns the options for the captcha JS widget. + * @return array the options + */ + protected function getClientOptions() + { + $options = array( + 'refreshUrl' => Html::url(array($this->captchaAction, CaptchaAction::REFRESH_GET_VAR => 1)), + 'hashKey' => "yiiCaptcha/{$this->captchaAction}", + ); + return $options; + } + + /** + * Checks if there is graphic extension available to generate CAPTCHA images. + * This method will check the existence of ImageMagick and GD extensions. + * @return string the name of the graphic extension, either "imagick" or "gd". + * @throws InvalidConfigException if neither ImageMagick nor GD is installed. + */ + public static function checkRequirements() + { + if (extension_loaded('imagick')) { + $imagick = new \Imagick(); + $imagickFormats = $imagick->queryFormats('PNG'); + if (in_array('PNG', $imagickFormats)) { + return 'imagick'; + } + } + if (extension_loaded('gd')) { + $gdInfo = gd_info(); + if (!empty($gdInfo['FreeType Support'])) { + return 'gd'; + } + } + throw new InvalidConfigException('GD with FreeType or ImageMagick PHP extensions are required.'); + } +} diff --git a/framework/widgets/ContentDecorator.php b/yii/widgets/ContentDecorator.php similarity index 100% rename from framework/widgets/ContentDecorator.php rename to yii/widgets/ContentDecorator.php diff --git a/framework/widgets/FragmentCache.php b/yii/widgets/FragmentCache.php similarity index 94% rename from framework/widgets/FragmentCache.php rename to yii/widgets/FragmentCache.php index 5b37f6e..aa24acd 100644 --- a/framework/widgets/FragmentCache.php +++ b/yii/widgets/FragmentCache.php @@ -107,7 +107,7 @@ class FragmentCache extends Widget $data = array($content, $this->dynamicPlaceholders); $this->cache->set($this->calculateKey(), $data, $this->duration, $this->dependency); - if ($this->view->cacheStack === array() && !empty($this->dynamicPlaceholders)) { + if (empty($this->view->cacheStack) && !empty($this->dynamicPlaceholders)) { $content = $this->updateDynamicContent($content, $this->dynamicPlaceholders); } echo $content; @@ -133,7 +133,7 @@ class FragmentCache extends Widget if (is_array($data) && count($data) === 2) { list ($content, $placeholders) = $data; if (is_array($placeholders) && count($placeholders) > 0) { - if ($this->view->cacheStack === array()) { + if (empty($this->view->cacheStack)) { // outermost cache: replace placeholder with dynamic content $content = $this->updateDynamicContent($content, $placeholders); } @@ -171,4 +171,4 @@ class FragmentCache extends Widget } return $this->cache->buildKey($factors); } -} +} diff --git a/yii/widgets/LinkPager.php b/yii/widgets/LinkPager.php new file mode 100644 index 0000000..1651246 --- /dev/null +++ b/yii/widgets/LinkPager.php @@ -0,0 +1,201 @@ + + * @since 2.0 + */ +class LinkPager extends Widget +{ + /** + * @var Pagination the pagination object that this pager is associated with. + * You must set this property in order to make LinkPager work. + */ + public $pagination; + /** + * @var array HTML attributes for the pager container tag. + */ + public $options = array('class' => 'pagination'); + /** + * @var string the CSS class for the "first" page button. + */ + public $firstPageCssClass = 'first'; + /** + * @var string the CSS class for the "last" page button. + */ + public $lastPageCssClass = 'last'; + /** + * @var string the CSS class for the "previous" page button. + */ + public $prevPageCssClass = 'prev'; + /** + * @var string the CSS class for the "next" page button. + */ + public $nextPageCssClass = 'next'; + /** + * @var string the CSS class for the active (currently selected) page button. + */ + public $activePageCssClass = 'active'; + /** + * @var string the CSS class for the disabled page buttons. + */ + public $disabledPageCssClass = 'disabled'; + /** + * @var integer maximum number of page buttons that can be displayed. Defaults to 10. + */ + public $maxButtonCount = 10; + /** + * @var string the label for the "next" page button. Note that this will NOT be HTML-encoded. + * If this property is null, the "next" page button will not be displayed. + */ + public $nextPageLabel = '»'; + /** + * @var string the text label for the previous page button. Note that this will NOT be HTML-encoded. + * If this property is null, the "previous" page button will not be displayed. + */ + public $prevPageLabel = '«'; + /** + * @var string the text label for the "first" page button. Note that this will NOT be HTML-encoded. + * If this property is null, the "first" page button will not be displayed. + */ + public $firstPageLabel; + /** + * @var string the text label for the "last" page button. Note that this will NOT be HTML-encoded. + * If this property is null, the "last" page button will not be displayed. + */ + public $lastPageLabel; + /** + * @var string the template used to render the content within the pager container. + * The token "{buttons}" will be replaced with the actual page buttons. + */ + public $template = '{buttons}'; + + + /** + * Initializes the pager. + */ + public function init() + { + if ($this->pagination === null) { + throw new InvalidConfigException('The "pagination" property must be set.'); + } + } + + /** + * Executes the widget. + * This overrides the parent implementation by displaying the generated page buttons. + */ + public function run() + { + $buttons = strtr($this->template, array( + '{buttons}' => Html::tag('ul', implode("\n", $this->createPageButtons())), + )); + echo Html::tag('div', $buttons, $this->options); + } + + /** + * Creates the page buttons. + * @return array a list of page buttons (in HTML code). + */ + protected function createPageButtons() + { + $buttons = array(); + + $pageCount = $this->pagination->pageCount; + $currentPage = $this->pagination->getPage(); + + // first page + if ($this->firstPageLabel !== null) { + $buttons[] = $this->createPageButton($this->firstPageLabel, 0, $this->firstPageCssClass, $currentPage <= 0, false); + } + + // prev page + if ($this->prevPageLabel !== null) { + if (($page = $currentPage - 1) < 0) { + $page = 0; + } + $buttons[] = $this->createPageButton($this->prevPageLabel, $page, $this->prevPageCssClass, $currentPage <= 0, false); + } + + // internal pages + list($beginPage, $endPage) = $this->getPageRange(); + for ($i = $beginPage; $i <= $endPage; ++$i) { + $buttons[] = $this->createPageButton($i + 1, $i, null, false, $i == $currentPage); + } + + // next page + if ($this->nextPageLabel !== null) { + if (($page = $currentPage + 1) >= $pageCount - 1) { + $page = $pageCount - 1; + } + $buttons[] = $this->createPageButton($this->nextPageLabel, $page, $this->nextPageCssClass, $currentPage >= $pageCount - 1, false); + } + + // last page + if ($this->lastPageLabel !== null) { + $buttons[] = $this->createPageButton($this->lastPageLabel, $pageCount - 1, $this->lastPageCssClass, $currentPage >= $pageCount - 1, false); + } + + return $buttons; + } + + /** + * Creates a page button. + * You may override this method to customize the generation of page buttons. + * @param string $label the text label for the button + * @param integer $page the page number + * @param string $class the CSS class for the page button. + * @param boolean $disabled whether this page button is disabled + * @param boolean $active whether this page button is active + * @return string the generated button + */ + protected function createPageButton($label, $page, $class, $disabled, $active) + { + if ($active) { + $class .= ' ' . $this->activePageCssClass; + } + if ($disabled) { + $class .= ' ' . $this->disabledPageCssClass; + } + $class = trim($class); + $options = array('class' => $class === '' ? null : $class); + return Html::tag('li', Html::a($label, $this->pagination->createUrl($page)), $options); + } + + /** + * @return array the begin and end pages that need to be displayed. + */ + protected function getPageRange() + { + $currentPage = $this->pagination->getPage(); + $pageCount = $this->pagination->getPageCount(); + + $beginPage = max(0, $currentPage - (int)($this->maxButtonCount / 2)); + if (($endPage = $beginPage + $this->maxButtonCount - 1) >= $pageCount) { + $endPage = $pageCount - 1; + $beginPage = max(0, $endPage - $this->maxButtonCount + 1); + } + return array($beginPage, $endPage); + } +} \ No newline at end of file diff --git a/yii/widgets/ListPager.php b/yii/widgets/ListPager.php new file mode 100644 index 0000000..1bc2b21 --- /dev/null +++ b/yii/widgets/ListPager.php @@ -0,0 +1,95 @@ + + * @since 2.0 + */ +class ListPager extends Widget +{ + /** + * @var Pagination the pagination object that this pager is associated with. + * You must set this property in order to make ListPager work. + */ + public $pagination; + /** + * @var array HTML attributes for the drop-down list tag. The following options are specially handled: + * + * - prompt: string, a prompt text to be displayed as the first option. + * + * 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. + */ + public $options = array(); + /** + * @var string the template used to render the label for each list option. + * The token "{page}" will be replaced with the actual page number (1-based). + */ + public $template = '{page}'; + + + /** + * Initializes the pager. + */ + public function init() + { + if ($this->pagination === null) { + throw new InvalidConfigException('The "pagination" property must be set.'); + } + } + + /** + * Executes the widget. + * This overrides the parent implementation by displaying the generated page buttons. + */ + public function run() + { + $pageCount = $this->pagination->pageCount; + $currentPage = $this->pagination->getPage(); + + $pages = array(); + for ($i = 0; $i < $pageCount; ++$i) { + $pages[$this->pagination->createUrl($i)] = $this->generatePageText($i); + } + $selection = $this->pagination->createUrl($currentPage); + + if (!isset($this->options['onchange'])) { + $this->options['onchange'] = "if(this.value!='') {window.location=this.value;};"; + } + + echo Html::dropDownList(null, $selection, $pages, $this->options); + } + + /** + * Generates the label of the list option for the specified page number. + * You may override this method to customize the option display. + * @param integer $page zero-based page number + * @return string the list option for the page number + */ + protected function generatePageText($page) + { + return strtr($this->template, array( + '{page}' => $page + 1, + )); + } + +} \ No newline at end of file diff --git a/yii/widgets/Menu.php b/yii/widgets/Menu.php new file mode 100644 index 0000000..b8f69e1 --- /dev/null +++ b/yii/widgets/Menu.php @@ -0,0 +1,287 @@ +widget('yii\widgets\Menu', array( + * 'items' => array( + * // Important: you need to specify url as 'controller/action', + * // not just as 'controller' even if default acion is used. + * array('label' => 'Home', 'url' => array('site/index')), + * // 'Products' menu item will be selected as long as the route is 'product/index' + * array('label' => 'Products', 'url' => array('product/index'), 'items' => array( + * array('label' => 'New Arrivals', 'url' => array('product/index', 'tag' => 'new')), + * array('label' => 'Most Popular', 'url' => array('product/index', 'tag' => 'popular')), + * )), + * array('label' => 'Login', 'url' => array('site/login'), 'visible' => Yii::app()->user->isGuest), + * ), + * )); + * ~~~ + * + * @author Qiang Xue + * @since 2.0 + */ +class Menu extends Widget +{ + /** + * @var array list of menu items. Each menu item should be an array of the following structure: + * + * - label: string, optional, specifies the menu item label. When [[encodeLabels]] is true, the label + * will be HTML-encoded. If the label is not specified, an empty string will be used. + * - url: string or array, optional, specifies the URL of the menu item. It will be processed by [[Html::url]]. + * When this is set, the actual menu item content will be generated using [[linkTemplate]]; + * otherwise, [[labelTemplate]] will be used. + * - visible: boolean, optional, whether this menu item is visible. Defaults to true. + * - items: array, optional, specifies the sub-menu items. Its format is the same as the parent items. + * - active: boolean, optional, whether this menu item is in active state (currently selected). + * If a menu item is active, its CSS class will be appended with [[activeCssClass]]. + * If this option is not set, the menu item will be set active automatically when the current request + * is triggered by [[url]]. For more details, please refer to [[isItemActive()]]. + * - template: string, optional, the template used to render the content of this menu item. + * The token `{url}` will be replaced by the URL associated with this menu item, + * and the token `{label}` will be replaced by the label of the menu item. + * If this option is not set, [[linkTemplate]] or [[labelTemplate]] will be used instead. + */ + public $items = array(); + /** + * @var string the template used to render the body of a menu which is a link. + * In this template, the token `{url}` will be replaced with the corresponding link URL; + * while `{label}` will be replaced with the link text. + * This property will be overridden by the `template` option set in individual menu items via [[items]]. + */ + public $linkTemplate = '{label}'; + /** + * @var string the template used to render the body of a menu which is NOT a link. + * In this template, the token `{label}` will be replaced with the label of the menu item. + * This property will be overridden by the `template` option set in individual menu items via [[items]]. + */ + public $labelTemplate = '{label}'; + /** + * @var string the template used to render a list of sub-menus. + * In this template, the token `{items}` will be replaced with the renderer sub-menu items. + */ + public $submenuTemplate = "\n
        \n{items}\n
      \n"; + /** + * @var boolean whether the labels for menu items should be HTML-encoded. + */ + public $encodeLabels = true; + /** + * @var string the CSS class to be appended to the active menu item. + */ + public $activeCssClass = 'active'; + /** + * @var boolean whether to automatically activate items according to whether their route setting + * matches the currently requested route. + * @see isItemActive + */ + public $activateItems = true; + /** + * @var boolean whether to activate parent menu items when one of the corresponding child menu items is active. + * The activated parent menu items will also have its CSS classes appended with [[activeCssClass]]. + */ + public $activateParents = false; + /** + * @var boolean whether to hide empty menu items. An empty menu item is one whose `url` option is not + * set and which has no visible child menu items. + */ + public $hideEmptyItems = true; + /** + * @var array the HTML attributes for the menu's container tag. + */ + public $options = array(); + /** + * @var string the CSS class that will be assigned to the first item in the main menu or each submenu. + * Defaults to null, meaning no such CSS class will be assigned. + */ + public $firstItemCssClass; + /** + * @var string the CSS class that will be assigned to the last item in the main menu or each submenu. + * Defaults to null, meaning no such CSS class will be assigned. + */ + public $lastItemCssClass; + /** + * @var string the route used to determine if a menu item is active or not. + * If not set, it will use the route of the current request. + * @see params + * @see isItemActive + */ + public $route; + /** + * @var array the parameters used to determine if a menu item is active or not. + * If not set, it will use `$_GET`. + * @see route + * @see isItemActive + */ + public $params; + + + /** + * Renders the menu. + */ + public function run() + { + if ($this->route === null && Yii::$app->controller !== null) { + $this->route = Yii::$app->controller->getRoute(); + } + if ($this->params === null) { + $this->params = $_GET; + } + $items = $this->normalizeItems($this->items, $hasActiveChild); + echo Html::tag('ul', $this->renderItems($items), $this->options); + } + + /** + * Recursively renders the menu items (without the container tag). + * @param array $items the menu items to be rendered recursively + * @return string the rendering result + */ + protected function renderItems($items) + { + $n = count($items); + $lines = array(); + foreach ($items as $i => $item) { + $options = isset($item['itemOptions']) ? $item['itemOptions'] : array(); + $class = array(); + if ($item['active']) { + $class[] = $this->activeCssClass; + } + if ($i === 0 && $this->firstItemCssClass !== null) { + $class[] = $this->firstItemCssClass; + } + if ($i === $n - 1 && $this->lastItemCssClass !== null) { + $class[] = $this->lastItemCssClass; + } + if (!empty($class)) { + if (empty($options['class'])) { + $options['class'] = implode(' ', $class); + } else { + $options['class'] .= ' ' . implode(' ', $class); + } + } + + $menu = $this->renderItem($item); + if (!empty($item['items'])) { + $menu .= strtr($this->submenuTemplate, array( + '{items}' => $this->renderItems($item['items']), + )); + } + $lines[] = Html::tag('li', $menu, $options); + } + return implode("\n", $lines); + } + + /** + * Renders the content of a menu item. + * Note that the container and the sub-menus are not rendered here. + * @param array $item the menu item to be rendered. Please refer to [[items]] to see what data might be in the item. + * @return string the rendering result + */ + protected function renderItem($item) + { + if (isset($item['url'])) { + $template = isset($item['template']) ? $item['template'] : $this->linkTemplate; + return strtr($template, array( + '{url}' => Html::url($item['url']), + '{label}' => $item['label'], + )); + } else { + $template = isset($item['template']) ? $item['template'] : $this->labelTemplate; + return strtr($template, array( + '{label}' => $item['label'], + )); + } + } + + /** + * Normalizes the [[items]] property to remove invisible items and activate certain items. + * @param array $items the items to be normalized. + * @param boolean $active whether there is an active child menu item. + * @return array the normalized menu items + */ + protected function normalizeItems($items, &$active) + { + foreach ($items as $i => $item) { + if (isset($item['visible']) && !$item['visible']) { + unset($items[$i]); + continue; + } + if (!isset($item['label'])) { + $item['label'] = ''; + } + if ($this->encodeLabels) { + $items[$i]['label'] = Html::encode($item['label']); + } + $hasActiveChild = false; + if (isset($item['items'])) { + $items[$i]['items'] = $this->normalizeItems($item['items'], $route, $hasActiveChild); + if (empty($items[$i]['items']) && $this->hideEmptyItems) { + unset($items[$i]['items']); + if (!isset($item['url'])) { + unset($items[$i]); + continue; + } + } + } + if (!isset($item['active'])) { + if ($this->activateParents && $hasActiveChild || $this->activateItems && $this->isItemActive($item)) { + $active = $items[$i]['active'] = true; + } else { + $items[$i]['active'] = false; + } + } elseif ($item['active']) { + $active = true; + } + } + return array_values($items); + } + + /** + * Checks whether a menu item is active. + * This is done by checking if [[route]] and [[params]] match that specified in the `url` option of the menu item. + * When the `url` option of a menu item is specified in terms of an array, its first element is treated + * as the route for the item and the rest of the elements are the associated parameters. + * Only when its route and parameters match [[route]] and [[params]], respectively, will a menu item + * be considered active. + * @param array $item the menu item to be checked + * @return boolean whether the menu item is active + */ + protected function isItemActive($item) + { + if (isset($item['url']) && is_array($item['url']) && trim($item['url'][0], '/') === $this->route) { + unset($item['url']['#']); + if (count($item['url']) > 1) { + foreach (array_splice($item['url'], 1) as $name => $value) { + if (!isset($this->params[$name]) || $this->params[$name] != $value) { + return false; + } + } + } + return true; + } + return false; + } + +} diff --git a/framework/yiic b/yii/yiic similarity index 100% rename from framework/yiic rename to yii/yiic diff --git a/framework/yiic.bat b/yii/yiic.bat similarity index 100% rename from framework/yiic.bat rename to yii/yiic.bat diff --git a/framework/yiic.php b/yii/yiic.php similarity index 94% rename from framework/yiic.php rename to yii/yiic.php index 3872e2f..7cd0c40 100644 --- a/framework/yiic.php +++ b/yii/yiic.php @@ -12,7 +12,7 @@ defined('YII_DEBUG') or define('YII_DEBUG', true); // fcgi doesn't have STDIN defined by default defined('STDIN') or define('STDIN', fopen('php://stdin', 'r')); -require(__DIR__ . '/yii.php'); +require(__DIR__ . '/Yii.php'); $application = new yii\console\Application(array( 'id' => 'yiic',