PageRenderTime 55ms CodeModel.GetById 13ms app.highlight 34ms RepoModel.GetById 2ms app.codeStats 0ms

/BlogEngine/DotNetSlave.BusinessLogic/Web/HttpHandlers/PingbackHandler.cs

#
C# | 435 lines | 240 code | 52 blank | 143 comment | 28 complexity | d2c0bb6404f831e97d2d5e3b7cad01af MD5 | raw file
  1namespace BlogEngine.Core.Web.HttpHandlers
  2{
  3    using System;
  4    using System.ComponentModel;
  5    using System.Linq;
  6    using System.Net;
  7    using System.Text;
  8    using System.Text.RegularExpressions;
  9    using System.Web;
 10    using System.Xml;
 11
 12    /// <summary>
 13    /// Recieves pingbacks from other blogs and websites, and 
 14    ///     registers them as a comment.
 15    /// </summary>
 16    public class PingbackHandler : IHttpHandler
 17    {
 18        #region Constants and Fields
 19
 20        /// <summary>
 21        /// The success.
 22        /// </summary>
 23        private const string Success =
 24            "<methodResponse><params><param><value><string>Thanks!</string></value></param></params></methodResponse>";
 25
 26        /// <summary>
 27        /// The regex html.
 28        /// </summary>
 29        private static readonly Regex RegexHtml =
 30            new Regex(
 31                @"</?\w+((\s+\w+(\s*=\s*(?:"".*?""|'.*?'|[^'"">\s]+))?)+\s*|\s*)/?>",
 32                RegexOptions.Singleline | RegexOptions.Compiled);
 33
 34        /// <summary>
 35        /// The regex title.
 36        /// </summary>
 37        private static readonly Regex RegexTitle = new Regex(
 38            @"(?<=<title.*>)([\s\S]*)(?=</title>)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
 39
 40        /// <summary>
 41        /// Whether contains html.
 42        /// </summary>
 43        private bool containsHtml;
 44
 45        /// <summary>
 46        /// Whether source has link.
 47        /// </summary>
 48        private bool sourceHasLink;
 49
 50        /// <summary>
 51        /// The title.
 52        /// </summary>
 53        private string title;
 54
 55        #endregion
 56
 57        #region Events
 58
 59        /// <summary>
 60        ///     Occurs when a pingback is accepted as valid and added as a comment.
 61        /// </summary>
 62        public static event EventHandler<EventArgs> Accepted;
 63
 64        /// <summary>
 65        ///     Occurs when a hit is made to the trackback.axd handler.
 66        /// </summary>
 67        public static event EventHandler<CancelEventArgs> Received;
 68
 69        /// <summary>
 70        ///     Occurs when a pingback request is rejected because the sending
 71        ///     website already made a trackback or pingback to the specific page.
 72        /// </summary>
 73        public static event EventHandler<EventArgs> Rejected;
 74
 75        /// <summary>
 76        ///     Occurs when the request comes from a spammer.
 77        /// </summary>
 78        public static event EventHandler<EventArgs> Spammed;
 79
 80        #endregion
 81
 82        #region Properties
 83
 84        /// <summary>
 85        ///     Gets a value indicating whether another request can use the <see cref = "T:System.Web.IHttpHandler"></see> instance.
 86        /// </summary>
 87        /// <value></value>
 88        /// <returns>true if the <see cref = "T:System.Web.IHttpHandler"></see> instance is reusable; otherwise, false.</returns>
 89        public bool IsReusable
 90        {
 91            get
 92            {
 93                return true;
 94            }
 95        }
 96
 97        #endregion
 98
 99        #region Public Methods
100
101        /// <summary>
102        /// Called when [spammed].
103        /// </summary>
104        /// <param name="url">The URL string.</param>
105        public static void OnSpammed(string url)
106        {
107            if (Spammed != null)
108            {
109                Spammed(url, EventArgs.Empty);
110            }
111        }
112
113        #endregion
114
115        #region Implemented Interfaces
116
117        #region IHttpHandler
118
119        /// <summary>
120        /// Enables processing of HTTP Web requests by a custom HttpHandler that 
121        ///     implements the <see cref="T:System.Web.IHttpHandler"></see> interface.
122        /// </summary>
123        /// <param name="context">
124        /// An <see cref="T:System.Web.HttpContext"></see> 
125        ///     object that provides references to the intrinsic server objects 
126        ///     (for example, Request, Response, Session, and Server) used to service HTTP requests.
127        /// </param>
128        public void ProcessRequest(HttpContext context)
129        {
130            if (!BlogSettings.Instance.IsCommentsEnabled || !BlogSettings.Instance.EnablePingBackReceive)
131            {
132                context.Response.StatusCode = 404;
133                context.Response.End();
134            }
135
136            var e = new CancelEventArgs();
137            this.OnReceived(e);
138            if (e.Cancel)
139            {
140                return;
141            }
142
143            var doc = RetrieveXmlDocument(context);
144            var list = doc.SelectNodes("methodCall/params/param/value/string") ??
145                       doc.SelectNodes("methodCall/params/param/value");
146
147            if (list == null)
148            {
149                return;
150            }
151
152            var sourceUrl = list[0].InnerText.Trim();
153            var targetUrl = list[1].InnerText.Trim();
154
155            this.ExamineSourcePage(sourceUrl, targetUrl);
156            context.Response.ContentType = "text/xml";
157
158            var post = GetPostByUrl(targetUrl);
159            if (post != null)
160            {
161                if (IsFirstPingBack(post, sourceUrl))
162                {
163                    if (this.sourceHasLink && !this.containsHtml)
164                    {
165                        this.AddComment(sourceUrl, post);
166                        this.OnAccepted(sourceUrl);
167                        context.Response.Write(Success);
168                    }
169                    else if (!this.sourceHasLink)
170                    {
171                        SendError(
172                            context,
173                            17,
174                            "The source URI does not contain a link to the target URI, and so cannot be used as a source.");
175                    }
176                    else
177                    {
178                        OnSpammed(sourceUrl);
179
180                        // Don't let spammers know we exist.
181                        context.Response.StatusCode = 404;
182                    }
183                }
184                else
185                {
186                    this.OnRejected(sourceUrl);
187                    SendError(context, 48, "The pingback has already been registered.");
188                }
189            }
190            else
191            {
192                SendError(context, 32, "The specified target URI does not exist.");
193            }
194        }
195
196        #endregion
197
198        #endregion
199
200        #region Methods
201
202        /// <summary>
203        /// Called when [accepted].
204        /// </summary>
205        /// <param name="url">The URL string.</param>
206        protected virtual void OnAccepted(string url)
207        {
208            if (Accepted != null)
209            {
210                Accepted(url, EventArgs.Empty);
211            }
212        }
213
214        /// <summary>
215        /// Raises the <see cref="Received"/> event.
216        /// </summary>
217        /// <param name="e">The <see cref="System.ComponentModel.CancelEventArgs"/> instance containing the event data.</param>
218        protected virtual void OnReceived(CancelEventArgs e)
219        {
220            if (Received != null)
221            {
222                Received(null, e);
223            }
224        }
225
226        /// <summary>
227        /// Called when [rejected].
228        /// </summary>
229        /// <param name="url">The URL string.</param>
230        protected virtual void OnRejected(string url)
231        {
232            if (Rejected != null)
233            {
234                Rejected(url, EventArgs.Empty);
235            }
236        }
237
238        /// <summary>
239        /// Parse the source URL to get the domain.
240        ///     It is used to fill the Author property of the comment.
241        /// </summary>
242        /// <param name="sourceUrl">
243        /// The source Url.
244        /// </param>
245        /// <returns>
246        /// The get domain.
247        /// </returns>
248        private static string GetDomain(string sourceUrl)
249        {
250            var start = sourceUrl.IndexOf("://") + 3;
251            var stop = sourceUrl.IndexOf("/", start);
252            return sourceUrl.Substring(start, stop - start).Replace("www.", string.Empty);
253        }
254
255        /// <summary>
256        /// Retrieve the post that belongs to the target URL.
257        /// </summary>
258        /// <param name="url">The url string.</param>
259        /// <returns>The post from the url.</returns>
260        private static Post GetPostByUrl(string url)
261        {
262            var start = url.LastIndexOf("/") + 1;
263            var stop = url.ToUpperInvariant().IndexOf(".ASPX");
264            var name = url.Substring(start, stop - start).ToLowerInvariant();
265
266            return (from post in Post.Posts
267                    let legalTitle = Utils.RemoveIllegalCharacters(post.Title).ToLowerInvariant()
268                    where name == legalTitle
269                    select post).FirstOrDefault();
270        }
271
272        /// <summary>
273        /// Checks to see if the source has already pinged the target.
274        ///     If it has, there is no reason to add it again.
275        /// </summary>
276        /// <param name="post">
277        /// The post to check.
278        /// </param>
279        /// <param name="sourceUrl">
280        /// The source Url.
281        /// </param>
282        /// <returns>
283        /// The is first ping back.
284        /// </returns>
285        private static bool IsFirstPingBack(Post post, string sourceUrl)
286        {
287            foreach (var comment in post.Comments)
288            {
289                if (comment.Website != null &&
290                    comment.Website.ToString().Equals(sourceUrl, StringComparison.OrdinalIgnoreCase))
291                {
292                    return false;
293                }
294
295                if (comment.IP != null && comment.IP == HttpContext.Current.Request.UserHostAddress)
296                {
297                    return false;
298                }
299            }
300
301            return true;
302        }
303
304        /// <summary>
305        /// Retrieves the content of the input stream
306        ///     and return it as plain text.
307        /// </summary>
308        /// <param name="context">
309        /// The context.
310        /// </param>
311        /// <returns>
312        /// The parse request.
313        /// </returns>
314        private static string ParseRequest(HttpContext context)
315        {
316            var buffer = new byte[context.Request.InputStream.Length];
317            context.Request.InputStream.Read(buffer, 0, buffer.Length);
318
319            return Encoding.Default.GetString(buffer);
320        }
321
322        /// <summary>
323        /// The retrieve xml document.
324        /// </summary>
325        /// <param name="context">
326        /// The context.
327        /// </param>
328        /// <returns>
329        /// An Xml Document.
330        /// </returns>
331        private static XmlDocument RetrieveXmlDocument(HttpContext context)
332        {
333            var xml = ParseRequest(context);
334            if (!xml.Contains("<methodName>pingback.ping</methodName>"))
335            {
336                context.Response.StatusCode = 404;
337                context.Response.End();
338            }
339
340            var doc = new XmlDocument();
341            doc.LoadXml(xml);
342            return doc;
343        }
344
345        /// <summary>
346        /// The send error.
347        /// </summary>
348        /// <param name="context">
349        /// The context.
350        /// </param>
351        /// <param name="code">
352        /// The code number.
353        /// </param>
354        /// <param name="message">
355        /// The message.
356        /// </param>
357        private static void SendError(HttpContext context, int code, string message)
358        {
359            var sb = new StringBuilder();
360            sb.Append("<?xml version=\"1.0\"?>");
361            sb.Append("<methodResponse>");
362            sb.Append("<fault>");
363            sb.Append("<value>");
364            sb.Append("<struct>");
365            sb.Append("<member>");
366            sb.Append("<name>faultCode</name>");
367            sb.AppendFormat("<value><int>{0}</int></value>", code);
368            sb.Append("</member>");
369            sb.Append("<member>");
370            sb.Append("<name>faultString</name>");
371            sb.AppendFormat("<value><string>{0}</string></value>", message);
372            sb.Append("</member>");
373            sb.Append("</struct>");
374            sb.Append("</value>");
375            sb.Append("</fault>");
376            sb.Append("</methodResponse>");
377
378            context.Response.Write(sb.ToString());
379        }
380
381        /// <summary>
382        /// Insert the pingback as a comment on the post.
383        /// </summary>
384        /// <param name="sourceUrl">
385        /// The source Url.
386        /// </param>
387        /// <param name="post">
388        /// The post to add the comment to.
389        /// </param>
390        private void AddComment(string sourceUrl, Post post)
391        {
392            var comment = new Comment
393                {
394                    Id = Guid.NewGuid(),
395                    Author = GetDomain(sourceUrl),
396                    Website = new Uri(sourceUrl)
397                };
398            comment.Content = string.Format("Pingback from {0}{1}{2}{3}", comment.Author, Environment.NewLine, Environment.NewLine, this.title);
399            comment.DateCreated = DateTime.Now;
400            comment.Email = "pingback";
401            comment.IP = HttpContext.Current.Request.UserHostAddress;
402            comment.Parent = post;
403            comment.IsApproved = true; // NOTE: Pingback comments are approved by default.
404            post.AddComment(comment);
405        }
406
407        /// <summary>
408        /// Parse the HTML of the source page.
409        /// </summary>
410        /// <param name="sourceUrl">
411        /// The source Url.
412        /// </param>
413        /// <param name="targetUrl">
414        /// The target Url.
415        /// </param>
416        private void ExamineSourcePage(string sourceUrl, string targetUrl)
417        {
418            try
419            {
420
421                var remoteFile = new RemoteFile(new Uri(sourceUrl), true);
422                var html = remoteFile.GetFileAsString();
423                this.title = RegexTitle.Match(html).Value.Trim();
424                this.containsHtml = RegexHtml.IsMatch(this.title);
425                this.sourceHasLink = html.ToUpperInvariant().Contains(targetUrl.ToUpperInvariant());
426            }
427            catch (WebException)
428            {
429                this.sourceHasLink = false;
430            }
431        }
432
433        #endregion
434    }
435}