Tuesday, February 06, 2007
by Nik Kalyani
Tuesday, February 06, 2007 7:56:25 PM (Pacific Standard Time, UTC-08:00)

Steve Jobs' rather unassumingly titled Thoughts on Music packs quite a punch. In what is surely going to be one of the most linked-to URL’s on the Internet, Jobs openly advocates an end to DRM-crippled music. Reactions from bloggers such as Don Dodge and Fred Wilson are positive and commend Jobs on this move.

Until now, it has been generally accepted that Apple uses DRM on iTunes tracks to protect its control on online digital music distribution. Turns out that Apple’s research data shows the average iPod has only 22 songs or 3% of music that comes from the iTunes store. So while iTunes and iPod appear to have been mega successes, it’s only the tip of the iceberg. The music companies require Applie to DRM-protect iTunes music and this constrains the market to those willing to live with DRM, liberal as the iTunes DRM policy may seem.

Jobs does a great job of educating us on how completely unbalanced and illogical the music industry’s approach is to online music distribution:

Why would the big four music companies agree to let Apple and others distribute their music without using DRM systems to protect it? The simplest answer is because DRMs haven’t worked, and may never work, to halt music piracy. Though the big four music companies require that all their music sold online be protected with DRMs, these same music companies continue to sell billions of CDs a year which contain completely unprotected music. That’s right! No DRM system was ever developed for the CD, so all the music distributed on CDs can be easily uploaded to the Internet, then (illegally) downloaded and played on any computer or player.

He takes a jab at European countries such as France who have legislation requiring companies to make DRM-enabled music playable on any device: 

Much of the concern over DRM systems has arisen in European countries.  Perhaps those unhappy with the current situation should redirect their energies towards persuading the music companies to sell their music DRM-free.  For Europeans, two and a half of the big four music companies are located right in their backyard.  

Overall, this is an excellent P.R. tactic by Jobs. With this one post, adverse public opinion will shift away from Apple and on to the music industry. Of course, this will not make much of a difference since the dinosaurs running  industry are too fixated on the old to look at the huge opportunities that await a music industry that is free.

Under a free and fair system, there will be no incentive for most people to download sub-standard quality music from questionable sources with embedded junk in the files. The pace of innovation in the music player business will increase as the market size increases. Artists will be fairly compensated for their work and the music industry will get fairly compensated for its marketing efforts. To make this happen, all the industry has to do is build on Apple’s successful 99 cent model and either remove DRM completely, or include DRM in such a way that from the perspective of the purchaser, the DRM does not exist. Another way to look at it is to change the positioning altogether — instead of Digital Rights Management, which serves to limit and take away rights, it should be DRG – Digital Rights Grant. By purchasing a piece of content, you are effectively granted the right to play it on any device that you have in your control.

This is not a very difficult technical problem. Most digital devices today have a configuration function which can be used by the consumer to customize various aspects of its operation. Why not allow the consumer to enter in a “key” of their choice. (Something akin to a password, but with limitations on the characters and the length allowed.) The consumer would enter the same key on every device or media playing software in her/his household. When purchasing digital content (the scheme can be applied to music, video, eBooks), the consumer provides the same key to the online store which locks the content with that key before allowing the consumer to download it. The content can then only be played on any device programmed with that key. This is the essence of DRG — the consumer is freely able to play the content he/she purchased on ANY device under her/his control. If he chooses to illegally share the content, he will also need to also share his key, and the other person(s) will need to configure his devices to use that key. It is certainly conceivable that those intent on stealing music will collude and share keys, but this can be mitigated by retiring keys that are known to be on illegal sharing sites. The music pirates are then left with a static collection of content. That should get boring very quickly.

I realize this is a gross over-simplification of the cryptographic controls that would be necessary to implement such content protection, but the main point I’m trying to make is that a simple and elegant system is possible if all the players cooperate. Device manufacturers and digital content distributors need to agree on a system that is easy for consumers to use. By focusing on “granting” rights for those who want to have legal content, instead of “restricting” rights for those bent on abusing the system, the industry can gradually change the game. If it has the will, there are plenty of super-smart people out there that can create a system that really works.

I truly believe that consumers will be quick to vote with their wallets for any system that makes content available under a fair pricing scheme and without limitations on playback on their devices. Jobs knows this and although it’s an uphill task, this open letter is a great first step in getting the music industry to wake-up to the realities of content distribution in a digital world. Whether the outcome is totally DRM-free music or music with real “fair play” rights, anything is better than the status quo.

#    Comments [0] - Trackback    

by Nik Kalyani
Tuesday, February 06, 2007 7:34:44 AM (Pacific Standard Time, UTC-08:00)

The GoDaddy-MySpace fiasco appears to have run its course. As I read blog posts and news stories, one thing that stood out was the intense focus on the legal and business implications of GoDaddy’s actions. I do not know the exact details, but I can speculate that “business” folks at MySpace contacted “business” folks at GoDaddy who then directed technical folks to act on the request to shut-down Seclists.org. GoDaddy and MySpace (but mostly GoDaddy) has been at the receiving end of a fair amount of negative P.R. as a result. Let’s put our business hats aside for a moment and put on our technical hats (after all this is a technology blog).

I think that if the folks at MySpace or GoDaddy had consulted with their in-house technical and information security resources a much simpler and effective solution could have been employed. It’s a big assumption on my part that in-house technical resources were not consulted going by the sheer stupidity of the public actions (or perhaps they were, and subsequently ignored, which is often the case).

Any sane information security person would have informed them of two very important things:

  • On the Internet there is no “unpublish” feature. Once information is out there, you have to assume that it is there in perpetuity.
  • Usernames and passwords, once compromised must be changed as the associated accounts are no longer secure.

The course of action that MySpace took was the exact opposite of what they could and should have done — run a script on the published list of usernames to permanently disable each one and contact the account owners about what they need to do to regain access. This would have been terribly inconvenient for all the users involved, but it would have made the leaked usernames inconsequential.

GoDaddy simply compounded things for itself by not pushing back on this and instead shutting down the domain at MySpace’s request.

There have been many blog posts asking people to reconsider using GoDaddy as a registrar, including this one by fellow LinkedIn blogger Marc Freedman. I thought about this and decided against it. First of all, it’s a huge pain to move active domains and even with careful planning there will likely be some site down time during the switch. Secondly, there is a cost associated with it.

I would switch if GoDaddy did this sort of thing repeatedly and blatantly. However I am not convinced this is the case. While it is more fun and buzz-worthy to bash GoDaddy on this issue from the grandiose perspective of freedom and laws, the truth of the matter is that this episode highlights one commonality between GoDaddy and MySpace — incompetent people. I somehow doubt switching registrars is going to provide any measure of protection.  

#    Comments [1] - Trackback    

 Wednesday, January 31, 2007
by Nik Kalyani
Wednesday, January 31, 2007 10:54:34 AM (Pacific Standard Time, UTC-08:00)

In a previous post DotNetNuke Module Settings Made Simple, I had shared some code to make DotNetNuke module settings easier to manage. One of the methods in an included class permitted the use of tokens in stored settings values. This allows for many possibilities that are user-, tab- or portal-driven. In that post, I had omitted the code for token substitution because it is a bit lengthy. Here, then, is code for substituting string tokens with portal values.

Although the intended use is for module settings, there’s no reason this code cannot be used for just about any scenario where tokens need to be injected. 

using System;
using System.Web;
using System.Text;
using System.Text.RegularExpressions;
using System.Collections;
using System.Reflection;
using DotNetNuke;
using DotNetNuke.Entities.Modules;
using DotNetNuke.Entities.Users;
using DotNetNuke.Entities.Tabs;
using DotNetNuke.Security;
using DotNetNuke.Security.Roles;
using DotNetNuke.Common;
using DotNetNuke.Common.Utilities;
using DotNetNuke.Services.Localization;
using DotNetNuke.Entities.Portals;
using DotNetNuke.Entities.Modules.Actions;
using DotNetNuke.Services.Personalization;
using DotNetNuke.Modules.HTMLEditorProvider;
using DotNetNuke.Framework.Providers;
/* Copyright (c) 2007, Speerio, Inc. This notice may not be removed. */
namespace Speerio.DNN.Common
{
    /// <summary>
    /// Summary description for PortalTokens.
    /// </summary>
    public class PortalTokens
    {
        public const string UnknownUser = "~Public";
 
        public static string Replace(string source)
        {
            return(Replace(source, UnknownUser, null));
        }
 
        public static string Replace(string source, ModuleInfo moduleConfig)
        {
            return(Replace(source, UnknownUser, moduleConfig));
        }
 
        public static string Replace(string source, string unknownUserText, ModuleInfo moduleConfig)
        {
            PortalSettings portalSettings = (PortalSettings) HttpContext.Current.Items["PortalSettings"];
 
            // PORTALSETTINGS tokens
            source = PortalTokens.ReplacePortalTokens(source, portalSettings);
 
            // USERINFO tokens
            source = PortalTokens.ReplaceUserTokens(source, unknownUserText);
 
            // ROLEMEMBER tokens
            source = PortalTokens.ReplaceRoleTokens(source);
 
            // TABSETTINGS tokens
            source = PortalTokens.ReplaceTabTokens(source, portalSettings.ActiveTab);
 
            // MODULESETTINGS tokens
            if (moduleConfig != null)
                source = PortalTokens.ReplaceModuleTokens(source, moduleConfig);
 
            // USERPROFILE tokens
            source = PortalTokens.ReplaceProfileTokens(source);
 
            // REGEX tokens
            source = PortalTokens.ReplaceRegExTokens(source);
 
            return(source);
        }
 
        private static string ReplaceRegExTokens(string source)
        {
            // REGEX tokens
            if (source.IndexOf("<REGEX:") > -1)
            {
                int reToken = source.IndexOf ("<");
                if (reToken > -1)
                {
                    try
                    {
                        string rePair = source.Substring(reToken+1);
                        int term = rePair.IndexOf(">");
                        if (term > -1)
                        {
                            rePair = rePair.Substring(0,term);
                            string[] reParams = rePair.Split(',');
                            if (reParams.Length == 2)
                            {
                                source = source.Replace("<" + reParams[0] + "," + reParams[1] + ">","");
                                reParams[0] = reParams[0].Replace("REGEX:","");
                                source = Regex.Replace(source, reParams[0], reParams[1], RegexOptions.IgnoreCase);
                            }
                        }
                    }
                    catch
                    {
                    }
                }
            }
            return(source);
        }
                        
        private static string ReplacePortalTokens(string source, PortalSettings portalSettings)
        {
            if (source.IndexOf("[PORTALSETTINGS:") > -1)
            {
                try { source = source.Replace("[PORTALSETTINGS:PortalId]",portalSettings.PortalId.ToString()); } catch {}
                try { source = source.Replace("[PORTALSETTINGS:UploadDirectory]",portalSettings.HomeDirectory); } catch {}
                try { source = source.Replace("[PORTALSETTINGS:HomeDirectory]",portalSettings.HomeDirectory); } catch {}
                try { source = source.Replace("[PORTALSETTINGS:PortalName]",portalSettings.PortalName); } catch {}
                try { source = source.Replace("[PORTALSETTINGS:AdministratorId]",portalSettings.AdministratorId.ToString()); } catch {}
                try { source = source.Replace("[PORTALSETTINGS:AdministratorRoleId]",portalSettings.AdministratorRoleId.ToString()); } catch {}
                try { source = source.Replace("[PORTALSETTINGS:DefaultLanguage]",portalSettings.DefaultLanguage); } catch {}
            }
            return(source);
        }
 
        private static string ReplaceUserTokens(string source, string unknownUserText)
        {
            if (source.IndexOf("[USERINFO:") > -1)                        
            {
                try
                {
                    bool auth = HttpContext.Current.Request.IsAuthenticated;
                    UserInfo userInfo = null;
                    if (auth)
                        userInfo = (UserInfo) HttpContext.Current.Items["UserInfo"];
 
                    try { source = source.Replace("[USERINFO:UserID]",(auth ? userInfo.UserID.ToString() : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:FullName]",(auth ? userInfo.FirstName + " " + userInfo.LastName : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:FirstName]",(auth ? userInfo.FirstName : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:LastName]",(auth ? userInfo.LastName : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:Street]",(auth ? userInfo.Profile.Street : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:City]",(auth ? userInfo.Profile.City : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:Region]",(auth ? userInfo.Profile.Region : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:PostalCode]",(auth ? userInfo.Profile.PostalCode : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:Country]",(auth ? userInfo.Profile.Country : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:Password]",(auth ? userInfo.Membership.Password : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:Email]",(auth ? userInfo.Membership.Email : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:Unit]",(auth ? userInfo.Profile.Unit : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:Telephone]",(auth ? userInfo.Profile.Telephone : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:Username]",(auth ? userInfo.Username : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:IsSuperUser]",(auth ? userInfo.IsSuperUser.ToString() : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:AffiliateID]",(auth ? userInfo.AffiliateID.ToString() : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:Website]",(auth ? userInfo.Profile.Website : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:TimeZone]",(auth ? userInfo.Profile.TimeZone.ToString() : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:IM]",(auth ? userInfo.Profile.IM : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:Fax]",(auth ? userInfo.Profile.Fax : unknownUserText)); } catch {}
                    try { source = source.Replace("[USERINFO:Cell]",(auth ? userInfo.Profile.Cell : unknownUserText)); } catch {}
 
                }
                catch
                {
                }
            }
            return(source);
        }
 
 
        private static string ReplaceRoleTokens(string source)
        {
            string tmp = source;
 
            if (source.IndexOf("<ROLEMEMBER:") > -1)
            {
                try
                {
                    string pattern = "(?<token>\\<ROLEMEMBER:(?<roles>.*?)\\|(?<unknown>.*?)\\>)";
                    Regex r = new Regex(pattern, RegexOptions.IgnoreCase );
 
                    ArrayList tokens = new ArrayList(10);
                    ArrayList roles = new ArrayList(10);
                    ArrayList unknowns = new ArrayList(10);
                    Match m;                           
                    for (m = r.Match(source); m.Success; m = m.NextMatch())
                    {
                        tokens.Add(m.Groups["token"].ToString());
                        roles.Add(m.Groups["roles"].ToString().Trim());
                        unknowns.Add(m.Groups["unknown"].ToString().Trim());
                    }
 
                    RoleController roleController = new RoleController();
                    bool auth = HttpContext.Current.Request.IsAuthenticated;
                    UserInfo userInfo = null;
                    string[] userRoles = {};
                    if (auth)
                    {
                        userInfo = (UserInfo) HttpContext.Current.Items["UserInfo"];
                        userRoles = roleController.GetPortalRolesByUser(userInfo.UserID, userInfo.PortalID);
                        if (userRoles != null)
                        {
                            for(int u=0;u<userRoles.Length;u++)
                                userRoles[u] = userRoles[u].ToLower();
                        }
                    }
                    for(int idx=0;idx<tokens.Count;idx++)
                    {
                        string token = tokens[idx].ToString();
                        string defaultValue = unknowns[idx].ToString();
                        string[] roleList = roles[idx].ToString().Split(',');
                        string selectedRole = "";
                        for(int l=0;l<roleList.Length;l++)
                        {
                            for(int u=0;u<userRoles.Length;u++)
                            {
                                if (roleList[l].ToLower() == userRoles[u])
                                {
                                    selectedRole = roleList[l];
                                    break;
                                }
                            }
                            if (selectedRole != "")
                                break;
                        }
 
                        try { source = source.Replace(tokens[idx].ToString(), (selectedRole == "" ? defaultValue : selectedRole));  } 
                        catch {}
                    }
                    return(source);
                }
                catch
                {              
                }
            }
            return(source);
        }
 
 
        private static string ReplaceProfileTokens(string source)
        {
            string tmp = source;
 
            if (source.IndexOf("<USERPROFILE:") > -1)
            {
                try
                {
                    string pattern = "(?<token><USERPROFILE:(?<container>.*?),(?<key>.*?),(?<unknown>.*?)>)";
                    Regex r = new Regex(pattern, RegexOptions.IgnoreCase );
 
                    ArrayList tokens = new ArrayList(10);
                    ArrayList containers = new ArrayList(10);
                    ArrayList keys = new ArrayList(10);
                    ArrayList unknowns = new ArrayList(10);
                    Match m;                           
                    for (m = r.Match(source); m.Success; m = m.NextMatch())
                    {
                        tokens.Add(m.Groups["token"].ToString());
                        containers.Add(m.Groups["container"].ToString().Trim());
                        keys.Add(m.Groups["key"].ToString().Trim());
                        unknowns.Add(m.Groups["unknown"].ToString().Trim());
                    }
                    for(int idx=0;idx<tokens.Count;idx++)
                    {
                        object keyObject = Personalization.GetProfile(containers[idx].ToString(),keys[idx].ToString());
                        string keyValue = "";
                        if (keyObject != null)
                            keyValue = keyObject.ToString();
                        try { source = source.Replace(tokens[idx].ToString(), (keyValue == "" ? unknowns[idx].ToString() : keyValue));  } catch {}
                    }
                    return(source);
                }
                catch
                {              
                }
            }
            return(source);
        }
 
        private static string ReplaceTabTokens(string source, TabInfo tabSettings)
        {
            if (source.IndexOf("[TABSETTINGS:") > -1)
            {
                try
                {
                    try { source = source.Replace("[TABSETTINGS:PortalId]",tabSettings.PortalID.ToString()); } catch {}
                    try { source = source.Replace("[TABSETTINGS:TabId]",tabSettings.TabID.ToString()); } catch {}
                    try { source = source.Replace("[TABSETTINGS:TabName]",tabSettings.TabName); } catch {}
                    try { source = source.Replace("[TABSETTINGS:Title]",tabSettings.Title); } catch {}
                    try { source = source.Replace("[TABSETTINGS:AuthorizedRoles]",tabSettings.AuthorizedRoles); } catch {}
                    try { source = source.Replace("[TABSETTINGS:AdministratorRoles]",tabSettings.AdministratorRoles); } catch {}
                    try { source = source.Replace("[TABSETTINGS:ParentId]",tabSettings.ParentId.ToString()); } catch {}
                    try { source = source.Replace("[TABSETTINGS:Level]",tabSettings.Level.ToString()); } catch {}
                    try { source = source.Replace("[TABSETTINGS:Skinsource]",tabSettings.SkinPath); } catch {}
                    try { source = source.Replace("[TABSETTINGS:SkinSrc]",tabSettings.SkinSrc); } catch {}
                }
                catch
                {
                }
            }
            return(source);
        }
 
        private static string ReplaceModuleTokens(string source, ModuleInfo moduleConfig)
        {
            if (source.IndexOf("[MODULESETTINGS:") > -1)
            {
                try
                {
                    try { source = source.Replace("[MODULESETTINGS:ModuleId]",moduleConfig.ModuleID.ToString()); } catch {}
                    try { source = source.Replace("[MODULESETTINGS:TabId]",moduleConfig.TabID.ToString()); } catch {}
                    try { source = source.Replace("[MODULESETTINGS:ModuleDefId]",moduleConfig.ModuleDefID.ToString()); } catch {}
                    try { source = source.Replace("[MODULESETTINGS:ModuleOrder]",moduleConfig.ModuleOrder.ToString()); } catch {}
                    try { source = source.Replace("[MODULESETTINGS:PaneName]",moduleConfig.PaneName); } catch {}
                    try { source = source.Replace("[MODULESETTINGS:ModuleTitle]",moduleConfig.ModuleTitle); } catch {}
                    try { source = source.Replace("[MODULESETTINGS:AuthorizedEditRoles]",moduleConfig.AuthorizedEditRoles); } catch {}
                    try { source = source.Replace("[MODULESETTINGS:AuthorizedViewRoles]",moduleConfig.AuthorizedViewRoles); } catch {}
                    try { source = source.Replace("[MODULESETTINGS:ControlSrc]",moduleConfig.ControlSrc); } catch {}
                    try { source = source.Replace("[MODULESETTINGS:ControlTitle]",moduleConfig.ControlTitle); } catch {}
                    try { source = source.Replace("[MODULESETTINGS:DesktopModuleId]",moduleConfig.DesktopModuleID.ToString()); } catch {}
                    try { source = source.Replace("[MODULESETTINGS:FriendlyName]",moduleConfig.FriendlyName); } catch {}
 
                }
                catch
                {
                }
            }
            return(source);
        }
 
    }
}
#    Comments [1] - Trackback    

 Tuesday, January 30, 2007
by Nik Kalyani
Tuesday, January 30, 2007 6:50:14 PM (Pacific Standard Time, UTC-08:00)

Many businesses that develop websites for customers using DotNetNuke have to field questions about reference sites. Here is the answer —


Computer Hardware:

Manufacturing:

Software:

Pharma:

Travel/Logistics:

Education:

Financial Services:

Non-Profits:

Entertainment:

 

#    Comments [1] - Trackback    

 Monday, January 29, 2007
by Nik Kalyani
Monday, January 29, 2007 10:23:52 AM (Pacific Standard Time, UTC-08:00)

I naively assumed that the Office team dog-fooded the Outlook 2007 RSS Feeds capability before shipping the product. Based on my experience, I am inclined to believe that no testing was done on this aspect of Outlook. Here are some issues that are obvious bugs or shortcomings. Some of these are so blatant that it makes you wonder if anyone on the Office team even uses RSS on an everyday basis.

RSS Page: There is no provision (at least none I could find) for easily customizing the page you see when you click RSS Feeds in Outlook. What I expect to see is a summary of the newest items from all the feeds in my subscription list. Instead I see a useless intro to RSS and a more useless directory of various Microsoft feeds.

Adding Feeds: Adding feeds should be as simple and easy as possible. If I chance upon a good feed in my browser, I am inclined to right-click and copy the feed link. I then right-click on RSS Feeds and select “Add a new RSS feed…” which displays the “New RSS Feed” dialog. This is where the problem begins. I cannot right-click and paste the link in my clipboard. I have no idea why. Instead, I have to use Ctrl-V or Edit – Paste. This is a nuisance. I also have no ability to customize anything about the feed when I add it. To do that, I have to click “Tools,” “Account Settings…,” “RSS Feeds,” highlight feed, “Change…”… This is ridiculously complicated.

Feed Handling: This is where things go seriously wrong. I am convinced that Outlook 2007 has a built-in mechanism for arbitratily flagging feed items as read or unread with the additional capability of automatically duplicating items. This gets annoying very quickly, especially on blog feeds. It’s a simple XML feed and Outlook should be able to figure out if an item is already present and not duplicate it. The read/unread flagging is also not rocket science.

Post Display: Reading a post with images is another annoyance. Most post images are not important, but when a blogger has posted a screen-cap or a photo, one needs to be able to look at the image. Outlook will mysteriously hide the images in a post until you click on it. This is weird behavior. Once you select the option to be able to view images, you should be able to view the images. Requiring an additional click on each image cannot be an intentional feature…this has to be a bug and a pretty serious one too.

All-in-all the RSS reader in Outlook is junk. It’s one saving grace is the ability to export the list of feeds in OPML format so you can switch to different reader.

 

#    Comments [2] - Trackback    

RSS feed
Search and Links
Bling

View Nik Kalyani's profile on LinkedIn

Contact me: nik*kalyani.com (replace "*")

TechBubble
www.flickr.com
This is a Flickr badge showing public photos from techbubble. Make your own badge here.
Statistics
Total Posts: 214
This Year: 32
This Month: 0
This Week: 0
Comments: 238
About the author/Disclaimer

Disclaimer
The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

© Copyright 2008
Nik Kalyani
Sign In
All Content © 2008, Nik Kalyani