PageRenderTime 35ms CodeModel.GetById 1ms app.highlight 26ms RepoModel.GetById 1ms app.codeStats 0ms

/BlogEngine/BlogEngine.NET/App_Code/Extensions/AkismetFilter.cs

#
C# | 606 lines | 303 code | 94 blank | 209 comment | 24 complexity | 4daae7c39f1a83ca4a4510055c44ef2d MD5 | raw file
  1/* Author:      Joel Thoms (http://joel.net)
  2 * Copyright:   2006 Joel Thoms (http://joel.net)
  3 * About:       Akismet (http://akismet.com) .Net 2.0 API allow you to check
  4 *              Akismet's spam database to verify your comments and prevent spam.
  5 * 
  6 * Note:        Do not launch 'DEBUG' code on your site.  Only build 'RELEASE' for your site.  Debug code contains
  7 *              Console.WriteLine's, which are not desireable on a website.
  8*/
  9
 10namespace Joel.Net
 11{
 12    using System;
 13    using System.IO;
 14    using System.Net;
 15    using System.Web;
 16    
 17    #region - public class AkismetComment -
 18
 19    /// <summary>
 20    /// The akismet comment.
 21    /// </summary>
 22    public class AkismetComment
 23    {
 24        #region Constants and Fields
 25
 26        /// <summary>
 27        ///     The blog.
 28        /// </summary>
 29        public string Blog { get; set; }
 30
 31        /// <summary>
 32        ///     The comment author.
 33        /// </summary>
 34        public string CommentAuthor { get; set; }
 35
 36        /// <summary>
 37        ///     The comment author email.
 38        /// </summary>
 39        public string CommentAuthorEmail { get; set; }
 40
 41        /// <summary>
 42        ///     The comment author url.
 43        /// </summary>
 44        public string CommentAuthorUrl { get; set; }
 45
 46        /// <summary>
 47        ///     The comment content.
 48        /// </summary>
 49        public string CommentContent { get; set; }
 50
 51        /// <summary>
 52        ///     The comment type.
 53        /// </summary>
 54        public string CommentType { get; set; }
 55
 56        /// <summary>
 57        ///     The permalink.
 58        /// </summary>
 59        public string Permalink { get; set; }
 60
 61        /// <summary>
 62        ///     The referrer.
 63        /// </summary>
 64        public string Referrer { get; set; }
 65
 66        /// <summary>
 67        ///     The user agent.
 68        /// </summary>
 69        public string UserAgent { get; set; }
 70
 71        /// <summary>
 72        ///     The user ip.
 73        /// </summary>
 74        public string UserIp { get; set; }
 75
 76        #endregion
 77    }
 78
 79    #endregion
 80
 81    /// <summary>
 82    /// The akismet.
 83    /// </summary>
 84    public class Akismet
 85    {
 86        #region Constants and Fields
 87
 88        /// <summary>
 89        ///     The api key.
 90        /// </summary>
 91        private readonly string apiKey;
 92
 93        /// <summary>
 94        ///     The blog string.
 95        /// </summary>
 96        private readonly string blog;
 97
 98        /// <summary>
 99        ///     The comment check url.
100        /// </summary>
101        private readonly string commentCheckUrl = "http://{0}.rest.akismet.com/1.1/comment-check";
102
103        /// <summary>
104        ///     The submit ham url.
105        /// </summary>
106        private readonly string submitHamUrl = "http://{0}.rest.akismet.com/1.1/submit-ham";
107
108        /// <summary>
109        ///     The submit spam url.
110        /// </summary>
111        private readonly string submitSpamUrl = "http://{0}.rest.akismet.com/1.1/submit-spam";
112
113        /// <summary>
114        ///     The user agent.
115        /// </summary>
116        private readonly string userAgent = "Joel.Net's Akismet API/1.0";
117
118        /// <summary>
119        ///     The verify url.
120        /// </summary>
121        private readonly string verifyUrl = "http://rest.akismet.com/1.1/verify-key";
122
123        #endregion
124
125        #region Constructors and Destructors
126
127        /// <summary>
128        /// Initializes a new instance of the <see cref="Akismet"/> class. 
129        ///     Creates an Akismet API object.
130        /// </summary>
131        /// <param name="apiKey">
132        /// Your wordpress.com API key.
133        /// </param>
134        /// <param name="blog">
135        /// URL to your blog
136        /// </param>
137        /// <param name="userAgent">
138        /// Name of application using API.  example: "Joel.Net's Akismet API/1.0"
139        /// </param>
140        /// <remarks>
141        /// Accepts required fields 'apiKey', 'blog', 'userAgent'.
142        /// </remarks>
143        public Akismet(string apiKey, string blog, string userAgent)
144        {
145            this.CharSet = "UTF-8";
146            this.apiKey = apiKey;
147            if (userAgent != null)
148            {
149                this.userAgent = string.Format("{0} | Akismet/1.11", userAgent);
150            }
151
152            this.blog = blog;
153        }
154
155        /// <summary>
156        /// Initializes a new instance of the <see cref="Akismet"/> class. 
157        ///     Create an Akismet API object
158        /// </summary>
159        /// <param name="apiKey">
160        /// Your wordpress.com API key.
161        /// </param>
162        /// <param name="blog">
163        /// URL to your blog
164        /// </param>
165        /// <param name="userAgent">
166        /// Name of application using API.  example: "Joel.Net's Akismet API/1.0"
167        /// </param>
168        /// <param name="serviceUrl">
169        /// The url for the service (uses Akismet service by default)
170        /// </param>
171        public Akismet(string apiKey, string blog, string userAgent, string serviceUrl)
172            : this(apiKey, blog, userAgent)
173        {
174            this.verifyUrl = string.Format("http://{0}/1.1/verify-key", serviceUrl);
175            this.commentCheckUrl = string.Format("http://{{0}}.{0}/1.1/comment-check", serviceUrl);
176            this.submitSpamUrl = string.Format("http://{{0}}.{0}/1.1/submit-spam", serviceUrl);
177            this.submitHamUrl = string.Format("http://{{0}}.{0}/1.1/submit-ham", serviceUrl);
178        }
179
180        #endregion
181
182        #region Properties
183
184        /// <summary>
185        ///     Gets or sets the character set.
186        /// </summary>
187        public string CharSet { get; set; }
188
189        #endregion
190
191        #region Public Methods
192
193        /// <summary>
194        /// Checks TypePadComment object against Akismet database.
195        /// </summary>
196        /// <param name="comment">
197        /// TypePadComment object to check.
198        /// </param>
199        /// <returns>
200        /// 'True' if spam, 'False' if not spam.
201        /// </returns>
202        public bool CommentCheck(AkismetComment comment)
203        {
204            var value =
205                Convert.ToBoolean(
206                    this.HttpPost(String.Format(this.commentCheckUrl, this.apiKey), CreateData(comment), this.CharSet));
207            return value;
208        }
209
210        /// <summary>
211        /// Retracts false positive from Akismet database.
212        /// </summary>
213        /// <param name="comment">
214        /// TypePadComment object to retract.
215        /// </param>
216        public void SubmitHam(AkismetComment comment)
217        {
218            this.HttpPost(String.Format(this.submitHamUrl, this.apiKey), CreateData(comment), this.CharSet);
219        }
220
221        /// <summary>
222        /// Submits TypePadComment object into Akismet database.
223        /// </summary>
224        /// <param name="comment">
225        /// TypePadComment object to submit.
226        /// </param>
227        public void SubmitSpam(AkismetComment comment)
228        {
229            this.HttpPost(String.Format(this.submitSpamUrl, this.apiKey), CreateData(comment), this.CharSet);
230        }
231
232        /// <summary>
233        /// Verifies your wordpress.com key.
234        /// </summary>
235        /// <returns>
236        /// 'True' is key is valid.
237        /// </returns>
238        public bool VerifyKey()
239        {
240            var response = this.HttpPost(
241                this.verifyUrl, 
242                String.Format("key={0}&blog={1}", this.apiKey, HttpUtility.UrlEncode(this.blog)), 
243                this.CharSet);
244            var value = (response == "valid") ? true : false;
245            return value;
246        }
247
248        #endregion
249
250        #region Methods
251
252        /// <summary>
253        /// Takes an TypePadComment object and returns an (escaped) string of data to POST.
254        /// </summary>
255        /// <param name="comment">
256        /// TypePadComment object to translate.
257        /// </param>
258        /// <returns>
259        /// A System.String containing the data to POST to Akismet API.
260        /// </returns>
261        private static string CreateData(AkismetComment comment)
262        {
263            var value =
264                String.Format(
265                    "blog={0}&user_ip={1}&user_agent={2}&referrer={3}&permalink={4}&comment_type={5}" +
266                    "&comment_author={6}&comment_author_email={7}&comment_author_url={8}&comment_content={9}", 
267                    HttpUtility.UrlEncode(comment.Blog), 
268                    HttpUtility.UrlEncode(comment.UserIp), 
269                    HttpUtility.UrlEncode(comment.UserAgent), 
270                    HttpUtility.UrlEncode(comment.Referrer), 
271                    HttpUtility.UrlEncode(comment.Permalink), 
272                    HttpUtility.UrlEncode(comment.CommentType), 
273                    HttpUtility.UrlEncode(comment.CommentAuthor), 
274                    HttpUtility.UrlEncode(comment.CommentAuthorEmail), 
275                    HttpUtility.UrlEncode(comment.CommentAuthorUrl), 
276                    HttpUtility.UrlEncode(comment.CommentContent));
277
278            return value;
279        }
280
281        /// <summary>
282        /// Performs a web request.
283        /// </summary>
284        /// <param name="url">
285        /// The URL of the request.
286        /// </param>
287        /// <param name="data">
288        /// The data to post.
289        /// </param>
290        /// <param name="charset">
291        /// The char set.
292        /// </param>
293        /// <returns>
294        /// The response string.
295        /// </returns>
296        private string HttpPost(string url, string data, string charset)
297        {
298            var value = string.Empty;
299
300            // Initialize Connection
301            var request = (HttpWebRequest)WebRequest.Create(url);
302            request.Method = "POST";
303            request.ContentType = string.Format("application/x-www-form-urlencoded; charset={0}", charset);
304            request.UserAgent = this.userAgent;
305            request.ContentLength = data.Length;
306
307            // Write Data
308            using (var writer = new StreamWriter(request.GetRequestStream()))
309            {
310                writer.Write(data);
311            }
312
313            // Read Response
314            var response = (HttpWebResponse)request.GetResponse();
315            var responseStream = response.GetResponseStream();
316            if (responseStream != null)
317            {
318                using (var reader = new StreamReader(responseStream))
319                {
320                    value = reader.ReadToEnd();
321                }
322            }
323
324            return value;
325        }
326
327        #endregion
328    }
329}
330
331namespace App_Code.Extensions
332{
333    using System;
334    using BlogEngine.Core;
335    using BlogEngine.Core.Web.Controls;
336    using BlogEngine.Core.Web.Extensions;
337    using System.Collections.Generic;
338    using Joel.Net;
339
340    /// <summary>
341    /// Akismet Filter
342    /// </summary>
343    [Extension("Akismet anti-spam comment filter", "1.0", "<a href=\"http://dotnetblogengine.net\">BlogEngine.NET</a>")]
344    public class AkismetFilter : ICustomFilter
345    {
346        #region Constants and Fields
347
348        /// <summary>
349        ///     The sync root.
350        /// </summary>
351        private static readonly object syncRoot = new object();
352
353        /// <summary>
354        ///     The api.
355        /// </summary>
356        private static Dictionary<Guid, Akismet> blogsApi = new Dictionary<Guid, Akismet>();
357
358        /// <summary>
359        ///     The key.
360        /// </summary>
361        private static Dictionary<Guid, string> blogsKey = new Dictionary<Guid, string>();
362
363        /// <summary>
364        ///     The settings.
365        /// </summary>
366        private static Dictionary<Guid, ExtensionSettings> blogsSettings = new Dictionary<Guid, ExtensionSettings>();
367
368        /// <summary>
369        ///     The site.
370        /// </summary>
371        private static Dictionary<Guid, string> blogsSite = new Dictionary<Guid, string>();
372
373        #endregion
374
375        #region Constructors and Destructors
376
377        /// <summary>
378        ///     Initializes a new instance of the <see cref = "AkismetFilter" /> class.
379        /// </summary>
380        public AkismetFilter()
381        {
382            this.InitSettings();
383        }
384
385        #endregion
386
387        #region Properties
388
389        private static Akismet Api
390        {
391            get
392            {
393                Akismet akismet = null;
394                blogsApi.TryGetValue(Blog.CurrentInstance.Id, out akismet);
395
396                return akismet;
397            }
398            set
399            {
400                blogsApi[Blog.CurrentInstance.Id] = value;
401            }
402        }
403
404        private static string Key
405        {
406            get
407            {
408                string key = null;
409                blogsKey.TryGetValue(Blog.CurrentInstance.Id, out key);
410
411                return key;
412            }
413            set
414            {
415                blogsKey[Blog.CurrentInstance.Id] = value;
416            }
417        }
418
419        private static string Site
420        {
421            get
422            {
423                string key = null;
424                blogsSite.TryGetValue(Blog.CurrentInstance.Id, out key);
425
426                return key;
427            }
428            set
429            {
430                blogsSite[Blog.CurrentInstance.Id] = value;
431            }
432        }
433
434        private static ExtensionSettings Settings
435        {
436            get
437            {
438                Guid blogId = Blog.CurrentInstance.Id;
439                ExtensionSettings settings = null;
440                blogsSettings.TryGetValue(blogId, out settings);
441
442                if (settings == null)
443                {
444                    lock (syncRoot)
445                    {
446                        blogsSettings.TryGetValue(blogId, out settings);
447
448                        if (settings == null)
449                        {
450                            var extensionSettings = new ExtensionSettings("AkismetFilter") { IsScalar = true };
451
452                            extensionSettings.AddParameter("SiteURL", "Site URL");
453                            extensionSettings.AddParameter("ApiKey", "API Key");
454
455                            extensionSettings.AddValue("SiteURL", "http://example.com/blog");
456                            extensionSettings.AddValue("ApiKey", "123456789");
457
458                            blogsSettings[blogId] = ExtensionManager.InitSettings("AkismetFilter", extensionSettings);
459                            ExtensionManager.SetStatus("AkismetFilter", false);
460                        }
461                    }
462                }
463
464                return settings;
465            }
466        }
467
468        /// <summary>
469        ///     Gets a value indicating whether FallThrough.
470        /// </summary>
471        public bool FallThrough { get; private set; }
472
473        #endregion
474
475        #region Implemented Interfaces
476
477        #region ICustomFilter
478
479        /// <summary>
480        /// Check if comment is spam
481        /// </summary>
482        /// <param name="comment">
483        /// BlogEngine comment
484        /// </param>
485        /// <returns>
486        /// True if comment is spam
487        /// </returns>
488        public bool Check(Comment comment)
489        {
490            if (Api == null)
491            {
492                this.Initialize();
493            }
494
495            if (Settings == null)
496            {
497                this.InitSettings();
498            }
499
500            var akismetComment = GetAkismetComment(comment);
501            var spam = Api.CommentCheck(akismetComment);
502            this.FallThrough = !spam;
503            return spam;
504        }
505
506        /// <summary>
507        /// Initializes anti-spam service
508        /// </summary>
509        /// <returns>
510        /// True if service online and credentials validated
511        /// </returns>
512        public bool Initialize()
513        {
514            if (!ExtensionManager.ExtensionEnabled("AkismetFilter"))
515            {
516                return false;
517            }
518
519            if (Settings == null)
520            {
521                this.InitSettings();
522            }
523
524            Site = Settings.GetSingleValue("SiteURL");
525            Key = Settings.GetSingleValue("ApiKey");
526            Api = new Akismet(Key, Site, string.Format("BlogEngine.NET {0}", BlogSettings.Instance.Version()));
527
528            return Api.VerifyKey();
529        }
530
531        /// <summary>
532        /// The report.
533        /// </summary>
534        /// <param name="comment">
535        /// The comment.
536        /// </param>
537        public void Report(Comment comment)
538        {
539            if (Api == null && !this.Initialize())
540            {
541                return;
542            }
543
544            if (Settings == null)
545            {
546                this.InitSettings();
547            }
548
549            var akismetComment = GetAkismetComment(comment);
550
551            if (comment.IsApproved)
552            {
553                Utils.Log(
554                    string.Format("Akismet: Reporting NOT spam from \"{0}\" at \"{1}\"", comment.Author, comment.IP));
555                Api.SubmitHam(akismetComment);
556            }
557            else
558            {
559                Utils.Log(string.Format("Akismet: Reporting SPAM from \"{0}\" at \"{1}\"", comment.Author, comment.IP));
560                Api.SubmitSpam(akismetComment);
561            }
562        }
563
564        #endregion
565
566        #endregion
567
568        #region Methods
569
570        /// <summary>
571        /// Gets an akismet comment.
572        /// </summary>
573        /// <param name="comment">
574        /// The comment.
575        /// </param>
576        /// <returns>
577        /// An Akismet Comment.
578        /// </returns>
579        private static AkismetComment GetAkismetComment(Comment comment)
580        {
581            var akismetComment = new AkismetComment
582                {
583                    Blog = Settings.GetSingleValue("SiteURL"), 
584                    UserIp = comment.IP, 
585                    CommentContent = comment.Content, 
586                    CommentType = "comment", 
587                    CommentAuthor = comment.Author, 
588                    CommentAuthorEmail = comment.Email
589                };
590            if (comment.Website != null)
591            {
592                akismetComment.CommentAuthorUrl = comment.Website.OriginalString;
593            }
594
595            return akismetComment;
596        }
597
598        private void InitSettings()
599        {
600            // call Settings getter so default settings are loaded on application start.
601            var s = Settings;
602        }
603
604        #endregion
605    }
606}