The Information Technology Blog brought to you by 4 Ace Technologies

Sunday, May 31, 2009

How to Make AJAX work Fast

Making Ajax Elegantly Fast

Sunday, September 30th, 2007

Sometime ago I was messing around with various techniques for ways that we can optimize asynchronous calls to the server via XMLHttpRequest (and the ActiveX equivalent of XMLHTTP). I wrote about one of those techniques known as branching in an article for Digital Web Magazine called Seven JavaScript Techniques You Should Be Using Today back in April, however, there was a flaw in the function that when one request is made before another one is finished, you would run into an error since it is using the same reference to the instance object for the request. That’s cool in theory, but bad in practice. The benefit you get with that is that you never create more than one XMLHttpRequest object for all your requests. This is fine for light weight use, and when you know for sure that you won’t be making frequent requests simultaneously. But when does that ever happen? Really.

Here’s what that code looked like:

Outdated asyncRequest function

var asyncRequest = function() {
function handleReadyState(o, callback) {
var poll = window.setInterval(function() {
if(o && o.readyState == 4) {
window.clearInterval(poll);
if ( callback ){
callback(o);
}
}
},
50);
}
var http;
try {
http = new XMLHttpRequest;
}
catch(e) {
var msxml = [
'MSXML2.XMLHTTP.3.0',
'MSXML2.XMLHTTP',
'Microsoft.XMLHTTP'
];
for ( var i=0, len = msxml.length; i < len; ++i ) {
try {
http = new ActiveXObject(msxml[i]);
break;
}
catch(e) {}
}
}
return function(method, uri, callback, postData) {
http.open(method, uri, true);
handleReadyState(http, callback);
http.send(postData || null);
return http;
};
}();

Note that the function still branches within the body of the closure, and that branch happens only once. The main issue here is that we need another function that essentially branches the way get the XMLHttpRequest object, but without being run multiple times. In other words, this would be bad:

bad example for XMLHttpRequest

function getXHR() {
var http;
try {
http = new XMLHttpRequest;
} catch (ex) {
var msxml = [
'MSXML2.XMLHTTP.3.0',
'MSXML2.XMLHTTP',
'Microsoft.XMLHTTP'
];
for (var i=0, len = msxml.length; i < len; ++i) {
try {
http = new ActiveXObject(msxml[i]);
break;
}
catch(e) {}
}
}
}

The reason this is slow is because every request for a new XMLHttpRequest object we have to run through the same logic upon each call to this function. Instead we can use what Peter Michaux coined as the Lazy Function Definition Pattern, and apply that technique here. Our optimized (and final) code would then look like this:

Optmized Speedy Ajax Code

var asyncRequest = function() {
function handleReadyState(o, callback) {
if (o && o.readyState == 4 && o.status == 200) {
if (callback) {
callback(o);
}
}
}
var getXHR = function() {
var http;
try {
http = new XMLHttpRequest;
getXHR = function() {
return new XMLHttpRequest;
};
}
catch(e) {
var msxml = [
'MSXML2.XMLHTTP.3.0',
'MSXML2.XMLHTTP',
'Microsoft.XMLHTTP'
];
for (var i=0, len = msxml.length; i < len; ++i) {
try {
http = new ActiveXObject(msxml[i]);
getXHR = function() {
return new ActiveXObject(msxml[i]);
};
break;
}
catch(e) {}
}
}
return http;
};
return function(method, uri, callback, postData) {
var http = getXHR();
http.open(method, uri, true);
handleReadyState(http, callback);
http.send(postData || null);
return http;
};
}();

Take closer look at how the getXHR function is redeclared within main declaration to simply return the appropriate object back to the client when requested. And that folks, is Ajax served elegantly fast! Feel free to try it yourself :)

asyncRequest in use

asyncRequest('GET', 'foo.php', function(o) {
console.log(o.responseText);
});

51 Responses to “Making Ajax Elegantly Fast”

  1. Calm_Pear

    Very interesting! Thanks for sharing that!

  2. Mourner

    Dustin, thanks for a great tip!
    It seems that you forgot to make a poll in handleReadyState definition in your updated code, as it would be called only once which is incorrect.

  3. Bhabishya Kumar


    I really liked your technique and so wanted to check if the popular JavaScript libraries are also using it. I just looked at jQuery1.1.4 and it just had a one-liner.



    var xml = window.ActiveXObject ? new ActiveXObject("Microsoft.XMLHTTP") : new XMLHttpRequest();


    Any problem with this?

  4. Graham Bradley

    I noticed that problem could occur with the example you gave, but never really thought it through properly to come up with an alternative. Really nice solution :)

  5. Dustin Diaz

    @Mourner: I removed the poll because that was an outdated piece of code that assumed IE6 leaked memory. Since then IE has fixed that.

    @Bhabishya: Yes, there is… that piece of logic is run upon every request. And not only that, it doesn’t take advantage of IE’s newer methods for XMLHTTP such as MSXML2 and XMLHTTP.3.0.

    @Graham: Yeah. It doesn’t seem to bother a lot of people. But working at Google has tweaked my mind to always think about speed and optimization techniques, hence this blog post :

  6. daniel

    @Graham: Yeah. It doesn’t seem to bother a lot of people. But working at Google has tweaked my mind to always think about speed and optimization techniques, hence this blog post

    yes, thats true
    changing workplaces often changes the thinking to

    ps: nice to read something new on your blog :)

  7. napyfab:blog» Blog Archive » links for 2007-09-30

    [...] Making Ajax Elegantly Fast (tags: ajax javascript fast web development webdev) This entry was written by delicious and posted on 1 Ottobre 2007 at 01:25 and filed under Del.icio.us. Bookmark the permalink. Follow any comments here with the RSS feed for this post. Post a comment or leave a trackback: Trackback URL. [...]

  8. Andrew Mattie

    Hey Dustin, thanks for the informative article. I’m left with three questions though that I hope you’ll be able to shed some light on.

    First, not that it’s a big deal, but what’s the point of the ‘return http;’ line in your original getXHR function? Since you are redefining the function anyway, it doesn’t seem like it would serve a purpose. However, since that wasn’t in your original writeup, I’m assuming you must have added it for a reason.

    Second, why don’t you test for XMLHttpRequest using a !! flag instead of a try / catch block? While the ‘catch’ block for the way you’re doing it would only get hit once in IE6, that’s once more than it needs to be hit it seems. The performance gain would probably be just a few milliseconds or so, and it’s maybe not even worth discussing, but I figured I’d ask anyway.

    Finally, is there a reason you chose to use Michaux’s pattern when you rewrote this instead of your original pattern based on function closures (not sure what else to call it really). For example, is there a reason you did this


    var asyncRequest = function() {
    ...
    var getXHR = function() {
    ...
    if (!!XMLHttpRequest)
    getXHR = function() { return new XMLHttpRequest(); }
    ...
    }
    ...
    }

    instead of this


    var asyncRequest = function() {
    ...
    var getXHR = function() {
    ...
    if (!!XMLHttpRequest)
    return function() { return new XMLHttpRequest(); }
    ...
    }();
    ...
    }

    ? As some who wrote a book on the subject of JavaScript patterns, I’m especially curious to hear more about this one from you.

    I’m honestly not trying to knock your example, nit-pick at things, or say anything you did was wrong. I’m just really curious to learn more about why you did what you did.

  9. Dustin Diaz

    @Andrew:

    1) http is returned so you always have the object incase you want to abort or grab other properties. In other words… you can do this…

    var requestObject = asyncRequest('GET', 'foo.php', function(o) { });
    ...
    requestObject.abort();

    2) and 3) Actually, I don’t know why I didn’t use my usual closure technique (which is always my favorite). We can take it a step further and use a ternary operator to decide which function is assigned to getXHR…

    var getXHR = function() {
    return !!XMLHttpRequest ?
    function() {
    return function() { return new XMLHttpRequest; }
    } :
    (function() {
    var msxml = [ ... ];
    for ( ... ) {
    try {
    ...
    } catch (ex) { }
    })();
    }();
  10. js fan

    Something I do not understand.
    1. You remove the poll in the handleReadyState function, and you say it may cause the ie to leak memory, but without the poll the asyncRequest can not get any response, have you test it by yourself this function?
    2. Code problem:


    var msxml = [
    ‘MSXML2.XMLHTTP.3.0′,
    ‘MSXML2.XMLHTTP’,
    ‘Microsoft.XMLHTTP’
    ];

    it is:


    var msxml = [
    'MSXML2.XMLHTTP.3.0',
    'MSXML2.XMLHTTP',
    'Microsoft.XMLHTTP'
    ];

    3. I do not like your lazy-like getXHR function implementation, below is my implementation:


    var getXHR= function(){
    if(window.XMLHttpRequest)
    return function(){return new XMLHttpRequest}
    else if(window.ActiveXObject)
    return function(){
    var http;
    var msxml = [
    'MSXML2.XMLHTTP.3.0',
    'MSXML2.XMLHTTP',
    'Microsoft.XMLHTTP'
    ];
    for (var i=0, len = msxml.length; i

    I believe it is much more reasonable and readable, and maybe this optimization is only for the stupid internet explorer 6.
    4. You add checking the status 200 in the handleReadyState function, but what about the local file system, and the file protocol, maybe “o && o.readyState == 4 && (o.status == 200 || o.status == 0)” would be better.
    Last: this blog is not quite convenient, First time I do not input the e-mail address and it tell me an error, and I returned to this page, and the the textarea has been cleared……. Sorry for posting again, I find last post has some problem please delete it, and this blog lack of the preview feature.

  11. js fan

    And this is my getXHR function:


    var getXHR= function(){
    if(window.XMLHttpRequest)
    return function(){return new XMLHttpRequest}
    else if(window.ActiveXObject)
    return function(){
    var http;
    var msxml = [
    'MSXML2.XMLHTTP.3.0',
    'MSXML2.XMLHTTP',
    'Microsoft.XMLHTTP'
    ];
    for (var i=0, len = msxml.length; i < len; ++i) {
    try {
    http = new ActiveXObject(msxml[i]);
    getXHR = function() {
    return new ActiveXObject(msxml[i]);
    };
    break;
    }
    catch(e) {}
    }
    return http;
    }
    }();

    Sorry for posting again, the HTML entities………….

  12. kentaromiura

    I use often this technique,
    but be careful because if you use closures AND you get a Dom node in the factory function you leech some memory on IE.
    In your case the problem don’t exists.

  13. Jonathan Snook

    For clarification, MSXML2.XMLHTTP.3.0, MSXML2.XMLHTTP, and Microsoft.XMLHTTP all map to the same object internally: “MSXML2.XMLHTTP.3.0″. (that is, on any currently supported Internet Explorer platform from Windows 2000 and up) Checking for each version as you are is unnecessary. The one version you should add is “Msxml2.XMLHTTP.6.0″ which is the version that ships with IE7 and would be used if somebody turned off the native XHR object (not a likely scenario but seems like the best way to handle it).

  14. kentaromiura

    @Snook:
    xmlhttp 6.0 can be installed as stand-alone so even an IE6 machine could use it.

    there is a urban legend out there that say that one of the 2 {MSXML2.XMLHTTP, and Microsoft.XMLHTTP} map to the LAST msxml installed on a machine, i don’t know if this is true.
    For what I know microsoft.* is the OLD namespace and MSXML2 the new one.

  15. Web Development News 10.01.07 | Matt Snider JavaScript Resource

    [...] http://www.dustindiaz.com/faster-ajax/ [...]

  16. Calm_Pear

    @Dustin > @Andrew
    I think both implementations work but for readability I prefer Dustins version…

    And about the namespaces, the MS XML Team has this to say:
    http://blogs.msdn.com/xmlteam/archive/2006/10/23/using-the-right-version-of-msxml-in-internet-explorer.aspx

    Which apparently results in using only: Msxml2.DOMDocument.6.0 and Msxml2.DOMDocument.3.0

  17. Calm_Pear

    crap… those are the DOMdocument objects… delete the comment Dustin… this’ll only create confusion… :s

  18. Elegantly Fast Ajax at TrickJarrett.com

    [...] Elegantly Fast Ajax - I dig it. Share: These icons link to social bookmarking sites where readers can share and discover new web pages. [...]

  19. eglencen

    […] Making Ajax Elegantly Fast (tags: ajax javascript fast web development webdev) This entry was written by delicious and posted on 1 Ottobre 2007 at 01:25 and filed under Del.icio.us. Bookmark the permalink. Follow any comments here with the RSS feed for this post. Post a comment or leave a trackback: Trackback URL. […]

  20. myblg

    First, not that it’s a big deal, but what’s the point of the ‘return http;’ line in your original getXHR function? Since you are redefining the function anyway, it doesn’t seem like it would serve a purpose. However, since that wasn’t in your original writeup, I’m assuming you must have added it for a reason.

    Second, why don’t you test for XMLHttpRequest using a !! flag instead of a try / catch block? While the ‘catch’ block for the way you’re doing it would only get hit once in IE6, that’s once more than it needs to be hit it seems. The performance gain would probably be just a few milliseconds or so, and it’s maybe not even worth discussing, but I figured I’d ask anyway.

  21. stimulant » Blog Archive » things to look at (October 1st)

    [...] Making Ajax Elegantly Fast - [...]

  22. links for 2007-10-02 « Gevã Schaefer

    [...] Making Ajax Elegantly Fast Uma função para requisições Ajax rápida e elegante (tags: development javascript web ajax) [...]

  23. Making Ajax Elegantly Fast

    [...] Making Ajax Elegantly Fast [...]

  24. Mats Lindblad

    I’m not gonna pretend like I know anything but after reading “Peter’s Blog” I got the impression that you should use return getXHR(); and not http.
    Or did I miss anything?

  25. links for 2007-10-02 « toonz

    [...] Making Ajax Elegantly Fast (tags: ajax javascript webdev) [...]

  26. koekoeh

    very great info ^^! thanks for sharing Dustin.
    Much appreciated!

  27. : Ajax : Tecnica de uso de ajax elegante y eficaz | Ajax, Javascript, Xul, Internet, Recursos, Informatica … | Ajaxman

    [...] Visto en | Optimized Speedy Ajax Code y en Making Ajax Elegantly Fast [...]

  28. Douglas

    @Mats Lindblad:

    Yes, you are righ. The http variable is redundant, you could remove the “http = new XMLHttpRequest;” and “http = new ActiveXObject(msxml[i]);” lines and simply do “return getXHR();”.

    Also note that the code on this page is only good for testing on a local machine where there is no latency in the request.


    http.open(method, uri, true);
    handleReadyState(http, callback);

    This only processes handleReadyState once, so if readystate == 1 the first time (ie, the request has not been completed between the first and second lines of code in the snippet above), then the callback will never be called.

    There is also no mechanism to handle a failed request.

  29. Javascript News » Blog Archive » Optimized Speedy Ajax Code

    [...] Dustin Diaz has revisited his seven JavaScript techniques and has updated his XHR-getting-function to be faster, using the Lazy Function Definition Pattern, which ends up looking like: PLAIN TEXT JAVASCRIPT: [...]

  30. Lazy Function Definition Pattern | Matt Snider JavaScript Resource

    [...] Faster Ajax posted by admin at 8:37 pm [...]

  31. henrah

    here is another way you could define getXHR:


    var params = [
    ['XMLHttpRequest', null],
    ['ActiveXObject', 'MSXML2.XMLHTTP.3.0'],
    ['ActiveXObject', 'MSXML2.XMLHTTP'],
    ['ActiveXObject', 'Microsoft.XMLHTTP']
    ];

    function getXHR(){
    do try {
    return new window[params[0][0]](params[0][1]);
    } catch(ex) {
    params.unshift();
    } while (true);
    };

    When it is first called, the function will unshift through params until it finds a working constructor/argument pair. This becomes the default value.

  32. henrah

    Actually, I would amend the previous comment:


    var params = [
    ['XMLHttpRequest', null],
    ['ActiveXObject', 'MSXML2.XMLHTTP.3.0'],
    ['ActiveXObject', 'MSXML2.XMLHTTP'],
    ['ActiveXObject', 'Microsoft.XMLHTTP']
    ];

    function getXHR(){
    do try {
    return new window[params[0][0]](params[0][1]);
    } catch(ex) {
    if (! params.unshift()) throw new Error('XMLHttpRequests are not supported by this browser.');
    } while (true);
    };
  33. Ashutosh Nilkanth’s Blog » Making Ajax Elegantly Fast

    [...] Dustin Diaz shares some cool tips on making Async JavaScript (Ajax) work faster on the client-side using the Lazy Function Definition Design Pattern. Entry Filed under: Technology, .NET Web Dev, Linklog [...]

  34. John Resig

    “Dustin: Yes, there is… that piece of logic is run upon every request. And not only that, it doesn’t take advantage of IE’s newer methods for XMLHTTP such as MSXML2 and XMLHTTP.3.0.”

    You’re kidding, right? A boolean check for the existence of a property is worse than using closured function to wrap object creation? Bwah?

    As far as the XMLHTTP vs. other modules goes, I’ve discussed it with the IE team and they agreed that referencing a single version of the XMLHttpRequest code would reduce the number of inter-browser-version bugs and inconsistencies, especially considering that the newer versions don’t provide any tangible benefits in performing the requests. Thus, the code is significantly simplified.

  35. Alex

    Good read, I’ve been trying to learn a little more js on my own lately until my computer fried. Hopefully I’m gonna get back into it soon and I can work on my AJAX a little bit too. Thanks for the post

  36. Theodor Zoulias

    Making AJAX fast this way, slows page loading (a tiny bit). In terms of performance smaller scripts usually beat cleverer ones, due to time-to-download factor.

  37. H5N1

    I faced this problem almost one year ago and I solved in a similar way: implementing a new Object and using an Array to collect all the Objects. Then each use of the callback could be pointed to the relative Object in the Array.
    Obviously my code was not so elegant! :D

  38. Optimized Speedy Ajax Code

    [...] Dustin Diaz has revisited his seven JavaScript techniques and has updated his XHR-getting-function to be faster, using the Lazy Function Definition Pattern, which ends up looking like: [...]

  39. hax

    Sorry, your post is another bad example. Besides many issues some comments here pointed before, Lazy Func Def is totally a ANTI-PATTERN. I’ve left some comments on Peter’s article, you should read them.

    You only use getXHR() internally, so it will not as bad as Peter’s and FredCK’s examples. But it still no sence, because you can write the code more clearly and memory leak free (for example, henrah’s version, though it still can be imporved).

    Finally, about performance, in fact, all versions here are just same in practice (if we don’t consider the bugs in some of them), because compare to network IO, the cost of object initilization and exceptions can be ignored at all.

  40. David R

    This doesn’t seem to work as described.
    It fetches the file but never fires the callback func.
    You only seem to check the readystate once.

    To make it work I changed:
    handleReadyState(http, callback);
    to:
    http.onreadystatechange = function(){handleReadyState(http, callback)};

    Or maybe I’m missing something?

  41. alex

    Hi , i know that im going to sound really special for asking this, but how do i actually use your solutions function?


    asyncRequest('GET', 'foo.php', function(o) {
    console.log(o.responseText);
    });

    what variable holds the returned data?

    thank you in advance!

  42. Mark

    I have rewritten the code a bit to work with our site:


    var oAjaxInterface = {
    mInit : function ( method, uri, callback, postData, formular ) {
    var http = oAjaxInterface.mGetXHR();
    http.open( method, uri, true );
    if(formular)
    http.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
    oAjaxInterface.mHandleReadyState( http, callback );
    http.send( postData || null );
    return http;
    },
    mHandleReadyState : function ( http, callback ) {
    http.onreadystatechange = function(){
    if ( http && http.readyState == 4 && http.status == 200 ) {
    if ( callback ) {
    callback( http );
    }
    }
    };
    },
    mGetXHR : function () {
    var http;
    try {
    http = new XMLHttpRequest;
    oAjaxInterface.mGetXHR = function() {
    return new XMLHttpRequest;
    }
    } catch(e) {
    var msxml = [
    "MSXML2.XMLHTTP.5.0",
    "MSXML2.XMLHTTP.4.0",
    "MSXML2.XMLHTTP.3.0",
    "MSXML2.XMLHTTP",
    "Microsoft.XMLHTTP"
    ];
    for ( var i=0, len = msxml.length; i < len; i ) {
    try {
    http = new ActiveXObject( msxml[i] );
    oAjaxInterface.mGetXHR = function() {
    return new ActiveXObject( msxml[i] );
    }
    break;
    } catch(e) {}
    }
    }
    return http;
    }
    }

    And then call it like this:



    oAjaxInterface.mInit( "GET", "Some URL",
    function( response ) {
    oSendReq.mHandleResQ( response );
    }
    );

    I’m not sure if this will work as you use a return function, and i stripped that out.
    Do you see any problems with this?

  43. uglychart.com » Blog Archive » links for 2007-10-30

    [...] Making Ajax Elegantly Fast (tags: web2.0 programming ajax) [...]

  44. 2007 Hand-Picked Web Design Tutorials

    [...] Making Ajax Elegantly Fast - This article uses the Lazy Function Definition Pattern to reduce the time need to initialize an [...]

  45. Better ways to writing JavaScript

    [...] to calling these requests. The first thing you’ll need is an asyncRequest function. As well, I wrote one of these just a bit ago. It works like [...]

  46. Tom

    Dear Dustin,

    isn’t o.onreadystatechange = function() {} necessary before if (o && o.readyState == 4 && o.status == 200) for callback to work as intended?

    Tom

  47. tre

    hi dustin - where are you? there have been some fairly significant issues raised here, i’m curious if you have any response.

    what the hell…!? this whole “blog” idea, i just don’t know if it is any good. cancel everything!

  48. Pablo

    Hi
    How can I send post data using this function?
    I tried but it doesn’t work.
    I make it work adding:
    http.setRequestHeader(”Content-type”, “application/x-www-form-urlencoded”);
    http.setRequestHeader(”Content-length”, postData.length);
    http.setRequestHeader(”Connection”, “close”);
    before the http.send();

    is it ok?

    Thanks!

  49. Tom

    @Pablo

    You would write:


    asyncRequest('POST', 'foo.php', callbackFunction, postData);

    where callbackFunction is function you wish to call to deal with response text and postData are form field values you wish to send to foo.php (postData looks like: field1=value1&field2=value2…)

  50. Optymalizacja Ajaksa | CSS Mania

    [...] Diaz podjął temat optymalizacji połączeń przez XMLHttpRequest w artykule pt. Making Ajax Elegantly Fast. (Brak głosów) Loading [...]

  51. Devin

    I put it in myself and it works! :)


    http.onreadystatechange = function () { handleReadyState(http, callback); };

Friday, May 29, 2009

Users Role Based Access System for PHP -

A Role-Based Access Control (RBAC) system for PHP

By Tony Marston

13th May 2004
Amended 9th March 2008

As of 10th April 2006 the software discussed in this article can be downloaded from www.radicore.org

Introduction
What is 'access control'?
What is 'role based'?
- Level based
- User based
- Group based
- Responsibility based
What is a 'menu system'?
My current design
Conclusion
Other types of Access Control
Amendment History

Introduction

An 'access control' system is just another name for a 'security system' or a 'permissions' system, and in my long career I have been involved in the design and development of several of these systems:

This article will document the features of some of the early systems I worked on and explain the features of my current design.

What is 'access control'?

In a single-user application which is typically found on a desktop computer there is no need for any access control - the user has access to every function within the application. However, in a multi-user application which is deployed over numerous devices which are linked together in a network it is more than likely that not all functionality will be available to all users. In this situation a method is required whereby functions within the application can only be accessed by persons to whom permission has been granted. This will typically require the maintenance of the following details:

  • A list of all the functions that are available within the system. These 'functions' are sometimes referred to as 'transactions' or 'tasks'.
  • A list of all the persons who are allowed to access the application as a whole. These 'persons' are sometimes referred to as 'users'. Typically this information is used in a logon process through which every user must pass before gaining access to any part of the application.
  • A list of permissions which identifies which functions are accessible by which users.

Each of these lists is normally maintained as a table within a database.

What is 'role based'?

There is more than one way to give different permissions to different users, but each method has its own set of advantages and disadvantages. Here are some that I have encountered:

Level based

This is a simple system as it only requires two database tables - USERS and TASKS - without any relationship between them, as shown in Figure 1:

Figure 1 - a level-based permission system

rbac-01 (1K)

In this system each TASK is given a security level number in the range 1 to 99, with 1 being the lowest level and 99 the highest. Each USER is then given a security level number and is allowed to access only those TASKs which have a security level which is the same or lower. Thus a USER with a security level of 5 can access a TASK which has a security level in the range 1-5.

The problem with this system is that it is totally cumulative - by raising the level number you can add more tasks, and you can only remove tasks by reducing the level number. Groups of tasks that share the same level number are either included or excluded as a group, there is no possibility to mix'n'match. For example, take a simple setup with two users, 'A' and 'B', and two tasks, 'A' and 'B'. Now try to give user 'A' access to task 'A' but not task 'B', and user 'B' access to task 'B' but not task 'A'. You will find that it cannot be done:

  • If both tasks have the same security level then access can be granted to both or neither.
  • If one task has a lower security level than the other then access can be granted to the lower level on its own, or to both levels. It is not possible to grant access to the higher level and exclude tasks at a lower level.

User based

In this system permissions are defined for individual users. This involves a many-to-many relationship between USERS and TASKS with PERMISSIONS being the link or intersection table, as shown in Figure 2.

Figure 2 - a user-based permission system

rbac-02 (1K)

I have seen several different implementations of this design:

  • In a system with complex tasks - where a single tasks can operate in create, read, update and delete mode - access to a task will include all of those modes.
  • Where access to individual modes within a task is required then the PERMISSIONS record needs to have a YES/NO switch against each one of those modes. This is often referred to as a CRUD matrix (where 'CRUD' stands for Create, Read, Update and Delete) as the arrangement of tasks rows and permission columns resembles a matrix.
  • In a system with simple tasks - where each of the modes is handled by a totally separate task - then a CRUD matrix is not required.

My personal preference is for a system to be comprised of small and simple tasks. This may increase the number of tasks, but each one is simple, easer to design, easier to specify, easier to develop and easier to use.

This disadvantage of this design is that where several users share the same permissions any change to those permissions needs to be repeated for each user.

Group based

In this design the users are split into groups and permissions are assigned to the group, not the individual user, as shown in Figure 3.

Figure 3 - a group-based permission system

rbac-03 (2K)

This design has the following advantages:

  • Once the user has been identified the USER record will supply the USER-GROUP identity which is all that is needed to access the PERMISSIONS table.
  • Any change made to a group's permissions will automatically be inherited by all members of that group
  • Changes to a group's permissions can be made very easily as there is only one table, the PERMISSIONS table, to maintain.
  • If an individual user is switched to another group this will sever all connections to the permissions of the previous group and replace them with those of the new group.

In this design the USER-GROUP table is sometimes known as SECURITY-CLASS or ROLE.

Responsibility based

In this design it is possible for a user to belong to more than one group at the same time. This involves two many-to-many relationships, as shown in Figure 4.

Figure 4 - a responsibility-based permission system (simple)

rbac-04 (2K)

The USER-GROUP table is sometimes referred to as AREA-OF-RESPONSIBILITY because an individual user may have responsibilities in more than one area.

This design has the following disadvantages:

  • It is only possible to add permissions by linking a user to another user group. It is not possible for the addition of another group to undo any permissions granted by an existing group.
  • There are now two tables to maintain in order to give a user access to a task - the USER-USER-GROUP table and the TASK-USER-GROUP table.

A more complex version of this design is shown in Figure 5.

Figure 5 - a responsibility-based permission system (complex)

rbac-05 (4K)

In this design there are now five many-to-many relationships which enables a far wider range of customisation. In the implementation I saw the tasks were complex (a single task could operate in Create, Read, Update and Delete mode) which meant that each of the link/intersection tables was a CRUD matrix. As these tables were read in a strict sequence and the task permissions on one table could be replaced by the task permissions on another table. It was therefore possible for a record with a permission checked ON to be superseded by a record from another table with that permission checked OFF.

Even though in theory this design appears to be much more flexible, in practice this created a problem with usability. As permissions can exist on five tables, and the permission granted on one table can be taken away by the contents of another table it becomes a more difficult process to track down which user has access to which task.

What is a 'menu system'?

Within an application that may contain dozens or even hundreds of functions a method is required whereby the user can quickly activate the function that will allow him to perform the desired task. In some of the first systems I worked on in the 1970s this 'list' was actually nothing more than a written list, and each part of the system had to be activated manually from the command line. A later innovation was to have this list presented on the computer screen with a mechanism so that simply selecting an item would cause it to be activated. With large systems it was cumbersome to have this list presented in a single unit, so it was split into smaller units or pages. These pages were arranged in a hierarchical structure, and links on one page could activate other pages at a lower level. This was the birth of the menu system.

In the first systems I worked on the menu pages were always hard-coded in a fixed and rigid structure. This method had the following disadvantages:

  • The menu pages had to be designed and constructed before development could start.
  • The menu pages could not be altered without changing and recompiling a piece of program code.
  • Every user always started at the same menu page after logging on, even if the only options which they were permitted to access existed on a sub-menu.
  • Options which were not accessible to a user were still visible on the menu screens.
  • Context could not be passed from one screen to another. This meant that if the user found a record on one screen and wanted to go to another screen to see more details the record identity had to be entered manually into the second screen. This was slow and frustrating work, especially if a mistake was made when keying in the record identity.

In the mid-1980s I was asked to design a menu system which did not include these disadvantages, so I came up with the following:

  • I added a 'menu' table to the security database so that menu screens could be constructed dynamically from the contents of the database. As the database already contained a list of transactions for security purposes I could use the same data in the definition and construction of menu screens.
  • This meant that menu pages could be constructed and modified without the need for programmer intervention. There was no need to define the entire menu structure at the start of the project as it could be modified 'on the fly'.
  • Where a user only had permission to access functions which existed on a sub-menu I allowed that sub-menu to appear immediately after the logon screen, thus bypassing all unnecessary menu screens.
  • As the menu screens were constructed from the database, and as the database also contained the user permissions, it was a simple matter to filter out all those options to which the user did not have permission to access. Instead of having to build a separate set of menu screens for each user I could build a single set which could be shared by all users, and each user would only see those options which he was permitted to access.
  • I created a mechanism whereby context could be passed from one screen to another, thus removing the need to enter record identities manually.

As you can see a security system and a menu system share a lot of common data, so it makes sense (to me at least) to combine them into a single system.

My current design

My current design has evolved over 20 years and has been written in three different languages. It has been used as the basis for many different systems for many different clients and has proved to be effective and robust. As it is driven by the contents of the database then changes can be made easily and quickly. Due to the modular design any changes in functionality can be made easily either by changing an existing module or by adding in a new module.

I chose to implement group based security around the USER<==ROLE==>ROLE TASK<==TASK tables as this gives sufficient flexibility without being as complicated as responsibility based security. The two significant reasons are:

  • Permissions can be maintained on a single screen.
  • At run-time permission can be verified with a single lookup on the ROLE-TASK table using ROLE_ID and TASK_ID.

Figure 6 - my current design

rbac-06 (5K)

Here is an explanation of the other tables in my database:

  • PATTERN - Each task conforms to one of the patterns in Transaction Patterns for Web Applications, and I have found it very useful to have this as an identifier on each TASK record. For example, when selecting entries for the ROLE-TASK (PERMISSIONS) table I can very quickly isolate all the tasks of any particular type.
  • SUBSYSTEM - Each application or system can often be broken down into discrete parts (or subsystems) which can be regarded as separate collections of components. For example, 'Menu and Security' is separate from 'Workflow' which is separate from 'Product' which is separate from 'Customer'. It is quite often that a user will have responsibility in only one of these areas, therefore it is useful to have this as part of the selection criteria when maintaining access permissions.
  • MENU - Entries on the TASK table which are of type 'MENU' require to have their contents maintained on the MENU table. When a user selects a menu the contents of the MENU table can be retrieved and displayed. By using the entries on the ROLE-TASK table any MENU option which is not accessible to the user can be filtered out, thus restricting the display to only those options which the user is actually allowed to access. The contents of the MENU table is displayed in the menu bar area of my web pages.
  • NAVIGATION BUTTON - There are some tasks which can be accessed without the need for any context being supplied, and these can be made available on any MENU screen. However, in my infrastructure I have forms families which are comprised of a parent form and several child forms. While the parent form may appear on any MENU, the child forms cannot be made available until the parent form is active. This is because the child forms require context, and context is supplied by the parent form. For example, you must select one or more entries in a LIST form before you can pass control to an UPDATE, ENQUIRE or a DELETE form. These child tasks are defined on their own database table, and are displayed in their own navigation bar area on the web page.
  • TASK-FIELD and ROLE-TASK-FIELD - I have sometimes been asked to provide a version of a screen which has access to individual fields downgraded to 'read-only' or even 'invisible'. In some languages it may not be possible to dynamically change the way a field is painted on the screen, in which case it is usually necessary to create a completely different version of the screen. However, as each web page is an HTML document which is built from an XSL transformation I have devised a mechanism whereby I can pass instructions to the transformation process to alter the way that individual fields are to be handled. By default each user has FULL access to every field on the screen, but this can be altered by first defining the 'special case' fields on the TASK-FIELD table, then by altering the field permissions on the ROLE-TASK-FIELD table. This means that a single screen can be viewed by different users with different capabilities and the field definitions will be altered dynamically.
  • HELP-TEXT - Within the screen for each task is a hyperlink which will bring up a separate page of help text for the current task. This is retrieved from the database and made available to the user through a standard 'help' function. This help text may contain hyperlinks to other documents.

Conclusion

Some people may say that having to maintain such a complex database is overkill for such a 'simple' requirement, but in my long experience the customer is always dreaming up 'little additions' to his requirements that cannot be satisfied with a primitive design. How easy would it be for you to change your current system to incorporate the following 'little requirements':

  • Can I have dynamic menus which I can easily modify instead of ones that can only be changed by modifying program code?
  • When a menu is displayed I only want to see the options that I can actually access.
  • Within a single screen I want to change access to certain fields for certain users.

I have also found over the years that by having such a database I can easily extend it to incorporate new requirements or features. For example, because of the TASK table I am able to include extra pieces of information which can be used by the task at run-time and which can be changed without going near any source code, such as the initial sort order for LIST screens.

All the maintenance screens for the PHP+MySQL version of my Menu and Security system are documented in User Guide to the Menu and Security (RBAC) System.

Other types of Access Control

The system described in this document grants or denies permission to use particular tasks within an application, and if a user is granted access to a particular task then he has automatic access to all the data which can be handled by that task. For example, permission to use the "Update Customer Details" task means that the user can update the details of any customer.

Some users may require a level of permissions which goes beyond the individual task and is applied at the data level, such as the following:

  • An application database usually holds the data for a single organisation, and all the users belong to that organisation, so all the users have access to all the data (subject to the tasks to which they have permission). In some cases the database may deal with several organisations, but each user can only access the data for the organisation to which he belongs. For example, a user of organisation #1 has access to the "Update Customer Details" task, but can only use it to access the data for organisation #1. The data for all other organisations is totally invisible to him.

    This requirement, which is sometimes known as a Virtual Private Database (VPD) or Row Level Security (RLS), can be implemented in the Radicore framework by following the instructions contained within Implementing Virtual Private Databases.

  • A variation of the above is where a user can access the data for any organisation, but has different permissions within each organisation. For example, he may have permission to use the "Update Customer Details" task within organisaton #1, but not for organisation #2.

    This variation could be implemented within the existing framework by each user having a separate logon for each account/organisation.

    If having a separate logon for each account/organisation is not acceptable, this would require a change to the framework so that each user could have a different set of permissions for each account/organisation, and to modify the LOGON screen to include a field to identify which account/organisation is going to be accessed.

Tuesday, May 26, 2009

Drupal 6 API Cheatsheet

I found a good cheatsheet for Drupal 6, very useful to increase speed of web development using drupal while developing a drupal module.

The cheat sheet of drupal 6 API can be read at:
http://growingventuresolutions.com/gvsfiles/drupal6_api_cheatsheet_0.pdf

Saturday, May 23, 2009

Edit and Delete Icons

ACTION



Tuesday, May 19, 2009

Admin Panel Templates

http://www.mostinspired.com/blog/2008/10/11/admin-templates/

Monday, May 18, 2009

Drupal Theming - Zen

Zen is the ultimate starting theme for Drupal. If you are building your own standards-compliant theme, you will find it much easier to start with Zen than to start with Garland or Bluemarine. This theme has fantastic online documentation and tons of code comments for both the PHP (template.php) and HTML (page.tpl.php, node.tpl.php).

This theme saved me at 2am. Three hours of messing with 1000+ lines of nasty Garland-adapted code later, I abandoned it and recoded the site as a Zen sub-theme in under an hour. Thank you, thank you, thank you.
- Greg

You can see more testimonials in the online documentation.

The idea behind the Zen theme is to have a very flexible standards-compliant and semantically correct XHTML theme that can be highly modified through CSS and an enhanced version of Drupal’s template system.

Out of the box, Zen is clean and simple with either a one, two, or three column layout of fixed or liquid width. In addition, the HTML source order has content placed before sidebars or the navbar for increased accessibility and SEO.

The name is an homage to the CSS Zen Garden site where designers can redesign the page purely through the use of CSS.

We are hoping to build up a library of Zen sub-themes, so feel free to hit this one like the Zen Garden and see how far you can extend it.

Features:

  • Well documented php and css files.
  • Lots of easy-to-access classes and ids for CSS developers.
  • Classes on body include items like "logged-in", "not-logged-in", "front", "not-front", as well as the node type ("node-type-story", "node-type-blog", etc) for single node pages. This can allow CSS developers to do things like have a different color for a given page item for non-logged-in users, have a larger header section on the front page, or put a different background color on a given node type.
  • Automatic (table-less) column resizing for 1, 2, and 3-column layouts.
  • Separate layout.css file to allow for changing the type of column layout (holy grail, jello mold, etc).
  • A print.css file optimizes print display automatically when sent to a printer — removes, sidebars and nav elements, optimizes font size, adds text to links showing href, etc.

Note:

There seems to be a conflict between some host installations of the SiteZen site builder and the Zen directory name which causes broken tabs, css files to be "not found", and other issues. To learn more about this problem and to learn its solution, visit the troubleshooting guide.

Add Edit Delete Icons

http://img295.imageshack.us/img295/7609/addez7.pnghttp://img183.imageshack.us/img183/6640/deletert9.pnghttp://img176.imageshack.us/img176/7128/editfr9.png

Wednesday, May 13, 2009

Color Combination Guides for Web Design

http://www.allwebdesignresources.com/colorschemescombinations.html

http://www.allwebdesignresources.com/webdesignblogs/graphics/webdesigncolorcombinationsguide/

http://www.allwebdesignresources.com/colorusageinwebdesign.html

Friday, May 8, 2009

15 Wonderfully Creative Uses for PHP

http://net.tutsplus.com/tutorials/php/15-wonderfully-creative-uses-for-php/

15 Wonderfully Creative Uses for PHP

May 4th in PHP by Joel Reyes

If you are familiar with the basics of PHP, then you're probably wondering how you can use it to make your website more appealing. The possibilities are endless, and you can write your own PHP scripts or implement widely available scripts from around the web. Let’s get started with 15 creative uses for PHP for your website!

PG

Author: Joel Reyes

Joel Reyes-Has been writing and developing web sites for several years now, this has lead him to be the creative mind behind Looney Designer a design resource and portfolio site that revolves around web and graphic design.

1. E-Commerce

E-commerce is one of the major uses for PHP. From a small business level to an enterprise level, businesses are always looking to create additional streams of revenue online. If you know how to integrate existing e-commerce solutions or build your own from scratch, this gives you a distinct advantage with your clients.

Advanced Coders

If you want to build your own shopping cart application, you can either code the entire application from scratch or implement a PHP framework. If you are an intermediate to advanced PHP coder, I personally recommend using a framework such as CodeIgniter or CakePHP. CakePHP has a bakery section with readily available source code for e-commerce applications. For instance, you can integrate Paypal with your site using ready-made scripts. CodeIgniter has a user guide and a few tutorials to get you up and running quickly. Both of these frameworks have extensive documentation on how to create web applications from the ground up; which one you use is really a matter of personal preference.

Beginners

If you are new to PHP, or you just know some basic PHP programming, I would suggest using an existing e-commerce solution. Some of the available options are:

While I understand that each of these solutions has its own quirks, it might be easier to spend a few days with the documentation or a tutorial than to learn PHP from the ground up. It really depends on how much time you are willing to invest in tackling e-commerce.


2. Project Management Tools

For both freelancers and web development firms alike, project management is an important aspect of your business. Your clients need a resource to be able to check the progress of the work and provide feedback. Ideally, with a good project management system in place, your clients will be thoroughly pleased with the end result.

There are several excellent web-based project management tools on the market today. If you can afford to use a subscription service, I would highly recommend Basecamp. Although Basecamp was written in Ruby on Rails, it is an excellent advertisement for effective, streamlined project management solutions. For those of us who do not necessarily need a subscription-based product, you can build your own!

Building your own project management tool from the ground up will require a bit of in-depth PHP and some Javascript knowledge. The most important aspects of the application are security, time-tracking, collaborative to-do lists, file sharing, a message board, and a live preview of the website - which could simply be a link to an index.html page. Of course, you can add to this list or remove features as you like. The takeaway point here is that you can learn a lot about PHP by creating this application and your customers will be happy to see their project take shape. Showing you how to get started on your own project management tool is beyond the scope of this article, but I hope I have given you a good basis to start generating ideas!


3. Graphical User Interface

For those of you who are up to the challenge, you can extend your PHP installation to create desktop applications. This one is a challenge because it requires some extensive knowledge of PHP and it might also be easier to create desktop applications in other programming languages. If PHP is your favorite programming language, then you can use some of these PHP extensions to get you started creating GUI applications.

  • PHP GTK - This extension is a popular open source that implements the GIMP toolkit
  • ZZEE PHP GUI - A paid solution that allows you to turn your PHP scripts into Windows applications

The main advantage of creating your own PHP GUI's is that you will learn a great deal about the language itself!

4. Building an Online Community

Whether your website is about business, entertainment, or products and services, internet users need to feel connected to the product or message. For example, if you develop web applications, a forum where your customers can discuss issues might be a good idea. As a user, if I have a question and I need support right away, a hotline or an e-mail form is often insufficient. With an online community, your visitors can help solve each other's product-related issues, and even answer technical questions. You still have to provide some level of after-sales support, but a community can effectively decrease your workload and provide useful feedback.

You can build your own PHP-driven online community, or choose from widely available scripts that you can implement into your website. Again, if you plan to build your own forum from scratch, I do recommend the use of a PHP Framework. CodeIgniter, for example, has classes and helpers to take care of the most routine tasks you can think of. Beyond that, you can use several different forum building tools. Some popular ones include:

5. Developing Facebook Applications

You can integrate Facebook with your website using PHP. If you have developed Facebook applications using another language or you would like to get started with PHP, the Facebook developer's wiki can help you to get started. The developer's wiki explains the Facebook PHP client library and provides detailed instructions on how to install and use the files included in the library. This is certainly worth a look if you are interested in programming for the Facebook platform. For Facebook users interested in the back-end of the platform, this would be a natural step.

6. Generating PDF Files

The PDF format is Adobe's proprietary file type for document exchange. Using a library called PDFLib, you can generate PDF files with PHP. This library is already included with PHP5; to access it, you need to uncomment the appropriate lines in your PHP configuration file. An example of why creating PDF files might come in useful is, if you were building an online invoicing application and you wanted to output an HTML-generated invoice in PDF format. You can then send the invoice via e-mail or print a copy of it to your client.

7. Parsing XML Files

PHP allows you to parse XML files. Parsing XML is an important feature of PHP 5 because not all browsers can output the contents of an XML file; so you can create a parser in PHP to facilitate this process. Using XML is important for RSS feeds, and also for data storage and rendering data on different devices - for example, cell phones use an implementation of XML called WML (Wireless Markup Language). Working with XML files in PHP is similar to handling the opening, closing, and reading of a file. The steps involved are creating an XML parser, setting functions to handle your opening and closing XML tags, opening the file for reading, reading the file incrementally and then closing it.

8. Mailing Lists

You can write your own script to send e-mail newsletters to your client, or use a ready-made script. PHP mailing lists are an excellent way to keep your clients informed about your services and products, holidays, vacations, and general announcements. Anything your clients need to know can be included in your automated newsletter. The PHP online documentation explains PHP mailing functions in more detail. There are also scripts you can download and install on your website:

9. Image Processing and Generation

Using the GD library with PHP, you can do more than just output HTML to the browser! You can output images in different file types including jpeg, png, and gif. This feature of PHP is useful because it allows you to create thumbnail pictures, add watermarks, resize and crop images, and even create a photo gallery!

10. Create Graphs and Charts

Do you need visual representations of numbers on your site? PHP can create graphs and charts too! Using Image_Graph, you can create up to fourteen different types of charts including pie charts, bar graphs, impulse, dot/scatter, step, candlestick, box & whisker, radar. This is incredibly useful for e-commerce websites or websites where you need to present graphical data in a concise manner. the Image_Graph website has more detail on how you can get started!

11. Content Management Systems

One of the most popular uses of PHP is creating or using Content Management System. A good CMS allows your clients to update their website and add content without any in-depth knowledge of HTML and CSS. A good Content Management System should be user friendly, extensible, produce clean URL's, and be search engine friendly among other things. There are several online resources you can use to assist you with coding your own CMS from scratch, or you can use one of the widely available free or commercial solutions listed below:

12. Create a PHP Photo Gallery

By simply using PHP's file handling functions, you can create your own photo gallery! you begin by placing your photos in a single directory, you then use PHP's exif function to get header information about the photo and output a thumbnail version of it. The process is as straightforward as it sounds and its also a great way to present your photos!


13. Create Dynamic Website Templates

Using PHP, you can make it easier to add pages and elements to your websites dynamically. You begin by creating the HTML page and splitting it into the header, main content, and footer sections. Add the .php extension to your subsequent pages and use server-side Includes for the header and footer for each new page. You can also have dynamic sidebars and top navigation sections. As a matter of fact, the more "templated" your site is, the easier it is to update the content.

14. Create Wordpress Plugins

If you have done any work with Wordpress, you will know that it is a highly flexible blogging system that you can use to do just about anything from e-commerce to content management. With that being said, if you know some PHP and you delve into the Wordpress Codex, you have everything you need to begin plugin development. If you need novel ideas for creating plugins, they even have a section where users post their plugin ideas.

Watch a screencast that teaches how to build your first WordPress plugin.

15. Creating Flash

The use of Flash in websites is a contentious issue to say the least! But there is nothing wrong with having Flash elements in certain places on your website; PHP can help with this! To these files you would use the Ming library to create Flash files in .swf format. With this library, you can generate movies, text, and even animations in Flash!

You've just learned fifteen creative uses of PHP which you can use to improve your website or just have fun with - enjoy them all!

Tuesday, May 5, 2009

PHP SECURITY: Preventing PHP sites from being exploited or hacked

Problem:

Help my php site has been exploited or hacked. What can I do to make sure this does not happen again?

Solution:

  1. Set register_globals to OFF
  2. Turn off Display Error/Warning Messages. Set display_error to ZERO.
  3. Never run unescaped queries
  4. Validate all user inputs. Items on Forms, in URLs and so on
  5. Move config.php and files containing Passwords to MySQL to a secure directory outside of the public_html folder
  6. Change permissions on any configuration files containing private information such as database passwords or email accounts to 440 so they cannot be written to and so there is no world permissions. If you need to edit them at a later time you will need to change it back to 640.
  7. Access Control: You don't want the user to have access to any Admin function or Clean up scripts
  8. The .htaccess file is your friend. Use it to deny access to your site or files. (We also have an easy IP Deny Manager tool in the cpanel)
  9. PHP can parse any valid script, whether it is called foo.php, very_long_name.php.php.php, or even deleteme.bat.
    • Using the default extension of ".php" means that before your hackers start you have already told them you are using PHP.
    • As mentioned, you can use any filename for your scripts - if you are using PHP for every script on your server, consider using the ".html" extension for your scripts and making PHP parse HTML files.
    • You can change your file extension by adding this line to the .htaccess or turn it on via the Apache Handlers in the cPanel (AddHandler application/x-httpd-php5 .html)
    • To protect against SQL injection attacks Sometimes hackers will try to screw up your database by inserting SQL code into your form input fields. They can for example, insert code that could delete all the data in your database!
    • To protect against this, you need to use this PHP function:
    • mysql_real_escape_string()
    • This function escapes (makes safe) any special characters in a string (programmers call text a 'string') for MySQL.
  10. Example: $name = $_REQUEST['name']; $safe_name = mysql_real_escape_string($name); Now you know the variable $safe_name, is safe to use with your SQL code.
  11. Keep the PHP code to yourself. If anyone can see it they can exploit vulnerabilities.
    • You should take care to store your PHP files and the necessary passwords to access your MySQL databases in protected files or folders.
    • The easy way to do this is to put the database access passwords in a file with a .inc.php extension (such as config.inc.php), and then place this file in a directory which is above the server's document root (and thus not accessible to surfers of your site).
    • Then, refer to the file in your PHP code with a require_once command.
    • By doing things this way, your PHP code can read the included file easily but hackers will find it almost impossible to hack your site.

You can find more information about hardening your PHP scripts at: PHPsec.org

Also, for security purposes, you can refer to these two websites:

PHPIDS - Web Application Security 2.0 - Index

BlogSecurity

Friday, May 1, 2009

Slideshow

Drupal Slide show modules can be compared from the following link:

http://drupal.org/node/418616