Click here to Skip to main content
15,861,168 members
Articles / Desktop Programming / WPF

GoalBook - A Hybrid Smart Client

Rate me:
Please Sign up or sign in to vote.
4.86/5 (24 votes)
25 Sep 2009CPOL10 min read 78.6K   834   69  
A WPF hybrid smart client that synchronises your goals with the Toodledo online To-do service.
//===============================================================================
// Goal Book.
// Copyright © 2009 Mark Brownsword. 
//===============================================================================

#region Using Statements
using System;
using System.IO;
using System.Net;
using System.Security.Cryptography;
using System.Text;
using System.Web;
using System.Xml;
using System.Xml.Linq;
using System.Xml.XPath;
using GoalBook.Infrastructure;
using GoalBook.Synchronisation.Properties;
#endregion

namespace GoalBook.Synchronisation.ToodleDo
{
    internal sealed class ToodledoConnector
    {        
        #region Constants and Enums

        // URL constants
        private const string GET_USERID_URL = @"http://api.toodledo.com/api.php?method=getUserid;email={0};pass={1}";
        private const string GET_TOKEN_URL = @"http://api.toodledo.com/api.php?method=getToken;userid={0};appid=GoalBook";
        private const string GET_SERVERINFO_URL = @"http://api.toodledo.com/api.php?method=getServerInfo;key={0}";
        private const string GET_ACCOUNTINFO_URL = @"http://api.toodledo.com/api.php?method=getAccountInfo;key={0}";
        
        // Server Info constants
        private const string SERVER = "server";
        private const string UNIXTIME = "unixtime";
        private const string DATE = "date";
        private const string TOKEN_EXPIRES = "tokenexpires";

        // Misc constants
        private const string MD5_FORMAT = "x2";
        private const string TOKEN = "token";
        private const string USERID = "userid";

        // Server Result constants        
        private const string ERROR = "error";

        // Account Info constants
        private const string ACCOUNT = "account";
        private const string ALIAS = "alias";
        private const string PRO = "pro";
        private const string DATE_FORMAT = "dateformat";
        private const string TIME_ZONE = "timezone";
        private const string HIDE_MONTHS = "hidemonths";
        private const string HOT_LIST_PRIORITY = "hotlistpriority";
        private const string HOT_LIST_DUEDATE = "hotlistduedate";
        private const string LAST_ADDEDIT = "lastaddedit";
        private const string LAST_DELETE = "lastdelete";
        private const string LAST_FOLDER_EDIT = "lastfolderedit";
        private const string LAST_CONTEXT_EDIT = "lastcontextedit";
        private const string LAST_GOAL_EDIT = "lastgoaledit";
        private const string LAST_NOTEBOOK_EDIT = "lastnotebookedit";

        // SyncMethod
        internal enum SyncMethod
        {
            Get,
            Post
        }
        #endregion

        #region Inner Classes and Structures
        #endregion

        #region Delegates and Events
        #endregion

        #region Instance and Shared Fields
        #endregion

        #region Constructors
        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="connectInfo">Credentials for logging into Toodledo</param>
        /// <param name="hostInfo">Proxy info for connecting to Toodledo through Proxy Server</param>
        /// <param name="tokenInfo">SyncTokenInfo saved session info</param>
        internal ToodledoConnector(Credentials connectInfo, Proxy hostInfo, SyncTokenInfo tokenInfo)
        {
            ConnectInfo = connectInfo;
            HostInfo = hostInfo;
            TokenInfo = tokenInfo;
        }
        #endregion

        #region Properties

        /// <summary>
        /// Reference to AccountInfo.
        /// </summary>
        public XDocument AccountInfo { get; private set; }

        /// <summary>
        /// Reference to ServerInfo.
        /// </summary>
        public XDocument ServerInfo { get; private set; }

        /// <summary>
        /// Reference to ConnectInfo.
        /// </summary>
        public Credentials ConnectInfo { get; private set; }

        /// <summary>
        /// Reference to HostInfo.
        /// </summary>
        public Proxy HostInfo { get; private set; }

        /// <summary>
        /// Reference to TokenInfo.
        /// </summary>
        public SyncTokenInfo TokenInfo { get; private set; }

        /// <summary>
        /// Reference to LastGoalEdit.
        /// </summary>
        public DateTime LastGoalEdit 
        {
            get { return GetLastServerEdit(LAST_GOAL_EDIT); } 
        }

        /// <summary>
        /// Reference to LastFolderEdit.
        /// </summary>
        public DateTime LastFolderEdit
        {
            get { return GetLastServerEdit(LAST_FOLDER_EDIT); } 
        }

        /// <summary>
        /// Reference to LastNoteEdit.
        /// </summary>
        public DateTime LastNoteEdit
        {
            get { return GetLastServerEdit(LAST_NOTEBOOK_EDIT); }
        }
        
        /// <summary>
        /// Reference to LastTaskEdit.
        /// </summary>
        public DateTime LastTaskEdit
        {
            get 
            {
                DateTime lastAddEdit = GetLastServerEdit(LAST_ADDEDIT);
                DateTime lastDelete = GetLastServerEdit(LAST_DELETE);

                return lastAddEdit > lastDelete ? lastAddEdit : lastDelete;
            }
        }
        #endregion

        #region Private and Protected Methods

        /// <summary>
        /// Get Last Server Edit for specified module. Adjusted for timezone.
        /// </summary>        
        private DateTime GetLastServerEdit(string module)
        {
            DateTime lastServerEdit = DateTime.MinValue;
            if (DateTime.TryParse(AccountInfo.XPathSelectElement(ACCOUNT).Element(module).Value, out lastServerEdit))
            {                
                lastServerEdit = Convert.ToDateTime(AccountInfo.XPathSelectElement(ACCOUNT).Element(module).Value).AddHours(GetUserTimeZoneOffset());
            }

            return lastServerEdit;
        }

        /// <summary>
        /// Get User TimeZone Offset.
        /// </summary>
        /// <returns>Users timezone offset</returns>
        private double GetUserTimeZoneOffset()
        {
            double timeZone = 0;
            if (double.TryParse(AccountInfo.XPathSelectElement(ACCOUNT).Element(TIME_ZONE).Value, out timeZone))
            {
                return timeZone / 2;
            }

            return timeZone;
        }

        /// <summary>
        /// Make an MD5 Hash of the supplied data.
        /// </summary>  
        /// <remarks>Credit to sharpgtd project for MD5 hashing functionality [http://code.google.com/p/sharpgtd/].</remarks>
        private string MD5(string data)
        {
            byte[] original_bytes = Encoding.ASCII.GetBytes(data);
            byte[] encoded_bytes = new MD5CryptoServiceProvider().ComputeHash(original_bytes);
            StringBuilder result = new StringBuilder();
            for (int i = 0; i < encoded_bytes.Length; i++)
            {
                result.Append(encoded_bytes[i].ToString(MD5_FORMAT));
            }

            return result.ToString();
        }

        /// <summary>
        /// Make Server Call using Http Get.
        /// </summary>
        /// <param name="url">url (parameters separated by ;)</param>
        /// <returns>XDocument</returns>
        private XDocument HttpGet(string url)
        {
            HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;

            if (HostInfo.Enabled && HostInfo.IsValid)
            {
                //Set manual HTTP Proxy configuration.
                request.Proxy = new WebProxy(HostInfo.Host, Convert.ToInt32(HostInfo.Port));
                request.Proxy.Credentials = CredentialCache.DefaultCredentials;
            }

            XDocument result = null;
            using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)            
            using (StreamReader reader = new StreamReader(response.GetResponseStream()))                
            using (XmlTextReader textReader = new XmlTextReader(reader))
            {
                // Load the response into an XDocument.
                result = XDocument.Load(textReader);
            }

            return result;
        }

        /// <summary>
        /// Make Server Call using Http Post.
        /// </summary>
        /// <param name="url">url parameter</param>
        /// <param name="parameters">parameters (separated by &)</param>
        /// <returns>XDocument</returns>
        private XDocument HttpPost(string url, string parameters)
        {            
            byte[] data = UTF8Encoding.UTF8.GetBytes(parameters);

            HttpWebRequest request = WebRequest.Create(new Uri(url)) as HttpWebRequest;
            request.Method = "POST";
            request.ContentType = "application/x-www-form-urlencoded";
            request.ContentLength = data.Length;

            if (HostInfo.Enabled && HostInfo.IsValid)
            {
                //Set manual HTTP Proxy configuration.
                request.Proxy = new WebProxy(HostInfo.Host, Convert.ToInt32(HostInfo.Port));
                request.Proxy.Credentials = CredentialCache.DefaultCredentials;
            }
            
            using (Stream stream = request.GetRequestStream())
            {
                // Write the request.
                stream.Write(data, 0, data.Length);
            }
            
            XDocument result = null;
            using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)            
            using (StreamReader reader = new StreamReader(response.GetResponseStream()))                
            using (XmlTextReader textReader = new XmlTextReader(reader))
            {
                // Load the response into an XDocument.
                result = XDocument.Load(textReader);
            }

            return result;
        }
        #endregion

        #region Public and internal Methods
        
        /// <summary>
        /// Initialise Toodledo Synchronisor.
        /// </summary>
        internal void Initialise()
        {
            //Get ServerInfo
            string serverInfoURL = string.Format(GET_SERVERINFO_URL, GetSessionKey());            
            ServerInfo = MakeServerCall(serverInfoURL);

            if (ServerInfo.Element(SERVER) != null)
            {
                //Set TokenExpires timestamp.
                TokenInfo.TokenExpires = DateTime.Now.AddMinutes(double.Parse(ServerInfo.XPathSelectElement(SERVER).Element(TOKEN_EXPIRES).Value));
            }
            else if (ServerInfo.Element(ERROR) != null)
            {
                throw new ArgumentException(string.Format(Resources.SyncExceptionMessage + " ({0})", ServerInfo.Element(ERROR).Value));
            }

            //Get AccountInfo      
            string accountInfoURL = string.Format(GET_ACCOUNTINFO_URL, GetSessionKey());            
            AccountInfo = MakeServerCall(accountInfoURL);
        }

        /// <summary>
        /// Get SessionKey.
        /// </summary>        
        internal string GetSessionKey()
        {
            if (string.IsNullOrEmpty(ConnectInfo.UserID))
            {
                //Get UserID.                
                XDocument result = MakeServerCall(string.Format(GET_USERID_URL, UrlEncodeParameterValue(ConnectInfo.Email),
                    UrlEncodeParameterValue(ConnectInfo.Password)));

                if (result.Element(USERID).Value == "1") { throw new InvalidDataException(Resources.SyncUserLookupFailedMessage); }
                ConnectInfo.UserID = result.Element(USERID).Value;
            }

            if (TokenInfo.TokenExpires == null || TokenInfo.TokenExpires <= DateTime.Now)
            {
                //Get Token.                
                XDocument result = MakeServerCall(string.Format(GET_TOKEN_URL, ConnectInfo.UserID));
                TokenInfo.Token = result.Element(TOKEN).Value;
            }

            //Create SessionKey.
            return MD5(MD5(ConnectInfo.Password) + TokenInfo.Token + ConnectInfo.UserID);
        }
        
        /// <summary>
        /// UrlEncode ParameterValue.
        /// </summary>        
        internal string UrlEncodeParameterValue(object parameter)
        {
            return HttpUtility.UrlEncode(parameter.ToString());
        }

        /// <summary>
        /// Adjust To ServerTime. Convert TO Toodledo time.
        /// </summary>
        /// <param name="parameter">DateTime parameter to convert</param>
        /// <returns>DateTime converted to server time</returns>
        internal DateTime AdjustToServerTime(DateTime parameter)
        {
            if (parameter == DateTime.MinValue)
            {
                return parameter;
            }

            return parameter.AddHours(-GetUserTimeZoneOffset());
        }

        /// <summary>
        /// Adjust From ServerTime. Convert FROM Toodledo time.
        /// </summary>
        /// <param name="parameter">DateTime parameter to convert</param>
        /// <returns>DateTime converted from server time</returns>
        internal DateTime AdjustFromServerTime(DateTime parameter)
        {
            if (parameter == DateTime.MinValue)
            {
                return parameter;
            }

            return parameter.AddHours(GetUserTimeZoneOffset());
        }
        
        /// <summary>
        /// Make ServerCall.
        /// </summary>
        internal XDocument MakeServerCall(string uri)
        {
            return this.MakeServerCall(uri, SyncMethod.Get);
        }

        /// <summary>
        /// Make ServerCall.
        /// </summary>
        /// <param name="uri">uri parameter</param>
        /// <param name="method">SyncMethod parameter</param>
        /// <returns>XDocument</returns>
        internal XDocument MakeServerCall(string uri, SyncMethod method)
        {            
            try
            {                    
                /*
                NB. There is a technological limit of 512 bytes in the whole 
                URL when using HTTP GET. While Toodledo (and other ReSTful 
                services) support uploading data using HTTP GET it is recommended
                to use HTTP POST for submitting to avoid loss of data.                                                                  
                */

                if (method == SyncMethod.Get)
                {
                    // HTTP GET.
                    return this.HttpGet(uri);
                }
                else
                {  
                    // HTTP POST.
                    string[] urlAndParams = uri.Split('?');
                    return this.HttpPost(urlAndParams[0], urlAndParams[1].Replace(';', '&'));
                }                    
            }
            catch (WebException ex)
            {
                throw new WebException(Resources.SyncExceptionMessage, ex);
            }
        }
        
        /// <summary>
        /// Finalise the Toodledo Connector.
        /// </summary>
        /// <returns>SyncTokenInfo</returns>
        internal SyncTokenInfo Finalise()
        {
            string accountInfoURL = string.Format(GET_ACCOUNTINFO_URL, GetSessionKey());
            AccountInfo = MakeServerCall(accountInfoURL);

            return TokenInfo;
        }

        #endregion

        #region Event Handlers
        #endregion

        #region Base Class Overrides
        #endregion        
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Australia Australia
I've been working as a software developer since 2000 and hold a Bachelor of Business degree from The Open Polytechnic of New Zealand. Computers are for people and I aim to build applications for people that they would want to use.

Comments and Discussions