See More

using System; using System.Collections.Generic; using System.Linq; using BlogEngine.Core.Providers; using System.Web; using System.Web.Hosting; using System.IO; using System.Text.RegularExpressions; using System.Net; using BlogEngine.Core.Providers.CacheProvider; namespace BlogEngine.Core { ///

/// Represents a blog instance. /// public class Blog : BusinessBase, IComparable { /// /// Whether the blog is deleted. /// private bool isDeleted; /// /// Blog name /// private string blogName; /// /// Whether the blog is the primary blog instance /// private bool isPrimary; /// /// Whether the blog is active /// private bool isActive; /// /// The hostname of the blog instance. /// private string hostname; /// /// Whether any text before the hostname is accepted. /// private bool isAnyTextBeforeHostnameAccepted; /// /// The storage container name of the blog's data /// private string storageContainerName; /// /// The virtual path to the blog instance /// private string virtualPath; /// /// The relative web root. /// private string relativeWebRoot; /// /// Whether this blog instance is an aggregration of data across all blog instances. /// private bool isSiteAggregation; /// /// The sync root. /// private static readonly object SyncRoot = new object(); /// /// The blogs. /// private static List blogs; /// /// Gets or sets a value indicating whether or not the blog is deleted. /// public bool IsDeleted { get { return this.isDeleted; } set { base.SetValue("IsDeleted", value, ref this.isDeleted); } } /// /// Gets whether the blog is the primary blog instance. /// public bool IsPrimary { get { return this.isPrimary; } internal set { // SetAsPrimaryInstance() exists as a public method to make // a blog instance be the primary one -- which makes sure other // instances are no longer primary. base.SetValue("IsPrimary", value, ref this.isPrimary); } } /// /// Gets whether this blog instance is an aggregration of data across all blog instances. /// public bool IsSiteAggregation { get { return this.isSiteAggregation; } set { base.SetValue("IsSiteAggregation", value, ref this.isSiteAggregation); } } /// /// Gets whether the blog instance is active. /// public bool IsActive { get { return this.isActive; } set { base.SetValue("IsActive", value, ref this.isActive); } } /// /// Gets the optional hostname of the blog instance. /// public string Hostname { get { return this.hostname; } set { base.SetValue("Hostname", value, ref this.hostname); } } /// /// Gets whether any text before the hostname is accepted. /// public bool IsAnyTextBeforeHostnameAccepted { get { return this.isAnyTextBeforeHostnameAccepted; } set { base.SetValue("IsAnyTextBeforeHostnameAccepted", value, ref this.isAnyTextBeforeHostnameAccepted); } } /// /// Gets or sets the blog name. /// public string Name { get { return this.blogName; } set { base.SetValue("Name", value, ref this.blogName); } } /// /// Gets or sets the storage container name. /// public string StorageContainerName { get { return this.storageContainerName; } set { base.SetValue("StorageContainerName", value, ref this.storageContainerName); } } /// /// Gets or sets the virtual path to the blog instance. /// public string VirtualPath { get { return this.virtualPath; } set { // RelativeWebRoot is based on VirtualPath. Clear relativeWebRoot // so RelativeWebRoot is re-generated. this.relativeWebRoot = null; base.SetValue("VirtualPath", value, ref this.virtualPath); } } /// /// Gets or sets the unique Identification of the blog instance. /// public override Guid Id { get { return base.Id; } set { base.Id = value; base.BlogId = value; } } /// /// Returns true if the blog resides in a subfolder of the application root directory; /// false otherwise. /// public bool IsSubfolderOfApplicationWebRoot { get { return this.RelativeWebRoot.Length > Utils.ApplicationRelativeWebRoot.Length; } } /// /// Attempts to delete the current Blog. /// public override void Delete() { if (this.IsPrimary) { throw new Exception("The primary blog cannot be deleted."); } base.Delete(); } /// /// Deletes the Blog from the current BlogProvider. /// protected override void DataDelete() { OnSaving(this, SaveAction.Delete); if (this.Deleted) { BlogService.DeleteBlogStorageContainer(this); BlogService.DeleteBlog(this); } Blogs.Remove(this); SortBlogs(); OnSaved(this, SaveAction.Delete); this.Dispose(); } /// /// Inserts a new blog to the current BlogProvider. /// protected override void DataInsert() { OnSaving(this, SaveAction.Insert); if (this.New) { BlogService.InsertBlog(this); } Blogs.Add(this); SortBlogs(); OnSaved(this, SaveAction.Insert); } /// /// Updates the object in its data store. /// protected override void DataUpdate() { OnSaving(this, SaveAction.Update); if (this.IsChanged) { BlogService.UpdateBlog(this); SortBlogs(); } OnSaved(this, SaveAction.Update); } /// /// Retrieves the object from the data store and populates it. /// /// /// The unique identifier of the object. /// /// /// The object that was selected from the data store. /// protected override Blog DataSelect(Guid id) { return BlogService.SelectBlog(id); } /// /// Reinforces the business rules by adding additional rules to the /// broken rules collection. /// protected override void ValidationRules() { this.AddRule("Name", "Name must be set", string.IsNullOrEmpty(this.Name)); } /// /// Gets whether the current user can delete this object. /// public override bool CanUserDelete { get { return Security.IsAdministrator && !this.IsPrimary; } } /// /// Sets the current Blog instance as the primary. /// public void SetAsPrimaryInstance() { for (int i = 0; i < Blogs.Count; i++) { // Ensure other blogs are not marked as primary. if (Blogs[i].Id != this.Id && Blogs[i].IsPrimary) { Blogs[i].IsPrimary = false; Blogs[i].Save(); } else if (Blogs[i].Id == this.Id) { Blogs[i].IsPrimary = true; Blogs[i].Save(); } } } /// /// Initializes a new instance of the class. /// The default contstructor assign default values. /// public Blog() { this.Id = Guid.NewGuid(); this.BlogId = this.Id; this.DateCreated = DateTime.Now; this.DateModified = DateTime.Now; } /// /// Gets all blogs. /// public static List Blogs { get { if (blogs == null) { lock (SyncRoot) { if (blogs == null) { blogs = BlogService.FillBlogs().ToList(); if (blogs.Count == 0) { // create the primary instance Blog blog = new Blog(); blog.Name = "Primary"; blog.hostname = string.Empty; blog.VirtualPath = BlogConfig.VirtualPath; blog.StorageContainerName = string.Empty; blog.IsPrimary = true; blog.IsSiteAggregation = false; blog.Save(); } SortBlogs(); } } } return blogs; } } private static void SortBlogs() { blogs.Sort(); } /// /// Marked as ThreadStatic so each thread has its own value. /// Need to be careful with this since when using ThreadPool.QueueUserWorkItem, /// after a thread is used, it is returned to the thread pool and /// any ThreadStatic values (such as this field) are not cleared, they will persist. /// /// This value is reset in WwwSubdomainModule.BeginRequest. /// /// [ThreadStatic] private static Guid _InstanceIdOverride; /// /// This is a thread-specific Blog Instance ID to override. /// If the current blog instance needs to be overridden, /// this property can be used. A typical use for this is when /// using BG/async threads where the current blog instance /// cannot be determined since HttpContext will be null. /// public static Guid InstanceIdOverride { get { return _InstanceIdOverride; } set { _InstanceIdOverride = value; } } /// /// The current blog instance. /// public static Blog CurrentInstance { get { if (_InstanceIdOverride != Guid.Empty) { Blog overrideBlog = Blogs.FirstOrDefault(b => b.Id == _InstanceIdOverride); if (overrideBlog != null) { return overrideBlog; } } const string CONTEXT_ITEM_KEY = "current-blog-instance"; HttpContext context = HttpContext.Current; Blog blog = context.Items[CONTEXT_ITEM_KEY] as Blog; if (blog != null) { return blog; } List blogs = Blogs; if (blogs.Count == 0) { return null; } if (blogs.Count == 1) { blog = blogs[0]; } else { // Determine which blog. // // Web service and Page method calls to the server need to be made to the // root level, and cannot be virtual URLs that get rewritten to the correct // physical location. When attempting to rewrite these URLs, the web // service/page method will throw a "405 Method Not Allowed" error. // For us to determine which blog these AJAX calls are on behalf of, // a request header on the AJAX calls will be appended to tell us which // blog instance they are for. // // The built-in ASP.NET Callback system works correctly even when // the URL is rewritten. For these, CurrentInstance will be determined // and stored in HttpContext.Items before the rewrite is done -- so even // after the rewrite, CurrentInstance will reference the correct blog // instance. // string blogIdHeader = context.Request.Headers["x-blog-instance"]; if (!string.IsNullOrWhiteSpace(blogIdHeader) && blogIdHeader.Length == 36) { blog = GetBlog(new Guid(blogIdHeader)); if (blog != null && !blog.IsActive) blog = null; } if (blog == null) { // Note, this.Blogs is sorted via SortBlogs() so the blogs with longer // RelativeWebRoots come first. This is important when matching so the // more specific matches are done first. // for the purposes here, adding a trailing slash to RawUrl, even if it's not // a correct URL. if a blog has a relative root of /blog1, RelativeWebRoot // will be /blog1/ (containing the trailing slash). for equal comparisons, // make sure rawUrl also has a trailing slash. string rawUrl = VirtualPathUtility.AppendTrailingSlash(context.Request.RawUrl); string hostname = context.Request.Url.Host; for (int i = 0; i < blogs.Count; i++) { Blog checkBlog = blogs[i]; if (checkBlog.isActive) { // first check the hostname, if a hostname is specified if (!string.IsNullOrWhiteSpace(checkBlog.hostname)) { bool isMatch = false; if (checkBlog.IsAnyTextBeforeHostnameAccepted) isMatch = hostname.EndsWith(checkBlog.hostname, StringComparison.OrdinalIgnoreCase); else isMatch = hostname.Equals(checkBlog.hostname, StringComparison.OrdinalIgnoreCase); // if isMatch, we still need to check the conditions below, to allow // multiple path variations for a particular hostname. if (!isMatch) { continue; } } // second check the path. if (rawUrl.StartsWith(checkBlog.RelativeWebRoot, StringComparison.OrdinalIgnoreCase)) { blog = checkBlog; break; } } } // if all blogs are inactive, or there are no matches for some reason, // select the primary blog. if (blog == null) { blog = blogs.FirstOrDefault(b => b.IsPrimary); } } } context.Items[CONTEXT_ITEM_KEY] = blog; return blog; } } /// /// Returns a blog based on the specified id. /// /// /// The blog id. /// /// /// The selected blog. /// public static Blog GetBlog(Guid id) { return Blogs.Find(b => b.Id == id); } /// /// Returns the site aggregation blog instance, if one exists. /// /// /// The site aggregation blog instance, if one exists. /// public static Blog SiteAggregationBlog { get { return Blogs.Find(b => b.IsSiteAggregation); } } /// /// Gets whether the hostname differs from the Site Aggreation blog. /// public bool DoesHostnameDifferFromSiteAggregationBlog { get { if (this.IsSiteAggregation) return false; Blog siteAggregationBlog = SiteAggregationBlog; if (siteAggregationBlog == null) return false; return siteAggregationBlog.Hostname != this.Hostname; } } /// /// Gets a mappable virtual path to the blog instance's storage folder. /// public string StorageLocation { get { // only the Primary blog instance should have an empty StorageContainerName if (string.IsNullOrWhiteSpace(this.StorageContainerName)) { return BlogConfig.StorageLocation; } return $"{BlogConfig.StorageLocation}{BlogConfig.BlogInstancesFolderName}/{StorageContainerName}/"; } } /// /// the root file storage directory for the blog. All File system management should start from the Root file store /// public FileSystem.Directory RootFileStore { get { return BlogService.GetDirectory(string.Concat(this.StorageLocation, Utils.FilesFolder)); } } /// /// Gets the relative root of the blog instance. /// /// A string that ends with a '/'. public string RelativeWebRoot { get { return relativeWebRoot ?? (relativeWebRoot = VirtualPathUtility.ToAbsolute(VirtualPathUtility.AppendTrailingSlash(this.VirtualPath ?? BlogConfig.VirtualPath))); } } /// /// Gets the "authority" portion of the absolute web root. /// public string AbsoluteWebRootAuthority { get { return AbsoluteWebRoot.GetLeftPart(UriPartial.Authority); } } /// /// Gets the absolute root of the blog instance. /// public Uri AbsoluteWebRoot { get { string contextItemKey = $"{Id}-absolutewebroot"; var context = HttpContext.Current; if (context == null) { throw new WebException("The current HttpContext is null"); } Uri absoluteWebRoot = context.Items[contextItemKey] as Uri; if (absoluteWebRoot != null) { return absoluteWebRoot; } UriBuilder uri = new UriBuilder(); if (!string.IsNullOrWhiteSpace(this.Hostname)) uri.Host = this.Hostname; else { uri.Host = context.Request.Url.Host; if (!context.Request.Url.IsDefaultPort) { uri.Port = context.Request.Url.Port; } } uri.Path = RelativeWebRoot; uri.Scheme = context.Request.Url.Scheme; // added for https support absoluteWebRoot = uri.Uri; context.Items[contextItemKey] = absoluteWebRoot; return absoluteWebRoot; } } /// /// Creates a new blog. /// public static Blog CreateNewBlog( string copyFromExistingBlogId, string blogName, string hostname, bool isAnyTextBeforeHostnameAccepted, string storageContainerName, string virtualPath, bool isActive, bool isSiteAggregation, out string message) { message = null; if (!ValidateProperties(true, null, blogName, hostname, isAnyTextBeforeHostnameAccepted, storageContainerName, virtualPath, isSiteAggregation, out message)) { if (string.IsNullOrWhiteSpace(message)) { message = "Validation for new blog failed."; } return null; } if (string.IsNullOrWhiteSpace(copyFromExistingBlogId) || copyFromExistingBlogId.Length != 36) { message = "An existing blog instance ID must be specified to create the new blog from."; return null; } Blog existingBlog = Blog.GetBlog(new Guid(copyFromExistingBlogId)); if (existingBlog == null) { message = "The existing blog instance to create the new blog from could not be found."; return null; } Blog newBlog = new Blog() { Name = blogName, StorageContainerName = storageContainerName, Hostname = hostname, IsAnyTextBeforeHostnameAccepted = isAnyTextBeforeHostnameAccepted, VirtualPath = virtualPath, IsActive = isActive, IsSiteAggregation = isSiteAggregation }; bool setupResult = false; try { setupResult = newBlog.SetupFromExistingBlog(existingBlog); } catch (Exception ex) { Utils.Log("Blog.CreateNewBlog", ex); message = "Failed to create new blog. Error: " + ex.Message; return null; } if (!setupResult) { message = "Failed during process of setting up the blog from the existing blog instance."; return null; } // save the blog for the first time. newBlog.Save(); return newBlog; } /// /// Validates the blog properties. /// public static bool ValidateProperties( bool isNew, Blog updateBlog, string blogName, string hostname, bool isAnyTextBeforeHostnameAccepted, string storageContainerName, string virtualPath, bool isSiteAggregation, out string message) { message = null; if (string.IsNullOrWhiteSpace(blogName)) { message = "Blog Name is Required."; return false; } if (!string.IsNullOrWhiteSpace(hostname)) { if (!Utils.IsHostnameValid(hostname) && !Utils.IsIpV4AddressValid(hostname) && !Utils.IsIpV6AddressValid(hostname)) { message = "Invalid Hostname. Hostname must be an IP address or domain name."; return false; } } Regex validChars = new Regex("^[a-z0-9-_]+$", RegexOptions.IgnoreCase); // if primary is being edited, allow an empty storage container name (bypass check). if (updateBlog == null || !updateBlog.IsPrimary) { if (string.IsNullOrWhiteSpace(storageContainerName)) { message = "Storage Container Name is Required."; return false; } } if (!string.IsNullOrWhiteSpace(storageContainerName) && !validChars.IsMatch(storageContainerName)) { message = "Storage Container Name contains invalid characters."; return false; } if (string.IsNullOrWhiteSpace(virtualPath)) { message = "Virtual Path is Required."; return false; } else { if (!virtualPath.StartsWith("~/")) { message = "Virtual Path must begin with ~/"; return false; } // note: a virtual path of ~/ without anything after it is allowed. this would // typically be for the primary blog, but can also be for blogs that are using // subdomains, where each instance might be ~/ string vPath = virtualPath.Substring(2); if (vPath.Length > 0) { if (!validChars.IsMatch(vPath)) { message = "The Virtual Path contains invalid characters after the ~/"; return false; } } } if (isSiteAggregation) { Blog siteAggregationBlog = Blog.SiteAggregationBlog; if ((updateBlog == null && siteAggregationBlog != null) || (updateBlog != null && siteAggregationBlog != null && updateBlog.Id != siteAggregationBlog.Id)) { message = "Another blog is already marked as being the Site Aggregation blog. Only one blog instance can be the Site Aggregration instance."; return false; } } if (Blog.Blogs.FirstOrDefault(b => (updateBlog == null || updateBlog.Id != b.Id) && (b.VirtualPath ?? string.Empty).Equals((virtualPath ?? string.Empty), StringComparison.OrdinalIgnoreCase) && (b.Hostname ?? string.Empty).Equals(hostname ?? string.Empty, StringComparison.OrdinalIgnoreCase)) != null) { message = "Another blog has the same combination of Hostname and Virtual Path."; return false; } return true; } /// /// Sets up the blog instance using the files and settings from an existing blog instance. /// /// The existing blog instance to use files and settings from. /// public bool SetupFromExistingBlog(Blog existing) { if (existing == null) throw new ArgumentException("existing"); if (string.IsNullOrWhiteSpace(this.StorageContainerName)) throw new ArgumentException("this.StorageContainerName"); // allow the blog provider to setup the necessary blog files, etc. bool providerResult = BlogService.SetupBlogFromExistingBlog(existing, this); if (!providerResult) return false; //if (Membership.Provider.Name.Equals("DbMembershipProvider", StringComparison.OrdinalIgnoreCase)) //{ //} //if (Roles.Provider.Name.Equals("DbRoleProvider", StringComparison.OrdinalIgnoreCase)) //{ //} return true; } internal bool DeleteBlogFolder() { // This method is called by the blog providers when a blog's storage container // is being deleted. Even the DbBlogProvider will call this method. // However, a different type of blog provider (e.g. Azure, etc) may not // need to call this method. try { string storagePath = HostingEnvironment.MapPath(this.StorageLocation); if (Directory.Exists(storagePath)) { Directory.Delete(storagePath, true); } } catch (Exception ex) { Utils.Log("Blog.DeleteBlogFolder", ex); return false; } return true; } internal bool CopyExistingBlogFolderToNewBlogFolder(Blog existingBlog) { // This method is called by the blog providers when a new blog is being setup. // Even the DbBlogProvider will call this method. However, a different type of // blog provider (e.g. Azure, etc) may not need to call this method. if (string.IsNullOrWhiteSpace(this.StorageContainerName)) throw new ArgumentException("this.StorageContainerName"); string existingBlogStoragePath = null; try { // Ensure the existing blog storage path exists. existingBlogStoragePath = HostingEnvironment.MapPath(existingBlog.StorageLocation); if (!Directory.Exists(existingBlogStoragePath)) { throw new Exception($"Storage folder for existing blog instance to copy from does not exist. Directory not found is: {existingBlogStoragePath}"); } } catch (Exception ex) { Utils.Log("Blog.CreateNewBlogFromExisting", ex); throw; // re-throw error so error message bubbles up. } // Ensure "BlogInstancesFolderName" exists. string blogInstancesFolder = HostingEnvironment.MapPath($"{BlogConfig.StorageLocation}{BlogConfig.BlogInstancesFolderName}"); if (!Utils.CreateDirectoryIfNotExists(blogInstancesFolder)) return false; // If newBlogStoragePath already exists, throw an exception as this may be a mistake // and we don't want to overwrite any existing data. string newBlogStoragePath = HostingEnvironment.MapPath(this.StorageLocation); try { if (Directory.Exists(newBlogStoragePath)) { throw new Exception($"Blog destination folder already exists. {newBlogStoragePath}"); } } catch (Exception ex) { Utils.Log("Blog.CopyExistingBlogFolderToNewBlogFolder", ex); throw; // re-throw error so error message bubbles up. } if (!Utils.CreateDirectoryIfNotExists(newBlogStoragePath)) return false; // Copy the entire directory contents. DirectoryInfo source = new DirectoryInfo(existingBlogStoragePath); DirectoryInfo target = new DirectoryInfo(newBlogStoragePath); try { // if the primary blog directly in App_Data is the 'source', when all the directories/files are // being copied to the new location, we don't want to copy the entire BlogInstancesFolderName // (by default ~/App_Data/blogs) to the new location. Everything except for that can be copied. // If the 'source' is a blog instance under ~/App_Data/blogs (e.g. ~/App_Data/blogs/template), // then this is not a concern. Utils.CopyDirectoryContents(source, target, new List() { BlogConfig.BlogInstancesFolderName }); } catch (Exception ex) { Utils.Log("Blog.CopyExistingBlogFolderToNewBlogFolder", ex); throw; // re-throw error so error message bubbles up. } return true; } /// /// Compares the current Blog instance to another. /// /// The other Blog instance to compare to. /// -1, 0, or 1. See ordering rules in the comments. public int CompareTo(Blog other) { // order so: // 1. active blogs come first // 2. blogs with longer Hostnames come first (length DESC) // 3. blogs not allowing any text before hostname come first. // 4. blogs with longer RelativeWebRoots come first (length DESC) // 5. blog name ASC. // it is sorted this way so the more specific criteria are evaluated first, // and pre-sorted to make CurrentInstance work as fast as possible. if (this.IsActive && !other.IsActive) return -1; else if (!this.IsActive && other.IsActive) return 1; int otherHostnameLength = other.Hostname.Length; int thisHostnameLength = this.hostname.Length; if (otherHostnameLength != thisHostnameLength) { return otherHostnameLength.CompareTo(thisHostnameLength); } // at this point, otherHostnameLength == thisHostnameLength. if (otherHostnameLength > 0) // if so, thisHostnameLength is also > 0. { if (this.IsAnyTextBeforeHostnameAccepted && !other.IsAnyTextBeforeHostnameAccepted) return 1; else if (!this.IsAnyTextBeforeHostnameAccepted && other.IsAnyTextBeforeHostnameAccepted) return -1; } int otherRelWebRootLength = other.RelativeWebRoot.Length; int thisRelWebRootLength = this.RelativeWebRoot.Length; if (otherRelWebRootLength != thisRelWebRootLength) { return otherRelWebRootLength.CompareTo(thisRelWebRootLength); } return this.Name.CompareTo(other.Name); } private CacheProvider _cache; /// /// blog instance cache /// public CacheProvider Cache { get { return _cache ?? (_cache = new CacheProvider(HttpContext.Current.Cache)); } } } }