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


using System;
using System.Collections;
using System.Collections.Specialized;
using System.Text.RegularExpressions;
using System.Web;
using System.Xml;
using CommunityServer;
using CommunityServer.Blogs.Components;
using CommunityServer.Components;
using CookComputing.XmlRpc;

namespace CommunityServer.Blogs.Components
{

	/// <summary>
	/// PingBack Interface
	/// See: http://www.hixie.ch/specs/pingback/pingback
	/// See: http://ln.hixie.ch/?start=1033171507&count=1
	/// </summary>
	public interface IPingBack
	{
		[XmlRpcMethod("pingback.ping",Description="Notifies the server that a link has been added to sourceURI, pointing to targetURI.")] 
		[return: XmlRpcReturnValue( Description="A Message String" )]
		string Ping(string sourceURI, string targetURI);
	}

	/// <summary>
	/// PingBack Service that receives and responds to XML-RPC PingBack requests.
	/// </summary>
	public class PingBack : XmlRpcService, IPingBack
	{

		#region Error Codes

			// Unknown error
			private readonly int errorCode_General = 0;

			// The source URI does not exis
			private readonly int errorCode_SourceURIDoesNotExist = 16;  

			// The source URI does not contain a link to the target URI, and so cannot be used as a source.
			private readonly int errorCode_SourceDoesNotContainTarget = 17;

			// The specified target URI does not exist
			private readonly int errorCode_TargetURIDoesNotExist = 32;

			// The specified target URI cannot be used as a PingBack target.
			private readonly int errorCode_TargetURIInvalid = 33;

			// The pingback has already been registered
			private readonly int errorCode_DuplicatePingBack = 48;

			// Access denied
			private readonly int errorCode_AccessDenied = 49;
		
		#endregion

		#region Constructor
		public PingBack()
		{
		}
		#endregion

		#region IPingBack Members

		/// <summary>
		/// Processes a PingBack request 
		/// </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>
		public string Ping(string sourceURI, string targetURI) 
		{ 

			try
			{
				// Attempt to create the PingBack and save it to the database
				CreatePingBack(sourceURI, targetURI);
			}
			catch (XmlRpcFaultException XmlRpcEx)
			{
				string message = String.Format("Pingback request received by Community Server but failed due to one or more reasons. SourceURI was [{0}] and TargetURI was [{1}]. Error message returned was: {2}.", sourceURI, targetURI, XmlRpcEx.FaultString);
				EventLogs.Warn(message, "Weblogs", 302, CSContext.Current.SettingsID);

				// If a XmlRpcFaultException was thrown then it was done deliberately
				//	Just rethrow it and the CookComputing.XmlRpc library will catch it and
				//	convert it into a XML-RPC Fault Code response.
				throw XmlRpcEx;
			}
			catch (System.Exception ex)
			{
				string message = String.Format("Pingback request received by Community Server but failed due to one or more reasons. SourceURI was [{0}] and TargetURI was [{1}]. Error message returned was: {2}.", sourceURI, targetURI, ex.Message);
				EventLogs.Warn(message, "Weblogs", 302, CSContext.Current.SettingsID);

				// If any other exception was thrown then something unexpected happened.
				// Return a General error XML-RPC Fault Code to the client since we don't know exactly what went wrong.
				throw new XmlRpcFaultException(errorCode_General, "Unknown error occurred while processing Pingback."); 
			}

			return "Thanks for the Pingback!";
		} 


		#endregion

		#region Private Helper Methods

		private void CreatePingBack(string sourceURI, string targetURI)
		{
			CSContext cntx = CSContext.Current;
			User user = cntx.User;

			// Check Parameters
			if (Globals.IsNullorEmpty(sourceURI))
			{
				throw new XmlRpcFaultException(errorCode_SourceURIDoesNotExist, "No source URI parameter found, please try harder!"); 
			}
			if (Globals.IsNullorEmpty(targetURI))
			{
				throw new XmlRpcFaultException(errorCode_TargetURIDoesNotExist, "The target URI does not exist!"); 
			}

			// Retrieve referenced weblog post
			WeblogPost trackedEntry = null;
			try
			{
				trackedEntry = GetPostFromUrl(targetURI);
			}
			catch
			{
				throw new XmlRpcFaultException(errorCode_TargetURIInvalid, "The target URI is invalid."); 
			}
			if(trackedEntry == null)
			{
				throw new XmlRpcFaultException(errorCode_TargetURIInvalid, "The target URI is invalid."); 
			}

			// Check weblog permissions and configuration                
			Weblog wl = trackedEntry.Section as Weblog;

			try
			{
				Permissions.AccessCheck(wl,Permission.View, user );
			}
			catch
			{
				throw new XmlRpcFaultException(errorCode_AccessDenied, "Access Denied."); 
			}

			if(!wl.ValidateTrackBacks(trackedEntry))
			{
				throw new XmlRpcFaultException(errorCode_AccessDenied, "Pingbacks are not enabled."); 
			}

			// Check if this is a duplicate Trackback (or PingBack)
			if(!IsNewTrackBack(trackedEntry, sourceURI))
			{
				throw new XmlRpcFaultException(errorCode_DuplicatePingBack, "A Trackback or PingbBack for this source URI already exists."); 
			}

			// Retrieve the source document and check if it actually contains a link to the target
			string pageTitle = null;
			if (!SourceContainsTarget(sourceURI, Globals.FullPath(BlogUrls.Instance().Post(trackedEntry, wl)), out pageTitle))
			{
				throw new XmlRpcFaultException(errorCode_SourceDoesNotContainTarget, "Sorry couldn't find a relevant link in " + sourceURI ); 
			}

			// Create the TrackBack item
			WeblogPost entry = new WeblogPost();
			entry.BlogPostType = BlogPostType.Trackback;
			entry.SectionID = wl.SectionID;
			entry.ParentID = trackedEntry.PostID;
			entry.Subject = pageTitle;
			entry.TitleUrl = sourceURI;
			entry.TrackBackName = pageTitle;
			entry.Body = ResourceManager.GetString("Weblog_PingBack_From") + sourceURI;
			entry.IsApproved = true;
			entry.PostDate = DateTime.Now;
			entry.UserTime = DateTime.Now;
			
			// Attempt to save to the database
			int postID;
			BlogPostResults status = WeblogPosts.Add(entry, user, out postID);

			if (status != BlogPostResults.Success)
			{
				throw new XmlRpcFaultException(errorCode_General, "Unknown error occurred while processing Pingback."); 
			}

			// Log success message to EventLog
			string message = String.Format("Pingback request received by Community Server and successfully saved to PostID {0}. SourceURI was [{1}] and TargetURI was [{2}].", trackedEntry.PostID, sourceURI, targetURI);
			EventLogs.Warn(message, "Weblogs", 302, cntx.SettingsID);

		}

		
		private bool SourceContainsTarget(string sourceURI, string targetURI, out string pageTitle)
		{
			pageTitle = string.Empty ;
			string page = CSRequest.GetPageText(sourceURI, targetURI);
			if (page == null ||  page.IndexOf(targetURI) < 0 )
				return false ;
			
			string pat = @"<head.*?>.*<title.*?>(.*)</title.*?>.*</head.*?>" ;
			Regex reg = new Regex(pat, RegexOptions.IgnoreCase | RegexOptions.Singleline  ) ;
			Match m = reg.Match(page) ;
			if ( m.Success )
			{
				pageTitle = m.Result("$1") ;
				return true ;
			}
 
			return false ;
		}

	
		private bool IsNewTrackBack(WeblogPost post, string trackbackUrl)
		{
			BlogThreadQuery query = new BlogThreadQuery();
			query.SectionID = post.SectionID;
			query.IncludeCategories = false;
			query.PostID = post.PostID;
			query.ReturnFullThread = true;
			PostSet ps = WeblogPosts.GetPosts(query,false);
			ps.Organize();

			foreach(WeblogPost p in ps.Replies)
			{
				if ( (string.Compare(trackbackUrl, p.TitleUrl, true) == 0) && (p.BlogPostType == BlogPostType.Trackback) )
					return false;
			}

			return true;
		}

		private static NameValueCollection GetQueryStringCollection(string queryString)
		{
			NameValueCollection qsc = new NameValueCollection();
			
			if (queryString.StartsWith("?"))
				queryString = queryString.Substring(1);
			
			string[] qs = queryString.Split('&');
			if (qs != null)
			{
				foreach (string item in qs)
				{
					if (item.IndexOf('=') != -1)
					{
						string key = item.Split('=')[0];
						string val = item.Split('=')[1];
						
						if (!Globals.IsNullorEmpty(key) && !Globals.IsNullorEmpty(val))
							qsc.Add(key.Trim(), val.Trim());
					}
				}
			}
			
			return qsc;
		}

 

		private WeblogPost GetPostFromUrl(string url)
		{
			// Rewrite URL to determine what blog post the URL points to
			Uri origUri = new Uri(url);
			string newPath =  UrlReWriteProvider.Instance().GetRewrittenUrl(origUri.AbsolutePath, origUri.Query);
			if(newPath != null)
			{
				// Get a collection of name/value pairs in the QueryString
				newPath = Globals.FullPath(newPath.Trim()).ToLower();
				NameValueCollection qs = GetQueryStringCollection(new Uri(newPath).Query);
				
				if (qs != null && qs.Count > 0)
				{
					int postID = -1;
					if (!Globals.IsNullorEmpty(qs["postid"]))
						postID = Int32.Parse(qs["postid"]);
					
					string postName = null;
					if (!Globals.IsNullorEmpty(qs["postname"]))
						postName = Globals.UrlDecodeFileComponent(qs["postname"]);

					string appKey = null;
					if (!Globals.IsNullorEmpty(qs["app"]))
						appKey = qs["app"];

					// Was a PostID found
					if (postID > 0)
						return WeblogPosts.GetPost(postID, false, true, false);

					// If not, was an AppKey and PostName found
					if( !Globals.IsNullorEmpty(appKey) && !Globals.IsNullorEmpty(postName) )
					{
						
						Weblog blog = Weblogs.GetWeblog(appKey, false, false);
						if (blog != null)
						{
							BlogThreadQuery query = BlogThreadQuery.CreateNewSingleBlogQuery(blog.SectionID);
							query.PostName = postName;
							query.IncludeCategories = false;
							query.ReturnFullThread = false;
							PostSet ps = WeblogPosts.GetPosts(query, false);

							if (ps.Posts != null && ps.Posts.Count > 0)
								return ps.Posts[0] as WeblogPost;
						}
					}
					
				}
			}

			return null;
		}


		#endregion

	}
}
