Deleting spam comments in .Text

They hit from time to time, slapping their advertisements for online poker or other vices on our .Text blog sites. You can go through the admin console and delete them one at a time, or you can do it from SQL.

delete from blog_content
where parentid <> -1
and title not like 're:%'

That’s a pretty reliable way to purge them — provided your legitimate comments use the default subject line that starts as a reply to the original blog title. You can further filter things down by using a date range, which is useful if you know you were slammed between a few days and don’t want to take the risk of touching anything outside that date range.

delete from blog_content
where parentid <> -1
and dateadded between '2005-06-01' and '2005-06-30'
and title not like 're:%'

Damn spammers…

Gamasutra article on multiplayer games and database design

If you’re developing games and use a database to keep track of things, you’ll be doing yourself some good by reading the article MMP Database Mini-Cookbook: A Half Dozen Recipes To Aid Development by Jay Lee, published yesterday (9/22/04) on Gamasutra. The author provides six tips on designing your database schema to handle multiple shards/servers, localization, class to database relationships, and more.

Lee’s other Gamasutra article, published about a year prior, is equally valuable. Relational Database Guidelines For MMOGs gives some insight into naming standards and basic database best practices that may not be evident to all database developers. Hats off to Lee for two good articles on database design and development in general – and for aiming it at the game development market!

Configuring Trusted SQL Connections in ASP.Net and Windows 2003

While configuring a new server running Microsoft Windows Server 2003, I decided to start using trusted SQL connections from ASP.Net Web sites (instead of specific SQL user accounts). One of the differences between Windows 2000 and Windows 2003 is the ASP.Net process account; the former uses ASPNET, the latter NETWORK SERVICE.

One thing you’ll notice is that when creating a new login for SQL Server is that you can’t select the NETWORK SERVICE account. (Oddly, you can select the ASPNET account, but I don’t know what it is used for in Windows 2003/IIS 6.0, since it’s been replaced with NETWORK SERVICE.) After plenty of searching I found a newsgroup posting from Microsoft that solved the problem.

In brief: Either specify the username as NT AUTHORITY\NETWORK SERVICE in SQL Enterprise Manager’s New Login window, or run the following query:

exec sp_grantlogin [NT AUTHORITY\NETWORK SERVICE]

Yet another example of Microsoft hiding the obvious.

Running Ad-Hoc SQL Queries in ASP

Have you ever wished you can run ad-hoc queries on your database from a Web page? This article shows you how a simple Web form and some ASP code can execute SQL statements against a database and display the results in your Web browser.

Step 1: Keep it secure!

The first thing to do is to secure the page you’re about to create! (In this article, we’ll call the page runsql.asp.) There are a number of ways to secure your Web page using IIS (which we won’t get in to here). If you don’t secure the page, you’re taking the chance that anyone can execute SQL statements on your server – something you likely don’t want to do – so be sure to start off with security in mind! You should also consider using the HTTPS protocol, as your SQL statements and results can be intercepted if they are sent in clear text.

Another suggestion is to use a read-only account in the connection string when connecting to a SQL Server database. Although this prohibits your from executing INSERT, UPDATE, and other SQL statements, it will eliminate a significant security risk.

Step 2: ASP Necessities

To keep things clean and proper, we’re going to use Option Explicit at the top of our ASP page. To trap errors later on, we’ll use On Error Resume Next. We’ll also declare the variables we’ll be using later on in the page, and set three ADO constants (which is much cleaner than importing the entire ADOVBS.INC file!).

Option Explicit
On Error Resume Next
Dim objConn, objRec, strSQL, i, iFields
Const adOpenForwardOnly = 0
Const adLockReadOnly = 1
Const adCmdText = &H0001

Step 3: Create the HTML form

The HTML form has a TEXTAREA field where we’ll type our SQL statements, and the typical submit and reset buttons. The form submits to itself (i.e. runsql.asp). Note that the TEXTAREA’s initial value is set to Request.Form("sql") – so after we submit the form, we’ll easily be able to see the query we just submitted and make changes to it.

<form action="runsql.asp" method="post">
<textarea name="sql" cols="60" wrap rows="10">
<%= Request.Form("sql") %>
</textarea><br>
<input type="submit">&nbsp;&nbsp;<input type="reset">
</form>

Step 4: Executing the SQL Query

We check to see if the form was submitted by examining Request.ServerVariables("REQUEST_METHOD"), which equals POST when our form is submitted. (Remember, we used the POST method in our form.)

If the form is submitted, we’ll be nice and display the SQL query received in Request.Form("sql"), then create our ADO Connection and Recordset objects. Be sure to open the Connection object with the connection string of your datasource.

Finally, we can open our Recordset. Note the use of adOpenForwardOnly and adLockReadOnly – since we don’t need any special recordset cursors or locking, specifying these options gives the best performance.

If Request.ServerVariables("REQUEST_METHOD") = "POST" Then
 
    Response.Write "Query:<br><b>" & Request.Form("sql") & "</b><br>"
 
    Set objConn = Server.CreateObject("ADODB.Connection")
    Set objRec = Server.CreateObject("ADODB.Recordset")
 
    strSQL = Request.Form("sql")
    objConn.Open "dsn=mydsn"
    objRec.Open strSQL, objConn, adOpenForwardOnly, adLockReadOnly, adCmdText

Step 5: Checking for Errors

Remember when we specified On Error Resume Next at the top of our code? We can trap the error here. Chances are if anything failed up to this point, the objRec.Open call will fail, too, generating an error. If there’s an error, we’ll display it to the user.

If Err.Number <> 0 Then
    Response.Write "An error has occurred!<br>"
    Response.Write "Error number:      " & Err.number & "<br>"
    Response.Write "Error description: " & Err.description & "<br>"

Step 6: Displaying the Results

Continuing right after the error checking, we have to handle two situations: if the recordset is empty, or if it is not empty. If it’s empty, we’ll simply tell the user.

ElseIf objRec.EOF Then
    Response.Write "Empty recordset returned."

If objRec is not empty, we need to display it in an HTML table. First, we want to display the field names as a header row. Recordset field names are stored in the Fields collection, which is the default member of the Recordset object. We’ll read the number of fields (objRec.Fields.Count) into a variable, then use a For loop to output the Name parameter of each field (which contains the column name for the field).

Note that we could have specified objRec(i).Name instead of objRec.Fields(i).Name. Since the Fields collection is the default member of the Recordset object, it isn’t necessary to specify it, assuming it’s what you want.

Else
    iFields = objRec.Fields.Count
    Response.Write "<table border=1><tr>"
    For i = 0 To iFields - 1
        Response.Write "<td><b>" & objRec.Fields(i).Name & "</b></td>"
    Next
    Response.Write "</tr>"

Next, to display the rows in the recordset, we’ll loop through the Recordset using a While loop, moving from record to record using Recordset.MoveNext. Using the same technique as above, we’ll loop through the Fields collection and return the Value parameter for each field, which contains the data in the field.

Again, we could have substituted objRec(i).Value for of objRec.Fields(i).Value, since Fields is the default member of the Recordset object.

    While Not objRec.EOF
        Response.Write "<tr>"
        For i = 0 To iFields - 1
            Response.Write "<td>" & objRec.Fields(i).Value & "</td>"
        Next
        Response.Write "</tr>"
        objRec.MoveNext
    Wend
    Response.Write "</table>"
End If

Step 7: Cleaning Up

As always, you should close your Connection and Recordset objects when you’re done with them, and set their values to Nothing. Don’t rely on the script engine to do this for you. One memory leak can do considerable damage to your server’s performance!

objRec.Close
Set objRec = Nothing
objConn.Close
Set objConn = Nothing

Summary: RUNSQL.ASP

Being able to execute SQL statements from a Web page can come in very handy, especially when you’re not able to connect to the SQL server directly (if it’s inaccessible through a firewall or proxy server). If you secure this properly, you’ll have a powerful and easy to use tool in your Web development toolbox.


COMPLETE SOURCE CODE FOLLOWS:


<%

Option Explicit

On Error Resume Next

Dim objConn, objRec, strSQL, i, iFields

Const adOpenForwardOnly = 0

Const adLockReadOnly = 1

Const adCmdText = &H0001

%>

<html>

<head>

<title>RUNSQL</title>

</head>

<body>

<form action="runsql.asp" method="post">

<textarea name="sql" cols=60 wrap rows=10>

<%= Request.Form("sql") %>

</textarea>

<input type="submit">&nbsp;&nbsp;<input type="reset">

</form>

<%

If Request.ServerVariables("REQUEST_METHOD") = "POST" Then

  Response.Write "Query:<br><b>" & Request.Form("sql") & "</b><br>"

  Set objConn = Server.CreateObject("ADODB.Connection")

  objConn.Open "dsn=mydsn"

  strSQL = Request.Form("sql")

  Set objRec = Server.CreateObject("ADODB.Recordset")

  objRec.Open strSQL, objConn, adOpenForwardOnly, adLockReadOnly, adCmdText

  If Err.Number <> 0 Then

   Response.Write "An error has occurred!<br>"

   Response.Write "Error number:      " & Err.number & "<br>"

   Response.Write "Error description: " & Err.description & "<br>"

  ElseIf objRec.EOF Then

   Response.Write "Empty recordset returned."

  Else

   iFields = objRec.Fields.Count

   Response.Write "<table border=1><tr>"

   For i = 0 To iFields – 1

    Response.Write "<td><b>" & objRec.Fields(i).Name & "</b></td>"

   Next

   Response.Write "</tr>"

   While Not objRec.EOF

    Response.Write "<tr>"

    For i = 0 To iFields – 1

     Response.Write "<td>" & objRec.Fields(i).Value & "</td>"

    Next

    Response.Write "</tr>"

    objRec.MoveNext

   Wend

  End If

  objRec.Close

  Set objRec = Nothing

  objConn.Close

  Set objConn = Nothing

End If

%>

</body>

</html>

COM Objects in SQL: Sending E-mail using SQL and CDONTS

Microsoft’s SQL Server provides e-mail services via the SQLMail service. Unfortunately, this service has some significants limitations. It uses MAPI, requiring an Outlook mail profile (usually tied with a Microsoft Exchange Server mailbox). As a result, all messages created will come from the same mailbox (everything is “from” the same person). For those who host multiple Web sites on their server, this may not do – you’d want one site to send messages from admin@domaina.com, another to send messages from admin@domainb.com, and so forth.

There is a free solution to this problem: use CDONTS. CDONTS, short for Collaborative Data Objects for NT Server, allows you to use a COM object to send email through the Microsoft SMTP service running on the same computer.

This article will not go in-depth into CDONTS, but it will show you how to interface with COM objects via SQL, and you’ll end up with a handy stored procedure which allows you to send customizable e-mail without relying on SQLMail.

Requirements

The scripts in this article have been written on a computer with Windows 2000 Server, SQL Server 2000, and the SMTP service installed. (The code will work fine on a computer with Windows 2000 Professional, SQL Server 2000, and the SMTP service.) Although I haven’t tested it, the code should work on Windows NT 4.0 Server, SQL Server 7.0, and the SMTP service (part of the Windows NT 4.0 Option Pack). It will not work with Windows NT 4.0 Workstation, which does not have the capability to run the SMTP service.

Note for SQL 7 users: There is a limitation in SQL 7 that truncates all strings passed to COM objects to 255 characters. As a result, even though the datatypes in the stored procedure allow more than 255 characters, only the first 255 characters will be passed to the CDONTS COM object. (It’s a limitation of the sp_OA… methods.) The only known workaround is to upgrade to SQL 2000 (not an inexpensive workaround!). One reader, Daniel M., notes that he was able to work around this by changing the VARCHAR fields to TEXT fields (a theoretical upper limit was not tested, but this goes beyond the 255 character limit).

Step 1: How it works – COM objects in SQL

The core functionality we will use is provided by a handful of SQL system stored procedures, grouped as the OLE Automation Extended Stored Procedures in MSDN. We’re concerned with five of these.

sp_OACreate – Creates an instance of a COM object
sp_OADestroy – Destroys an instance of a COM object
sp_OAMethod – Calls the method of a COM object
sp_OASetProperty – Sets the property of a COM object
sp_OAGetProperty – Gets the property of a COM object

Each of these stored procedures returns an error code. Anything but a return value of zero indicates an error. (We’re not concerned with handling different types of errors here; we’ll assume every error is critical.)

Step 2: Creating and Destroying Object References

Before you can work with a COM object, you have to create an instance of it with the sp_OACreate stored procedure. When you’re done working with the instance you created, you should destroy it with the sp_OADestroy stored procedure. Destroying every instance of an object when you’re done with it is good programming practice – always remember to do it!

sp_OACreate takes two parameters. The first is a VARCHAR() (string) parameter that contains either the class identifier (ClsID) or the programmatic identifier (ProgID) of the COM object. For the CDONTS “NewMail” object which we’ll be using, the ProgID (which is much easier to remember and use than class IDs) is CDONTS.NewMail. The second parameter is an INT OUTPUT (integer output) parameter which returns the object token for the instance created. This is required when calling subsequent OLE stored procedures.

sp_OADestroy takes one parameter: the object token of the COM object that we received when we called sp_OACreate. (See why it’s important to keep that value?)

The following block of code illustrates how to create the instance of the COM object and destroy it. Note how we’re trapping errors using the return values of each stored procedure.

    DECLARE @result INT
    DECLARE @object INT

    PRINT 'Creating the CDONTS.NewMail object'
    EXEC @result = sp_OACreate 'CDONTS.NewMail', @object OUTPUT

    IF @result <> 0
        PRINT 'sp_OACreate Failed'
    ELSE BEGIN
        PRINT 'Destroying the CDONTS.NewMail object'
        EXEC @result = sp_OADestroy @object
        IF @result <> 0
            PRINT 'sp_OADestroy Failed'
    END

Step 3: Setting and Getting Object Properties

Now that we have our object reference, we can set and get the properties of the object. Setting a property is done using the sp_OASetProperty stored procedure; getting a property is done using the sp_OAGetProperty stored procedure.

sp_OASetProperty takes three parameters. The first is the object token of the COM object that we received when we called sp_OACreate. The second parameter is a VARCHAR() (string) parameter that contains the property name you wish to set the value of. The third parameter is an OUTPUT parameter that contains the value of the property. Note that the datatype of this last parameter depends on the datatype of the property – strings, VARCHAR()s, integers to INTs, and so forth. If the property returns an object reference, use an INT datatype – this can then be used as an object token, much like we use the @object variable in our sample code.

sp_OAGetProperty also takes three parameters. The first and second parameters are the same as those of sp_OASetProperty – the object token of the COM object and the property name you wish to get the value of. The third parameter is the value you wish to set the property to. Again, the datatype of the last parameter depends on the datatype of the property – strings, integers, object tokens, and the like.

The following block of code adds these two stored procedures to our existing code, and illustrates how to get a property value (the “Version” property) and set a property value (the “From” property). Error trapping is getting a bit difficult to look at, but we’ll fix this when we transform this code into a stored procedure later.

    DECLARE @result INT
    DECLARE @object INT
    DECLARE @propertyvalue VARCHAR(255)

    PRINT 'Creating the CDONTS.NewMail object'
    EXEC @result = sp_OACreate 'CDONTS.NewMail', @object OUTPUT

    IF @result <> 0
        PRINT 'sp_OACreate Failed'
    ELSE BEGIN
        PRINT 'Get the From property'
        EXEC @result = sp_OAGetProperty @object, 'Version', @propertyvalue OUTPUT
        IF @result <> 0
            PRINT 'sp_OAGetProperty Failed'
        ELSE BEGIN
            PRINT 'CDONTS Version = ' + @propertyvalue
            PRINT 'Set the From property'
            EXEC @result = sp_OASetProperty @object, 'From', 'brian@demarzo.net'
            IF @result <> 0
                PRINT 'sp_OASetProperty Failed'
            ELSE BEGIN
                PRINT 'Destroying the CDONTS.NewMail object'
                EXEC @result = sp_OADestroy @object
                IF @result <> 0
                    PRINT 'sp_OADestroy Failed'
            END
        END
    END

Step 4: Calling Object Methods

To make our object actually do something, we usually have to call one of its methods. You call a method using the sp_OAMethod stored procedure.

sp_OAMethod takes a variable number of parameters, depending on the number of parameters required by the method being called. The first parameter is the object token of the COM object. The second parameter is a VARCHAR() (string) parameter that contains the name of the method you wish to call. The third parameter is an OUTPUT parameter that contains the return value of the method. The datatype of this parameter depends on the datatype of the return value (just like the last parameter in the sp_OAGetProperty stored procedure). If you do not need the return value, or if there is none, simply specify NULL. The fourth and subsequent parameters are passed as parameters to the method – datatypes depending on what the method expects. If you expect the method to return a value via the parameter, you must use a local variable and the OUTPUT keyword to retrieve the value.

(This stored procedure is one of the most complex, and most powerful, of those discussed here, so don’t be alarmed if it’s a bit confusing. When you see it used, it’ll make much more sense.)

We’ve added a call to sp_OAMethod to our sample code (below) to call the “Send” method of our object. This method takes four parameters: “From”, “To” “Subject”, and “Body” – each corresponding to the appropriate part of the e-mail to send. It does not return a value, so we will use NULL for the third parameter of our stored procedure.

    DECLARE @result INT
    DECLARE @object INT
    DECLARE @propertyvalue VARCHAR(255)

    PRINT 'Creating the CDONTS.NewMail object'
    EXEC @result = sp_OACreate 'CDONTS.NewMail', @object OUTPUT

    IF @result <> 0
        PRINT 'sp_OACreate Failed'
    ELSE BEGIN
        PRINT 'Get the From property'
        EXEC @result = sp_OAGetProperty @object, 'Version', @propertyvalue OUTPUT
        IF @result <> 0
            PRINT 'sp_OAGetProperty Failed'
        ELSE BEGIN
            PRINT 'CDONTS Version = ' + @propertyvalue
            PRINT 'Set the From property'
            EXEC @result = sp_OASetProperty @object, 'From', 'brian@demarzo.net'
            IF @result <> 0
                PRINT 'sp_OASetProperty Failed'
            ELSE BEGIN
                PRINT 'Sending the message using the Send method'
                EXEC @result = sp_OAMethod @object, 'Send', NULL, 'brian@demarzo.net', 'brian@demarzo.net', 
                    'My test message', 'Hello world! Look at my body!'
                IF @result <> 0
                    PRINT 'sp_OAMethod Failed'
                ELSE BEGIN
                    PRINT 'Destroying the CDONTS.NewMail object'
                    EXEC @result = sp_OADestroy @object
                    IF @result <> 0
                        PRINT 'sp_OADestroy Failed'
                END
            END
        END
    END

Step 5: Bringing it together – SP_CDONTS_NewMail_Send

Now that we know how everything works, we’ll create a stored procedure that does the work for us. The SP_CDONTS_NewMail_Send stored procedure will accept four parameters: “From”, “To” “Subject”, and “Body”. These will be passed to the “Send” method.

Since we’re working in the confines of a stored procedure, we can trap errors a bit more efficiently, returning the error code to the caller using the RETURN command.

The sample code below includes the script to create the stored procedure, then a bit of sample code to illustrate how to interface with it. Note how, in our stored procedure, we used VARCHAR(8000) as the datatype for each of our parameters. This may be overkill, but it illustrates what our capabilities truly are.

    --start stored procedure code
    CREATE PROCEDURE SP_CDONTS_NewMail_Send
        @from VARCHAR(8000),
        @to VARCHAR(8000),
        @subject VARCHAR(8000),
        @body VARCHAR(8000)

    AS

    DECLARE @result INT
    DECLARE @object INT

    PRINT 'Creating the CDONTS.NewMail object'
    EXEC @result = sp_OACreate 'CDONTS.NewMail', @object OUTPUT
    IF @result <> 0
    BEGIN
        PRINT 'sp_OACreate Failed'
        RETURN @result
    END

    PRINT 'Sending the message using the Send method'
    EXEC @result = sp_OAMethod @object, 'Send', NULL, @from, @to, @subject, @body
    IF @result <> 0
    BEGIN
        PRINT 'sp_OAMethod Failed'
        RETURN @result
    END

    PRINT 'Destroying the CDONTS.NewMail object'
    EXEC @result = sp_OADestroy @object
    IF @result <> 0
    BEGIN
        PRINT 'sp_OADestroy Failed'
        RETURN @result
    END

    RETURN 0

    GO
    --end stored procedure code

    DECLARE @result INT
    EXEC @result = SP_CDONTS_NewMail_Send 'brian@demarzo.net',
        'brian@demarzo.net',
        'My test message',
        'Hello world! Look at my body!'

    PRINT @result

Summary: COM, SQL, and SP_CDONTS_NewMail_Send

There’s much more functionality that CDONTS provides which isn’t touched on here, including support for attachments, blind carbon copy (bcc), and more – so be sure to check out Microsoft’s CDO for NTS site on MSDN, under Messaging and Collaboration.

As well, you can interface with any COM object via SQL – including other mail components, such as /n software’s IP*Works!; Windows Script components; and thousands of others. It’s a great way to expand the capabilities of SQL!