Click here to Skip to main content
Click here to Skip to main content
Add your own
alternative version

GoalBook - A Hybrid Smart Client

, 25 Sep 2009
A WPF hybrid smart client that synchronises your goals with the Toodledo online To-do service.
Prize winner in Competition "Smart Client Article Contest"
GoalBook.zip
GoalBook
Dependencies
CAG WPF 2
Microsoft.Practices.Composite.dll
Microsoft.Practices.Composite.Presentation.dll
Microsoft.Practices.Composite.UnityExtensions.dll
Microsoft.Practices.ObjectBuilder2.dll
Microsoft.Practices.ServiceLocation.dll
Microsoft.Practices.Unity.dll
vssver2.scc
CSLA
Csla.dll
Csla.XmlSerializers.dll
vssver2.scc
Infragistics
Infragistics3.Wpf.DataPresenter.v9.1.Express.dll
Infragistics3.Wpf.Editors.v9.1.Express.dll
Infragistics3.Wpf.v9.1.Express.dll
vssver2.scc
WPFToolkit
vssver2.scc
WPFToolkit.dll
GoalBook.Controls
CheckedList
vssver2.scc
Properties
vssver2.scc
vssver2.scc
GoalBook.Goals
GoalBook.Goals.csproj.user
Properties
vssver2.scc
Views
vssver2.scc
vssver2.scc
GoalBook.Infrastructure
Comparers
vssver2.scc
Constants
vssver2.scc
Controls
Converters
vssver2.scc
Enums
vssver2.scc
Events
vssver2.scc
GoalBook.Infrastructure.csproj.user
Helpers
vssver2.scc
Interfaces
vssver2.scc
ObjectModel
vssver2.scc
Printing
vssver2.scc
Properties
vssver2.scc
vssver2.scc
GoalBook.Notes
GoalBook.Notes.csproj.user
Properties
vssver2.scc
Resources
arrow_redo.png
arrow_undo.png
cut.png
page_copy.png
paste_plain.png
text_bold.png
text_indent.png
text_indent_remove.png
text_italic.png
text_list_bullets.png
text_list_numbers.png
vssver2.scc
world_link.png
Views
vssver2.scc
vssver2.scc
GoalBook.Public
Encryption
vssver2.scc
HtmlConverter
vssver2.scc
HtmlParser
vssver2.scc
Misc
vssver2.scc
Properties
vssver2.scc
vssver2.scc
GoalBook.Shell
App.ico
Commands
vssver2.scc
GoalBook.Shell.csproj.user
Journal.ico
Journal48.ico
Misc
vssver2.scc
Modules
vssver2.scc
Properties
licenses.licx
vssver2.scc
Resources
arrow_refresh.png
arrow_undo.png
checked.gif
cross.png
disk.png
email.png
erase.png
error.png
exclamation.png
flag_red.png
group.png
help.png
information.png
magnifier.png
page_red.png
pencil.png
printer.png
user.png
vssver2.scc
Wave.jpg
world.png
world_link.png
Services
vssver2.scc
Splash.png
Views
vssver2.scc
vssver2.scc
Windows
vssver2.scc
GoalBook.snk
GoalBook.Synchronisation
Events
vssver2.scc
Properties
vssver2.scc
ToodleDo
vssver2.scc
vssver2.scc
GoalBook.Tasks
Controls
vssver2.scc
Properties
vssver2.scc
Views
vssver2.scc
vssver2.scc
vssver2.scc
//===============================================================================
// 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)

About the Author

Mark Brownsword
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.

| Advertise | Privacy | Mobile
Web02 | 2.8.140721.1 | Last Updated 25 Sep 2009
Article Copyright 2009 by Mark Brownsword
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid