diff --git a/framework/YiiBase.php b/framework/YiiBase.php index 16ae1ff..6e1e1a3 100644 --- a/framework/YiiBase.php +++ b/framework/YiiBase.php @@ -218,6 +218,7 @@ class YiiBase * - a URL (e.g. `http://www.yiiframework.com`) * - a path alias (e.g. `@yii/base`). In this case, the path alias will be converted into the * actual path first by calling [[getAlias]]. + * @throws \yii\base\Exception if $path is an invalid alias * @see getAlias */ public static function setAlias($alias, $path) diff --git a/framework/base/Application.php b/framework/base/Application.php index c93d414..c80fcaa 100644 --- a/framework/base/Application.php +++ b/framework/base/Application.php @@ -118,7 +118,9 @@ class Application extends Module \Yii::$application = $this; $this->id = $id; $this->setBasePath($basePath); - \Yii::setAlias('application', $this->getBasePath()); + \Yii::$aliases['@application'] = $this->getBasePath(); + \Yii::$aliases['@entry'] = dirname($_SERVER['SCRIPT_FILENAME']); + \Yii::$aliases['@www'] = ''; $this->registerCoreComponents(); } diff --git a/framework/base/Theme.php b/framework/base/Theme.php new file mode 100644 index 0000000..30a4623 --- /dev/null +++ b/framework/base/Theme.php @@ -0,0 +1,115 @@ + + * @since 2.0 + */ +class Theme extends ApplicationComponent +{ + private $_name; + private $_basePath; + private $_baseUrl; + + /** + * Constructor. + * @param string $name name of the theme + * @param string $basePath base theme path + * @param string $baseUrl base theme URL + */ + public function __construct($name, $basePath, $baseUrl) + { + $this->_name = $name; + $this->_baseUrl = $baseUrl; + $this->_basePath = $basePath; + } + + /** + * @return string theme name + */ + public function getName() + { + return $this->_name; + } + + /** + * @return string the relative URL to the theme folder (without ending slash) + */ + public function getBaseUrl() + { + return $this->_baseUrl; + } + + /** + * @return string the file path to the theme folder + */ + public function getBasePath() + { + return $this->_basePath; + } + + /** + * @return string the path for controller views. Defaults to 'ThemeRoot/views'. + */ + public function getViewPath() + { + return $this->_basePath . DIRECTORY_SEPARATOR . 'views'; + } + + /** + * Finds the view file for the specified controller's view. + * @param CController $controller the controller + * @param string $viewName the view name + * @return string the view file path. False if the file does not exist. + */ + public function getViewFile($controller, $viewName) + { + $moduleViewPath = $this->getViewPath(); + if (($module = $controller->getModule()) !== null) + { + $moduleViewPath .= '/' . $module->getId(); + } + return $controller->resolveViewFile($viewName, $this->getViewPath() . '/' . $controller->getUniqueId(), $this->getViewPath(), $moduleViewPath); + } + + /** + * Finds the layout file for the specified controller's layout. + * @param CController $controller the controller + * @param string $layoutName the layout name + * @return string the layout file path. False if the file does not exist. + */ + public function getLayoutFile($controller, $layoutName) + { + $moduleViewPath = $basePath = $this->getViewPath(); + $module = $controller->getModule(); + if (empty($layoutName)) { + while ($module !== null) { + if ($module->layout === false) + return false; + if (!empty($module->layout)) + break; + $module = $module->getParentModule(); + } + if ($module === null) + $layoutName = Yii::app()->layout; + else { + $layoutName = $module->layout; + $moduleViewPath .= '/' . $module->getId(); + } + } + else if ($module !== null) + $moduleViewPath .= '/' . $module->getId(); + + return $controller->resolveViewFile($layoutName, $moduleViewPath . '/layouts', $basePath, $moduleViewPath); + } +} diff --git a/framework/base/ThemeManager.php b/framework/base/ThemeManager.php new file mode 100644 index 0000000..2259749 --- /dev/null +++ b/framework/base/ThemeManager.php @@ -0,0 +1,94 @@ + + * @since 2.0 + */ +class ThemeManager extends ApplicationComponent +{ + /** + * default themes base path + */ + const DEFAULT_BASEPATH = 'themes'; + + /** + * @var string the name of the theme class for representing a theme. + * Defaults to {@link Theme}. This can also be a class name in dot syntax. + */ + public $themeClass = 'Theme'; + /** + * @var string the base path containing all themes. Defaults to '@entry/themes'. + */ + public $basePath = '@entry/themes'; + /** + * @var string the base URL for all themes. Defaults to "@www/themes". + */ + public $baseUrl = '@www/themes'; + + + /** + * @param string $name name of the theme to be retrieved + * @return Theme the theme retrieved. Null if the theme does not exist. + */ + public function getTheme($name) + { + $themePath = $this->getBasePath() . DIRECTORY_SEPARATOR . $name; + if (is_dir($themePath)) { + $class = Yii::import($this->themeClass, true); + return new $class($name, $themePath, $this->getBaseUrl() . '/' . $name); + } else { + return null; + } + } + + /** + * @return array list of available theme names + */ + public function getThemeNames() + { + static $themes; + if ($themes === null) { + $themes = array(); + $basePath = $this->getBasePath(); + $folder = @opendir($basePath); + while (($file = @readdir($folder)) !== false) { + if ($file !== '.' && $file !== '..' && $file !== '.svn' && $file !== '.gitignore' && is_dir($basePath . DIRECTORY_SEPARATOR . $file)) { + $themes[] = $file; + } + } + closedir($folder); + sort($themes); + } + return $themes; + } +} diff --git a/framework/base/View.php b/framework/base/View.php index e60d9cc..c1b4bf6 100644 --- a/framework/base/View.php +++ b/framework/base/View.php @@ -10,7 +10,6 @@ namespace yii\base; use yii\util\FileHelper; -use yii\util\ArrayHelper; /** * @author Qiang Xue @@ -47,7 +46,7 @@ class View extends Component /** * Renders a view. * - * The method first identifies the actual view file corresponding to the specified view. + * The method first finds the actual view file corresponding to the specified view. * It then calls [[renderFile()]] to render the view file. The rendering result is returned * as a string. If the view file does not exist, an exception will be thrown. * @@ -58,36 +57,57 @@ class View extends Component * or a path relative to [[basePath]]. The file suffix is optional and defaults to `.php` if not given * in the view name. * - * @param string $view the view to be rendered. This can be a path alias or a path relative to [[basePath]]. + * @param string $view the view to be rendered. This can be either a path alias or a path relative to [[basePath]]. * @param array $params the parameters that should be made available in the view. The PHP function `extract()` * will be called on this variable to extract the variables from this parameter. * @return string the rendering result - * @throws Exception if the view file cannot be found + * @throws Exception if the view file cannot be found */ public function render($view, $params = array()) { $file = $this->findViewFile($view); if ($file !== false) { - $this->renderFile($file, $params); + return $this->renderFile($file, $params); } else { throw new Exception("Unable to find the view file for view '$view'."); } } + /** + * Renders a view file. + * @param string $file the view file path + * @param array $params the parameters to be extracted and made available in the view file + * @return string the rendering result + */ public function renderFile($file, $params = array()) { - $this->renderFileInternal($file, $params); + return $this->renderFileInternal($file, $params); + } + + public function createWidget($class, $properties = array()) + { + $properties['class'] = $class; + return \Yii::createObject($properties, $this->owner); } public function widget($class, $properties = array()) { $widget = $this->createWidget($class, $properties); - $widget->run(); + echo $widget->run(); return $widget; } + /** + * @var Widget[] the widgets that are currently not ended + */ private $_widgetStack = array(); + /** + * Begins a widget. + * @param string $class the widget class + * @param array $properties the initial property values of the widget + * @return Widget the widget instance + */ public function beginWidget($class, $properties = array()) { $widget = $this->createWidget($class, $properties); @@ -95,43 +115,34 @@ class View extends Component return $widget; } + /** + * Ends a widget. + * Note that the rendering result of the widget is directly echoed out. + * If you want to capture the rendering result of a widget, you may use + * [[createWidget()]] and [[Widget::run()]]. + * @return Widget the widget instance + * @throws Exception if [[beginWidget()]] and [[endWidget()]] calls are not properly nested + */ public function endWidget() { if (($widget = array_pop($this->_widgetStack)) !== null) { - $widget->run(); + echo $widget->run(); return $widget; } else { throw new Exception("Unmatched beginWidget() and endWidget() calls."); } } - public function createWidget($class, $properties = array()) - { - $properties['class'] = $class; - - // todo: widget skin should be something global, similar to theme - if ($this->enableSkin) { - if ($this->skinnableWidgets === null || in_array($class, $this->skinnableWidgets)) { - $skinName = isset($properties['skin']) ? $properties['skin'] : 'default'; - if ($skinName !== false && ($skin = $this->getSkin($class, $skinName)) !== array()) { - $properties = $properties === array() ? $skin : ArrayHelper::merge($skin, $properties); - } - } - } - - return \Yii::createObject($properties, $this->owner); - } - /** * Begins recording a clip. - * This method is a shortcut to beginning [[yii\web\widgets\ClipWidget]] + * This method is a shortcut to beginning [[yii\widgets\Clip]] * @param string $id the clip ID. - * @param array $properties initial property values for [[yii\web\widgets\ClipWidget]] + * @param array $properties initial property values for [[yii\widgets\Clip]] */ public function beginClip($id, $properties = array()) { $properties['id'] = $id; - $this->beginWidget('yii\web\widgets\ClipWidget', $properties); + $this->beginWidget('yii\widgets\Clip', $properties); } /** @@ -158,14 +169,14 @@ class View extends Component * ~~~ * * @param string $id a unique ID identifying the fragment to be cached. - * @param array $properties initial property values for [[yii\web\widgets\OutputCache]] + * @param array $properties initial property values for [[yii\widgets\OutputCache]] * @return boolean whether we need to generate content for caching. False if cached version is available. * @see endCache */ public function beginCache($id, $properties = array()) { $properties['id'] = $id; - $cache = $this->beginWidget('yii\web\widgets\OutputCache', $properties); + $cache = $this->beginWidget('yii\widgets\OutputCache', $properties); if ($cache->getIsContentCached()) { $this->endCache(); return false; @@ -195,11 +206,11 @@ class View extends Component * {@link CWebModule::layout default layout}. * @param array $params the variables (name=>value) to be extracted and made available in the decorative view. * @see endContent - * @see yii\web\widgets\ContentDecorator + * @see yii\widgets\ContentDecorator */ - public function beginContent($view = null, $params = array()) + public function beginContent($view, $params = array()) { - $this->beginWidget('yii\web\widgets\ContentDecorator', array( + $this->beginWidget('yii\widgets\ContentDecorator', array( 'view' => $view, 'params' => $params, )); @@ -214,17 +225,32 @@ class View extends Component $this->endWidget(); } + /** + * Renders a view file. + * This method will extract the given parameters and include the view file. + * It captures the output of the included view file and returns it as a string. + * @param string $_file_ the view file. + * @param array $_params_ the parameters (name-value pairs) that will be extracted and made available in the view file. + * @return string the rendering result + */ protected function renderFileInternal($_file_, $_params_ = array()) { + ob_start(); + ob_implicit_flush(false); extract($_params_, EXTR_OVERWRITE); require($_file_); + return ob_get_clean(); } + /** + * Finds the view file based on the given view name. + * @param string $view the view name or path alias. If the view name does not specify + * the view file extension name, it will use `.php` as the extension name. + * @return string|boolean the view file if it exists. False if the view file cannot be found. + */ public function findViewFile($view) { - if ($view[0] === '/') { - throw new Exception('The view name "$view" should not start with a slash "/".'); - } + $view = ltrim($view, '/'); if (($extension = FileHelper::getExtension($view)) === '') { $view .= '.php'; diff --git a/framework/base/Widget.php b/framework/base/Widget.php new file mode 100644 index 0000000..4f1a92e --- /dev/null +++ b/framework/base/Widget.php @@ -0,0 +1,122 @@ + + * @since 2.0 + */ +class Widget extends Component implements Initable +{ + /** + * @var mixed the name of the skin to be used by this widget. Defaults to 'default'. + * If this is set as false, no skin will be applied to this widget. + */ + public $skin = 'default'; + /** + * @var Widget|Controller the owner/creator of this widget. It could be either a widget or a controller. + */ + public $owner; + /** + * @var string id of the widget. + */ + private $_id; + /** + * @var integer a counter used to generate IDs for widgets. + */ + private static $_counter = 0; + + /** + * Constructor. + * @param Widget|Controller $owner owner/creator of this widget. + */ + public function __construct($owner) + { + $this->owner = $owner; + } + + /** + * Returns the ID of the widget. + * @param boolean $autoGenerate whether to generate an ID if it is not set previously + * @return string ID of the widget. + */ + public function getId($autoGenerate = true) + { + if ($autoGenerate && $this->_id === null) { + $this->_id = 'yw' . self::$_counter++; + } + return $this->_id; + } + + /** + * Sets the ID of the widget. + * @param string $value id of the widget. + */ + public function setId($value) + { + $this->_id = $value; + } + + /** + * Initializes the widget. + */ + public function init() + { + } + + /** + * Executes the widget. + * @return string the rendering result of the widget + */ + public function run() + { + } + + /** + * Renders a view. + * + * The method first finds the actual view file corresponding to the specified view. + * It then calls [[renderFile()]] to render the view file. The rendering result is returned + * as a string. If the view file does not exist, an exception will be thrown. + * + * To determine which view file should be rendered, the method calls [[findViewFile()]] which + * will search in the directories as specified by [[basePath]]. + * + * View name can be a path alias representing an absolute file path (e.g. `@app/views/layout/index`), + * or a path relative to [[basePath]]. The file suffix is optional and defaults to `.php` if not given + * in the view name. + * + * @param string $view the view to be rendered. This can be either a path alias or a path relative to [[basePath]]. + * @param array $params the parameters that should be made available in the view. The PHP function `extract()` + * will be called on this variable to extract the variables from this parameter. + * @return string the rendering result + * @throws Exception if the view file cannot be found + */ + public function render($view, $params = array()) + { + return $this->createView()->render($view, $params); + } + + /** + * @return View + */ + public function createView() + { + $view = new View; + if (($theme = \Yii::$application->getTheme()) !== null) { + $view->basePath[] = $theme->getViewPath() . DIRECTORY_SEPARATOR . str_replace('\\', '_', get_class($this)); + } + $class = new \ReflectionClass($this); + $view->basePath[] = dirname($class->getFileName()) . DIRECTORY_SEPARATOR . 'views'; + return $view; + } +} \ No newline at end of file diff --git a/framework/db/dao/Command.php b/framework/db/dao/Command.php index 41601ce..769fde7 100644 --- a/framework/db/dao/Command.php +++ b/framework/db/dao/Command.php @@ -103,6 +103,7 @@ class Command extends \yii\base\Component * this may improve performance. * For SQL statement with binding parameters, this method is invoked * automatically. + * @throws Exception if there is any DB error */ public function prepare() { @@ -221,7 +222,7 @@ class Command extends \yii\base\Component } \Yii::trace("Executing SQL: {$sql}{$paramLog}", __CLASS__); -//echo $sql . "\n\n"; + try { if ($this->connection->enableProfiling) { \Yii::beginProfile(__METHOD__ . "($sql)", __CLASS__); diff --git a/framework/web/Application.php b/framework/web/Application.php new file mode 100644 index 0000000..10bd6ce --- /dev/null +++ b/framework/web/Application.php @@ -0,0 +1,34 @@ + + * @since 2.0 + */ +class Application extends \yii\base\Application +{ + /** + * Processes the request. + * @return integer the exit status of the controller action (0 means normal, non-zero values mean abnormal) + */ + public function processRequest() + { + $route = $this->resolveRequest(); + return $this->runController($route, null); + } + + protected function resolveRequest() + { + return array(); + } +} diff --git a/framework/web/AssetManager.php b/framework/web/AssetManager.php new file mode 100644 index 0000000..452317a --- /dev/null +++ b/framework/web/AssetManager.php @@ -0,0 +1,304 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + + +/** + * CAssetManager is a Web application component that manages private files (called assets) and makes them accessible by Web clients. + * + * It achieves this goal by copying assets to a Web-accessible directory + * and returns the corresponding URL for accessing them. + * + * To publish an asset, simply call {@link publish()}. + * + * The Web-accessible directory holding the published files is specified + * by {@link setBasePath basePath}, which defaults to the "assets" directory + * under the directory containing the application entry script file. + * The property {@link setBaseUrl baseUrl} refers to the URL for accessing + * the {@link setBasePath basePath}. + * + * @property string $basePath The root directory storing the published asset files. Defaults to 'WebRoot/assets'. + * @property string $baseUrl The base url that the published asset files can be accessed. + * Note, the ending slashes are stripped off. Defaults to '/AppBaseUrl/assets'. + * + * @author Qiang Xue + * @version $Id$ + * @package system.web + * @since 1.0 + */ +class CAssetManager extends CApplicationComponent +{ + /** + * Default web accessible base path for storing private files + */ + const DEFAULT_BASEPATH='assets'; + /** + * @var boolean whether to use symbolic link to publish asset files. Defaults to false, meaning + * asset files are copied to public folders. Using symbolic links has the benefit that the published + * assets will always be consistent with the source assets. This is especially useful during development. + * + * However, there are special requirements for hosting environments in order to use symbolic links. + * In particular, symbolic links are supported only on Linux/Unix, and Windows Vista/2008 or greater. + * The latter requires PHP 5.3 or greater. + * + * Moreover, some Web servers need to be properly configured so that the linked assets are accessible + * to Web users. For example, for Apache Web server, the following configuration directive should be added + * for the Web folder: + *
+	 * Options FollowSymLinks
+	 * 
+ * + * @since 1.1.5 + */ + public $linkAssets=false; + /** + * @var array list of directories and files which should be excluded from the publishing process. + * Defaults to exclude '.svn' and '.gitignore' files only. This option has no effect if {@link linkAssets} is enabled. + * @since 1.1.6 + **/ + public $excludeFiles=array('.svn','.gitignore'); + /** + * @var integer the permission to be set for newly generated asset files. + * This value will be used by PHP chmod function. + * Defaults to 0666, meaning the file is read-writable by all users. + * @since 1.1.8 + */ + public $newFileMode=0666; + /** + * @var integer the permission to be set for newly generated asset directories. + * This value will be used by PHP chmod function. + * Defaults to 0777, meaning the directory can be read, written and executed by all users. + * @since 1.1.8 + */ + public $newDirMode=0777; + /** + * @var string base web accessible path for storing private files + */ + private $_basePath; + /** + * @var string base URL for accessing the publishing directory. + */ + private $_baseUrl; + /** + * @var array published assets + */ + private $_published=array(); + + /** + * @return string the root directory storing the published asset files. Defaults to 'WebRoot/assets'. + */ + public function getBasePath() + { + if($this->_basePath===null) + { + $request=Yii::app()->getRequest(); + $this->setBasePath(dirname($request->getScriptFile()).DIRECTORY_SEPARATOR.self::DEFAULT_BASEPATH); + } + return $this->_basePath; + } + + /** + * Sets the root directory storing published asset files. + * @param string $value the root directory storing published asset files + * @throws CException if the base path is invalid + */ + public function setBasePath($value) + { + if(($basePath=realpath($value))!==false && is_dir($basePath) && is_writable($basePath)) + $this->_basePath=$basePath; + else + throw new CException(Yii::t('yii','CAssetManager.basePath "{path}" is invalid. Please make sure the directory exists and is writable by the Web server process.', + array('{path}'=>$value))); + } + + /** + * @return string the base url that the published asset files can be accessed. + * Note, the ending slashes are stripped off. Defaults to '/AppBaseUrl/assets'. + */ + public function getBaseUrl() + { + if($this->_baseUrl===null) + { + $request=Yii::app()->getRequest(); + $this->setBaseUrl($request->getBaseUrl().'/'.self::DEFAULT_BASEPATH); + } + return $this->_baseUrl; + } + + /** + * @param string $value the base url that the published asset files can be accessed + */ + public function setBaseUrl($value) + { + $this->_baseUrl=rtrim($value,'/'); + } + + /** + * Publishes a file or a directory. + * This method will copy the specified asset to a web accessible directory + * and return the URL for accessing the published asset. + * + * + * Note: On rare scenario, a race condition can develop that will lead to a + * one-time-manifestation of a non-critical problem in the creation of the directory + * that holds the published assets. This problem can be avoided altogether by 'requesting' + * in advance all the resources that are supposed to trigger a 'publish()' call, and doing + * that in the application deployment phase, before system goes live. See more in the following + * discussion: http://code.google.com/p/yii/issues/detail?id=2579 + * + * @param string $path the asset (file or directory) to be published + * @param boolean $hashByName whether the published directory should be named as the hashed basename. + * If false, the name will be the hash taken from dirname of the path being published and path mtime. + * Defaults to false. Set true if the path being published is shared among + * different extensions. + * @param integer $level level of recursive copying when the asset is a directory. + * Level -1 means publishing all subdirectories and files; + * Level 0 means publishing only the files DIRECTLY under the directory; + * level N means copying those directories that are within N levels. + * @param boolean $forceCopy whether we should copy the asset file or directory even if it is already published before. + * This parameter is set true mainly during development stage when the original + * assets are being constantly changed. The consequence is that the performance + * is degraded, which is not a concern during development, however. + * This parameter has been available since version 1.1.2. + * @return string an absolute URL to the published asset + * @throws CException if the asset to be published does not exist. + */ + public function publish($path,$hashByName=false,$level=-1,$forceCopy=false) + { + if(isset($this->_published[$path])) + return $this->_published[$path]; + else if(($src=realpath($path))!==false) + { + if(is_file($src)) + { + $dir=$this->hash($hashByName ? basename($src) : dirname($src).filemtime($src)); + $fileName=basename($src); + $dstDir=$this->getBasePath().DIRECTORY_SEPARATOR.$dir; + $dstFile=$dstDir.DIRECTORY_SEPARATOR.$fileName; + + if($this->linkAssets) + { + if(!is_file($dstFile)) + { + if(!is_dir($dstDir)) + { + mkdir($dstDir); + @chmod($dstDir, $this->newDirMode); + } + symlink($src,$dstFile); + } + } + else if(@filemtime($dstFile)<@filemtime($src)) + { + if(!is_dir($dstDir)) + { + mkdir($dstDir); + @chmod($dstDir, $this->newDirMode); + } + copy($src,$dstFile); + @chmod($dstFile, $this->newFileMode); + } + + return $this->_published[$path]=$this->getBaseUrl()."/$dir/$fileName"; + } + else if(is_dir($src)) + { + $dir=$this->hash($hashByName ? basename($src) : $src.filemtime($src)); + $dstDir=$this->getBasePath().DIRECTORY_SEPARATOR.$dir; + + if($this->linkAssets) + { + if(!is_dir($dstDir)) + symlink($src,$dstDir); + } + else if(!is_dir($dstDir) || $forceCopy) + { + CFileHelper::copyDirectory($src,$dstDir,array( + 'exclude'=>$this->excludeFiles, + 'level'=>$level, + 'newDirMode'=>$this->newDirMode, + 'newFileMode'=>$this->newFileMode, + )); + } + + return $this->_published[$path]=$this->getBaseUrl().'/'.$dir; + } + } + throw new CException(Yii::t('yii','The asset "{asset}" to be published does not exist.', + array('{asset}'=>$path))); + } + + /** + * Returns the published path of a file path. + * This method does not perform any publishing. It merely tells you + * if the file or directory is published, where it will go. + * @param string $path directory or file path being published + * @param boolean $hashByName whether the published directory should be named as the hashed basename. + * If false, the name will be the hash taken from dirname of the path being published and path mtime. + * Defaults to false. Set true if the path being published is shared among + * different extensions. + * @return string the published file path. False if the file or directory does not exist + */ + public function getPublishedPath($path,$hashByName=false) + { + if(($path=realpath($path))!==false) + { + $base=$this->getBasePath().DIRECTORY_SEPARATOR; + if(is_file($path)) + return $base . $this->hash($hashByName ? basename($path) : dirname($path).filemtime($path)) . DIRECTORY_SEPARATOR . basename($path); + else + return $base . $this->hash($hashByName ? basename($path) : $path.filemtime($path)); + } + else + return false; + } + + /** + * Returns the URL of a published file path. + * This method does not perform any publishing. It merely tells you + * if the file path is published, what the URL will be to access it. + * @param string $path directory or file path being published + * @param boolean $hashByName whether the published directory should be named as the hashed basename. + * If false, the name will be the hash taken from dirname of the path being published and path mtime. + * Defaults to false. Set true if the path being published is shared among + * different extensions. + * @return string the published URL for the file or directory. False if the file or directory does not exist. + */ + public function getPublishedUrl($path,$hashByName=false) + { + if(isset($this->_published[$path])) + return $this->_published[$path]; + if(($path=realpath($path))!==false) + { + if(is_file($path)) + return $this->getBaseUrl().'/'.$this->hash($hashByName ? basename($path) : dirname($path).filemtime($path)).'/'.basename($path); + else + return $this->getBaseUrl().'/'.$this->hash($hashByName ? basename($path) : $path.filemtime($path)); + } + else + return false; + } + + /** + * Generate a CRC32 hash for the directory path. Collisions are higher + * than MD5 but generates a much smaller hash string. + * @param string $path string to be hashed. + * @return string hashed string. + */ + protected function hash($path) + { + return sprintf('%x',crc32($path.Yii::getVersion())); + } +} diff --git a/framework/web/Controller.php b/framework/web/Controller.php new file mode 100644 index 0000000..a6fb782 --- /dev/null +++ b/framework/web/Controller.php @@ -0,0 +1,48 @@ + + * @since 2.0 + */ +class Controller extends \yii\base\Controller +{ + /** + * Returns the request parameters that will be used for action parameter binding. + * Default implementation simply returns an empty array. + * Child classes may override this method to customize the parameters to be provided + * for action parameter binding (e.g. `$_GET`). + * @return array the request parameters (name-value pairs) to be used for action parameter binding + */ + public function getActionParams() + { + return $_GET; + } + + /** + * This method is invoked when the request parameters do not satisfy the requirement of the specified action. + * The default implementation will throw an exception. + * @param Action $action the action being executed + * @param Exception $exception the exception about the invalid parameters + * @throws HttpException $exception a 400 HTTP exception + */ + public function invalidActionParams($action, $exception) + { + throw new HttpException(400, \Yii::t('yii', 'Your request is invalid.')); + } +} \ No newline at end of file diff --git a/framework/web/Cookie.php b/framework/web/Cookie.php new file mode 100644 index 0000000..abb0e00 --- /dev/null +++ b/framework/web/Cookie.php @@ -0,0 +1,63 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * A CHttpCookie instance stores a single cookie, including the cookie name, value, domain, path, expire, and secure. + * + * @author Qiang Xue + * @version $Id$ + * @package system.web + * @since 1.0 + */ +class CHttpCookie extends CComponent +{ + /** + * @var string name of the cookie + */ + public $name; + /** + * @var string value of the cookie + */ + public $value=''; + /** + * @var string domain of the cookie + */ + public $domain=''; + /** + * @var integer the timestamp at which the cookie expires. This is the server timestamp. Defaults to 0, meaning "until the browser is closed". + */ + public $expire=0; + /** + * @var string the path on the server in which the cookie will be available on. The default is '/'. + */ + public $path='/'; + /** + * @var boolean whether cookie should be sent via secure connection + */ + public $secure=false; + /** + * @var boolean whether the cookie should be accessible only through the HTTP protocol. + * By setting this property to true, the cookie will not be accessible by scripting languages, + * such as JavaScript, which can effectly help to reduce identity theft through XSS attacks. + * Note, this property is only effective for PHP 5.2.0 or above. + */ + public $httpOnly=false; + + /** + * Constructor. + * @param string $name name of this cookie + * @param string $value value of this cookie + */ + public function __construct($name,$value) + { + $this->name=$name; + $this->value=$value; + } +} diff --git a/framework/web/Pagination.php b/framework/web/Pagination.php new file mode 100644 index 0000000..7a05b73 --- /dev/null +++ b/framework/web/Pagination.php @@ -0,0 +1,241 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CPagination represents information relevant to pagination. + * + * When data needs to be rendered in multiple pages, we can use CPagination to + * represent information such as {@link getItemCount total item count}, + * {@link getPageSize page size}, {@link getCurrentPage current page}, etc. + * These information can be passed to {@link CBasePager pagers} to render + * pagination buttons or links. + * + * Example: + * + * Controller action: + *
+ * function actionIndex(){
+ *     $criteria=new CDbCriteria();
+ *     $count=Article::model()->count($criteria);
+ *     $pages=new CPagination($count);
+ *
+ *     // results per page
+ *     $pages->pageSize=10;
+ *     $pages->applyLimit($criteria);
+ *     $models=Article::model()->findAll($criteria);
+ *
+ *     $this->render('index', array(
+ *     'models' => $models,
+ *          'pages' => $pages
+ *     ));
+ * }
+ * 
+ * + * View: + *
+ * 
+ *     // display a model
+ * 
+ *
+ * // display pagination
+ * widget('CLinkPager', array(
+ *     'pages' => $pages,
+ * )) ?>
+ * 
+ * + * @property integer $pageSize Number of items in each page. Defaults to 10. + * @property integer $itemCount Total number of items. Defaults to 0. + * @property integer $pageCount Number of pages. + * @property integer $currentPage The zero-based index of the current page. Defaults to 0. + * @property integer $offset The offset of the data. This may be used to set the + * OFFSET value for a SQL statement for fetching the current page of data. + * @property integer $limit The limit of the data. This may be used to set the + * LIMIT value for a SQL statement for fetching the current page of data. + * This returns the same value as {@link pageSize}. + * + * @author Qiang Xue + * @version $Id$ + * @package system.web + * @since 1.0 + */ +class CPagination extends CComponent +{ + /** + * The default page size. + */ + const DEFAULT_PAGE_SIZE=10; + /** + * @var string name of the GET variable storing the current page index. Defaults to 'page'. + */ + public $pageVar='page'; + /** + * @var string the route (controller ID and action ID) for displaying the paged contents. + * Defaults to empty string, meaning using the current route. + */ + public $route=''; + /** + * @var array of parameters (name=>value) that should be used instead of GET when generating pagination URLs. + * Defaults to null, meaning using the currently available GET parameters. + */ + public $params; + /** + * @var boolean whether to ensure {@link currentPage} is returning a valid page number. + * When this property is true, the value returned by {@link currentPage} will always be between + * 0 and ({@link pageCount}-1). Because {@link pageCount} relies on the correct value of {@link itemCount}, + * it means you must have knowledge about the total number of data items when you want to access {@link currentPage}. + * This is fine for SQL-based queries, but may not be feasible for other kinds of queries (e.g. MongoDB). + * In those cases, you may set this property to be false to skip the validation (you may need to validate yourself then). + * Defaults to true. + * @since 1.1.4 + */ + public $validateCurrentPage=true; + + private $_pageSize=self::DEFAULT_PAGE_SIZE; + private $_itemCount=0; + private $_currentPage; + + /** + * Constructor. + * @param integer $itemCount total number of items. + */ + public function __construct($itemCount=0) + { + $this->setItemCount($itemCount); + } + + /** + * @return integer number of items in each page. Defaults to 10. + */ + public function getPageSize() + { + return $this->_pageSize; + } + + /** + * @param integer $value number of items in each page + */ + public function setPageSize($value) + { + if(($this->_pageSize=$value)<=0) + $this->_pageSize=self::DEFAULT_PAGE_SIZE; + } + + /** + * @return integer total number of items. Defaults to 0. + */ + public function getItemCount() + { + return $this->_itemCount; + } + + /** + * @param integer $value total number of items. + */ + public function setItemCount($value) + { + if(($this->_itemCount=$value)<0) + $this->_itemCount=0; + } + + /** + * @return integer number of pages + */ + public function getPageCount() + { + return (int)(($this->_itemCount+$this->_pageSize-1)/$this->_pageSize); + } + + /** + * @param boolean $recalculate whether to recalculate the current page based on the page size and item count. + * @return integer the zero-based index of the current page. Defaults to 0. + */ + public function getCurrentPage($recalculate=true) + { + if($this->_currentPage===null || $recalculate) + { + if(isset($_GET[$this->pageVar])) + { + $this->_currentPage=(int)$_GET[$this->pageVar]-1; + if($this->validateCurrentPage) + { + $pageCount=$this->getPageCount(); + if($this->_currentPage>=$pageCount) + $this->_currentPage=$pageCount-1; + } + if($this->_currentPage<0) + $this->_currentPage=0; + } + else + $this->_currentPage=0; + } + return $this->_currentPage; + } + + /** + * @param integer $value the zero-based index of the current page. + */ + public function setCurrentPage($value) + { + $this->_currentPage=$value; + $_GET[$this->pageVar]=$value+1; + } + + /** + * Creates the URL suitable for pagination. + * This method is mainly called by pagers when creating URLs used to + * perform pagination. The default implementation is to call + * the controller's createUrl method with the page information. + * You may override this method if your URL scheme is not the same as + * the one supported by the controller's createUrl method. + * @param CController $controller the controller that will create the actual URL + * @param integer $page the page that the URL should point to. This is a zero-based index. + * @return string the created URL + */ + public function createPageUrl($controller,$page) + { + $params=$this->params===null ? $_GET : $this->params; + if($page>0) // page 0 is the default + $params[$this->pageVar]=$page+1; + else + unset($params[$this->pageVar]); + return $controller->createUrl($this->route,$params); + } + + /** + * Applies LIMIT and OFFSET to the specified query criteria. + * @param CDbCriteria $criteria the query criteria that should be applied with the limit + */ + public function applyLimit($criteria) + { + $criteria->limit=$this->getLimit(); + $criteria->offset=$this->getOffset(); + } + + /** + * @return integer the offset of the data. This may be used to set the + * OFFSET value for a SQL statement for fetching the current page of data. + * @since 1.1.0 + */ + public function getOffset() + { + return $this->getCurrentPage()*$this->getPageSize(); + } + + /** + * @return integer the limit of the data. This may be used to set the + * LIMIT value for a SQL statement for fetching the current page of data. + * This returns the same value as {@link pageSize}. + * @since 1.1.0 + */ + public function getLimit() + { + return $this->getPageSize(); + } +} \ No newline at end of file diff --git a/framework/web/Request.php b/framework/web/Request.php new file mode 100644 index 0000000..a03984e --- /dev/null +++ b/framework/web/Request.php @@ -0,0 +1,1111 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + + +/** + * CHttpRequest encapsulates the $_SERVER variable and resolves its inconsistency among different Web servers. + * + * CHttpRequest also manages the cookies sent from and sent to the user. + * By setting {@link enableCookieValidation} to true, + * cookies sent from the user will be validated to see if they are tampered. + * The property {@link getCookies cookies} returns the collection of cookies. + * For more details, see {@link CCookieCollection}. + * + * CHttpRequest is a default application component loaded by {@link CWebApplication}. It can be + * accessed via {@link CWebApplication::getRequest()}. + * + * @property string $url Part of the request URL after the host info. + * @property string $hostInfo Schema and hostname part (with port number if needed) of the request URL (e.g. http://www.yiiframework.com). + * @property string $baseUrl The relative URL for the application. + * @property string $scriptUrl The relative URL of the entry script. + * @property string $pathInfo Part of the request URL that is after the entry script and before the question mark. + * Note, the returned pathinfo is decoded starting from 1.1.4. + * Prior to 1.1.4, whether it is decoded or not depends on the server configuration + * (in most cases it is not decoded). + * @property string $requestUri The request URI portion for the currently requested URL. + * @property string $queryString Part of the request URL that is after the question mark. + * @property boolean $isSecureConnection If the request is sent via secure channel (https). + * @property string $requestType Request type, such as GET, POST, HEAD, PUT, DELETE. + * @property boolean $isPostRequest Whether this is a POST request. + * @property boolean $isDeleteRequest Whether this is a DELETE request. + * @property boolean $isPutRequest Whether this is a PUT request. + * @property boolean $isAjaxRequest Whether this is an AJAX (XMLHttpRequest) request. + * @property boolean $isFlashRequest Whether this is an Adobe Flash or Adobe Flex request. + * @property string $serverName Server name. + * @property integer $serverPort Server port number. + * @property string $urlReferrer URL referrer, null if not present. + * @property string $userAgent User agent, null if not present. + * @property string $userHostAddress User IP address. + * @property string $userHost User host name, null if cannot be determined. + * @property string $scriptFile Entry script file path (processed w/ realpath()). + * @property array $browser User browser capabilities. + * @property string $acceptTypes User browser accept types, null if not present. + * @property integer $port Port number for insecure requests. + * @property integer $securePort Port number for secure requests. + * @property CCookieCollection $cookies The cookie collection. + * @property string $preferredLanguage The user preferred language. + * @property string $csrfToken The random token for CSRF validation. + * + * @author Qiang Xue + * @version $Id$ + * @package system.web + * @since 1.0 + */ +class CHttpRequest extends CApplicationComponent +{ + /** + * @var boolean whether cookies should be validated to ensure they are not tampered. Defaults to false. + */ + public $enableCookieValidation=false; + /** + * @var boolean whether to enable CSRF (Cross-Site Request Forgery) validation. Defaults to false. + * By setting this property to true, forms submitted to an Yii Web application must be originated + * from the same application. If not, a 400 HTTP exception will be raised. + * Note, this feature requires that the user client accepts cookie. + * You also need to use {@link CHtml::form} or {@link CHtml::statefulForm} to generate + * the needed HTML forms in your pages. + * @see http://seclab.stanford.edu/websec/csrf/csrf.pdf + */ + public $enableCsrfValidation=false; + /** + * @var string the name of the token used to prevent CSRF. Defaults to 'YII_CSRF_TOKEN'. + * This property is effectively only when {@link enableCsrfValidation} is true. + */ + public $csrfTokenName='YII_CSRF_TOKEN'; + /** + * @var array the property values (in name-value pairs) used to initialize the CSRF cookie. + * Any property of {@link CHttpCookie} may be initialized. + * This property is effective only when {@link enableCsrfValidation} is true. + */ + public $csrfCookie; + + private $_requestUri; + private $_pathInfo; + private $_scriptFile; + private $_scriptUrl; + private $_hostInfo; + private $_baseUrl; + private $_cookies; + private $_preferredLanguage; + private $_csrfToken; + private $_deleteParams; + private $_putParams; + + /** + * Initializes the application component. + * This method overrides the parent implementation by preprocessing + * the user request data. + */ + public function init() + { + parent::init(); + $this->normalizeRequest(); + } + + /** + * Normalizes the request data. + * This method strips off slashes in request data if get_magic_quotes_gpc() returns true. + * It also performs CSRF validation if {@link enableCsrfValidation} is true. + */ + protected function normalizeRequest() + { + // normalize request + if(function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc()) + { + if(isset($_GET)) + $_GET=$this->stripSlashes($_GET); + if(isset($_POST)) + $_POST=$this->stripSlashes($_POST); + if(isset($_REQUEST)) + $_REQUEST=$this->stripSlashes($_REQUEST); + if(isset($_COOKIE)) + $_COOKIE=$this->stripSlashes($_COOKIE); + } + + if($this->enableCsrfValidation) + Yii::app()->attachEventHandler('onBeginRequest',array($this,'validateCsrfToken')); + } + + + /** + * Strips slashes from input data. + * This method is applied when magic quotes is enabled. + * @param mixed $data input data to be processed + * @return mixed processed data + */ + public function stripSlashes(&$data) + { + return is_array($data)?array_map(array($this,'stripSlashes'),$data):stripslashes($data); + } + + /** + * Returns the named GET or POST parameter value. + * If the GET or POST parameter does not exist, the second parameter to this method will be returned. + * If both GET and POST contains such a named parameter, the GET parameter takes precedence. + * @param string $name the GET parameter name + * @param mixed $defaultValue the default parameter value if the GET parameter does not exist. + * @return mixed the GET parameter value + * @see getQuery + * @see getPost + */ + public function getParam($name,$defaultValue=null) + { + return isset($_GET[$name]) ? $_GET[$name] : (isset($_POST[$name]) ? $_POST[$name] : $defaultValue); + } + + /** + * Returns the named GET parameter value. + * If the GET parameter does not exist, the second parameter to this method will be returned. + * @param string $name the GET parameter name + * @param mixed $defaultValue the default parameter value if the GET parameter does not exist. + * @return mixed the GET parameter value + * @see getPost + * @see getParam + */ + public function getQuery($name,$defaultValue=null) + { + return isset($_GET[$name]) ? $_GET[$name] : $defaultValue; + } + + /** + * Returns the named POST parameter value. + * If the POST parameter does not exist, the second parameter to this method will be returned. + * @param string $name the POST parameter name + * @param mixed $defaultValue the default parameter value if the POST parameter does not exist. + * @return mixed the POST parameter value + * @see getParam + * @see getQuery + */ + public function getPost($name,$defaultValue=null) + { + return isset($_POST[$name]) ? $_POST[$name] : $defaultValue; + } + + /** + * Returns the named DELETE parameter value. + * If the DELETE parameter does not exist or if the current request is not a DELETE request, + * the second parameter to this method will be returned. + * If the DELETE request was tunneled through POST via _method parameter, the POST parameter + * will be returned instead (available since version 1.1.11). + * @param string $name the DELETE parameter name + * @param mixed $defaultValue the default parameter value if the DELETE parameter does not exist. + * @return mixed the DELETE parameter value + * @since 1.1.7 + */ + public function getDelete($name,$defaultValue=null) + { + if($this->getIsDeleteViaPostRequest()) + return $this->getPost($name, $defaultValue); + + if($this->_deleteParams===null) + $this->_deleteParams=$this->getIsDeleteRequest() ? $this->getRestParams() : array(); + return isset($this->_deleteParams[$name]) ? $this->_deleteParams[$name] : $defaultValue; + } + + /** + * Returns the named PUT parameter value. + * If the PUT parameter does not exist or if the current request is not a PUT request, + * the second parameter to this method will be returned. + * If the PUT request was tunneled through POST via _method parameter, the POST parameter + * will be returned instead (available since version 1.1.11). + * @param string $name the PUT parameter name + * @param mixed $defaultValue the default parameter value if the PUT parameter does not exist. + * @return mixed the PUT parameter value + * @since 1.1.7 + */ + public function getPut($name,$defaultValue=null) + { + if($this->getIsPutViaPostReqest()) + return $this->getPost($name, $defaultValue); + + if($this->_putParams===null) + $this->_putParams=$this->getIsPutRequest() ? $this->getRestParams() : array(); + return isset($this->_putParams[$name]) ? $this->_putParams[$name] : $defaultValue; + } + + /** + * Returns the PUT or DELETE request parameters. + * @return array the request parameters + * @since 1.1.7 + */ + protected function getRestParams() + { + $result=array(); + if(function_exists('mb_parse_str')) + mb_parse_str(file_get_contents('php://input'), $result); + else + parse_str(file_get_contents('php://input'), $result); + return $result; + } + + /** + * Returns the currently requested URL. + * This is the same as {@link getRequestUri}. + * @return string part of the request URL after the host info. + */ + public function getUrl() + { + return $this->getRequestUri(); + } + + /** + * Returns the schema and host part of the application URL. + * The returned URL does not have an ending slash. + * By default this is determined based on the user request information. + * You may explicitly specify it by setting the {@link setHostInfo hostInfo} property. + * @param string $schema schema to use (e.g. http, https). If empty, the schema used for the current request will be used. + * @return string schema and hostname part (with port number if needed) of the request URL (e.g. http://www.yiiframework.com) + * @see setHostInfo + */ + public function getHostInfo($schema='') + { + if($this->_hostInfo===null) + { + if($secure=$this->getIsSecureConnection()) + $http='https'; + else + $http='http'; + if(isset($_SERVER['HTTP_HOST'])) + $this->_hostInfo=$http.'://'.$_SERVER['HTTP_HOST']; + else + { + $this->_hostInfo=$http.'://'.$_SERVER['SERVER_NAME']; + $port=$secure ? $this->getSecurePort() : $this->getPort(); + if(($port!==80 && !$secure) || ($port!==443 && $secure)) + $this->_hostInfo.=':'.$port; + } + } + if($schema!=='') + { + $secure=$this->getIsSecureConnection(); + if($secure && $schema==='https' || !$secure && $schema==='http') + return $this->_hostInfo; + + $port=$schema==='https' ? $this->getSecurePort() : $this->getPort(); + if($port!==80 && $schema==='http' || $port!==443 && $schema==='https') + $port=':'.$port; + else + $port=''; + + $pos=strpos($this->_hostInfo,':'); + return $schema.substr($this->_hostInfo,$pos,strcspn($this->_hostInfo,':',$pos+1)+1).$port; + } + else + return $this->_hostInfo; + } + + /** + * Sets the schema and host part of the application URL. + * This setter is provided in case the schema and hostname cannot be determined + * on certain Web servers. + * @param string $value the schema and host part of the application URL. + */ + public function setHostInfo($value) + { + $this->_hostInfo=rtrim($value,'/'); + } + + /** + * Returns the relative URL for the application. + * This is similar to {@link getScriptUrl scriptUrl} except that + * it does not have the script file name, and the ending slashes are stripped off. + * @param boolean $absolute whether to return an absolute URL. Defaults to false, meaning returning a relative one. + * @return string the relative URL for the application + * @see setScriptUrl + */ + public function getBaseUrl($absolute=false) + { + if($this->_baseUrl===null) + $this->_baseUrl=rtrim(dirname($this->getScriptUrl()),'\\/'); + return $absolute ? $this->getHostInfo() . $this->_baseUrl : $this->_baseUrl; + } + + /** + * Sets the relative URL for the application. + * By default the URL is determined based on the entry script URL. + * This setter is provided in case you want to change this behavior. + * @param string $value the relative URL for the application + */ + public function setBaseUrl($value) + { + $this->_baseUrl=$value; + } + + /** + * Returns the relative URL of the entry script. + * The implementation of this method referenced Zend_Controller_Request_Http in Zend Framework. + * @return string the relative URL of the entry script. + */ + public function getScriptUrl() + { + if($this->_scriptUrl===null) + { + $scriptName=basename($_SERVER['SCRIPT_FILENAME']); + if(basename($_SERVER['SCRIPT_NAME'])===$scriptName) + $this->_scriptUrl=$_SERVER['SCRIPT_NAME']; + else if(basename($_SERVER['PHP_SELF'])===$scriptName) + $this->_scriptUrl=$_SERVER['PHP_SELF']; + else if(isset($_SERVER['ORIG_SCRIPT_NAME']) && basename($_SERVER['ORIG_SCRIPT_NAME'])===$scriptName) + $this->_scriptUrl=$_SERVER['ORIG_SCRIPT_NAME']; + else if(($pos=strpos($_SERVER['PHP_SELF'],'/'.$scriptName))!==false) + $this->_scriptUrl=substr($_SERVER['SCRIPT_NAME'],0,$pos).'/'.$scriptName; + else if(isset($_SERVER['DOCUMENT_ROOT']) && strpos($_SERVER['SCRIPT_FILENAME'],$_SERVER['DOCUMENT_ROOT'])===0) + $this->_scriptUrl=str_replace('\\','/',str_replace($_SERVER['DOCUMENT_ROOT'],'',$_SERVER['SCRIPT_FILENAME'])); + else + throw new CException(Yii::t('yii','CHttpRequest is unable to determine the entry script URL.')); + } + return $this->_scriptUrl; + } + + /** + * Sets the relative URL for the application entry script. + * This setter is provided in case the entry script URL cannot be determined + * on certain Web servers. + * @param string $value the relative URL for the application entry script. + */ + public function setScriptUrl($value) + { + $this->_scriptUrl='/'.trim($value,'/'); + } + + /** + * Returns the path info of the currently requested URL. + * This refers to the part that is after the entry script and before the question mark. + * The starting and ending slashes are stripped off. + * @return string part of the request URL that is after the entry script and before the question mark. + * Note, the returned pathinfo is decoded starting from 1.1.4. + * Prior to 1.1.4, whether it is decoded or not depends on the server configuration + * (in most cases it is not decoded). + * @throws CException if the request URI cannot be determined due to improper server configuration + */ + public function getPathInfo() + { + if($this->_pathInfo===null) + { + $pathInfo=$this->getRequestUri(); + + if(($pos=strpos($pathInfo,'?'))!==false) + $pathInfo=substr($pathInfo,0,$pos); + + $pathInfo=$this->decodePathInfo($pathInfo); + + $scriptUrl=$this->getScriptUrl(); + $baseUrl=$this->getBaseUrl(); + if(strpos($pathInfo,$scriptUrl)===0) + $pathInfo=substr($pathInfo,strlen($scriptUrl)); + else if($baseUrl==='' || strpos($pathInfo,$baseUrl)===0) + $pathInfo=substr($pathInfo,strlen($baseUrl)); + else if(strpos($_SERVER['PHP_SELF'],$scriptUrl)===0) + $pathInfo=substr($_SERVER['PHP_SELF'],strlen($scriptUrl)); + else + throw new CException(Yii::t('yii','CHttpRequest is unable to determine the path info of the request.')); + + $this->_pathInfo=trim($pathInfo,'/'); + } + return $this->_pathInfo; + } + + /** + * Decodes the path info. + * This method is an improved variant of the native urldecode() function and used in {@link getPathInfo getPathInfo()} to + * decode the path part of the request URI. You may override this method to change the way the path info is being decoded. + * @param string $pathInfo encoded path info + * @return string decoded path info + * @since 1.1.10 + */ + protected function decodePathInfo($pathInfo) + { + $pathInfo = urldecode($pathInfo); + + // is it UTF-8? + // http://w3.org/International/questions/qa-forms-utf-8.html + if(preg_match('%^(?: + [\x09\x0A\x0D\x20-\x7E] # ASCII + | [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte + | \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs + | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte + | \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates + | \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3 + | [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15 + | \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16 + )*$%xs', $pathInfo)) + { + return $pathInfo; + } + else + { + return utf8_encode($pathInfo); + } + } + + /** + * Returns the request URI portion for the currently requested URL. + * This refers to the portion that is after the {@link hostInfo host info} part. + * It includes the {@link queryString query string} part if any. + * The implementation of this method referenced Zend_Controller_Request_Http in Zend Framework. + * @return string the request URI portion for the currently requested URL. + * @throws CException if the request URI cannot be determined due to improper server configuration + */ + public function getRequestUri() + { + if($this->_requestUri===null) + { + if(isset($_SERVER['HTTP_X_REWRITE_URL'])) // IIS + $this->_requestUri=$_SERVER['HTTP_X_REWRITE_URL']; + else if(isset($_SERVER['REQUEST_URI'])) + { + $this->_requestUri=$_SERVER['REQUEST_URI']; + if(!empty($_SERVER['HTTP_HOST'])) + { + if(strpos($this->_requestUri,$_SERVER['HTTP_HOST'])!==false) + $this->_requestUri=preg_replace('/^\w+:\/\/[^\/]+/','',$this->_requestUri); + } + else + $this->_requestUri=preg_replace('/^(http|https):\/\/[^\/]+/i','',$this->_requestUri); + } + else if(isset($_SERVER['ORIG_PATH_INFO'])) // IIS 5.0 CGI + { + $this->_requestUri=$_SERVER['ORIG_PATH_INFO']; + if(!empty($_SERVER['QUERY_STRING'])) + $this->_requestUri.='?'.$_SERVER['QUERY_STRING']; + } + else + throw new CException(Yii::t('yii','CHttpRequest is unable to determine the request URI.')); + } + + return $this->_requestUri; + } + + /** + * Returns part of the request URL that is after the question mark. + * @return string part of the request URL that is after the question mark + */ + public function getQueryString() + { + return isset($_SERVER['QUERY_STRING'])?$_SERVER['QUERY_STRING']:''; + } + + /** + * Return if the request is sent via secure channel (https). + * @return boolean if the request is sent via secure channel (https) + */ + public function getIsSecureConnection() + { + return isset($_SERVER['HTTPS']) && !strcasecmp($_SERVER['HTTPS'],'on'); + } + + /** + * Returns the request type, such as GET, POST, HEAD, PUT, DELETE. + * Request type can be manually set in POST requests with a parameter named _method. Useful + * for RESTful request from older browsers which do not support PUT or DELETE + * natively (available since version 1.1.11). + * @return string request type, such as GET, POST, HEAD, PUT, DELETE. + */ + public function getRequestType() + { + if(isset($_POST['_method'])) + return strtoupper($_POST['_method']); + + return strtoupper(isset($_SERVER['REQUEST_METHOD'])?$_SERVER['REQUEST_METHOD']:'GET'); + } + + /** + * Returns whether this is a POST request. + * @return boolean whether this is a POST request. + */ + public function getIsPostRequest() + { + return isset($_SERVER['REQUEST_METHOD']) && !strcasecmp($_SERVER['REQUEST_METHOD'],'POST'); + } + + /** + * Returns whether this is a DELETE request. + * @return boolean whether this is a DELETE request. + * @since 1.1.7 + */ + public function getIsDeleteRequest() + { + return (isset($_SERVER['REQUEST_METHOD']) && !strcasecmp($_SERVER['REQUEST_METHOD'],'DELETE')) || $this->getIsDeleteViaPostRequest(); + } + + /** + * Returns whether this is a DELETE request which was tunneled through POST. + * @return boolean whether this is a DELETE request tunneled through POST. + * @since 1.1.11 + */ + protected function getIsDeleteViaPostRequest() + { + return isset($_POST['_method']) && !strcasecmp($_POST['_method'],'DELETE'); + } + + /** + * Returns whether this is a PUT request. + * @return boolean whether this is a PUT request. + * @since 1.1.7 + */ + public function getIsPutRequest() + { + return (isset($_SERVER['REQUEST_METHOD']) && !strcasecmp($_SERVER['REQUEST_METHOD'],'PUT')) || $this->getIsPutViaPostReqest(); + } + + /** + * Returns whether this is a PUT request which was tunneled through POST. + * @return boolean whether this is a PUT request tunneled through POST. + * @since 1.1.11 + */ + protected function getIsPutViaPostReqest() + { + return isset($_POST['_method']) && !strcasecmp($_POST['_method'],'PUT'); + } + + /** + * Returns whether this is an AJAX (XMLHttpRequest) request. + * @return boolean whether this is an AJAX (XMLHttpRequest) request. + */ + public function getIsAjaxRequest() + { + return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH']==='XMLHttpRequest'; + } + + /** + * Returns whether this is an Adobe Flash or Adobe Flex request. + * @return boolean whether this is an Adobe Flash or Adobe Flex request. + * @since 1.1.11 + */ + public function getIsFlashRequest() + { + return isset($_SERVER['HTTP_USER_AGENT']) && (stripos($_SERVER['HTTP_USER_AGENT'],'Shockwave')!==false || stripos($_SERVER['HTTP_USER_AGENT'],'Flash')!==false); + } + + /** + * Returns the server name. + * @return string server name + */ + public function getServerName() + { + return $_SERVER['SERVER_NAME']; + } + + /** + * Returns the server port number. + * @return integer server port number + */ + public function getServerPort() + { + return $_SERVER['SERVER_PORT']; + } + + /** + * Returns the URL referrer, null if not present + * @return string URL referrer, null if not present + */ + public function getUrlReferrer() + { + return isset($_SERVER['HTTP_REFERER'])?$_SERVER['HTTP_REFERER']:null; + } + + /** + * Returns the user agent, null if not present. + * @return string user agent, null if not present + */ + public function getUserAgent() + { + return isset($_SERVER['HTTP_USER_AGENT'])?$_SERVER['HTTP_USER_AGENT']:null; + } + + /** + * Returns the user IP address. + * @return string user IP address + */ + public function getUserHostAddress() + { + return isset($_SERVER['REMOTE_ADDR'])?$_SERVER['REMOTE_ADDR']:'127.0.0.1'; + } + + /** + * Returns the user host name, null if it cannot be determined. + * @return string user host name, null if cannot be determined + */ + public function getUserHost() + { + return isset($_SERVER['REMOTE_HOST'])?$_SERVER['REMOTE_HOST']:null; + } + + /** + * Returns entry script file path. + * @return string entry script file path (processed w/ realpath()) + */ + public function getScriptFile() + { + if($this->_scriptFile!==null) + return $this->_scriptFile; + else + return $this->_scriptFile=realpath($_SERVER['SCRIPT_FILENAME']); + } + + /** + * Returns information about the capabilities of user browser. + * @param string $userAgent the user agent to be analyzed. Defaults to null, meaning using the + * current User-Agent HTTP header information. + * @return array user browser capabilities. + * @see http://www.php.net/manual/en/function.get-browser.php + */ + public function getBrowser($userAgent=null) + { + return get_browser($userAgent,true); + } + + /** + * Returns user browser accept types, null if not present. + * @return string user browser accept types, null if not present + */ + public function getAcceptTypes() + { + return isset($_SERVER['HTTP_ACCEPT'])?$_SERVER['HTTP_ACCEPT']:null; + } + + private $_port; + + /** + * Returns the port to use for insecure requests. + * Defaults to 80, or the port specified by the server if the current + * request is insecure. + * You may explicitly specify it by setting the {@link setPort port} property. + * @return integer port number for insecure requests. + * @see setPort + * @since 1.1.3 + */ + public function getPort() + { + if($this->_port===null) + $this->_port=!$this->getIsSecureConnection() && isset($_SERVER['SERVER_PORT']) ? (int)$_SERVER['SERVER_PORT'] : 80; + return $this->_port; + } + + /** + * Sets the port to use for insecure requests. + * This setter is provided in case a custom port is necessary for certain + * server configurations. + * @param integer $value port number. + * @since 1.1.3 + */ + public function setPort($value) + { + $this->_port=(int)$value; + $this->_hostInfo=null; + } + + private $_securePort; + + /** + * Returns the port to use for secure requests. + * Defaults to 443, or the port specified by the server if the current + * request is secure. + * You may explicitly specify it by setting the {@link setSecurePort securePort} property. + * @return integer port number for secure requests. + * @see setSecurePort + * @since 1.1.3 + */ + public function getSecurePort() + { + if($this->_securePort===null) + $this->_securePort=$this->getIsSecureConnection() && isset($_SERVER['SERVER_PORT']) ? (int)$_SERVER['SERVER_PORT'] : 443; + return $this->_securePort; + } + + /** + * Sets the port to use for secure requests. + * This setter is provided in case a custom port is necessary for certain + * server configurations. + * @param integer $value port number. + * @since 1.1.3 + */ + public function setSecurePort($value) + { + $this->_securePort=(int)$value; + $this->_hostInfo=null; + } + + /** + * Returns the cookie collection. + * The result can be used like an associative array. Adding {@link CHttpCookie} objects + * to the collection will send the cookies to the client; and removing the objects + * from the collection will delete those cookies on the client. + * @return CCookieCollection the cookie collection. + */ + public function getCookies() + { + if($this->_cookies!==null) + return $this->_cookies; + else + return $this->_cookies=new CCookieCollection($this); + } + + /** + * Redirects the browser to the specified URL. + * @param string $url URL to be redirected to. If the URL is a relative one, the base URL of + * the application will be inserted at the beginning. + * @param boolean $terminate whether to terminate the current application + * @param integer $statusCode the HTTP status code. Defaults to 302. See {@link http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html} + * for details about HTTP status code. + */ + public function redirect($url,$terminate=true,$statusCode=302) + { + if(strpos($url,'/')===0) + $url=$this->getHostInfo().$url; + header('Location: '.$url, true, $statusCode); + if($terminate) + Yii::app()->end(); + } + + /** + * Returns the user preferred language. + * The returned language ID will be canonicalized using {@link CLocale::getCanonicalID}. + * This method returns false if the user does not have language preference. + * @return string the user preferred language. + */ + public function getPreferredLanguage() + { + if($this->_preferredLanguage===null) + { + if(isset($_SERVER['HTTP_ACCEPT_LANGUAGE']) && ($n=preg_match_all('/([\w\-_]+)\s*(;\s*q\s*=\s*(\d*\.\d*))?/',$_SERVER['HTTP_ACCEPT_LANGUAGE'],$matches))>0) + { + $languages=array(); + for($i=0;$i<$n;++$i) + $languages[$matches[1][$i]]=empty($matches[3][$i]) ? 1.0 : floatval($matches[3][$i]); + arsort($languages); + foreach($languages as $language=>$pref) + return $this->_preferredLanguage=CLocale::getCanonicalID($language); + } + return $this->_preferredLanguage=false; + } + return $this->_preferredLanguage; + } + + /** + * Sends a file to user. + * @param string $fileName file name + * @param string $content content to be set. + * @param string $mimeType mime type of the content. If null, it will be guessed automatically based on the given file name. + * @param boolean $terminate whether to terminate the current application after calling this method + */ + public function sendFile($fileName,$content,$mimeType=null,$terminate=true) + { + if($mimeType===null) + { + if(($mimeType=CFileHelper::getMimeTypeByExtension($fileName))===null) + $mimeType='text/plain'; + } + header('Pragma: public'); + header('Expires: 0'); + header('Cache-Control: must-revalidate, post-check=0, pre-check=0'); + header("Content-type: $mimeType"); + if(ob_get_length()===false) + header('Content-Length: '.(function_exists('mb_strlen') ? mb_strlen($content,'8bit') : strlen($content))); + header("Content-Disposition: attachment; filename=\"$fileName\""); + header('Content-Transfer-Encoding: binary'); + + if($terminate) + { + // clean up the application first because the file downloading could take long time + // which may cause timeout of some resources (such as DB connection) + Yii::app()->end(0,false); + echo $content; + exit(0); + } + else + echo $content; + } + + /** + * Sends existing file to a browser as a download using x-sendfile. + * + * X-Sendfile is a feature allowing a web application to redirect the request for a file to the webserver + * that in turn processes the request, this way eliminating the need to perform tasks like reading the file + * and sending it to the user. When dealing with a lot of files (or very big files) this can lead to a great + * increase in performance as the web application is allowed to terminate earlier while the webserver is + * handling the request. + * + * The request is sent to the server through a special non-standard HTTP-header. + * When the web server encounters the presence of such header it will discard all output and send the file + * specified by that header using web server internals including all optimizations like caching-headers. + * + * As this header directive is non-standard different directives exists for different web servers applications: + *
    + *
  • Apache: {@link http://tn123.org/mod_xsendfile X-Sendfile}
  • + *
  • Lighttpd v1.4: {@link http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file X-LIGHTTPD-send-file}
  • + *
  • Lighttpd v1.5: {@link http://redmine.lighttpd.net/projects/lighttpd/wiki/X-LIGHTTPD-send-file X-Sendfile}
  • + *
  • Nginx: {@link http://wiki.nginx.org/XSendfile X-Accel-Redirect}
  • + *
  • Cherokee: {@link http://www.cherokee-project.com/doc/other_goodies.html#x-sendfile X-Sendfile and X-Accel-Redirect}
  • + *
+ * So for this method to work the X-SENDFILE option/module should be enabled by the web server and + * a proper xHeader should be sent. + * + * Note: + * This option allows to download files that are not under web folders, and even files that are otherwise protected (deny from all) like .htaccess + * + * Side effects: + * If this option is disabled by the web server, when this method is called a download configuration dialog + * will open but the downloaded file will have 0 bytes. + * + * Example: + *
+	 * request->xSendFile('/home/user/Pictures/picture1.jpg',array(
+	 *        'saveName'=>'image1.jpg',
+	 *        'mimeType'=>'image/jpeg',
+	 *        'terminate'=>false,
+	 *    ));
+	 * ?>
+	 * 
+ * @param string $filePath file name with full path + * @param array $options additional options: + *
    + *
  • saveName: file name shown to the user, if not set real file name will be used
  • + *
  • mimeType: mime type of the file, if not set it will be guessed automatically based on the file name, if set to null no content-type header will be sent.
  • + *
  • xHeader: appropriate x-sendfile header, defaults to "X-Sendfile"
  • + *
  • terminate: whether to terminate the current application after calling this method, defaults to true
  • + *
  • forceDownload: specifies whether the file will be downloaded or shown inline, defaults to true. (Since version 1.1.9.)
  • + *
  • addHeaders: an array of additional http headers in header-value pairs (available since version 1.1.10)
  • + *
+ */ + public function xSendFile($filePath, $options=array()) + { + if(!isset($options['forceDownload']) || $options['forceDownload']) + $disposition='attachment'; + else + $disposition='inline'; + + if(!isset($options['saveName'])) + $options['saveName']=basename($filePath); + + if(!isset($options['mimeType'])) + { + if(($options['mimeType']=CFileHelper::getMimeTypeByExtension($filePath))===null) + $options['mimeType']='text/plain'; + } + + if(!isset($options['xHeader'])) + $options['xHeader']='X-Sendfile'; + + if($options['mimeType'] !== null) + header('Content-type: '.$options['mimeType']); + header('Content-Disposition: '.$disposition.'; filename="'.$options['saveName'].'"'); + if(isset($options['addHeaders'])) + { + foreach($options['addHeaders'] as $header=>$value) + header($header.': '.$value); + } + header(trim($options['xHeader']).': '.$filePath); + + if(!isset($options['terminate']) || $options['terminate']) + Yii::app()->end(); + } + + /** + * Returns the random token used to perform CSRF validation. + * The token will be read from cookie first. If not found, a new token + * will be generated. + * @return string the random token for CSRF validation. + * @see enableCsrfValidation + */ + public function getCsrfToken() + { + if($this->_csrfToken===null) + { + $cookie=$this->getCookies()->itemAt($this->csrfTokenName); + if(!$cookie || ($this->_csrfToken=$cookie->value)==null) + { + $cookie=$this->createCsrfCookie(); + $this->_csrfToken=$cookie->value; + $this->getCookies()->add($cookie->name,$cookie); + } + } + + return $this->_csrfToken; + } + + /** + * Creates a cookie with a randomly generated CSRF token. + * Initial values specified in {@link csrfCookie} will be applied + * to the generated cookie. + * @return CHttpCookie the generated cookie + * @see enableCsrfValidation + */ + protected function createCsrfCookie() + { + $cookie=new CHttpCookie($this->csrfTokenName,sha1(uniqid(mt_rand(),true))); + if(is_array($this->csrfCookie)) + { + foreach($this->csrfCookie as $name=>$value) + $cookie->$name=$value; + } + return $cookie; + } + + /** + * Performs the CSRF validation. + * This is the event handler responding to {@link CApplication::onBeginRequest}. + * The default implementation will compare the CSRF token obtained + * from a cookie and from a POST field. If they are different, a CSRF attack is detected. + * @param CEvent $event event parameter + * @throws CHttpException if the validation fails + */ + public function validateCsrfToken($event) + { + if($this->getIsPostRequest()) + { + // only validate POST requests + $cookies=$this->getCookies(); + if($cookies->contains($this->csrfTokenName) && isset($_POST[$this->csrfTokenName])) + { + $tokenFromCookie=$cookies->itemAt($this->csrfTokenName)->value; + $tokenFromPost=$_POST[$this->csrfTokenName]; + $valid=$tokenFromCookie===$tokenFromPost; + } + else + $valid=false; + if(!$valid) + throw new CHttpException(400,Yii::t('yii','The CSRF token could not be verified.')); + } + } +} + + +/** + * CCookieCollection implements a collection class to store cookies. + * + * You normally access it via {@link CHttpRequest::getCookies()}. + * + * Since CCookieCollection extends from {@link CMap}, it can be used + * like an associative array as follows: + *
+ * $cookies[$name]=new CHttpCookie($name,$value); // sends a cookie
+ * $value=$cookies[$name]->value; // reads a cookie value
+ * unset($cookies[$name]);  // removes a cookie
+ * 
+ * + * @author Qiang Xue + * @version $Id$ + * @package system.web + * @since 1.0 + */ +class CCookieCollection extends CMap +{ + private $_request; + private $_initialized=false; + + /** + * Constructor. + * @param CHttpRequest $request owner of this collection. + */ + public function __construct(CHttpRequest $request) + { + $this->_request=$request; + $this->copyfrom($this->getCookies()); + $this->_initialized=true; + } + + /** + * @return CHttpRequest the request instance + */ + public function getRequest() + { + return $this->_request; + } + + /** + * @return array list of validated cookies + */ + protected function getCookies() + { + $cookies=array(); + if($this->_request->enableCookieValidation) + { + $sm=Yii::app()->getSecurityManager(); + foreach($_COOKIE as $name=>$value) + { + if(is_string($value) && ($value=$sm->validateData($value))!==false) + $cookies[$name]=new CHttpCookie($name,@unserialize($value)); + } + } + else + { + foreach($_COOKIE as $name=>$value) + $cookies[$name]=new CHttpCookie($name,$value); + } + return $cookies; + } + + /** + * Adds a cookie with the specified name. + * This overrides the parent implementation by performing additional + * operations for each newly added CHttpCookie object. + * @param mixed $name Cookie name. + * @param CHttpCookie $cookie Cookie object. + * @throws CException if the item to be inserted is not a CHttpCookie object. + */ + public function add($name,$cookie) + { + if($cookie instanceof CHttpCookie) + { + $this->remove($name); + parent::add($name,$cookie); + if($this->_initialized) + $this->addCookie($cookie); + } + else + throw new CException(Yii::t('yii','CHttpCookieCollection can only hold CHttpCookie objects.')); + } + + /** + * Removes a cookie with the specified name. + * This overrides the parent implementation by performing additional + * cleanup work when removing a CHttpCookie object. + * @param mixed $name Cookie name. + * @return CHttpCookie The removed cookie object. + */ + public function remove($name) + { + if(($cookie=parent::remove($name))!==null) + { + if($this->_initialized) + $this->removeCookie($cookie); + } + return $cookie; + } + + /** + * Sends a cookie. + * @param CHttpCookie $cookie cookie to be sent + */ + protected function addCookie($cookie) + { + $value=$cookie->value; + if($this->_request->enableCookieValidation) + $value=Yii::app()->getSecurityManager()->hashData(serialize($value)); + if(version_compare(PHP_VERSION,'5.2.0','>=')) + setcookie($cookie->name,$value,$cookie->expire,$cookie->path,$cookie->domain,$cookie->secure,$cookie->httpOnly); + else + setcookie($cookie->name,$value,$cookie->expire,$cookie->path,$cookie->domain,$cookie->secure); + } + + /** + * Deletes a cookie. + * @param CHttpCookie $cookie cookie to be deleted + */ + protected function removeCookie($cookie) + { + if(version_compare(PHP_VERSION,'5.2.0','>=')) + setcookie($cookie->name,null,0,$cookie->path,$cookie->domain,$cookie->secure,$cookie->httpOnly); + else + setcookie($cookie->name,null,0,$cookie->path,$cookie->domain,$cookie->secure); + } +} diff --git a/framework/web/Session.php b/framework/web/Session.php new file mode 100644 index 0000000..4544fc0 --- /dev/null +++ b/framework/web/Session.php @@ -0,0 +1,572 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CHttpSession provides session-level data management and the related configurations. + * + * To start the session, call {@link open()}; To complete and send out session data, call {@link close()}; + * To destroy the session, call {@link destroy()}. + * + * If {@link autoStart} is set true, the session will be started automatically + * when the application component is initialized by the application. + * + * CHttpSession can be used like an array to set and get session data. For example, + *
+ *   $session=new CHttpSession;
+ *   $session->open();
+ *   $value1=$session['name1'];  // get session variable 'name1'
+ *   $value2=$session['name2'];  // get session variable 'name2'
+ *   foreach($session as $name=>$value) // traverse all session variables
+ *   $session['name3']=$value3;  // set session variable 'name3'
+ * 
+ * + * The following configurations are available for session: + *
    + *
  • {@link setSessionID sessionID};
  • + *
  • {@link setSessionName sessionName};
  • + *
  • {@link autoStart};
  • + *
  • {@link setSavePath savePath};
  • + *
  • {@link setCookieParams cookieParams};
  • + *
  • {@link setGCProbability gcProbability};
  • + *
  • {@link setCookieMode cookieMode};
  • + *
  • {@link setUseTransparentSessionID useTransparentSessionID};
  • + *
  • {@link setTimeout timeout}.
  • + *
+ * See the corresponding setter and getter documentation for more information. + * Note, these properties must be set before the session is started. + * + * CHttpSession can be extended to support customized session storage. + * Override {@link openSession}, {@link closeSession}, {@link readSession}, + * {@link writeSession}, {@link destroySession} and {@link gcSession} + * and set {@link useCustomStorage} to true. + * Then, the session data will be stored and retrieved using the above methods. + * + * CHttpSession is a Web application component that can be accessed via + * {@link CWebApplication::getSession()}. + * + * @property boolean $useCustomStorage Whether to use custom storage. + * @property boolean $isStarted Whether the session has started. + * @property string $sessionID The current session ID. + * @property string $sessionName The current session name. + * @property string $savePath The current session save path, defaults to '/tmp'. + * @property array $cookieParams The session cookie parameters. + * @property string $cookieMode How to use cookie to store session ID. Defaults to 'Allow'. + * @property integer $gCProbability The probability (percentage) that the gc (garbage collection) process is started on every session initialization, defaults to 1 meaning 1% chance. + * @property boolean $useTransparentSessionID Whether transparent sid support is enabled or not, defaults to false. + * @property integer $timeout The number of seconds after which data will be seen as 'garbage' and cleaned up, defaults to 1440 seconds. + * @property CHttpSessionIterator $iterator An iterator for traversing the session variables. + * @property integer $count The number of session variables. + * @property array $keys The list of session variable names. + * + * @author Qiang Xue + * @version $Id$ + * @package system.web + * @since 1.0 + */ +class CHttpSession extends CApplicationComponent implements IteratorAggregate,ArrayAccess,Countable +{ + /** + * @var boolean whether the session should be automatically started when the session application component is initialized, defaults to true. + */ + public $autoStart=true; + + + /** + * Initializes the application component. + * This method is required by IApplicationComponent and is invoked by application. + */ + public function init() + { + parent::init(); + if($this->autoStart) + $this->open(); + register_shutdown_function(array($this,'close')); + } + + /** + * Returns a value indicating whether to use custom session storage. + * This method should be overriden to return true if custom session storage handler should be used. + * If returning true, make sure the methods {@link openSession}, {@link closeSession}, {@link readSession}, + * {@link writeSession}, {@link destroySession}, and {@link gcSession} are overridden in child + * class, because they will be used as the callback handlers. + * The default implementation always return false. + * @return boolean whether to use custom storage. + */ + public function getUseCustomStorage() + { + return false; + } + + /** + * Starts the session if it has not started yet. + */ + public function open() + { + if($this->getUseCustomStorage()) + @session_set_save_handler(array($this,'openSession'),array($this,'closeSession'),array($this,'readSession'),array($this,'writeSession'),array($this,'destroySession'),array($this,'gcSession')); + + @session_start(); + if(YII_DEBUG && session_id()=='') + { + $message=Yii::t('yii','Failed to start session.'); + if(function_exists('error_get_last')) + { + $error=error_get_last(); + if(isset($error['message'])) + $message=$error['message']; + } + Yii::log($message, CLogger::LEVEL_WARNING, 'system.web.CHttpSession'); + } + } + + /** + * Ends the current session and store session data. + */ + public function close() + { + if(session_id()!=='') + @session_write_close(); + } + + /** + * Frees all session variables and destroys all data registered to a session. + */ + public function destroy() + { + if(session_id()!=='') + { + @session_unset(); + @session_destroy(); + } + } + + /** + * @return boolean whether the session has started + */ + public function getIsStarted() + { + return session_id()!==''; + } + + /** + * @return string the current session ID + */ + public function getSessionID() + { + return session_id(); + } + + /** + * @param string $value the session ID for the current session + */ + public function setSessionID($value) + { + session_id($value); + } + + /** + * Updates the current session id with a newly generated one . + * Please refer to {@link http://php.net/session_regenerate_id} for more details. + * @param boolean $deleteOldSession Whether to delete the old associated session file or not. + * @since 1.1.8 + */ + public function regenerateID($deleteOldSession=false) + { + session_regenerate_id($deleteOldSession); + } + + /** + * @return string the current session name + */ + public function getSessionName() + { + return session_name(); + } + + /** + * @param string $value the session name for the current session, must be an alphanumeric string, defaults to PHPSESSID + */ + public function setSessionName($value) + { + session_name($value); + } + + /** + * @return string the current session save path, defaults to '/tmp'. + */ + public function getSavePath() + { + return session_save_path(); + } + + /** + * @param string $value the current session save path + * @throws CException if the path is not a valid directory + */ + public function setSavePath($value) + { + if(is_dir($value)) + session_save_path($value); + else + throw new CException(Yii::t('yii','CHttpSession.savePath "{path}" is not a valid directory.', + array('{path}'=>$value))); + } + + /** + * @return array the session cookie parameters. + * @see http://us2.php.net/manual/en/function.session-get-cookie-params.php + */ + public function getCookieParams() + { + return session_get_cookie_params(); + } + + /** + * Sets the session cookie parameters. + * The effect of this method only lasts for the duration of the script. + * Call this method before the session starts. + * @param array $value cookie parameters, valid keys include: lifetime, path, domain, secure. + * @see http://us2.php.net/manual/en/function.session-set-cookie-params.php + */ + public function setCookieParams($value) + { + $data=session_get_cookie_params(); + extract($data); + extract($value); + if(isset($httponly)) + session_set_cookie_params($lifetime,$path,$domain,$secure,$httponly); + else + session_set_cookie_params($lifetime,$path,$domain,$secure); + } + + /** + * @return string how to use cookie to store session ID. Defaults to 'Allow'. + */ + public function getCookieMode() + { + if(ini_get('session.use_cookies')==='0') + return 'none'; + else if(ini_get('session.use_only_cookies')==='0') + return 'allow'; + else + return 'only'; + } + + /** + * @param string $value how to use cookie to store session ID. Valid values include 'none', 'allow' and 'only'. + */ + public function setCookieMode($value) + { + if($value==='none') + { + ini_set('session.use_cookies','0'); + ini_set('session.use_only_cookies','0'); + } + else if($value==='allow') + { + ini_set('session.use_cookies','1'); + ini_set('session.use_only_cookies','0'); + } + else if($value==='only') + { + ini_set('session.use_cookies','1'); + ini_set('session.use_only_cookies','1'); + } + else + throw new CException(Yii::t('yii','CHttpSession.cookieMode can only be "none", "allow" or "only".')); + } + + /** + * @return integer the probability (percentage) that the gc (garbage collection) process is started on every session initialization, defaults to 1 meaning 1% chance. + */ + public function getGCProbability() + { + return (int)ini_get('session.gc_probability'); + } + + /** + * @param integer $value the probability (percentage) that the gc (garbage collection) process is started on every session initialization. + * @throws CException if the value is beyond [0,100] + */ + public function setGCProbability($value) + { + $value=(int)$value; + if($value>=0 && $value<=100) + { + ini_set('session.gc_probability',$value); + ini_set('session.gc_divisor','100'); + } + else + throw new CException(Yii::t('yii','CHttpSession.gcProbability "{value}" is invalid. It must be an integer between 0 and 100.', + array('{value}'=>$value))); + } + + /** + * @return boolean whether transparent sid support is enabled or not, defaults to false. + */ + public function getUseTransparentSessionID() + { + return ini_get('session.use_trans_sid')==1; + } + + /** + * @param boolean $value whether transparent sid support is enabled or not. + */ + public function setUseTransparentSessionID($value) + { + ini_set('session.use_trans_sid',$value?'1':'0'); + } + + /** + * @return integer the number of seconds after which data will be seen as 'garbage' and cleaned up, defaults to 1440 seconds. + */ + public function getTimeout() + { + return (int)ini_get('session.gc_maxlifetime'); + } + + /** + * @param integer $value the number of seconds after which data will be seen as 'garbage' and cleaned up + */ + public function setTimeout($value) + { + ini_set('session.gc_maxlifetime',$value); + } + + /** + * Session open handler. + * This method should be overridden if {@link useCustomStorage} is set true. + * Do not call this method directly. + * @param string $savePath session save path + * @param string $sessionName session name + * @return boolean whether session is opened successfully + */ + public function openSession($savePath,$sessionName) + { + return true; + } + + /** + * Session close handler. + * This method should be overridden if {@link useCustomStorage} is set true. + * Do not call this method directly. + * @return boolean whether session is closed successfully + */ + public function closeSession() + { + return true; + } + + /** + * Session read handler. + * This method should be overridden if {@link useCustomStorage} is set true. + * Do not call this method directly. + * @param string $id session ID + * @return string the session data + */ + public function readSession($id) + { + return ''; + } + + /** + * Session write handler. + * This method should be overridden if {@link useCustomStorage} is set true. + * Do not call this method directly. + * @param string $id session ID + * @param string $data session data + * @return boolean whether session write is successful + */ + public function writeSession($id,$data) + { + return true; + } + + /** + * Session destroy handler. + * This method should be overridden if {@link useCustomStorage} is set true. + * Do not call this method directly. + * @param string $id session ID + * @return boolean whether session is destroyed successfully + */ + public function destroySession($id) + { + return true; + } + + /** + * Session GC (garbage collection) handler. + * This method should be overridden if {@link useCustomStorage} is set true. + * Do not call this method directly. + * @param integer $maxLifetime the number of seconds after which data will be seen as 'garbage' and cleaned up. + * @return boolean whether session is GCed successfully + */ + public function gcSession($maxLifetime) + { + return true; + } + + //------ The following methods enable CHttpSession to be CMap-like ----- + + /** + * Returns an iterator for traversing the session variables. + * This method is required by the interface IteratorAggregate. + * @return CHttpSessionIterator an iterator for traversing the session variables. + */ + public function getIterator() + { + return new CHttpSessionIterator; + } + + /** + * Returns the number of items in the session. + * @return integer the number of session variables + */ + public function getCount() + { + return count($_SESSION); + } + + /** + * Returns the number of items in the session. + * This method is required by Countable interface. + * @return integer number of items in the session. + */ + public function count() + { + return $this->getCount(); + } + + /** + * @return array the list of session variable names + */ + public function getKeys() + { + return array_keys($_SESSION); + } + + /** + * Returns the session variable value with the session variable name. + * This method is very similar to {@link itemAt} and {@link offsetGet}, + * except that it will return $defaultValue if the session variable does not exist. + * @param mixed $key the session variable name + * @param mixed $defaultValue the default value to be returned when the session variable does not exist. + * @return mixed the session variable value, or $defaultValue if the session variable does not exist. + * @since 1.1.2 + */ + public function get($key,$defaultValue=null) + { + return isset($_SESSION[$key]) ? $_SESSION[$key] : $defaultValue; + } + + /** + * Returns the session variable value with the session variable name. + * This method is exactly the same as {@link offsetGet}. + * @param mixed $key the session variable name + * @return mixed the session variable value, null if no such variable exists + */ + public function itemAt($key) + { + return isset($_SESSION[$key]) ? $_SESSION[$key] : null; + } + + /** + * Adds a session variable. + * Note, if the specified name already exists, the old value will be removed first. + * @param mixed $key session variable name + * @param mixed $value session variable value + */ + public function add($key,$value) + { + $_SESSION[$key]=$value; + } + + /** + * Removes a session variable. + * @param mixed $key the name of the session variable to be removed + * @return mixed the removed value, null if no such session variable. + */ + public function remove($key) + { + if(isset($_SESSION[$key])) + { + $value=$_SESSION[$key]; + unset($_SESSION[$key]); + return $value; + } + else + return null; + } + + /** + * Removes all session variables + */ + public function clear() + { + foreach(array_keys($_SESSION) as $key) + unset($_SESSION[$key]); + } + + /** + * @param mixed $key session variable name + * @return boolean whether there is the named session variable + */ + public function contains($key) + { + return isset($_SESSION[$key]); + } + + /** + * @return array the list of all session variables in array + */ + public function toArray() + { + return $_SESSION; + } + + /** + * This method is required by the interface ArrayAccess. + * @param mixed $offset the offset to check on + * @return boolean + */ + public function offsetExists($offset) + { + return isset($_SESSION[$offset]); + } + + /** + * This method is required by the interface ArrayAccess. + * @param integer $offset the offset to retrieve element. + * @return mixed the element at the offset, null if no element is found at the offset + */ + public function offsetGet($offset) + { + return isset($_SESSION[$offset]) ? $_SESSION[$offset] : null; + } + + /** + * This method is required by the interface ArrayAccess. + * @param integer $offset the offset to set element + * @param mixed $item the element value + */ + public function offsetSet($offset,$item) + { + $_SESSION[$offset]=$item; + } + + /** + * This method is required by the interface ArrayAccess. + * @param mixed $offset the offset to unset element + */ + public function offsetUnset($offset) + { + unset($_SESSION[$offset]); + } +} diff --git a/framework/web/Sort.php b/framework/web/Sort.php new file mode 100644 index 0000000..c8ba5fa --- /dev/null +++ b/framework/web/Sort.php @@ -0,0 +1,459 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CSort represents information relevant to sorting. + * + * When data needs to be sorted according to one or several attributes, + * we can use CSort to represent the sorting information and generate + * appropriate hyperlinks that can lead to sort actions. + * + * CSort is designed to be used together with {@link CActiveRecord}. + * When creating a CSort instance, you need to specify {@link modelClass}. + * You can use CSort to generate hyperlinks by calling {@link link}. + * You can also use CSort to modify a {@link CDbCriteria} instance by calling {@link applyOrder} so that + * it can cause the query results to be sorted according to the specified + * attributes. + * + * In order to prevent SQL injection attacks, CSort ensures that only valid model attributes + * can be sorted. This is determined based on {@link modelClass} and {@link attributes}. + * When {@link attributes} is not set, all attributes belonging to {@link modelClass} + * can be sorted. When {@link attributes} is set, only those attributes declared in the property + * can be sorted. + * + * By configuring {@link attributes}, one can perform more complex sorts that may + * consist of things like compound attributes (e.g. sort based on the combination of + * first name and last name of users). + * + * The property {@link attributes} should be an array of key-value pairs, where the keys + * represent the attribute names, while the values represent the virtual attribute definitions. + * For more details, please check the documentation about {@link attributes}. + * + * @property string $orderBy The order-by columns represented by this sort object. + * This can be put in the ORDER BY clause of a SQL statement. + * @property array $directions Sort directions indexed by attribute names. + * The sort direction. Can be either CSort::SORT_ASC for ascending order or + * CSort::SORT_DESC for descending order. + * + * @author Qiang Xue + * @version $Id$ + * @package system.web + */ +class CSort extends CComponent +{ + /** + * Sort ascending + * @since 1.1.10 + */ + const SORT_ASC = false; + + /** + * Sort descending + * @since 1.1.10 + */ + const SORT_DESC = true; + + /** + * @var boolean whether the sorting can be applied to multiple attributes simultaneously. + * Defaults to false, which means each time the data can only be sorted by one attribute. + */ + public $multiSort=false; + /** + * @var string the name of the model class whose attributes can be sorted. + * The model class must be a child class of {@link CActiveRecord}. + */ + public $modelClass; + /** + * @var array list of attributes that are allowed to be sorted. + * For example, array('user_id','create_time') would specify that only 'user_id' + * and 'create_time' of the model {@link modelClass} can be sorted. + * By default, this property is an empty array, which means all attributes in + * {@link modelClass} are allowed to be sorted. + * + * This property can also be used to specify complex sorting. To do so, + * a virtual attribute can be declared in terms of a key-value pair in the array. + * The key refers to the name of the virtual attribute that may appear in the sort request, + * while the value specifies the definition of the virtual attribute. + * + * In the simple case, a key-value pair can be like 'user'=>'user_id' + * where 'user' is the name of the virtual attribute while 'user_id' means the virtual + * attribute is the 'user_id' attribute in the {@link modelClass}. + * + * A more flexible way is to specify the key-value pair as + *
+	 * 'user'=>array(
+	 *     'asc'=>'first_name, last_name',
+	 *     'desc'=>'first_name DESC, last_name DESC',
+	 *     'label'=>'Name'
+	 * )
+	 * 
+ * where 'user' is the name of the virtual attribute that specifies the full name of user + * (a compound attribute consisting of first name and last name of user). In this case, + * we have to use an array to define the virtual attribute with three elements: 'asc', + * 'desc' and 'label'. + * + * The above approach can also be used to declare virtual attributes that consist of relational + * attributes. For example, + *
+	 * 'price'=>array(
+	 *     'asc'=>'item.price',
+	 *     'desc'=>'item.price DESC',
+	 *     'label'=>'Item Price'
+	 * )
+	 * 
+ * + * Note, the attribute name should not contain '-' or '.' characters because + * they are used as {@link separators}. + * + * Starting from version 1.1.3, an additional option named 'default' can be used in the virtual attribute + * declaration. This option specifies whether an attribute should be sorted in ascending or descending + * order upon user clicking the corresponding sort hyperlink if it is not currently sorted. The valid + * option values include 'asc' (default) and 'desc'. For example, + *
+	 * 'price'=>array(
+	 *     'asc'=>'item.price',
+	 *     'desc'=>'item.price DESC',
+	 *     'label'=>'Item Price',
+	 *     'default'=>'desc',
+	 * )
+	 * 
+ * + * Also starting from version 1.1.3, you can include a star ('*') element in this property so that + * all model attributes are available for sorting, in addition to those virtual attributes. For example, + *
+	 * 'attributes'=>array(
+	 *     'price'=>array(
+	 *         'asc'=>'item.price',
+	 *         'desc'=>'item.price DESC',
+	 *         'label'=>'Item Price',
+	 *         'default'=>'desc',
+	 *     ),
+	 *     '*',
+	 * )
+	 * 
+ * Note that when a name appears as both a model attribute and a virtual attribute, the position of + * the star element in the array determines which one takes precedence. In particular, if the star + * element is the first element in the array, the model attribute takes precedence; and if the star + * element is the last one, the virtual attribute takes precedence. + */ + public $attributes=array(); + /** + * @var string the name of the GET parameter that specifies which attributes to be sorted + * in which direction. Defaults to 'sort'. + */ + public $sortVar='sort'; + /** + * @var string the tag appeared in the GET parameter that indicates the attribute should be sorted + * in descending order. Defaults to 'desc'. + */ + public $descTag='desc'; + /** + * @var mixed the default order that should be applied to the query criteria when + * the current request does not specify any sort. For example, 'name, create_time DESC' or + * 'UPPER(name)'. + * + * Starting from version 1.1.3, you can also specify the default order using an array. + * The array keys could be attribute names or virtual attribute names as declared in {@link attributes}, + * and the array values indicate whether the sorting of the corresponding attributes should + * be in descending order. For example, + *
+	 * 'defaultOrder'=>array(
+	 *     'price'=>CSort::SORT_DESC,
+	 * )
+	 * 
+ * `SORT_DESC` and `SORT_ASC` are available since 1.1.10. In earlier Yii versions you should use + * `true` and `false` respectively. + * + * Please note when using array to specify the default order, the corresponding attributes + * will be put into {@link directions} and thus affect how the sort links are rendered + * (e.g. an arrow may be displayed next to the currently active sort link). + */ + public $defaultOrder; + /** + * @var string the route (controller ID and action ID) for generating the sorted contents. + * Defaults to empty string, meaning using the currently requested route. + */ + public $route=''; + /** + * @var array separators used in the generated URL. This must be an array consisting of + * two elements. The first element specifies the character separating different + * attributes, while the second element specifies the character separating attribute name + * and the corresponding sort direction. Defaults to array('-','.'). + */ + public $separators=array('-','.'); + /** + * @var array the additional GET parameters (name=>value) that should be used when generating sort URLs. + * Defaults to null, meaning using the currently available GET parameters. + */ + public $params; + + private $_directions; + + /** + * Constructor. + * @param string $modelClass the class name of data models that need to be sorted. + * This should be a child class of {@link CActiveRecord}. + */ + public function __construct($modelClass=null) + { + $this->modelClass=$modelClass; + } + + /** + * Modifies the query criteria by changing its {@link CDbCriteria::order} property. + * This method will use {@link directions} to determine which columns need to be sorted. + * They will be put in the ORDER BY clause. If the criteria already has non-empty {@link CDbCriteria::order} value, + * the new value will be appended to it. + * @param CDbCriteria $criteria the query criteria + */ + public function applyOrder($criteria) + { + $order=$this->getOrderBy(); + if(!empty($order)) + { + if(!empty($criteria->order)) + $criteria->order.=', '; + $criteria->order.=$order; + } + } + + /** + * @return string the order-by columns represented by this sort object. + * This can be put in the ORDER BY clause of a SQL statement. + * @since 1.1.0 + */ + public function getOrderBy() + { + $directions=$this->getDirections(); + if(empty($directions)) + return is_string($this->defaultOrder) ? $this->defaultOrder : ''; + else + { + if($this->modelClass!==null) + $schema=CActiveRecord::model($this->modelClass)->getDbConnection()->getSchema(); + $orders=array(); + foreach($directions as $attribute=>$descending) + { + $definition=$this->resolveAttribute($attribute); + if(is_array($definition)) + { + if($descending) + $orders[]=isset($definition['desc']) ? $definition['desc'] : $attribute.' DESC'; + else + $orders[]=isset($definition['asc']) ? $definition['asc'] : $attribute; + } + else if($definition!==false) + { + $attribute=$definition; + if(isset($schema)) + { + if(($pos=strpos($attribute,'.'))!==false) + $attribute=$schema->quoteTableName(substr($attribute,0,$pos)).'.'.$schema->quoteColumnName(substr($attribute,$pos+1)); + else + $attribute=CActiveRecord::model($this->modelClass)->getTableAlias(true).'.'.$schema->quoteColumnName($attribute); + } + $orders[]=$descending?$attribute.' DESC':$attribute; + } + } + return implode(', ',$orders); + } + } + + /** + * Generates a hyperlink that can be clicked to cause sorting. + * @param string $attribute the attribute name. This must be the actual attribute name, not alias. + * If it is an attribute of a related AR object, the name should be prefixed with + * the relation name (e.g. 'author.name', where 'author' is the relation name). + * @param string $label the link label. If null, the label will be determined according + * to the attribute (see {@link resolveLabel}). + * @param array $htmlOptions additional HTML attributes for the hyperlink tag + * @return string the generated hyperlink + */ + public function link($attribute,$label=null,$htmlOptions=array()) + { + if($label===null) + $label=$this->resolveLabel($attribute); + if(($definition=$this->resolveAttribute($attribute))===false) + return $label; + $directions=$this->getDirections(); + if(isset($directions[$attribute])) + { + $class=$directions[$attribute] ? 'desc' : 'asc'; + if(isset($htmlOptions['class'])) + $htmlOptions['class'].=' '.$class; + else + $htmlOptions['class']=$class; + $descending=!$directions[$attribute]; + unset($directions[$attribute]); + } + else if(is_array($definition) && isset($definition['default'])) + $descending=$definition['default']==='desc'; + else + $descending=false; + + if($this->multiSort) + $directions=array_merge(array($attribute=>$descending),$directions); + else + $directions=array($attribute=>$descending); + + $url=$this->createUrl(Yii::app()->getController(),$directions); + + return $this->createLink($attribute,$label,$url,$htmlOptions); + } + + /** + * Resolves the attribute label for the specified attribute. + * This will invoke {@link CActiveRecord::getAttributeLabel} to determine what label to use. + * If the attribute refers to a virtual attribute declared in {@link attributes}, + * then the label given in the {@link attributes} will be returned instead. + * @param string $attribute the attribute name. + * @return string the attribute label + */ + public function resolveLabel($attribute) + { + $definition=$this->resolveAttribute($attribute); + if(is_array($definition)) + { + if(isset($definition['label'])) + return $definition['label']; + } + else if(is_string($definition)) + $attribute=$definition; + if($this->modelClass!==null) + return CActiveRecord::model($this->modelClass)->getAttributeLabel($attribute); + else + return $attribute; + } + + /** + * Returns the currently requested sort information. + * @return array sort directions indexed by attribute names. + * Sort direction can be either CSort::SORT_ASC for ascending order or + * CSort::SORT_DESC for descending order. + */ + public function getDirections() + { + if($this->_directions===null) + { + $this->_directions=array(); + if(isset($_GET[$this->sortVar]) && is_string($_GET[$this->sortVar])) + { + $attributes=explode($this->separators[0],$_GET[$this->sortVar]); + foreach($attributes as $attribute) + { + if(($pos=strrpos($attribute,$this->separators[1]))!==false) + { + $descending=substr($attribute,$pos+1)===$this->descTag; + if($descending) + $attribute=substr($attribute,0,$pos); + } + else + $descending=false; + + if(($this->resolveAttribute($attribute))!==false) + { + $this->_directions[$attribute]=$descending; + if(!$this->multiSort) + return $this->_directions; + } + } + } + if($this->_directions===array() && is_array($this->defaultOrder)) + $this->_directions=$this->defaultOrder; + } + return $this->_directions; + } + + /** + * Returns the sort direction of the specified attribute in the current request. + * @param string $attribute the attribute name + * @return mixed Sort direction of the attribute. Can be either CSort::SORT_ASC + * for ascending order or CSort::SORT_DESC for descending order. Value is null + * if the attribute doesn't need to be sorted. + */ + public function getDirection($attribute) + { + $this->getDirections(); + return isset($this->_directions[$attribute]) ? $this->_directions[$attribute] : null; + } + + /** + * Creates a URL that can lead to generating sorted data. + * @param CController $controller the controller that will be used to create the URL. + * @param array $directions the sort directions indexed by attribute names. + * The sort direction can be either CSort::SORT_ASC for ascending order or + * CSort::SORT_DESC for descending order. + * @return string the URL for sorting + */ + public function createUrl($controller,$directions) + { + $sorts=array(); + foreach($directions as $attribute=>$descending) + $sorts[]=$descending ? $attribute.$this->separators[1].$this->descTag : $attribute; + $params=$this->params===null ? $_GET : $this->params; + $params[$this->sortVar]=implode($this->separators[0],$sorts); + return $controller->createUrl($this->route,$params); + } + + /** + * Returns the real definition of an attribute given its name. + * + * The resolution is based on {@link attributes} and {@link CActiveRecord::attributeNames}. + *
    + *
  • When {@link attributes} is an empty array, if the name refers to an attribute of {@link modelClass}, + * then the name is returned back.
  • + *
  • When {@link attributes} is not empty, if the name refers to an attribute declared in {@link attributes}, + * then the corresponding virtual attribute definition is returned. Starting from version 1.1.3, if {@link attributes} + * contains a star ('*') element, the name will also be used to match against all model attributes.
  • + *
  • In all other cases, false is returned, meaning the name does not refer to a valid attribute.
  • + *
+ * @param string $attribute the attribute name that the user requests to sort on + * @return mixed the attribute name or the virtual attribute definition. False if the attribute cannot be sorted. + */ + public function resolveAttribute($attribute) + { + if($this->attributes!==array()) + $attributes=$this->attributes; + else if($this->modelClass!==null) + $attributes=CActiveRecord::model($this->modelClass)->attributeNames(); + else + return false; + foreach($attributes as $name=>$definition) + { + if(is_string($name)) + { + if($name===$attribute) + return $definition; + } + else if($definition==='*') + { + if($this->modelClass!==null && CActiveRecord::model($this->modelClass)->hasAttribute($attribute)) + return $attribute; + } + else if($definition===$attribute) + return $attribute; + } + return false; + } + + /** + * Creates a hyperlink based on the given label and URL. + * You may override this method to customize the link generation. + * @param string $attribute the name of the attribute that this link is for + * @param string $label the label of the hyperlink + * @param string $url the URL + * @param array $htmlOptions additional HTML options + * @return string the generated hyperlink + */ + protected function createLink($attribute,$label,$url,$htmlOptions) + { + return CHtml::link($label,$url,$htmlOptions); + } +} \ No newline at end of file diff --git a/framework/web/Theme.php b/framework/web/Theme.php new file mode 100644 index 0000000..337e968 --- /dev/null +++ b/framework/web/Theme.php @@ -0,0 +1,141 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CTheme represents an application theme. + * + * @property string $name Theme name. + * @property string $baseUrl The relative URL to the theme folder (without ending slash). + * @property string $basePath The file path to the theme folder. + * @property string $viewPath The path for controller views. Defaults to 'ThemeRoot/views'. + * @property string $systemViewPath The path for system views. Defaults to 'ThemeRoot/views/system'. + * @property string $skinPath The path for widget skins. Defaults to 'ThemeRoot/views/skins'. + * + * @author Qiang Xue + * @version $Id$ + * @package system.web + * @since 1.0 + */ +class CTheme extends CComponent +{ + private $_name; + private $_basePath; + private $_baseUrl; + + /** + * Constructor. + * @param string $name name of the theme + * @param string $basePath base theme path + * @param string $baseUrl base theme URL + */ + public function __construct($name,$basePath,$baseUrl) + { + $this->_name=$name; + $this->_baseUrl=$baseUrl; + $this->_basePath=$basePath; + } + + /** + * @return string theme name + */ + public function getName() + { + return $this->_name; + } + + /** + * @return string the relative URL to the theme folder (without ending slash) + */ + public function getBaseUrl() + { + return $this->_baseUrl; + } + + /** + * @return string the file path to the theme folder + */ + public function getBasePath() + { + return $this->_basePath; + } + + /** + * @return string the path for controller views. Defaults to 'ThemeRoot/views'. + */ + public function getViewPath() + { + return $this->_basePath.DIRECTORY_SEPARATOR.'views'; + } + + /** + * @return string the path for system views. Defaults to 'ThemeRoot/views/system'. + */ + public function getSystemViewPath() + { + return $this->getViewPath().DIRECTORY_SEPARATOR.'system'; + } + + /** + * @return string the path for widget skins. Defaults to 'ThemeRoot/views/skins'. + * @since 1.1 + */ + public function getSkinPath() + { + return $this->getViewPath().DIRECTORY_SEPARATOR.'skins'; + } + + /** + * Finds the view file for the specified controller's view. + * @param CController $controller the controller + * @param string $viewName the view name + * @return string the view file path. False if the file does not exist. + */ + public function getViewFile($controller,$viewName) + { + $moduleViewPath=$this->getViewPath(); + if(($module=$controller->getModule())!==null) + $moduleViewPath.='/'.$module->getId(); + return $controller->resolveViewFile($viewName,$this->getViewPath().'/'.$controller->getUniqueId(),$this->getViewPath(),$moduleViewPath); + } + + /** + * Finds the layout file for the specified controller's layout. + * @param CController $controller the controller + * @param string $layoutName the layout name + * @return string the layout file path. False if the file does not exist. + */ + public function getLayoutFile($controller,$layoutName) + { + $moduleViewPath=$basePath=$this->getViewPath(); + $module=$controller->getModule(); + if(empty($layoutName)) + { + while($module!==null) + { + if($module->layout===false) + return false; + if(!empty($module->layout)) + break; + $module=$module->getParentModule(); + } + if($module===null) + $layoutName=Yii::app()->layout; + else + { + $layoutName=$module->layout; + $moduleViewPath.='/'.$module->getId(); + } + } + else if($module!==null) + $moduleViewPath.='/'.$module->getId(); + + return $controller->resolveViewFile($layoutName,$moduleViewPath.'/layouts',$basePath,$moduleViewPath); + } +} diff --git a/framework/web/ThemeManager.php b/framework/web/ThemeManager.php new file mode 100644 index 0000000..0cf7a5f --- /dev/null +++ b/framework/web/ThemeManager.php @@ -0,0 +1,131 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CThemeManager manages the themes for the Web application. + * + * A theme is a collection of view/layout files and resource files + * (e.g. css, image, js files). When a theme is active, {@link CController} + * will look for the specified view/layout under the theme folder first. + * The corresponding view/layout files will be used if the theme provides them. + * Otherwise, the default view/layout files will be used. + * + * By default, each theme is organized as a directory whose name is the theme name. + * All themes are located under the "WebRootPath/themes" directory. + * + * To activate a theme, set the {@link CWebApplication::setTheme theme} property + * to be the name of that theme. + * + * Since a self-contained theme often contains resource files that are made + * Web accessible, please make sure the view/layout files are protected from Web access. + * + * @property array $themeNames List of available theme names. + * @property string $basePath The base path for all themes. Defaults to "WebRootPath/themes". + * @property string $baseUrl The base URL for all themes. Defaults to "/WebRoot/themes". + * + * @author Qiang Xue + * @version $Id$ + * @package system.web + * @since 1.0 + */ +class CThemeManager extends CApplicationComponent +{ + /** + * default themes base path + */ + const DEFAULT_BASEPATH='themes'; + + /** + * @var string the name of the theme class for representing a theme. + * Defaults to {@link CTheme}. This can also be a class name in dot syntax. + */ + public $themeClass='CTheme'; + + private $_basePath=null; + private $_baseUrl=null; + + + /** + * @param string $name name of the theme to be retrieved + * @return CTheme the theme retrieved. Null if the theme does not exist. + */ + public function getTheme($name) + { + $themePath=$this->getBasePath().DIRECTORY_SEPARATOR.$name; + if(is_dir($themePath)) + { + $class=Yii::import($this->themeClass, true); + return new $class($name,$themePath,$this->getBaseUrl().'/'.$name); + } + else + return null; + } + + /** + * @return array list of available theme names + */ + public function getThemeNames() + { + static $themes; + if($themes===null) + { + $themes=array(); + $basePath=$this->getBasePath(); + $folder=@opendir($basePath); + while(($file=@readdir($folder))!==false) + { + if($file!=='.' && $file!=='..' && $file!=='.svn' && $file!=='.gitignore' && is_dir($basePath.DIRECTORY_SEPARATOR.$file)) + $themes[]=$file; + } + closedir($folder); + sort($themes); + } + return $themes; + } + + /** + * @return string the base path for all themes. Defaults to "WebRootPath/themes". + */ + public function getBasePath() + { + if($this->_basePath===null) + $this->setBasePath(dirname(Yii::app()->getRequest()->getScriptFile()).DIRECTORY_SEPARATOR.self::DEFAULT_BASEPATH); + return $this->_basePath; + } + + /** + * @param string $value the base path for all themes. + * @throws CException if the base path does not exist + */ + public function setBasePath($value) + { + $this->_basePath=realpath($value); + if($this->_basePath===false || !is_dir($this->_basePath)) + throw new CException(Yii::t('yii','Theme directory "{directory}" does not exist.',array('{directory}'=>$value))); + } + + /** + * @return string the base URL for all themes. Defaults to "/WebRoot/themes". + */ + public function getBaseUrl() + { + if($this->_baseUrl===null) + $this->_baseUrl=Yii::app()->getBaseUrl().'/'.self::DEFAULT_BASEPATH; + return $this->_baseUrl; + } + + /** + * @param string $value the base URL for all themes. + */ + public function setBaseUrl($value) + { + $this->_baseUrl=rtrim($value,'/'); + } +} diff --git a/framework/web/UrlManager.php b/framework/web/UrlManager.php new file mode 100644 index 0000000..755529e --- /dev/null +++ b/framework/web/UrlManager.php @@ -0,0 +1,849 @@ + + * @link http://www.yiiframework.com/ + * @copyright Copyright © 2008-2011 Yii Software LLC + * @license http://www.yiiframework.com/license/ + */ + +/** + * CUrlManager manages the URLs of Yii Web applications. + * + * It provides URL construction ({@link createUrl()}) as well as parsing ({@link parseUrl()}) functionality. + * + * URLs managed via CUrlManager can be in one of the following two formats, + * by setting {@link setUrlFormat urlFormat} property: + *
    + *
  • 'path' format: /path/to/EntryScript.php/name1/value1/name2/value2...
  • + *
  • 'get' format: /path/to/EntryScript.php?name1=value1&name2=value2...
  • + *
+ * + * When using 'path' format, CUrlManager uses a set of {@link setRules rules} to: + *
    + *
  • parse the requested URL into a route ('ControllerID/ActionID') and GET parameters;
  • + *
  • create URLs based on the given route and GET parameters.
  • + *
+ * + * A rule consists of a route and a pattern. The latter is used by CUrlManager to determine + * which rule is used for parsing/creating URLs. A pattern is meant to match the path info + * part of a URL. It may contain named parameters using the syntax '<ParamName:RegExp>'. + * + * When parsing a URL, a matching rule will extract the named parameters from the path info + * and put them into the $_GET variable; when creating a URL, a matching rule will extract + * the named parameters from $_GET and put them into the path info part of the created URL. + * + * If a pattern ends with '/*', it means additional GET parameters may be appended to the path + * info part of the URL; otherwise, the GET parameters can only appear in the query string part. + * + * To specify URL rules, set the {@link setRules rules} property as an array of rules (pattern=>route). + * For example, + *
+ * array(
+ *     'articles'=>'article/list',
+ *     'article//*'=>'article/read',
+ * )
+ * 
+ * Two rules are specified in the above: + *
    + *
  • The first rule says that if the user requests the URL '/path/to/index.php/articles', + * it should be treated as '/path/to/index.php/article/list'; and vice versa applies + * when constructing such a URL.
  • + *
  • The second rule contains a named parameter 'id' which is specified using + * the <ParamName:RegExp> syntax. It says that if the user requests the URL + * '/path/to/index.php/article/13', it should be treated as '/path/to/index.php/article/read?id=13'; + * and vice versa applies when constructing such a URL.
  • + *
+ * + * The route part may contain references to named parameters defined in the pattern part. + * This allows a rule to be applied to different routes based on matching criteria. + * For example, + *
+ * array(
+ *      '<_c:(post|comment)>//<_a:(create|update|delete)>'=>'<_c>/<_a>',
+ *      '<_c:(post|comment)>/'=>'<_c>/view',
+ *      '<_c:(post|comment)>s/*'=>'<_c>/list',
+ * )
+ * 
+ * In the above, we use two named parameters '<_c>' and '<_a>' in the route part. The '<_c>' + * parameter matches either 'post' or 'comment', while the '<_a>' parameter matches an action ID. + * + * Like normal rules, these rules can be used for both parsing and creating URLs. + * For example, using the rules above, the URL '/index.php/post/123/create' + * would be parsed as the route 'post/create' with GET parameter 'id' being 123. + * And given the route 'post/list' and GET parameter 'page' being 2, we should get a URL + * '/index.php/posts/page/2'. + * + * It is also possible to include hostname into the rules for parsing and creating URLs. + * One may extract part of the hostname to be a GET parameter. + * For example, the URL http://admin.example.com/en/profile may be parsed into GET parameters + * user=admin and lang=en. On the other hand, rules with hostname may also be used to + * create URLs with parameterized hostnames. + * + * In order to use parameterized hostnames, simply declare URL rules with host info, e.g.: + *
+ * array(
+ *     'http://.example.com//profile' => 'user/profile',
+ * )
+ * 
+ * + * Starting from version 1.1.8, one can write custom URL rule classes and use them for one or several URL rules. + * For example, + *
+ * array(
+ *   // a standard rule
+ *   '' => 'site/',
+ *   // a custom rule using data in DB
+ *   array(
+ *     'class' => 'application.components.MyUrlRule',
+ *     'connectionID' => 'db',
+ *   ),
+ * )
+ * 
+ * Please note that the custom URL rule class should extend from {@link CBaseUrlRule} and + * implement the following two methods, + *
    + *
  • {@link CBaseUrlRule::createUrl()}
  • + *
  • {@link CBaseUrlRule::parseUrl()}
  • + *
+ * + * CUrlManager is a default application component that may be accessed via + * {@link CWebApplication::getUrlManager()}. + * + * @property string $baseUrl The base URL of the application (the part after host name and before query string). + * If {@link showScriptName} is true, it will include the script name part. + * Otherwise, it will not, and the ending slashes are stripped off. + * @property string $urlFormat The URL format. Defaults to 'path'. Valid values include 'path' and 'get'. + * Please refer to the guide for more details about the difference between these two formats. + * + * @author Qiang Xue + * @version $Id$ + * @package system.web + * @since 1.0 + */ +class CUrlManager extends CApplicationComponent +{ + const CACHE_KEY='Yii.CUrlManager.rules'; + const GET_FORMAT='get'; + const PATH_FORMAT='path'; + + /** + * @var array the URL rules (pattern=>route). + */ + public $rules=array(); + /** + * @var string the URL suffix used when in 'path' format. + * For example, ".html" can be used so that the URL looks like pointing to a static HTML page. Defaults to empty. + */ + public $urlSuffix=''; + /** + * @var boolean whether to show entry script name in the constructed URL. Defaults to true. + */ + public $showScriptName=true; + /** + * @var boolean whether to append GET parameters to the path info part. Defaults to true. + * This property is only effective when {@link urlFormat} is 'path' and is mainly used when + * creating URLs. When it is true, GET parameters will be appended to the path info and + * separate from each other using slashes. If this is false, GET parameters will be in query part. + */ + public $appendParams=true; + /** + * @var string the GET variable name for route. Defaults to 'r'. + */ + public $routeVar='r'; + /** + * @var boolean whether routes are case-sensitive. Defaults to true. By setting this to false, + * the route in the incoming request will be turned to lower case first before further processing. + * As a result, you should follow the convention that you use lower case when specifying + * controller mapping ({@link CWebApplication::controllerMap}) and action mapping + * ({@link CController::actions}). Also, the directory names for organizing controllers should + * be in lower case. + */ + public $caseSensitive=true; + /** + * @var boolean whether the GET parameter values should match the corresponding + * sub-patterns in a rule before using it to create a URL. Defaults to false, meaning + * a rule will be used for creating a URL only if its route and parameter names match the given ones. + * If this property is set true, then the given parameter values must also match the corresponding + * parameter sub-patterns. Note that setting this property to true will degrade performance. + * @since 1.1.0 + */ + public $matchValue=false; + /** + * @var string the ID of the cache application component that is used to cache the parsed URL rules. + * Defaults to 'cache' which refers to the primary cache application component. + * Set this property to false if you want to disable caching URL rules. + */ + public $cacheID='cache'; + /** + * @var boolean whether to enable strict URL parsing. + * This property is only effective when {@link urlFormat} is 'path'. + * If it is set true, then an incoming URL must match one of the {@link rules URL rules}. + * Otherwise, it will be treated as an invalid request and trigger a 404 HTTP exception. + * Defaults to false. + */ + public $useStrictParsing=false; + /** + * @var string the class name or path alias for the URL rule instances. Defaults to 'CUrlRule'. + * If you change this to something else, please make sure that the new class must extend from + * {@link CBaseUrlRule} and have the same constructor signature as {@link CUrlRule}. + * It must also be serializable and autoloadable. + * @since 1.1.8 + */ + public $urlRuleClass='CUrlRule'; + + private $_urlFormat=self::GET_FORMAT; + private $_rules=array(); + private $_baseUrl; + + + /** + * Initializes the application component. + */ + public function init() + { + parent::init(); + $this->processRules(); + } + + /** + * Processes the URL rules. + */ + protected function processRules() + { + if(empty($this->rules) || $this->getUrlFormat()===self::GET_FORMAT) + return; + if($this->cacheID!==false && ($cache=Yii::app()->getComponent($this->cacheID))!==null) + { + $hash=md5(serialize($this->rules)); + if(($data=$cache->get(self::CACHE_KEY))!==false && isset($data[1]) && $data[1]===$hash) + { + $this->_rules=$data[0]; + return; + } + } + foreach($this->rules as $pattern=>$route) + $this->_rules[]=$this->createUrlRule($route,$pattern); + if(isset($cache)) + $cache->set(self::CACHE_KEY,array($this->_rules,$hash)); + } + + /** + * Adds new URL rules. + * In order to make the new rules effective, this method must be called BEFORE + * {@link CWebApplication::processRequest}. + * @param array $rules new URL rules (pattern=>route). + * @param boolean $append whether the new URL rules should be appended to the existing ones. If false, + * they will be inserted at the beginning. + * @since 1.1.4 + */ + public function addRules($rules, $append=true) + { + if ($append) + { + foreach($rules as $pattern=>$route) + $this->_rules[]=$this->createUrlRule($route,$pattern); + } + else + { + foreach($rules as $pattern=>$route) + array_unshift($this->_rules, $this->createUrlRule($route,$pattern)); + } + } + + /** + * Creates a URL rule instance. + * The default implementation returns a CUrlRule object. + * @param mixed $route the route part of the rule. This could be a string or an array + * @param string $pattern the pattern part of the rule + * @return CUrlRule the URL rule instance + * @since 1.1.0 + */ + protected function createUrlRule($route,$pattern) + { + if(is_array($route) && isset($route['class'])) + return $route; + else + return new $this->urlRuleClass($route,$pattern); + } + + /** + * Constructs a URL. + * @param string $route the controller and the action (e.g. article/read) + * @param array $params list of GET parameters (name=>value). Both the name and value will be URL-encoded. + * If the name is '#', the corresponding value will be treated as an anchor + * and will be appended at the end of the URL. + * @param string $ampersand the token separating name-value pairs in the URL. Defaults to '&'. + * @return string the constructed URL + */ + public function createUrl($route,$params=array(),$ampersand='&') + { + unset($params[$this->routeVar]); + foreach($params as $i=>$param) + if($param===null) + $params[$i]=''; + + if(isset($params['#'])) + { + $anchor='#'.$params['#']; + unset($params['#']); + } + else + $anchor=''; + $route=trim($route,'/'); + foreach($this->_rules as $i=>$rule) + { + if(is_array($rule)) + $this->_rules[$i]=$rule=Yii::createComponent($rule); + if(($url=$rule->createUrl($this,$route,$params,$ampersand))!==false) + { + if($rule->hasHostInfo) + return $url==='' ? '/'.$anchor : $url.$anchor; + else + return $this->getBaseUrl().'/'.$url.$anchor; + } + } + return $this->createUrlDefault($route,$params,$ampersand).$anchor; + } + + /** + * Creates a URL based on default settings. + * @param string $route the controller and the action (e.g. article/read) + * @param array $params list of GET parameters + * @param string $ampersand the token separating name-value pairs in the URL. + * @return string the constructed URL + */ + protected function createUrlDefault($route,$params,$ampersand) + { + if($this->getUrlFormat()===self::PATH_FORMAT) + { + $url=rtrim($this->getBaseUrl().'/'.$route,'/'); + if($this->appendParams) + { + $url=rtrim($url.'/'.$this->createPathInfo($params,'/','/'),'/'); + return $route==='' ? $url : $url.$this->urlSuffix; + } + else + { + if($route!=='') + $url.=$this->urlSuffix; + $query=$this->createPathInfo($params,'=',$ampersand); + return $query==='' ? $url : $url.'?'.$query; + } + } + else + { + $url=$this->getBaseUrl(); + if(!$this->showScriptName) + $url.='/'; + if($route!=='') + { + $url.='?'.$this->routeVar.'='.$route; + if(($query=$this->createPathInfo($params,'=',$ampersand))!=='') + $url.=$ampersand.$query; + } + else if(($query=$this->createPathInfo($params,'=',$ampersand))!=='') + $url.='?'.$query; + return $url; + } + } + + /** + * Parses the user request. + * @param CHttpRequest $request the request application component + * @return string the route (controllerID/actionID) and perhaps GET parameters in path format. + */ + public function parseUrl($request) + { + if($this->getUrlFormat()===self::PATH_FORMAT) + { + $rawPathInfo=$request->getPathInfo(); + $pathInfo=$this->removeUrlSuffix($rawPathInfo,$this->urlSuffix); + foreach($this->_rules as $i=>$rule) + { + if(is_array($rule)) + $this->_rules[$i]=$rule=Yii::createComponent($rule); + if(($r=$rule->parseUrl($this,$request,$pathInfo,$rawPathInfo))!==false) + return isset($_GET[$this->routeVar]) ? $_GET[$this->routeVar] : $r; + } + if($this->useStrictParsing) + throw new CHttpException(404,Yii::t('yii','Unable to resolve the request "{route}".', + array('{route}'=>$pathInfo))); + else + return $pathInfo; + } + else if(isset($_GET[$this->routeVar])) + return $_GET[$this->routeVar]; + else if(isset($_POST[$this->routeVar])) + return $_POST[$this->routeVar]; + else + return ''; + } + + /** + * Parses a path info into URL segments and saves them to $_GET and $_REQUEST. + * @param string $pathInfo path info + */ + public function parsePathInfo($pathInfo) + { + if($pathInfo==='') + return; + $segs=explode('/',$pathInfo.'/'); + $n=count($segs); + for($i=0;$i<$n-1;$i+=2) + { + $key=$segs[$i]; + if($key==='') continue; + $value=$segs[$i+1]; + if(($pos=strpos($key,'['))!==false && ($m=preg_match_all('/\[(.*?)\]/',$key,$matches))>0) + { + $name=substr($key,0,$pos); + for($j=$m-1;$j>=0;--$j) + { + if($matches[1][$j]==='') + $value=array($value); + else + $value=array($matches[1][$j]=>$value); + } + if(isset($_GET[$name]) && is_array($_GET[$name])) + $value=CMap::mergeArray($_GET[$name],$value); + $_REQUEST[$name]=$_GET[$name]=$value; + } + else + $_REQUEST[$key]=$_GET[$key]=$value; + } + } + + /** + * Creates a path info based on the given parameters. + * @param array $params list of GET parameters + * @param string $equal the separator between name and value + * @param string $ampersand the separator between name-value pairs + * @param string $key this is used internally. + * @return string the created path info + */ + public function createPathInfo($params,$equal,$ampersand, $key=null) + { + $pairs = array(); + foreach($params as $k => $v) + { + if ($key!==null) + $k = $key.'['.$k.']'; + + if (is_array($v)) + $pairs[]=$this->createPathInfo($v,$equal,$ampersand, $k); + else + $pairs[]=urlencode($k).$equal.urlencode($v); + } + return implode($ampersand,$pairs); + } + + /** + * Removes the URL suffix from path info. + * @param string $pathInfo path info part in the URL + * @param string $urlSuffix the URL suffix to be removed + * @return string path info with URL suffix removed. + */ + public function removeUrlSuffix($pathInfo,$urlSuffix) + { + if($urlSuffix!=='' && substr($pathInfo,-strlen($urlSuffix))===$urlSuffix) + return substr($pathInfo,0,-strlen($urlSuffix)); + else + return $pathInfo; + } + + /** + * Returns the base URL of the application. + * @return string the base URL of the application (the part after host name and before query string). + * If {@link showScriptName} is true, it will include the script name part. + * Otherwise, it will not, and the ending slashes are stripped off. + */ + public function getBaseUrl() + { + if($this->_baseUrl!==null) + return $this->_baseUrl; + else + { + if($this->showScriptName) + $this->_baseUrl=Yii::app()->getRequest()->getScriptUrl(); + else + $this->_baseUrl=Yii::app()->getRequest()->getBaseUrl(); + return $this->_baseUrl; + } + } + + /** + * Sets the base URL of the application (the part after host name and before query string). + * This method is provided in case the {@link baseUrl} cannot be determined automatically. + * The ending slashes should be stripped off. And you are also responsible to remove the script name + * if you set {@link showScriptName} to be false. + * @param string $value the base URL of the application + * @since 1.1.1 + */ + public function setBaseUrl($value) + { + $this->_baseUrl=$value; + } + + /** + * Returns the URL format. + * @return string the URL format. Defaults to 'path'. Valid values include 'path' and 'get'. + * Please refer to the guide for more details about the difference between these two formats. + */ + public function getUrlFormat() + { + return $this->_urlFormat; + } + + /** + * Sets the URL format. + * @param string $value the URL format. It must be either 'path' or 'get'. + */ + public function setUrlFormat($value) + { + if($value===self::PATH_FORMAT || $value===self::GET_FORMAT) + $this->_urlFormat=$value; + else + throw new CException(Yii::t('yii','CUrlManager.UrlFormat must be either "path" or "get".')); + } +} + + +/** + * CBaseUrlRule is the base class for a URL rule class. + * + * Custom URL rule classes should extend from this class and implement two methods: + * {@link createUrl} and {@link parseUrl}. + * + * @author Qiang Xue + * @version $Id$ + * @package system.web + * @since 1.1.8 + */ +abstract class CBaseUrlRule extends CComponent +{ + /** + * @var boolean whether this rule will also parse the host info part. Defaults to false. + */ + public $hasHostInfo=false; + /** + * Creates a URL based on this rule. + * @param CUrlManager $manager the manager + * @param string $route the route + * @param array $params list of parameters (name=>value) associated with the route + * @param string $ampersand the token separating name-value pairs in the URL. + * @return mixed the constructed URL. False if this rule does not apply. + */ + abstract public function createUrl($manager,$route,$params,$ampersand); + /** + * Parses a URL based on this rule. + * @param CUrlManager $manager the URL manager + * @param CHttpRequest $request the request object + * @param string $pathInfo path info part of the URL (URL suffix is already removed based on {@link CUrlManager::urlSuffix}) + * @param string $rawPathInfo path info that contains the potential URL suffix + * @return mixed the route that consists of the controller ID and action ID. False if this rule does not apply. + */ + abstract public function parseUrl($manager,$request,$pathInfo,$rawPathInfo); +} + +/** + * CUrlRule represents a URL formatting/parsing rule. + * + * It mainly consists of two parts: route and pattern. The former classifies + * the rule so that it only applies to specific controller-action route. + * The latter performs the actual formatting and parsing role. The pattern + * may have a set of named parameters. + * + * @author Qiang Xue + * @version $Id$ + * @package system.web + * @since 1.0 + */ +class CUrlRule extends CBaseUrlRule +{ + /** + * @var string the URL suffix used for this rule. + * For example, ".html" can be used so that the URL looks like pointing to a static HTML page. + * Defaults to null, meaning using the value of {@link CUrlManager::urlSuffix}. + */ + public $urlSuffix; + /** + * @var boolean whether the rule is case sensitive. Defaults to null, meaning + * using the value of {@link CUrlManager::caseSensitive}. + */ + public $caseSensitive; + /** + * @var array the default GET parameters (name=>value) that this rule provides. + * When this rule is used to parse the incoming request, the values declared in this property + * will be injected into $_GET. + */ + public $defaultParams=array(); + /** + * @var boolean whether the GET parameter values should match the corresponding + * sub-patterns in the rule when creating a URL. Defaults to null, meaning using the value + * of {@link CUrlManager::matchValue}. When this property is false, it means + * a rule will be used for creating a URL if its route and parameter names match the given ones. + * If this property is set true, then the given parameter values must also match the corresponding + * parameter sub-patterns. Note that setting this property to true will degrade performance. + * @since 1.1.0 + */ + public $matchValue; + /** + * @var string the HTTP verb (e.g. GET, POST, DELETE) that this rule should match. + * If this rule can match multiple verbs, please separate them with commas. + * If this property is not set, the rule can match any verb. + * Note that this property is only used when parsing a request. It is ignored for URL creation. + * @since 1.1.7 + */ + public $verb; + /** + * @var boolean whether this rule is only used for request parsing. + * Defaults to false, meaning the rule is used for both URL parsing and creation. + * @since 1.1.7 + */ + public $parsingOnly=false; + /** + * @var string the controller/action pair + */ + public $route; + /** + * @var array the mapping from route param name to token name (e.g. _r1=><1>) + */ + public $references=array(); + /** + * @var string the pattern used to match route + */ + public $routePattern; + /** + * @var string regular expression used to parse a URL + */ + public $pattern; + /** + * @var string template used to construct a URL + */ + public $template; + /** + * @var array list of parameters (name=>regular expression) + */ + public $params=array(); + /** + * @var boolean whether the URL allows additional parameters at the end of the path info. + */ + public $append; + /** + * @var boolean whether host info should be considered for this rule + */ + public $hasHostInfo; + + /** + * Constructor. + * @param string $route the route of the URL (controller/action) + * @param string $pattern the pattern for matching the URL + */ + public function __construct($route,$pattern) + { + if(is_array($route)) + { + foreach(array('urlSuffix', 'caseSensitive', 'defaultParams', 'matchValue', 'verb', 'parsingOnly') as $name) + { + if(isset($route[$name])) + $this->$name=$route[$name]; + } + if(isset($route['pattern'])) + $pattern=$route['pattern']; + $route=$route[0]; + } + $this->route=trim($route,'/'); + + $tr2['/']=$tr['/']='\\/'; + + if(strpos($route,'<')!==false && preg_match_all('/<(\w+)>/',$route,$matches2)) + { + foreach($matches2[1] as $name) + $this->references[$name]="<$name>"; + } + + $this->hasHostInfo=!strncasecmp($pattern,'http://',7) || !strncasecmp($pattern,'https://',8); + + if($this->verb!==null) + $this->verb=preg_split('/[\s,]+/',strtoupper($this->verb),-1,PREG_SPLIT_NO_EMPTY); + + if(preg_match_all('/<(\w+):?(.*?)?>/',$pattern,$matches)) + { + $tokens=array_combine($matches[1],$matches[2]); + foreach($tokens as $name=>$value) + { + if($value==='') + $value='[^\/]+'; + $tr["<$name>"]="(?P<$name>$value)"; + if(isset($this->references[$name])) + $tr2["<$name>"]=$tr["<$name>"]; + else + $this->params[$name]=$value; + } + } + $p=rtrim($pattern,'*'); + $this->append=$p!==$pattern; + $p=trim($p,'/'); + $this->template=preg_replace('/<(\w+):?.*?>/','<$1>',$p); + $this->pattern='/^'.strtr($this->template,$tr).'\/'; + if($this->append) + $this->pattern.='/u'; + else + $this->pattern.='$/u'; + + if($this->references!==array()) + $this->routePattern='/^'.strtr($this->route,$tr2).'$/u'; + + if(YII_DEBUG && @preg_match($this->pattern,'test')===false) + throw new CException(Yii::t('yii','The URL pattern "{pattern}" for route "{route}" is not a valid regular expression.', + array('{route}'=>$route,'{pattern}'=>$pattern))); + } + + /** + * Creates a URL based on this rule. + * @param CUrlManager $manager the manager + * @param string $route the route + * @param array $params list of parameters + * @param string $ampersand the token separating name-value pairs in the URL. + * @return mixed the constructed URL or false on error + */ + public function createUrl($manager,$route,$params,$ampersand) + { + if($this->parsingOnly) + return false; + + if($manager->caseSensitive && $this->caseSensitive===null || $this->caseSensitive) + $case=''; + else + $case='i'; + + $tr=array(); + if($route!==$this->route) + { + if($this->routePattern!==null && preg_match($this->routePattern.$case,$route,$matches)) + { + foreach($this->references as $key=>$name) + $tr[$name]=$matches[$key]; + } + else + return false; + } + + foreach($this->defaultParams as $key=>$value) + { + if(isset($params[$key])) + { + if($params[$key]==$value) + unset($params[$key]); + else + return false; + } + } + + foreach($this->params as $key=>$value) + if(!isset($params[$key])) + return false; + + if($manager->matchValue && $this->matchValue===null || $this->matchValue) + { + foreach($this->params as $key=>$value) + { + if(!preg_match('/\A'.$value.'\z/u'.$case,$params[$key])) + return false; + } + } + + foreach($this->params as $key=>$value) + { + $tr["<$key>"]=urlencode($params[$key]); + unset($params[$key]); + } + + $suffix=$this->urlSuffix===null ? $manager->urlSuffix : $this->urlSuffix; + + $url=strtr($this->template,$tr); + + if($this->hasHostInfo) + { + $hostInfo=Yii::app()->getRequest()->getHostInfo(); + if(stripos($url,$hostInfo)===0) + $url=substr($url,strlen($hostInfo)); + } + + if(empty($params)) + return $url!=='' ? $url.$suffix : $url; + + if($this->append) + $url.='/'.$manager->createPathInfo($params,'/','/').$suffix; + else + { + if($url!=='') + $url.=$suffix; + $url.='?'.$manager->createPathInfo($params,'=',$ampersand); + } + + return $url; + } + + /** + * Parses a URL based on this rule. + * @param CUrlManager $manager the URL manager + * @param CHttpRequest $request the request object + * @param string $pathInfo path info part of the URL + * @param string $rawPathInfo path info that contains the potential URL suffix + * @return mixed the route that consists of the controller ID and action ID or false on error + */ + public function parseUrl($manager,$request,$pathInfo,$rawPathInfo) + { + if($this->verb!==null && !in_array($request->getRequestType(), $this->verb, true)) + return false; + + if($manager->caseSensitive && $this->caseSensitive===null || $this->caseSensitive) + $case=''; + else + $case='i'; + + if($this->urlSuffix!==null) + $pathInfo=$manager->removeUrlSuffix($rawPathInfo,$this->urlSuffix); + + // URL suffix required, but not found in the requested URL + if($manager->useStrictParsing && $pathInfo===$rawPathInfo) + { + $urlSuffix=$this->urlSuffix===null ? $manager->urlSuffix : $this->urlSuffix; + if($urlSuffix!='' && $urlSuffix!=='/') + return false; + } + + if($this->hasHostInfo) + $pathInfo=strtolower($request->getHostInfo()).rtrim('/'.$pathInfo,'/'); + + $pathInfo.='/'; + + if(preg_match($this->pattern.$case,$pathInfo,$matches)) + { + foreach($this->defaultParams as $name=>$value) + { + if(!isset($_GET[$name])) + $_REQUEST[$name]=$_GET[$name]=$value; + } + $tr=array(); + foreach($matches as $key=>$value) + { + if(isset($this->references[$key])) + $tr[$this->references[$key]]=$value; + else if(isset($this->params[$key])) + $_REQUEST[$key]=$_GET[$key]=$value; + } + if($pathInfo!==$matches[0]) // there're additional GET params + $manager->parsePathInfo(ltrim(substr($pathInfo,strlen($matches[0])),'/')); + if($this->routePattern!==null) + return strtr($this->route,$tr); + else + return $this->route; + } + else + return false; + } +}