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