diff --git a/CHANGELOG b/CHANGELOG index fab4a6149..1df0e4b87 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -27,6 +27,7 @@ Version 1.0.3 to be released - New #138: Added support to specify additional attributes for OPTION tags (Qiang) - New #140: Allow the route in a URL rule to contain parameters by itself (Qiang) - New #146: Added CUrlManager.appendParams which allows creating URLs in path format whose GET parameters are all in the query part (Qiang) +- New #150: Register CTabView css file with media type='screen' (Qiang) - New #156: Added CUrlManager.cacheID to allow disabling URL rule caching (Qiang) - New: Upgraded jquery to 1.3.1 (Qiang) - New: Upgraded jquery star rating to 2.61 (Qiang) @@ -45,7 +46,7 @@ Version 1.0.3 to be released - New: Added support for auto-incremental composite primary keys in active record (Qiang) - New: Added support for application modules (Qiang) - New: Added "module" command for yiic shell tool (Qiang) - +- New: Added support for using default roles in RBAC (Qiang) Version 1.0.2 February 1, 2009 ------------------------------ diff --git a/docs/guide/basics.controller.txt b/docs/guide/basics.controller.txt index cb0a909d5..64d384f8c 100644 --- a/docs/guide/basics.controller.txt +++ b/docs/guide/basics.controller.txt @@ -52,6 +52,9 @@ and action. >class files are in lower case, and both [controller map|CWebApplication::controllerMap] >and [action map|CController::actions] are using keys in lower case. +Since version 1.0.3, an application can contain [modules](/doc/guide/basics.module). The route for a controller action inside a module is in the format of `moduleID/controllerID/actionID`. For more details, see the [section about modules](/doc/guide/basics.module). + + Controller Instantiation ------------------------ @@ -72,10 +75,12 @@ controller instance. - If the ID is in the format of `'path/to/xyz'`, the controller class name is assumed to be `XyzController` and the corresponding class file is `protected/controllers/path/to/XyzController.php`. For example, a controller -ID `admin.user` would be resolved as the controller class `UserController` +ID `admin/user` would be resolved as the controller class `UserController` and the class file `protected/controllers/admin/UserController.php`. If the class file does not exist, a 404 [CHttpException] will be raised. +In case when [modules](/doc/guide/basics.module) are used (available since version 1.0.3), the above process is slighly different. In particular, the application will check if the ID refers to a controller inside a module, and if so, the module instance will be created first followed by the controller instance. + Action ------ diff --git a/docs/guide/basics.module.txt b/docs/guide/basics.module.txt new file mode 100644 index 000000000..c3bd51ead --- /dev/null +++ b/docs/guide/basics.module.txt @@ -0,0 +1,93 @@ +Module +====== + +> Note: Support for module has been available since version 1.0.3. + +A module is a self-contained software unit that consists of [models](/doc/guide/basics.model), [views](/doc/guide/basics.view), [controllers](/doc/guide/basics.controller) and other supporting components. In many aspects, a module resembles to an [application](/doc/guide/basics.application). The main difference is that a module cannot be deployed alone and it must reside inside of an application. Users can access the controllers in a module like they do with normal application controllers. + +Modules are useful in several scenarios. For a large-scale application, we may divide it into several modules, each being developed and maintained separately. Some commonly used features, such as user management, comment management, may be developed in terms of modules so that they can be reused easily in future projects. + + +Creating Module +--------------- + +A module is organized as a directory whose name serves as its unique [ID|CWebModule::id]. The structure of the module directory is similar to that of the [application base directory](/doc/guide/basics.application#application-base-directory). The following shows the typical directory structure of a module named `forum`: + +~~~ +forum/ + ForumModule.php the module class file + components/ containing reusable user components + views/ containing view files for widgets + controllers/ containing controller class files + DefaultController.php the default controller class file + extensions/ containing third-party extensions + models/ containing model class files + views/ containing controller view and layout files + layouts/ containing layout view files + default/ containing view files for DefaultController + index.php the index view file +~~~ + +A module must have a module class that extends from [CWebModule]. The class name is determined using the expression `ucfirst($id).'Module'`, where `$id` refers to the module ID (or the module directory name). The module class serves as the central place for storing information shared among the module code. For example, we can use [CWebModule::params] to store module parameters, and use [CWebModule::components] to share [application components](/doc/guide/basics.application#application-component) at the module level. + +> Tip: We can use the `yiic` tool to create the basic skeleton of a new module. For example, to create the above `forum` module, we can execute the following commands in a command line window: +> +> ~~~ +> % cd WebRoot/testdrive +> % protected/yiic shell +> Yii Interactive Tool v1.0 +> Please type 'help' for help. Type 'exit' to quit. +> >> module forum +> ~~~ + + +Using Module +------------ + +To use a module, first place the module directory under `modules` of the [application base directory](/doc/guide/basics.application#application-base-directory). Then declare the module ID in the [modules|CWebApplication::modules] property of the application. For example, in order to use the above `forum` module, we can use the following [application configuration](/doc/guide/basics.application#application-configuration): + +~~~ +[php] +return array( + ...... + 'modules'=>array('forum',...), + ...... +); +~~~ + +A module can also be configured with initial property values. The usage is very similar to configuring [application components](/doc/guide/basics.application#application-component). For example, the `forum` module may have a property named `postPerPage` in its module class which can be configured in the [application configuration](/doc/guide/basics.application#application-configuration) as follows: + +~~~ +[php] +return array( + ...... + 'modules'=>array( + 'forum'=>array( + 'postPerPage'=>20, + ), + ), + ...... +); +~~~ + +The module instance may be accessed via the [module|CController::module] property of the currently active controller. Through the module instance, we can then access information that are shared at the module level. For example, in order to access the above `postPerPage` information, we can use the following expression: + +~~~ +[php] +$postPerPage=Yii::app()->controller->module->postPerPage; +// or the following if $this refers to the controller instance +// $postPerPage=$this->module->postPerPage; +~~~ + +A controller action in a module can be accessed using the [route](/doc/guide/basics.controller#route) `moduleID/controllerID/actionID`. For example, assuming the above `forum` module has a controller named `PostController`, we can use the [route](/doc/guide/basics.controller#route) `forum/post/create` to refer to the `create` action in this controller. The corresponding URL for this route would be `http://www.example.com/index.php?r=forum/post/create`. + +> Tip: If a controller is in a sub-directory of `controllers`, we can still use the above [route](/doc/guide/basics.controller#route) format. For example, assuming `PostController` is under `forum/controllers/admin`, we can refer to the `create` action using `forum/admin/post/create`. + + +Nested Module +------------- + +Modules can be nested. That is, a module can contain another module. We call the former *parent module* while the latter *child module*. Child modules must be placed under the `modules` directory of the parent module. To access a controller action in a child module, we should use the route `parentModuleID/childModuleID/controllerID/actionID`. + + +
$Id$
\ No newline at end of file diff --git a/docs/guide/toc.txt b/docs/guide/toc.txt index f5e6e1c1d..8c2853f8c 100644 --- a/docs/guide/toc.txt +++ b/docs/guide/toc.txt @@ -12,6 +12,7 @@ - [Model](basics.model) - [View](basics.view) - [Component](basics.component) + - [Module](basics.module) - [Path Alias and Namespace](basics.namespace) - [Conventions](basics.convention) - [Development Workflow](basics.workflow) diff --git a/docs/guide/topics.auth.txt b/docs/guide/topics.auth.txt index b7f61137c..69a8ea000 100644 --- a/docs/guide/topics.auth.txt +++ b/docs/guide/topics.auth.txt @@ -498,4 +498,37 @@ if(Yii::app()->user->checkAccess('updateOwnPost',$params)) } ~~~ + +### Using Default Roles + +> Note: The default role feature has been available since version 1.0.3 + +Many Web applications need some very special roles that would be assigned to +every or most of the system users. For example, we may want to assign some +privileges to all authenticated users. It poses a lot of maintenance trouble +if we explicitly specify and store these role assignments. We can exploit +*default roles* to solve this problem. + +A default role is a role that is implicitly assigned to every user, including +both authenticated and guest. We do not need to explicitly assign it to a user. +When [CWebUser::checkAccess], default roles will be checked first as if they are +assigned to the user. + +Default roles must be declared in the [CAuthManager::defaultRoles] property. + +Because a default role is assigned to every user, it usually needs to be +associated with a business rule that determines whether the role +really applies to the user. For example, the following code defines two +roles, "authenticated" and "guest", which effectively apply to authenticated +users and guest users, respectively. + +~~~ +[php] +$bizRule='return !Yii::app()->user->isGuest;'; +$auth->createRole('authenticated'); + +$bizRule='return Yii::app()->user->isGuest;'; +$auth->createRole('guest',$bizRule); +~~~ +
$Id$
\ No newline at end of file diff --git a/docs/guide/topics.console.txt b/docs/guide/topics.console.txt index 094a9ea3c..d5492e593 100644 --- a/docs/guide/topics.console.txt +++ b/docs/guide/topics.console.txt @@ -88,7 +88,7 @@ will generate two files under the `protected` directory: `yiic` and specifically for the Web application. We can then create our own commands under the `protected/commands` -directory. Run the local `yiic` tool, we will see that our own commands +directory. Running the local `yiic` tool, we will see that our own commands appearing together with the standard ones. We can also create our own commands to be used when `yiic shell` is used. To do so, just drop our command class files under the `protected/commands/shell` directory. diff --git a/framework/base/CApplication.php b/framework/base/CApplication.php index 757f0e70d..5e6899428 100644 --- a/framework/base/CApplication.php +++ b/framework/base/CApplication.php @@ -449,10 +449,9 @@ abstract class CApplication extends CComponent */ public function setParams($value) { - if(is_array($value)) - $this->_params=new CAttributeCollection($value); - else - $this->_params=$value; + $params=$this->getParams(); + foreach($value as $k=>$v) + $params->add($k,$v); } /** @@ -759,20 +758,24 @@ abstract class CApplication extends CComponent * Please make sure you specify the {@link getBasePath basePath} property in the configuration, * which should point to the root directory containing all application logic, template and data. */ - protected function configure($config) + public function configure($config) { if(is_string($config)) $config=require($config); - if(isset($config['basePath'])) + + if($this->_basePath===null) { - $basePath=$config['basePath']; - unset($config['basePath']); + if(isset($config['basePath'])) + { + $basePath=$config['basePath']; + unset($config['basePath']); + } + else + $basePath='protected'; + $this->setBasePath($basePath); + Yii::setPathOfAlias('application',$this->getBasePath()); + Yii::setPathOfAlias('webroot',dirname($_SERVER['SCRIPT_FILENAME'])); } - else - $basePath='protected'; - $this->setBasePath($basePath); - Yii::setPathOfAlias('application',$this->getBasePath()); - Yii::setPathOfAlias('webroot',dirname($_SERVER['SCRIPT_FILENAME'])); if(is_array($config)) { diff --git a/framework/cli/views/shell/module/module.php b/framework/cli/views/shell/module/module.php index fca0b395e..0aee7df19 100644 --- a/framework/cli/views/shell/module/module.php +++ b/framework/cli/views/shell/module/module.php @@ -2,8 +2,9 @@ class extends CWebModule { - public function init() + public function init($config) { + parent::init($config); // this method is called when the module is being created // you may place code here to customize the module or the application } diff --git a/framework/cli/views/webapp/protected/views/layouts/main.php b/framework/cli/views/webapp/protected/views/layouts/main.php index aacd6b75f..6e25c0ebf 100644 --- a/framework/cli/views/webapp/protected/views/layouts/main.php +++ b/framework/cli/views/webapp/protected/views/layouts/main.php @@ -16,10 +16,10 @@ diff --git a/framework/i18n/CDateFormatter.php b/framework/i18n/CDateFormatter.php index babe84bc6..c7dfc22c5 100644 --- a/framework/i18n/CDateFormatter.php +++ b/framework/i18n/CDateFormatter.php @@ -84,7 +84,7 @@ class CDateFormatter extends CComponent { if(is_string($time)) $time=strtotime($time); - $date=CTimestamp::getDate($time,false,true); + $date=CTimestamp::getDate($time,false,false); $tokens=$this->parseFormat($pattern); foreach($tokens as &$token) { diff --git a/framework/web/CWebApplication.php b/framework/web/CWebApplication.php index 2b4634b77..5f431a23a 100644 --- a/framework/web/CWebApplication.php +++ b/framework/web/CWebApplication.php @@ -364,7 +364,7 @@ class CWebApplication extends CApplication { if($owner===null) $owner=$this; - if($route==='') + if(($route=trim($route,'/'))==='') $route=$owner->defaultController; $caseSensitive=$this->getUrlManager()->caseSensitive; @@ -654,9 +654,7 @@ class CWebApplication extends CApplication else $module=new $className($owner->getId().'/'.$id,$owner); Yii::setPathOfAlias($className,$module->getBasePath()); - foreach($config as $k=>$v) - $module->$k=$v; - $module->init(); + $module->init($config); return $module; } } diff --git a/framework/web/CWebModule.php b/framework/web/CWebModule.php index 62b90c18d..c79954868 100644 --- a/framework/web/CWebModule.php +++ b/framework/web/CWebModule.php @@ -27,6 +27,10 @@ class CWebModule extends CComponent * @var string the ID of the default controller for this module. Defaults to 'default'. */ public $defaultController='default'; + /** + * @var array the IDs of the module components that should be preloaded. + */ + public $preload=array(); /** * @var mixed the layout that is shared by the controllers inside this module. * If a controller has explicitly declared its own {@link CController::layout layout}, @@ -40,6 +44,12 @@ class CWebModule extends CComponent * Pleaser refer to {@link CWebApplication::controllerMap} for more details. */ public $controllerMap=array(); + /** + * @var array the behaviors that should be attached to the module. + * The behaviors will be attached to the application when {@link init} is called. + * Please refer to {@link CModel::behaviors} on how to specify the value of this property. + */ + public $behaviors=array(); private $_id; private $_parentModule; @@ -47,6 +57,8 @@ class CWebModule extends CComponent private $_basePath; private $_moduleConfig=array(); private $_modules=array(); + private $_components=array(); + private $_componentConfig=array(); /** * Constructor. @@ -59,13 +71,50 @@ class CWebModule extends CComponent $this->_parentModule=$parent; } + /** + * Getter magic method. + * This method is overridden to support accessing module components + * like reading module properties. + * @param string module component or property name + * @return mixed the named property value + */ + public function __get($name) + { + if($this->hasComponent($name)) + return $this->getComponent($name); + else + return parent::__get($name); + } + + /** + * Checks if a property value is null. + * This method overrides the parent implementation by checking + * if the named module component is loaded. + * @param string the property name or the event name + * @return boolean whether the property value is null + * @since 1.0.1 + */ + public function __isset($name) + { + if($this->hasComponent($name)) + return $this->getComponent($name)!==null; + else + return parent::__isset($name); + } + /** * Initializes this module. * This method is invoked automatically when the module is initially created. * You may override this method to customize the module or the application. + * Make sure you call the parent implementation so that the module gets configured. + * @param mixed the configuration array or a PHP script returning the configuration array. + * The configuration will be applied to this module. */ - public function init() + public function init($config) { + $this->configure($config); + $this->attachBehaviors($this->behaviors); + $this->preloadComponents(); } /** @@ -253,4 +302,125 @@ class CWebModule extends CComponent $this->_moduleConfig[$id]=$module; } } + + /** + * Loads static module components. + */ + protected function preloadComponents() + { + foreach($this->preload as $id) + $this->getComponent($id); + } + + /** + * @param string module component ID + * @return boolean whether the named module component exists (including both loaded and disabled.) + */ + public function hasComponent($id) + { + return isset($this->_components[$id]) || isset($this->_componentConfig[$id]); + } + + /** + * Retrieves the named module component. + * @param string application component ID (case-sensitive) + * @return IApplicationComponent the module component instance, null if the module component is disabled or does not exist. + * @see hasComponent + */ + public function getComponent($id) + { + if(isset($this->_components[$id])) + return $this->_components[$id]; + else if(isset($this->_componentConfig[$id])) + { + $config=$this->_componentConfig[$id]; + unset($this->_componentConfig[$id]); + if(!isset($config['enabled']) || $config['enabled']) + { + Yii::trace("Loading \"$id\" module component",'system.web.CWebModule'); + unset($config['enabled']); + $component=Yii::createComponent($config); + $component->init(); + return $this->_components[$id]=$component; + } + } + } + + /** + * Puts a component under the management of the module. + * The component will be initialized (by calling its {@link CApplicationComponent::init() init()} + * method if it has not done so. + * @param string component ID + * @param IApplicationComponent the component + */ + public function setComponent($id,$component) + { + $this->_components[$id]=$component; + if(!$component->getIsInitialized()) + $component->init(); + } + + /** + * @return array the currently loaded components (indexed by their IDs) + */ + public function getComponents() + { + return $this->_components; + } + + /** + * Sets the module components. + * + * When a configuration is used to specify a component, it should consist of + * the component's initial property values (name-value pairs). Additionally, + * a component can be enabled (default) or disabled by specifying the 'enabled' value + * in the configuration. + * + * If a configuration is specified with an ID that is the same as an existing + * component or configuration, the existing one will be replaced silently. + * + * The following is the configuration for two components: + *
+	 * array(
+	 *     'db'=>array(
+	 *         'class'=>'CDbConnection',
+	 *         'connectionString'=>'sqlite:path/to/file.db',
+	 *     ),
+	 *     'cache'=>array(
+	 *         'class'=>'CDbCache',
+	 *         'connectionID'=>'db',
+	 *         'enabled'=>!YII_DEBUG,  // enable caching in non-debug mode
+	 *     ),
+	 * )
+	 * 
+ * + * @param array module components(id=>component configuration or instances) + */ + public function setComponents($components) + { + foreach($components as $id=>$component) + { + if($component instanceof IApplicationComponent) + $this->setComponent($id,$component); + else if(isset($this->_componentConfig[$id])) + $this->_componentConfig[$id]=CMap::mergeArray($this->_componentConfig[$id],$component); + else + $this->_componentConfig[$id]=$component; + } + } + + /** + * Configures the module with the specified configuration. + * @param mixed the configuration array or a PHP script returning the configuration array. + */ + public function configure($config) + { + if(is_string($config)) + $config=require($config); + if(is_array($config)) + { + foreach($config as $key=>$value) + $this->$key=$value; + } + } } diff --git a/framework/web/auth/CAuthManager.php b/framework/web/auth/CAuthManager.php index 7f03ea05f..876cb4013 100644 --- a/framework/web/auth/CAuthManager.php +++ b/framework/web/auth/CAuthManager.php @@ -37,6 +37,19 @@ */ abstract class CAuthManager extends CApplicationComponent implements IAuthManager { + /** + * @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 {@link 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. + * @since 1.0.3 + */ + public $defaultRoles=array(); + /** * Creates a role. * This is a shortcut method to {@link IAuthManager::createAuthItem}. diff --git a/framework/web/auth/CDbAuthManager.php b/framework/web/auth/CDbAuthManager.php index 0a77f0738..191a15923 100644 --- a/framework/web/auth/CDbAuthManager.php +++ b/framework/web/auth/CDbAuthManager.php @@ -81,6 +81,8 @@ class CDbAuthManager extends CAuthManager */ public function checkAccess($itemName,$userId,$params=array()) { + if(!empty($this->defaultRoles) && $this->checkDefaultRoles($itemName,$userId,$params)) + return true; $sql="SELECT name, type, description, t1.bizrule, t1.data, t2.bizrule AS bizrule2, t2.data AS data2 FROM {$this->itemTable} t1, {$this->assignmentTable} t2 WHERE name=itemname AND userid=:userid"; $command=$this->db->createCommand($sql); $command->bindValue(':userid',$userId); @@ -98,6 +100,43 @@ class CDbAuthManager extends CAuthManager return false; } + /** + * Checks the access based on the default roles as declared in {@link defaultRoles}. + * @param string the name of the operation that need access check + * @param mixed the user ID. This should can be either an integer and a string representing + * the unique identifier of a user. See {@link IWebUser::getId}. + * @param array 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 according to the default roles. + * @since 1.0.3 + */ + protected function checkDefaultRoles($itemName,$userId,$params) + { + $names=array(); + foreach($this->defaultRoles as $role) + { + if(is_string($role)) + $names[]=$this->db->quoteValue($role); + else + $names[]=$role; + } + if(count($condition)<4) + $condition='name='.implode(' OR name=',$names); + else + $condition='name IN ('.implode(', ',$names).')'; + $sql="SELECT name, type, description, bizrule, data FROM {$this->itemTable} WHERE $condition"; + $command=$this->db->createCommand($sql); + $rows=$command->queryAll(); + + foreach($rows as $row) + { + $item=new CAuthItem($this,$row['name'],$row['type'],$row['description'],$row['bizrule'],unserialize($row['data'])); + if($item->checkAccess($itemName,$params)) + return true; + } + return false; + } + /** * Adds an item as a child of another item. * @param string the parent item name diff --git a/framework/web/auth/CPhpAuthManager.php b/framework/web/auth/CPhpAuthManager.php index 0e21f71af..b8328aafd 100644 --- a/framework/web/auth/CPhpAuthManager.php +++ b/framework/web/auth/CPhpAuthManager.php @@ -63,6 +63,8 @@ class CPhpAuthManager extends CAuthManager */ public function checkAccess($itemName,$userId,$params=array()) { + if(!empty($this->defaultRoles) && $this->checkDefaultRoles($itemName,$userId,$params)) + return true; foreach($this->getAuthAssignments($userId) as $assignment) { $item=$this->getAuthItem($assignment->getItemName()); @@ -72,6 +74,27 @@ class CPhpAuthManager extends CAuthManager return false; } + /** + * Checks the access based on the default roles as declared in {@link defaultRoles}. + * @param string the name of the operation that need access check + * @param mixed the user ID. This should can be either an integer and a string representing + * the unique identifier of a user. See {@link IWebUser::getId}. + * @param array 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 according to the default roles. + * @since 1.0.3 + */ + protected function checkDefaultRoles($itemName,$userId,$params) + { + foreach($this->defaultRoles as $role) + { + $item=$this->getAuthItem($role); + if($item!==null && $item->checkAccess($itemName,$params)) + return true; + } + return false; + } + /** * Adds an item as a child of another item. * @param string the parent item name diff --git a/framework/web/widgets/CTabView.php b/framework/web/widgets/CTabView.php index 6583051e2..42584a183 100644 --- a/framework/web/widgets/CTabView.php +++ b/framework/web/widgets/CTabView.php @@ -163,7 +163,7 @@ class CTabView extends CWidget $cs=Yii::app()->getClientScript(); if($url===null) $url=$cs->getCoreScriptUrl().'/yiitab/jquery.yiitab.css'; - $cs->registerCssFile($url); + $cs->registerCssFile($url,'screen'); } /**