Posts tagged: inline code

Script to compare deployed BlackBerry handheld firmware with a local repository

By Scott, February 9, 2010 11:08 AM

If you use, say, BlackBerry Web Desktop Manager to facilitate firmware upgrades for users, then you have a bunch of them installed somewhere. (I have 49 versions among 15 carriers.) I keep a list of the ones I have installed, but I wanted to be able to compare that to what is actually in use among all the users in the organization. This way I can know which installs are obsolete, when new models are in use, and when newer versions are in use by users.

To use this script, in BlackBerry Manager (I am using 4.1.6) highlight all of your users and right-click on them, then select Export Asset Summary Data. The script will look for BESHandhelds.txt, but you can edit it to be whatever you want. The file that I use to track installed versions is just a csv with the following headers: Carrier, Model, Model Name, Version, Installed. Model Name is not used in the script, but mirrors the fields I use in an internal KB article that users can reference. Here is a sample table of entries:

Carrier Model Model Name Version Installed
AT&T 8320 Curve 4.5.0.182
AT&T 8520 Curve 4.6.1.314 No
KPN 8820 4.5.0.55

I added the Installed field as a way to track models that don’t actually have a local installation either because there is only one release from the carrier or all handhelds are already on that version so there isn’t a reason to have it installed. The script will look for a file called BBFirmware.csv but, again, you can change it to whatever you want.

The switch statement starting in line 17 is to map the carrier name as recorded in BES to how I record it in my file. The output of the script will let you know carriers that are not defined in the statement, but you will want to add or remove carriers as necessary first.

The output will also let you know if a model is in use that is not found in your installation file, if a there is a newer version for a particular model that is in use, and if all users are using the latest version you have installed. What this script does not do is determine if you have the latest version that has been released by the carrier (since that would be much harder to do). Lastly, it will let you know of installations you have for models that are not in use anymore so that you can uninstall them.

I am a perfectionist, so I can already see ways to improve the script, but I would never be done if I didn’t stop somewhere. You can download the ps1 file below.

  BBFirmwareCheck.zip (1.5 KB)

OCS Archiving Reporter: Group by conversation, filter dates

By Scott, December 3, 2009 4:46 PM

Edit: The inline code is not the latest version of the script. To get the latest version, download the full script on the Downloads page (or via the link at the end of the post).

The foundation for my script is from the OCS team at Microsoft, who created the original version.

The script from Microsoft grabs all the messages and outputs them all in one big table. I wanted it to be displayed more logically, grouping an entire conversation (session) together so it is easier to follow the context of the messages. This proved to be more daunting than I had anticipated.

All messages that occur in the same session have the same value for the SessionIdTime. With that I can group those messages together. But I needed a way to know when a new conversation started and ended. To get that I used the RANK function in SQL so that any rank of 1 indicates a new conversation. The resulting SQL query for a single user is the following (the ticks at the end of the lines are for PowerShell):

SELECT RANK() OVER(PARTITION BY SessionIdTime ORDER BY SessionIdTime, MessageIdTime) AS 'Rank', `
	SessionIdTime, MessageIdTime, Body, ContentTypeId, [from], [to] `
FROM `
(SELECT SessionIdTime, MessageIdTime, Body, ContentTypeId, u1.UserUri AS [from], u2.UserUri AS [to] FROM Messages, `
	Users u1, Users u2 WHERE Messages.FromId = u1.UserId AND Messages.ToId = u2.UserId AND u1.UserUri = '$User1' `
	AND LcsLog.dbo.Messages.Toast IS NULL `
UNION ALL `
SELECT SessionIdTime, MessageIdTime, Body, ContentTypeId, u2.UserUri AS [from], u1.UserUri AS [to] FROM Messages, `
	Users u2, Users u1 WHERE Messages.FromId = u2.UserId AND Messages.ToId = u1.UserId AND u1.UserUri = '$User1' `
	AND LcsLog.dbo.Messages.Toast IS NULL `
)AS dConversation

Now that I had the data, I needed to be able to use the XSLT to conditionally close a table the next time a rank of 1 is in the loop. I had the hardest time trying to make the xsl:if and xsl:choose statements work. Trying to include a </table> tag without the loop seeing the opening tag only caused errors. If I used HTML notation, &lt;/table&gt;, those characters were rendered as their literal ASCII representations rather than being parsed as HTML tags.

After trying for many, many hours, I decided to give up relying on the XML parser to convert the file to HTML and just do that parsing in the PowerShell script. This has the benefit of not requiring external files to run the script (the XSLT and msxsl.exe). The code below takes the XML output from the first part of the script and loops through each node (message). If the rank is 1, it closes the previous table, opens a new one, and writes the message to a row. If the rank is not 1, it means that a conversation has already been started and the message can simply being written in a new row.

#Convert XML to HTML
$sourceXML = [xml](Get-Content $LocalPath)
Remove-Item -path IM.html -ea SilentlyContinue
pwd | % {[string]$LocalPath = $_.path}
$LocalPath = $LocalPath + "\IM.html"
Add-Content -path $LocalPath -Value '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"'
Add-Content -Path $LocalPath -Value '  "http://www.w3.org/TR/html4/loose.dtd">'
Add-Content -Path $LocalPath -Value "<html>"
Add-Content -Path $LocalPath -Value "<head>"
Add-Content -Path $LocalPath -Value "<title>IM Conversation Archive</title>"
Add-Content -Path $LocalPath -Value "</head>"
Add-Content -Path $LocalPath -Value "<body>"

Add-Content -Path $LocalPath -Value '<font size="2" face="Verdana">'
Add-Content -Path $LocalPath -Value "<h2 align=`"center`">$($sourceXML.IMConversation.Title)</h2>"
Add-Content -Path $LocalPath -Value "<h3 align=`"center`">$($sourceXML.IMConversation.Subtitle)</h2>"
Add-Content -Path $LocalPath -Value "</font>"
Add-Content -Path $LocalPath -Value '<table border="0" cellpadding="1" width="100%" style="FONT-SIZE:8pt;FONT-FAMILY:verdana">'
foreach($IM in $sourceXML.IMConversation.IM)
	{
	if ($IM.Rank -eq "1")
		{
		Add-Content -Path $LocalPath -Value "</tbody></table><br>"
		Add-Content -Path $LocalPath -Value '<table border="1" cellpadding="1" width="100%" style="FONT-SIZE:8pt;FONT-FAMILY:verdana">'
		Add-Content -Path $LocalPath -Value '<thead><tr bgcolor="#C0C0C0" align="center">'
		Add-Content -Path $LocalPath -Value	'<th><font color="#0000FF">Session Time (UTC):</font></th>'
		Add-Content -Path $LocalPath -Value	'<th><font color="#0000FF">Message Time (UTC):</font></th>'
		Add-Content -Path $LocalPath -Value	'<th><font color="#0000FF">From:</font></th>'
		Add-Content -Path $LocalPath -Value	'<th><font color="#0000FF">To:</font></th>'
		Add-Content -Path $LocalPath -Value '<th width="40%"><font color="#0000FF">Message:</font></th>'
		Add-Content -Path $LocalPath -Value	"</tr></thead>"
		Add-Content -Path $LocalPath -Value	"<tbody><tr>"
		Add-Content -Path $LocalPath -Value	"<td rowspan=`"100`" valign=`"top`">$($IM.SessionTime)</td>"
		Add-Content -Path $LocalPath -Value	"<td>$($IM.DateTime)</td>"
		Add-Content -Path $LocalPath -Value	"<td>$($IM.From)</td>"
		Add-Content -Path $LocalPath -Value	"<td>$($IM.To)</td>"
		Add-Content -Path $LocalPath -Value	"<td>$($IM.Body.get_FirstChild().get_Data())</td>"
		Add-Content -Path $LocalPath -Value	"</tr>"
		}
	else
		{
		Add-Content -Path $LocalPath -Value	"<tr>"
		Add-Content -Path $LocalPath -Value	"<td>$($IM.DateTime)</td>"
		Add-Content -Path $LocalPath -Value	"<td>$($IM.From)</td>"
		Add-Content -Path $LocalPath -Value	"<td>$($IM.To)</td>"
		Add-Content -Path $LocalPath -Value	"<td>$($IM.Body.get_FirstChild().get_Data())</td>"
		Add-Content -Path $LocalPath -Value	"</tr>"
		}
	}

Add-Content -Path $LocalPath -Value "</table>"
Add-Content -Path $LocalPath -Value "</body>"
Add-Content -Path $LocalPath -Value "</html>"

Lastly, I wanted to be able to filter on a date range within the archiving database. So I added two arguments to the script after the users: -start and -end, both which are independently optional. If you don’t specify a second user or you only specify an end date with one or two users, you need to use the parameter names in the command. Otherwise, they are positional. The date format to use is "YYYY-MM-DD". To account for the presence of the date filter, this code is added to the script:

If ($start -ne '')
	{
	#Start date has been specified
	$startdate = $start
	$start = "and Messages.MessageIdTime >= '$start' "
	}
Else
	{
	$startdate = "Oldest record in the database"
	}

If ($end -ne '')
	{
	#End date has been specified
	$enddate = $end
	$end = "and Messages.MessageIdTime <= '$end' "
	}
Else
	{
	$enddate = "Newest record in the database"
	}

The code is able to correctly parse the presence of a start date, end date, or both. Now the following can be added to the end of the code block in the SQL query that makes up the derived table (the part in the nested parentheses): + $start + $end +.

Finally, the output is a pretty page with the subtitle that indicates the date range used, the table headers are formatted to visually separate each conversation, and each table is an indicated of a single conversation, regardless of the number of messages shown.

I don’t expect you to copy and paste all the code above into the script and have it magically work (especially since I made other minor changes to account for the subtitle, additional arguments, etc.), so you can download the entire script below or on the Downloads page.

  Get-ArchivingData.zip (3.1 KB)

How to clear the mail attribute using PowerShell

By Scott, November 9, 2009 8:09 AM

I have been struggling to delete the value in the mail attribute after a mailbox has been deleted. Exchange populates the mail attribute when a mailbox is created (even though Exchange has no use for the attribute), but doesn’t clear the attribute when the mailbox is deleted. With ADUC integration removed in Exchange 2007, a quick way to know if an account has a mailbox is to look at the mail attribute. But if removing a mailbox no longer clears that attribute, it is difficult know (just by looking at a user account in ADUC) if the account still has a mailbox.

Since Exchange doesn’t use the mail attribute, you can’t use the Set-Mailbox attribute, especially if the mailbox is deleted anyway. I tried using Set-User with the -WindowsEmailAddress parameter, but because the data type is Microsoft.Exchange.Data.SmtpAddress, setting the value to "" or $null doesn’t work because those aren’t properly formatted SMTP addresses.

So, I figured I needed to get away from any Exchange cmdlet. I used PowerShell’s native support for ADSI to bind to the user object: New-Object DirectoryServices.DirectoryEntry "LDAP://UserDN". But you will get an error if you try to set the attribute to null ($user.mail = $null). You can set it to an empty value (""), but you will then get an error when you try to commit the change: $user.SetInfo().

How can you possibly clear this attribute, one that is so easy to do in ADUC just by deleting the value in it? It is necessary to fall back to the PutEx method. Using that will let you use the ADS_PROPERTY_CLEAR constant (indicated by the numeric one in the first argument). It has taken me days to finally get to this point, so hopefully this post will shorten that time for others trying to do the same thing.

$user = Get-User "username"
$ldapDN = "LDAP://" + $user.distinguishedName
$adUser = New-Object DirectoryServices.DirectoryEntry $ldapDN
$adUser.PutEx(1, "mail", $null)
$adUser.SetInfo()

Add caching to mailbox creation load balancing script

By Scott, September 22, 2009 12:57 PM

In a previous post I instructed how to load balance new mailboxes across databases. In a large environment, however, this determination can take upwards of 90 seconds. Normally I don’t run the mailbox creation script at my company (our IT Security department does), but I ran it yesterday and it took 55 seconds to determine the database. That’s just too long for me.

So I decided to add what was going to be my fallback option in the first place: using a cached list. This code uses an xml file to keep track of the database to use at a given location and a timestamp of when that entry was last updated. The xml file uses the following format:

<?xml version="1.0"?>
<sites>
	<site id="Name1">
		<database name="Server1\Storage Group 1\Mailbox Store 1" />
		<timestamp time="2009-09-22T08:59:29.4878574-07:00" />
	</site>
	<site id="Name2">
		<database name="Server2\Storage Group 5\Mailbox Store 1" />
		<timestamp time="2009-09-22T08:17:51.5632031-07:00" />
	</site>
....
</sites>

Set the $sourcexml variable to the full path to the file. The function that calls this code should set the $Location parameter to whatever value is stored in the id attribute of the site nodes. The xml file is opened, the timestamp for a given site name is returned, and compared to a time interval (in this case, 24 hours, but you can set it to anything you want). If less than 24 hours old, the database name in the site node is used. If more than 24 hours old, it will run the code to determine the database to use (from the other post). After the determination is made, it writes the database name and the current time back into the xml file and saves it.

The on-demand query for one site ran in 53 seconds through mailbox creation for the first run, but created the second mailbox in 11 seconds. That is much more tenable, at least for an impatient person like myself.

$start = Get-Date
$sourcexml = "Path to input xml file.xml"
[xml]$db = Get-Content $sourcexml
$site = $db.sites.site | where {$_.id -eq $Location}
[datetime]$lastUpdate = $site.Timestamp.Time
if (($start - $lastUpdate).TotalHours -lt 24)
	{
	Write-Host "Timstamp for selected site is less than 24 hours old.  Using cached entry."
	$site.database.name
	}
else
	{
	Write-Host "Timestamp for selected site is more than 24 hours old.  Calculating database to use."
	#Run code here to determine database to use
	#
	#When done, continue
	$site.database.name = ($mailboxcount.GetEnumerator() | sort value | select -first 1).key
	$site.timestamp.time = (Get-Date -Format o).ToString()
	$db.save($sourcexml)
	$site.database.name
	}

Get automatic notification when running low on BES licenses

By Scott, July 30, 2009 4:57 PM

This VBScript uses native SQL connectivity via ADO to query your BlackBerry Enterprise Server configuration database for the current number of users. Since the license total is not stored in the database you have to set it as a variable in the script. It then compare the two numbers and if your defined threshold is exceeded, it will email you a notification. Just schedule the script to run daily. Download a zipped version here, or copy below.

'BES license usage monitor.  Set current license total, alert threshold, And
'other variables to be notified when the license usage exceeds the threshold.
Dim strSQLServer, strBESDB, iCurrentLicenseTotal, iLicenseThreshold
Dim strMailRecipients, strMailSender, strMailSubject, strMailServer
'*************************************
'------Configurable Variables---------
iCurrentLicenseTotal = 1000 'Total number of licenses in License Management
iLicenseThreshold = 20 'Free licenses threshold for notification
strSQLServer = "ServerName" 'NetBIOS name of SQL Server
strBESDB = "DatabaseName" 'Database name in SQL
'------Notification configuration-----
strMailRecipients = "user1@domain.com,user2@domain.com" 'Command-separated list of addresses
strMailSender = "BESLicenseCounter@domain.com" 'From address
strMailSubject = "BES license usage at critical level" 'Subject of notification message
strMailServer = "smtpserver.domain.com" 'FQDN of smarthost
'*************************************

Dim iCurrentUserTotal, iAvailableLicenses, strSQLQuery, strSQLConn
Dim objSQLConn, objSQLRecordSet, objSQLExec
strSQLConn = "Provider=SQLOLEDB.1;Integrated Security=SSPI;Data Source=" & _
	strSQLServer & "; Initial Catalog=" & strBESDB
strSQLQuery = "Select Count(*) As Total_Users From UserStats"
Set objSQLConn = CreateObject("ADODB.Connection")
Set objSQLRecordSet = CreateObject("ADODB.Recordset")
objSQLConn.Open = strSQLConn
objSQLRecordSet.Open strSQLQuery, objSQLConn
Set objSQLExec = objSQLRecordSet
While Not objSQLExec.EOF
	iCurrentUserTotal = objSQLExec.Fields("Total_Users").Value
	objSQLExec.MoveNext
Wend
iAvailableLicenses = iCurrentLicenseTotal - iCurrentUserTotal
If iAvailableLicenses < iLicenseThreshold Then
	Dim strMailBody
	strMailBody = iCurrentUserTotal & " of " & iCurrentLicenseTotal & " licenses are in use."
	strMailBody = strMailBody & "  It is time to order more licenses."
	SendMail
End If

Sub SendMail
	Set objEmail = CreateObject("CDO.Message")
	objEmail.Configuration.Fields.Item("http://schemas.microsoft.com/cdo/configuration/sendusing") = 2
	objEmail.Configuration.Fields.Item("http://schemas.microsoft.com/cdo/configuration/smtpserver") = strMailServer
	objEmail.Configuration.Fields.Item("http://schemas.microsoft.com/cdo/configuration/smtpserverport") = 25
	objEmail.Configuration.Fields.Update
	objEmail.From = strMailSender
	objEmail.To = strMailRecipients
	objEmail.Subject = strMailSubject
	objEmail.HTMLBody = strMailBody
	objEmail.Send
End Sub

Load balance new mailbox creation across multiple databases and servers using PowerShell

By Scott, May 27, 2009 2:42 PM

I needed a way to have new mailboxes be automatically distributed across a list of databases on multiple servers, and I recalled seeing a script somewhere.  The blog post with those details can be found here.  As the author points out, it can take a long time to build the hash table, too long for my requirements.  For example, one of my longer queries builds the table for 9 databases with over 5000 mailboxes in 58 seconds.  That is just way too  long for someone to feed a user account into to get a mailbox created.

So I broke down the query being used to find where efficiency could be improved.  The Get-ExchangeServer cmdlet slows the script down and can be avoided altogether by feeding the server list directly into the Get-MailboxDatabase cmdlet.  Additionally, I discovered just how inefficient the Exchange cmdlets are.  I already knew they were based on running other cmdlets for targets servers across a WAN that return not a lot of data, but this reinforced those findings.

To get away from the Exchange cmdlets, I query AD directly by instantiating ADSI in .NET manually.  Instead of slowly building the hash table I just take the record count of the search results and populate the table entry at once.  This reduced the time to completion from 58 seconds to 24 seconds.  These findings are consistent across different database counts and mailbox totals: 60% reduction in time.  While still too long for my taste, it certainly is much better than before and is within reason for the people at my company that make use of the script.

Since my script encompasses more than just the load balancing part (I create a GUI form where the location for the mailbox is selected, which feeds a list of appropriate servers to the function), this is just the part that creates the hash table with the database counts and returns the database with the lowest count:

$mbxServers = @()
$mailboxcount = @{}
$domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
$root = [ADSI]"GC://$($domain.Name)"
$mbxServers | %{Get-MailboxDatabase -server $_} | ?{$_.recovery -eq $FALSE} | %{
$filter = "(&(objectcategory=user)(homeMDB=" + ($_.DistinguishedName) + "))"
$search = new-Object System.DirectoryServices.DirectorySearcher($root,$filter)
$search.PropertiesToLoad.Add("homeMDB") | Out-Null
$result = $search.FindAll()
$mailboxcount["$($_.Identity)"] = $result.count
}
($mailboxcount.GetEnumerator() | sort value | select -first 1).key

PowerShell script to report last successful full backup of Exchange 2007

By Scott, July 22, 2008 7:28 AM

Edit: The inline code in this post is not the latest version of the script. Get the latest version from the downloads page.

This script is a port of my original backup report that was written in VBScript.  That script reports on both 2003 and 2007 servers, but lacked some of the features that I wanted to put in.  PowerShell natively supports date-awareness, which makes it much easier to add the number one feature I wanted to add: highlighting servers that haven’t had backups since a specified period of time.

Because I am using the native Exchange cmdlets instead of WMI or CDOEXM, this only reports on Exchange 2007 servers.  I figure accommodating both is more work than it is worth, so I just modified my VBScript version to not include any server in the Exchange 12 admin group and I have both run every day until my migration to 2007 is complete.

The script reports the last successful full backup of any Exchange 2007 server with the mailbox role installed.  It checks for the presence of storage groups and databases within them.  It notes if a backup is currently in progress, as well as if a backup has never completed.  If a backup has not completed in the last 72 hours (modifiable), it is highlighted in red so it is easy to spot.  If a backup is less than the defined number of hours old, I use the Marlett font to display a green checkmark.  This allows for a checkmark without having to reference an external image or embed one.  Lastly, the report is emailed.  The script is shown below, but you can also just download it.

#Last Backup Report for Exchange 2007 servers
#Version 1.0 - 7/9/08
#--------------------------------------------

#Begin customization-------------------------
$SmtpServer = "server.domain.com" #Enter FQDN of SMTP server
$SmtpFrom = "Exchange Backups <exchangebackupreport@domain.com>" #Enter sender email address
$SmtpTo = "user1@domain.com","user2@domain.com" #Enter one or more recipient addresses in an array
$SmtpSubject = "Exchange 2007 Last Backup Report" #Enter subject of message
$iNomHours = "72" #Enter number of hours since last backup that requires attention
#End customization---------------------------

$date = Get-Date
$sSpace = "&nbsp;&nbsp;&nbsp;&nbsp;"
$sOutput = "<table>"

#Checkmark to indicate last backup within nominal time
$sCheckMark = "<span style=""font-family: Marlett; color: green; font-size: 14pt; font-weight: bold"">a</span>"

#Retrieve Exchange servers with mailbox role
$ExServer = Get-ExchangeServer | where {$_.IsMailboxServer -eq $True} | Sort-Object Name
Foreach ($server in $ExServer)
	{
	$sOutput += "<tr><td><font size=2><u><b>$server</b></u></font></td><td></td></tr>"
	#Retrieve storage groups for a given server
	$StorageGroup = $server | Get-StorageGroup | Sort-Object Name
	#Check for absence of any storage groups
	If (($StorageGroup | Measure-Object Name).Count -eq $null)
		{
		$sOutput += "<tr><td><font size=2>" + $sSpace + "No storage groups present.</font></td></tr>"
		}
	Else
		{
		Foreach ($sg in $StorageGroup)
			{
			$sOutput += "<tr><td><font size=2>" + $sSpace + $sg.Name + "</font></td></tr>"
			#Retrieve mailbox databases for a given storage group
			$MailboxDatabase = $StorageGroup | Get-MailboxDatabase -Status | Sort-Object Name
			#Check for absence of any databases in storage group
			If (($MailboxDatabase | Measure-Object Name).Count -eq $null)
				{
				$sOutput += "<tr><td><font size=2>" + $sSpace + $sSpace + "No mailbox stores present.</font></td></tr>"				}
			Else
				{
				Foreach ($db in $MailboxDatabase)
					{
					$sBackupRunning = ""
					#Note if backup is currently running
					If ($db.BackupInProgress -eq $true)
						{$sBackupRunning = "<font size=2 color=blue>(Backup In Progress)</font>"}
					#Determine if backup has ever completed
					If ($db.LastFullBackup -ne $null)
						{
						$sBackupDay = $db.LastFullBackup.get_DayofWeek()
						$sBackupDateTime = $db.LastFullBackup.ToString("g")
						#Flag if last completed backup started longer than defined variable
						If (($date - $db.LastFullBackup).TotalHours -gt $iNomHours)
							{
							$sLastBackup = "<font size=2>Last Backup Started At: <font color=red>" + $sBackupDay + ", " + $sBackupDateTime + "</font></font>"
							}
						Else
							{
							$sLastBackup = "<font size=2>Last Backup Started At: " + $sBackupDay + ", " + $sBackupDateTime + " </font>" + $sCheckmark
							}
						}
					Else
						{
						$sLastBackup = "<font size=2>No full backup has completed yet.</font>"
						}
					$sOutput += "<tr><td><font size=2>" + $sSpace + $sSpace + $db.Name + " </font></td><td>" + $sLastBackup + $sBackupRunning + "</td></tr>"
					}
				}
			}
		}
	}
$sOutput += "</table>"

#Email results
$SmtpClient = New-Object System.Net.Mail.SmtpClient
$MailMessage = New-Object System.Net.Mail.MailMessage
$SmtpClient.Host = $SmtpServer
$MailMessage.From = $SmtpFrom
Foreach ($address in $smtpTo)
	{$MailMessage.To.Add($address)}
$MailMessage.Subject = $SmtpSubject
$MailMessage.IsBodyHTML = $true
$MailMessage.Body = $sOutput
$SmtpClient.Send($MailMessage)

Script to format an LDAP filter for readability

By Scott, August 7, 2007 7:32 AM

I have automated DLs that use LDAP filters for membership criteria.  These filters are stored as binary data in the extensionData attribute of the group object, so they are not easily accessible by users who want to know what filter is being applied to a DL they own.

It was easy enough to extract the LDAP filter from the attribute, convert it to a string value, and display it.  But users don’t generally know how to read LDAP filters, let alone represented as a long line of text.  So I started looking for utilities or scripts that would take an LDAP filter, parse it, and display it with nesting.  Uh, yeah, there aren’t any.  I was determined, and so countless hours later I have a function that do such a thing.

The difficult part was keeping track of the level of nesting/indentation at any point. Since a filter can be written in almost any order as long as the resulting equation equals what you want, I couldn’t use any kind of static detection. Just because an open parentheses is following by another one doesn’t mean that you are, say, three levels nested. So the important part of the script keeps track of the indentation level as the cursor position moves through the filter.

I replace ampersands in the filter with crosshatches while working with it because I was having weird results otherwise, most likely because the ampersand is an operator in both LDAP and VBScript. And because the output is to an IE window and the indentation uses non-breaking spaces instead of the traditional tab (which doesn’t exist in HTML), I substitute the HTML string for a non-breaking space with a unique string of letters so the ampersand in that doesn’t get in the way.

Because this is part of a bigger script, I pulled out just the portion that formats the LDAP filter and displays it in an IE window. You just need to provide the "raw" filter, whether directly in the script or some other method. Copy and paste the code below or download it here.

Option Explicit
Dim strLDAPFilter, strFormattedFilter
Dim objIE, objDoc
strLDAPFilter = "YourLDAPFilter"
Set objIE =  CreateObject("InternetExplorer.Application")   objIE.AddressBar = False
objIE.Menubar = False
objIE.Toolbar = False
objIE.Resizable = True
objIE.Height = 450
objIE.Width = 700
objIE.Visible = True
objIE.Navigate("about:blank")
While objIE.Busy
	WScript.Sleep 100
Wend

Set objDoc = objIE.Document
objDoc.Open
objDoc.Write("<TITLE>LDAP Filter Display</TITLE>")
objDoc.Write("<BODY BGCOLOR=#C0C0C0>")

'Parse the LDAP filter and format results for display
'Apply nesting for readability
strFormattedFilter = FormatLDAPDisplay(strLDAPFilter)
objDoc.Write(strFormattedFilter)
'Format single-line LDAP filter to include nesting
Function FormatLDAPDisplay(strLDAPFilter)
	'Replace ampersands with crosshatches to keep them from interfering
	strLDAPFilter = Replace(strLDAPFilter, Chr(38), Chr(35))

	'Iterate through each character in filter and insert CRLF and nesting
	Dim intPos, intIndentCount, intWhileIndent
	Dim strIndentation, strInsert, strCharacter, strNewLDAPFilter
	Dim bolDblClose
	intPos = 1
	intIndentCount = 0
	Do While intPos < Len(strLDAPFilter)
		strCharacter = Mid(strLDAPFilter, intPos, 1)
		intWhileIndent = 1
		strIndentation = ""
		Select Case strCharacter
			'LDAP operators to watch for to modify nesting level.
			'NOT operator ignored because only used in one-off attribute value
			Case Chr(35), Chr(124)
				'Operator followed by open paren means nesting increase
				If Mid(strLDAPFilter, intPos + 1, 1) = Chr(40) Then
					intIndentCount = intIndentCount + 1
					'Build nest based on number of indentations
					Do While intWhileIndent <= intIndentCount
						'Use unique string as placeholder for nesting with HTML spaces
						strIndentation = strIndentation & "QZNBSPQZNBSPQZNBSPQZNBSP"
						intWhileIndent = intWhileIndent + 1
					Loop
					'Insert new string for formatting
					strNewLDAPFilter = Replace(strLDAPFilter, strCharacter, strCharacter & "<br>" & strIndentation, intPos, 1)
					'Restore full filter including new string
					strLDAPFilter = Left(strLDAPFilter, intPos - 1) & strNewLDAPFilter
					'Move current position to next character after inserted string
					intPos = intPos + Len(strIndentation) + 6
				Else
					intPos = intPos + 1
				End If
			Case Chr(40)
				Do While intWhileIndent <= intIndentCount
					strIndentation = "QZNBSPQZNBSPQZNBSPQZNBSP" & strIndentation
					intWhileIndent = intWhileIndent + 1
				Loop
				If Not strIndentation = "" Then
					'If open paren follows close paren, insert CRLF
					If Mid(strLDAPFilter, intPos - 1, 1) = Chr(41) Then
						strIndentation = strIndentation & "<br>"
					End If
					strNewLDAPFilter = Replace(strLDAPFilter, strCharacter, strIndentation & strCharacter, intPos, 1)
					strLDAPFilter = Left(strLDAPFilter, intPos - 1) & strNewLDAPFilter
					intPos = intPos + Len(strIndentation) +1
				Else
					intPos = intPos + 1
				End If
			Case Chr(41)
				'Two consecutive close paren means nesting reduces one level
				If Mid(strLDAPFilter, intPos + 1, 1) = Chr(41) Then
					intIndentCount = intIndentCount - 1
					bolDblClose = True
				End If
				Do While intWhileIndent <= intIndentCount
					strIndentation = strIndentation & "QZNBSPQZNBSPQZNBSPQZNBSP"
					intWhileIndent = intWhileIndent + 1
				Loop
				strNewLDAPFilter = Replace(strLDAPFilter, strCharacter, strCharacter & "<br>" & strIndentation, intPos, 1)
				strLDAPFilter = Left(strLDAPFilter, intPos - 1) & strNewLDAPFilter
				'Adjust position to account for two close paren
				If bolDblClose = True Then
					intPos = intPos + Len(strIndentation) + 5
					bolDblClose = Empty
				Else
					intPos = intPos + Len(strIndentation) + 6
				End If
			Case Else
				'No paren or operator means move to next character
				intPos = intPos + 1
		End Select
	Loop

	'Replace LDAP operators with words
	strLDAPFilter = Replace(strLDAPFilter, Chr(35), "<i>AND</i>")
	strLDAPFilter = Replace(strLDAPFilter, Chr(124), "<i>OR</i>")
	strLDAPFilter = Replace(strLDAPFilter, Chr(33), "<i>NOT </i>")
	'Replace spaceholders with HTML spaces
	strLDAPFilter = Replace(strLDAPFilter, "QZNBSP", Chr(38) & "nbsp;")
	FormatLDAPDisplay = strLDAPFilter
End Function

Set objIE = Nothing

Add comment notification to simplebog 3.0

By Scott, July 30, 2007 11:52 AM

Since I am using approval for comments on my site, I had no way of knowing when someone posted a comment pending approval.  And since you can’t simply look for comments to posts without going into the database directly, I needed a way to know when someone has posted a comment and to which post it belongs.

To do this, add this subroutine to the end of functions.asp, which is the CDO code to send a message.  I didn’t use variables that are assigned in config.asp, so you will have to set them in the subroutine directly for SMTP server, from address, to address, and domain name in the body. 

<%
'Send comment notification
Sub SendEmail (strBDate, strBID)
	Dim objMail
	Set objMail = CreateObject("CDO.Message")
	objMail.Configuration.Fields.Item ("http://schemas.microsoft.com/cdo/configuration/sendusing")      = 2
	objMail.Configuration.Fields.Item ("http://schemas.microsoft.com/cdo/configuration/smtpserver")     = "SMTP hostname"
	objMail.Configuration.Fields.Item ("http://schemas.microsoft.com/cdo/configuration/smtpserverport") = 25
	objMail.Configuration.Fields.Update
	objMail.From     = "fromaddress@yourdomain.com"
	objMail.To       = "toaddress@yourdomain.com"
	objMail.Subject  = "Comment has been submitted for approval"   
	objMail.HTMLBody = "A comment has been submitted for approval.  Go to http://www.yourdomain.com/admin?cmd=bloglist&view=calendar&blogDate=" & strBDate & "&comments=" & strBID & " to approve."
	objMail.Send
	Set objMail = Nothing
End Sub
%>

Then you need to add code to functions.asp to call this subroutine from the subroutine that inserts the comment.  Find the subroutine labeled InsertComment(), which should be around line 475 depending on other mods of mine or your own that you may have inserted.  Right before the existing line: 

Response.Redirect("default.asp?view=plink&id=" & bID & "&comments=1")

insert this code:

' convert blog ID to blog Date
strSQL = "SELECT * FROM  T_WEBLOG WHERE id = " & bID & " ORDER BY id DESC"
Set Rs = Server.CreateObject("ADODB.Recordset")
Rs.ActiveConnection = strConn
Rs.Source = strSQL
Rs.CursorType = 0
Rs.CursorLocation = 2
Rs.LockType = 1
Rs.Open()

If Not rs.EOF Then
	rs.MoveFirst
	While Not rs.EOF
		strBDate = rs("b_date")
		rs.MoveNext
	Wend
End If

SendEmail strBDate, bID

This is necessary because the comments are not accessed by using the blog entry ID, but the blog entry’s date.  So it is necessary to cross-reference the entry ID to the entry date, and then link to the comments for a given entry ID on that date.  The last line calls the email function which will include a hyperlink to the comments for the blog entry that has a new comment.

Now, that is all fine and dandy.  You will receive the email with the link, but when you follow it to your site, you probably won’t have an active session so you will have to log in.  The way the admin default and login pages work, you will lose the link to the entry you need to approve so you have to click the link in the email again.  I work around this by implementing the use of query strings so the site remembers where you were trying to go before you had to log in.

To add this feature, edit admindefault.asp.  At line 8, replace the Response.Redirect line with the following code: 

Dim sProtocol, sDomain, sPath, sQuerystring, sResult
sProtocol = "http://"
If UCase(Request.ServerVariables("HTTPS")) = "ON" Then
	sProtocol = "https://"
	sDomain = LCase(Request.ServerVariables("SERVER_NAME"))
	sPath = LCase(Request.ServerVariables("SCRIPT_NAME"))
	sQuerystring = LCase(Request.Querystring)
	sResult = sProtocol & sDomain & sPath
	If Len(sQuerystring) > 0 Then
		sResult = sResult & "?" & sQuerystring
		sResult = Server.URLEncode(sResult)
		Response.Redirect("login.asp") & "?sURL=" & sResult

This builds a query string of the URL you were going to go to before you are redirected to the login page.  Now edit adminlogin.asp.  At line 34, before the If statement to check for a postback, insert the following line: 

strSourceURL = Request.QueryString("sURL")

This retrieves the query string and puts it into a variable.  At line 39, before the SQL statement to select the users from the database, insert the following code: 

If Not Trim(Request.Form("sourceURL")) = "" Then
	strSourceURL = Request.Form("sourceURL")
Else
	strSourceURL = "./?"
End If

This extra code is added to accommodate you entering a bad password or if you are logging in without following a comment link.  At line 74, comment out Johann’s Response.Redirect line after a successful login and then insert and replace the Response.Redirect for no user found with this: 

	Response.Redirect(strSourceURL)
Else
	Response.Redirect("login.asp?error=nouser") & "&sURL=" & Server.URLEncode(strSourceURL)

This sends you to the page you were intending to go to in the first place.  If you entered a bad password, this also preserves the original URL after the login page reloads.

Last, but not least, you need to a hidden form field that will store the original URL when you submit the form to log in.  Near the end of the file, insert a line before the close form tag and paste this: 

<input name="sourceURL" type="hidden" value="<%= strSourceURL %>" />

I hope that’s not too complicated to follow.  It’s a little more convoluted for this modification because it is necessary to add code in multiple places in multiple files, rather than just a chunk of code in one file.  But, now you will receive an email when a comment is posted and be easily able to approve or deny the comment just by following the link in the email.

Updated: Copy DLs from one user to another

By Scott, July 22, 2007 4:01 PM

The first version of the script really was quick and dirty, requiring you to manually put the source and target users’ DNs in the script.  Since a coworker has been using the script, I thought it appropriate to update it to prompt for the usernames.  In addition, I added a new feature I recently read about, which is to output the results in real-time to a GUI.  This is done by creating an object for IE and writing the output similar to wscript.echo, but with the Write method of the object.

Like the original script, since we use automated DLs, too, I look for an indication that a given DL is a SmartDL and skip it.  And I now use PrimalScript to work with my scripts, so I use its packager to make an exectuable.  This makes it easier and nicer for non-IT end-users who will be running scripts like these.

Download it here, or copy/paste below.

'Version 2.0 - July 23, 2007
'Copy distribution group membership from one user to another,
'excluding automated DLs (SmartDL).
'Get source user
While Not bolExit = True
	strOldSamUser = InputBox("Enter the sAMAccountName of the person to copy DLs FROM." _
		, "Enter username")
	If strOldSamUser = "" Then
		WScript.Quit
	End If

	'Find the Global Catalog server
	Set objCont = GetObject("GC:")
	For Each objGC In objCont
		strADsPath = objGC.ADsPath
	Next

	Set objConnection = CreateObject("ADODB.Connection")
	Set objRecordset = CreateObject("ADODB.Recordset")
	objConnection.Provider = "ADsDSOObject"

	objConnection.Open "ADs Provider"
	strQuery = "<" & strADsPath & ">;(&(objectcategory=user)(sAMAccountName=" & strOldSamUser & _
		"));displayName,distinguishedName;subtree"
	Set objRecordset = objConnection.Execute(strQuery)

	If Trim(objRecordset.Fields("distinguishedName")) = "" Then
		strNoUser = MsgBox("Warning: User cannot be found.  Verify sAMAccountName.", vbCritical, "User not found!")
		bolExit = False
	Else
		intCorrectUser = MsgBox("Is this the correct user?" & VbCrLf & VbCrLf & "Display Name: " & _
		objRecordset.Fields("displayName") & VbCrLf & "DN: " & objRecordset.Fields("distinguishedName"), _
			vbYesNo, "Old user?")
		If intCorrectUser <> 6 Then
			bolExit = False
		Else
			strSrcDN = objRecordset.Fields("distinguishedName")
			bolExit = True
		End If
	End If
 Wend
'Open IE to display progress and results
Set objIE = CreateObject("InternetExplorer.Application")
objIE.AddressBar = False
objIE.Menubar = False
objIE.Toolbar = False
objIE.Resizable = True
objIE.Left = 10
objIE.Height = 450
objIE.Width = 800
objIE.Visible = True
objIE.Navigate("about:blank")
While objIE.Busy
	WScript.Sleep 100
Wend
Set objDoc = objIE.Document
objDoc.Open
objDoc.Write("<TITLE>Copy DL Membership</TITLE>")
objDoc.Write("<BODY BGCOLOR=#C0C0C0>")
objDoc.Write("<P><b>Source:</b> " & objRecordset.Fields("distinguishedName") & "<br>")
'Get target user
bolExit = False
While Not bolExit = True
	strNewSamUser = InputBox("Enter the sAMAccountName of the person to copy DLs TO." _
		, "Enter username")
	If strNewSamUser = "" Then
		objIE.Quit
		WScript.Quit
	End If
	strQuery = "<" & strADsPath & ">;(&(objectcategory=user)(sAMAccountName=" & strNewSamUser & _
		"));displayName,distinguishedName;subtree"
	Set objRecordset = objConnection.Execute(strQuery)
	If Trim(objRecordset.Fields("distinguishedName")) = "" Then
		strNoUser = MsgBox("Warning: User cannot be found.  Verify sAMAccountName.", vbCritical, "User not found!")
		bolExit = False
	Else
		intCorrectUser = MsgBox("Is this the correct user?" & VbCrLf & VbCrLf & "Display Name: " & _
			objRecordset.Fields("displayName") & VbCrLf & "DN: " & objRecordset.Fields("distinguishedName"), _
			vbYesNo, "Old user?")
		If intCorrectUser <> 6 Then
			bolExit = False
		Else
			Set objTargetUser = GetObject("LDAP://" & objRecordset.Fields("distinguishedName"))
			bolExit = True
		End If
	End If
 Wend
 'Write target user to IE window
 objDoc.Write("<b>Target:</b> " & objRecordset.Fields("distinguishedName") & "</P>")
 'Copy DLs
 strDomFQDN = Mid(strSrcDN, InStr(LCase(strSrcDN), ",dc=") + 4)
 strGCFQDN = Replace(LCase(strDomFQDN), ",dc=", ".")
 Set objOldUser = GetObject("GC://" & strGCFQDN & "/" & strSrcDN)
 For Each strGroup in objOldUser.MemberOf
	On Error Resume Next
	Set objGroup = GetObject("LDAP://" & strGroup)
	If Not Trim(objGroup.mailNickname) = "" Then
		If Not Instr(objGroup.info, "SmartDL") > 0 Then
			objGroup.Add(objTargetUser.ADsPath)
			If Err.Number = 0 Then
				objDoc.Write(objGroup.DisplayName & ": Update successful.<br>")
			Else
				objDoc.Write(objGroup.DisplayName & ": Update UNSUCCESSFUL.<br>")
			End If
		Else
			objDoc.Write(objGroup.DisplayName & ": Skipped (SmartDL).<br>")
		End If
	End If
	On Error Goto 0
 Next

 Set objOldUser = Nothing
 Set objTargetUser = Nothing
 Set objIE = Nothing
 Set objRecordset = Nothing
 Set objConnection = Nothing
 

Panorama Theme by Themocracy