StructureCMS

January 27, 2012

Using the SmarterStats Query API service in ColdFusion

Filed under: ColdFusion, Programming — joel.cass @ 3:53 pm

I must admit that it’s been a while since I’ve posted anything in this blog. To be honest, it’s mainly because there’s been nothing remarkable to post about. Nothing anyone would benefit from knowing, really.

It’s even to the point that my role involves much more than just programming these days – recently I have been involved in reviewing web statistics packages in the hope of landing on a solution. Well, the solution is found in SmarterStats – quite a remarkable web statistics package that does most things. It even has an API.

About that API – it has a largely undocumented ‘Query’ webservice. It is not enabled by default, but once enabled it allows you to query the statistics data via a web service.

To enable the service, you will need to create an authorisation key. A good one to use is a UUID. Then, you need to define that key in [SmarterStats_Root]\MRS\App_Data\Config\AppConfig.xml:

    <LocalHostDeleted>false</LocalHostDeleted>
++  <WebServiceAuthorizationCode>nnnnnnnn-nnnn-nnnn-nnnnnnnnnnnnnnnn</WebServiceAuthorizationCode>
    <ExpirationNotification />

Then you can access the service via http://localhost:9999/Services/Query.asmx, WSDL path http://localhost:9999/Services/Query.asmx?wsdl

The query service uses some form of psuedo-sql that runs a query against what looks like a function result, ie. SELECT * FROM fTableName(siteId, dateFrom, dateTo, maxItems, extraParam**) WHERE blah=blah ORDER BY blah.

** Parameters maxItems and extraParam are undocumented and were found by chance. MaxItems is pretty obvious, it’s the number of records to retrieve. Use extraParam to filter certain queries by page. Only certain queries support the extra parameter so try them out.

A list of tables is available in the file [SmarterStats_Root]\MRS\App_Data\Config\ReportConfig.xml – you can copy this to your development directory and then use it to generate a list of available reports.

Here are some example reports you can call using the web service (assume site id = 1):

  • Get Daily Site Traffic
    SELECT * FROM fActivityTotalTrend(1, '2012-01-01T00:00:00', '2012-01-27T00:00:00', 50)
  • Get Daily traffic to home page
    SELECT * FROM fDailyActivityForFile(1, '2012-01-01T00:00:00', '2012-01-27T00:00:00', 20, '/')
  • All pages, ordered by popularity
    SELECT * FROM fTopPages(1, '2012-01-01T00:00:00', '2012-01-27T00:00:00')
  • Top 10 most popular pages
    SELECT * FROM fTopPages(1, '2012-01-01T00:00:00', '2012-01-27T00:00:00', 10)
  • Web browsers
    SELECT * FROM fBrowsers(1, '2012-01-01T00:00:00', '2012-01-27T00:00:00')
  • Visitors by City
    SELECT * FROM fGeographicCountryByCity(1, '2012-01-01T00:00:00', '2012-01-27T00:00:00')
  • Visitors by City, to a certain URL
    SELECT * FROM fGeographicsByFile(1, '2012-01-01T00:00:00', '2012-01-27T00:00:00', 20, '/some/url/')

Calling the web service from coldfusion is pretty easy:

<cfsetting enablecfoutputonly="true">

<!--- 'static' service parameters --->
<cfset strUsername = "username">
<cfset strPassword = "password">
<cfset strAuthCode = "nnnnnnnn-nnnn-nnnn-nnnnnnnnnnnnnnnn">
<cfset strWebServiceUrl = "http://127.0.0.1:9999/Services/Query.asmx?wsdl">

<!--- initialise web service --->
<cfset objWebService = createObject("webservice", strWebServiceUrl)>

<!--- query parameters (you can pass these in from a form) --->
<cfset strReport = "fTopPages">
<cfset numSiteId = 1>
<cfset dateFrom = createDate(year(now()), month(now()), 1)>
<cfset dateTo = createDate(year(now()), month(now()), day(now()))>
<cfset numRows = 20>

<!--- compose query --->
<cfset strQuery = "SELECT * FROM #strReport#(#numSiteId#, '#dateformat(dateFrom,'yyyy-mm-dd')#T00:00:00', '#dateformat(dateTo,'yyyy-mm-dd')#T00:00:00')">

<!--- execute query --->
<cfset dsResult = objWebservice.executeQuery(strUsername, strPassword, strAuthCode, numSiteId, strQuery, numRows)>

<!--- convert to query --->
<cfset qryResult = datasetToQuery(dsResult, 'Table1')><!--- version 7: it is 'Table1', version 6: it is 'results' --->

<!--- output result --->
<cfdump var="#qryResult#">

<!--- FUNCTION TO CONVERT .NET DATASET TO QUERY (added here for convenience - move to helper class) --->

<cffunction name="datasetToQuery" access="public" returntype="query" output="false">
	<cfargument type="any" name="dataset" required="true">
	<cfargument type="string" name="tablename" required="true">

	<!--- dataset has 2 nodes: 1) Column definitions 2) Data --->

	<cfset var qryResult = "">
	<cfset var lstColumns = "">
	<cfset var lstTypes = "">
	<cfset var aryDataset = ARGUMENTS.dataset.get_any()>
	<cfset var aryColumns = XmlSearch(aryDataset[1].getAsString(), "/xs:schema/xs:element[@name='#ARGUMENTS.tablename#']/xs:complexType/xs:sequence/xs:element")>
	<cfset var aryData = XmlSearch(aryDataset[2].getAsString(), "/diffgr:diffgram/NewDataSet/#ARGUMENTS.tablename#")>
	<cfset var i = 0>
	<cfset var c = 0>

	<!--- get columns --->
	<cfloop from="1" to="#arrayLen(aryColumns)#" index="i">
		<cfset lstColumns = listAppend(lstColumns, aryColumns[i].xmlAttributes.name)>
		<cfswitch expression="#aryColumns[i].xmlAttributes.type#">
			<cfcase value="xs:double,xs:long">
				<cfset lstTypes = listAppend(lstTypes, 'double')>
			</cfcase>
			<cfcase value="xs:date">
				<cfset lstTypes = listAppend(lstTypes, 'timestamp')>
			</cfcase>
			<cfdefaultcase>
				<cfset lstTypes = listAppend(lstTypes, 'varchar')>
			</cfdefaultcase>
		</cfswitch>
	</cfloop>

	<!--- create query object --->
	<cfset qryResult = queryNew(lstColumns, lstTypes)>

	<!--- populate query --->
	<cfloop from="1" to="#arrayLen(aryData)#" index="i">
		<cfset queryAddRow(qryResult)>
		<cfloop from="1" to="#arrayLen(aryData[i].xmlChildren)#" index="c">
			<cfset querySetCell(qryResult, aryData[i].xmlChildren[c].xmlName, aryData[i].xmlChildren[c].xmlText)>
		</cfloop>
	</cfloop>

	<cfreturn qryResult>
</cffunction>

<cfsetting enablecfoutputonly="false">

All this was tested in SmarterStats 7.

August 17, 2011

ColdFusion on Linux – Make sure your hostname is correct!

Filed under: ColdFusion, Technology — joel.cass @ 10:52 am

Recently I have been given the task to install ColdFusion on CentOS. Everything went well, Apache installed fine, related dependencies installed fine, even ColdFusion installed fine. Until I tried accessing the site, upon which I was presented with this error:

java.lang.NullPointerException
	at java.lang.String.indexOf(String.java:1733)
	at java.lang.String.indexOf(String.java:1715)
	at jrun.servlet.session.SessionService.getUrlSessionID(SessionService.java:1097)
	at jrun.servlet.ForwardRequest.getRequestedSessionId(ForwardRequest.java:426)
	at jrun.servlet.ForwardRequest.isRequestedSessionIdValid(ForwardRequest.java:467)
	at jrun.servlet.ForwardRequest.getSession(ForwardRequest.java:344)
	at jrun.servlet.ForwardRequest.create(ForwardRequest.java:135)
	at jrun.servlet.JRunRequestDispatcher.invoke(JRunRequestDispatcher.java:253)
	at jrun.servlet.ServletEngineService.dispatch(ServletEngineService.java:543)
	at jrun.servlet.http.WebService.invokeRunnable(WebService.java:172)
	at jrunx.scheduler.ThreadPool$ThreadThrottle.invokeRunnable(ThreadPool.java:428)
	at jrunx.scheduler.WorkerThread.run(WorkerThread.java:66)

I had installed ColdFusion 9, in a similar method to that described here. So, I tried installing ColdFusion 8 following the same method. Still got the error. I then tried installing ColdFusion 9 to use the root user (not recommended). Still got the error.

So I pulled my hair out for a bit, and then started scanning logs, upon which I came across this line:

08/16 23:23:54 error centostemplate.xxxx.xxx.au: centostemplate.xxxx.xxx.au
java.net.UnknownHostException: centostemplate.xxxx.xxx.au: centostemplate.xxxx.xxx.au

…and then it all fell together! The instance I had been given was created off a template, in which the host name was set in /etc/sysconfig/network to ‘centostemplate.xxxx.xxx.au’ – which did not resolve via DNS! So, the easy fix was to map this over in /etc/hosts to localhost, i.e.

127.0.0.1    centostemplate.xxxx.xxx.au

Restart the services, fixed! This is CentOS in my case, but I think if you ever run into this problem on a *nix platform, check your network config and ensure that your configured hostname resolves to an IP address.

February 9, 2011

Retrieving HTTP URLs in PHP

Filed under: PHP — joel.cass @ 9:32 am

It’s strange how many different ways there are to do the same thing in PHP. For example,if you want to retrieve a URL, it can be as easy as calling file_get_contents($url), or you can use the PECL libraries, or you can go dig up an open source project such as this one.

I was messing around one night and figured it would be possible to just run an http request over a socket. As it turns out it’s not so difficult, there is tons of information out there on how to do it and it wasn’t long before I had a method figured out.

The advantage of this is that it is lightweight and gives you some control over the headers (etc) that you want to send/receive. This has only been tested on text-only requests.

function get_http_content ($url, $timeout = 3, $headers = array()) {
	// initialise return variable
	$stcReturn = array("headers"=>array(), "content"=>"");

	// get server name, port, path from URL
	$strRegex = "/^(http[s]?)\:\/\/([^\:\/]+)[\:]?([0-9]*)(.*)$/";
	$strServer = preg_replace($strRegex,"$2",$url);
	$strPath = preg_replace($strRegex,"$4",$url);
	$numPort = preg_replace($strRegex,"$3",$url);
	if ($numPort == "") {
		if (preg_replace($strRegex,"$1",$url) == "https") {
			$stcReturn["headers"]["Status-Code"] = "0";
			$stcReturn["headers"]["Status"] = "HTTPS is not supported";
			$stcReturn["content"] = "Error: HTTPS is not supported";
		} else {
			$numPort = 80;
		}
	}

	// connect to server, run request
	$objSocket = fsockopen($strServer, $numPort, $numError, $strError, $timeout);
	if (!$objSocket) {
		// connection not possible
		$stcReturn["headers"]["Status-Code"] = $numError;
		$stcReturn["headers"]["Status"] = $strError;
		$stcReturn["content"] = "Error: {$strError} ({$numError})";
	} else {
		// connection made - send headers
		$strOut = "GET {$strPath} HTTP/1.1\r\n";
		$strOut .= "Host: {$strServer}\r\n";
		$strOut .= "Connection: Close\r\n";
		foreach ($headers as $strName=>$strValue) {
		$strOut .= "$strName: $strValue\r\n";
	}
	$strOut .= "\r\n";
	// get data
	fwrite($objSocket, $strOut);
	$strIn = "";
	while (!feof($objSocket)) {
		$strIn .= fgets($objSocket, 128);
	}
	fclose($objSocket);

	// split data into lines
	$aryIn = explode("\r\n", $strIn);

	// data is split into headers/content by double CR
	$bHeader = true;
		foreach ($aryIn as $i=>$strLine) {
			if ($i == 0) {
				// first line is [protocol] [status code] [status]
				$stcReturn["headers"]["Protocol"] = preg_replace("/^([^ ]+) ([^ ]+) (.+)$/", "$1", $strLine);
				$stcReturn["headers"]["Status-Code"] = preg_replace("/^([^ ]+) ([^ ]+) (.+)$/", "$2", $strLine);
				$stcReturn["headers"]["Status"] = preg_replace("/^([^ ]+) ([^ ]+) (.+)$/", "$3", $strLine);
			} elseif ($bHeader && $strLine == "") {
				// if line is empty headers have ended
				$bHeader = false;
			} elseif ($bHeader) {
				// set header
				$stcReturn["headers"][preg_replace("/^([^\:]+)\:[ ]*(.+)$/", "$1", $strLine)] = preg_replace("/^([^\:]+)\:[ ]*(.+)$/", "$2", $strLine);
			} else {
				// set content
				$stcReturn["content"] .= $strLine;
				if ($i < count($aryIn)-1) {
					$stcReturn["content"] .= "\r\n";
				}
			}
		}
	}
	return $stcReturn;
}

December 7, 2010

Configuring ColdFusion to have different JVM Settings per instance

Filed under: ColdFusion, Programming — joel.cass @ 9:08 am

Recently I had upgraded a ColdFusion server from standalone to multiple instance. This was easy – basically a matter of installing a new copy of ColdFusion as multi instance and copying the settings from the standalone instance. However, the issue has now arisen that every app on the server is to run as it’s own instance and if they all share the same settings, there will not be enough memory to run each instance smoothly.

The problem is, that some instances will require more memory while some will require less. Adobe had posted how to do this on their website but it seems to have been deleted recently. Luckily the instructions were still available google cache! I’ve copied the instructions here for later reference. As I will forget…

Basically, it’s three steps:

  1. Open up the JRun/bin directory
  2. Copy jvm.config to jvm_<server_name>.config
  3. Configure the startup script by
    • Windows: use jrunsvc -remove "<service_name>", then jrunsvc -install <server_name> <server_name> "<service_name>" -config jvm_<server_name>.config
    • Linux/Unix/Mac: add -config jvm_<server_name>.config to the startup command, e.g. jrun -start default -config jvm_<server_name>.config

* <server_name> is the name of the folder under the JRun/servers directory that contains the server, e.g “cfusion”
** <service_name> is the name of the service in windows, e.g. “Macromedia JRun cfusion Service”

November 5, 2010

Setting up an extranet login page in Sitecore

Filed under: Sitecore — joel.cass @ 8:30 am

Recently, I had issues with the setup of a public logon page in Sitecore. The setup was very similar to the way that login work in the Intranet solution, e.g.

1. A login.aspx page is created in the project folder
2. Settings are added to the web.config <site> tag: loginPage=”/login.aspx” + requireLogin=”true”
3. The login page either displays a form or is secured in IIS and then gets the AUTH_USER header to login users (if implmenting an AD solution)

The problem is, so it seems, that the latest version of sitecore (6.2) works differently from previous versions as documented here and here. The URL parameters item, user, and site are no longer passed. Furthermore, adding a SecurityResolver pipeline didn’t seem to work any longer.

So in 6.2, when a user cannot be authenticated to access a page, they are simply redirected to /login.aspx without any return URL or other useful information. This makes the situation even worse if you are trying to preview a page from the administration interface – basically every initial request is redirected to /login.aspx, and once authenticated the user is returned to the home page, as the original URL was lost when the user was redirected to /login.aspx.

Things seemed futile until a text search of the various config’s revealed the following setting in the web config:

      <!--  SAVE RAW URL ON LOGIN
            Specifies whether the original request URL is passed to the login page
            (saved in 'url' query string parameter).
            Default: false
      -->
      <setting name="Authentication.SaveRawUrl" value="false" />

Changing this setting to “true” now means that the return URL is passed through to /login.aspx as the ‘url’ querystring parameter. You’ll need to modify your login.aspx to look for this parameter and decode the parameter using Server.UrlDecode before redirecting.

This solution is simpler than the previous options available. It’s probably documented somewhere, I just never got a chance to read about it. I hope this is of help to anyone else who may be facing the same issues.

October 26, 2010

Setting the editor stylesheet in Sitecore

Filed under: Sitecore — joel.cass @ 11:02 am

Recently I was working on a site in Sitecore, and was thinking that it would be great if the editor stylesheet could be changed in the system.

Searching the Internet was generally fruitless, and looking through the core data, I couldn’t find any stylesheet config strings. Then, I stumbled upon the SIP Intranet guide, which pointed out that the editor stylesheet is in the web.config:

(section 4.2.1) The stylesheet that is used for the styling of the rich text fields within the editor is determined by the WebStylesheet setting in the web.config file.

And there it is:



Changing this field did actually update the CSS used in the editor, as well as populate the styles listed.

October 6, 2010

SQL Server 2000 Replication – Good one Microsoft

Filed under: Database — joel.cass @ 3:56 pm

One thing that I hate having to deal with, is replication in SQL Server 2000. It just seems like it’s half finished, and no-one bothered to think about what they’re doing when they wrote it. I’m not going to profess that I’m an expert in the area, I just think that it should have been done differently.

Recently I’ve had to set up replication going both ways between two servers. I had been recommended to stick to transactional replication, as it is being used on other databases set up similarly. One problem I have been having with transactional replication however, is that if a table is replicated both ways, the transactions related to the replication of data will be replicated, resulting in a horrible circular reference and before too long, full logs and no more disk space.

Furthermore, you will get random errors that occur anytime new data is inserted into a table on either database: “Cannot insert duplicate key row in object [blah]” or “Violation of [blah] constraint ‘[blah]‘. Cannot insert duplicate key in object ‘[blah]‘.”. Microsoft erroneously suggest that you add the term “-SkipErrors 2601;2627″ to the startup of the Distribution agent. Wrong. It should be “-SkipErrors 2601:2627″ – and, no error occurs on startup if the parameter is incorrect.

So, solving the full logs issue? You will need to stop both agents from running continuously, and schedule one or both of the transfers to happen every [x] minutes, otherwise the transactions will be replicated non-stop until the logs are full.

But the best solution for two-way replication would not be to bother at all with SQL Server transactional replication, at least for 2000. You could try merge replication, or set up a web service to allow data only to be written to a master db and replicated back to the child databases. Two-way replication is a bad idea.

May 26, 2010

.Net based HTTP Client in ColdFusion?!

Filed under: .net, ColdFusion — joel.cass @ 5:12 pm

I’ve been banging my head up against the metaphorical walls around here for ages trying to get ColdFusion to access websites via a proxy server that only supports NTLM authentication.

Short answer: don’t bother. CFHTTP does not support NTLM Authentication. Most of the Java libraries claiming to do so are hopeless. Support is inconsistent because no-one knows anything about the standard. Except Microsoft.

So, it only came naturally that the best way to solve the issue would be to use .net – and now that ColdFusion has a gateway to .net components, I could actually write something that solves the problem!

So, what I have done is written a wrapper that can be accessed by ColdFusion, and a simple custom tag to finish it off.

Some more information regarding download and implementation is in the Projects section.

May 13, 2010

StructureCMS 1.4 Released

Filed under: StructureCMS — joel.cass @ 10:50 am

Recently I have received (finally) a little bit of feedback regarding StructureCMS. Mainly that the administration interface is, well… Ugly. SO I spent a little time sprucing it up and clearing out the cobwebs. Here are some screenshots:

I have also made the following modifications:
- Remove flash-based image uploader (mainly because I was having trouble using it via proxy)
- Added a logo to all templates
- Checked administration system functionality across all major browsers.

Project home | Download File

May 12, 2010

Good, Fast, or Cheap. Choose Two.

Filed under: Musings, Programming — joel.cass @ 4:53 pm

How true is the above statement, really? With the rise of open source products one would think that it’s possible to find a product that ticks all of the above products, and is free to boot.

I found this out recently when I tried using SQL server to load in some website log data so I could generate some reports. Geez, it was slow. It ran at about 45 records per second. Inserting about 64 million records could take a loooooong time. On the other hand, I remembered the faithful MySQL server that I had used a while ago. Loading data into MySQL was fast – about 600 records/sec fast. And it’s free. But is it good?

When actually getting around to running the reports, I was finding that MySQL was falling short. Due to its architecture, sorting operations had to be done by writing a temporary table to disk. This could be worked around by using indexes, however often the indexes would not be picked up, plus the reconfiguration of an index over 64 million records can take 2-3 hours, thus slowing things down.

So either way it was a headache. I know that SQL server is very efficient at sorting and searching records and has its own optimised low-level methods for searching data. If you’ve ever tried to run a database from a compressed drive / folder you would know that certain operations do not work because SQL server accesses data at such a low level. And I think it’s that sort of optimisation that you would happily pay for. Whilst it may be slow on the insert side, it’s fast on the searching side.

So, “Good, Fast, or Cheap. Choose Two.” – I think the saying has real meaning here. An open source product may have the commitment of a small group of developers or no commitment at all. A paid product has a real business motive to keep developers “on the ball”, continually optimising and improving the product.

Older Posts »