namespace BlogEngine.Core { using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; using System.Linq; using System.Net.Mail; using System.Text; using System.Web; using BlogEngine.Core.Data.Models; using BlogEngine.Core.Providers; /// /// A post is an entry on the blog - a blog post. /// [Serializable] public class Post : BusinessBase, IComparable, IPublishable { #region Constants and Fields /// /// The sync root. /// private static readonly object SyncRoot = new object(); /// /// The categories. /// private readonly StateList categories; /// /// All comments, including deleted /// private readonly List allcomments; /// /// The notification emails. /// private readonly StateList notificationEmails; /// /// The post tags. /// private readonly StateList tags; /// /// The posts. /// private static Dictionary> posts = new Dictionary>(); /// /// The deleted posts. /// private static Dictionary> deletedposts = new Dictionary>(); /// /// The author. /// private string author; /// /// The content. /// private string content; /// /// The description. /// private string description; /// /// Whether the post is comments enabled. /// private bool hasCommentsEnabled; /// /// The nested comments. /// private List nestedComments; /// /// Whether the post is published. /// private bool isPublished; /// /// Whether the post is deleted. /// private bool isDeleted; /// /// The raters. /// private int raters; /// /// The rating. /// private float rating; /// /// The slug of the post. /// private string slug; /// /// The title. /// private string title; #endregion #region Constructors and Destructors /// /// Initializes a new instance of the class. /// The default contstructor assign default values. /// public Post() { this.Id = Guid.NewGuid(); this.allcomments = new List(); this.categories = new StateList(); this.tags = new StateList(); this.notificationEmails = new StateList(); this.DateCreated = new DateTime(); this.isPublished = true; this.hasCommentsEnabled = true; } static Post() { Blog.Saved += (s, e) => { if (e.Action == SaveAction.Delete) { Blog blog = s as Blog; if (blog != null) { RefreshPostLists(blog); } } }; } #endregion #region Events /// /// Occurs before a new comment is added. /// public static event EventHandler AddingComment; /// /// Occurs when a comment is added. /// public static event EventHandler CommentAdded; /// /// Occurs when a comment has been removed. /// public static event EventHandler CommentRemoved; /// /// Occurs when a comment has been purged. /// public static event EventHandler CommentPurged; /// /// Occurs when a comment has been restored. /// public static event EventHandler CommentRestored; /// /// Occurs when a comment is updated. /// public static event EventHandler CommentUpdated; /// /// Occurs when a visitor rates the post. /// public static event EventHandler Rated; /// /// Occurs before comment is removed. /// public static event EventHandler RemovingComment; /// /// Occurs before comment is purged. /// public static event EventHandler PurgingComment; /// /// Occurs before comment is restored. /// public static event EventHandler RestoringComment; /// /// Occurs when the post is being served to the output stream. /// public static event EventHandler Serving; /// /// Occurs when the post is being published. /// public static event EventHandler Publishing; /// /// Occurs when a post is published. /// public static event EventHandler Published; /// /// Occurs before a new comment is updated. /// public static event EventHandler UpdatingComment; #endregion #region Post Properties /// /// Gets a sorted collection of all undeleted posts in the blog. /// Sorted by date. /// public static List Posts { get { Blog blog = Blog.CurrentInstance; List blogPosts; if (!posts.TryGetValue(blog.BlogId, out blogPosts)) { lock (SyncRoot) { if (!posts.TryGetValue(blog.BlogId, out blogPosts)) { posts[blog.Id] = blogPosts = BlogService.FillPosts().Where(p => p.IsDeleted == false).ToList(); blogPosts.TrimExcess(); AddRelations(blogPosts); } } } return blogPosts; } } /// /// Gets a sorted collection of all undeleted posts across all blogs. /// Sorted by date. /// public static List AllBlogPosts { get { List blogs = Blog.Blogs.Where(b => b.IsActive).ToList(); Guid originalBlogInstanceIdOverride = Blog.InstanceIdOverride; List postsAcrossAllBlogs = new List(); // Posts are not loaded for a blog instance until that blog // instance is first accessed. For blog instances where the // posts have not yet been loaded, using InstanceIdOverride to // temporarily switch the blog CurrentInstance blog so the Posts // for that blog instance can be loaded. // for (int i = 0; i < blogs.Count; i++) { List blogPosts; if (!posts.TryGetValue(blogs[i].Id, out blogPosts)) { // temporarily override the Current BlogId to the // blog Id we need posts to be loaded for. Blog.InstanceIdOverride = blogs[i].Id; blogPosts = Posts; Blog.InstanceIdOverride = originalBlogInstanceIdOverride; } postsAcrossAllBlogs.AddRange(blogPosts); } postsAcrossAllBlogs.Sort(); // do not call AddRelations(). that will change the Next/Previous properties // to point to posts in other blogs, which leads to the Next / Previous // posts pointing to posts in other blog instances when viewing a single post // (in post.aspx). If Next/Previous is needed for the posts returned // here in AllBlogPosts, would be better to create new properties // (e.g. AllBlogsNextPost, AllBlogsPreviousPost). // AddRelations(postsAcrossAllBlogs); return postsAcrossAllBlogs; } } /// /// Gets a sorted collection of all undeleted posts, taking into account the /// current blog instance's Site Aggregation status in determining if posts /// from just the current instance or all instances should be returned. /// Sorted by date. /// /// /// This logic could be put into the normal 'Posts' property, however /// there are times when a Site Aggregation blog instance may just need /// its own posts. So ApplicablePosts can be called when data across /// all blog instances may be needed, and Posts can be called when data /// for just the current blog instance is needed. /// public static List ApplicablePosts { get { if (Blog.CurrentInstance.IsSiteAggregation) return AllBlogPosts; else return Posts; } } /// /// Gets a sorted collection of all deleted posts in the blog. /// Sorted by date. /// public static List DeletedPosts { get { Blog blog = Blog.CurrentInstance; List blogPosts; if (!deletedposts.TryGetValue(blog.Id, out blogPosts)) { lock (SyncRoot) { if (!deletedposts.TryGetValue(blog.Id, out blogPosts)) { blogPosts = BlogService.FillPosts().Where(p => p.IsDeleted == true).ToList(); deletedposts[blog.Id] = blogPosts; } } } return blogPosts; } } /// /// Gets or sets the Author or the post. /// public string Author { get { return this.author; } set { base.SetValue("Author", value, ref this.author); } } /// /// Gets AuthorProfile. /// public AuthorProfile AuthorProfile { get { return AuthorProfile.GetProfile(this.Author); } } /// /// Gets an unsorted List of categories. /// public StateList Categories { get { return this.categories; } } /// /// Gets or sets the Content or the post. /// public string Content { get { return this.content; } set { base.SetValue("Content", value, ref this.content); // This is commented out only because I can't find any reference to // this cache item anywhere in the project. So it seems pretty obscure // if it's supposed to be used by plugins or something else. //if (base.SetValue("Content", value, ref this.content)) //{ // Blog.CurrentInstance.Cache.Remove("content_" + this.Id); //} } } /// /// Gets or sets the Description or the post. /// public string Description { get { return this.description; } set { base.SetValue("Description", value, ref this.description); } } /// /// Gets if the Post have been changed. /// public override bool IsChanged { get { if (base.IsChanged) { return true; } if (this.Categories.IsChanged || this.Tags.IsChanged || this.NotificationEmails.IsChanged) { return true; } return false; } } /// /// Gets the next post relative to this one based on time. /// /// If this post is the newest, then it returns null. /// /// public Post Next { get; private set; } /// /// Gets a collection of email addresses that is signed up for /// comment notification on the specific post. /// public StateList NotificationEmails { get { return this.notificationEmails; } } /// /// Gets the absolute permanent link to the post. /// public Uri PermaLink { get { return new Uri($"{Blog.AbsoluteWebRoot}post.aspx?id={Id}"); //return new Uri(string.Format("{0}post/{1}", this.Blog.AbsoluteWebRoot, this.Slug)); } } /// /// Gets the previous post relative to this one based on time. /// /// If this post is the oldest, then it returns null. /// /// public Post Previous { get; private set; } /// /// Gets or sets a value indicating whether or not the post is published. /// public bool IsPublished { get { return this.isPublished; } set { base.SetValue("IsPublished", value, ref this.isPublished); } } /// /// Gets or sets a value indicating whether or not the post is deleted. /// public bool IsDeleted { get { return this.isDeleted; } set { base.SetValue("IsDeleted", value, ref this.isDeleted); } } /// /// Gets or sets the number of raters or the object. /// public int Raters { get { return this.raters; } set { base.SetValue("Raters", value, ref this.raters); } } /// /// Gets or sets the rating or the post. /// public float Rating { get { return this.rating; } set { base.SetValue("Rating", value, ref this.rating); } } /// /// Gets the absolute link to the post. /// public Uri AbsoluteLink { get { return new Uri(this.Blog.AbsoluteWebRootAuthority + this.RelativeLink); } } /// /// Gets a relative-to-the-site-root path to the post. /// Only for in-site use. /// public string RelativeLink { get { // taking into account aggregated posts var settings = BlogSettings.GetInstanceSettings(Blog); var ext = string.IsNullOrEmpty(BlogConfig.FileExtension) ? ".aspx" : BlogConfig.FileExtension; var theslug = Utils.RemoveIllegalCharacters(this.Slug); if (!settings.RemoveExtensionsFromUrls) theslug += ext; var BlogUrl = ""; if (this.BlogId != Blog.CurrentInstance.Id) { // point it to child blog BlogUrl = this.Blog.Name + "/"; } return settings.TimeStampPostLinks ? string.Format("{0}{1}post/{2}{3}", Blog.RelativeWebRoot, BlogUrl, DateCreated.ToString("yyyy/MM/dd/", CultureInfo.InvariantCulture), theslug) : string.Format("{0}{1}post/{2}", Utils.RelativeWebRoot, BlogUrl, theslug); } } /// /// Returns a relative link if possible if the hostname of this blog instance matches the /// hostname of the site aggregation blog. If the hostname is different, then the /// absolute link is returned. /// public string RelativeOrAbsoluteLink { get { return Blog.DoesHostnameDifferFromSiteAggregationBlog ? AbsoluteLink.ToString() : RelativeLink; } } /// /// Gets or sets the Slug of the Post. /// A Slug is the relative URL used by the posts. /// public string Slug { get { return string.IsNullOrEmpty(this.slug) ? GetUniqueSlug(this.title, this.Id) : this.slug; } set { base.SetValue("Slug", value, ref this.slug); } } /// /// Gets an unsorted collection of tags. /// public StateList Tags { get { return this.tags; } } /// /// Gets or sets the Title or the post. /// public string Title { get { return this.title; } set { base.SetValue("Title", value, ref this.title); } } /// /// Gets the trackback link to the post. /// public Uri TrackbackLink { get { return new Uri($"{Blog.AbsoluteWebRoot}trackback.axd?id={Id}"); } } /// /// Gets a value indicating whether or not the post is visible or not. /// public bool IsVisible { get { if (this.IsDeleted) return false; else if (this.IsPublished && this.DateCreated <= BlogSettings.Instance.FromUtc()) return true; else if (Security.IsAuthorizedTo(Rights.ViewUnpublishedPosts)) return true; return false; } } /// /// Gets a value indicating whether a post is available to visitors not logged into the blog. /// public bool IsVisibleToPublic { get { return (this.IsPublished && this.IsDeleted == false && this.DateCreated <= BlogSettings.Instance.FromUtc()); } } /// /// URL of the first image in the post, if any /// public string FirstImgSrc { get { int idx = Content.IndexOf(" idx) { var len = idxEnd - idx; return Content.Substring(idx, len); } } catch (Exception) { } } return ""; } } #endregion #region Comment Properties /// /// Gets a Collection of All Comments for the post /// public List Comments { get { return this.AllComments.FindAll(c => !c.IsDeleted); } } /// /// Gets a Collection of All Comments for the post /// public List AllComments { get { return this.allcomments; } } /// /// Gets a collection of Approved comments for the post sorted by date. /// When moderation is enabled, unapproved comments go to pending. /// Whith moderation off, they shown as approved. /// public List ApprovedComments { get { if (BlogSettings.Instance.EnableCommentsModeration) { return this.Comments.FindAll(c => c.IsApproved && !c.IsSpam && !c.IsPingbackOrTrackback); } else { return this.Comments.FindAll(c => !c.IsSpam && !c.IsPingbackOrTrackback); } } } /// /// Gets a collection of comments waiting for approval for the post, sorted by date /// excluding comments rejected as spam /// public List NotApprovedComments { get { return this.Comments.FindAll(c => !c.IsApproved && !c.IsSpam && !c.IsPingbackOrTrackback); } } /// /// Gets a collection of pingbacks and trackbacks for the post, sorted by date /// public List Pingbacks { get { return this.Comments.FindAll(c => c.IsApproved && !c.IsSpam && c.IsPingbackOrTrackback); } } /// /// Gets a collection of comments marked as spam for the post, sorted by date. /// public List SpamComments { get { return this.Comments.FindAll(c => c.IsSpam && !c.IsDeleted); } } /// /// Gets a collection of comments marked as deleted for the post, sorted by date. /// public List DeletedComments { get { return this.allcomments.FindAll(c => c.IsDeleted); } } /// /// Gets a collection of the comments that are nested as replies /// public List NestedComments { get { if (this.nestedComments == null) { this.CreateNestedComments(); } return this.nestedComments; } } /// /// Gets or sets a value indicating whether this instance has comments enabled. /// /// /// true if this instance has comments enabled; otherwise, false. /// public bool HasCommentsEnabled { get { return this.hasCommentsEnabled; } set { base.SetValue("HasCommentsEnabled", value, ref this.hasCommentsEnabled); } } #endregion #region Post Public Methods /// /// Gets whether the current user can publish this post. /// /// The author of the post without needing to assign it to the Author property. /// public bool CanPublish(string author) { bool isOwnPost = Security.CurrentUser.Identity.Name.Equals(author, StringComparison.OrdinalIgnoreCase); if (isOwnPost && !Security.IsAuthorizedTo(Rights.PublishOwnPosts)) { return false; } else if (!isOwnPost && !Security.IsAuthorizedTo(Rights.PublishOtherUsersPosts)) { return false; } return true; } /// /// Gets whether or not the current user owns this post. /// /// public override bool CurrentUserOwns { get { return Security.CurrentUser.Identity.Name.Equals(this.Author, StringComparison.OrdinalIgnoreCase); } } /// /// Gets whether the current user can delete this post. /// /// public override bool CanUserDelete { get { if (CurrentUserOwns && Security.IsAuthorizedTo(Rights.DeleteOwnPosts)) return true; else if (!CurrentUserOwns && Security.IsAuthorizedTo(Rights.DeleteOtherUsersPosts)) return true; return false; } } /// /// Gets whether the current user can edit this post. /// /// public override bool CanUserEdit { get { if (CurrentUserOwns && Security.IsAuthorizedTo(Rights.EditOwnPosts)) return true; else if (!CurrentUserOwns && Security.IsAuthorizedTo(Rights.EditOtherUsersPosts)) return true; return false; } } /// /// Returns a post based on the specified id. /// /// /// The post id. /// /// /// The selected post. /// public static Post GetPost(Guid id) { return Posts.Find(p => p.Id == id); } /// /// Returns all posts written by the specified author. /// /// /// The author. /// /// /// A list of Post. /// public static List GetPostsByAuthor(string author) { var legalAuthor = Utils.RemoveIllegalCharacters(author); var list = ApplicablePosts.FindAll( p => { var legalTitle = Utils.RemoveIllegalCharacters(p.Author); return legalAuthor.Equals(legalTitle, StringComparison.OrdinalIgnoreCase); }); return list; } /// /// Get blog by author /// /// Post author /// Blog if author wrote any posts there public static Blog GetBlogByAuthor(string author) { var legalAuthor = Utils.RemoveIllegalCharacters(author); var post = ApplicablePosts.FirstOrDefault( p => { var legalTitle = Utils.RemoveIllegalCharacters(p.Author); return legalAuthor.Equals(legalTitle, StringComparison.OrdinalIgnoreCase); }); return post == null ? null : post.Blog; } /// /// Returns all posts in the specified category /// /// /// The category Id. /// /// /// A list of Post. /// public static List GetPostsByCategory(Guid categoryId) { var cat = Category.GetCategory(categoryId, Blog.CurrentInstance.IsSiteAggregation); return GetPostsByCategory(cat); } /// /// Returns all posts in the specified category /// /// Category objuect /// A list of posts public static List GetPostsByCategory(Category cat) { if (cat == null) { return null; } var col = new List(); foreach (var p in Post.ApplicablePosts) { foreach (var c in p.Categories) { if (Blog.CurrentInstance.IsSiteAggregation) { if (c.Title == cat.Title) col.Add(p); } else { if (c.Title == cat.Title && c.Id == cat.Id) col.Add(p); } } } //var col = Post.ApplicablePosts.Where(p => p.Categories.Contains(cat)).ToList(); col.Sort(); return col; } /// /// Returns all posts published between the two dates. /// /// /// The date From. /// /// /// The date To. /// /// /// A list of Post. /// public static List GetPostsByDate(DateTime dateFrom, DateTime dateTo) { var list = ApplicablePosts.FindAll(p => p.DateCreated.Date >= dateFrom && p.DateCreated.Date <= dateTo); return list; } /// /// Returns all posts tagged with the specified tag. /// /// /// The tag of the post. /// /// /// A list of Post. /// public static List GetPostsByTag(string tag) { tag = Utils.RemoveIllegalCharacters(tag); var list = ApplicablePosts.FindAll( p => p.Tags.Any(t => Utils.RemoveIllegalCharacters(t).Equals(tag, StringComparison.OrdinalIgnoreCase))); return list; } /// /// Checks to see if the specified title has already been used /// by another post. /// /// Titles must be unique because the title is part of the URL. /// /// /// /// The title. /// /// /// The is title unique. /// public static bool IsTitleUnique(string title) { var legal = Utils.RemoveIllegalCharacters(title); return Posts.All( post => !Utils.RemoveIllegalCharacters(post.Title).Equals(legal, StringComparison.OrdinalIgnoreCase)); } /// /// Called when [serving]. /// /// /// The post being served. /// /// /// The instance containing the event data. /// public static void OnServing(Post post, ServingEventArgs arg) { if (Serving != null) { Serving(post, arg); } } /// /// Force reload of all posts /// public static void Reload() { RefreshPostLists(Blog.CurrentInstance); } /// /// Imports Post (without all standard saving routines /// public void Import() { if (this.Deleted) { if (!this.New) { BlogService.DeletePost(this); } } else { if (this.New) { BlogService.InsertPost(this); } else { BlogService.UpdatePost(this); } } } /// /// Marks the object as being an clean, /// which means not dirty. /// public override void MarkOld() { this.Categories.MarkOld(); this.Tags.MarkOld(); this.NotificationEmails.MarkOld(); base.MarkOld(); } /// /// Adds a rating to the post. /// /// /// The rating. /// public void Rate(int newRating) { if (this.Raters > 0) { var total = this.Raters * this.Rating; total += newRating; this.Raters++; this.Rating = total / this.Raters; } else { this.Raters = 1; this.Rating = newRating; } this.DataUpdate(); this.OnRated(this); } /// /// Check if slug is unique and if not generate it /// /// Post slug /// Post id /// Slug that is unique across blogs public static string GetUniqueSlug(string slug, Guid postId) { string s = Utils.RemoveIllegalCharacters(slug.Trim()); // will do for up to 100 unique post titles for (int i = 1; i < 101; i++) { if (IsUniqueSlug(s, postId)) break; s = $"{slug}{i}"; } return s; } /// /// Returns a that represents the current . /// /// /// A that represents the current . /// public override string ToString() { return this.Title; } #endregion #region Comment Public Methods /// /// Adds a comment to the collection and saves the post. /// /// /// The comment to add to the post. /// public void AddComment(Comment comment) { var e = new CancelEventArgs(); this.OnAddingComment(comment, e); if (e.Cancel) { return; } this.AllComments.Add(comment); this.DataUpdate(); this.OnCommentAdded(comment); if (comment.IsApproved) { this.SendNotifications(comment); } } /// /// Approves all the comments in a post. Included to save time on the approval process. /// public void ApproveAllComments() { foreach (var comment in this.Comments) { this.ApproveComment(comment); } } /// /// Approves a Comment for publication. /// /// /// The Comment to approve /// public void ApproveComment(Comment comment) { var e = new CancelEventArgs(); Comment.OnApproving(comment, e); if (e.Cancel) { return; } var inx = this.Comments.IndexOf(comment); this.Comments[inx].IsApproved = true; this.Comments[inx].IsSpam = false; this.DateModified = comment.DateCreated; this.DataUpdate(); Comment.OnApproved(comment); this.SendNotifications(comment); } /// /// Disapproves a Comment as Spam. /// /// /// The Comment to approve /// public void DisapproveComment(Comment comment) { var e = new CancelEventArgs(); Comment.OnDisapproving(comment, e); if (e.Cancel) { return; } var inx = this.Comments.IndexOf(comment); this.Comments[inx].IsApproved = false; this.Comments[inx].IsSpam = true; this.DateModified = comment.DateCreated; this.DataUpdate(); Comment.OnDisapproved(comment); this.SendNotifications(comment); } /// /// Imports a comment to comment collection. Does not /// notify user or run extension events. /// /// /// The comment to add to the post. /// public void ImportComment(Comment comment) { this.AllComments.Add(comment); } /// /// Updates a comment in the collection and saves the post. /// /// /// The comment to update in the post. /// public void UpdateComment(Comment comment) { var e = new CancelEventArgs(); this.OnUpdatingComment(comment, e); if (e.Cancel) { return; } var inx = this.Comments.IndexOf(comment); this.Comments[inx].IsApproved = comment.IsApproved; this.Comments[inx].Content = comment.Content; this.Comments[inx].Author = comment.Author; this.Comments[inx].Country = comment.Country; this.Comments[inx].Email = comment.Email; this.Comments[inx].IP = comment.IP; this.Comments[inx].Website = comment.Website; this.Comments[inx].ModeratedBy = comment.ModeratedBy; this.Comments[inx].IsSpam = comment.IsSpam; this.Comments[inx].IsDeleted = comment.IsDeleted; // need to mark post as "dirty" this.DateModified = DateTime.Now; this.DataUpdate(); this.OnCommentUpdated(comment); } /// /// Removes a comment from the collection. /// Will update the post if is set to true. /// If is set to false, you have to call manually! /// /// /// public void RemoveComment(Comment comment, bool updatePost) { var e = new CancelEventArgs(); this.OnRemovingComment(comment, e); if (e.Cancel) { return; } var comm = Comments.FirstOrDefault(c => c.Id.Equals(comment.Id)); if (comm == null) { return; } comm.IsDeleted = true; if (updatePost) { this.DataUpdate(); } this.OnCommentRemoved(comment); } /// /// Removes a comment from the collection and saves the post. /// /// /// The comment to remove from the post. /// public void RemoveComment(Comment comment) { RemoveComment(comment, true); } /// /// Removes comment from the post /// /// Comment public void PurgeComment(Comment comment) { var e = new CancelEventArgs(); this.OnPurgingComment(comment, e); if (e.Cancel) { return; } this.allcomments.Remove(comment); this.DataUpdate(); this.OnCommentPurged(comment); } /// /// Restores comment from recycle bin /// /// Comment public void RestoreComment(Comment comment) { var e = new CancelEventArgs(); this.OnRestoringComment(comment, e); if (e.Cancel) { return; } var comm = allcomments.FirstOrDefault(c => c.Id.Equals(comment.Id)); if (comm == null) { return; } comm.IsDeleted = false; this.DataUpdate(); this.OnCommentRestored(comment); } #endregion #region Implemented Interfaces #region IComparable /// /// Compares the current object with another object of the same type. /// /// /// An object to compare with this object. /// /// /// A 32-bit signed integer that indicates the relative order of the /// objects being compared. The return value has the following meanings: /// Value Meaning Less than zero This object is less than the other parameter.Zero /// This object is equal to other. Greater than zero This object is greater than other. /// public int CompareTo(Post other) { return other.DateCreated.CompareTo(this.DateCreated); } #endregion #region IPublishable /// /// Raises the Serving event /// /// /// The event Args. /// public void OnServing(ServingEventArgs eventArgs) { if (Serving != null) { Serving(this, eventArgs); } } /// /// When post is publishing /// /// Event arguments public void OnPublishing(CancelEventArgs e) { if (Publishing != null) { Publishing(this, e); } } /// /// On post published /// protected virtual void OnPublished() { if (Published != null) { Published(this, EventArgs.Empty); } } #endregion #endregion #region Methods /// /// Deletes the Post from the current BlogProvider. /// protected override void DataDelete() { this.IsDeleted = true; DataUpdate(); lock (SyncRoot) { Guid blogId = Blog.CurrentInstance.Id; if (posts.ContainsKey(blogId)) posts.Remove(blogId); if (deletedposts.ContainsKey(blogId)) deletedposts.Remove(blogId); } } /// /// Deletes the Post from the current BlogProvider. /// public void Purge() { BlogService.DeletePost(this); if (!Posts.Contains(this)) { RefreshPostLists(Blog.CurrentInstance); return; } Posts.Remove(this); this.Dispose(); AddRelations(Posts); RefreshPostLists(Blog.CurrentInstance); } private static void RefreshPostLists(Blog blog) { lock (SyncRoot) { if (posts.ContainsKey(blog.BlogId)) posts.Remove(blog.BlogId); if (deletedposts.ContainsKey(blog.BlogId)) deletedposts.Remove(blog.BlogId); } } /// /// Restores the deleted posts. /// public void Restore() { this.IsDeleted = false; DataUpdate(); RefreshPostLists(Blog.CurrentInstance); } /// /// Inserts a new post to the current BlogProvider. /// protected override void DataInsert() { if (this.isPublished) { var e = new CancelEventArgs(); this.OnPublishing(e); if (e.Cancel) { this.isPublished = false; } } BlogService.InsertPost(this); if (!this.New) { return; } Posts.Add(this); Posts.Sort(); AddRelations(Posts); if (this.isPublished) { this.OnPublished(); } } /// /// Returns a Post based on the specified id. /// /// /// The post id. /// /// /// The selected Post. /// protected override Post DataSelect(Guid id) { return BlogService.SelectPost(id); } /// /// Updates the Post. /// protected override void DataUpdate() { if (AboutToPublishPost()) { var e = new CancelEventArgs(); OnPublishing(e); if (e.Cancel) { isPublished = false; } } // trigger even only when post goes from unpublished to published var updateAndPublish = false; try { var isOldPublished = BlogService.SelectPost(Id).IsPublished; if(isPublished && !isOldPublished && !isDeleted) { updateAndPublish = true; } } catch (Exception) { } BlogService.UpdatePost(this); Posts.Sort(); AddRelations(Posts); ResetNestedComments(); if (updateAndPublish) { OnPublished(); } } bool AboutToPublishPost() { if (isPublished && !IsDeleted) { var p = DataSelect(Id); if (p != null && !p.isPublished) { return true; } } return false; } /// /// Called when [adding comment]. /// /// /// The comment. /// /// /// The instance containing the event data. /// protected virtual void OnAddingComment(Comment comment, CancelEventArgs e) { if (AddingComment != null) { AddingComment(comment, e); } } /// /// Called when [comment added]. /// /// /// The comment. /// protected virtual void OnCommentAdded(Comment comment) { if (CommentAdded != null) { CommentAdded(comment, EventArgs.Empty); } } /// /// Called when [comment removed]. /// /// /// The comment. /// protected virtual void OnCommentRemoved(Comment comment) { if (CommentRemoved != null) { CommentRemoved(comment, EventArgs.Empty); } } /// /// Called when [comment purged]. /// /// /// The comment. /// protected virtual void OnCommentPurged(Comment comment) { if (CommentPurged != null) { CommentPurged(comment, EventArgs.Empty); } } /// /// Called when [comment restored]. /// /// /// The comment. /// protected virtual void OnCommentRestored(Comment comment) { if (CommentRestored != null) { CommentRestored(comment, EventArgs.Empty); } } /// /// Called when [comment updated]. /// /// /// The comment. /// protected virtual void OnCommentUpdated(Comment comment) { if (CommentUpdated != null) { CommentUpdated(comment, EventArgs.Empty); } } /// /// Called when [rated]. /// /// /// The rated post. /// protected virtual void OnRated(Post post) { if (Rated != null) { Rated(post, EventArgs.Empty); } } /// /// Called when [removing comment]. /// /// /// The comment. /// /// /// The instance containing the event data. /// protected virtual void OnRemovingComment(Comment comment, CancelEventArgs e) { if (RemovingComment != null) { RemovingComment(comment, e); } } /// /// Called when [purging comment]. /// /// /// The comment. /// /// /// The instance containing the event data. /// protected virtual void OnPurgingComment(Comment comment, CancelEventArgs e) { if (PurgingComment != null) { PurgingComment(comment, e); } } /// /// Called when [restoring comment]. /// /// /// The comment. /// /// /// The instance containing the event data. /// protected virtual void OnRestoringComment(Comment comment, CancelEventArgs e) { if (RestoringComment != null) { RestoringComment(comment, e); } } /// /// Called when [updating comment]. /// /// /// The comment. /// /// /// The instance containing the event data. /// protected virtual void OnUpdatingComment(Comment comment, CancelEventArgs e) { if (UpdatingComment != null) { UpdatingComment(comment, e); } } /// /// Validates the Post instance. /// protected override void ValidationRules() { this.AddRule("Title", "Title must be set", String.IsNullOrEmpty(this.Title)); this.AddRule("Content", "Content must be set", String.IsNullOrEmpty(this.Content)); } /// /// Sets the Previous and Next properties to all posts. /// private static void AddRelations(List posts) { for (var i = 0; i < posts.Count; i++) { posts[i].Next = null; posts[i].Previous = null; if (i > 0) { posts[i].Next = posts[i - 1]; } if (i < posts.Count - 1) { posts[i].Previous = posts[i + 1]; } } } /// /// Nests comments based on Id and ParentId /// private void CreateNestedComments() { // instantiate object this.nestedComments = new List(); // temporary ID/Comment table var commentTable = new Hashtable(); foreach (var comment in this.Comments) { // add to hashtable for lookup commentTable.Add(comment.Id, comment); // check if this is a child comment if (comment.ParentId == Guid.Empty) { // root comment, so add it to the list this.nestedComments.Add(comment); } else { // child comment, so find parent var parentComment = commentTable[comment.ParentId] as Comment; if (parentComment != null) { // double check that this sub comment has not already been added if (parentComment.Comments.IndexOf(comment) == -1) { parentComment.Comments.Add(comment); } } else { // just add to the base to prevent an error this.nestedComments.Add(comment); } } } } /// /// Clears all nesting of comments /// private void ResetNestedComments() { // void the List<> this.nestedComments = null; // go through all comments and remove sub comments foreach (var c in this.Comments) { c.Comments.Clear(); } } /// /// Sends a notification to all visitors that has registered /// to retrieve notifications for the specific post. /// /// /// The comment. /// private void SendNotifications(Comment comment) { if (this.NotificationEmails.Count == 0 || comment.IsApproved == false) { return; } foreach (var email in this.NotificationEmails) { if (email == comment.Email) { continue; } // Intentionally using AbsoluteLink instead of PermaLink so the "unsubscribe-email" QS parameter // isn't dropped when post.aspx.cs does a 301 redirect to the RelativeLink, before the unsubscription // process takes place. var unsubscribeLink = this.AbsoluteLink.ToString(); unsubscribeLink += string.Format( "{0}unsubscribe-email={1}", unsubscribeLink.Contains("?") ? "&" : "?", HttpUtility.UrlEncode(email)); var defaultCulture = Utils.GetDefaultCulture(); var sb = new StringBuilder(); sb.AppendFormat( "
New Comment added by {0}

", comment.Author); sb.AppendFormat("{0}

", comment.Content.Replace(Environment.NewLine, "
")); sb.AppendFormat( "{0}: {3}
", Utils.Translate("post", null, defaultCulture), this.PermaLink, comment.Id, this.Title); sb.Append("
_______________________________________________________________________________
"); sb.AppendFormat( "{1}
", unsubscribeLink, Utils.Translate("commentNotificationUnsubscribe")); var mail = new MailMessage { From = new MailAddress(BlogSettings.Instance.Email, BlogSettings.Instance.Name), Subject = $"New comment on {Title}", Body = sb.ToString() }; mail.To.Add(email); Utils.SendMailMessageAsync(mail); } } /// /// Check if post slug is unique accross all blogs /// /// Post slug /// Post id /// True if unique private static bool IsUniqueSlug(string slug, Guid postId) { return Post.ApplicablePosts.Where(p => p.slug != null && p.slug.ToLower() == slug.ToLower() && p.Id != postId).FirstOrDefault() == null ? true : false; } #endregion #region Custom Fields /// /// Custom fields /// public Dictionary CustomFields { get { var postFields = BlogService.Provider.FillCustomFields().Where(f => f.CustomType == "POST" && f.ObjectId == this.Id.ToString()).ToList(); if (postFields == null || postFields.Count < 1) return null; var fields = new Dictionary(); foreach (var item in postFields) { fields.Add(item.Key, item); } return fields; } } #endregion } }