//------------------------------------------------------------------------------
// <copyright company="Telligent Systems">
//     Copyright (c) Telligent Systems Corporation.  All rights reserved.
// </copyright> 
//------------------------------------------------------------------------------

using System;
using System.Collections.Specialized;
using System.IO;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using CommunityServer.Blogs.Components;
using CommunityServer.Components;
using CommunityServer.Configuration;
using CookComputing.XmlRpc;

namespace CommunityServer.Blogs.Components
{
    /// <summary>
    /// Summary description for TrackBackNotificationProxy.
    /// </summary>
    public class TrackBackNotificationProxy
    {
        string body, title, link, blogname, description, hostPath;
		int postID;
		private int settingsID;
		private static readonly Regex trackBackRDFRegex = new Regex(@"<rdf:\w+\s[^>]*?>(</rdf:rdf>)?", RegexOptions.IgnoreCase|RegexOptions.Compiled);
		private static readonly Regex trackBackPingRegex = new Regex("trackback:ping=\"([^\"]+)\"", RegexOptions.IgnoreCase|RegexOptions.Compiled);


        /// <summary>
        /// Creates a new instance of TrackBackNotificationProxy setting the values needed to complete all the TrackBacks and PingBacks
        /// </summary>
        public TrackBackNotificationProxy(int settingsID, string body, string title, string link, string blogname, string description, int postID, string hostPath)
        {
			this.settingsID = settingsID;
			this.body = body;
			this.title = title;
			this.link = link;
			this.blogname = blogname;
			this.description = description;
			this.postID = postID;
			this.hostPath = hostPath;
		}


		/// <summary>
        /// Creates a new instance of TrackBackNotificationProxy and determines if the trackbacks should happen on a second thread
        /// (Managed ThreadPool)
        /// </summary>
        public static void Track(WeblogPost post, bool backGroundThread)
        {
            Weblog wl = post.Section as Weblog;
            if(wl != null && wl.ValidateTrackBacks(post))
            {
                string desc = post.HasExcerpt ? post.Excerpt : Formatter.RemoveHtml(post.FormattedBody,100);
            	CSContext cntx = CSContext.Current;
            	TrackBackNotificationProxy tbnp = new TrackBackNotificationProxy(cntx.SettingsID, post.FormattedBody, post.Subject, Globals.FullPath(BlogUrls.Instance().Post(post,wl)), wl.Name, desc, post.PostID, cntx.HostPath);

                if(backGroundThread)
                    ManagedThreadPool.QueueUserWorkItem(new WaitCallback(tbnp.Track));
                else
                    tbnp.Track();
            }

        }


        /// <summary>
        /// Simply calls this.Track();
        /// </summary>
        public void Track(object state)
        {
			// Create new Context
			CSContext.Create(this.settingsID);

            Track();
        }

        /// <summary>
        /// Creates an array of links based on the post's body. Then walks through each link and attempts to "TrackBack"
        /// </summary>
        public void Track()
        {
            StringCollection links = GetLinks(body);
            for(int i = 0; i < links.Count; i++)
            {
				string externalUrl = links[i];
                try
                {
					HttpWebResponse response = CSRequest.GetResponse(externalUrl, link);
					WebHeaderCollection headers = response.Headers;
					string pageText = CSRequest.GetPageText(response);

					if(pageText != null)
                        TryToPing(headers, pageText, externalUrl);
                }
				catch (System.Exception ex)
				{
					if (externalUrl == null)
						externalUrl = "null";

					string message = String.Format("Trackback/Pingback attempt to the url [{0}] failed for PostID {1} while retrieving the remote document. Error message returned was: {2}.", externalUrl, this.postID, ex.Message);
					EventLogs.Warn(message, "Weblogs", 301, CSContext.Current.SettingsID);
				}
			}

        }

        /// <summary>
        /// Attempts to make a TrackBack or PingBack request to the supplied Url. The page is requested and then search for TrackBack and PingBack links
        /// </summary>
        /// <param name="pageText">Full Text (HTML) of the page to search</param>
        /// <param name="externalUrl">Url to ping</param>
        /// <returns></returns>
        public bool TryToPing(WebHeaderCollection headers, string pageText, string externalUrl)
        {
        	CSContext cntx = CSContext.Current;
        	// First try to send a TrackBack if the required RDF info was embedded in the HTML
			try
			{
				string trackBackItem = GetTrackBackText(pageText,externalUrl,link);
				if (!Globals.IsNullorEmpty(trackBackItem))
				{
					if(!trackBackItem.ToLower().StartsWith("http://") && !trackBackItem.ToLower().StartsWith("https://"))
						trackBackItem = "http://" + trackBackItem;
				
					SendTrackBackPing(trackBackItem);
					
					string message = String.Format("Trackback sent to {0} for PostID {1}.", externalUrl, this.postID);
					EventLogs.Info(message, "Weblogs", 301, cntx.SettingsID);

					return true;
				}
			}
			catch (System.Exception ex)
			{
				string message = String.Format("Trackback attempt to the url [{0}] failed for PostID {1}. Error message returned was: {2}.", externalUrl, this.postID, ex.Message);
				EventLogs.Warn(message, "Weblogs", 301, cntx.SettingsID);
			}

			// Now try to send a PingBack if the required PingBack XML-RPC service URL exists in the HTTP Header or HTML Head
			try
			{
				string PingBackServerURI = GetPingBackServerURI(headers, pageText);
				if (!Globals.IsNullorEmpty(PingBackServerURI))
				{
					SendPingBackPing(PingBackServerURI.Trim(), externalUrl);

					string message = String.Format("Pingback sent to {0} for PostID {1}.", externalUrl, this.postID);
					EventLogs.Info(message, "Weblogs", 303, cntx.SettingsID);

					return true;
				}
			}
			catch (CookComputing.XmlRpc.XmlRpcException ex)
			{
				string message = String.Format("Pingback attempt to the url [{0}] failed for PostID {1}. Error message returned was: {2}.", externalUrl, this.postID, ex.Message);
				EventLogs.Warn(message, "Weblogs", 303, cntx.SettingsID);
			}
			catch (System.Exception ex)
			{
				string message = String.Format("Pingback attempt to the url [{0}] failed for PostID {1}. Error message returned was: {2}.", externalUrl, this.postID, ex.Message);
				EventLogs.Warn(message, "Weblogs", 303, cntx.SettingsID);
			}

            return false;
        }

        /// <summary>
        /// If the current site/link supports a TrackBack, we will ping the site here
        /// </summary>
        /// <param name="trackBackItem"></param>
        protected virtual void SendTrackBackPing(string trackBackItem)
        {
			string parameters = "title=" + Globals.UrlEncode(Globals.HtmlDecode(title)) + "&url=" + Globals.UrlEncode(link) + "&blog_name=" + Globals.UrlEncode(Globals.HtmlDecode(blogname)) + "&excerpt=" + Globals.UrlEncode(description);
			byte[] payload = Encoding.UTF8.GetBytes(parameters);

            HttpWebRequest request = CSRequest.CreateRequest(trackBackItem,link);
            request.Method = "POST";
            request.ContentLength = payload.Length;
            request.ContentType = "application/x-www-form-urlencoded";
            request.KeepAlive = false;
			
            using(Stream st = request.GetRequestStream())
            {
                st.Write(payload, 0, payload.Length);
                st.Close();
				
                using(WebResponse response = request.GetResponse())
                {
                    response.Close();
                }
            }
        }

		/// <summary>
		/// If the current site/link supports a PingBack, we will ping the site here
		/// </summary>
		protected virtual void SendPingBackPing(string PingBackServerURI, string externalUrl)
		{
			PingBackClientProxy pingBackClient = new PingBackClientProxy(PingBackServerURI);
			pingBackClient.Ping(link, externalUrl);
		}

        #region Helpers

        private static string GetTrackBackText(string pageText, string url, string PostUrl)
        {
            if(!Regex.IsMatch(pageText,PostUrl,RegexOptions.IgnoreCase|RegexOptions.Singleline))
            {
                Match m;
			
                for (m = trackBackRDFRegex.Match(pageText); m.Success; m = m.NextMatch()) 
                {
                    if(m.Groups.ToString().Length > 0)
                    {
					
                        string text = m.Groups[0].ToString();
                        if(text.IndexOf(url) > 0)
                        {
                            Match m2 = trackBackPingRegex.Match(text) ;
                            if ( m2.Success )
                            {
                                return m2.Result("$1") ;
                            }

                            return text;
                        }
                    }
                }
            }

            return null;	
        }


		private static string GetPingBackServerURI(WebHeaderCollection headers, string pageText)
		{
			string PingBackServerURI = null;

			// First look for the X-Pingback HTTP Header
			if (headers != null && headers.HasKeys())
			{
				foreach (string key in headers.Keys)
				{
					if (key.ToLower().Trim() == "x-pingback")
					{
						PingBackServerURI = headers[key];
						continue;
					}
				}
			}

			// If the HTTP header was not found, look for the PingBack <Link> element in the body
			if (Globals.IsNullorEmpty(PingBackServerURI) && !Globals.IsNullorEmpty(pageText))
			{
				Match m = trackBackPingRegex.Match(pageText) ;
				if ( m.Success )
				{
					PingBackServerURI = m.Result("$1");
				}
			}

			return PingBackServerURI;
		}

        /// <summary>
        /// Gets a list of all of the valid html links from a string
        /// </summary>
        /// <param name="text"></param>
        /// <returns></returns>
        public StringCollection GetLinks(string text)
        {
			// Convert any relative links in the body to fully qualified URLS
			text = Formatter.ConvertLocalUrls(text, hostPath);

			StringCollection links = new StringCollection();
            string sPattern = @"(?:[hH][rR][eE][fF]\s*=)" +
                @"(?:[\s""']*)(?!#|[Mm]ailto|[lL]ocation.|[jJ]avascript|.*css|.*this\.)" +
                @"(.*?)(?:[\s>""'])";

            Regex r = new Regex(sPattern,RegexOptions.IgnoreCase);
            Match m;
            string link = null;
            for (m = r.Match(text); m.Success; m = m.NextMatch()) 
            {
				
                if(m.Groups.ToString().Length > 0 )
                {
					
                    link = 	m.Groups[1].ToString();	
                    if(!links.Contains(link))
                    {
                        links.Add(link);
                    }
                }
            }
            return links;	
        }

        #endregion
    }


	/// <summary>
	/// PingBack XML-RPC client proxy
	/// </summary>
	public class PingBackClientProxy : XmlRpcClientProtocol
	{
		public PingBackClientProxy(string remoteServerURI)
		{
			this.UserAgent = CSRequest.UserAgent;
			this.Timeout = 60000;
			this.Url = remoteServerURI;

			// This improves compatibility with XML-RPC servers that do not fully comply with the XML-RPC specification.
			this.NonStandard = XmlRpcNonStandard.All;

			// Use a proxy server if one has been configured
			CSConfiguration config = CSConfiguration.GetConfig();
			if (config.ProxyHost != string.Empty)
			{
				WebProxy proxy = new WebProxy(config.ProxyHost, config.ProxyPort);

				if (config.ProxyBypassList != string.Empty)
					proxy.BypassList = config.ProxyBypassList.Split(',');
				proxy.BypassProxyOnLocal = config.ProxyBypassOnLocal;

				if (config.ProxyUsername != string.Empty)
					proxy.Credentials = new NetworkCredential(config.ProxyUsername, config.ProxyPassword);

				this.Proxy = proxy;
			}
		}

		/// <summary>
		/// Sends a PingBack request using XML-RPC 
		/// </summary>
		/// <param name="sourceURI">The absolute URI of the post on the source page containing the link to the target site.</param>
		/// <param name="targetURI">The absolute URI of the target of the link, as given on the source page.</param>
		[XmlRpcMethod("pingback.ping",Description="Notifies the server that a link has been added to sourceURI, pointing to targetURI.")] 
		[return: XmlRpcReturnValue( Description="A Message String" )]
		public string Ping(string sourceURI, string targetURI)
		{
			return (string) Invoke("Ping", new object[] {sourceURI, targetURI});
		}

	}

}

