Вы находитесь на странице: 1из 39

Building an Online ASP based discussion forum

In part II of our membership and personalization series, we are going to enhance the
membership database introduced in our first article with a flexible discussion forum
built using SQL Server and ASP. By the end of this article, you will have the
knowledge and scripts necessary to add multiple forums to your web-site.

We are going to create a discussion forum with the following features:

• Unlimited number of forums


• Unlimited number of single-threaded topics per forum (easily extended to full-
threading)
• Automatic page-chunking of topics
• Integration with the Site Membership table
• Easy Administration Features
o Add/ Edit/ Remove Forums
o Quickly Edit/ Delete any Message

The Discussion Forum is built using the same methodology as the Site Membership
database. All logic that manipulates the database is contained within stored
procedures and database triggers. The ASP scripts are simply a middle tier for
interfacing the user with the database. The front-end client is a simple, script-free
HTML user-interface.

On the last page you will be able to download all the necessary SQL scripts and
sample files to start adding site membership and discussion forums. These scripts
were run and tested against IIS 4.0 with SQL Server 6.5. If you are familiar with SQL
you should be able to adapt them for other SQL backends.

Interface
Our goal is to provide you with a framework for building your own interactive and
personalized community. For this reason, the sample code merely demonstrates
each feature. We leave the task of clean integration with your system and backend
up to you.

The basic integration point is by calling each stored procedure. By understanding


the arguments required by each stored procedure, you can easily build your own
custom interface. For example, we created a simple index page for listing each
discussion forum. This list is displayed in the order the forums were added. Since
many sites rarely define new forums this page may be better served by a statically
designed page.

For example, lets look at the loop that generates the list of forums and see how
the URL for viewing a forum is constructed:

while not oRS.eof


Response.Write("<LI><A" &_
HREF=""viewForum.asp?forum_id=" & oRS.fields("forum_id") & """>")
Response.Write(oRS.fields("forum_name") & "</A>")
if not isnull(oRS.fields("forum_update")) then
Response.Write(" (last updated " & oRS.fields("forum_update") & ")")
end if
Response.Write("<BR>" & oRS.fields("forum_desc"))
oRS.movenext
wend
Each forum URL is constructed as pageName.asp?argumentList. For reading a
forum, the URL is viewForum.asp?forum_id=id. Therefore, to link directly to a
particular forum you can create a link similar to the following:

<A HREF="viewForum.asp?forum_id=1">Discuss Web Development</A>


With this approach you can build any custom layout or quickly link to the
discussion page from anywhere on your site.

This is the simplest integration point. This gets slightly more complex when you
want to interact with the user. For example, let's imagine you are creating a new
message page for a specific discussion forum. First, looking at the stored procedure
for adding messages, sp_starttopic, you will notice that the stored procedure
requires the forum id, a parent id if this is a reply, the user id posting the message, a
message name, and the message itself.

If you were to create a form for starting new topics, you need to make sure all this
information is specified and then passed back to the server. When creating the form,
we use hidden fields for contextual information (forum id, etc) the user does not
need to see. For example:

' BuildForm and BuildInput are helper functions for generating the FORM
and INPUT tags.
Response.Write("<P>Start new topic in " & sForumName)
Response.Write(BuildForm("startTopic","post"))
Response.Write("<TABLE>")
Response.Write("<TR><TD>Forum:</TD><TD>" &_
sForumName & "</TD></TR>")
Response.Write("<TR><TD>User Name:</TD><TD>" &_
Session("u_name") & "</TD></TR>")
Response.Write("<TR><TD>Topic:</TD><TD>" &_
BuildInput("text","topic_name","",20,200) & "</TD></TR>")
Response.Write("<TR><TD valign=top>Message</TD><TD>" &_
BuildTextArea("topic_message","",10,35) & "</TD></TR>")
Response.Write("<TR><TD COLSPAN=2>" &_
BuildInput("submit","action",ACTIONSAVE,"","") & "</TD></TR>")
Response.Write("</TABLE>")
This creates the front-end interface. The next step is to write the interface
between the user's input and the backend database. This is as simple as transferring
the user's arguments into the sp_starttopic stored procedure:

if (request.form("action")=ACTIONSAVE) then
Set oCmd = GetStoredProcedure(getConnection(),"sp_StartTopic")
oCmd.Parameters.append oCmd.CreateParameter("forum_id", adInteger,
adParamInput,10,forumID)
oCmd.Parameters.append oCmd.CreateParameter("topic_parent",
adInteger, adParamInput,10,-1)
oCmd.Parameters.append oCmd.CreateParameter("u_id", adInteger,
adParamInput,10,session("u_id"))
oCmd.Parameters.append oCmd.CreateParameter("topic_name", adVarChar,
adParamInput,200,escapeString(Request.Form("topic_name")))
oCmd.Parameters.append oCmd.CreateParameter("topic_message",
adLongVarChar,
adParamInput,32000,escapeStringWithCR(Request.Form("topic_message")))
oCmd.execute()
end if
By building your server interfaces correctly, you are essentially creating an HTTP
based API for your web-site. With a little more work, you create an interface page
that could allow external agents to manipulate and query your backend database.
With this approach, you would most likely return structured information instead of
traditional web pages. We will be pursuing this concept more in future articles.

Next, we continue by looking at the backend implementation. First we start with


the database schema.

Schema
The discussion forum consists of two tables (three if you include the user
membership table):

t_user Table
This is the user membership table from the first article. This table stores information
about each registered user. The most important field in the t_user table is u_id which
represents an individual user. This field is used to associate a user with a particular
message.

t_forums Table
This table contains the list of message forums. Adding new forums is as simple as
adding new records to this table.

Column Data Type


Special Notes
Name (length)
forum_id Integer Auto-incrementing primary key. Used to associate the
posted message with a particular forum.
forum_name String (50) The name of the forum.
forum_desc String (255) A short description of the forum.
forum_update DateTime The date and time of the last update. This field is
automatically updated whenever a message is posted.
Create Table dbo.t_forums
(
forum_id int Not Null Identity (1, 1),
forum_name varchar(50) Not Null,
forum_desc varchar(255) Not Null,
forum_update datetime Null
)
Go
Alter Table dbo.t_forums Add Constraint
PK_t_forums Primary Key Nonclustered
(
forum_id
)
Go

t_topics Table
This table contains the list of messages. Each message is associated with a specific
forum through the forum_id field. We designed this table so that it can be extended
to support threaded messages. However, for this example, we limit all discussions to
a single thread (just like the SiteExperts.com discussion forums).

This table also contains two triggers for handling message deletions, message
counts and last updates. We will explain the triggers in more detail when we walk
through the stored procedures.

Data Type
Column Name Special Notes
(length)
forum_id Integer The forum the message is posted in.
Auto-incrementing primary key representing the
topic_id Integer
individual message.
u_id Integer The ID of the user who posted the message.
topic_name String (200) The name of the discussion thread.
topic_create DateTime The time and date the message was posted.
The time and date the thread was responded to. Only
topic_update DateTime
updated for the original message of the thread.
topic_count Integer The number of replies to the message.
The topic_id of the parent message. Defaults to -1 for new
topic_parent Integer
threads.
topic_message Text The actual message.
Create Table dbo.t_topics
(
forum_id int Not Null,
topic_id int Not Null Identity (1, 1),
u_id int Not Null,
topic_name varchar(200) Null,
topic_create datetime Not Null Constraint DF_t_topics_topic_date
Default (getdate()),
topic_update datetime Not Null Constraint DF_t_topics_topic_update
Default (getdate()),
topic_count int Not Null Constraint DF_t_topics_topic_count Default
(0),
topic_parent int Not Null Constraint DF_t_topics_topic_parent Default
((-1)),
topic_message text Null
)
Go
Alter Table dbo.t_topics Add Constraint
FK_t_topics_t_forums Foreign Key
(
forum_id
) References dbo.t_forums
(
forum_id
)
Go
Alter Table dbo.t_topics Add Constraint
FK_t_topics_t_user Foreign Key
(
u_id
) References dbo.t_user
(
u_id
)
Go

Create Trigger t_topics_Delete


On dbo.t_topics
For Delete
As
Declare @topic_id int
select @topic_id = (select topic_parent from deleted)
if @topic_id=-1
begin
DELETE t_topics FROM t_topics a , deleted b WHERE (a.topic_parent
= b.topic_id)
end
else
update t_topics
set a.topic_count = a.topic_count -1
from t_topics a, deleted b
where a.topic_id = b.topic_parent
Go

Create Trigger t_topics_Insert


On dbo.t_topics
For Insert
As
if (select topic_parent from inserted)<>-1
update t_topics
set a.topic_count = a.topic_count + 1, a.topic_update = getDate()
from t_topics a, inserted b where a.topic_id = b.topic_parent
update t_forums set forum_update = getDate()
from t_forums a, inserted b where a.forum_id = b.forum_id
Go

Table Relationships
The relationships between each table is fairly straightforward. We reuse the same
field name in all tables to represent the relationship. All our relationships are one-to-
many relationships between the primary and foreign key. This means that each
primary key can be related with any number of records in the related table. For
example, looking at the first relationship below, an individual user can post any
number of messages.

Primary Key Foreign Key


t_user->u_id t_topics->u_id
t_forums->forum_id t_topics->forum_id
t_topics->topic_id t_topics->topic_parent
Our approach is to always build and test the database before writing any of the
ASP pages. Therefore, next we walk you through the stored procedures used to
access and manipulate the discussion forums. These stored procedures expose all
the operations for the discussion forum.

Stored Procedures
The discussion forum uses a collection of stored procedures for posting and editing
messages.

Store Procedure Description


sp_deletemessage Used by the site administrator to delete a message or message
thread from the system.
sp_editmessage Used by the site administrator (and possibly the message poster)
to edit a specific message.
sp_getforum Returns all the information (name, description, and update) for a
specific forum.
sp_getmessagesfortopic Given a topic id, returns the message and all the replies to the
message.
sp_gettopicsforforum Given a forum id, returns all the top-level topics sorted by most
recent update.
sp_saveforum Creates or edits an existing forum name and description.
sp_starttopic Used to post a new message or reply to an existing message.
We will now take you through each stored procedure.

Listing Forums and Messages


The stored procedures sp_gettopicsforforum and sp_getmessagesfortopic are used to
retrieve individual messages and the messages within each forum. (we create the list
of forums directly from the t_forums table so no stored procedure is used).

sp_gettopicsforforum
This stored procedure returns two recordsets given a forum id: one
containing the forum information and the other containing the list of topics
for the specified forum. The forum information allows you to access meta-
information (eg., forum name and description). The list of topics is based on
all messages in the forum that have a topic_parent of -1. This defines that
the message is the first message in the topic. All replies have a topic_parent
pointing to the originating message.

Create Procedure sp_getTopicsForForum


@forum_id int
As
set nocount on
SELECT * FROM t_forums WHERE forum_id=@forum_id
SELECT *, t_user.u_name, t_user.u_id FROM t_topics
INNER JOIN t_user ON t_topics.u_id = t_user.u_id
WHERE (t_topics.forum_id = @forum_id) AND (t_topics.topic_parent =
- 1)
ORDER BY t_topics.topic_update DESC
return

sp_getmessagesfortopic
This stored procedure returns two recordsets representing a topic and all its replies.
The first recordset returns the original message with information about the forum the
message is contained within. The second recordset returns all the replies to the
original message. Message replies are easily retrieved by finding messages with the
topic_parent equal to the starting topic's ID.

Create Procedure sp_GetMessagesForTopic


@topic_id int
As
set nocount on
SELECT t_forums.forum_name, t_user.u_name, t_topics.forum_id,
t_topics.topic_id, t_topics.topic_name, t_topics.topic_create,
t_topics.u_id, t_topics.topic_update, t_topics.topic_count,
t_topics.topic_message
FROM t_topics
INNER JOIN t_forums ON t_topics.forum_id = t_forums.forum_id
INNER JOIN t_user ON t_topics.u_id = t_user.u_id
WHERE (t_topics.topic_id = @topic_id)
SELECT t_user.u_name, t_topics.*
FROM t_topics
INNER JOIN t_user ON t_topics.u_id = t_user.u_id
WHERE (t_topics.topic_parent = @topic_id)
ORDER BY t_topics.topic_create DESC
return

sp_getforum
This stored procedure is very simple and just returns all the meta-information for a
particular forum.

Create Procedure sp_GetForum


@forum_id int
As
set nocount on
SELECT forum_id, forum_name, forum_desc, forum_update
FROM t_forums
WHERE (forum_id = @forum_id)
return

Message Manipulation
The remaining stored procedures are used to post, edit, and delete messages.

sp_deletemessage
This stored procedure is used to delete an individual message from the message
board. This stored procedure is very simple and only deletes a single message.

Create Procedure sp_DeleteMessage


@topic_id int
As
set nocount on
DELETE FROM t_topics WHERE (topic_id = @topic_id)
return
By itself, this stored procedure is inadequate. For example, when you delete a
message with replies you most likely want the replies to also be deleted. This does
happen, but it happens inside the t_topics table where through a delete trigger.
Triggers are specialized stored procedures that are automatically executed when a
table is updated either through an insert, edit, or delete operation.

If you delete the first message in a topic, we go ahead and delete all replies. If the
message you are deleting is a reply, then we decrement the counter that tracks how
many replies have been posted.

Create Trigger t_topics_Delete


On dbo.t_topics
For Delete
As
Declare @topic_id int
SELECT @topic_id = (select topic_parent from deleted)
if @topic_id=-1
begin
DELETE t_topics FROM t_topics a , deleted b
WHERE (a.topic_parent = b.topic_id)
end
else
update t_topics
set a.topic_count = a.topic_count -1
from t_topics a, deleted b
where a.topic_id = b.topic_parent

sp_editmessage
This stored procedure is used by the administration screens to allow you to update
an existing message.

Create Procedure sp_EditMessage


@topic_id int,
@topic_name varchar(200)="",
@topic_message text
As
set nocount on
UPDATE t_topics
SET topic_name = @topic_name, topic_message=@topic_message
WHERE (topic_id = @topic_id)
return

sp_starttopic
This stored procedure is used to post new messages to a specific forum. By
specifying a topic_parent, you can use this procedure to reply to a specific message.

Create Procedure sp_StartTopic


@forum_id int,
@topic_parent int = null,
@u_id int,
@topic_name varchar(200),
@topic_message text
As
set nocount on
INSERT INTO
t_topics(forum_id, u_id, topic_name, topic_parent, topic_message)
VALUES (@forum_id, @u_id, @topic_name, @topic_parent,@topic_message)

return
Whenever a message is posted, we call an update trigger on the t_topics table.
This trigger updates the message counter and last update information:

Create Trigger t_topics_Insert


On dbo.t_topics
For Insert
As
if (select topic_parent from inserted)<>-1
update t_topics set a.topic_count = a.topic_count + 1,
a.topic_update = getDate()
from t_topics a, inserted b
where a.topic_id = b.topic_parent
update t_forums
set forum_update = getDate()
from t_forums a, inserted b
where a.forum_id = b.forum_id
These are all the stored procedures used by the message forum. If you are looking
for a challenge, try extending the stored procedures and table to support a threaded
message forum (hint - think about how you can use the topic_parent field). If you
choose to add threading, be sure to update the delete stored procedure and update
and delete triggers. They are not currently designed to work correctly with a
threaded system.

We are now ready to take you through the ASP Scripts that interact with the
database and expose a simple user-interface.

The ASP Scripts


Our ASP scripts are designed to act as the foundation for your discussion forums.
The user-interface is minimal and designed to just expose necessary functionality.
We created three ASP pages for administering the discussion forums four ASP pages
that provide the end-user experience:

User Pages
forums.asp Dipslay a list of each discussion forum and a description.
viewForum.asp Display a list of messages within a specific forum.
Displays an individual message with all the replies. This page includes
viewTopic.asp
a form for replying to the thread.
startTopic.asp Displays a form for starting a new topic.
Administration Pages
editForum.asp Allows you to select a forum to edit or to add new forums.
AdminForum.asp Used to edit the messages within an individual forum.
adminTopic.asp Used to edit or delete an individual message or reply.
The user pages integrate with the site membership information introduced in
our first article. If you are not logged in, you can only read the articles. To post or
reply to an article, you must be logged into the system.

Next we take you through the user pages.

The entry page to the discussion forum is forums.asp. This page simply enumerates
and outputs all the discussion forums with their descriptions. From this page, the
user can select an individual forum taking them to the viewForum.asp.

Forums.asp
The script for forums.asp is very simple. Rather than execute a query, we merely
open the t_forums table and output each record. In a production system, you may
want to add ordering information to the table and apply a sort, or depending upon
your layout needs, you may want to create an index page manually.

<%@ Language=VBScript %>


<% option explicit %>
<HTML>
<HEAD>
<!-- #include virtual="/inc/utility.asp" -->
</HEAD>
<BODY>

<%
Dim oRS, oCmd, oConn
Const ACTIONSAVE = "Save..."

set oConn = GetConnection()

Response.Write("<H2>" & HOMEPAGE & " : " & SITENAME & " Discussion
Forums</H2>")
Set oRS = GetTable(oConn,"t_forums")
Response.Write("<UL>")
while not oRS.eof
Response.Write("<LI><A HREF=""viewForum.asp?forum_id=" &
oRS.fields("forum_id") & """>")
Response.write(oRS.fields("forum_name") & "</A>")
if not isnull(oRS.fields("forum_update")) then
' Output date of last update if one exists
Response.Write(" (last updated " & oRS.fields("forum_update") &
")")
end if
Response.Write("<BR>" & oRS.fields("forum_desc"))
oRS.movenext
wend
Response.Write("</UL>")
%>
</BODY>
</HTML>

viewForum.asp
viewForum.asp is a much more interesting script. This page outputs the topics within
a particular forum. To accomodate larger topic lists, this page also supports paging
by displaying 20 topics at a time (this value can be changed).
Below we take you through the commented script.

<%@ Language=VBScript %>


<% option explicit

Dim forumID, iStart

' The forum being viewed


forumID = Request.QueryString("forum_id")

' The start record


iStart = Request.QueryString("start")

' Make sure forum and start records are numbers


if forumID="" or not isnumeric(forumid) then
Response.redirect("forums.asp")
if not isnumeric(iStart) or iStart<0 then iStart=0

Dim oCmd, oRS

' Get the list of messages


Set oCmd = GetStoredProcedure(getConnection(),"sp_getTopicsForForum")
oCmd.Parameters.append oCmd.CreateParameter("forum_id", adInteger,
adParamInput,10,forumID)
set oRS = oCmd.execute()

' Two recordsets are returned. The first recordset contains


' information about the forum. If no forum exists (bad id) then
' redirect back to the forum list
if oRS.eof then Response.Redirect("forums.asp")

' Get forum information


Dim sForumName, sForumDesc
sForumName = oRS.fields("forum_name")
sForumDesc = oRS.fields("forum_desc")

' Get next recordset - the list of topics


set oRS = oRS.nextRecordset

%>
<HTML>
<HEAD>
<!-- #include virtual="/inc/utility.asp" -->
<TITLE><%=sForumName%></TITLE>
</HEAD>
<BODY>

<%

' Output Header


Response.Write("<H2>" & HOMEPAGE & " : <A
HREF=""forums.asp"">Discussion Forums</A> : " & sForumName & "
Forum</H2><P>" & sForumDesc)

' This defines the number of records to display per page


const chunking = 20
Dim iCount
iCount = 0

' A link to start a new topic (security check occurs on the


starttopic.asp page)
Response.Write("<P><B><A HREF=""startTopic.asp?forum_id=" & forumID &
""">Start New Topic</A></B>")

' If messages exist, move to the start record (for paging)


if not oRS.eof then oRS.move iStart

' If starting past first record, display previous page link


if iStart>0 then
Response.Write("<P><A HREF=""viewForum.asp?forum_id=" & forumID &
"&start=" & (iStart - Chunking) & """>Previous Messages...</A>")
end if

' If no more records, display message


if oRS.eof then
Response.Write("<P><B><EM>No more messages</EM></B>")
end if

' Output each topic until no more topics or reach per page record limit
Response.Write("<UL>")
while not oRS.eof and iCount<chunking
' Topic Name
Response.Write("<LI><A HREF=""viewtopic.asp?topic_id=" &
oRS.fields("topic_id") & """>" & oRS.fields("topic_name") & "</A>")

' Topic Stats


if (oRS.fields("topic_count")=0) then
Response.Write(" (Started by " & oRS.fields("u_name") & " on " &
oRS.fields("topic_create") & ")")
else
Response.Write(" (Started by " & oRS.fields("u_name") & ", Last
reply on " & oRS.fields("topic_update") & " : " &
oRS.fields("topic_count") & " Repl")
if oRS.fields("topic_count")=1 then Response.write("y)") else
Response.write("ies)")
end if

' Count rows


iCount = iCount + 1
oRS.movenext
wend
Response.Write("</UL>")

' If more records, output link to next page


if not oRS.eof then
Response.Write("<A HREF=""viewForum.asp?forum_id=" & forumID &
"&start=" & (iCount + iStart) & """>More Messages...</A>")
end if
%>
</BODY>
</HTML>
On the next page, we continue by explaining how to create new topics and display
the topics and replies.
User Pages - Viewing Messages
Now we take you through the two asp pages for displaying messages and starting
new topics.

viewTopic.asp
When you view an individual topic, we first display the original message then we
display all the replies. For this page, we did not implement chunking of the replies.
In an active message board you may want to enhance this page with message
chunking. You can easily add chunking by copying the script from the previous page.

<%@ Language=VBScript %>


<% option explicit

dim topicid

' The topic being viewed


topicid = Request.QueryString("topic_id")

' Make sure topic is a number


if topicID="" or not isnumeric(topicID) then
Response.redirect("forums.asp")

Dim oCmd, oRS

if Request.Form("action")=actionSave then
' Save a message reply
' Replies are submitted directly back to the viewing page.
' This reply is automatically returned with the query that populates
the page.
Set oCmd = GetStoredProcedure(getConnection(),"sp_StartTopic")
oCmd.Parameters.append oCmd.CreateParameter("forum_id", adInteger,
adParamInput,10,Request.Form("forum_id"))
oCmd.Parameters.append oCmd.CreateParameter("topic_parent",
adInteger, adParamInput,10,Request.Form("topic_id"))
oCmd.Parameters.append oCmd.CreateParameter("u_id", adInteger,
adParamInput,10,session("u_id"))
oCmd.Parameters.append oCmd.CreateParameter("topic_name", adVarChar,
adParamInput,200,"")
oCmd.Parameters.append oCmd.CreateParameter("topic_message",
adLongVarChar,
adParamInput,32000,escapeStringWithCR(Request.Form("topic_message")))
oCmd.execute()
Response.Write("Message Posted")
end if

' Get Message for specified topic


Set oCmd = GetStoredProcedure(getConnection(),"sp_getMessagesForTopic")
oCmd.Parameters.append oCmd.CreateParameter("topic_id", adInteger,
adParamInput,10,topicID)
set oRS = oCmd.execute()

' Two recordsets are returned. The first recordset


' contains forum information, the number of replies,
' and the original message
' If bad forum ID no records are returned
if oRS.eof then Response.Redirect("forums.asp")

' Get forum information and reply count


Dim sForumName, sForumDesc, iForumID, iReplies
iForumID = oRS.fields("forum_id")
sForumName = oRS.fields("forum_name")
iReplies = oRS.fields("topic_count")

%>
<HTML>
<HEAD>
<!-- #include virtual="/inc/utility.asp" -->
<TITLE><%=sForumName%></TITLE>
</HEAD>
<BODY>

<%

const actionSave = "Post..."

Response.Write("<H2>" & HOMEPAGE & " : <A


HREF=""forums.asp"">Discussion Forums</A> : <A HREF=""viewforum.asp?
forum_id=" & iforumid & """>" & sForumName & " Forum</A></H2><P>")

' Output original message


Response.Write("<TABLE BGCOLOR=lightgrey WIDTH=400><TR><TD><FONT
SIZE=+1>"
Response.Write("<B>" & oRS.fields("topic_name") &
"</B></FONT></TD></TR>")
Response.Write("<TR><TD>" & oRS.fields("topic_message") & "</TD></TR>")
Response.Write("<TR><TD><SMALL><EM>Started By " & oRS.fields("u_name"))
Response.Write(" on:" & oRS.fields("topic_create") &
"</EM></SMALL></TD></TR></TABLE>")

' Get next recordset. This contains all the replies


' to the original message
set oRS = oRS.nextrecordset

' If replies, start outputting


if (not oRS.eof) then
' First display number of replies
if iReplies=1 then
Response.Write("<P>1 Reply")
else
Response.Write("<P>" & iReplies & " Replies")
end if
' Output each reply
while not oRS.eof
Response.Write("<P><B>" & oRS.fields("u_name") & " on " &
oRS.fields("topic_update") & "</B><BR>")
Response.Write(oRS.fields("topic_message"))
oRS.movenext
wend
end if

' If user logged in display "reply" form


if session("u_id")<>"" then
Response.Write(BuildForm("startTopic","post"))
' Store topic and forum in a hidden field
Response.Write(BuildInput("hidden","topic_id",TopicID,10,10))
Response.Write(BuildInput("hidden","forum_id",iForumID,10,10))
Response.Write("<HR ALIGN=LEFT WIDTH=400><B>Post Reply...</B></BR>")
Response.Write("<TABLE>")
Response.Write("<TR><TD>User Name:</TD><TD>" & Session("u_name") &
"</TD></TR>")
Response.Write("<TR><TD VALIGN=TOP>Message:</TD><TD>" &
BuildTextArea("topic_message","",10,35) & "</TD></TR>")
Response.Write("<TR><TD COLSPAN=2>" &
BuildInput("submit","action",ACTIONSAVE,"","") & "</TD></TR>")
Response.Write("</TABLE>")
else
' Not logged in - can't reply
Response.Write("<P>You must <A HREF=""/members/logon.asp"">Log-on</A>
to post replies")
end if
%>
</BODY>
</HTML>
The last task for creating the user pages is to create a page for starting new
topics.

startTopic.asp
Compared to the previous page, this page is quite simple. It contains a form for
starting new topics. For simplicity, we filter and automatically escape all HTML. This
can be easily changed by modifying the escape* functions contained within the
utility.asp file. Allowing HTML can create a real challenge to administrating and
managing your discussion forums. We allow HTML on our message boards. However,
our algorithm for checking HTML is very simplistic so we often have to manually fix
messages that unintentionally disrupt the page's layout.

<%@ Language=VBScript %>


<% option explicit

dim forumID

' Start topic in which forum


forumID = Request.QueryString("forum_id")
if forumID="" or not isnumeric(forumid) then
Response.redirect("forums.asp")

' Make sure logged in


if session("u_id")="" then Response.Redirect("/members/logon.asp")

Dim oCmd, oRS

' Get forum information


Set oCmd = GetStoredProcedure(getConnection(),"sp_getForum")
oCmd.Parameters.append oCmd.CreateParameter("forum_id", adInteger,
adParamInput,10,forumID)
set oRS = oCmd.execute()

' Make sure valid forum


if oRS.eof then Response.Redirect("forums.asp")
Dim sForumName, sForumDesc
' Retrieve forum information
sForumName = oRS.fields("forum_name")
sForumDesc = oRS.fields("forum_desc")

oRS.close()

%>
<HTML>
<HEAD>
<!-- #include virtual="/inc/utility.asp" -->
<TITLE><%=sForumName%></TITLE>
</HEAD>
<BODY>

<%

const actionSave = "Post..."

Response.Write("<H2>" & HOMEPAGE & " : <A


HREF=""forums.asp"">Discussion Forums</A> : <A HREF=""viewforum.asp?
forum_id=" & forumid & """>" & sForumName & " Forum</A></H2><P>")

if Request.Form("action")=actionSave then
' Save new topic
Set oCmd = GetStoredProcedure(getConnection(),"sp_StartTopic")
oCmd.Parameters.append oCmd.CreateParameter("forum_id", adInteger,
adParamInput,10,forumID)
oCmd.Parameters.append oCmd.CreateParameter("topic_parent",
adInteger, adParamInput,10,-1)
oCmd.Parameters.append oCmd.CreateParameter("u_id", adInteger,
adParamInput,10,session("u_id"))
' Escape all HTML
oCmd.Parameters.append oCmd.CreateParameter("topic_name", adVarChar,
adParamInput,200,escapeString(Request.Form("topic_name")))
oCmd.Parameters.append oCmd.CreateParameter("topic_message",
adLongVarChar,
adParamInput,32000,escapeStringWithCR(Request.Form("topic_message")))
oCmd.execute()
Response.Write("<B>Topic: " &
escapeString(Request.Form("topic_name")) & "</B><BR>")
Response.Write(escapeStringwithCR(Request.Form("topic_message")))
Response.Write("<P><A HREF=""viewForum.asp?forum_id=" & forumID &
""">View Messages...</A>")
else
' Output form for posting message
Response.Write("<P>Start new topic in " & sForumName)
Response.Write(BuildForm("startTopic","post"))
Response.Write("<TABLE>")
Response.Write("<TR><TD>Forum:</TD><TD>" & sForumName & "</TD></TR>")
Response.Write("<TR><TD>User Name:</TD><TD>" & Session("u_name") &
"</TD></TR>")
Response.Write("<TR><TD>Topic:</TD><TD>" &
BuildInput("text","topic_name","",20,200) & "</TD></TR>")
Response.Write("<TR><TD valign=top>Message</TD><TD>" &
BuildTextArea("topic_message","",10,35) & "</TD></TR>")
Response.Write("<TR><TD COLSPAN=2>" &
BuildInput("submit","action",ACTIONSAVE,"","") & "</TD></TR>")
Response.Write("</TABLE>")
end if

%>
</BODY>
</HTML>
This defines all the user pages for the discussion forum. Next we take you through
the pages for administrating and managing the message boards.

Administration Pages
Equally important as the user pages are the administration pages. From these pages
you can create new forums and edit or delete existing messages. The pages for
creating forums and choosing a topic are very similar to the pages used to view
forums and topics. Therefore, we are not going to go into those pages details (they
are included in the download). However, we are going to show you the script for the
edit topic page. This page is interesting because it provides one-click in place editing
of each message.

<%@ Language=VBScript %>


<% option explicit

' The ID of the topic you are editing


dim topicid
topicid = Request.QueryString("topic_id")

if topicID="" or not isnumeric(topicID) then


Response.redirect("forums.asp")

' This should be updated to the administrators account


' (for real security - the admin pages should be
' in an NT-secured administrator directory
if session("u_id")="" then Response.Redirect("/members/logon.asp")

Dim oCmd, oRS

' The 3 actions available


const ACTIONDELETE = "Delete"
const ACTIONEDIT = "Edit"
const ACTIONSAVE = "Save"

if Request.Form("action")=ACTIONDELETE then
' Delete the message
Set oCmd = GetStoredProcedure(getConnection(),"sp_DeleteMessage")
oCmd.Parameters.append oCmd.CreateParameter("topic_id", adInteger,
adParamInput,200,Request.Form("topic_id"))
oCmd.execute()
end if

if Request.Form("action")=ACTIONSAVE then
' Save Updated Message
Set oCmd = GetStoredProcedure(getConnection(),"sp_EditMessage")
oCmd.Parameters.append oCmd.CreateParameter("topic_id", adInteger,
adParamInput,10,Request.Form("topic_id"))
oCmd.Parameters.append oCmd.CreateParameter("topic_name", adVarChar,
adParamInput,200,escapeString(Request.Form("topic_name")))
oCmd.Parameters.append oCmd.CreateParameter("topic_message",
adLongVarChar,
adParamInput,32000,escapeStringWithCR(Request.Form("topic_message")))
oCmd.execute()
end if

' Get the messages for the topic


' (same technique as the user page)
Set oCmd = GetStoredProcedure(getConnection(),"sp_getMessagesForTopic")
oCmd.Parameters.append oCmd.CreateParameter("topic_id", adInteger,
adParamInput,10,topicID)
set oRS = oCmd.execute()

if oRS.eof then Response.Redirect("editForums.asp")

Dim sForumName, sForumDesc, iForumID, iReplies


' Forum information
iForumID = oRS.fields("forum_id")
sForumName = oRS.fields("forum_name")
iReplies = oRS.fields("topic_count")

%>
<HTML>
<HEAD>
<!-- #include virtual="/inc/utility.asp" -->
<TITLE><%=sForumName%></TITLE>
</HEAD>
<BODY>

<%

Response.Write("<H2>" & HOMEPAGE & " : <A HREF=""manager.asp"">Site


Administrator</A> : <A HREF=""editforums.asp"">Forum Manager</A> : <A
HREF=""adminforum.asp?forum_id=" & iForumID & """>" & sForumName &
"</A></H2>")

Response.Write("<P>You can edit or delete messages in this topic.


Deleting the starting message removes the entire topic.")
Response.Write(BuildForm("","POST") & "<TABLE BGCOLOR=lightgrey
WIDTH=400>")
if cInt(Request.Form("topic_id"))=cInt(oRS.fields("topic_id")) and
Request.Form("action")=ACTIONEDIT then
' Editing the original topic
Response.Write("<TR><TD>" &
BuildInput("text","topic_name",oRS.fields("topic_name"),25,200) &
"</TD></TR>")
Response.Write("<TR><TD>" &
BuildTextArea("topic_message",oRS.fields("topic_message"),15,35) &
"</TD></TR>")
Response.Write("<TR><TD><SMALL><EM>Started By " &
oRS.fields("u_name") & " on:" & oRS.fields("topic_create") &
"</EM></SMALL></TD></TR>")
Response.Write("<TR><TD>" &
BuildInput("hidden","topic_id",oRS.fields("topic_id"),"","") &
BuildInput("submit","action",ACTIONSAVE,"","") & " " &
BuildInput("submit","action",ACTIONDELETE,"","") & "</TD></TR>")
else
' Not editing original topic - just output
Response.Write("<TR><TD><FONT SIZE=+1><B>" & oRS.fields("topic_name")
& "</B></FONT></TD></TR>")
Response.Write("<TR><TD>" & oRS.fields("topic_message") &
"</TD></TR>")
Response.Write("<TR><TD><SMALL><EM>Started By " &
oRS.fields("u_name"))
Response.Write(" on:" & oRS.fields("topic_create") &
"</EM></SMALL></TD></TR>")
' Output edit and delete buttons
Response.Write("<TR><TD>" &
BuildInput("hidden","topic_id",oRS.fields("topic_id"),"",""))
Response.Write(BuildInput("submit","action",ACTIONEDIT,"","") & " ")
Response.Write(BuildInput("submit","action",ACTIONDELETE,"","") &
"</TD></TR>")
end if
Response.Write("</TABLE></FORM>")
set oRS = oRS.nextrecordset

' Output messages


' We reused much of the code from viewTopic.asp
if (not oRS.eof) then
if iReplies=1 then
Response.Write("<P>1 Reply")
else
Response.Write("<P>" & iReplies & " Replies")
end if
while not oRS.eof
Response.Write(BuildForm("","POST"))
if cInt(Request.Form("topic_id"))=cInt(oRS.fields("topic_id")) and
Request.Form("action")=ACTIONEDIT then
' Editing reply - display form
Response.Write("<P><B>" & oRS.fields("u_name") & " on " &
oRS.fields("topic_update") & "</B><BR>")
Response.Write("<TR><TD>" &
BuildTextArea("topic_message",oRS.fields("topic_message"),15,35) &
"</TD></TR>")
Response.Write("<BR>" &
BuildInput("hidden","topic_name","","","") &
BuildInput("hidden","topic_id",oRS.fields("topic_id"),"","") &
BuildInput("submit","action",ACTIONSAVE,"","") & " " &
BuildInput("submit","action",ACTIONDELETE,"","") & "</FORM>")
else
' Not editing reply - output
Response.Write("<P><B>" & oRS.fields("u_name") & " on " &
oRS.fields("topic_update") & "</B><BR>")
Response.Write(oRS.fields("topic_message"))
' Output Edit and Delete buttons
Response.Write("<TR><TD>" &
BuildInput("hidden","topic_id",oRS.fields("topic_id"),"",""))
Response.Write(BuildInput("submit","action",ACTIONEDIT,"","") & "
")
Response.Write(BuildInput("submit","action",ACTIONDELETE,"","") &
"</TD></TR>")
end if
oRS.movenext
wend
end if
%>
</BODY>
</HTML>
That's it! You now have seen all the SQL queries and ASP scripts required to add
simple discussion forums to your web site. On the next page we explain how
to download and install the sample files

Download and Installation


These scripts require a Window's NT machine with IIS 4.0 and SQL Server. To install
this discussion forum, you need to download discussion.zip and decompress all the
files and directories into your web-sites root directory. Once the files are in your root
directory, you need to create your SQL Server database.

We built this demonstration using SQL Server 6.5 but there is no reason it should
not run with SQL Server 7.0. To make it easy to create the database, you will find a
file called discussion.sql that contains all the SQL commands necessary to create the
tables and stored procedures. You just need to execute this SQL file against a new
SQL Server database. (In SQL Server 6.5, after creating the database, open the SQL
Enterprise Manager, select the database, choose SQL Query Tool from the Tools
menu and open and run the discussion.sql file).

After creating your database you need to create an ODBC DSN (from the ODBC
control panel) so the ASP scripts can connect to the database. After creating the
DSN, you need to update the GetConnection function inside of utility.asp (in the inc
directory) with the correct DSN name, user, and password:

function GetConnection()
const DSN = "membershipdb"
const UID = "webuser"
const PASSWORD = "password"

Dim p_oConn, sDSN


Set p_oConn = server.createObject("ADODB.Connection")
sDSN = "DSN=" & DSN & ";uid=" & UID & ";password=" & PASSWORD
p_oConn.open sDSN
Set GetConnection = p_oConn
end function
Included in this download are all the files for creating the membership table from
the article Adding Site Membership. If you have already set up the user member
table, be sure to delete the SQL statements that generate the t_user table or you will
lose your data.

As with all articles in this series we are merely providing the core pieces for adding
membership and community features. Before introducing this into a production
system you need to evaluate the performance, security and error handling of the
scripts. We permit you to reuse these scripts as a learning device. To reuse this work
in a production system, we ask that you make available to us for possible publishing
any updates or changes you make to the database or interactions with the database.
This includes any extensions that are built over this code.
Adding Site Registration
By Scott Isaacs
This is the first article in a new series demonstrating how to personalization and
community features to your web-site. Each article will contain all the ASP scripts and
SQL queries for a basic implementation. However, as with all database and
transaction-based systems, you should do a careful review to meet an appropriate
level of security, performance and error handling.

In writing the samples we are focusing on simplicity and clarity sometimes at the
expense of robustness. To keep things straightforward, the user-interface used in all
samples is very minimalistic. The web pages merely act as a demonstration on how
to interface with the backend, rather than how to create a rich user experience.

In this article we are going to cover the first step and show you how to create a
membership database. The membership directory supports the following features:

• Joining the Web-Site


• Logging in the Web-Site
• Changing Passwords
• Member List Administration

We organized this article into two parts. First we explain the backend database.
The backend database includes the user table and all the stored procedures used to
manipulate the data. Once finished, we explain the ASP scripts which interact with
the backend database and produce the web-pages for the user.

First, we start with explaining the user table.

Defining the User Table


We store all user information in a single table, t_user. This table stores the user
name, password, e-mail address, and other user-specific information. We set-up our
membership table as follows:

Column Name Data Type (length) Special Notes


u_id Integer Auto-incremented Primary Key
u_name String (50) Unique Index
u_password String (12)
u_firstname String (50)
u_lastname String (50)
u_email String (50) Indexed (non-unique)
u_logcount Integer The number of log-ons
u_createDate DateTime Defaults to user creation date
u_lastDate DateTime The date and time of the last log-on

For the SQL guru's, below is the SQL queries that generates the user table.
Create Table dbo.t_user
(
u_id int Not Null Identity (1, 1),
u_name varchar(50) Not Null,
u_password varchar(12) Not Null,
u_firstname varchar(50) Not Null,
u_lastname varchar(50) Not Null,
u_email varchar(50) Not Null,
u_logcount int Not Null Constraint DF_t_user_u_logcount Default (1),
u_createDate datetime Not Null Constraint DF_t_user_u_createDate
Default (getdate()),
u_lastDate datetime Not Null Constraint DF_t_user_u_lastDate Default
(getdate())
)
Go
Alter Table dbo.t_user Add Constraint
PK_t_user_name Primary Key Nonclustered
(
u_id
)
Go
Alter Table dbo.t_user Add Constraint
IX_t_user_name Unique Nonclustered
(
u_name
)
Go
Create Nonclustered Index IX_t_user_email On dbo.t_user
(
u_email
)
Go
Depending upon your needs, this table can also be extended with additional
information. For example, you can have fields for the user's address, phone number,
etc.

Now that this table is created, the next step is to write the necessary scripts that
interact with the user table. Our approach to building a web-based database
application is to encapsulate all the database manipulations and queries into stored
procedures and use ASP scripts to enumerate and process query results. This
separation allows us to build and test our database interactions separate from the
testing the web-site. Next we explain the stored procedures used to add, edit, log-in,
and find users.

The Stored Procedures


We built all the database manipulations directly into the SQL database as stored
procedures. This approach allows us to test the database independently from the
ASP scripts. Therefore, our first step is to explain each of the stored procedures and
how they are used in the system.

Once we are finish exploring the stored procedures, we explain how these
procedures are manipulated from the ASP scripts. For the most part, the ASP scripts
are quite straightforward, but they do demonstrate techniques for processing forms
on the server. However, in a few cases we did include validation logic that could
easily be incorporated into the backend database rather than manipulated through
script.

First, let's look at the stored procedures we created for our membership database:

Stored Procedure Description


sp_adduser This stored procedure first verifies a user does not exist and then adds
the user to the database.
sp_changepassword This stored procedure is used to change a user's password.
sp_getuser This stored procedure is used by the site manager to returns all the
information for a specified user.
sp_logonuser This stored procedure takes a user name and password and tries to
log in a user. This also updates the log-in count for the user and the
time stamp for the last visit.
sp_saveuser This stored procedure is used by the site manager to update a user's
record.

We start by explaining the two stored procedures for adding and logging in users.

sp_adduser
Adding a user to the database is a very simple process with one simple caveat - you
must first ensure the specified user name is available. We accomplish this with a
simple check against the user table before adding the record. If no record exists for
the specified user-name we add the user. If a user exists, we return a user-id of -1
which signifies the duplicate.

Create Procedure sp_adduser


@u_name varchar(50),
@u_password varchar(12),
@u_firstname varchar(50),
@u_lastname varchar(50),
@u_email varchar(50),
@u_id int output
As
set nocount on
if not exists(select u_id from t_user where u_name=@u_name)
begin
INSERT INTO t_user(u_name, u_password, u_firstname, u_lastname,
u_email)
VALUES (@u_name, @u_password, @u_firstname, @u_lastname,
@u_email)
select @u_id=@@identity
end
else
select @u_id=-1
return
This stored procedure is missing data validation. For example, there is no
validation that the password meets a certain constraint (eg., between 4 and 12
characters). We currently do this validation in our ASP scripts (explained later). In a
production system, such constraints may be better defined within the stored
procedure with the addition of more detailed return codes. In addition, a production
system should probably encrypt the user's password. In this demonstration, the
password is stored as entered.

sp_logonuser
This stored procedure is very simple. Given a user name and password, we locate
and return the corresponding record:

Create Procedure sp_logonuser


@u_name varchar(50),
@u_password varchar(12)
As
set nocount on
UPDATE t_user SET u_lastDate = GETDATE(), u_logcount = u_logcount + 1
WHERE (u_name = @u_name) AND (u_password = @u_password)
SELECT * from t_user where u_name=@u_name and u_password=@u_password
return
The first query in this procedure increments the number of visits and updates the
last log-in date. Once updated the user record itself is returned to the user. We do
not need to check for the existance of the user record as this is handled
automatically by the where clause that tests for a matching user-name and
password. If there is no match an empty recordset is returned.

Next we explain the three stored procedures related to administration.

The Stored Procedures


We built all the database manipulations directly into the SQL database as stored
procedures. This approach allows us to test the database independently from the
ASP scripts. Therefore, our first step is to explain each of the stored procedures and
how they are used in the system.

Once we are finish exploring the stored procedures, we explain how these
procedures are manipulated from the ASP scripts. For the most part, the ASP scripts
are quite straightforward, but they do demonstrate techniques for processing forms
on the server. However, in a few cases we did include validation logic that could
easily be incorporated into the backend database rather than manipulated through
script.

First, let's look at the stored procedures we created for our membership database:

Stored Procedure Description


sp_adduser This stored procedure first verifies a user does not exist and then adds
the user to the database.
sp_changepassword This stored procedure is used to change a user's password.
sp_getuser This stored procedure is used by the site manager to returns all the
information for a specified user.
sp_logonuser This stored procedure takes a user name and password and tries to
log in a user. This also updates the log-in count for the user and the
time stamp for the last visit.
sp_saveuser This stored procedure is used by the site manager to update a user's
record.

We start by explaining the two stored procedures for adding and logging in users.

sp_adduser
Adding a user to the database is a very simple process with one simple caveat - you
must first ensure the specified user name is available. We accomplish this with a
simple check against the user table before adding the record. If no record exists for
the specified user-name we add the user. If a user exists, we return a user-id of -1
which signifies the duplicate.

Create Procedure sp_adduser


@u_name varchar(50),
@u_password varchar(12),
@u_firstname varchar(50),
@u_lastname varchar(50),
@u_email varchar(50),
@u_id int output
As
set nocount on
if not exists(select u_id from t_user where u_name=@u_name)
begin
INSERT INTO t_user(u_name, u_password, u_firstname, u_lastname,
u_email)
VALUES (@u_name, @u_password, @u_firstname, @u_lastname,
@u_email)
select @u_id=@@identity
end
else
select @u_id=-1
return
This stored procedure is missing data validation. For example, there is no
validation that the password meets a certain constraint (eg., between 4 and 12
characters). We currently do this validation in our ASP scripts (explained later). In a
production system, such constraints may be better defined within the stored
procedure with the addition of more detailed return codes. In addition, a production
system should probably encrypt the user's password. In this demonstration, the
password is stored as entered.

sp_logonuser
This stored procedure is very simple. Given a user name and password, we locate
and return the corresponding record:

Create Procedure sp_logonuser


@u_name varchar(50),
@u_password varchar(12)
As
set nocount on
UPDATE t_user SET u_lastDate = GETDATE(), u_logcount = u_logcount + 1
WHERE (u_name = @u_name) AND (u_password = @u_password)
SELECT * from t_user where u_name=@u_name and u_password=@u_password
return
The first query in this procedure increments the number of visits and updates the
last log-in date. Once updated the user record itself is returned to the user. We do
not need to check for the existance of the user record as this is handled
automatically by the where clause that tests for a matching user-name and
password. If there is no match an empty recordset is returned.

Next we explain the three stored procedures related to administration.

Administration Procedures
There are three stored procedures related to site administration. One,
sp_changepassword, is intended for use by your members, and the other two,
sp_getuser and sp_saveuser are intended for use by the site administrator.

sp_changepassword
This simple stored procedure is used to change the user's password:

Create Procedure sp_ChangePassword


@u_id int,
@old_password varchar(12),
@new_password varchar(12),
@success int output
As
set nocount on
if exists
(SELECT u_id FROM t_user WHERE (u_id = @u_id) AND (u_password =
@old_password))
begin
UPDATE t_user SET u_password = @new_password WHERE (u_id = @u_id)
select @success = 1
end
else
select @success = -1
return
Before updating the password, first we verify that the user entered the correct
password for their user-id. If the password is correct, we simply update the
password field. If the old password is entered incorrectly, we return a failure code.

sp_getuser
This is a very simple and self-explanatory stored procedure that returns the complete
user record for a specified user-id.

Create Procedure sp_GetUser


@u_id int
As
set nocount on
select * from t_user where u_id=@u_id
return
We use this stored procedure to allow the site administrator to retrieve any user's
record for editing. To complement this stored procedure, we created sp_saveuser
which is used to update the user's record.

sp_saveuser
This stored procedure is used to update all the fields (except the user's id) for any
particular user:

Create Procedure sp_SaveUser


@u_id int,
@u_name varchar(50),
@u_password varchar(12),
@u_firstname varchar(50),
@u_lastname varchar(50),
@u_email varchar(50)
As
set nocount on
if not exists(select u_id from t_user where u_name=@u_name and
u_id<>@u_id)
UPDATE t_user
SET u_name = @u_name, u_password = @u_password, u_firstname =
@u_firstname,
u_lastname = @u_lastname, u_email = @u_email
WHERE (u_id = @u_id)
return
Before updating the record, we first verify that no other user is already using a
particular name. If none exists, we update all the fields for the specified user.

This concludes our introduction to all the stored procedures. Next we walk you
through the ASP scripts used to coordinate and inteface with the backend database.

Adding Site Registration : The ASP Scripts


You have seen how the backend was created. Now we take you through the ASP
scripts that interact with the database. To manage our database connections, we
create an ASP file, utility.asp. This asp page is included in all pages that interact with
the database. We use the ASP extension instead of INC (the typical include
extension) to ensure that this page cannot be easily viewed by the client since it
contains the name and password to connect to the database. In a production system,
this file should be placed in a non-browsable directory with the appropriate NT
security permissions.

Below are the commented functions used that interact with the database.

function GetConnection()
' GetConnection
'Used to obtain a connection to the backend database

' Update the following for your connection


const DSN = "membershipdemo"
const UID = "webuser"
const PASSWORD = "mypassword"

Dim p_oConn, sDSN


Set p_oConn = server.createObject("ADODB.Connection")
sDSN = "DSN=" & DSN & ";uid=" & UID & ";password=" & PASSWORD
p_oConn.open sDSN
Set GetConnection = p_oConn
end function
function GetStoredProcedure(oConnection, sName)
' GetStoredProcedure
' A helper function for quickly creating
' a stored procedure object
Dim oCmd
Set oCmd = Server.createobject("ADODB.Command")
Set oCmd.ActiveConnection = oConnection
oCmd.CommandText = sName
oCmd.CommandType = adCmdStoredProc
Set GetStoredProcedure = oCmd
end function

function GetTable(oConnection, sName)


' GetTable
' A helper function for quickly opening a
' table.
Set GetTable = oConnection.Execute(sName)
end function
In addition, the utility.asp file contains a few generic functions that help simplify
constructing a form. While these functions are very simplistic they help to provide
style guidelines. For example, we created the BuildInput() function to make sure that
all text input fields have an appropriate size and maximum length specified. Without
this function, it becomes very easy to forget this information.

function BuildInput(sType,sName,sValue,iSize,iLength)
' A helper function for building input boxes.
select case sType
case "text":
BuildInput = "<INPUT TYPE=""text"" NAME=""" & sName & """
VALUE=""" &
sValue & """ SIZE=""" & iSize & """ MAXLENGTH=""" & iLength &
""">"
case "password"
BuildInput = "<INPUT TYPE=""password"" NAME=""" & sName & """
VALUE=""" &
sValue & """ SIZE=""" & iSize & """ MAXLENGTH=""" & iLength &
""">"
case "submit"
BuildInput = "<INPUT TYPE=""submit"" NAME=""" & sName & """
VALUE=""" &
sValue & """>"
end select
end function

function EscapeString(str)
' Used to prevent HTML from being entered and
' and to escape any quotation marks.

' Escaping quotes is necessary so when the values can be


' specified in an input field.
str = replace(str,"<","<")
str = Trim(replace(str,"""","""))
escapeString = str
end function

function BuildForm(sName,sMethod)
' Returns a simple form tag. Used to ensure tha the appropriate
' submission method is specified
BuildForm = "<FORM NAME=""" & sName & """ METHOD=""" & sMethod &
""">"
end function
This file also includes the constants necessary for interacting with the stored
procedures. These constants are a subset of all the constants available for
manipulating databases and recordsets. We created our own subset to reduce the
overall size of our pages. Rather than list the constants, they are in the download
available at the end of the article. In addition, we defined two additional constants
that help define the site's name and home page:

const SITENAME = "SiteExperts.com"


const HOMEPAGE = "<A HREF=""/default.asp"">Home Page</A>"

function OutputHeader()
OutputHeader="<H2>" & HOMEPAGE & "</H2>"
end function
We included this to demonstrate ways to more generically define your web-site. By
defining globally used as constants, you can quickly change aspects of your site. You
will see these constants used to generate our page headers and other items.

Finally, as we said earlier, utility.asp is intended to be included in every page that


interacts with the database. For example:

<%@ Language=VBScript %>


<% option explicit %>
<HTML>
<HEAD>
<!-- #include virtual="/inc/utility.asp" -->
</HEAD>
<BODY>
...
</BODY>
Next we explain how user's can join and log into the community.

Adding Site Registration : Joining a Site


The page, join.asp, contains all the scripting logic to add user's to the community.
This page interfaces with the sp_adduser stored procedure introduced earlier. We are
going to walk you through the interesting portions of the join.asp page.

First, we track user information using ASP session objects. Therefore, the first test
on the join.asp page is to make sure the user is not already logged on. If they are,
we redirect them to the logon.asp screen (which will also detect the user is logged on
and just display the user's information). Since we are doing a redirect, this test must
be done before any HTML is sent to the client:

<% @Language=VBScript %>


<% Option Explicit %>
<% if session("u_name")<>"" then Response.Redirect("logon.asp") %>
Next we include the utility.asp file and output the page title. Notice that we are
using the SITENAME constant defined in utility.asp. This allows us to rename the site
without having to edit each page:
<HTML>
<HEAD>
<!-- #include virtual="/inc/utility.asp" -->
<TITLE>Join <%=SITENAME%></TITLE>
</HEAD>
Now we are ready to output the body of the page. Before joining, the user is
presented with a form. The form submission is sent back to the same join.asp page
for processing. If the user is added, they are presented with welcome information. If
the action fails, the form is redisplayed with the appropriate error information.

We came up with a unique technique for managing form actions on the same
page. We take advantage of the SUBMIT buttons name and value fields. When a
submit button is pressed, it's name and value field are sent to the server. This
makes it easy to distinguish between multiple actions on the same form or even the
same page. For example, you can easily distinguish between an UPDATE and DELETE
operation as follows:

<%
select case Request.Form("action")
case "Update...":
' update
case "Delete...":
' delete
end select
%>
<FORM ACTION=post>
<INPUT TYPE=submit NAME=action VALUE="Update...">
<INPUT TYPE=submit NAME=action VALUE="Delete...">
</FORM>
One problem with the above approach is that you need to remember to update the
script if the submit button's text ever changes. To avoid this, we store the value in a
constant and use this constant throughout the page. With this approach, we can
easily update the button's value without worrying about not updating the script.

Continuing with the join.asp page, you will see we declare a constant
ACTIONJOIN. This constant represents the submit button text. Below we outline the
remainder of the page's logic:

<BODY>
<%

Response.Write(outputHeader())
' The text for the submit button

const ACTIONJOIN = "Join..."

Dim sName,sFirstName,sLastName,sEmailName, sError, bJoin

' Initialize
bJoin = false
sName=""
sFirstName=""
sLastName=""
sEmailName=""
sError=""
if (Request.Form("action")=ACTIONJOIN) then
' Joining
Dim sPassword
sName = Request.Form("u_name")
sFirstName = Request.Form("u_firstname")
sLastName = Request.Form("u_lastname")
sEmailName = Request.Form("u_emailname")
sPassword = Request.Form("u_password")
' In a production system, this validation may be better served inside
' the database itself.

' Do simple validation


if len(sPassword)<4 or len(sPassword)>12 or sPassword="" then
sError = sError & "<BR>Your password must be between 4 and 12
characters"
end if
if trim(sFirstName)="" or trim(sLastName)="" or trim(sEmailName)=""
or trim(sPassword)="" then
sError = sError & "<BR>You must fill out all fields to join."
end if
if sPassword<>Request.Form("u_password2") then
sError = sError & "<BR>Your passwords do not match"
end if
if sError="" then
' No errors - try to add user
Dim oCmd, oReturn, oConn
set oConn = GetConnection()
Set oCmd = GetStoredProcedure(oConn,"sp_adduser")
oCmd.Parameters.append oCmd.CreateParameter("u_name", adVarChar,
adParamInput,50,sName)
oCmd.Parameters.append oCmd.CreateParameter("u_password",
adVarChar, adParamInput,12,spassword)
oCmd.Parameters.append oCmd.CreateParameter("u_firstName",
adVarChar, adParamInput,50,sFirstName)
oCmd.Parameters.append oCmd.CreateParameter("u_lastName",
adVarChar, adParamInput,50,sLastName)
oCmd.Parameters.append oCmd.CreateParameter("u_email", adVarChar,
adParamInput,50,sEmailName)
set oReturn = oCmd.CreateParameter("u_id", adInteger, adParamOutput)
oCmd.Parameters.append oReturn
oCmd.execute()
if oReturn.value=-1 then
' Duplicate user name
sError = sError & "<BR>Your user name already exists. Please
select a new one."
else
' Success - Login and store information in the session object
bJoin = true
session("u_id") = oReturn.value
session("u_name") = sName
session("u_lastdate") = ""
session("u_logcount") = 1
end if
end if
end if

if not bJoin then


Response.Write("<P>Joining " & SITENAME & " allows you to personalize
and improve your user experience.")
if sError<>"" then
' Output error if failed join attempt
Response.Write("<P><B>Your membership was not added
because:</B><FONT COLOR=darkred>" & sError & "</FONT>")
end if
Response.Write(BuildForm("username","post"))
Response.Write("<TABLE>")
Response.Write("<TR><TD>User Name</TD>")
Response.Write("<TD>" &
BuildInput("text","u_name",escapeString(sName),20,50) & "</TD></TR>")
Response.Write("<TR><TD>Password</TD>")
Response.Write("<TD>" & BuildInput("password","u_password","",12,12)
& "</TD></TR>")
Response.Write("<TR><TD>Confirm Password</TD>")
Response.Write("<TD>" & BuildInput("password","u_password2","",12,12)
& "</TD></TR>")
Response.Write("<TR><TD>First Name</TD>")
Response.Write("<TD>" &
BuildInput("text","u_firstname",escapeString(sFirstName),20,50) &
"</TD></TR>")
Response.Write("<TR><TD>Last Name</TD>")
Response.Write("<TD>" &
BuildInput("text","u_lastname",escapeString(sLastName),20,50) &
"</TD></TR>")
Response.Write("<TR><TD>Email Name</TD>")
Response.Write("<TD>" &
BuildInput("text","u_emailname",escapeString(sEmailName),20,50) &
"</TD></TR>")
Response.Write("<TR><TD COLSPAN=2>" &
BuildInput("submit","action",ACTIONJOIN,"","") & "</TD></TR>")
Response.Write("</TABLE>")
else
' Output welcome text
Response.Write("<P>Welcome to the Community!")
end if
%>
</FORM>
</BODY>
</HTML>
The other pages in the system work in a very similar manner. Each page contains
an input form and the necessary logic to process the form's input. For subsequent
visits, we need a log-in page and a place where users can change their password.
Next we explain how to create both these pages.

Logging into a Site


Once a user joins, they need to be able to log back in and change their password.
Logging users in is accomplished with the logon.asp page, and password
management is handled by changepassword.asp. First we will look at logon.asp.

logon.asp
The logon.asp page serves two purposes. To allow existing users to log into the
system and to allow users to manually log themselves off. Since the session object
eventually times out automatically logging the user off, the log out functionality is
provided mostly as a convenience to the user.

Since this page supports two distinct actions, we use the form management
technique presented on the previous page. Below we walk you through the complete
logon.asp script:

<% @Language=VBScript %>


<% Option Explicit %>
<HTML>
<HEAD>
<!-- #include virtual="/inc/utility.asp" -->

<TITLE>Logon <%=SITENAME%></TITLE>
</HEAD>
<BODY>
<%=OutputHeader()%>
<%
' The text for the submit button and to differentiate
' form actions
const LOGON_ACTION = "Logon..."
const LOGOFF_ACTION = "Logoff..."

Dim sError, bLogon

' Does the user have to log in


bLogon = Session("u_name")=""

if Request.Form("action")=LOGOFF_ACTION then
' User chose to log-off.
' Just abandon the session object
Session.Abandon()
bLogon = true
elseif Request.Form("action")=LOGON_ACTION then
' Logon
Dim sPassword, sName
sName = Trim(Request.Form("u_name"))
sPassword = Trim(Request.Form("u_password"))
if sName="" or sPassword="" then
sError = "<BR>You did not enter a name and/ or password."
else
' Try to log the user in
Dim oCmd, oConn, oRS
set oConn = GetConnection()
Set oCmd = GetStoredProcedure(oConn,"sp_logonuser")
oCmd.Parameters.append oCmd.CreateParameter("u_name", adVarChar,
adParamInput,50,sName)
oCmd.Parameters.append oCmd.CreateParameter("u_password",
adVarChar, adParamInput,12,spassword)
set oRS = oCmd.execute()
' If no record returned - login failed
if oRS.eof then
sError = "<BR>Invalid User Name or Password."
else
' Store user information in session object
session("u_id") = oRS.fields("u_id")
session("u_name") = oRS.fields("u_name")
session("u_lastdate") = oRS.fields("u_lastdate")
session("u_logcount") = oRS.fields("u_logcount")
bLogon = false
end if
end if
end if

if bLogon then
Response.Write("<P>Logon " & SITENAME & " or <A
HREF=""join.asp"">Join Now</A>.")
if sError<>"" then
' Report any errors
Response.Write("<P><B>You were not logged in because:</B><FONT
COLOR=darkred>" & sError & "</FONT>")
end if
Response.Write(BuildForm("username","post"))
Response.Write("<TABLE>")
Response.Write("<TR><TD>User Name</TD>")
Response.Write("<TD>" & BuildInput("text","u_name",sName,20,50) &
"</TD></TR>")
Response.Write("<TR><TD>Password</TD>")
Response.Write("<TD>" & BuildInput("password","u_password","",12,12)
& "</TD></TR>")
Response.Write("<TR><TD COLSPAN=2>" &
BuildInput("submit","action",LOGON_ACTION,"","") & "</TD></TR>")
Response.Write("</TABLE>")
Response.Write("</FORM>")
else
' User logged in - output welcome back message
Response.Write("<H2>Welcome Back " & session("u_name") & "</H2>")
Response.Write(BuildForm("","post"))
Response.Write(BuildInput("submit","action",LOGOFF_ACTION,"",""))
Response.Write("</FORM>")
end if
%>
</FORM>
</BODY>
</HTML>

changepassword.asp
All membership related sites should allow the user to update their password.
Creating the change password page is very simple. First, you make sure the user is
logged in, then you ask the user for their old password and a new one. Below is the
complete script for the changepassword.asp page:

<%@ Language=VBScript %>


<% option explicit %>
<%
' Make sure user is logged in
if session("u_name")="" then Response.Redirect("logon.asp")
%>
<HTML>
<HEAD>
<!-- #include virtual="/inc/utility.asp" -->
</HEAD>
<BODY>
<%
Dim sError
const ACTION="Change Password"

sError = ""

if Request.Form("action") = ACTION then


' Try and update password
if len(Request.Form("newpassword"))<4 or
len(Request.Form("newpassword"))>12 then
' Make sure password is appropriate length
sError = "<BR>Your password must be between 4 and 12 characters."
elseif Request.Form("newpassword")=Request.Form("newpassword2") then
' Make sure new password and confirmation are the same.
' If so, try and change password
Dim oCmd, oReturn, oConn
set oConn = GetConnection()
Set oCmd = GetStoredProcedure(oConn,"sp_ChangePassword")
oCmd.Parameters.append oCmd.CreateParameter("u_id", adInteger,
adParamInput,10,session("u_id"))
oCmd.Parameters.append oCmd.CreateParameter("old_password",
adVarChar, adParamInput,12,Request.Form("oldpassword"))
oCmd.Parameters.append oCmd.CreateParameter("new_password",
adVarChar, adParamInput,50,Request.Form("newpassword"))
set oReturn = oCmd.CreateParameter("success", adInteger,
adParamOutput)
oCmd.Parameters.append oReturn
oCmd.execute()
if oReturn.value=-1 then
' Failure - report error
sError = "<BR>Password <B>not</B> changed. Did you enter your
current password correctly?"
end if
else
' New password and New Password confirmation do not match.
sError = "<BR>New passwords do not match."
end if
end if

Response.write(outputHeader())

if Request.Form("action")="" or sError<>"" then


if sError<>"" then
' Report any errors.
Response.Write("<P>The following errors occurred when changing your
password:" & sError)
end if
Response.Write("<P>Change your password:")
Response.Write(BuildForm("password","post"))
Response.Write("<TABLE><TR><TD>Current Password</TD>")
Response.Write("<TD>" & BuildInput("password","oldpassword","",12,12)
& "</TD></TR>")
Response.Write("<TR><TD>New Password</TD>")
Response.Write("<TD>" & BuildInput("password","newpassword","",12,12)
& "</TD></TR>")
Response.Write("<TR><TD>Confirm New Password</TD>")
Response.Write("<TD>" &
BuildInput("password","newpassword2","",12,12) & "</TD></TR>")
Response.Write("<TR><TD ALIGN=CENTER COLSPAN=2>")
Response.Write(BuildInput("submit","action",ACTION,"","") &
"</TD></TR>")
Response.Write("</FORM>")
else
Response.Write("Password Changed")
end if
%>
</BODY>
</HTML>
This completes all the end-user related pages. You now have everything necessary
to add a site membership table. However, as a site administrator, you need access to
edit and update the user's information. Next we show you how we created a simple
management page.

User Management
The amount of site management needed is dependent upon your site's purpose and
goals. For this demonstration we create a very simple site management page,
userlist.asp, that allows you to select and edit any user. Since this page gives you
complete access to all users, it should be appropriately secured on any production
system.

<%@ Language=VBScript %>


<% option explicit %>
<HTML>
<HEAD>
<!-- #include virtual="/inc/utility.asp" -->
</HEAD>
<BODY>

<%
Dim oRS, oCmd, oConn
Const ACTIONSAVE = "Save..."

set oConn = GetConnection()

Response.Write(OutputHeader() & "<H2>User Manager</H2>")

if Request.Form("action")=ACTIONSAVE then
' Update user
Set oCmd = GetStoredProcedure(oConn,"sp_SaveUser")
oCmd.Parameters.append oCmd.CreateParameter("u_id", adInteger,
adParamInput,10,Request.QueryString("u_id"))
oCmd.Parameters.append oCmd.CreateParameter("u_name", adVarChar,
adParamInput,50,Request.Form("u_name"))
oCmd.Parameters.append oCmd.CreateParameter("u_password", adVarChar,
adParamInput,50,Request.Form("u_password"))
oCmd.Parameters.append oCmd.CreateParameter("u_firstname", adVarChar,
adParamInput,50,Request.Form("u_firstname"))
oCmd.Parameters.append oCmd.CreateParameter("u_lastname", adVarChar,
adParamInput,50,Request.Form("u_lastname"))
oCmd.Parameters.append oCmd.CreateParameter("u_email", adVarChar,
adParamInput,50,Request.Form("u_email"))
oCmd.execute()
end if
if Request.QueryString("u_id")<>"" then
' Get user information
Set oCmd = GetStoredProcedure(oConn,"sp_GetUser")
oCmd.Parameters.append oCmd.CreateParameter("u_name", adInteger,
adParamInput,10,Request.QueryString("u_id"))
set oRS = oCmd.execute()
Response.Write("<A HREF=""userlist.asp"">User List</A>")
if oRS.eof then
' Bad user id
Response.Write("User not found")
else
Response.Write(BuildForm("edituser","post"))
Response.Write(BuildInput("hidden","u_id",Request.QueryString("u_id
"),"",""))
Response.Write("<TABLE>")
Response.Write("<TR><TD>User Name</TD>")
Response.Write("<TD>" &
BuildInput("text","u_name",escapeString(oRS.fields("u_name")),20,50) &
"</TD></TR>")
Response.Write("<TR><TD>Password</TD>")
Response.Write("<TD>" &
BuildInput("text","u_password",oRS.fields("u_password"),12,12) &
"</TD></TR>")
Response.Write("<TR><TD>First Name</TD>")
Response.Write("<TD>" &
BuildInput("text","u_firstname",escapeString(oRS.fields("u_firstname"))
,20,50) & "</TD></TR>")
Response.Write("<TR><TD>Last Name</TD>")
Response.Write("<TD>" &
BuildInput("text","u_lastname",escapeString(oRS.fields("u_lastname")),2
0,50) & "</TD></TR>")
Response.Write("<TR><TD>Email Name</TD>")
Response.Write("<TD>" &
BuildInput("text","u_email",escapeString(oRS.fields("u_email")),20,50)
& "</TD></TR>")
Response.Write("<TR><TD COLSPAN=2>"
Response.Write(BuildInput("submit","action",ACTIONSAVE,"","") &
"</TD></TR>")
Response.Write("</TABLE>")
end if
else
' Output all users.
Set oRS = GetTable(oConn,"t_user")
Response.Write("<UL>")
while not oRS.eof
Response.Write("<LI><A HREF=""?u_id=" & oRS.fields("u_id") & """>"
& oRS.fields("u_name"))
oRS.movenext
wend
Response.Write("</UL>")
end if
%>
</BODY>
</HTML>
This page is interesting in that it provides three pieces of functionality. In its
default form (userlist.asp) it displays a list of all the site's users. When called with a
user id (userlist.asp?u_id=23) it returns a form for editing the user. If called with a
user id plus the appropriate form submission, the user's data will be updated and the
form will be redisplayed. All of this is handled by the large if-then blocks:

if Request.Form("action")=ACTIONSAVE then
' Save user information
end if
if Request.QueryString("u_id")<>"" then
' Output form
else
' Output list of users
end if
Next we wrap up our introduction to adding site membership and explain how
to download and install the demonstration

Conclusion and Download Files


You have seen how to add a site membership table to your web-site. By itself, this
membership table is not very useful. However, by combining the site membership
table with additional features you can create a rich, personalized experience. For
example, in the t_user table every user has a unique id. This id serves as the user's
identifier throughout the system. For example, in a message board, this id is used to
associate a message post with a particular user. For our next article, we plan on
extending the concepts and database introduced in this to create a simple message
board.

Installing the Demo


This demonstration requires a Window's NT machine with IIS 4.0 and SQL Server. To
install this demonstration, you need to download membership.zip and decompress all
the files and directories into your web-sites root directory. Once the files are in your
root directory, you need to create your SQL Server database.

We built this demonstration using SQL Server 6.5 but there is no reason it should
not run with SQL Server 7.0. To make it easy to create the database, you will find a
file called membership.sql that contains all the SQL commands necessary to create
the tables and stored procedures. You just need to execute this SQL file against a
new SQL Server database. (In SQL Server 6.5, after creating the database, open the
SQL Enterprise Manager, select the database, choose SQL Query Tool from the Tools
menu and open and run the membership.sql file).

As we said in the beginning of this article, by itself this code is not intended to
serve as the foundation for a user membership system. Before introducing this into a
production system you need to evaluate the performance, security and error
handling of the scripts. We permit you to reuse these scripts as a learning device. To
reuse this work in a production system, we ask that you make available to us for
possible publishing any updates or changes you make to the database or interactions
with the database. This includes any extensions built into the system (eg., adding
message boards, etc).

THE END

Thanks

Вам также может понравиться