From 05869f23f798c9393e2bc6d310d56a97a11d1acd Mon Sep 17 00:00:00 2001
From: xue <>
Date: Mon, 29 May 2006 02:05:19 +0000
Subject: Added blog demo (not done yet)

---
 demos/blog/index.php                               |  18 +
 demos/blog/protected/Common/BlogDataModule.php     | 535 +++++++++++++++++++++
 demos/blog/protected/Common/BlogErrors.php         |  23 +
 demos/blog/protected/Common/BlogException.php      |  14 +
 demos/blog/protected/Common/BlogPage.php           |  26 +
 demos/blog/protected/Common/BlogUser.php           |  38 ++
 demos/blog/protected/Common/BlogUserManager.php    |  56 +++
 demos/blog/protected/Common/XListMenu.php          | 127 +++++
 demos/blog/protected/Common/messages.txt           |   4 +
 demos/blog/protected/Common/schema.sql             |  70 +++
 demos/blog/protected/Data/Options.xml              |   8 +
 demos/blog/protected/Layouts/MainLayout.php        |   7 +
 demos/blog/protected/Layouts/MainLayout.tpl        |  47 ++
 demos/blog/protected/Pages/Admin/AdminMenu.php     |   7 +
 demos/blog/protected/Pages/Admin/AdminMenu.tpl     |  16 +
 demos/blog/protected/Pages/Admin/ConfigMan.page    |  56 +++
 demos/blog/protected/Pages/Admin/ConfigMan.php     |  15 +
 demos/blog/protected/Pages/Admin/PostMan.page      |  76 +++
 demos/blog/protected/Pages/Admin/PostMan.php       |  56 +++
 demos/blog/protected/Pages/Admin/Settings.page     |   4 +
 demos/blog/protected/Pages/Admin/UserMan.page      |  95 ++++
 demos/blog/protected/Pages/Admin/UserMan.php       |  58 +++
 demos/blog/protected/Pages/Admin/config.xml        |   8 +
 demos/blog/protected/Pages/ErrorReport.page        |  15 +
 demos/blog/protected/Pages/ErrorReport.php         |  12 +
 demos/blog/protected/Pages/Posts/EditCategory.page |  36 ++
 demos/blog/protected/Pages/Posts/EditCategory.php  |  44 ++
 demos/blog/protected/Pages/Posts/EditPost.page     |  41 ++
 demos/blog/protected/Pages/Posts/EditPost.php      |  51 ++
 demos/blog/protected/Pages/Posts/ListPost.page     |  27 ++
 demos/blog/protected/Pages/Posts/ListPost.php      |  44 ++
 demos/blog/protected/Pages/Posts/MyPost.page       |  46 ++
 demos/blog/protected/Pages/Posts/MyPost.php        |  34 ++
 demos/blog/protected/Pages/Posts/NewCategory.page  |  36 ++
 demos/blog/protected/Pages/Posts/NewCategory.php   |  24 +
 demos/blog/protected/Pages/Posts/NewPost.page      |  41 ++
 demos/blog/protected/Pages/Posts/NewPost.php       |  34 ++
 demos/blog/protected/Pages/Posts/ViewPost.page     | 113 +++++
 demos/blog/protected/Pages/Posts/ViewPost.php      |  73 +++
 demos/blog/protected/Pages/Posts/config.xml        |   7 +
 demos/blog/protected/Pages/Users/EditUser.page     |  74 +++
 demos/blog/protected/Pages/Users/EditUser.php      |  43 ++
 demos/blog/protected/Pages/Users/NewUser.page      | 104 ++++
 demos/blog/protected/Pages/Users/NewUser.php       |  31 ++
 demos/blog/protected/Pages/Users/ViewUser.page     |  21 +
 demos/blog/protected/Pages/Users/ViewUser.php      |  19 +
 demos/blog/protected/Pages/Users/config.xml        |   7 +
 demos/blog/protected/Portlets/AccountPortlet.php   |  14 +
 demos/blog/protected/Portlets/AccountPortlet.tpl   |  20 +
 demos/blog/protected/Portlets/ArchivePortlet.php   |  45 ++
 demos/blog/protected/Portlets/ArchivePortlet.tpl   |  15 +
 demos/blog/protected/Portlets/CategoryPortlet.php  |  15 +
 demos/blog/protected/Portlets/CategoryPortlet.tpl  |  24 +
 demos/blog/protected/Portlets/LoginPortlet.php     |  22 +
 demos/blog/protected/Portlets/LoginPortlet.tpl     |  36 ++
 demos/blog/protected/Portlets/Portlet.php          |   7 +
 demos/blog/protected/Portlets/SearchPortlet.php    |  22 +
 demos/blog/protected/Portlets/SearchPortlet.tpl    |  21 +
 demos/blog/protected/application.xml               |  38 ++
 demos/blog/sitemap.txt                             | 106 ++++
 demos/blog/themes/Basic/style.css                  | 261 ++++++++++
 61 files changed, 2987 insertions(+)
 create mode 100644 demos/blog/index.php
 create mode 100644 demos/blog/protected/Common/BlogDataModule.php
 create mode 100644 demos/blog/protected/Common/BlogErrors.php
 create mode 100644 demos/blog/protected/Common/BlogException.php
 create mode 100644 demos/blog/protected/Common/BlogPage.php
 create mode 100644 demos/blog/protected/Common/BlogUser.php
 create mode 100644 demos/blog/protected/Common/BlogUserManager.php
 create mode 100644 demos/blog/protected/Common/XListMenu.php
 create mode 100644 demos/blog/protected/Common/messages.txt
 create mode 100644 demos/blog/protected/Common/schema.sql
 create mode 100644 demos/blog/protected/Data/Options.xml
 create mode 100644 demos/blog/protected/Layouts/MainLayout.php
 create mode 100644 demos/blog/protected/Layouts/MainLayout.tpl
 create mode 100644 demos/blog/protected/Pages/Admin/AdminMenu.php
 create mode 100644 demos/blog/protected/Pages/Admin/AdminMenu.tpl
 create mode 100644 demos/blog/protected/Pages/Admin/ConfigMan.page
 create mode 100644 demos/blog/protected/Pages/Admin/ConfigMan.php
 create mode 100644 demos/blog/protected/Pages/Admin/PostMan.page
 create mode 100644 demos/blog/protected/Pages/Admin/PostMan.php
 create mode 100644 demos/blog/protected/Pages/Admin/Settings.page
 create mode 100644 demos/blog/protected/Pages/Admin/UserMan.page
 create mode 100644 demos/blog/protected/Pages/Admin/UserMan.php
 create mode 100644 demos/blog/protected/Pages/Admin/config.xml
 create mode 100644 demos/blog/protected/Pages/ErrorReport.page
 create mode 100644 demos/blog/protected/Pages/ErrorReport.php
 create mode 100644 demos/blog/protected/Pages/Posts/EditCategory.page
 create mode 100644 demos/blog/protected/Pages/Posts/EditCategory.php
 create mode 100644 demos/blog/protected/Pages/Posts/EditPost.page
 create mode 100644 demos/blog/protected/Pages/Posts/EditPost.php
 create mode 100644 demos/blog/protected/Pages/Posts/ListPost.page
 create mode 100644 demos/blog/protected/Pages/Posts/ListPost.php
 create mode 100644 demos/blog/protected/Pages/Posts/MyPost.page
 create mode 100644 demos/blog/protected/Pages/Posts/MyPost.php
 create mode 100644 demos/blog/protected/Pages/Posts/NewCategory.page
 create mode 100644 demos/blog/protected/Pages/Posts/NewCategory.php
 create mode 100644 demos/blog/protected/Pages/Posts/NewPost.page
 create mode 100644 demos/blog/protected/Pages/Posts/NewPost.php
 create mode 100644 demos/blog/protected/Pages/Posts/ViewPost.page
 create mode 100644 demos/blog/protected/Pages/Posts/ViewPost.php
 create mode 100644 demos/blog/protected/Pages/Posts/config.xml
 create mode 100644 demos/blog/protected/Pages/Users/EditUser.page
 create mode 100644 demos/blog/protected/Pages/Users/EditUser.php
 create mode 100644 demos/blog/protected/Pages/Users/NewUser.page
 create mode 100644 demos/blog/protected/Pages/Users/NewUser.php
 create mode 100644 demos/blog/protected/Pages/Users/ViewUser.page
 create mode 100644 demos/blog/protected/Pages/Users/ViewUser.php
 create mode 100644 demos/blog/protected/Pages/Users/config.xml
 create mode 100644 demos/blog/protected/Portlets/AccountPortlet.php
 create mode 100644 demos/blog/protected/Portlets/AccountPortlet.tpl
 create mode 100644 demos/blog/protected/Portlets/ArchivePortlet.php
 create mode 100644 demos/blog/protected/Portlets/ArchivePortlet.tpl
 create mode 100644 demos/blog/protected/Portlets/CategoryPortlet.php
 create mode 100644 demos/blog/protected/Portlets/CategoryPortlet.tpl
 create mode 100644 demos/blog/protected/Portlets/LoginPortlet.php
 create mode 100644 demos/blog/protected/Portlets/LoginPortlet.tpl
 create mode 100644 demos/blog/protected/Portlets/Portlet.php
 create mode 100644 demos/blog/protected/Portlets/SearchPortlet.php
 create mode 100644 demos/blog/protected/Portlets/SearchPortlet.tpl
 create mode 100644 demos/blog/protected/application.xml
 create mode 100644 demos/blog/sitemap.txt
 create mode 100644 demos/blog/themes/Basic/style.css

(limited to 'demos/blog')

diff --git a/demos/blog/index.php b/demos/blog/index.php
new file mode 100644
index 00000000..43c0b436
--- /dev/null
+++ b/demos/blog/index.php
@@ -0,0 +1,18 @@
+<?php
+
+$basePath=dirname(__FILE__);
+$frameworkPath=$basePath.'/../../framework/prado.php';
+$assetsPath=$basePath.'/assets';
+$runtimePath=$basePath.'/protected/runtime';
+
+if(!is_writable($assetsPath))
+	die("Please make sure that the directory $assetsPath is writable by Web server process.");
+if(!is_writable($runtimePath))
+	die("Please make sure that the directory $runtimePath is writable by Web server process.");
+
+require_once($frameworkPath);
+
+$application=new TApplication;
+$application->run();
+
+?>
\ No newline at end of file
diff --git a/demos/blog/protected/Common/BlogDataModule.php b/demos/blog/protected/Common/BlogDataModule.php
new file mode 100644
index 00000000..714743e7
--- /dev/null
+++ b/demos/blog/protected/Common/BlogDataModule.php
@@ -0,0 +1,535 @@
+<?php
+
+// post status: 0 - draft, 1 - published
+// comment status: 0 - awaiting approval, 1 - published
+class BlogDataModule extends TModule
+{
+	const DB_FILE_EXT='.db';
+	const DEFAULT_DB_FILE='Application.Data.Blog';
+	private $_db=null;
+	private $_dbFile=null;
+
+	public function init($config)
+	{
+		$this->connectDatabase();
+	}
+
+	public function getDbFile()
+	{
+		if($this->_dbFile===null)
+			$this->_dbFile=Prado::getPathOfNamespace(self::DEFAULT_DB_FILE,self::DB_FILE_EXT);
+		return $this->_dbFile;
+	}
+
+	public function setDbFile($value)
+	{
+		if(($this->_dbFile=Prado::getPathOfNamespace($value,self::DB_FILE_EXT))===null)
+			throw new BlogException('blogdatamodule_dbfile_invalid',$value);
+	}
+
+	protected function createDatabase()
+	{
+		$schemaFile=dirname(__FILE__).'/schema.sql';
+		$statements=explode(';',file_get_contents($schemaFile));
+		foreach($statements as $statement)
+		{
+			if(trim($statement)!=='')
+			{
+				if(@sqlite_query($this->_db,$statement)===false)
+					throw new BlogException('blogdatamodule_createdatabase_failed',sqlite_error_string(sqlite_last_error($this->_db)),$statement);
+			}
+		}
+	}
+
+	protected function connectDatabase()
+	{
+		$dbFile=$this->getDbFile();
+		$newDb=!is_file($dbFile);
+		$error='';
+		if(($this->_db=sqlite_open($dbFile,0666,$error))===false)
+			throw new BlogException('blogdatamodule_dbconnect_failed',$error);
+		if($newDb)
+			$this->createDatabase();
+	}
+
+	protected function generateModifier($filter,$orderBy,$limit)
+	{
+		$modifier='';
+		if($filter!=='')
+			$modifier=' WHERE '.$filter;
+		if($orderBy!=='')
+			$modifier.=' ORDER BY '.$orderBy;
+		if($limit!=='')
+			$modifier.=' LIMIT '.$limit;
+		return $modifier;
+	}
+
+	public function query($sql)
+	{
+		if(($result=@sqlite_query($this->_db,$sql))!==false)
+			return $result;
+		else
+			throw new BlogException('blogdatamodule_query_failed',sqlite_error_string(sqlite_last_error($this->_db)),$sql);
+	}
+
+	protected function populateUserRecord($row)
+	{
+		$userRecord=new UserRecord;
+		$userRecord->ID=(integer)$row['id'];
+		$userRecord->Name=$row['name'];
+		$userRecord->FullName=$row['full_name'];
+		$userRecord->Role=(integer)$row['role'];
+		$userRecord->Password=$row['passwd'];
+		$userRecord->VerifyCode=$row['vcode'];
+		$userRecord->Email=$row['email'];
+		$userRecord->CreateTime=(integer)$row['reg_time'];
+		$userRecord->Status=(integer)$row['status'];
+		$userRecord->Website=$row['website'];
+		return $userRecord;
+	}
+
+	public function queryUsers($filter='',$orderBy='',$limit='')
+	{
+		if($filter!=='')
+			$filter='WHERE '.$filter;
+		$sql="SELECT * FROM tblUsers $filter $orderBy $limit";
+		$result=$this->query($sql);
+		$rows=sqlite_fetch_all($result,SQLITE_ASSOC);
+		$users=array();
+		foreach($rows as $row)
+			$users[]=$this->populateUserRecord($row);
+		return $users;
+	}
+
+	public function queryUserCount($filter)
+	{
+		if($filter!=='')
+			$filter='WHERE '.$filter;
+		$sql="SELECT COUNT(id) AS user_count FROM tblUsers $filter";
+		$result=$this->query($sql);
+		if(($row=sqlite_fetch_array($result,SQLITE_ASSOC))!==false)
+			return $row['user_count'];
+		else
+			return 0;
+	}
+
+	public function queryUserByID($id)
+	{
+		$sql="SELECT * FROM tblUsers WHERE id=$id";
+		$result=$this->query($sql);
+		if(($row=sqlite_fetch_array($result,SQLITE_ASSOC))!==false)
+			return $this->populateUserRecord($row);
+		else
+			return null;
+	}
+
+	public function queryUserByName($name)
+	{
+		$name=sqlite_escape_string($name);
+		$sql="SELECT * FROM tblUsers WHERE name='$name'";
+		$result=$this->query($sql);
+		if(($row=sqlite_fetch_array($result,SQLITE_ASSOC))!==false)
+			return $this->populateUserRecord($row);
+		else
+			return null;
+	}
+
+	public function insertUser($user)
+	{
+		$name=sqlite_escape_string($user->Name);
+		$fullName=sqlite_escape_string($user->FullName);
+		$passwd=sqlite_escape_string($user->Password);
+		$email=sqlite_escape_string($user->Email);
+		$website=sqlite_escape_string($user->Website);
+		$createTime=time();
+		$sql="INSERT INTO tblUsers ".
+				"(name,full_name,role,passwd,email,reg_time,website) ".
+				"VALUES ('$name','$fullName',{$user->Role},'$passwd','$email',$createTime,'$website')";
+		$this->query($sql);
+		$user->ID=sqlite_last_insert_rowid($this->_db);
+	}
+
+	public function updateUser($user)
+	{
+		$name=sqlite_escape_string($user->Name);
+		$fullName=sqlite_escape_string($user->FullName);
+		$passwd=sqlite_escape_string($user->Password);
+		$email=sqlite_escape_string($user->Email);
+		$website=sqlite_escape_string($user->Website);
+		$sql="UPDATE tblUsers SET
+				name='$name',
+				full_name='$fullName',
+				role={$user->Role},
+				passwd='$passwd',
+				vcode='{$user->VerifyCode}',
+				email='$email',
+				status={$user->Status},
+				website='$website'
+				WHERE id={$user->ID}";
+		$this->query($sql);
+	}
+
+	public function deleteUser($id)
+	{
+		$this->query("DELETE FROM tblUsers WHERE id=$id");
+	}
+
+	protected function populatePostRecord($row)
+	{
+		$postRecord=new PostRecord;
+		$postRecord->ID=(integer)$row['id'];
+		$postRecord->AuthorID=(integer)$row['author_id'];
+		if($row['author_full_name']!=='')
+			$postRecord->AuthorName=$row['author_full_name'];
+		else
+			$postRecord->AuthorName=$row['author_name'];
+		$postRecord->CreateTime=(integer)$row['create_time'];
+		$postRecord->ModifyTime=(integer)$row['modify_time'];
+		$postRecord->Title=$row['title'];
+		$postRecord->Content=$row['content'];
+		$postRecord->Status=(integer)$row['status'];
+		$postRecord->CommentCount=(integer)$row['comment_count'];
+		return $postRecord;
+	}
+
+	public function queryPosts($authorFilter,$timeFilter,$categoryFilter,$orderBy,$limit)
+	{
+		$filter='';
+		if($authorFilter!=='')
+			$filter.=" AND $authorFilter";
+		if($timeFilter!=='')
+			$filter.=" AND $timeFilter";
+		if($categoryFilter!=='')
+			$filter.=" AND a.id IN (SELECT post_id AS id FROM tblPost2Category WHERE $categoryFilter)";
+		$sql="SELECT a.id AS id,
+					a.author_id AS author_id,
+					b.name AS author_name,
+					b.full_name AS author_full_name,
+					a.create_time AS create_time,
+					a.modify_time AS modify_time,
+					a.title AS title,
+					a.content AS content,
+					a.status AS status,
+					a.comment_count AS comment_count
+				FROM tblPosts a, tblUsers b
+				WHERE a.author_id=b.id $filter $orderBy $limit";
+		$result=$this->query($sql);
+		$rows=sqlite_fetch_all($result,SQLITE_ASSOC);
+		$posts=array();
+		foreach($rows as $row)
+			$posts[]=$this->populatePostRecord($row);
+		return $posts;
+	}
+
+	public function queryPostCount($authorFilter,$timeFilter,$categoryFilter)
+	{
+		$filter='';
+		if($authorFilter!=='')
+			$filter.=" AND $authorFilter";
+		if($timeFilter!=='')
+			$filter.=" AND $timeFilter";
+		if($categoryFilter!=='')
+			$filter.=" AND a.id IN (SELECT post_id AS id FROM tblPost2Category WHERE $categoryFilter)";
+		$sql="SELECT COUNT(a.id) AS post_count
+				FROM tblPosts a, tblUsers b
+				WHERE a.author_id=b.id $filter";
+		$result=$this->query($sql);
+		if(($row=sqlite_fetch_array($result,SQLITE_ASSOC))!==false)
+			return $row['post_count'];
+		else
+			return 0;
+	}
+
+	public function queryPostByID($id)
+	{
+		$sql="SELECT a.id AS id,
+		             a.author_id AS author_id,
+		             b.name AS author_name,
+		             b.full_name AS author_full_name,
+		             a.create_time AS create_time,
+		             a.modify_time AS modify_time,
+		             a.title AS title,
+		             a.content AS content,
+		             a.status AS status,
+		             a.comment_count AS comment_count
+		      FROM tblPosts a, tblUsers b
+		      WHERE a.id=$id AND a.author_id=b.id";
+		$result=$this->query($sql);
+		if(($row=sqlite_fetch_array($result,SQLITE_ASSOC))!==false)
+			return $this->populatePostRecord($row);
+		else
+			return null;
+	}
+
+	public function insertPost($post,$catIDs)
+	{
+		$title=sqlite_escape_string($post->Title);
+		$content=sqlite_escape_string($post->Content);
+		$sql="INSERT INTO tblPosts
+				(author_id,create_time,title,content,status)
+				VALUES ({$post->AuthorID},{$post->CreateTime},'$title','$content',{$post->Status})";
+		$this->query($sql);
+		$post->ID=sqlite_last_insert_rowid($this->_db);
+		foreach($catIDs as $catID)
+			$this->insertPostCategory($post->ID,$catID);
+	}
+
+	public function updatePost($post,$newCatIDs=null)
+	{
+		if($newCatIDs!==null)
+		{
+			$cats=$this->queryCategoriesByPostID($post->ID);
+			$catIDs=array();
+			foreach($cats as $cat)
+				$catIDs[]=$cat->ID;
+			$deleteIDs=array_diff($catIDs,$newCatIDs);
+			foreach($deleteIDs as $id)
+				$this->deletePostCategory($post->ID,$id);
+			$insertIDs=array_diff($newCatIDs,$catIDs);
+			foreach($insertIDs as $id)
+				$this->insertPostCategory($post->ID,$id);
+		}
+
+		$title=sqlite_escape_string($post->Title);
+		$content=sqlite_escape_string($post->Content);
+		$sql="UPDATE tblPosts SET
+				modify_time={$post->ModifyTime},
+				title='$title',
+				content='$content',
+				status={$post->Status}
+				WHERE id={$post->ID}";
+		$this->query($sql);
+	}
+
+	public function deletePost($id)
+	{
+		$cats=$this->queryCategoriesByPostID($id);
+		foreach($cats as $cat)
+			$this->deletePostCategory($id,$cat->ID);
+		$this->query("DELETE FROM tblComments WHERE post_id=$id");
+		$this->query("DELETE FROM tblPosts WHERE id=$id");
+	}
+
+	protected function populateCommentRecord($row)
+	{
+		$commentRecord=new CommentRecord;
+		$commentRecord->ID=(integer)$row['id'];
+		$commentRecord->PostID=(integer)$row['post_id'];
+		$commentRecord->AuthorName=$row['author_name'];
+		$commentRecord->AuthorEmail=$row['author_email'];
+		$commentRecord->AuthorWebsite=$row['author_website'];
+		$commentRecord->AuthorIP=$row['author_ip'];
+		$commentRecord->CreateTime=(integer)$row['create_time'];
+		$commentRecord->Content=$row['content'];
+		$commentRecord->Status=(integer)$row['status'];
+		return $commentRecord;
+	}
+
+	public function queryCommentsByPostID($id)
+	{
+		$sql="SELECT * FROM tblComments WHERE post_id=$id";
+		$result=$this->query($sql);
+		$rows=sqlite_fetch_all($result,SQLITE_ASSOC);
+		$comments=array();
+		foreach($rows as $row)
+			$comments[]=$this->populateCommentRecord($row);
+		return $comments;
+	}
+
+	public function insertComment($comment)
+	{
+		$authorName=sqlite_escape_string($comment->AuthorName);
+		$authorEmail=sqlite_escape_string($comment->AuthorEmail);
+		$authorWebsite=sqlite_escape_string($comment->AuthorWebsite);
+		$content=sqlite_escape_string($comment->Content);
+		$sql="INSERT INTO tblComments
+				(post_id,author_name,author_email,author_website,author_ip,create_time,status,content)
+				VALUES ({$comment->PostID},'$authorName','$authorEmail','$authorWebsite','{$comment->AuthorIP}',{$comment->CreateTime},{$comment->Status},'$content')";
+		$this->query($sql);
+		$comment->ID=sqlite_last_insert_rowid($this->_db);
+		$this->query("UPDATE tblPosts SET comment_count=comment_count+1 WHERE id={$comment->PostID}");
+	}
+
+	public function updateComment($comment)
+	{
+		$authorName=sqlite_escape_string($comment->AuthorName);
+		$authorEmail=sqlite_escape_string($comment->AuthorEmail);
+		$content=sqlite_escape_string($comment->Content);
+		$sql="UPDATE tblComments SET status={$comment->Status} WHERE id={$comment->ID}";
+		$this->query($sql);
+	}
+
+	public function deleteComment($id)
+	{
+		$result=$this->query("SELECT post_id FROM tblComments WHERE id=$id");
+		if(($row=sqlite_fetch_array($result,SQLITE_ASSOC))!==false)
+		{
+			$postID=$row['post_id'];
+			$this->query("DELETE FROM tblComments WHERE id=$id");
+			$this->query("UPDATE tblPosts SET comment_count=comment_count-1 WHERE id=$postID");
+		}
+	}
+
+	protected function populateCategoryRecord($row)
+	{
+		$catRecord=new CategoryRecord;
+		$catRecord->ID=(integer)$row['id'];
+		$catRecord->Name=$row['name'];
+		$catRecord->Description=$row['description'];
+		$catRecord->PostCount=$row['post_count'];
+		return $catRecord;
+	}
+
+	public function queryCategories()
+	{
+		$sql="SELECT * FROM tblCategories";
+		$result=$this->query($sql);
+		$rows=sqlite_fetch_all($result,SQLITE_ASSOC);
+		$cats=array();
+		foreach($rows as $row)
+			$cats[]=$this->populateCategoryRecord($row);
+		return $cats;
+	}
+
+	public function queryCategoriesByPostID($postID)
+	{
+		$sql="SELECT a.id AS id,
+				a.name AS name,
+				a.description AS description,
+				a.post_count AS post_count
+				FROM tblCategories a, tblPost2Category b
+				WHERE a.id=b.category_id AND b.post_id=$postID";
+		$result=$this->query($sql);
+		$rows=sqlite_fetch_all($result,SQLITE_ASSOC);
+		$cats=array();
+		foreach($rows as $row)
+			$cats[]=$this->populateCategoryRecord($row);
+		return $cats;
+	}
+
+	public function queryCategoryByID($id)
+	{
+		$sql="SELECT * FROM tblCategories WHERE id=$id";
+		$result=$this->query($sql);
+		if(($row=sqlite_fetch_array($result,SQLITE_ASSOC))!==false)
+			return $this->populateCategoryRecord($row);
+		else
+			return null;
+	}
+
+	public function queryCategoryByName($name)
+	{
+		$name=sqlite_escape_string($name);
+		$sql="SELECT * FROM tblCategories WHERE name='$name'";
+		$result=$this->query($sql);
+		if(($row=sqlite_fetch_array($result,SQLITE_ASSOC))!==false)
+			return $this->populateCategoryRecord($row);
+		else
+			return null;
+	}
+
+	public function insertCategory($category)
+	{
+		$name=sqlite_escape_string($category->Name);
+		$description=sqlite_escape_string($category->Description);
+		$sql="INSERT INTO tblCategories
+				(name,description)
+				VALUES ('$name','$description')";
+		$this->query($sql);
+		$category->ID=sqlite_last_insert_rowid($this->_db);
+	}
+
+	public function updateCategory($category)
+	{
+		$name=sqlite_escape_string($category->Name);
+		$description=sqlite_escape_string($category->Description);
+		$sql="UPDATE tblCategories SET name='$name', description='$description', post_count={$category->PostCount} WHERE id={$category->ID}";
+		$this->query($sql);
+	}
+
+	public function deleteCategory($id)
+	{
+		$sql="DELETE FROM tblPost2Category WHERE category_id=$id";
+		$this->query($sql);
+		$sql="DELETE FROM tblCategories WHERE id=$id";
+		$this->query($sql);
+	}
+
+	public function insertPostCategory($postID,$categoryID)
+	{
+		$sql="INSERT INTO tblPost2Category (post_id, category_id) VALUES ($postID, $categoryID)";
+		$this->query($sql);
+		$sql="UPDATE tblCategories SET post_count=post_count+1 WHERE id=$categoryID";
+		$this->query($sql);
+	}
+
+	public function deletePostCategory($postID,$categoryID)
+	{
+		$sql="DELETE FROM tblPost2Category WHERE post_id=$postID AND category_id=$categoryID";
+		if($this->query($sql)>0)
+		{
+			$sql="UPDATE tblCategories SET post_count=post_count-1 WHERE id=$categoryID";
+			$this->query($sql);
+		}
+	}
+
+	public function queryEarliestPostTime()
+	{
+		$sql="SELECT MIN(create_time) AS create_time FROM tblPosts";
+		$result=$this->query($sql);
+		if(($row=sqlite_fetch_array($result,SQLITE_ASSOC))!==false)
+			return $row['create_time'];
+		else
+			return time();
+	}
+}
+
+class UserRecord
+{
+	public $ID;
+	public $Name;
+	public $FullName;
+	public $Role;
+	public $Password;
+	public $VerifyCode;
+	public $Email;
+	public $CreateTime;
+	public $Status;
+	public $Website;
+}
+
+class PostRecord
+{
+	public $ID;
+	public $AuthorID;
+	public $AuthorName;
+	public $CreateTime;
+	public $ModifyTime;
+	public $Title;
+	public $Content;
+	public $Status;
+	public $CommentCount;
+}
+
+class CommentRecord
+{
+	public $ID;
+	public $PostID;
+	public $AuthorName;
+	public $AuthorEmail;
+	public $AuthorWebsite;
+	public $AuthorIP;
+	public $CreateTime;
+	public $Status;
+	public $Content;
+}
+
+class CategoryRecord
+{
+	public $ID;
+	public $Name;
+	public $Description;
+	public $PostCount;
+}
+
+?>
\ No newline at end of file
diff --git a/demos/blog/protected/Common/BlogErrors.php b/demos/blog/protected/Common/BlogErrors.php
new file mode 100644
index 00000000..501ec1c9
--- /dev/null
+++ b/demos/blog/protected/Common/BlogErrors.php
@@ -0,0 +1,23 @@
+<?php
+
+class BlogErrors
+{
+	const ERROR_UKNOWN=0;
+	const ERROR_POST_NOT_FOUND=1;
+	const ERROR_USER_NOT_FOUND=2;
+	const ERROR_PERMISSION_DENIED=3;
+
+	private static $_errorMessages=array(
+		self::ERROR_UKNOWN=>'Unknown error.',
+		self::ERROR_POST_NOT_FOUND=>'The specified post cannot be found.',
+		self::ERROR_USER_NOT_FOUND=>'The specified user account cannot be found.',
+		self::ERROR_PERMISSION_DENIED=>'Sorry, you do not have permission to perform this action.',
+	);
+
+	public static function getMessage($errorCode)
+	{
+		return isset(self::$_errorMessages[$errorCode])?self::$_errorMessages[$errorCode]:self::$_errorMessages[0];
+	}
+}
+
+?>
\ No newline at end of file
diff --git a/demos/blog/protected/Common/BlogException.php b/demos/blog/protected/Common/BlogException.php
new file mode 100644
index 00000000..ab8020d1
--- /dev/null
+++ b/demos/blog/protected/Common/BlogException.php
@@ -0,0 +1,14 @@
+<?php
+
+class BlogException extends TApplicationException
+{
+	/**
+	 * @return string path to the error message file
+	 */
+	protected function getErrorMessageFile()
+	{
+		return dirname(__FILE__).'/messages.txt';
+	}
+}
+
+?>
\ No newline at end of file
diff --git a/demos/blog/protected/Common/BlogPage.php b/demos/blog/protected/Common/BlogPage.php
new file mode 100644
index 00000000..f1634a80
--- /dev/null
+++ b/demos/blog/protected/Common/BlogPage.php
@@ -0,0 +1,26 @@
+<?php
+
+class BlogPage extends TPage
+{
+	public function getDataAccess()
+	{
+		return $this->getApplication()->getModule('data');
+	}
+
+	public function gotoDefaultPage()
+	{
+		$this->Response->redirect($this->Service->constructUrl($this->Service->DefaultPage));
+	}
+
+	public function gotoPage($pagePath,$getParameters=null)
+	{
+		$this->Response->redirect($this->Service->constructUrl($pagePath,$getParameters));
+	}
+
+	public function reportError($errorCode)
+	{
+		$this->gotoPage('ErrorReport',array('id'=>$errorCode));
+	}
+}
+
+?>
\ No newline at end of file
diff --git a/demos/blog/protected/Common/BlogUser.php b/demos/blog/protected/Common/BlogUser.php
new file mode 100644
index 00000000..af49c8d7
--- /dev/null
+++ b/demos/blog/protected/Common/BlogUser.php
@@ -0,0 +1,38 @@
+<?php
+
+Prado::using('System.Security.TUser');
+
+class BlogUser extends TUser
+{
+	private $_id;
+
+	public function getID()
+	{
+		return $this->_id;
+	}
+
+	public function setID($value)
+	{
+		$this->_id=$value;
+	}
+
+	public function saveToString()
+	{
+		$a=array($this->_id,parent::saveToString());
+		return serialize($a);
+	}
+
+	public function loadFromString($data)
+	{
+		if(!empty($data))
+		{
+			list($id,$str)=unserialize($data);
+			$this->_id=$id;
+			return parent::loadFromString($str);
+		}
+		else
+			return $this;
+	}
+}
+
+?>
\ No newline at end of file
diff --git a/demos/blog/protected/Common/BlogUserManager.php b/demos/blog/protected/Common/BlogUserManager.php
new file mode 100644
index 00000000..c3ddb80b
--- /dev/null
+++ b/demos/blog/protected/Common/BlogUserManager.php
@@ -0,0 +1,56 @@
+<?php
+
+Prado::using('System.Security.IUserManager');
+Prado::using('Application.Common.BlogUser');
+
+class BlogUserManager extends TModule implements IUserManager
+{
+	public function getGuestName()
+	{
+		return 'Guest';
+	}
+
+	/**
+	 * Returns a user instance given the user name.
+	 * @param string user name, null if it is a guest.
+	 * @return TUser the user instance, null if the specified username is not in the user database.
+	 */
+	public function getUser($username=null)
+	{
+		if($username===null)
+			return new BlogUser($this);
+		else
+		{
+			$username=strtolower($username);
+			$db=$this->Application->getModule('data');
+			if(($userRecord=$db->queryUserByName($username))!==null)
+			{
+				$user=new BlogUser($this);
+				$user->setID($userRecord->ID);
+				$user->setName($username);
+				$user->setIsGuest(false);
+				$user->setRoles($userRecord->Role===0?'user':'admin');
+				return $user;
+			}
+			else
+				return null;
+		}
+	}
+
+	/**
+	 * Validates if the username and password are correct.
+	 * @param string user name
+	 * @param string password
+	 * @return boolean true if validation is successful, false otherwise.
+	 */
+	public function validateUser($username,$password)
+	{
+		$db=$this->Application->getModule('data');
+		if(($userRecord=$db->queryUserByName($username))!==null)
+			return $userRecord->Password===md5($password);
+		else
+			return false;
+	}
+}
+
+?>
\ No newline at end of file
diff --git a/demos/blog/protected/Common/XListMenu.php b/demos/blog/protected/Common/XListMenu.php
new file mode 100644
index 00000000..f8223585
--- /dev/null
+++ b/demos/blog/protected/Common/XListMenu.php
@@ -0,0 +1,127 @@
+<?php
+/**
+ * XListMenu and XListMenuItem class file
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @link http://www.pradosoft.com/
+ * @copyright Copyright &copy; 2006 PradoSoft
+ * @license http://www.pradosoft.com/license/
+ * @version $Revision: $  $Date: $
+ */
+
+Prado::using('System.Web.UI.WebControls.TListControl');
+
+/**
+ * XListMenu class
+ *
+ * XListMenu displays a list of hyperlinks that can be used for page menus.
+ * Menu items adjust their css class automatically according to the current
+ * page displayed. In particular, a menu item is considered as active if
+ * the URL it represents is for the page currently displayed.
+ *
+ * Usage of XListMenu is similar to PRADO list controls. Each list item has
+ * two extra properties: {@link XListMenuItem::setPagePath PagePath} and
+ * {@link XListMenuItem::setNavigateUrl NavigateUrl}. The former is used to
+ * determine if the item is active or not, while the latter specifies the
+ * URL for the item. If the latter is not specified, a URL to the page is
+ * generated automatically.
+ *
+ * In template, you may use the following tags to specify a menu:
+ * <code>
+ *   <com:XListMenu ActiveCssClass="class1" InactiveCssClass="class2">
+ *      <com:XListMenuItem Text="Menu 1" PagePath="Page1" />
+ *      <com:XListMenuItem Text="Menu 2" PagePath="Page2" NavigateUrl="/page2" />
+ *   </com:XListMenu>
+ * </code>
+ *
+ * @author Qiang Xue <qiang.xue@gmail.com>
+ * @link http://www.pradosoft.com/
+ * @copyright Copyright &copy; 2006 PradoSoft
+ * @license http://www.pradosoft.com/license/
+ */
+class XListMenu extends TListControl
+{
+	public function addParsedObject($object)
+	{
+		if($object instanceof XListMenuItem)
+			parent::addParsedObject($object);
+	}
+
+	public function getActiveCssClass()
+	{
+		return $this->getViewState('ActiveCssClass','');
+	}
+
+	public function setActiveCssClass($value)
+	{
+		$this->setViewState('ActiveCssClass',$value,'');
+	}
+
+	public function getInactiveCssClass()
+	{
+		return $this->getViewState('InactiveCssClass','');
+	}
+
+	public function setInactiveCssClass($value)
+	{
+		$this->setViewState('InactiveCssClass',$value,'');
+	}
+
+	public function render($writer)
+	{
+		if(($activeClass=$this->getActiveCssClass())!=='')
+			$activeClass=' class="'.$activeClass.'"';
+		if(($inactiveClass=$this->getInactiveCssClass())!=='')
+			$inactiveClass=' class="'.$inactiveClass.'"';
+		$currentPagePath=$this->getPage()->getPagePath();
+		$writer->write("<ul>\n");
+		foreach($this->getItems() as $item)
+		{
+			$pagePath=$item->getPagePath();
+			//if(strpos($currentPagePath.'.',$pagePath.'.')===0)
+			if($pagePath[strlen($pagePath)-1]==='*')
+			{
+				if(strpos($currentPagePath.'.',rtrim($pagePath,'*'))===0)
+					$cssClass=$activeClass;
+				else
+					$cssClass=$inactiveClass;
+			}
+			else
+			{
+				if($pagePath===$currentPagePath)
+					$cssClass=$activeClass;
+				else
+					$cssClass=$inactiveClass;
+			}
+			if(($url=$item->getNavigateUrl())==='')
+				$url=$this->getService()->constructUrl($pagePath);
+			$writer->write("<li><a href=\"$url\"$cssClass>".$item->getText()."</a></li>\n");
+		}
+		$writer->write("</ul>");
+	}
+}
+
+class XListMenuItem extends TListItem
+{
+	public function getPagePath()
+	{
+		return $this->getValue();
+	}
+
+	public function setPagePath($value)
+	{
+		$this->setValue($value);
+	}
+
+	public function getNavigateUrl()
+	{
+		return $this->hasAttribute('NavigateUrl')?$this->getAttribute('NavigateUrl'):'';
+	}
+
+	public function setNavigateUrl($value)
+	{
+		$this->setAttribute('NavigateUrl',$value);
+	}
+}
+
+?>
\ No newline at end of file
diff --git a/demos/blog/protected/Common/messages.txt b/demos/blog/protected/Common/messages.txt
new file mode 100644
index 00000000..deb15ee3
--- /dev/null
+++ b/demos/blog/protected/Common/messages.txt
@@ -0,0 +1,4 @@
+blogdatamodule_dbconnect_failed			= Unable to connect to database: {0}
+blogdatamodule_dbfile_invalid			= BlogDataModule.DbFile='{0}' is invalid.
+blogdatamodule_createdatabase_failed	= BlogDataModule failed to create database when executing SQL: {1}. Last SQL error is: {0}.
+blogdatamodule_query_failed				= Failed to execute SQL: {1}. Last SQL error is: {0}.
\ No newline at end of file
diff --git a/demos/blog/protected/Common/schema.sql b/demos/blog/protected/Common/schema.sql
new file mode 100644
index 00000000..49f6f429
--- /dev/null
+++ b/demos/blog/protected/Common/schema.sql
@@ -0,0 +1,70 @@
+CREATE TABLE tblUsers (
+  id			INTEGER NOT NULL PRIMARY KEY,
+  name			VARCHAR(128) NOT NULL UNIQUE,
+  full_name		VARCHAR(128) DEFAULT '',
+  role          INTEGER NOT NULL DEFAULT 0, /* 0: user, 1: admin */
+  passwd		VARCHAR(128) NOT NULL,
+  vcode			VARCHAR(128) DEFAULT '',
+  email		    VARCHAR(128) NOT NULL,
+  reg_time		INTEGER NOT NULL,
+  status		INTEGER NOT NULL DEFAULT 0, /* 0: normal, 1: disabled, 2: pending approval */
+  website	    VARCHAR(128) DEFAULT ''
+);
+
+CREATE TABLE tblPosts (
+  id			INTEGER NOT NULL PRIMARY KEY,
+  author_id		INTEGER NOT NULL,
+  create_time	INTEGER NOT NULL,
+  modify_time	INTEGER DEFAULT 0,
+  title			VARCHAR(256) NOT NULL,
+  content		TEXT NOT NULL,
+  status		INTEGER NOT NULL DEFAULT 0, /* 0: published, 1: draft, 2: pending approval */
+  comment_count	INTEGER NOT NULL DEFAULT 0
+);
+
+CREATE TABLE tblComments (
+  id			INTEGER NOT NULL PRIMARY KEY,
+  post_id		INTEGER NOT NULL,
+  author_name	VARCHAR(64) NOT NULL,
+  author_email	VARCHAR(128) NOT NULL,
+  author_website VARCHAR(128) DEFAULT '',
+  author_ip		CHAR(16) NOT NULL,
+  create_time	INTEGER NOT NULL,
+  status		INTEGER NOT NULL DEFAULT 0, /* 0: published, 1: pending approval */
+  content		TEXT NOT NULL
+);
+
+CREATE TABLE tblCategories (
+  id			INTEGER NOT NULL PRIMARY KEY,
+  name			VARCHAR(128) NOT NULL UNIQUE,
+  description	TEXT DEFAULT '',
+  post_count	INTEGER NOT NULL DEFAULT 0
+);
+
+CREATE TABLE tblAttachments (
+  id			VARCHAR(128) NOT NULL PRIMARY KEY,
+  post_id		INTEGER NOT NULL,
+  create_time	INTEGER NOT NULL,
+  file_name		VARCHAR(128) NOT NULL,
+  file_size		INTEGER NOT NULL,
+  mime_type		VARCHAR(32) NOT NULL DEFAULT 'text/html',
+  download_count INTEGER NOT NULL DEFAULT 0
+);
+
+CREATE TABLE tblPost2Category (
+  post_id		INTEGER NOT NULL,
+  category_id	INTEGER NOT NULL,
+  PRIMARY KEY (post_id, category_id)
+);
+
+INSERT INTO tblUsers (id,name,full_name,role,status,passwd,email,reg_time,website)
+	VALUES (1,'admin','Prado User',1,0,'4d688da592969d0a56b5accec3ce8554','admin@example.com',1148819681,'http://www.pradosoft.com');
+
+INSERT INTO tblPosts (id,author_id,create_time,title,content,status)
+	VALUES (1,1,1148819691,'Welcome to Prado Weblog','Congratulations! You have successfully installed Prado Weblog. An administrator account has been created. Please login with <b>admin/prado</b> and update your password as soon as possible.',0);
+
+INSERT INTO tblCategories (name,description,post_count)
+	VALUES ('Miscellaneous','This category holds posts on any topic.',1);
+
+INSERT INTO tblPost2Category (post_id,category_id)
+	VALUES (1,1);
diff --git a/demos/blog/protected/Data/Options.xml b/demos/blog/protected/Data/Options.xml
new file mode 100644
index 00000000..02e51a98
--- /dev/null
+++ b/demos/blog/protected/Data/Options.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<parameters>
+	<parameter id="SiteTitle" value="Qiang's Blog" />
+	<parameter id="SiteSubtitle" value="A PRADO-driven weblog" />
+	<parameter id="SiteOwner" value="Qiang Xue" />
+	<parameter id="AdminEmail" value="admin@example.com" />
+</parameters>
\ No newline at end of file
diff --git a/demos/blog/protected/Layouts/MainLayout.php b/demos/blog/protected/Layouts/MainLayout.php
new file mode 100644
index 00000000..253d6c03
--- /dev/null
+++ b/demos/blog/protected/Layouts/MainLayout.php
@@ -0,0 +1,7 @@
+<?php
+
+class MainLayout extends TTemplateControl
+{
+}
+
+?>
\ No newline at end of file
diff --git a/demos/blog/protected/Layouts/MainLayout.tpl b/demos/blog/protected/Layouts/MainLayout.tpl
new file mode 100644
index 00000000..f171de7f
--- /dev/null
+++ b/demos/blog/protected/Layouts/MainLayout.tpl
@@ -0,0 +1,47 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" >
+
+<com:THead Title=<%$ SiteName %> >
+<meta http-equiv="Expires" content="Fri, Jan 01 1900 00:00:00 GMT"/>
+<meta http-equiv="Pragma" content="no-cache"/>
+<meta http-equiv="Cache-Control" content="no-cache"/>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+<meta http-equiv="content-language" content="en"/>
+</com:THead>
+
+<body>
+<div id="page">
+<com:TForm>
+
+<div id="header">
+<h1 id="header-title"><%$ SiteTitle %></h1>
+<h2 id="header-subtitle"><%$ SiteSubtitle %></h2>
+</div><!-- end of header -->
+
+<div id="main">
+<com:TContentPlaceHolder ID="Main" />
+</div><!-- end of main -->
+
+<div id="sidebar">
+
+<com:Application.Portlets.LoginPortlet Visible=<%= $this->User->IsGuest %>/>
+
+<com:Application.Portlets.AccountPortlet Visible=<%= !$this->User->IsGuest %>/>
+
+<com:Application.Portlets.SearchPortlet />
+
+<com:Application.Portlets.CategoryPortlet />
+
+<com:Application.Portlets.ArchivePortlet />
+
+</div><!-- end of sidebar -->
+
+<div id="footer">
+Copyright &copy; 2006 <%$ SiteOwner %>.<br/>
+<%= Prado::poweredByPrado() %>
+</div><!-- end of footer -->
+
+</com:TForm>
+</div><!-- end of page -->
+</body>
+</html>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/Admin/AdminMenu.php b/demos/blog/protected/Pages/Admin/AdminMenu.php
new file mode 100644
index 00000000..40f40b88
--- /dev/null
+++ b/demos/blog/protected/Pages/Admin/AdminMenu.php
@@ -0,0 +1,7 @@
+<?php
+
+class AdminMenu extends TTemplateControl
+{
+}
+
+?>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/Admin/AdminMenu.tpl b/demos/blog/protected/Pages/Admin/AdminMenu.tpl
new file mode 100644
index 00000000..596f3ed2
--- /dev/null
+++ b/demos/blog/protected/Pages/Admin/AdminMenu.tpl
@@ -0,0 +1,16 @@
+<div class="submenu">
+<com:XListMenu ActiveCssClass="submenu-active" InactiveCssClass="submenu-inactive">
+	<com:XListMenuItem
+		Text="Posts"
+		PagePath="Admin.PostMan"
+		NavigateUrl=<%= $this->Service->constructUrl('Admin.PostMan') %> />
+	<com:XListMenuItem
+		Text="Users"
+		PagePath="Admin.UserMan"
+		NavigateUrl=<%= $this->Service->constructUrl('Admin.UserMan') %> />
+	<com:XListMenuItem
+		Text="Configurations"
+		PagePath="Admin.ConfigMan"
+		NavigateUrl=<%= $this->Service->constructUrl('Admin.ConfigMan') %> />
+</com:XListMenu>
+</div>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/Admin/ConfigMan.page b/demos/blog/protected/Pages/Admin/ConfigMan.page
new file mode 100644
index 00000000..ad728284
--- /dev/null
+++ b/demos/blog/protected/Pages/Admin/ConfigMan.page
@@ -0,0 +1,56 @@
+<com:TContent ID="Main">
+
+<h2>Administration Center</h2>
+
+<com:Application.Pages.Admin.AdminMenu />
+
+<com:TPanel GroupingText="Site settings">
+
+<span class="input-label">Title</span>
+<br/>
+<com:TTextBox ID="SiteTitle" />
+<br/>
+
+<span class="input-label">Subtitle</span>
+<br/>
+<com:TTextBox ID="SiteSubtitle" />
+<br/>
+
+<span class="input-label">Owner name</span>
+<br/>
+<com:TTextBox ID="SiteOwner" />
+<br/>
+
+<span class="input-label">Owner email</span>
+<br/>
+<com:TTextBox ID="AdminEmail" />
+<br/>
+
+<span class="input-label">Site theme</span>
+<br/>
+<com:TDropDownList ID="ThemeList" />
+<br/>
+
+</com:TPanel>
+
+
+<com:TPanel GroupingText="Account settings">
+
+<com:TCheckBox ID="MultipleUser" Text="Allow multiple users" />
+<br/>
+
+<com:TCheckBox ID="AccountApproval" Text="New accounts need approval" />
+<br/>
+
+</com:TPanel>
+
+<com:TPanel GroupingText="Post settings">
+
+<com:TCheckBox ID="PostApproval" Text="New posts need approval" />
+<br/>
+
+</com:TPanel>
+
+<com:TLinkButton Text="Save" OnClick="saveButtonClicked" />
+
+</com:TContent>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/Admin/ConfigMan.php b/demos/blog/protected/Pages/Admin/ConfigMan.php
new file mode 100644
index 00000000..dcbe1537
--- /dev/null
+++ b/demos/blog/protected/Pages/Admin/ConfigMan.php
@@ -0,0 +1,15 @@
+<?php
+
+class ConfigMan extends BlogPage
+{
+	public function onLoad($param)
+	{
+		parent::onLoad($param);
+	}
+
+	public function saveButtonClicked($sender,$param)
+	{
+	}
+}
+
+?>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/Admin/PostMan.page b/demos/blog/protected/Pages/Admin/PostMan.page
new file mode 100644
index 00000000..8ba8ef29
--- /dev/null
+++ b/demos/blog/protected/Pages/Admin/PostMan.page
@@ -0,0 +1,76 @@
+<com:TContent ID="Main">
+
+<h2>Administration Center</h2>
+
+<com:Application.Pages.Admin.AdminMenu />
+
+<com:TDataGrid ID="PostGrid"
+	AutoGenerateColumns="false"
+	DataKeyField="ID"
+	CssClass="grid"
+	HeaderStyle.CssClass="grid-header"
+	ItemStyle.CssClass="grid-row1"
+	SelectedItemStyle.CssClass="grid-row-selected"
+	AlternatingItemStyle.CssClass="grid-row2"
+	AllowPaging="true"
+	AllowCustomPaging="true"
+	PageSize="20"
+	PagerStyle.CssClass="grid-pager"
+	PagerStyle.Mode="Numeric"
+	OnPageIndexChanged="changePage"
+	OnPagerCreated="pagerCreated"
+	OnEditCommand="editItem"
+	OnUpdateCommand="saveItem"
+	OnCancelCommand="cancelItem"
+	>
+	<com:THyperLinkColumn
+		HeaderText="Title"
+		DataNavigateUrlField="ID"
+		DataNavigateUrlFormatString="#$this->Service->constructUrl('Posts.ViewPost',array('id'=>{0}))"
+		DataTextField="Title"
+		/>
+	<com:THyperLinkColumn
+		HeaderText="Author"
+		DataNavigateUrlField="AuthorID"
+		DataNavigateUrlFormatString="#$this->Service->constructUrl('Users.ViewUser',array('id'=>{0}))"
+		DataTextField="AuthorName"
+		/>
+	<com:TTemplateColumn
+		HeaderText="Status"
+		ItemStyle.HorizontalAlign="Center"
+		ItemStyle.Width="90px" >
+		<prop:ItemTemplate>
+		<%#
+			$this->Parent->DataItem->Status===0 ?
+				'Published' :
+				($this->Parent->DataItem->Status===1 ? 'Draft' : 'Pending')
+		%>
+		</prop:ItemTemplate>
+		<prop:EditItemTemplate>
+		<com:TDropDownList ID="PostStatus" SelectedValue=<%# $this->Parent->DataItem->Status %> >
+			<com:TListItem Value="0" Text="Published" />
+			<com:TListItem Value="1" Text="Draft" />
+			<com:TListItem Value="2" Text="Pending" />
+		</com:TDropDownList>
+		</prop:EditItemTemplate>
+	</com:TTemplateColumn>
+	<com:TBoundColumn
+		HeaderText="Time"
+		ReadOnly="true"
+		DataField="CreateTime"
+		DataFormatString="#date('M j, Y',{0})"
+		ItemStyle.Wrap="false"
+		ItemStyle.Width="90px"
+		ItemStyle.HorizontalAlign="Center"
+		/>
+	<com:TEditCommandColumn
+		HeaderText="Command"
+		HeaderStyle.Width="80px"
+		UpdateText="Save"
+		ItemStyle.HorizontalAlign="Center"
+		ItemStyle.Wrap="false"
+		ItemStyle.Width="80px"
+		/>
+</com:TDataGrid>
+
+</com:TContent>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/Admin/PostMan.php b/demos/blog/protected/Pages/Admin/PostMan.php
new file mode 100644
index 00000000..a99332eb
--- /dev/null
+++ b/demos/blog/protected/Pages/Admin/PostMan.php
@@ -0,0 +1,56 @@
+<?php
+
+class PostMan extends BlogPage
+{
+	protected function bindData()
+	{
+		$offset=$this->PostGrid->CurrentPageIndex*$this->PostGrid->PageSize;
+		$limit=$this->PostGrid->PageSize;
+		$this->PostGrid->DataSource=$this->DataAccess->queryPosts('','','','ORDER BY a.status DESC, create_time DESC',"LIMIT $offset,$limit");
+		$this->PostGrid->VirtualItemCount=$this->DataAccess->queryPostCount('','','');
+		$this->PostGrid->dataBind();
+	}
+
+	public function onLoad($param)
+	{
+		parent::onLoad($param);
+		if(!$this->IsPostBack)
+			$this->bindData();
+	}
+
+	public function changePage($sender,$param)
+	{
+		$this->PostGrid->CurrentPageIndex=$param->NewPageIndex;
+		$this->bindData();
+	}
+
+	public function pagerCreated($sender,$param)
+	{
+		$param->Pager->Controls->insertAt(0,'Page: ');
+	}
+
+	public function editItem($sender,$param)
+	{
+		$this->PostGrid->EditItemIndex=$param->Item->ItemIndex;
+		$this->bindData();
+	}
+
+	public function saveItem($sender,$param)
+	{
+		$item=$param->Item;
+		$postID=$this->PostGrid->DataKeys[$item->ItemIndex];
+		$postRecord=$this->DataAccess->queryPostByID($postID);
+		$postRecord->Status=TPropertyValue::ensureInteger($item->Cells[2]->PostStatus->SelectedValue);
+		$this->DataAccess->updatePost($postRecord);
+		$this->PostGrid->EditItemIndex=-1;
+		$this->bindData();
+	}
+
+	public function cancelItem($sender,$param)
+	{
+		$this->PostGrid->EditItemIndex=-1;
+		$this->bindData();
+	}
+}
+
+?>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/Admin/Settings.page b/demos/blog/protected/Pages/Admin/Settings.page
new file mode 100644
index 00000000..48dfde96
--- /dev/null
+++ b/demos/blog/protected/Pages/Admin/Settings.page
@@ -0,0 +1,4 @@
+<com:TContent ID="main" >
+Welcome, <com:TLabel Text=<%= $this->User->Name %> />!
+This page contains site settings accessible only to site admin.
+</com:TContent>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/Admin/UserMan.page b/demos/blog/protected/Pages/Admin/UserMan.page
new file mode 100644
index 00000000..a8b634c6
--- /dev/null
+++ b/demos/blog/protected/Pages/Admin/UserMan.page
@@ -0,0 +1,95 @@
+<com:TContent ID="Main">
+
+<h2>Administration Center</h2>
+
+<com:Application.Pages.Admin.AdminMenu />
+
+<com:TDataGrid ID="UserGrid"
+	AutoGenerateColumns="false"
+	DataKeyField="ID"
+	CssClass="grid"
+	HeaderStyle.CssClass="grid-header"
+	ItemStyle.CssClass="grid-row1"
+	SelectedItemStyle.CssClass="grid-row-selected"
+	AlternatingItemStyle.CssClass="grid-row2"
+	AllowPaging="true"
+	AllowCustomPaging="true"
+	PageSize="20"
+	PagerStyle.CssClass="grid-pager"
+	PagerStyle.Mode="Numeric"
+	OnPageIndexChanged="changePage"
+	OnPagerCreated="pagerCreated"
+	OnEditCommand="editItem"
+	OnUpdateCommand="saveItem"
+	OnCancelCommand="cancelItem"
+	>
+	<com:THyperLinkColumn
+		HeaderText="Name"
+		DataNavigateUrlField="ID"
+		DataNavigateUrlFormatString="#$this->Service->constructUrl('Users.ViewUser',array('id'=>{0}))"
+		DataTextField="Name"
+		/>
+	<com:TTemplateColumn
+		HeaderText="Role"
+		ItemStyle.HorizontalAlign="Center"
+		ItemStyle.Wrap="false"
+		ItemStyle.Width="7px" >
+		<prop:ItemTemplate>
+		<%#	$this->Parent->DataItem->Role===0 ?	'User' : 'Admin' %>
+		</prop:ItemTemplate>
+		<prop:EditItemTemplate>
+		<com:TDropDownList ID="UserRole" SelectedValue=<%# $this->Parent->DataItem->Role %> >
+			<com:TListItem Value="0" Text="User" />
+			<com:TListItem Value="1" Text="Admin" />
+		</com:TDropDownList>
+		</prop:EditItemTemplate>
+	</com:TTemplateColumn>
+	<com:TTemplateColumn
+		HeaderText="Status"
+		ItemStyle.HorizontalAlign="Center"
+		ItemStyle.Wrap="false"
+		ItemStyle.Width="70px" >
+		<prop:ItemTemplate>
+		<%#
+			$this->Parent->DataItem->Status===0 ?
+				'Normal' :
+				($this->Parent->DataItem->Status===1 ? 'Disabled' : 'Pending')
+		%>
+		</prop:ItemTemplate>
+		<prop:EditItemTemplate>
+		<com:TDropDownList ID="UserStatus" SelectedValue=<%# $this->Parent->DataItem->Status %> >
+			<com:TListItem Value="0" Text="Normal" />
+			<com:TListItem Value="1" Text="Disabled" />
+			<com:TListItem Value="2" Text="Pending" />
+		</com:TDropDownList>
+		</prop:EditItemTemplate>
+	</com:TTemplateColumn>
+	<com:TBoundColumn
+		HeaderText="Email"
+		DataField="Email"
+		ItemStyle.Wrap="false"
+		ItemStyle.Width="90px"
+		ReadOnly="true"	>
+		<prop:DataFormatString>#
+		'<a href="mailto:'.{0}.'">'.{0}.'</a>'
+		</prop:DataFormatString>
+	</com:TBoundColumn>
+	<com:TBoundColumn
+		HeaderText="Reg. Date"
+		DataField="CreateTime"
+		DataFormatString="#date('M j, Y',{0})"
+		ReadOnly="true"
+		ItemStyle.Wrap="false"
+		ItemStyle.Width="90px"
+		/>
+	<com:TEditCommandColumn
+		HeaderText="Command"
+		HeaderStyle.Width="80px"
+		UpdateText="Save"
+		ItemStyle.HorizontalAlign="Center"
+		ItemStyle.Wrap="false"
+		ItemStyle.Width="80px"
+		/>
+</com:TDataGrid>
+
+</com:TContent>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/Admin/UserMan.php b/demos/blog/protected/Pages/Admin/UserMan.php
new file mode 100644
index 00000000..1cb62482
--- /dev/null
+++ b/demos/blog/protected/Pages/Admin/UserMan.php
@@ -0,0 +1,58 @@
+<?php
+
+class UserMan extends BlogPage
+{
+	protected function bindData()
+	{
+		$author=$this->User->ID;
+		$offset=$this->UserGrid->CurrentPageIndex*$this->UserGrid->PageSize;
+		$limit=$this->UserGrid->PageSize;
+		$this->UserGrid->DataSource=$this->DataAccess->queryUsers('','ORDER BY status DESC, name ASC',"LIMIT $offset,$limit");
+		$this->UserGrid->VirtualItemCount=$this->DataAccess->queryUserCount('');
+		$this->UserGrid->dataBind();
+	}
+
+	public function onLoad($param)
+	{
+		parent::onLoad($param);
+		if(!$this->IsPostBack)
+			$this->bindData();
+	}
+
+	public function changePage($sender,$param)
+	{
+		$this->UserGrid->CurrentPageIndex=$param->NewPageIndex;
+		$this->bindData();
+	}
+
+	public function pagerCreated($sender,$param)
+	{
+		$param->Pager->Controls->insertAt(0,'Page: ');
+	}
+
+	public function editItem($sender,$param)
+	{
+		$this->UserGrid->EditItemIndex=$param->Item->ItemIndex;
+		$this->bindData();
+	}
+
+	public function saveItem($sender,$param)
+	{
+		$item=$param->Item;
+		$userID=$this->UserGrid->DataKeys[$item->ItemIndex];
+		$userRecord=$this->DataAccess->queryUserByID($userID);
+		$userRecord->Role=TPropertyValue::ensureInteger($item->Cells[1]->UserRole->SelectedValue);
+		$userRecord->Status=TPropertyValue::ensureInteger($item->Cells[2]->UserStatus->SelectedValue);
+		$this->DataAccess->updateUser($userRecord);
+		$this->UserGrid->EditItemIndex=-1;
+		$this->bindData();
+	}
+
+	public function cancelItem($sender,$param)
+	{
+		$this->UserGrid->EditItemIndex=-1;
+		$this->bindData();
+	}
+}
+
+?>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/Admin/config.xml b/demos/blog/protected/Pages/Admin/config.xml
new file mode 100644
index 00000000..c99e5892
--- /dev/null
+++ b/demos/blog/protected/Pages/Admin/config.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<configuration>
+  <authorization>
+    <allow roles="admin" />
+    <deny users="*" />
+  </authorization>
+</configuration>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/ErrorReport.page b/demos/blog/protected/Pages/ErrorReport.page
new file mode 100644
index 00000000..a9b461d9
--- /dev/null
+++ b/demos/blog/protected/Pages/ErrorReport.page
@@ -0,0 +1,15 @@
+<com:TContent ID="Main">
+
+<h2>Error</h2>
+
+<p>
+<%= $this->ErrorMessage %>
+</p>
+
+<p>
+Please <a href="mailto:<%$ AdminEmail %>">report to  us</a>
+if you believe this error is caused by our system. Thanks!
+</p>
+
+
+</com:TContent>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/ErrorReport.php b/demos/blog/protected/Pages/ErrorReport.php
new file mode 100644
index 00000000..3b24170f
--- /dev/null
+++ b/demos/blog/protected/Pages/ErrorReport.php
@@ -0,0 +1,12 @@
+<?php
+
+class ErrorReport extends BlogPage
+{
+	public function getErrorMessage()
+	{
+		$id=TPropertyValue::ensureInteger($this->Request['id']);
+		return BlogErrors::getMessage($id);
+	}
+}
+
+?>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/Posts/EditCategory.page b/demos/blog/protected/Pages/Posts/EditCategory.page
new file mode 100644
index 00000000..fdde2648
--- /dev/null
+++ b/demos/blog/protected/Pages/Posts/EditCategory.page
@@ -0,0 +1,36 @@
+<com:TContent ID="Main">
+
+<h2>Update Post Category</h2>
+
+<span class="input-label">Category name</span>
+<com:TRequiredFieldValidator
+	Display="Dynamic"
+	ControlToValidate="CategoryName"
+	ErrorMessage="...is required"
+	ValidationGroup="category" />
+<com:TCustomValidator
+	ControlToValidate="CategoryName"
+	ValidationGroup="category"
+	Display="Dynamic"
+	OnServerValidate="checkCategoryName"
+	Text="...must be unique"
+	ControlCssClass="inputerror" />
+<br/>
+<com:TTextBox ID="CategoryName" Columns="50" MaxLength="128" />
+<br/>
+
+<span class="input-label">Description</span>
+<br/>
+<com:TTextBox
+	ID="CategoryDescription"
+	TextMode="MultiLine"
+	Columns="50"
+	Rows="5" />
+<br/>
+
+<com:TLinkButton
+	Text="Save"
+	OnClick="saveButtonClicked"
+	ValidationGroup="category" />
+
+</com:TContent>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/Posts/EditCategory.php b/demos/blog/protected/Pages/Posts/EditCategory.php
new file mode 100644
index 00000000..fd2d0707
--- /dev/null
+++ b/demos/blog/protected/Pages/Posts/EditCategory.php
@@ -0,0 +1,44 @@
+<?php
+
+class EditCategory extends BlogPage
+{
+	public function getCurrentCategory()
+	{
+		$id=TPropertyValue::ensureInteger($this->Request['id']);
+		if(($cat=$this->DataAccess->queryCategoryByID($id))!==null)
+			return $cat;
+		else
+			throw new BlogException('xxx');
+	}
+
+	public function onLoad($param)
+	{
+		parent::onLoad($param);
+		if(!$this->IsPostBack)
+		{
+			$catRecord=$this->getCurrentCategory();
+			$this->CategoryName->Text=$catRecord->Name;
+			$this->CategoryDescription->Text=$catRecord->Description;
+		}
+	}
+
+	public function saveButtonClicked($sender,$param)
+	{
+		if($this->IsValid)
+		{
+			$categoryRecord=$this->getCurrentCategory();
+			$categoryRecord->Name=$this->CategoryName->Text;
+			$categoryRecord->Description=$this->CategoryDescription->Text;
+			$this->DataAccess->updateCategory($categoryRecord);
+			$this->gotoPage('Posts.ListPost',array('cat'=>$categoryRecord->ID));
+		}
+	}
+
+	public function checkCategoryName($sender,$param)
+	{
+		$name=$this->CategoryName->Text;
+		$param->IsValid=$this->DataAccess->queryCategoryByName($name)===null;
+	}
+}
+
+?>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/Posts/EditPost.page b/demos/blog/protected/Pages/Posts/EditPost.page
new file mode 100644
index 00000000..591f5945
--- /dev/null
+++ b/demos/blog/protected/Pages/Posts/EditPost.page
@@ -0,0 +1,41 @@
+<com:TContent ID="Main">
+
+<h2>Update Post</h2>
+
+Title
+<com:TRequiredFieldValidator
+Display="Dynamic"
+	ControlToValidate="Title"
+	ErrorMessage="...is required"
+	ValidationGroup="post" />
+<br/>
+<com:TTextBox ID="Title" Columns="70" MaxLength="256" />
+<br/>
+
+Content
+<com:TRequiredFieldValidator
+Display="Dynamic"
+	ControlToValidate="Content"
+	ErrorMessage="...is required"
+	ValidationGroup="post" />
+<br/>
+<com:THtmlArea ID="Content" Width="450px" />
+<br/>
+
+Categories<br/>
+<com:TListBox
+	ID="Categories"
+	SelectionMode="Multiple"
+	DataTextField="Name"
+	DataValueField="ID" />
+<br/>
+
+<com:TCheckBox ID="DraftMode" Text="in draft mode (the post will not be published)" />
+<br/>
+
+<com:TLinkButton
+	Text="Save"
+	OnClick="saveButtonClicked"
+	ValidationGroup="post" />
+
+</com:TContent>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/Posts/EditPost.php b/demos/blog/protected/Pages/Posts/EditPost.php
new file mode 100644
index 00000000..57e92b1c
--- /dev/null
+++ b/demos/blog/protected/Pages/Posts/EditPost.php
@@ -0,0 +1,51 @@
+<?php
+
+class EditPost extends BlogPage
+{
+	public function getCurrentPost()
+	{
+		$id=TPropertyValue::ensureInteger($this->Request['id']);
+		if(($post=$this->DataAccess->queryPostByID($id))!==null)
+			return $post;
+		else
+			throw new BlogException('xxx');
+	}
+
+	public function onLoad($param)
+	{
+		parent::onLoad($param);
+		if(!$this->IsPostBack)
+		{
+			$postRecord=$this->getCurrentPost();
+			$this->Title->Text=$postRecord->Title;
+			$this->Content->Text=$postRecord->Content;
+			$this->DraftMode->Checked=$postRecord->Status===0;
+			$this->Categories->DataSource=$this->DataAccess->queryCategories();
+			$this->Categories->dataBind();
+			$cats=$this->DataAccess->queryCategoriesByPostID($postRecord->ID);
+			$catIDs=array();
+			foreach($cats as $cat)
+				$catIDs[]=$cat->ID;
+			$this->Categories->SelectedValues=$catIDs;
+		}
+	}
+
+	public function saveButtonClicked($sender,$param)
+	{
+		if($this->IsValid)
+		{
+			$postRecord=$this->getCurrentPost();
+			$postRecord->Title=$this->Title->Text;
+			$postRecord->Content=$this->Content->Text;
+			$postRecord->Status=$this->DraftMode->Checked?0:1;
+			$postRecord->ModifyTime=time();
+			$cats=array();
+			foreach($this->Categories->SelectedValues as $value)
+				$cats[]=TPropertyValue::ensureInteger($value);
+			$this->DataAccess->updatePost($postRecord,$cats);
+			$this->gotoPage('Posts.ViewPost',array('id'=>$postRecord->ID));
+		}
+	}
+}
+
+?>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/Posts/ListPost.page b/demos/blog/protected/Pages/Posts/ListPost.page
new file mode 100644
index 00000000..15fc3d0c
--- /dev/null
+++ b/demos/blog/protected/Pages/Posts/ListPost.page
@@ -0,0 +1,27 @@
+<com:TContent ID="Main">
+
+<com:TRepeater ID="PostList" EnableViewState="false">
+	<prop:ItemTemplate>
+<div class="post">
+<div class="post-title">
+<%# $this->DataItem->Title %>
+</div>
+<div class="post-time">
+<%# date('l, F j, Y \a\t h:i:s a',$this->DataItem->CreateTime) %>
+</div>
+<div class="post-content">
+<%# $this->DataItem->Content %>
+</div>
+<div class="post-footer">
+posted by
+<%# '<a href="' . $this->Service->constructUrl('Users.ViewUser',array('id'=>$this->DataItem->AuthorID)) . '">' . $this->DataItem->AuthorName . '</a>' %>
+|
+<%# '<a href="' . $this->Service->constructUrl('Posts.ViewPost',array('id'=>$this->DataItem->ID)) . '">PermaLink</a>' %>
+|
+<%# '<a href="' . $this->Service->constructUrl('Posts.ViewPost',array('id'=>$this->DataItem->ID)) . '#comments">Comments (' . $this->DataItem->CommentCount . ')</a>' %>
+</div>
+</div>
+	</prop:ItemTemplate>
+</com:TRepeater>
+
+</com:TContent>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/Posts/ListPost.php b/demos/blog/protected/Pages/Posts/ListPost.php
new file mode 100644
index 00000000..6d56b543
--- /dev/null
+++ b/demos/blog/protected/Pages/Posts/ListPost.php
@@ -0,0 +1,44 @@
+<?php
+
+class ListPost extends BlogPage
+{
+	const DEFAULT_LIMIT=10;
+
+	public function getPosts()
+	{
+		$timeFilter='';
+		$catFilter='';
+		if(($time=TPropertyValue::ensureInteger($this->Request['time']))>0)
+		{
+			$year=(integer)($time/100);
+			$month=$time%100;
+			$startTime=mktime(0,0,0,$month,1,$year);
+			if(++$month>12)
+			{
+				$month=1;
+				$year++;
+			}
+			$endTime=mktime(0,0,0,$month,1,$year);
+			$timeFilter="create_time>=$startTime AND create_time<$endTime";
+		}
+		if(($catID=$this->Request['cat'])!==null)
+		{
+			$catID=TPropertyValue::ensureInteger($catID);
+			$catFilter="category_id=$catID";
+		}
+		if(($offset=TPropertyValue::ensureInteger($this->Request['offset']))<=0)
+			$offset=0;
+		if(($limit=TPropertyValue::ensureInteger($this->Request['limit']))<=0)
+			$limit=self::DEFAULT_LIMIT;
+		return $this->DataAccess->queryPosts('',$timeFilter,$catFilter,'ORDER BY create_time DESC',"LIMIT $offset,$limit");
+	}
+
+	public function onLoad($param)
+	{
+		parent::onLoad($param);
+		$this->PostList->DataSource=$this->getPosts();
+		$this->PostList->dataBind();
+	}
+}
+
+?>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/Posts/MyPost.page b/demos/blog/protected/Pages/Posts/MyPost.page
new file mode 100644
index 00000000..95a32ac9
--- /dev/null
+++ b/demos/blog/protected/Pages/Posts/MyPost.page
@@ -0,0 +1,46 @@
+<com:TContent ID="Main">
+
+<h2>My Posts</h2>
+
+<com:TDataGrid ID="PostGrid"
+	AutoGenerateColumns="false"
+	CssClass="grid"
+	HeaderStyle.CssClass="grid-header"
+	ItemStyle.CssClass="grid-row1"
+	AlternatingItemStyle.CssClass="grid-row2"
+	AllowPaging="true"
+	AllowCustomPaging="true"
+	PageSize="20"
+	PagerStyle.CssClass="grid-pager"
+	PagerStyle.Mode="Numeric"
+	OnPageIndexChanged="changePage"
+	OnPagerCreated="pagerCreated"
+	>
+	<com:THyperLinkColumn
+		HeaderText="Title"
+		DataNavigateUrlField="ID"
+		DataNavigateUrlFormatString="#$this->Service->constructUrl('Posts.ViewPost',array('id'=>{0}))"
+		DataTextField="Title"
+		/>
+	<com:TBoundColumn
+		HeaderText="Status"
+		DataField="Status"
+		DataFormatString="#{0}?'Published':'Draft'"
+		ItemStyle.Width="70px"
+		/>
+	<com:TBoundColumn
+		HeaderText="Comments"
+		DataField="CommentCount"
+		ItemStyle.HorizontalAlign="Center"
+		ItemStyle.Width="80px"
+		/>
+	<com:TBoundColumn
+		HeaderText="Time"
+		DataField="CreateTime"
+		DataFormatString="#date('M j, Y',{0})"
+		ItemStyle.Wrap="false"
+		ItemStyle.Width="90px"
+		/>
+</com:TDataGrid>
+
+</com:TContent>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/Posts/MyPost.php b/demos/blog/protected/Pages/Posts/MyPost.php
new file mode 100644
index 00000000..be03ca63
--- /dev/null
+++ b/demos/blog/protected/Pages/Posts/MyPost.php
@@ -0,0 +1,34 @@
+<?php
+
+class MyPost extends BlogPage
+{
+	protected function bindData()
+	{
+		$author=$this->User->ID;
+		$offset=$this->PostGrid->CurrentPageIndex*$this->PostGrid->PageSize;
+		$limit=$this->PostGrid->PageSize;
+		$this->PostGrid->DataSource=$this->DataAccess->queryPosts("author_id=$author",'','','ORDER BY a.status ASC, create_time DESC',"LIMIT $offset,$limit");
+		$this->PostGrid->VirtualItemCount=$this->DataAccess->queryPostCount("author_id=$author",'','');
+		$this->PostGrid->dataBind();
+	}
+
+	public function onLoad($param)
+	{
+		parent::onLoad($param);
+		if(!$this->IsPostBack)
+			$this->bindData();
+	}
+
+	public function changePage($sender,$param)
+	{
+		$this->PostGrid->CurrentPageIndex=$param->NewPageIndex;
+		$this->bindData();
+	}
+
+	public function pagerCreated($sender,$param)
+	{
+		$param->Pager->Controls->insertAt(0,'Page: ');
+	}
+}
+
+?>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/Posts/NewCategory.page b/demos/blog/protected/Pages/Posts/NewCategory.page
new file mode 100644
index 00000000..92fe1468
--- /dev/null
+++ b/demos/blog/protected/Pages/Posts/NewCategory.page
@@ -0,0 +1,36 @@
+<com:TContent ID="Main">
+
+<h2>New Post Category</h2>
+
+<span class="input-label">Category name</span>
+<com:TRequiredFieldValidator
+	Display="Dynamic"
+	ControlToValidate="CategoryName"
+	ErrorMessage="...is required"
+	ValidationGroup="category" />
+<com:TCustomValidator
+	ControlToValidate="CategoryName"
+	ValidationGroup="category"
+	Display="Dynamic"
+	OnServerValidate="checkCategoryName"
+	Text="...must be unique"
+	ControlCssClass="inputerror" />
+<br/>
+<com:TTextBox ID="CategoryName" Columns="50" MaxLength="128" />
+<br/>
+
+<span class="input-label">Description</span>
+<br/>
+<com:TTextBox
+	ID="CategoryDescription"
+	TextMode="MultiLine"
+	Columns="50"
+	Rows="5" />
+<br/>
+
+<com:TLinkButton
+	Text="Save"
+	OnClick="saveButtonClicked"
+	ValidationGroup="category" />
+
+</com:TContent>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/Posts/NewCategory.php b/demos/blog/protected/Pages/Posts/NewCategory.php
new file mode 100644
index 00000000..d36f6af1
--- /dev/null
+++ b/demos/blog/protected/Pages/Posts/NewCategory.php
@@ -0,0 +1,24 @@
+<?php
+
+class NewCategory extends BlogPage
+{
+	public function saveButtonClicked($sender,$param)
+	{
+		if($this->IsValid)
+		{
+			$categoryRecord=new CategoryRecord;
+			$categoryRecord->Name=$this->CategoryName->Text;
+			$categoryRecord->Description=$this->CategoryDescription->Text;
+			$this->DataAccess->insertCategory($categoryRecord);
+			$this->gotoPage('Posts.ListPost',array('cat'=>$categoryRecord->ID));
+		}
+	}
+
+	public function checkCategoryName($sender,$param)
+	{
+		$name=$this->CategoryName->Text;
+		$param->IsValid=$this->DataAccess->queryCategoryByName($name)===null;
+	}
+}
+
+?>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/Posts/NewPost.page b/demos/blog/protected/Pages/Posts/NewPost.page
new file mode 100644
index 00000000..a49188f6
--- /dev/null
+++ b/demos/blog/protected/Pages/Posts/NewPost.page
@@ -0,0 +1,41 @@
+<com:TContent ID="Main">
+
+<h2>Write a New Post</h2>
+
+Title
+<com:TRequiredFieldValidator
+Display="Dynamic"
+	ControlToValidate="Title"
+	ErrorMessage="...is required"
+	ValidationGroup="post" />
+<br/>
+<com:TTextBox ID="Title" Columns="70" MaxLength="256" />
+<br/>
+
+Content
+<com:TRequiredFieldValidator
+Display="Dynamic"
+	ControlToValidate="Content"
+	ErrorMessage="...is required"
+	ValidationGroup="post" />
+<br/>
+<com:THtmlArea ID="Content" Width="450px" />
+<br/>
+
+Categories<br/>
+<com:TListBox
+	ID="Categories"
+	SelectionMode="Multiple"
+	DataTextField="Name"
+	DataValueField="ID" />
+<br/>
+
+<com:TCheckBox ID="DraftMode" Text="in draft mode (the post will not be published)" />
+<br/>
+
+<com:TLinkButton
+	Text="Save"
+	OnClick="saveButtonClicked"
+	ValidationGroup="post" />
+
+</com:TContent>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/Posts/NewPost.php b/demos/blog/protected/Pages/Posts/NewPost.php
new file mode 100644
index 00000000..055c7f92
--- /dev/null
+++ b/demos/blog/protected/Pages/Posts/NewPost.php
@@ -0,0 +1,34 @@
+<?php
+
+class NewPost extends BlogPage
+{
+	public function onLoad($param)
+	{
+		parent::onLoad($param);
+		if(!$this->IsPostBack)
+		{
+			$this->Categories->DataSource=$this->DataAccess->queryCategories();
+			$this->Categories->dataBind();
+		}
+	}
+
+	public function saveButtonClicked($sender,$param)
+	{
+		if($this->IsValid)
+		{
+			$postRecord=new PostRecord;
+			$postRecord->Title=$this->Title->Text;
+			$postRecord->Content=$this->Content->Text;
+			$postRecord->Status=$this->DraftMode->Checked?0:1;
+			$postRecord->CreateTime=time();
+			$postRecord->AuthorID=$this->User->ID;
+			$cats=array();
+			foreach($this->Categories->SelectedValues as $value)
+				$cats[]=TPropertyValue::ensureInteger($value);
+			$this->DataAccess->insertPost($postRecord,$cats);
+			$this->gotoPage('Posts.ViewPost',array('id'=>$postRecord->ID));
+		}
+	}
+}
+
+?>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/Posts/ViewPost.page b/demos/blog/protected/Pages/Posts/ViewPost.page
new file mode 100644
index 00000000..4b233615
--- /dev/null
+++ b/demos/blog/protected/Pages/Posts/ViewPost.page
@@ -0,0 +1,113 @@
+<com:TContent ID="Main">
+
+<div class="post">
+<div class="post-title">
+<%= $this->CurrentPost->Title %>
+</div>
+<div class="post-time">
+<%= date('l, F j, Y \a\t h:i:s a',$this->CurrentPost->CreateTime) %>
+by
+<%= '<a href="' . $this->Service->constructUrl('Users.ViewUser',array('id'=>$this->CurrentPost->AuthorID)) . '">' . $this->CurrentPost->AuthorName . '</a>' %>
+<%= $this->CanEditPost ? '| <a href="' . $this->Service->constructUrl('Posts.EditPost',array('id'=>$this->CurrentPost->ID)) . '">Edit</a> | ' : '';
+%>
+<com:TLinkButton
+	Text="Delete"
+	OnClick="deleteButtonClicked"
+	Visible=<%= $this->CanEditPost %>
+	Attributes.onclick="if(!confirm('Are you sure to delete this post? This will also delete all related comments.')) return false;"
+	/>
+</div>
+<div class="post-content">
+<%= $this->CurrentPost->Content %>
+</div>
+<div class="post-footer">
+<com:TRepeater ID="CategoryList" EnableViewState="false">
+	<prop:ItemTemplate>
+	[
+	<a href="<%# $this->Service->constructUrl('Posts.ListPost',array('cat'=>$this->DataItem->ID)) %>"><%# $this->DataItem->Name %></a>
+	]
+	</prop:ItemTemplate>
+</com:TRepeater>
+</div>
+</div>
+
+<div class="comments">
+<a name="comments"></a>
+<h3>Comments</h3>
+
+<com:TRepeater ID="CommentList" OnItemCommand="repeaterItemCommand">
+	<prop:ItemTemplate>
+<div class="comment">
+<div class="comment-header">
+<com:TLinkButton
+	Text="Delete"
+	Attributes.onclick="if(!confirm('Are you sure to delete this comment?')) return false;"
+	CommandParameter=<%# $this->DataItem->ID %>
+	Visible=<%= $this->Page->CanEditPost %> Style="float:right"/>
+<%# date('F j, Y \a\t h:i:s a',$this->DataItem->CreateTime) %>
+by
+<%# $this->DataItem->AuthorWebsite==='' ?
+		$this->DataItem->AuthorName :
+		'<a href="' . $this->DataItem->AuthorWebsite . '">' . $this->DataItem->AuthorName . '</a>' %>
+</div>
+<div class="comment-content">
+<%# $this->DataItem->Content %>
+</div>
+</div>
+	</prop:ItemTemplate>
+</com:TRepeater>
+
+<h4>Leave your comment</h4>
+
+<span class="input-label">Name</span>
+<com:TRequiredFieldValidator
+	ControlToValidate="CommentAuthor"
+	ValidationGroup="comment""
+	Display="Dynamic"
+	Text="...is required"
+	ControlCssClass="inputerror" />
+<br/>
+<com:TTextBox ID="CommentAuthor" />
+<br/>
+
+<span class="input-label">Email address</span>
+<com:TRequiredFieldValidator
+	ControlToValidate="CommentEmail"
+	ValidationGroup="comment""
+	Display="Dynamic"
+	Text="...is required"
+	ControlCssClass="inputerror" />
+<com:TEmailAddressValidator
+	ControlToValidate="CommentEmail"
+	ValidationGroup="comment"
+	Display="Dynamic"
+	Text="*"
+	ErrorMessage="You entered an invalid email address."
+	ControlCssClass="inputerror" />
+<br/>
+<com:TTextBox ID="CommentEmail" />
+<br/>
+
+<span class="input-label">Personal website</span>
+<br/>
+<com:TTextBox ID="CommentWebsite" Columns="70"/>
+<br/>
+
+<span class="input-label">Comment</span>
+<com:TRequiredFieldValidator
+	ControlToValidate="CommentContent"
+	ValidationGroup="comment"
+	Display="Dynamic"
+	Text="...is required"
+	ControlCssClass="inputerror" />
+<br/>
+<com:TTextBox ID="CommentContent" TextMode="MultiLine" Columns="55" Rows="10"/>
+<br/>
+
+<com:TLinkButton
+	Text="Submit"
+	ValidationGroup="comment"
+	OnClick="submitCommentButtonClicked" />
+
+</div>
+</com:TContent>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/Posts/ViewPost.php b/demos/blog/protected/Pages/Posts/ViewPost.php
new file mode 100644
index 00000000..309bedc1
--- /dev/null
+++ b/demos/blog/protected/Pages/Posts/ViewPost.php
@@ -0,0 +1,73 @@
+<?php
+
+class ViewPost extends BlogPage
+{
+	private $_postID=null;
+	private $_post=null;
+
+	public function getPostID()
+	{
+		if($this->_postID===null)
+			$this->_postID=TPropertyValue::ensureInteger($this->Request['id']);
+		return $this->_postID;
+	}
+
+	public function getCurrentPost()
+	{
+		if($this->_post===null)
+		{
+			if(($this->_post=$this->DataAccess->queryPostByID($this->getPostID()))===null)
+				$this->reportError(BlogErrors::ERROR_POST_NOT_FOUND);
+		}
+		return $this->_post;
+	}
+
+	public function getCanEditPost()
+	{
+		$user=$this->getUser();
+		$authorID=$this->getCurrentPost()->AuthorID;
+		return $authorID===$user->getID() || $user->isInRole('admin');
+	}
+
+	public function onLoad($param)
+	{
+		parent::onLoad($param);
+		$this->CategoryList->DataSource=$this->DataAccess->queryCategoriesByPostID($this->getPostID());
+		$this->CategoryList->dataBind();
+		$this->CommentList->DataSource=$this->DataAccess->queryCommentsByPostID($this->getPostID());
+		$this->CommentList->dataBind();
+	}
+
+	public function submitCommentButtonClicked($sender,$param)
+	{
+		if($this->IsValid)
+		{
+			$commentRecord=new CommentRecord;
+			$commentRecord->PostID=$this->CurrentPost->ID;
+			$commentRecord->AuthorName=$this->CommentAuthor->Text;
+			$commentRecord->AuthorEmail=$this->CommentEmail->Text;
+			$commentRecord->AuthorWebsite=$this->CommentWebsite->Text;
+			$commentRecord->AuthorIP=$this->Request->UserHostAddress;
+			$commentRecord->Content=$this->CommentContent->Text;
+			$commentRecord->CreateTime=time();
+			$commentRecord->Status=0;
+			$this->DataAccess->insertComment($commentRecord);
+			$this->Response->reload();
+		}
+	}
+
+	public function deleteButtonClicked($sender,$param)
+	{
+		$this->DataAccess->deletePost($this->PostID);
+		$this->gotoDefaultPage();
+	}
+
+	public function repeaterItemCommand($sender,$param)
+	{
+		$id=TPropertyValue::ensureInteger($param->CommandParameter);
+		$this->DataAccess->deleteComment($id);
+		$this->Response->reload();
+	}
+}
+
+?>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/Posts/config.xml b/demos/blog/protected/Pages/Posts/config.xml
new file mode 100644
index 00000000..1c04e946
--- /dev/null
+++ b/demos/blog/protected/Pages/Posts/config.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<configuration>
+  <authorization>
+    <deny pages="EditPost,NewPost,MyPost" users="?" />
+  </authorization>
+</configuration>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/Users/EditUser.page b/demos/blog/protected/Pages/Users/EditUser.page
new file mode 100644
index 00000000..8c21fd50
--- /dev/null
+++ b/demos/blog/protected/Pages/Users/EditUser.page
@@ -0,0 +1,74 @@
+<com:TContent ID="Main">
+
+<h2>Update Profile</h2>
+
+<com:TValidationSummary Display="Dynamic" ValidationGroup="user" />
+
+<span class="input-label">Username</span>
+<br/>
+<com:TLabel ID="Username" />
+
+<br/>
+
+<span class="input-label">Full name</span>
+<br/>
+<com:TTextBox ID="FullName" />
+
+<br/>
+
+<span class="input-label">Password</span>
+<br/>
+<com:TTextBox ID="Password" TextMode="Password" />
+<com:TRegularExpressionValidator
+	ControlToValidate="Password"
+	ValidationGroup="user"
+	Display="Dynamic"
+	RegularExpression="[\w\.]{6,16}"
+	Text="*"
+	ErrorMessage="Your password must contain only letters, digits and underscores, and it must contain at least 6 and at most 16 characters."
+	ControlCssClass="inputerror" />
+
+<br/>
+
+<span class="input-label">Re-type Password</span>
+<br/>
+<com:TTextBox ID="Password2" TextMode="Password" />
+<com:TCompareValidator
+	ControlToValidate="Password"
+	ControlToCompare="Password2"
+	ValidationGroup="user"
+	Display="Dynamic"
+	Text="*"
+	ErrorMessage="Your password entries did not match."
+	ControlCssClass="inputerror" />
+
+<br/>
+
+<span class="input-label">Email Address</span>
+<br/>
+<com:TTextBox ID="Email" />
+<com:TRequiredFieldValidator
+	ControlToValidate="Email"
+	ValidationGroup="user"
+	Text="*"
+	ErrorMessage="Please provide your email address."
+	ControlCssClass="inputerror" />
+<com:TEmailAddressValidator
+	ControlToValidate="Email"
+	ValidationGroup="user"
+	Display="Dynamic"
+	Text="*"
+	ErrorMessage="You entered an invalid email address."
+	ControlCssClass="inputerror" />
+
+<br/>
+
+<span class="input-label">Personal Website</span>
+<br/>
+<com:TTextBox ID="Website" AutoTrim="true" />
+
+<br/>
+
+<com:TLinkButton Text="Save" ValidationGroup="user" OnClick="saveButtonClicked" />
+
+</com:TContent>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/Users/EditUser.php b/demos/blog/protected/Pages/Users/EditUser.php
new file mode 100644
index 00000000..e3efcfd1
--- /dev/null
+++ b/demos/blog/protected/Pages/Users/EditUser.php
@@ -0,0 +1,43 @@
+<?php
+
+class EditUser extends BlogPage
+{
+	public function getCurrentUser()
+	{
+		if(($user=$this->DataAccess->queryUserByID($this->User->ID))!==null)
+			return $user;
+		else
+			throw new BlogException('xxx');
+	}
+
+	public function onLoad($param)
+	{
+		parent::onLoad($param);
+		if(!$this->IsPostBack)
+		{
+			$userRecord=$this->getCurrentUser();
+			$this->Username->Text=$userRecord->Name;
+			$this->FullName->Text=$userRecord->FullName;
+			$this->Email->Text=$userRecord->Email;
+			$this->Website->Text=$userRecord->Website;
+		}
+	}
+
+	public function saveButtonClicked($sender,$param)
+	{
+		if($this->IsValid)
+		{
+			$userRecord=$this->getCurrentUser();
+			if($this->Password->Text!=='')
+				$userRecord->Password=md5($this->Password->Text);
+			$userRecord->FullName=$this->FullName->Text;
+			$userRecord->Email=$this->Email->Text;
+			$userRecord->Website=$this->Website->Text;
+			$this->DataAccess->updateUser($userRecord);
+			$authManager=$this->Application->getModule('auth');
+			$this->gotoPage('Users.ViewUser',array('id'=>$userRecord->ID));
+		}
+	}
+}
+
+?>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/Users/NewUser.page b/demos/blog/protected/Pages/Users/NewUser.page
new file mode 100644
index 00000000..eba2dcec
--- /dev/null
+++ b/demos/blog/protected/Pages/Users/NewUser.page
@@ -0,0 +1,104 @@
+<com:TContent ID="Main">
+
+<h2>Create New Account</h2>
+
+<com:TValidationSummary Display="Dynamic" ValidationGroup="NewUser" />
+
+<span class="input-label">Username</span>
+<br/>
+<com:TTextBox ID="Username" />
+<com:TRequiredFieldValidator
+	ControlToValidate="Username"
+	ValidationGroup="NewUser"
+	Display="Dynamic"
+	Text="*"
+	ErrorMessage="Please choose a username."
+	ControlCssClass="inputerror" />
+<com:TRegularExpressionValidator
+	ControlToValidate="Username"
+	ValidationGroup="NewUser"
+	Display="Dynamic"
+	RegularExpression="[\w]{3,16}"
+	Text="*"
+	ErrorMessage="Your username must contain only letters, digits and underscores, and it must contain at least 3 and at most 16 characters."
+	ControlCssClass="inputerror" />
+<com:TCustomValidator
+	ControlToValidate="Username"
+	ValidationGroup="NewUser"
+	Display="Dynamic"
+	OnServerValidate="checkUsername"
+	Text="*"
+	ErrorMessage="Sorry, your username is taken by someone else. Please choose another username."
+	ControlCssClass="inputerror" />
+
+<br/>
+
+<span class="input-label">Full name</span>
+<br/>
+<com:TTextBox ID="FullName" />
+
+<br/>
+
+<span class="input-label">Password</span>
+<br/>
+<com:TTextBox ID="Password" TextMode="Password" />
+<com:TRequiredFieldValidator
+	ControlToValidate="Password"
+	ValidationGroup="NewUser"
+	Display="Dynamic"
+	Text="*"
+	ErrorMessage="Please choose a password."
+	ControlCssClass="inputerror" />
+<com:TRegularExpressionValidator
+	ControlToValidate="Password"
+	ValidationGroup="NewUser"
+	Display="Dynamic"
+	RegularExpression="[\w\.]{6,16}"
+	Text="*"
+	ErrorMessage="Your password must contain only letters, digits and underscores, and it must contain at least 6 and at most 16 characters."
+	ControlCssClass="inputerror" />
+
+<br/>
+
+<span class="input-label">Re-type Password</span>
+<br/>
+<com:TTextBox ID="Password2" TextMode="Password" />
+<com:TCompareValidator
+	ControlToValidate="Password"
+	ControlToCompare="Password2"
+	ValidationGroup="NewUser"
+	Display="Dynamic"
+	Text="*"
+	ErrorMessage="Your password entries did not match."
+	ControlCssClass="inputerror" />
+
+<br/>
+
+<span class="input-label">Email Address</span>
+<br/>
+<com:TTextBox ID="Email" />
+<com:TRequiredFieldValidator
+	ControlToValidate="Email"
+	ValidationGroup="NewUser"
+	Text="*"
+	ErrorMessage="Please provide your email address."
+	ControlCssClass="inputerror" />
+<com:TEmailAddressValidator
+	ControlToValidate="Email"
+	ValidationGroup="NewUser"
+	Display="Dynamic"
+	Text="*"
+	ErrorMessage="You entered an invalid email address."
+	ControlCssClass="inputerror" />
+
+<br/>
+
+<span class="input-label">Personal Website</span>
+<br/>
+<com:TTextBox ID="Website" AutoTrim="true" />
+
+<br/>
+
+<com:TLinkButton Text="Register" ValidationGroup="NewUser" OnClick="createUser" />
+
+</com:TContent>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/Users/NewUser.php b/demos/blog/protected/Pages/Users/NewUser.php
new file mode 100644
index 00000000..166abf66
--- /dev/null
+++ b/demos/blog/protected/Pages/Users/NewUser.php
@@ -0,0 +1,31 @@
+<?php
+
+class NewUser extends BlogPage
+{
+	public function checkUsername($sender,$param)
+	{
+		$username=$this->Username->Text;
+		$param->IsValid=$this->DataAccess->queryUserByName($username)===null;
+	}
+
+	public function createUser($sender,$param)
+	{
+		if($this->IsValid)
+		{
+			$userRecord=new UserRecord;
+			$userRecord->Name=$this->Username->Text;
+			$userRecord->FullName=$this->FullName->Text;
+			$userRecord->Role=0;
+			$userRecord->Password=md5($this->Password->Text);
+			$userRecord->Email=$this->Email->Text;
+			$userRecord->CreateTime=time();
+			$userRecord->Website=$this->Website->Text;
+			$this->DataAccess->insertUser($userRecord);
+			$authManager=$this->Application->getModule('auth');
+			$authManager->login($this->Username->Text,$this->Password->Text);
+			$this->gotoDefaultPage();
+		}
+	}
+}
+
+?>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/Users/ViewUser.page b/demos/blog/protected/Pages/Users/ViewUser.page
new file mode 100644
index 00000000..2dba6b77
--- /dev/null
+++ b/demos/blog/protected/Pages/Users/ViewUser.page
@@ -0,0 +1,21 @@
+<com:TContent ID="Main">
+
+<h2>User Profile</h2>
+
+Username: <%= $this->CurrentUser->Name %>
+<br/>
+
+Full name: <%= $this->CurrentUser->FullName %>
+<br/>
+
+Email: <%= $this->CurrentUser->Email %>
+<br/>
+
+Privilege: <%= $this->CurrentUser->Role===0? 'User':'Administrator' %>
+<br/>
+
+Personal website: <%= $this->CurrentUser->Website %>
+<br/>
+
+
+</com:TContent>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/Users/ViewUser.php b/demos/blog/protected/Pages/Users/ViewUser.php
new file mode 100644
index 00000000..3485f56b
--- /dev/null
+++ b/demos/blog/protected/Pages/Users/ViewUser.php
@@ -0,0 +1,19 @@
+<?php
+
+class ViewUser extends BlogPage
+{
+	private $_currentUser=null;
+
+	public function getCurrentUser()
+	{
+		if($this->_currentUser===null)
+		{
+			$id=TPropertyValue::ensureInteger($this->Request['id']);
+			if(($this->_currentUser=$this->DataAccess->queryUserByID($id))===null)
+				throw new BlogException('xxx');
+		}
+		return $this->_currentUser;
+	}
+}
+
+?>
\ No newline at end of file
diff --git a/demos/blog/protected/Pages/Users/config.xml b/demos/blog/protected/Pages/Users/config.xml
new file mode 100644
index 00000000..df8e4ad1
--- /dev/null
+++ b/demos/blog/protected/Pages/Users/config.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<configuration>
+  <authorization>
+    <deny pages="EditUser" users="?" />
+  </authorization>
+</configuration>
\ No newline at end of file
diff --git a/demos/blog/protected/Portlets/AccountPortlet.php b/demos/blog/protected/Portlets/AccountPortlet.php
new file mode 100644
index 00000000..0f0e60c6
--- /dev/null
+++ b/demos/blog/protected/Portlets/AccountPortlet.php
@@ -0,0 +1,14 @@
+<?php
+
+Prado::using('Application.Portlets.Portlet');
+
+class AccountPortlet extends Portlet
+{
+	public function logout($sender,$param)
+	{
+		$this->Application->getModule('auth')->logout();
+		$this->Response->reload();
+	}
+}
+
+?>
\ No newline at end of file
diff --git a/demos/blog/protected/Portlets/AccountPortlet.tpl b/demos/blog/protected/Portlets/AccountPortlet.tpl
new file mode 100644
index 00000000..2a401f41
--- /dev/null
+++ b/demos/blog/protected/Portlets/AccountPortlet.tpl
@@ -0,0 +1,20 @@
+<div class="portlet">
+
+<h2 class="portlet-title">Account</h2>
+
+<div class="portlet-content">
+Welcome, <b><%= $this->User->Name %></b>!
+<ul>
+<li><a href="<%= $this->Service->constructUrl('Posts.NewPost') %>">New post</a></li>
+<li><a href="<%= $this->Service->constructUrl('Posts.MyPost') %>">My post</a></li>
+<li><a href="<%= $this->Service->constructUrl('Users.ViewUser',array('id'=>$this->User->ID)) %>">Profile</a></li>
+<%%
+if($this->User->isInRole('admin'))
+    echo '<li><a href="'.$this->Service->constructUrl('Admin.PostMan').'">Admin</a></li>';
+%>
+<li><com:TLinkButton Text="Logout" OnClick="logout" /></li>
+</ul>
+
+</div><!-- end of portlet-content -->
+
+</div><!-- end of portlet -->
diff --git a/demos/blog/protected/Portlets/ArchivePortlet.php b/demos/blog/protected/Portlets/ArchivePortlet.php
new file mode 100644
index 00000000..a004c7a9
--- /dev/null
+++ b/demos/blog/protected/Portlets/ArchivePortlet.php
@@ -0,0 +1,45 @@
+<?php
+
+Prado::using('Application.Portlets.Portlet');
+
+class ArchivePortlet extends Portlet
+{
+	private function makeMonthTime($timestamp)
+	{
+		$date=getdate($timestamp);
+		return mktime(0,0,0,$date['mon'],1,$date['year']);
+	}
+
+	public function onLoad($param)
+	{
+		$currentTime=time();
+		$startTime=$this->Application->getModule('data')->queryEarliestPostTime();
+		if(empty($startTime))	// if no posts
+			$startTime=$currentTime;
+
+		// obtain the timestamp for the initial month
+		$date=getdate($startTime);
+		$startTime=mktime(0,0,0,$date['mon'],1,$date['year']);
+
+		$date=getdate($currentTime);
+		$month=$date['mon'];
+		$year=$date['year'];
+
+		$timestamps=array();
+		while(true)
+		{
+			if(($timestamp=mktime(0,0,0,$month,1,$year))<$startTime)
+				break;
+			$timestamps[]=$timestamp;
+			if(--$month===0)
+			{
+				$month=12;
+				$year--;
+			}
+		}
+		$this->MonthList->DataSource=$timestamps;
+		$this->MonthList->dataBind();
+	}
+}
+
+?>
\ No newline at end of file
diff --git a/demos/blog/protected/Portlets/ArchivePortlet.tpl b/demos/blog/protected/Portlets/ArchivePortlet.tpl
new file mode 100644
index 00000000..c576e9f5
--- /dev/null
+++ b/demos/blog/protected/Portlets/ArchivePortlet.tpl
@@ -0,0 +1,15 @@
+<div class="portlet">
+
+<h2 class="portlet-title">Archives</h2>
+
+<div class="portlet-content">
+<ul>
+<com:TRepeater ID="MonthList" EnableViewState="false">
+	<prop:ItemTemplate>
+	<li><a href="<%# $this->Service->constructUrl('Posts.ListPost',array('time'=>date('Ym',$this->DataItem))) %>"><%# date('F Y',$this->DataItem) %></a></li>
+	</prop:ItemTemplate>
+</com:TRepeater>
+</ul>
+</div><!-- end of portlet-content -->
+
+</div><!-- end of portlet -->
diff --git a/demos/blog/protected/Portlets/CategoryPortlet.php b/demos/blog/protected/Portlets/CategoryPortlet.php
new file mode 100644
index 00000000..9c2033aa
--- /dev/null
+++ b/demos/blog/protected/Portlets/CategoryPortlet.php
@@ -0,0 +1,15 @@
+<?php
+
+Prado::using('Application.Portlets.Portlet');
+
+class CategoryPortlet extends Portlet
+{
+	public function onLoad($param)
+	{
+		parent::onLoad($param);
+		$this->CategoryList->DataSource=$this->Application->getModule('data')->queryCategories();
+		$this->CategoryList->dataBind();
+	}
+}
+
+?>
\ No newline at end of file
diff --git a/demos/blog/protected/Portlets/CategoryPortlet.tpl b/demos/blog/protected/Portlets/CategoryPortlet.tpl
new file mode 100644
index 00000000..acbd3bec
--- /dev/null
+++ b/demos/blog/protected/Portlets/CategoryPortlet.tpl
@@ -0,0 +1,24 @@
+<div class="portlet">
+
+<h2 class="portlet-title">
+Categories
+<com:THyperLink
+	Text="[+]"
+	Tooltip="Create a new category"
+	NavigateUrl=<%= $this->Service->constructUrl('Posts.NewCategory') %>
+	Visible=<%= $this->User->isInRole('admin') %> />
+</h2>
+
+<div class="portlet-content">
+<ul>
+<com:TRepeater ID="CategoryList" EnableViewState="false">
+	<prop:ItemTemplate>
+	<li>
+	<a href="<%# $this->Service->constructUrl('Posts.ListPost',array('cat'=>$this->DataItem->ID)) %>"><%# $this->DataItem->Name . ' (' . $this->DataItem->PostCount . ')' %></a>
+	</li>
+	</prop:ItemTemplate>
+</com:TRepeater>
+</ul>
+</div><!-- end of portlet-content -->
+
+</div><!-- end of portlet -->
diff --git a/demos/blog/protected/Portlets/LoginPortlet.php b/demos/blog/protected/Portlets/LoginPortlet.php
new file mode 100644
index 00000000..0085c17f
--- /dev/null
+++ b/demos/blog/protected/Portlets/LoginPortlet.php
@@ -0,0 +1,22 @@
+<?php
+
+Prado::using('Application.Portlets.Portlet');
+
+class LoginPortlet extends Portlet
+{
+	public function validateUser($sender,$param)
+	{
+		$authManager=$this->Application->getModule('auth');
+		if(!$authManager->login($this->Username->Text,$this->Password->Text))
+			$param->IsValid=false;
+	}
+
+	public function loginButtonClicked($sender,$param)
+	{
+		if($this->Page->IsValid)
+			$this->Response->reload();
+			//$this->Response->redirect($this->Application->getModule('auth')->getReturnUrl());
+	}
+}
+
+?>
\ No newline at end of file
diff --git a/demos/blog/protected/Portlets/LoginPortlet.tpl b/demos/blog/protected/Portlets/LoginPortlet.tpl
new file mode 100644
index 00000000..6f8c5d4a
--- /dev/null
+++ b/demos/blog/protected/Portlets/LoginPortlet.tpl
@@ -0,0 +1,36 @@
+<div class="portlet">
+
+<h2 class="portlet-title">Login</h2>
+
+<com:TPanel CssClass="portlet-content" DefaultButton="LoginButton">
+Username
+<com:TRequiredFieldValidator
+	ControlToValidate="Username"
+	ValidationGroup="login"
+	Text="...is required"
+	Display="Dynamic"/>
+<br/>
+<com:TTextBox ID="Username" />
+<br/>
+
+Password
+<com:TCustomValidator
+	ControlToValidate="Password"
+	ValidationGroup="login"
+	Text="...is invalid"
+	Display="Dynamic"
+	OnServerValidate="validateUser" />
+<br/>
+<com:TTextBox ID="Password" TextMode="Password" />
+
+<br/>
+<com:TLinkButton
+	ID="LoginButton"
+	Text="Login"
+	ValidationGroup="login"
+	OnClick="loginButtonClicked" />
+| <a href="<%= $this->Service->constructUrl('Users.NewUser') %>">Register</a>
+
+</com:TPanel><!-- end of portlet-content -->
+
+</div><!-- end of portlet -->
diff --git a/demos/blog/protected/Portlets/Portlet.php b/demos/blog/protected/Portlets/Portlet.php
new file mode 100644
index 00000000..4b1c80e9
--- /dev/null
+++ b/demos/blog/protected/Portlets/Portlet.php
@@ -0,0 +1,7 @@
+<?php
+
+class Portlet extends TTemplateControl
+{
+}
+
+?>
\ No newline at end of file
diff --git a/demos/blog/protected/Portlets/SearchPortlet.php b/demos/blog/protected/Portlets/SearchPortlet.php
new file mode 100644
index 00000000..1bad7f1c
--- /dev/null
+++ b/demos/blog/protected/Portlets/SearchPortlet.php
@@ -0,0 +1,22 @@
+<?php
+
+Prado::using('Application.Portlets.Portlet');
+
+class SearchPortlet extends Portlet
+{
+	public function onInit($param)
+	{
+		parent::onInit($param);
+		if(!$this->Page->IsPostBack && ($keyword=$this->Request['keyword'])!==null)
+			$this->Keyword->Text=$keyword;
+	}
+
+	public function search($sender,$param)
+	{
+		$keyword=$this->Keyword->Text;
+		$url=$this->Service->constructUrl('SearchPost',array('keyword'=>$keyword));
+		$this->Response->redirect($url);
+	}
+}
+
+?>
\ No newline at end of file
diff --git a/demos/blog/protected/Portlets/SearchPortlet.tpl b/demos/blog/protected/Portlets/SearchPortlet.tpl
new file mode 100644
index 00000000..f88fca7e
--- /dev/null
+++ b/demos/blog/protected/Portlets/SearchPortlet.tpl
@@ -0,0 +1,21 @@
+<div class="portlet">
+
+<h2 class="portlet-title">Search</h2>
+
+<com:TPanel CssClass="portlet-content" DefaultButton="SearchButton">
+Keyword
+<com:TRequiredFieldValidator
+	ControlToValidate="Keyword"
+	ValidationGroup="search"
+	Text="...is required"
+	Display="Dynamic"/>
+<br/>
+<com:TTextBox ID="Keyword" />
+<com:TLinkButton
+	ID="SearchButton"
+	Text="Search"
+	ValidationGroup="search"
+	OnClick="search" />
+</com:TPanel><!-- end of portlet-content -->
+
+</div><!-- end of portlet -->
diff --git a/demos/blog/protected/application.xml b/demos/blog/protected/application.xml
new file mode 100644
index 00000000..9bca115c
--- /dev/null
+++ b/demos/blog/protected/application.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<application id="personal" mode="Debug">
+  <paths>
+    <using namespace="Application.Common.*" />
+  </paths>
+  <!-- modules configured and loaded for all services -->
+  <modules>
+    <!-- Remove this comment mark to enable caching
+    <module id="cache" class="System.Caching.TSqliteCache" />
+    -->
+    <!-- Remove this comment mark to enable PATH url format
+    <module id="request" class="THttpRequest" UrlFormat="Path" />
+    -->
+    <!--
+    <module id="session" class="THttpSession" />
+    <module id="log" class="System.Util.TLogRouter">
+      <route class="TBrowserLogRoute" />
+      <route class="TFileLogRoute" Categories="System" Levels="Notice,Warning,Error,Alert,Fatal" />
+    </module>
+    -->
+    <module class="System.Util.TParameterModule" ParameterFile="Application.Data.Options" />
+  </modules>
+  <services>
+    <!-- page service -->
+    <service id="page" class="TPageService" BasePath="Application.Pages" DefaultPage="Posts.ListPost">
+      <!-- modules configured and loaded when page service is requested -->
+      <modules>
+        <!-- user manager module -->
+        <module id="users" class="Application.Common.BlogUserManager" />
+        <!-- auth manager module -->
+        <module id="auth" class="System.Security.TAuthManager" UserManager="users" LoginPage="Posts.ListPost" />
+        <module id="data" class="Application.Common.BlogDataModule" />
+      </modules>
+	  <pages MasterClass="Application.Layouts.MainLayout" Theme="Basic" />
+    </service>
+  </services>
+</application>
\ No newline at end of file
diff --git a/demos/blog/sitemap.txt b/demos/blog/sitemap.txt
new file mode 100644
index 00000000..8326855b
--- /dev/null
+++ b/demos/blog/sitemap.txt
@@ -0,0 +1,106 @@
+Be careful about username case sensitivity!!
+
+Home : list of blogs filtered by a category or time range, with paging
+
+ViewBlog : read a single blog with all comments and a comment input form
+NewBlog : create a new blog, with file attachment form and THtmlArea
+EditBlog : edit an existing blog
+
+LoginUser : login page
+NewUser : create a new user
+EditUser : edit the current user
+
+Admin : whether allow multiple users, whether HTML is allowed (first user is always the admin)
+
+URL design:
+
+index.php?page=ListBlog&timespan=123,456&limit=123,456 : list of latest blogs, equivalent to:
+index.php?page=NewBlog
+index.php?page=EditBlog&id=123
+index.php?page=ViewBlog&id=123
+index.php?page=NewUser
+index.php?page=EditUser
+index.php?page=ViewUser
+index.php?page=Admin
+
+
+Use Case 1: Add a post
+1. Authorization check
+2. display UI for adding post
+3. input validation
+4. add post to DB
+5. display UI for post list
+
+
+DB Logic needed:
+
+class Post extends DataObject
+{
+	public $xxx;
+}
+
+class Comment extends DataObject
+{
+}
+
+class UserProfile extends DataObject
+{
+
+}
+
+class DataObject extends TComponent
+{
+	protected static $mapping=array();
+
+	public function __construct($db)
+	{
+	}
+
+	protected static function generateModifier($filter,$orderBy,$limit)
+	{
+		$modifier='';
+		if($filter!=='')
+			$modifier=' WHERE '.$filter;
+		if($orderBy!=='')
+			$modifier.=' ORDER BY '.$orderBy;
+		if($limit!=='')
+			$modifier.=' LIMIT '.$limit;
+		return $modifier;
+	}
+
+	public static function queryRow($filter='')
+	{
+		$modifier=self::generateModifier($filter,'','');
+	}
+
+	public static function query($filter='',$orderBy='',$limit='')
+	{
+		$modifier=self::generateModifier($filter,$orderBy,$limit);
+	}
+
+	public function save()
+	{
+	}
+
+	public function delete()
+	{
+	}
+}
+
+public function queryUsers($filter='',$sortBy='',$limit='')
+public function queryUser($id)
+public function insertUser($user)
+public function updateUser($user)
+public function deleteUser($id)
+
+public function queryPosts($filter='',$sortBy='',$limit='')
+public function queryPost($id)
+public function insertPost($post)
+public function updatePost($post)
+public function deletePost($id)
+
+public function queryComments($filter='',$sortBy='',$limit='')
+public function queryComment($id)
+public function insertComment($comment)
+public function updateComment($comment)
+public function deleteComment($id)
diff --git a/demos/blog/themes/Basic/style.css b/demos/blog/themes/Basic/style.css
new file mode 100644
index 00000000..b8e9ca89
--- /dev/null
+++ b/demos/blog/themes/Basic/style.css
@@ -0,0 +1,261 @@
+html {
+	margin: 0;
+	padding: 0;
+}
+
+body {
+	margin: 0;
+	padding: 0;
+	font-family: verdana, 'trebuchet ms', sans-serif;
+	font-size: 12px;
+	color: #333;
+	background: #36414d;
+	min-width:750px;
+}
+
+form {
+	margin: 0;
+	padding: 0;
+}
+
+a {
+	text-decoration: underline;
+}
+
+a img {
+	border: 0px none;
+}
+
+#page {
+	background:#fff;
+	margin:0 auto;
+	width:800px;
+}
+
+#header {
+	background: #a3b8cc;
+	border-bottom: 1px solid silver;
+}
+
+#header h1 {
+	padding:5px;
+	padding-left: 20px;
+    margin:0;
+	font-size: 16pt;
+}
+
+#header h2 {
+	padding:5px;
+	padding-left: 20px;
+    margin:0;
+	font-size: 12pt;
+}
+
+#main {
+	background:#fff;
+	float:left;
+	width:560px;
+	padding: 20px;
+}
+
+#main h2 {
+	border-bottom: 1px solid silver;
+}
+
+#sidebar {
+	background:#e6ecf2;
+	float:right;
+	width:200px;
+}
+
+#sidebar ul {
+	margin-bottom:0;
+}
+
+#sidebar h3, #sidebar p {
+	padding:0 10px 0 0;
+}
+
+#footer {
+	background:#fff;
+	clear:both;
+	color: gray;
+	font-size:8pt;
+	text-align:center;
+	padding-top:25px;
+	padding-bottom:10px;
+}
+
+.portlet {
+	margin: 10px;
+	border-bottom: 1px solid silver;
+	border-right: 1px solid silver;
+	background: #dae0e6;
+}
+
+.portlet-title {
+	/* ie win (5, 5.5, 6) bugfix */
+	p\osition: relative;
+	width: 100%;
+	w\idth: auto;
+
+	margin: 0;
+	border-left: 5px solid #36414d;
+	border-bottom: 1px solid silver;
+	padding: 5px;
+	color: #fff;
+	background: #a3b8cc;
+	font-size: 8pt;
+	font-weight: bold;
+	line-height: 1;
+	text-transform: uppercase;
+}
+
+.portlet-title a {
+	color: yellow;
+	text-decoration: none;
+	text-transform: none;
+}
+
+.portlet-title a:hover {
+	color: red;
+}
+
+.portlet-content {
+	margin: 0 0 10px 0;
+	border-top: 1px solid #cfd4d9;
+	padding: 10px 10px 0 10px;
+	font-size: 10px;
+}
+
+.portlet-content ul {
+	margin: 0 15px 10px 15px;
+	padding: 0;
+	list-style: square;
+}
+
+.portlet-content li {
+	color: #666;
+	margin-top: 3px;
+}
+
+.portlet-content a {
+	color: #36414d;
+	text-decoration: none;
+}
+
+.portlet-content a:hover {
+	color: red;
+}
+
+.post {
+	margin-bottom: 15px;
+}
+
+.post-title {
+	font-size: 14pt;
+	border-bottom: 1px silver solid;
+}
+
+.post-time {
+	font-size: 8pt;
+	color: gray;
+}
+
+.post-content {
+	margin-top: 10px;
+	margin-bottom: 10px;
+}
+
+.post-footer {
+	text-align: right;
+	font-size: 8pt;
+}
+
+.post-footer a {
+	color: #36414d;
+}
+
+.comments {
+	border-top: 1px silver solid;
+}
+
+.comments h3 {
+	font-size: 12pt;
+}
+
+.comment {
+	margin-bottom: 10px;
+}
+
+.comment-header {
+	background-color: #e6ecf2;
+	padding: 3px;
+	font-size: 8pt;
+}
+
+.grid {
+	width: 100%;
+}
+
+.grid td {
+	padding: 3px;
+}
+
+.grid-header {
+	color: #fff;
+	background: #a3b8cc;
+}
+
+.grid-row1 {
+	background: #dae0e6;
+}
+
+.grid-row2 {
+	background: #e6ecf2;
+}
+
+.grid-row-selected {
+	background: lightyellow;
+}
+
+.grid-pager {
+	text-align: right;
+	color: silver;
+}
+
+.grid-pager a {
+	color: green;
+	text-decoration: none;
+}
+
+.submenu {
+	margin-bottom: 10px;
+	border-bottom: 5px solid #a3b8cc;
+	padding-right: 10px;
+	text-align: right;
+}
+
+.submenu ul {
+	margin:0;
+	padding:0;
+	list-style:none;
+}
+
+.submenu li {
+	display:inline;
+	margin:0;
+	padding:0;
+}
+
+.submenu-active {
+	text-decoration: none;
+	background: #a3b8cc;
+	padding: 3px 5px 0 5px;
+}
+
+.submenu-inactive {
+	text-decoration: none;
+	background: #dae0e6;
+	padding: 3px 5px 0 5px;
+}
\ No newline at end of file
-- 
cgit v1.2.3