StructureCMS

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.

April 20, 2010

Google Maps – Create your own store locator

Filed under: AJAX, Web Development — joel.cass @ 12:43 pm

Recently I have had to create a store locator for a website I have been working on. On the outset, it sounds easy. Simply add a number of points to the map and calculate the distance between a point of reference and the points you have added.

Then the question comes up: How do you calculate distance? As the crow flies, or by road directions?

To test this out, I created a Google Maps Distance Calculator.

Road directions work, and are not too difficult to get from the API. However, they are dead slow and require callbacks. Sometimes a request may take up to 30 seconds, which means that it could take ages to render the distance between 10 or so points. Furthermore, sometimes the distance cannot be calculated at all.

So the more efficent and reliable way is to draw a line and measure it. That’s much easier and does not require a callback method:

var polyline = new GPolyline([
		point1.getLatLng(), // starting point
		point2.getLatLng() // finishing point
	],
	"#0000FF", // color
	1 // opacity
);
numDistance = polyline.getLength(); // in metres

I could go on about the intricacies of storing and sorting an array of points, but I won’t. Instead I have created a script that can do this for you. First off, you will need to get a google maps key. This is easy, you can do it here. Then, you need to call the google maps V2 API as follows:

<head>
...
<script type="text/javascript" src="http://www.google.com/jsapi?key=__________________KEY_________________"></script>
<script type="text/javascript">
	// initialise google maps
	google.load("maps", "2");
</script>
</head>
....

You will need to add some divs for containing the map and listing areas. By default, the map element is assumed to be a <div> tag and the listing container is a <table> tag.

<body>
...
	<div id="map"></div>
	<table id="table"></table>
...
</body>

(We will get into how you can change the listing container later)

Then, you will need to include the pointSorter.js script as follows:

<head>
...
</script>
<script type="text/javascript" src="pointSorter.js"></script>
<script type="text/javascript">
	// set target divs
	pointSorter.setDiv("map");
	pointSorter.setListingContainer("table");

	// load maps, point sorter etc
	google.setOnLoadCallback(function () {
		// initialise point sorter
		pointSorter.init();

		// add points
		...
	});
</script>
...
</head>

You may have noticed that I have not entered anything below “// add points” in the block above. There are two ways to add points:

// add point by coordinate
pointSorter.addPoint(
	new GLatLng(37.423156,-122.084917),
	"Google Inc.","<strong>Google Inc. Head Office</strong>"
);

// add point by address
pointSorter.addPointByAddress(
	"701 First Avenue, Sunnyvale, California",
	"<strong>Yahoo! Head Office</strong>"
);

Both methods take similar arguments. The first takes a GLatLng (point) object + a description, whilst the second takes an address and a description. The first method is much faster than the second as no request has to be made to the server to look up an address.

By now (after some style tweaking), your code may be looking something like this:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">

<html>

<head>
	<title>Maps Test</title>
	<style type="text/css">
		body { font-family:sans-serif; font-size:0.8em; }
		/* ID Selectors */
		#map {
			width:70%;
			height:500px;
			float:left;
		}
		#table {
			margin-top:1em;
			width:30%;
			height:500px;
			overflow-y:auto;
			float:right;
		}
		/* classes */
		.info { color:#008000; }
	</style>
	<script type="text/javascript" src="http://www.google.com/jsapi?key=_____________KEY____________"></script>
	<script type="text/javascript" src="pointSorter.js"></script>
	<script type="text/javascript">
	// initialise google maps
	google.load("maps", "2");

	// set target divs
	pointSorter.setDiv("map");
	pointSorter.setListingContainer("table");

	// load maps, point sorter etc
	google.setOnLoadCallback(function () {
		// initialise point sorter
		pointSorter.init();

		// add point by coordinate
		pointSorter.addPoint(
			new GLatLng(37.423156,-122.084917),
			"Google Inc.","<strong>Google Inc. Head Office</strong>"
		);

		// add point by address
		pointSorter.addPointByAddress(
			"701 First Avenue, Sunnyvale, California",
			"<strong>Yahoo! Head Office</strong>"
		);

	});
	</script>
</head>

<body>

<div id="map"></div>
<table id="table"></table>

</body>
</html>

If you run this in your browser, you should see a map appear with two locations shown. Clicking on either will reveal the name of the location.

Now the only thing left to do is allow users to enter in an address. This is as simple as adding the method and form below. When the form is “submitted”, a search is made for the address.

<script type="text/javascript">
	...
	function setLocation (location) {
		// set point of reference
		pointSorter.setPointOfReference(
			location,
			"<strong>Reference Location</strong><p>"+location+"</p>"
		);
	}
	</script>
</head>

<body>

<form action="" onsubmit="setLocation(this.address.value);return false;">
	<input type="text" name="address" />
	<input type="submit" value="Set Reference Location" />
</form>
...
</html>

When the form is submitted, a new point is added to the map and the distances are shown.

Here are some other tips about the script.

You can set icons for the map pointers by using the following methods:

// set icon for reference location
pointSorter.setHomeIcon("http://www.google.com/mapfiles/dd-start.png", 20, 34);
// set icon for surrounding point locations
pointSorter.setPointerIcon("http://www.google.com/mapfiles/dd-end.png", 20, 34);

If you have a lot of locations, you can limit the display to only list locations within a certain distance from the reference point, e.g. 100km:

// distance in kilometres
pointSorter.setDistanceThreshold(100)

if you are not going to use a table, you need to set the item template for listing items. The example below allows your items to be listed inside an unordered list (<ul>) tag:

pointSorter.setItemTemplate(
	"
  • " + " {description} ({distance_direct}km)" + "
  • " );

    February 25, 2010

    Rediscovering MySQL

    Filed under: ColdFusion, Database — joel.cass @ 9:14 am

    Recently I have had to create a reporting system for some server log files. It was sort of an ad-hoc thing; It was really done in a rush because we couldn’t get AWStats to process the files properly and the customer had a really steep deadline to meet.

    Anyway, I started off by creating a system that can read in log files following a regular expression. It would then add the contents of these logs to the database for reporting purposes. The code I used is below:

    <cfsetting enablecfoutputonly="true" requesttimeout="864000" />
    
    <cfparam name="logpath" default="#expandPath('logs/')#" />
    <cfparam name="filter" default="^nc([0-9]*)\.log$" />
    
    <cfdirectory action="list" directory="#logpath#" name="files" sort="name DESC" />
    
    <cfset regex = "^([^ ]*) ([^ ]*) ([^ ]*) (\[[^\]]*\]) ""([^ ]*) (.*) ([^ ]*)"" ([^ ]*) ([^ ]*)$" />
    <cfset date_regex = "^\[([0-9]*)/([A-Za-z]*)/([0-9]*)\:([0-9]*)\:([0-9]*)\:([0-9]*).*\]$" />
    
    <cfset month_map = structNew() />
    <cfset month_map["Jan"] = "01" />
    <cfset month_map["Feb"] = "02" />
    <cfset month_map["Mar"] = "03" />
    <cfset month_map["Apr"] = "04" />
    <cfset month_map["May"] = "05" />
    <cfset month_map["Jun"] = "06" />
    <cfset month_map["Jul"] = "07" />
    <cfset month_map["Aug"] = "08" />
    <cfset month_map["Sep"] = "09" />
    <cfset month_map["Oct"] = "10" />
    <cfset month_map["Nov"] = "11" />
    <cfset month_map["Dec"] = "12" />
    
    <cfif structKeyExists(URL, "resetDB")>
      <cfquery datasource="#application.dsn#">
      DROP TABLE #application.tablename#;
      CREATE TABLE #application.tablename#(
        [id] [bigint] NOT NULL,
        [ip] [nvarchar](50) NULL,
        [datetime] [datetime] NULL,
        [url] [ntext] NULL,
        [url_hash] [nvarchar](50) NULL,
        [method] [nvarchar](50) NULL,
        [status] [nvarchar](50) NULL,
        [size] [nvarchar](50) NULL,
        [unknown1] [nvarchar](50) NULL,
        [unknown2] [nvarchar](50) NULL,
      CONSTRAINT [PK_#application.tablename#] PRIMARY KEY CLUSTERED (
        [id] ASC
      ) WITH (
        PAD_INDEX = OFF,
        STATISTICS_NORECOMPUTE = OFF,
        IGNORE_DUP_KEY = OFF,
        ALLOW_ROW_LOCKS = ON,
        ALLOW_PAGE_LOCKS = ON
      ) ON [PRIMARY]
      ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY];
    
      CREATE NONCLUSTERED INDEX [idx_#application.tablename#_date] ON [dbo].[#application.tablename#] (
        [datetime] ASC
      ) WITH (
        PAD_INDEX = OFF,
        STATISTICS_NORECOMPUTE = OFF,
        SORT_IN_TEMPDB = OFF,
        IGNORE_DUP_KEY = OFF,
        DROP_EXISTING = OFF,
        ONLINE = OFF,
        ALLOW_ROW_LOCKS = ON,
        ALLOW_PAGE_LOCKS = ON
      ) ON [PRIMARY];
    
      CREATE NONCLUSTERED INDEX [idx_#application.tablename#_ip] ON [dbo].[#application.tablename#] (
        [ip] ASC
      ) WITH (
        PAD_INDEX = OFF,
        STATISTICS_NORECOMPUTE = OFF,
        SORT_IN_TEMPDB = OFF,
        IGNORE_DUP_KEY = OFF,
        DROP_EXISTING = OFF,
        ONLINE = OFF,
        ALLOW_ROW_LOCKS = ON,
        ALLOW_PAGE_LOCKS  = ON
      ) ON [PRIMARY];
    
      CREATE NONCLUSTERED INDEX [idx_#application.tablename#_url] ON [dbo].[#application.tablename#] (
        [url_hash] ASC
      ) WITH (
        PAD_INDEX  = OFF,
        STATISTICS_NORECOMPUTE  = OFF,
        SORT_IN_TEMPDB = OFF,
        IGNORE_DUP_KEY = OFF,
        DROP_EXISTING = OFF,
        ONLINE = OFF,
        ALLOW_ROW_LOCKS  = ON,
        ALLOW_PAGE_LOCKS  = ON
      ) ON [PRIMARY];
      </cfquery>
    </cfif>
    
    <cfloop query="files">
    
      <cfif REFindNoCase(filter, files.name)>
    
          <cfset i = 0 />
          <cfset strFile = files.name />
          <cfset strFileId = REReplace(strFile, filter, "\1") />
          <cfset objSystem = CreateObject("java", "java.lang.System") />
          <cfset getMaxId = "" />
          <cfset start_i = 0 />
    
          <cflock name="GenerateStats_#application.sitename#_#strFileId#" type="exclusive" timeout="10">
    
            <cflog file="GenerateStats_#application.sitename#" text="#strFile# Started" />
    
            <cfquery name="getMaxId" datasource="#application.dsn#">
            SELECT
              max(id) as n
            FROM
              #application.tablename#
            WHERE
              id >= <cfqueryparam cfsqltype="CF_SQL_BIGINT" value="1#strFileId##numberFormat(0,'0000000000')#">
            AND
              id <= <cfqueryparam cfsqltype="CF_SQL_BIGINT" value="1#strFileId##numberFormat(9999999999,'0000000000')#">
            </cfquery>
    
            <cfif getMaxId.recordcount GT 0 AND getMaxId.n GT 0>
              <cfset start_i = getMaxId.n - "1#strFileId#0000000000" />
            </cfif>
    
            <cflog file="GenerateStats_#application.sitename#" text="#strFile# Log start = #start_i#" />
    
            <cfloop file="#logpath#\#strFile#" index="line">
              <cfset i = i + 1 />
    
              <cfif i GT start_i>
    
                <cfset strId = "1#strFileId##numberFormat(i,'0000000000')#" />
    
                <cftry>
    
                  <cfset strIp       = REReplaceNoCase(line, regex, "\1") />
                  <cfset strUnknown1 = REReplaceNoCase(line, regex, "\2") />
                  <cfset strUnknown2 = REReplaceNoCase(line, regex, "\3") />
                  <cfset strDatetime = REReplaceNoCase(line, regex, "\4") />
                  <cfset strUrl      = REReplaceNoCase(line, regex, "\6") />
                  <cfset strMethod   = REReplaceNoCase(line, regex, "\5") />
                  <cfset strStatus   = REReplaceNoCase(line, regex, "\8") />
                  <cfset strSize     = REReplaceNoCase(line, regex, "\9") />
    
                  <cfset dtDateTime = CreateDateTime(
                    REReplaceNoCase(strDatetime, date_regex, "\3"),
                    month_map[REReplaceNoCase(strDatetime, date_regex, "\2")],
                    REReplaceNoCase(strDatetime, date_regex, "\1"),
                    REReplaceNoCase(strDatetime, date_regex, "\4"),
                    REReplaceNoCase(strDatetime, date_regex, "\5"),
                    REReplaceNoCase(strDatetime, date_regex, "\6")
                  ) />
    
                  <cfquery datasource="#application.dsn#">
                  INSERT INTO #application.tablename# (
                    id,
                    ip,
                    unknown1,
                    unknown2,
                    datetime,
                    url,
                    url_hash,
                    method,
                    status,
                    size
                  ) VALUES (
                    <cfqueryparam cfsqltype="CF_SQL_BIGINT" value="#strId#" />,
                    <cfqueryparam cfsqltype="CF_SQL_VARCHAR" value="#strIp#" />,
                    <cfqueryparam cfsqltype="CF_SQL_VARCHAR" value="#strUnknown1#" />,
                    <cfqueryparam cfsqltype="CF_SQL_VARCHAR" value="#strUnknown2#" />,
                    <cfqueryparam cfsqltype="CF_SQL_TIMESTAMP" value="#dtDateTime#" />,
                    <cfqueryparam cfsqltype="CF_SQL_VARCHAR" value="#strUrl#" />,
                    <cfqueryparam cfsqltype="CF_SQL_VARCHAR" value="#hash(strUrl)#" />,
                    <cfqueryparam cfsqltype="CF_SQL_VARCHAR" value="#strMethod#" />,
                    <cfqueryparam cfsqltype="CF_SQL_VARCHAR" value="#strStatus#" />,
                    <cfqueryparam cfsqltype="CF_SQL_INTEGER" value="#strSize#" />
                  )
                  </cfquery>
    
                  <cfcatch>
                    <cflog file="GenerateStats_#application.sitename#" text="Error on #strFile# line #i#: #cfcatch.message# (#cfcatch.detail#)" />
                  </cfcatch>
    
                </cftry>
    
              </cfif>
    
              <cfif i MOD 500 EQ 0>
                <cfif fileexists(expandPath("stop.file"))>
                  <cflog file="GenerateStats_#application.sitename#" text="#strFile# Aborted: Stop file exists." />
                  <cfbreak />
                </cfif>
                <cfset objSystem.GC() />
                <cfthread action="sleep" duration="100" />
              </cfif>
    
              <cfif i MOD 10000 EQ 0>
                <cflog file="GenerateStats_#application.sitename#" text="#strFile# Processed #numberformat(i)# lines" />
              </cfif>
    
            </cfloop>
    
            <cflog file="GenerateStats_#application.sitename#" text="#strFile# Completed (#i# lines)" />
    
          </cflock>
    
      </cfif>
    
      <cfif fileexists(expandPath("stop.file"))>
        <cfbreak />
      </cfif>
    
    </cfloop>
    
    <cfsetting enablecfoutputonly="false" />

    This was going OK with SQL Server Express. The logs would be imported and then analysed. Whilst it wasn’t blazingly fast, it could import a few hundred meg of logs data in a few minutes so I could start generating reports.

    Then one day I got the request to do the same thing but for the company’s most busiest site – their intranet. Each log file exceeded a gig, which often meant over 7 million records per file. Unfortunately, SQL Server Express was not keeping up all too well, it would start off well, processing around 500 records a second, but as time went by this crawled down to <10 records per second, PLUS, after an overnight run I realised that SQL Server stopped accepting records, with the data file blown out to 4GB at just 3 million records. That's only HALF a log file!

    I started looking at my options. The company I work at loves SQL Server, so I thought of ways to make it work. Hmm. Storing less fields? Sacrificing my indexes? Those are not good options. Then, I thought back to my old days of using MySQL. I remember how spastic it used to be, joins were always slow, queries had to be arranged so that they would be optimised in a certain way etc etc. But then, my app is only using one table. My queries are basic (for the most part). It was worth a try.

    So, I started again. I created the table in InnoDB. That was a bad move, it was slow from the start at only 40 records per sec. But then, I'm the only one using this app. I don't need transactions, why am I using InnoDB? The answer was clear - try MyISAM. So I dropped and recreated the table as MyISAM and restarted the import. Wow - instantly the result was clear - over 1000 records/sec!

    Here is the equivalent MySQL code:

    	<cfquery datasource=”#application.dsn#”>
    	DROP TABLE #application.tablename#;
    	</cfquery>
    	<cfquery datasource=”#application.dsn#”>
    	CREATE TABLE #application.tablename# (
    	  id BIGINT UNSIGNED NOT NULL,
    	  ip VARCHAR(50) NULL,
    	  datetime DATETIME NULL,
    	  url TEXT NULL,
    	  url_hash VARCHAR(50) NULL,
    	  method VARCHAR(50) NULL,
    	  status VARCHAR(50) NULL,
    	  size VARCHAR(50) NULL,
    	  unknown1 VARCHAR(50) NULL,
    	  unknown2 VARCHAR(50) NULL,
    	  PRIMARY KEY (id),
    	  INDEX idx_#application.tablename#_date(datetime),
    	  INDEX idx_#application.tablename#_ip(ip),
    	  INDEX idx_#application.tablename#_url(url_hash)
    	) ENGINE=MyISAM;
    	</cfquery>

    The import has now been running overnight and there is no performance degradation. Imports are still running between 800-1000 records/sec and the table now contains 48 million records! And the file size? 10GB including indexes - this is only 2 and a half times the SQL Server file yet it is holding 16 times the amount of data! AND I have just started running queries - queries that took over 60 seconds to execute in SQL server on less than a million rows are now taking a similar time but I have over 48 million records in MySQL.

    I think this is a perfect case of the right tool for the right purpose. MySQL - you're a life saver.

    February 19, 2010

    Another way to stop wordpress spammers

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

    It seems that getting a good ranking on a search engine can be a double-edged sword. Whilst it helps people find you, it also helps nasty spammers to find a way into your site so they can post their comments. Sure, wordpress does have some spam filtering ability thanks to the akismet plugin, but it would be so mauch better if they can be stopped at the source.

    One way I have stopped spammers on my site is by implementing a text field inside a hidden block, as follows:

    <!-- would recommend that this actually goes into a CSS file -->
    <style type="text/css">
    	.spam-check { display:none; }
    </style>
    ..........................
    <!-- spam detection -->
    <p class="spam-check">
    	<input type="text" name="spamcheck" id="fldSpamCheck" value="">
    	<label for="fldSpamCheck">Please leave this field blank</label>
    </p>
    <!-- /spam detection -->
    

    ..and then on the processing side I would do something as follows:

    // jnet spam detection
    if (!isset($_POST['spamcheck']) || $_POST['spamcheck'] != "") {
    	die('Error: please do not fill in the field that tells you not to fill it in.');
    }
    // end jnet spam detection
    

    The beauty of it is that there are no CAPTCHA’s involved, and no thinking on the user’s side. The common trap that these spambots fall into is that they fill out all the fields in the form with useless garbage. Because this field is meant to be blank, the submission fails.

    Furthermore, any user with a CSS enabled browser does not see the field. A user who can see the field can tell from the label that it is not meant to be filled in. So everyone wins! (Except the nasty spammers).

    In wordpress, you will need to add the first code block to the files:

    • /wp-content/themes/[your_theme]/comments.php
    • /wp-content/themes/[your_theme]/comments-popup.php

    …and you will need to add the second code block to the following file:

    • /wp-comments-post.php

    …and you’ll be done! At least until the spammers figure out how to get around it.

    February 2, 2010

    Enable AJAX on almost any site using jQuery

    Filed under: AJAX — joel.cass @ 11:47 am

    A website I was working on a while back had a requirement for an animated background to be running whilst users were on the website. The main problem was, that every time a user would click on a new page, the background would restart, interrupting the user experience and slowing things down in general. So what do we do? Inline frames? Create an AJAX service that returns the site content?

    No, the answer is even simpler. Enter AJAX using jQuery. All you need is to have two div elements with identifiers surrounding the main content of your page, for example:

    Example page structure

    Example page structure

    If you have the above structure and have the jQuery libraries loaded, then all you need to do is implement the following function:

    function loadPage(URL) {
        $("#content-outer").load(URL + " #content", null, ajax_loaded);
        return false;
    }

    JQuery uses css selectors to identify elements, e.g. <div id=”content”> = #content, so what this function does is it looks up the element #content-outer, executes the AJAX request, and then looks up the #content element from the next page and replaces the contents of the #content-outer element with the new element, as demonstrated in the below diagram.

    How requests are made using jQuery

    How requests are made using jQuery

    I have also added the following routine that will be called on startup. It effectively adds the loadPage(URL) method to each link’s onclick method. Because loadPage(URL) returns false, when the link is clicked, the AJAX call will be initialised and the user’s action will be cancelled.

    function check_on_load() {
        // update links
        updateLinks("A");
    }
    
    $(document).ready(check_on_load);
    
    function updateLinks(DOMLocation) {
        var links = $(DOMLocation);
        var baseURL = location.protocol + "//" + location.host + "/";
        for (var i = 0; i < links.length; i++) {
            var el = links[i]
            /* exclusions to the rule
             * - onclick must be null,
             * - url must contain the site's base url (http://site_domain)
             * - url cannot already have an anchor
             * - target must be empty
             * - classname must not contain 'no-ajax'
            */
            if (el.onclick == null &&
                    el.href.indexOf(baseURL) == 0 &&
                    el.href.indexOf("#") < 0 &&
                    el.target == "" &&
                    el.className.toLowerCase().indexOf("no-ajax") < 0) {
                el.onclick = Function("return loadPage(this.href);");
            }
        }
    }

    Furthermore, I have also added a callback to the method which will update the page title and replace any links within the new page content.

    function ajax_loaded(responseText, textStatus, XMLHttpRequest) {
        if (textStatus == "error") {
            prompt("URL Failed: ", XMLHttpRequest);
        } else {
            // set title
            document.title = titleFromHTML(responseText);
            // update links
            updateLinks("#content-outer A");
        }
    }
    
    function titleFromHTML(HTML) {
        var regex = new RegExp("");
        var matches = regex.exec(HTML);
        if (matches.length > 1) {
            return matches[1];
        } else {
            return "";
        }
    }

    ...And the best part? By simply including the script within your page along with jQuery (and making sure that your structure is correct), you will instantly enable AJAX-based loading on your page. Even better, if your browser does not fully support AJAX, javascript, or jQuery, all links will remain untouched and function just as they normally do.

    Download jQuery here: http://docs.jquery.com/Downloading_jQuery
    Download the script here: ajax-plugin_no_history.js
    Demo: demo_no_history.htm

    The only thing I have left out of this is history management. By using the script above, users lose the ability to switch back and forth between pages. By using the jQuery history plugin, you can give users the ability to switch back and forth between pages, and also the ability to bookmark AJAX'd pages.

    Download jQuery history plugin here: http://www.mikage.to/jquery/jquery_history.html
    Download the history enabled script here: ajax-plugin_with_history.js
    Demo: demo_with_history.htm

    December 18, 2009

    Bypass Friendly Internet Explorer 404 Messages

    Filed under: ColdFusion, PHP — joel.cass @ 9:22 am

    I had to build a custom 404 page today and was bashing my head against a wall regarding Internet Explorer’s friendly “The webpage cannot be found” error message page.

    Well, according to this link, the solution is simple – the page has to be at least 512 bytes. Too easy.

    Of course, you can simply just add 512 bytes of whitespace and/or reduce the amount of whitespace to 512 minus the original size of the page. If your page is greater than 512 bytes you have nothing to worry about.

    The ColdFusion repeatString and PHP str_repeat methods would be useful for this.

    December 11, 2009

    Automatic image resizing in PHP

    Filed under: PHP, StructureCMS — joel.cass @ 9:54 am

    One big issue in content management systems is that images are usually handled poorly. Even with the best file management capabilities, you can’t help users from uploading their 12 megapixel 15 megabyte images from their recent Christmas party and then chucking them into the content and resizing them down to fit the screen. The problem is that even though the image appears ’small’ in the browser, the full size image is downloaded and displayed, wasting bandwidth and slowing down the user experience.

    A concept I really admired in Sitecore and have copied to StructureCMS is that resized images could automatically be created on the server-side and returned to the client simply by adding the parameters ‘w’ or ‘h’ to the URL, e.g. take these images for example:

    http://www.jozza.net/blog/wp-content/uploads/2009/12/test120×120.gif
    test120x120

    The attributes “w” and “h” can be added to the url to resize the image:

    http://www.jozza.net/blog/wp-content/uploads/2009/12/test120×120.gif?w=60
    test120x120

    http://www.jozza.net/blog/wp-content/uploads/2009/12/test120×120.gif?h=200
    test120x120, h=200

    (if both parameters are defined, image is resized to ‘fit’ within the dimensions)

    If you look at the images on their own, you will notice that they are resized by the server, and you can change the dimensions. The crunched down, resized version is sent back to the user, saving bandwidth and improving the user experience. Images are then cached so that future requests do not require any server resources.

    How did I do this?

    1. I created image.php in the website root – this gets URL params and creates a new resized image (I’m not going to explain the code – it’s pretty simple)
    2. I installed the apache mod_rewrite module – this is as simple as opening your httpd.conf file and unhashing the line ‘LoadModule rewrite_module modules/mod_rewrite.so’ (and then restarting apache)
    3. I created an .htaccess file that rewrites URL’s
    4. I then modified the tinyMCE image.js file to add the size attributes to the URL

    The beauty of this modification is that it won’t break anything if apache nor mod_rewrite are installed. And, with some modification this code could be used on any website.

    December 2, 2009

    Measuring disk space in Coldfusion

    Filed under: ColdFusion — joel.cass @ 2:15 pm

    If you’re supporting any legacy applications, then it’s probably only a matter of time until you will get that call early in the morning… “Hi, this is technical support, your server’s down. It seems to have run out of space on drive X…”.

    This could have easily been avoided if you had set up some sort of alert. The should be easy right, as easy as using a cfdirectory tag… Oh wait, hold on, other than a recursive listing and summary being extremely inefficient, cfdirectory doesn’t tell you how much space is left on your drive.

    Well, we’re all lucky that ColdFusion is built on top of Java. As it turns out, since JDK 6.0 Java has exposed drive information via the java.io.File library.

    For example, if you wanted to get a list of all drives on the server, you could use the method File.listRoots():

    <cfset objFile = createObject("java","java.io.File")>
    <cfset aryRoots = objFile.listRoots()>
    
    <cfset lstDrives = "">
    
    <cfloop from="1" to="#arrayLen(aryRoots)#" index="i">
        <cfset lstDrives = listAppend(lstDrives, aryRoots[i].getPath())>
    </cfloop>
    

    Dumping out #lstDrives# would return something like “A:\,C:\,D:\” etc.

    From that, you can then get the amount of space available:

    <cfset objFile = createObject("java","java.io.File").init("C:\")>
    
    <cfset stcReturn = structNew()>
    <cfset stcReturn.freeSpace = objFile.getFreeSpace()>
    <cfset stcReturn.totalSpace = objFile.getTotalSpace()>
    <cfset stcReturn.readAccess = objFile.canRead()>
    <cfset stcReturn.writeAccess = objFile.canWrite()>
    

    If you wanted to set up an alert, you could use the readAccess / writeAccess flags to determine whether the drive should be checked. Chances are that if a drive cannot be read or written it is most probably a CD drive or an external media drive that is empty.

    November 30, 2009

    StructureCMS 1.3 Released.

    Filed under: StructureCMS, Web Development — joel.cass @ 3:43 pm

    Well, it’s been a while in the making, but the third point-release of StructureCMS has been released.

    What’s new?

    • Password Encryption
    • Installer Script
    • Latest TinyMCE
    • Integrated SWFUpload functionality with TinyMCE
    • Site settings node
    • Themes, including a suckerfish interface and a new lo-fi “classic” theme currently used on jozza.net
    • Admin display tweaks (logout link, tweaked buttons, menus etc)

    Give it a shot and let me know what you think!

    Project Home Page: http://code.google.com/p/structure-cms/

    Direct Download: http://structure-cms.googlecode.com/files/structurecms-1.3.zip

    « Newer PostsOlder Posts »