()
+ .InformationalVersion;
+ }
+ }
+ public static string OSDescription
+ {
+ get
+ {
+ return System.Runtime.InteropServices.RuntimeInformation.OSDescription;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Blogifier.Core/Blogifier.Core.csproj b/src/Blogifier.Core/Blogifier.Core.csproj
new file mode 100644
index 000000000..346a8bb43
--- /dev/null
+++ b/src/Blogifier.Core/Blogifier.Core.csproj
@@ -0,0 +1,56 @@
+
+
+
+ netcoreapp3.1
+ 2.7.3.0
+ blogifierdotnet
+ Blogifier Core Library provides Database and file I/O support to Blogifier projects via common service layer
+ Blogifier.Net
+ LICENSE
+ http://blogifier.net
+ https://github.com/blogifierdotnet/Blogifier.Core
+ blog,blogifier,asp.net,.net core,blogifier.net,asp.net core
+ icon.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+
+
+
+
+
+ True
+
+
+
+ True
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Blogifier.Core/Constants.cs b/src/Blogifier.Core/Constants.cs
new file mode 100644
index 000000000..8f631561e
--- /dev/null
+++ b/src/Blogifier.Core/Constants.cs
@@ -0,0 +1,42 @@
+namespace Blogifier.Core
+{
+ public class Constants
+ {
+ public static string ConfigSectionKey = "Blogifier";
+ public static string ConfigRepoKey = "GithubRepoUrl";
+ public static string ConfigNotificationsKey = "GithubNotificationsUrl";
+
+ public static string NewestVersion = "last-version";
+ public static string UpgradeDirectory = "_upgrade";
+
+ // blog settings in custom fields
+ public static string BlogTitle = "blog-title";
+ public static string BlogDescription = "blog-description";
+ public static string BlogItemsPerPage = "blog-items-per-page";
+ public static string BlogTheme = "blog-theme";
+ public static string BlogLogo = "blog-logo";
+ public static string BlogCover = "blog-cover";
+ public static string Culture = "culture";
+ public static string IncludeFeatured = "blog-include-featured";
+ public static string HeaderScript = "blog-header-script";
+ public static string FooterScript = "blog-footer-script";
+
+ public static string DummyEmail = "dummy@blog.com";
+
+ public static string DefaultAvatar = "admin/img/avatar.jpg";
+ public static string ImagePlaceholder = "admin/img/img-placeholder.png";
+ public static string ThemeScreenshot = "screenshot.png";
+ public static string ThemeEditReturnUrl = "~/admin/settings/theme";
+ public static string ThemeDataFile = "data.json";
+ }
+
+ public enum UploadType
+ {
+ Avatar,
+ Attachement,
+ AppLogo,
+ AppCover,
+ PostCover,
+ PostImage
+ }
+}
\ No newline at end of file
diff --git a/src/Blogifier.Core/Data/AppData.cs b/src/Blogifier.Core/Data/AppData.cs
new file mode 100644
index 000000000..0267f92be
--- /dev/null
+++ b/src/Blogifier.Core/Data/AppData.cs
@@ -0,0 +1,152 @@
+using Blogifier.Core.Helpers;
+using System;
+using System.Linq;
+
+namespace Blogifier.Core.Data
+{
+ public class AppData
+ {
+ public static void Seed(AppDbContext context)
+ {
+ if (context.BlogPosts.Any())
+ return;
+
+ context.Authors.Add(new Author
+ {
+ AppUserName = "admin",
+ Email = "admin@us.com",
+ DisplayName = "Administrator",
+ Avatar = "data/admin/avatar.png",
+ Bio = "Something about administrator, maybe HTML or markdown formatted text goes here.
Should be customizable and editable from user profile.
",
+ IsAdmin = true,
+ Created = DateTime.UtcNow.AddDays(-120)
+ });
+
+ context.Authors.Add(new Author
+ {
+ AppUserName = "demo",
+ Email = "demo@us.com",
+ DisplayName = "Demo user",
+ Bio = "Short description about this user and blog.",
+ Created = DateTime.UtcNow.AddDays(-110)
+ });
+
+ context.SaveChanges();
+
+ var adminId = context.Authors.Single(a => a.AppUserName == "admin").Id;
+ var demoId = context.Authors.Single(a => a.AppUserName == "demo").Id;
+
+ context.BlogPosts.Add(new BlogPost
+ {
+ Title = "Welcome to Blogifier!",
+ Slug = "welcome-to-blogifier!",
+ Description = SeedData.FeaturedDesc,
+ Content = SeedData.PostWhatIs,
+ Categories = "welcome,blog",
+ AuthorId = adminId,
+ Cover = "data/admin/cover-blog.png",
+ PostViews = 5,
+ Rating = 4.5,
+ IsFeatured = true,
+ Published = DateTime.UtcNow.AddDays(-100)
+ });
+
+ context.BlogPosts.Add(new BlogPost
+ {
+ Title = "Blogifier Features",
+ Slug = "blogifier-features",
+ Description = "List of the main features supported by Blogifier, includes user management, content management, markdown editor, simple search and others. This is not the full list and work in progress.",
+ Content = SeedData.PostFeatures,
+ Categories = "blog",
+ AuthorId = adminId,
+ Cover = "data/admin/cover-globe.png",
+ PostViews = 15,
+ Rating = 4.0,
+ Published = DateTime.UtcNow.AddDays(-55)
+ });
+
+ context.BlogPosts.Add(new BlogPost
+ {
+ Title = "Demo post",
+ Slug = "demo-post",
+ Description = "This demo site is a sandbox to test Blogifier features. It runs in-memory and does not save any data, so you can try everything without making any mess. Have fun!",
+ Content = SeedData.PostDemo,
+ AuthorId = demoId,
+ Cover = "data/demo/demo-cover.jpg",
+ PostViews = 25,
+ Rating = 3.5,
+ Published = DateTime.UtcNow.AddDays(-10)
+ });
+
+ context.Notifications.Add(new Notification
+ {
+ Notifier = "Blogifier",
+ AlertType = AlertType.System,
+ AuthorId = 0,
+ Content = "Welcome to Blogifier!",
+ Active = true,
+ DateNotified = SystemClock.Now()
+ });
+
+ context.SaveChanges();
+ }
+ }
+
+ public class SeedData
+ {
+ public static readonly string FeaturedDesc = @"Blogifier is simple, beautiful, light-weight open source blog written in .NET Core. This cross-platform, highly extendable and customizable web application brings all the best blogging features in small, portable package.
+
+#### To login:
+* User: demo
+* Pswd: demo";
+
+ public static readonly string PostWhatIs = @"## What is Blogifier
+
+Blogifier is simple, beautiful, light-weight open source blog written in .NET Core. This cross-platform, highly extendable and customizable web application brings all the best blogging features in small, portable package.
+
+## System Requirements
+
+* Windows, Mac or Linux
+* ASP.NET Core 2.1
+* Visual Studio 2017, VS Code or other code editor (Atom, Sublime etc)
+* SQLite by default, MS SQL Server tested, EF compatible databases should work
+
+## Getting Started
+
+1. Clone or download source code
+2. Run application in Visual Studio or using your code editor
+3. Use admin/admin to log in as admininstrator
+4. Use demo/demo to log in as user
+
+## Demo site
+
+The [demo site](http://blogifier.azurewebsites.net) is a playground to check out Blogifier features. You can write and publish posts, upload files and test application before install. And no worries, it is just a sandbox and will clean itself.
+
+![Demo-1.png](/data/admin/admin-editor.png)";
+
+ public static readonly string PostFeatures = @"### User Management
+Blogifier is multi-user application with simple admin/user roles, allowing every user write and publish posts and administrator create new users.
+
+### Content Management
+Built-in file manager allows upload images and files and use them as links in the post editor.
+
+![file-mgr.png](/data/admin/admin-files.png)
+
+### Plugin System
+Blogifier built as highly extendable application allowing modules to be side-loaded and added to blog at runtime.
+
+### Markdown Editor
+The post editor uses markdown syntax, which many writers prefer over HTML for its simplicity.
+
+### Simple Search
+There is simple but quick and functional search in the post lists, as well as search in the image/file list in the file manager.
+";
+
+ public static readonly string PostDemo = @"This demo site is a sandbox to test Blogifier features. It runs in-memory and does not save any data, so you can try everything without making any mess. Have fun!
+
+#### To login:
+* User: demo
+* Pswd: demo";
+
+ }
+}
diff --git a/src/Blogifier.Core/Data/AppDbContext.cs b/src/Blogifier.Core/Data/AppDbContext.cs
new file mode 100644
index 000000000..c381bf0c8
--- /dev/null
+++ b/src/Blogifier.Core/Data/AppDbContext.cs
@@ -0,0 +1,32 @@
+using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore;
+
+namespace Blogifier.Core.Data
+{
+ public class AppDbContext : IdentityDbContext
+ {
+ public AppDbContext(DbContextOptions options) : base(options)
+ {
+ }
+
+ public DbSet BlogPosts { get; set; }
+ public DbSet Authors { get; set; }
+ public DbSet Notifications { get; set; }
+ public DbSet CustomFields { get; set; }
+ public DbSet HtmlWidgets { get; set; }
+ public DbSet Newsletters { get; set; }
+
+ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ {
+ if (!optionsBuilder.IsConfigured)
+ {
+ AppSettings.DbOptions(optionsBuilder);
+ }
+ }
+
+ protected override void OnModelCreating(ModelBuilder builder)
+ {
+ base.OnModelCreating(builder);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Blogifier.Core/Data/Domain/AppUser.cs b/src/Blogifier.Core/Data/Domain/AppUser.cs
new file mode 100644
index 000000000..7f5379e10
--- /dev/null
+++ b/src/Blogifier.Core/Data/Domain/AppUser.cs
@@ -0,0 +1,6 @@
+using Microsoft.AspNetCore.Identity;
+
+namespace Blogifier.Core.Data
+{
+ public class AppUser : IdentityUser { }
+}
\ No newline at end of file
diff --git a/src/Blogifier.Core/Data/Domain/Author.cs b/src/Blogifier.Core/Data/Domain/Author.cs
new file mode 100644
index 000000000..b429728a7
--- /dev/null
+++ b/src/Blogifier.Core/Data/Domain/Author.cs
@@ -0,0 +1,34 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+
+namespace Blogifier.Core.Data
+{
+ public class Author
+ {
+ public Author() { }
+
+ ///
+ /// Author ID
+ ///
+ public int Id { get; set; }
+
+ [StringLength(160)]
+ public string AppUserName { get; set; }
+ [EmailAddress]
+ public string Email { get; set; }
+
+ [Required]
+ [StringLength(160)]
+ [Display(Name = "User name")]
+ public string DisplayName { get; set; }
+
+ [Display(Name = "User bio")]
+ public string Bio { get; set; }
+
+ [StringLength(160)]
+ public string Avatar { get; set; }
+
+ public bool IsAdmin { get; set; }
+ public DateTime Created { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Blogifier.Core/Data/Domain/BlogPost.cs b/src/Blogifier.Core/Data/Domain/BlogPost.cs
new file mode 100644
index 000000000..41013c0b9
--- /dev/null
+++ b/src/Blogifier.Core/Data/Domain/BlogPost.cs
@@ -0,0 +1,42 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+
+namespace Blogifier.Core.Data
+{
+ public class BlogPost
+ {
+ public BlogPost() { }
+
+ public int Id { get; set; }
+ public int AuthorId { get; set; }
+
+ [Required]
+ [StringLength(160)]
+ public string Title { get; set; }
+
+ [Required]
+ [RegularExpression("^[a-z0-9-]+$", ErrorMessage = "Slug format not valid.")]
+ [StringLength(160)]
+ public string Slug { get; set; }
+
+ [Required]
+ [StringLength(450)]
+ public string Description { get; set; }
+
+ [Required]
+ public string Content { get; set; }
+
+ [StringLength(2000)]
+ public string Categories { get; set; }
+
+ [StringLength(255)]
+ public string Cover { get; set; }
+
+ public int PostViews { get; set; }
+ public double Rating { get; set; }
+
+ public bool IsFeatured { get; set; }
+
+ public DateTime Published { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Blogifier.Core/Data/Domain/CustomField.cs b/src/Blogifier.Core/Data/Domain/CustomField.cs
new file mode 100644
index 000000000..0cf1a1723
--- /dev/null
+++ b/src/Blogifier.Core/Data/Domain/CustomField.cs
@@ -0,0 +1,17 @@
+namespace Blogifier.Core.Data
+{
+ public class CustomField
+ {
+ public int Id { get; set; }
+ public int AuthorId { get; set; }
+ public string Name { get; set; }
+ public string Content { get; set; }
+ }
+
+ public class SocialField : CustomField
+ {
+ public string Title { get; set; }
+ public string Icon { get; set; }
+ public int Rank { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Blogifier.Core/Data/Domain/Notification.cs b/src/Blogifier.Core/Data/Domain/Notification.cs
new file mode 100644
index 000000000..4f43a6120
--- /dev/null
+++ b/src/Blogifier.Core/Data/Domain/Notification.cs
@@ -0,0 +1,30 @@
+using System;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Blogifier.Core.Data
+{
+ public class Notification
+ {
+ public int Id { get; set; }
+ public int AuthorId { get; set; }
+ public AlertType AlertType { get; set; }
+ public string Content { get; set; }
+ public DateTime DateNotified { get; set; }
+ public string Notifier { get; set; }
+ public bool Active { get; set; }
+ }
+
+ public class Newsletter
+ {
+ public int Id { get; set; }
+ public string Email { get; set; }
+ public string Ip { get; set; }
+ public DateTime Created { get; set; }
+ }
+
+ public enum AlertType
+ {
+ System, User, Contact
+ }
+}
\ No newline at end of file
diff --git a/src/Blogifier.Core/Data/Domain/Plugins.cs b/src/Blogifier.Core/Data/Domain/Plugins.cs
new file mode 100644
index 000000000..56170a5b2
--- /dev/null
+++ b/src/Blogifier.Core/Data/Domain/Plugins.cs
@@ -0,0 +1,11 @@
+namespace Blogifier.Core.Data
+{
+ public class HtmlWidget
+ {
+ public int Id { get; set; }
+ public string Name { get; set; }
+ public string Theme { get; set; }
+ public string Author { get; set; }
+ public string Content { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Blogifier.Core/Data/Models/AccountModel.cs b/src/Blogifier.Core/Data/Models/AccountModel.cs
new file mode 100644
index 000000000..54cee9b2c
--- /dev/null
+++ b/src/Blogifier.Core/Data/Models/AccountModel.cs
@@ -0,0 +1,47 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace Blogifier.Core.Data.Models
+{
+ public class RegisterModel
+ {
+ [Required]
+ public string UserName { get; set; }
+
+ [Required]
+ [EmailAddress]
+ public string Email { get; set; }
+
+ [Required]
+ [DataType(DataType.Password)]
+ public string Password { get; set; }
+
+ [Required]
+ [Compare("Password")]
+ [DataType(DataType.Password)]
+ public string ConfirmPassword { get; set; }
+
+ public bool SetAsAdmin { get; set; }
+ }
+
+ public class ChangePasswordModel
+ {
+ [Required]
+ public string UserName { get; set; }
+
+ [Required]
+ [DataType(DataType.Password)]
+ [Display(Name = "Current password")]
+ public string OldPassword { get; set; }
+
+ [Required]
+ [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 4)]
+ [DataType(DataType.Password)]
+ [Display(Name = "New password")]
+ public string NewPassword { get; set; }
+
+ [DataType(DataType.Password)]
+ [Display(Name = "Confirm new password")]
+ [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
+ public string ConfirmPassword { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Blogifier.Core/Data/Models/AppModel.cs b/src/Blogifier.Core/Data/Models/AppModel.cs
new file mode 100644
index 000000000..68988c649
--- /dev/null
+++ b/src/Blogifier.Core/Data/Models/AppModel.cs
@@ -0,0 +1,43 @@
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+
+namespace Blogifier.Core.Data
+{
+ public class AppItem
+ {
+ public string Avatar { get; set; }
+ public bool DemoMode { get; set; }
+ public string ImageExtensions { get; set; }
+ public string ImportTypes { get; set; }
+ public bool SeedData { get; set; }
+ }
+
+ public class BlogItem
+ {
+ [Required]
+ [StringLength(160)]
+ public string Title { get; set; } = "Blog Title";
+ [Required]
+ [StringLength(255)]
+ public string Description { get; set; }
+ [Display(Name = "Items per page")]
+ public int ItemsPerPage { get; set; }
+ [StringLength(160)]
+ [Display(Name = "Blog cover URL")]
+ public string Cover { get; set; }
+ [StringLength(160)]
+ [Display(Name = "Blog logo URL")]
+ public string Logo { get; set; }
+ [Required]
+ [StringLength(120)]
+ public string Theme { get; set; }
+ [Required]
+ [StringLength(15)]
+ public string Culture { get; set; }
+ public bool IncludeFeatured { get; set; }
+ public List SocialFields { get; set; }
+
+ public string HeaderScript { get; set; }
+ public string FooterScript { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Blogifier.Core/Data/Models/AssetModel.cs b/src/Blogifier.Core/Data/Models/AssetModel.cs
new file mode 100644
index 000000000..d83d792fa
--- /dev/null
+++ b/src/Blogifier.Core/Data/Models/AssetModel.cs
@@ -0,0 +1,42 @@
+using Blogifier.Core.Helpers;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+
+namespace Blogifier.Core.Data
+{
+ public class AssetsModel
+ {
+ public Pager Pager { get; set; }
+ public IEnumerable Assets { get; set; }
+ }
+
+ public class AssetItem
+ {
+ public AssetType AssetType {
+ get {
+ return Path.IsImagePath() ? AssetType.Image : AssetType.Attachment;
+ }
+ }
+
+ [Required]
+ [StringLength(160)]
+ public string Title { get; set; }
+
+ [Required]
+ [StringLength(250)]
+ public string Path { get; set; }
+
+ [Required]
+ [StringLength(250)]
+ public string Url { get; set; }
+
+ [StringLength(250)]
+ public string Image { get; set; }
+ }
+
+ public enum AssetType
+ {
+ Image = 0,
+ Attachment = 1
+ }
+}
\ No newline at end of file
diff --git a/src/Blogifier.Core/Data/Models/GithubModel.cs b/src/Blogifier.Core/Data/Models/GithubModel.cs
new file mode 100644
index 000000000..789608ee9
--- /dev/null
+++ b/src/Blogifier.Core/Data/Models/GithubModel.cs
@@ -0,0 +1,101 @@
+using System;
+using System.Collections.Generic;
+
+namespace Blogifier.Core.Data.Github
+{
+ public class Author
+ {
+ public string login { get; set; }
+ public int id { get; set; }
+ public string node_id { get; set; }
+ public string avatar_url { get; set; }
+ public string gravatar_id { get; set; }
+ public string url { get; set; }
+ public string html_url { get; set; }
+ public string followers_url { get; set; }
+ public string following_url { get; set; }
+ public string gists_url { get; set; }
+ public string starred_url { get; set; }
+ public string subscriptions_url { get; set; }
+ public string organizations_url { get; set; }
+ public string repos_url { get; set; }
+ public string events_url { get; set; }
+ public string received_events_url { get; set; }
+ public string type { get; set; }
+ public bool site_admin { get; set; }
+ }
+
+ public class Uploader
+ {
+ public string login { get; set; }
+ public int id { get; set; }
+ public string node_id { get; set; }
+ public string avatar_url { get; set; }
+ public string gravatar_id { get; set; }
+ public string url { get; set; }
+ public string html_url { get; set; }
+ public string followers_url { get; set; }
+ public string following_url { get; set; }
+ public string gists_url { get; set; }
+ public string starred_url { get; set; }
+ public string subscriptions_url { get; set; }
+ public string organizations_url { get; set; }
+ public string repos_url { get; set; }
+ public string events_url { get; set; }
+ public string received_events_url { get; set; }
+ public string type { get; set; }
+ public bool site_admin { get; set; }
+ }
+
+ public class Asset
+ {
+ public string url { get; set; }
+ public int id { get; set; }
+ public string node_id { get; set; }
+ public string name { get; set; }
+ public object label { get; set; }
+ public Uploader uploader { get; set; }
+ public string content_type { get; set; }
+ public string state { get; set; }
+ public int size { get; set; }
+ public int download_count { get; set; }
+ public DateTime created_at { get; set; }
+ public DateTime updated_at { get; set; }
+ public string browser_download_url { get; set; }
+ }
+
+ public class Repository
+ {
+ public string url { get; set; }
+ public string assets_url { get; set; }
+ public string upload_url { get; set; }
+ public string html_url { get; set; }
+ public int id { get; set; }
+ public string node_id { get; set; }
+ public string tag_name { get; set; }
+ public string target_commitish { get; set; }
+ public string name { get; set; }
+ public bool draft { get; set; }
+ public Author author { get; set; }
+ public bool prerelease { get; set; }
+ public DateTime created_at { get; set; }
+ public DateTime published_at { get; set; }
+ public List assets { get; set; }
+ public string tarball_url { get; set; }
+ public string zipball_url { get; set; }
+ public string body { get; set; }
+ }
+
+ public class GithubFile
+ {
+ public string name { get; set; }
+ public string path { get; set; }
+ public string sha { get; set; }
+ public int size { get; set; }
+ public string url { get; set; }
+ public string html_url { get; set; }
+ public string git_url { get; set; }
+ public string download_url { get; set; }
+ public string type { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Blogifier.Core/Data/Models/NotificationModel.cs b/src/Blogifier.Core/Data/Models/NotificationModel.cs
new file mode 100644
index 000000000..4c8b2a161
--- /dev/null
+++ b/src/Blogifier.Core/Data/Models/NotificationModel.cs
@@ -0,0 +1,27 @@
+using Blogifier.Core.Helpers;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+
+namespace Blogifier.Core.Data.Models
+{
+ public class NewsletterModel
+ {
+ public IEnumerable Emails { get; set; }
+ public Pager Pager { get; set; }
+ }
+
+ public class NotificationModel
+ {
+ public IEnumerable Notifications { get; set; }
+ public Pager Pager { get; set; }
+ }
+
+ public class ContactModel
+ {
+ [Required]
+ public string Name { get; set; }
+ [Required, EmailAddress]
+ public string Email { get; set; }
+ public string Content { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Blogifier.Core/Data/Models/PostModel.cs b/src/Blogifier.Core/Data/Models/PostModel.cs
new file mode 100644
index 000000000..38e0ca0ba
--- /dev/null
+++ b/src/Blogifier.Core/Data/Models/PostModel.cs
@@ -0,0 +1,149 @@
+using Blogifier.Core.Helpers;
+using Microsoft.AspNetCore.Http;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+
+namespace Blogifier.Core.Data
+{
+ public class PostModel
+ {
+ public BlogItem Blog { get; set; }
+ public PostItem Post { get; set; }
+ public PostItem Older { get; set; }
+ public PostItem Newer { get; set; }
+ }
+
+ public class ListModel
+ {
+ public BlogItem Blog { get; set; }
+ public Author Author { get; set; } // posts by author
+ public string Category { get; set; } // posts by category
+
+ public IEnumerable Posts { get; set; }
+ public Pager Pager { get; set; }
+
+ public PostListType PostListType { get; set; }
+ }
+
+ public class PageListModel
+ {
+ public IEnumerable Posts { get; set; }
+ public Pager Pager { get; set; }
+ }
+
+ public class PostItem : IEquatable
+ {
+ public int Id { get; set; }
+ [Required]
+ public string Title { get; set; }
+ public string Slug { get; set; }
+ public string Description { get; set; }
+ [Required]
+ public string Content { get; set; }
+ public string Categories { get; set; }
+ public string Cover { get; set; }
+ public int PostViews { get; set; }
+ public double Rating { get; set; }
+ public DateTime Published { get; set; }
+ public bool IsPublished { get { return Published > DateTime.MinValue; } }
+ public bool Featured { get; set; }
+
+ public Author Author { get; set; }
+ public SaveStatus Status { get; set; }
+ public List SocialFields { get; set; }
+
+ #region IEquatable
+ // to be able compare two posts
+ // if(post1 == post2) { ... }
+ public bool Equals(PostItem other)
+ {
+ if (Id == other.Id)
+ return true;
+
+ return false;
+ }
+
+ public override int GetHashCode()
+ {
+ return Id.GetHashCode();
+ }
+ #endregion
+ }
+
+ public enum PostListType
+ {
+ Blog, Category, Author, Search
+ }
+
+ public class PostListFilter
+ {
+ HttpRequest _req;
+
+ public PostListFilter(HttpRequest request)
+ {
+ _req = request;
+ }
+
+ public string Page
+ {
+ get
+ {
+ return string.IsNullOrEmpty(_req.Query["page"])
+ ? "" : _req.Query["page"].ToString();
+ }
+ }
+ public string Status
+ {
+ get
+ {
+ return string.IsNullOrEmpty(_req.Query["status"])
+ ? "A" : _req.Query["status"].ToString();
+ }
+ }
+ public string Search
+ {
+ get
+ {
+ return string.IsNullOrEmpty(_req.Query["search"])
+ ? "" : _req.Query["search"].ToString();
+ }
+ }
+ public string Qstring
+ {
+ get
+ {
+ var q = "";
+ if (!string.IsNullOrEmpty(Status)) q += $"&status={Status}";
+ if (!string.IsNullOrEmpty(Search)) q += $"&search={Search}";
+ return q;
+ }
+ }
+
+ public string IsChecked(string status)
+ {
+ return status == Status ? "checked" : "";
+ }
+ }
+
+ public class CategoryItem: IComparable
+ {
+ public string Category { get; set; }
+ public int PostCount { get; set; }
+
+ public int CompareTo(CategoryItem other)
+ {
+ return Category.ToLower().CompareTo(other.Category.ToLower());
+ }
+ }
+
+ public enum SaveStatus
+ {
+ Saving = 1, Publishing = 2, Unpublishing = 3
+ }
+
+ public enum PublishedStatus
+ {
+ All, Published, Drafts
+ }
+}
\ No newline at end of file
diff --git a/src/Blogifier.Core/Data/Models/ThemeModel.cs b/src/Blogifier.Core/Data/Models/ThemeModel.cs
new file mode 100644
index 000000000..7422cd11a
--- /dev/null
+++ b/src/Blogifier.Core/Data/Models/ThemeModel.cs
@@ -0,0 +1,16 @@
+namespace Blogifier.Core.Data
+{
+ public class ThemeDataModel
+ {
+ public string Theme { get; set; }
+ public string Data { get; set; }
+ }
+
+ public class ThemeItem
+ {
+ public string Title { get; set; }
+ public string Cover { get; set; }
+ public bool IsCurrent { get; set; }
+ public bool HasSettings { get; set; }
+ }
+}
diff --git a/src/Blogifier.Core/Data/Models/WidgetsModel.cs b/src/Blogifier.Core/Data/Models/WidgetsModel.cs
new file mode 100644
index 000000000..51469eb15
--- /dev/null
+++ b/src/Blogifier.Core/Data/Models/WidgetsModel.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+
+namespace Blogifier.Core.Data
+{
+ public class WidgetsModel
+ {
+ public List Widgets { get; set; }
+ }
+
+ public class WidgetItem
+ {
+ public string Widget { get; set; }
+ public string Title { get; set; }
+ }
+
+ public class ThemeWidget
+ {
+ public string Theme { get; set; }
+ public WidgetItem Widget { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Blogifier.Core/Data/Repositories/AuthorRepository.cs b/src/Blogifier.Core/Data/Repositories/AuthorRepository.cs
new file mode 100644
index 000000000..2db5c0ea8
--- /dev/null
+++ b/src/Blogifier.Core/Data/Repositories/AuthorRepository.cs
@@ -0,0 +1,117 @@
+using Blogifier.Core.Helpers;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Threading.Tasks;
+
+namespace Blogifier.Core.Data
+{
+ public interface IAuthorRepository : IRepository
+ {
+ Task GetItem(Expression> predicate, bool sanitized = false);
+ Task> GetList(Expression> predicate, Pager pager, bool sanitize = false);
+ Task Save(Author author);
+ Task Remove(int id);
+ }
+
+ public class AuthorRepository : Repository, IAuthorRepository
+ {
+ AppDbContext _db;
+
+ public AuthorRepository(AppDbContext db) : base(db)
+ {
+ _db = db;
+ }
+
+ public async Task GetItem(Expression> predicate, bool sanitized = false)
+ {
+ try
+ {
+ var author = _db.Authors.Single(predicate);
+
+ if (author != null)
+ {
+ author.Avatar = author.Avatar ?? AppSettings.Avatar;
+ author.Email = sanitized ? Constants.DummyEmail : author.Email;
+ }
+
+ return await Task.FromResult(author);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ public async Task> GetList(Expression> predicate, Pager pager, bool sanitize = false)
+ {
+ var take = pager.ItemsPerPage == 0 ? 10 : pager.ItemsPerPage;
+ var skip = pager.CurrentPage * take - take;
+
+ var users = _db.Authors.Where(predicate)
+ .OrderBy(u => u.DisplayName).ToList();
+
+ pager.Configure(users.Count);
+
+ var list = users.Skip(skip).Take(take).ToList();
+
+ foreach (var item in list)
+ {
+ if (string.IsNullOrEmpty(item.Avatar))
+ item.Avatar = Constants.DefaultAvatar;
+
+ if (sanitize)
+ item.Email = Constants.DummyEmail;
+ }
+
+ if (sanitize)
+ {
+ foreach (var item in list)
+ {
+ item.Email = Constants.DummyEmail;
+ }
+ }
+
+ return await Task.FromResult(list);
+ }
+
+ public async Task Save(Author author)
+ {
+ if (author.Created == DateTime.MinValue)
+ {
+ author.DisplayName = author.AppUserName;
+ author.Avatar = AppSettings.Avatar;
+ author.Created = SystemClock.Now();
+ await _db.Authors.AddAsync(author);
+ }
+ else
+ {
+ var dbAuthor = _db.Authors.Single(a => a.Id == author.Id);
+
+ dbAuthor.DisplayName = author.DisplayName;
+ dbAuthor.Avatar = author.Avatar;
+ dbAuthor.Email = author.Email;
+ dbAuthor.Bio = author.Bio;
+ dbAuthor.IsAdmin = author.IsAdmin;
+ dbAuthor.Created = SystemClock.Now();
+
+ _db.Authors.Update(dbAuthor);
+ }
+
+ await _db.SaveChangesAsync();
+ }
+
+ public async Task Remove(int id)
+ {
+ var authorPosts = _db.BlogPosts
+ .Where(p => p.AuthorId == id).ToList();
+
+ if (authorPosts != null && authorPosts.Any())
+ _db.BlogPosts.RemoveRange(authorPosts);
+
+ _db.Authors.Remove(_db.Authors.Single(a => a.Id == id));
+ await _db.SaveChangesAsync();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Blogifier.Core/Data/Repositories/CustomFieldRepository.cs b/src/Blogifier.Core/Data/Repositories/CustomFieldRepository.cs
new file mode 100644
index 000000000..82787cfc6
--- /dev/null
+++ b/src/Blogifier.Core/Data/Repositories/CustomFieldRepository.cs
@@ -0,0 +1,192 @@
+using System.Linq;
+using System.Threading.Tasks;
+using System.Collections.Generic;
+
+namespace Blogifier.Core.Data
+{
+ public interface ICustomFieldRepository : IRepository
+ {
+ Task GetBlogSettings();
+ Task SaveBlogSettings(BlogItem blog);
+
+ string GetCustomValue(string name);
+ Task SaveCustomValue(string name, string value);
+
+ Task> GetSocial(int authorId = 0);
+ Task SaveSocial(SocialField socialField);
+ }
+
+ public class CustomFieldRepository : Repository, ICustomFieldRepository
+ {
+ AppDbContext _db;
+
+ public CustomFieldRepository(AppDbContext db) : base(db)
+ {
+ _db = db;
+ }
+
+ #region Basic get/set
+
+ public string GetCustomValue(string name)
+ {
+ var field = _db.CustomFields.Where(f => f.Name == name).FirstOrDefault();
+ return field == null ? "" : field.Content;
+ }
+
+ public async Task SaveCustomValue(string name, string value)
+ {
+ var field = _db.CustomFields.Where(f => f.Name == name).FirstOrDefault();
+ if (field == null)
+ {
+ _db.CustomFields.Add(new CustomField { Name = name, Content = value, AuthorId = 0 });
+ }
+ else
+ {
+ field.Content = value;
+ }
+ await _db.SaveChangesAsync();
+ }
+
+ #endregion
+
+ #region Blog setttings
+
+ public async Task GetBlogSettings()
+ {
+ var blog = new BlogItem();
+ CustomField title, desc, items, cover, logo, theme, culture, includefeatured, headerscript, footerscript;
+
+ title = _db.CustomFields.Where(f => f.AuthorId == 0 && f.Name == Constants.BlogTitle).FirstOrDefault();
+ desc = _db.CustomFields.Where(f => f.AuthorId == 0 && f.Name == Constants.BlogDescription).FirstOrDefault();
+ items = _db.CustomFields.Where(f => f.AuthorId == 0 && f.Name == Constants.BlogItemsPerPage).FirstOrDefault();
+ cover = _db.CustomFields.Where(f => f.AuthorId == 0 && f.Name == Constants.BlogCover).FirstOrDefault();
+ logo = _db.CustomFields.Where(f => f.AuthorId == 0 && f.Name == Constants.BlogLogo).FirstOrDefault();
+ theme = _db.CustomFields.Where(f => f.AuthorId == 0 && f.Name == Constants.BlogTheme).FirstOrDefault();
+ culture = _db.CustomFields.Where(f => f.AuthorId == 0 && f.Name == Constants.Culture).FirstOrDefault();
+ includefeatured = _db.CustomFields.Where(f => f.AuthorId == 0 && f.Name == Constants.IncludeFeatured).FirstOrDefault();
+ headerscript = _db.CustomFields.Where(f => f.AuthorId == 0 && f.Name == Constants.HeaderScript).FirstOrDefault();
+ footerscript = _db.CustomFields.Where(f => f.AuthorId == 0 && f.Name == Constants.FooterScript).FirstOrDefault();
+
+ blog.Title = title == null ? "Blog Title" : title.Content;
+ blog.Description = desc == null ? "Short blog description" : desc.Content;
+ blog.ItemsPerPage = items == null ? 10 : int.Parse(items.Content);
+ blog.Cover = cover == null ? "admin/img/cover.png" : cover.Content;
+ blog.Logo = logo == null ? "admin/img/logo-white.png" : logo.Content;
+ blog.Theme = theme == null ? "Standard" : theme.Content;
+ blog.Culture = culture == null ? "en-US" : culture.Content;
+ blog.IncludeFeatured = includefeatured == null ? false : bool.Parse(includefeatured.Content);
+ blog.HeaderScript = headerscript == null ? "" : headerscript.Content;
+ blog.FooterScript = footerscript == null ? "" : footerscript.Content;
+ blog.SocialFields = await GetSocial();
+
+ return await Task.FromResult(blog);
+ }
+
+ public async Task SaveBlogSettings(BlogItem blog)
+ {
+ var title = _db.CustomFields.Where(f => f.AuthorId == 0 && f.Name == Constants.BlogTitle).FirstOrDefault();
+ var desc = _db.CustomFields.Where(f => f.AuthorId == 0 && f.Name == Constants.BlogDescription).FirstOrDefault();
+ var items = _db.CustomFields.Where(f => f.AuthorId == 0 && f.Name == Constants.BlogItemsPerPage).FirstOrDefault();
+ var cover = _db.CustomFields.Where(f => f.AuthorId == 0 && f.Name == Constants.BlogCover).FirstOrDefault();
+ var logo = _db.CustomFields.Where(f => f.AuthorId == 0 && f.Name == Constants.BlogLogo).FirstOrDefault();
+ var culture = _db.CustomFields.Where(f => f.AuthorId == 0 && f.Name == Constants.Culture).FirstOrDefault();
+ var theme = _db.CustomFields.Where(f => f.AuthorId == 0 && f.Name == Constants.BlogTheme).FirstOrDefault();
+ var includefeatured = _db.CustomFields.Where(f => f.AuthorId == 0 && f.Name == Constants.IncludeFeatured).FirstOrDefault();
+ var headerscript = _db.CustomFields.Where(f => f.AuthorId == 0 && f.Name == Constants.HeaderScript).FirstOrDefault();
+ var footerscript = _db.CustomFields.Where(f => f.AuthorId == 0 && f.Name == Constants.FooterScript).FirstOrDefault();
+
+ if (title == null) _db.CustomFields.Add(new CustomField { AuthorId = 0, Name = Constants.BlogTitle, Content = blog.Title });
+ else title.Content = blog.Title;
+
+ if (desc == null) _db.CustomFields.Add(new CustomField { AuthorId = 0, Name = Constants.BlogDescription, Content = blog.Description });
+ else desc.Content = blog.Description;
+
+ if (items == null) _db.CustomFields.Add(new CustomField { AuthorId = 0, Name = Constants.BlogItemsPerPage, Content = blog.ItemsPerPage.ToString() });
+ else items.Content = blog.ItemsPerPage.ToString();
+
+ if (cover == null) _db.CustomFields.Add(new CustomField { AuthorId = 0, Name = Constants.BlogCover, Content = blog.Cover });
+ else cover.Content = blog.Cover;
+
+ if (logo == null) _db.CustomFields.Add(new CustomField { AuthorId = 0, Name = Constants.BlogLogo, Content = blog.Logo });
+ else logo.Content = blog.Logo;
+
+ if (culture == null) _db.CustomFields.Add(new CustomField { AuthorId = 0, Name = Constants.Culture, Content = blog.Culture });
+ else culture.Content = blog.Culture;
+
+ if (theme == null) _db.CustomFields.Add(new CustomField { AuthorId = 0, Name = Constants.BlogTheme, Content = blog.Theme });
+ else theme.Content = blog.Theme;
+
+ if (includefeatured == null) _db.CustomFields.Add(new CustomField { AuthorId = 0, Name = Constants.IncludeFeatured, Content = blog.IncludeFeatured.ToString() });
+ else includefeatured.Content = blog.IncludeFeatured.ToString();
+
+ if (headerscript == null) _db.CustomFields.Add(new CustomField { AuthorId = 0, Name = Constants.HeaderScript, Content = blog.HeaderScript });
+ else headerscript.Content = blog.HeaderScript;
+
+ if (footerscript == null) _db.CustomFields.Add(new CustomField { AuthorId = 0, Name = Constants.FooterScript, Content = blog.FooterScript });
+ else footerscript.Content = blog.FooterScript;
+
+ await _db.SaveChangesAsync();
+ }
+
+ #endregion
+
+ #region Social fields
+
+ ///
+ /// This depends on convetion - custom fields must be saved in the common format
+ /// For example: Name = "social|facebook|1" and Content = "http://your.facebook.page.com"
+ ///
+ /// Author ID or 0 if field is blog level
+ /// List of fields normally used to build social buttons in UI
+ public async Task> GetSocial(int authorId = 0)
+ {
+ var socials = new List();
+ var customFields = _db.CustomFields.Where(f => f.Name.StartsWith("social|") && f.AuthorId == authorId);
+
+ if (customFields.Any())
+ {
+ foreach (CustomField field in customFields)
+ {
+ var fieldArray = field.Name.Split('|');
+ if (fieldArray.Length > 2)
+ {
+ socials.Add(new SocialField
+ {
+ Title = fieldArray[1].Capitalize(),
+ Icon = $"fa-{fieldArray[1]}",
+ Rank = int.Parse(fieldArray[2]),
+ Id = field.Id,
+ Name = field.Name,
+ AuthorId = field.AuthorId,
+ Content = field.Content
+ });
+ }
+ }
+ }
+ return await Task.FromResult(socials.OrderBy(s => s.Rank).ToList());
+ }
+
+ public async Task SaveSocial(SocialField socialField)
+ {
+ var field = _db.CustomFields.Where(f => f.AuthorId == socialField.AuthorId
+ && f.Name.ToLower().StartsWith($"social|{socialField.Title.ToLower()}")).FirstOrDefault();
+
+ if (field == null)
+ {
+ _db.CustomFields.Add(new CustomField {
+ Name = socialField.Name,
+ Content = socialField.Content,
+ AuthorId = socialField.AuthorId
+ });
+ }
+ else
+ {
+ field.Content = socialField.Content;
+ field.Name = socialField.Name;
+ }
+ await _db.SaveChangesAsync();
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/Blogifier.Core/Data/Repositories/HtmlWidgetRepository.cs b/src/Blogifier.Core/Data/Repositories/HtmlWidgetRepository.cs
new file mode 100644
index 000000000..b1e0bc809
--- /dev/null
+++ b/src/Blogifier.Core/Data/Repositories/HtmlWidgetRepository.cs
@@ -0,0 +1,16 @@
+namespace Blogifier.Core.Data
+{
+ public interface IHtmlWidgetRepository : IRepository
+ {
+ }
+
+ public class HtmlWidgetRepository : Repository, IHtmlWidgetRepository
+ {
+ AppDbContext _db;
+
+ public HtmlWidgetRepository(AppDbContext db) : base(db)
+ {
+ _db = db;
+ }
+ }
+}
diff --git a/src/Blogifier.Core/Data/Repositories/NewsletterRepository.cs b/src/Blogifier.Core/Data/Repositories/NewsletterRepository.cs
new file mode 100644
index 000000000..a74f918ab
--- /dev/null
+++ b/src/Blogifier.Core/Data/Repositories/NewsletterRepository.cs
@@ -0,0 +1,39 @@
+using Blogifier.Core.Helpers;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Threading.Tasks;
+
+namespace Blogifier.Core.Data
+{
+ public interface INewsletterRepository : IRepository
+ {
+ Task> GetList(Expression> predicate, Pager pager);
+ }
+
+ public class NewsletterRepository : Repository, INewsletterRepository
+ {
+ AppDbContext _db;
+
+ public NewsletterRepository(AppDbContext db) : base(db)
+ {
+ _db = db;
+ }
+
+ public async Task> GetList(Expression> predicate, Pager pager)
+ {
+ var take = pager.ItemsPerPage == 0 ? 10 : pager.ItemsPerPage;
+ var skip = pager.CurrentPage * take - take;
+
+ var emails = _db.Newsletters.Where(predicate)
+ .OrderByDescending(e => e.Id).ToList();
+
+ pager.Configure(emails.Count);
+
+ var list = emails.Skip(skip).Take(take).ToList();
+
+ return await Task.FromResult(list);
+ }
+ }
+}
diff --git a/src/Blogifier.Core/Data/Repositories/NotificationRepository.cs b/src/Blogifier.Core/Data/Repositories/NotificationRepository.cs
new file mode 100644
index 000000000..48477cc66
--- /dev/null
+++ b/src/Blogifier.Core/Data/Repositories/NotificationRepository.cs
@@ -0,0 +1,39 @@
+using Blogifier.Core.Helpers;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Threading.Tasks;
+
+namespace Blogifier.Core.Data
+{
+ public interface INotificationRepository : IRepository
+ {
+ Task> GetList(Expression> predicate, Pager pager);
+ }
+
+ public class NotificationRepository : Repository, INotificationRepository
+ {
+ AppDbContext _db;
+
+ public NotificationRepository(AppDbContext db) : base(db)
+ {
+ _db = db;
+ }
+
+ public async Task> GetList(Expression> predicate, Pager pager)
+ {
+ var take = pager.ItemsPerPage == 0 ? 10 : pager.ItemsPerPage;
+ var skip = pager.CurrentPage * take - take;
+
+ var messages = _db.Notifications.Where(predicate)
+ .OrderByDescending(e => e.Id).ToList();
+
+ pager.Configure(messages.Count);
+
+ var list = messages.Skip(skip).Take(take).ToList();
+
+ return await Task.FromResult(list);
+ }
+ }
+}
diff --git a/src/Blogifier.Core/Data/Repositories/PostRepository.cs b/src/Blogifier.Core/Data/Repositories/PostRepository.cs
new file mode 100644
index 000000000..83bf9ff27
--- /dev/null
+++ b/src/Blogifier.Core/Data/Repositories/PostRepository.cs
@@ -0,0 +1,377 @@
+using Blogifier.Core.Helpers;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Dynamic.Core;
+using System.Linq.Expressions;
+using System.Text.RegularExpressions;
+using System.Threading.Tasks;
+
+namespace Blogifier.Core.Data
+{
+ public interface IPostRepository : IRepository
+ {
+ Task> GetList(Expression> predicate, Pager pager);
+ Task> GetList(Pager pager, int author = 0, string category = "", string include = "", bool sanitize = false);
+ Task> GetPopular(Pager pager, int author = 0);
+ Task> Search(Pager pager, string term, int author = 0, string include = "", bool sanitize = false);
+ Task GetItem(Expression> predicate, bool sanitize = false);
+ Task GetModel(string slug);
+ Task SaveItem(PostItem item);
+ Task SaveCover(int postId, string asset);
+ Task> Categories();
+ }
+
+ public class PostRepository : Repository, IPostRepository
+ {
+ AppDbContext _db;
+ ICustomFieldRepository _customFieldRepository;
+
+ public PostRepository(AppDbContext db, ICustomFieldRepository customFieldRepository) : base(db)
+ {
+ _db = db;
+ _customFieldRepository = customFieldRepository;
+ }
+
+ public async Task> GetList(Expression> predicate, Pager pager)
+ {
+ var skip = pager.CurrentPage * pager.ItemsPerPage - pager.ItemsPerPage;
+
+ var drafts = _db.BlogPosts
+ .Where(p => p.Published == DateTime.MinValue).Where(predicate)
+ .OrderByDescending(p => p.Published).ToList();
+
+ var pubs = _db.BlogPosts
+ .Where(p => p.Published > DateTime.MinValue).Where(predicate)
+ .OrderByDescending(p => p.IsFeatured)
+ .ThenByDescending(p => p.Published).ToList();
+
+ var items = drafts.Concat(pubs).ToList();
+ pager.Configure(items.Count);
+
+ var postPage = items.Skip(skip).Take(pager.ItemsPerPage).ToList();
+
+ return await Task.FromResult(PostListToItems(postPage));
+ }
+
+ public async Task> GetList(Pager pager, int author = 0, string category = "", string include = "", bool sanitize = true)
+ {
+ var skip = pager.CurrentPage * pager.ItemsPerPage - pager.ItemsPerPage;
+
+ var posts = new List();
+ foreach (var p in GetPosts(include, author))
+ {
+ if (string.IsNullOrEmpty(category))
+ {
+ posts.Add(p);
+ }
+ else
+ {
+ if (!string.IsNullOrEmpty(p.Categories))
+ {
+ var cats = p.Categories.ToLower().Split(',');
+ if (cats.Contains(category.ToLower()))
+ {
+ posts.Add(p);
+ }
+ }
+ }
+ }
+ pager.Configure(posts.Count);
+
+ var items = new List();
+ foreach (var p in posts.Skip(skip).Take(pager.ItemsPerPage).ToList())
+ {
+ items.Add(await PostToItem(p, sanitize));
+ }
+ return await Task.FromResult(items);
+ }
+
+ public async Task> GetPopular(Pager pager, int author = 0)
+ {
+ var skip = pager.CurrentPage * pager.ItemsPerPage - pager.ItemsPerPage;
+
+ var posts = new List();
+
+ if(author > 0)
+ {
+ posts = _db.BlogPosts.Where(p => p.Published > DateTime.MinValue && p.AuthorId == author)
+ .OrderByDescending(p => p.PostViews).ThenByDescending(p => p.Published).ToList();
+ }
+ else
+ {
+ posts = _db.BlogPosts.Where(p => p.Published > DateTime.MinValue)
+ .OrderByDescending(p => p.PostViews).ThenByDescending(p => p.Published).ToList();
+ }
+
+ pager.Configure(posts.Count);
+
+ var items = new List();
+ foreach (var p in posts.Skip(skip).Take(pager.ItemsPerPage).ToList())
+ {
+ items.Add(await PostToItem(p, true));
+ }
+ return await Task.FromResult(items);
+ }
+
+ public async Task> Search(Pager pager, string term, int author = 0, string include = "", bool sanitize = false)
+ {
+ var skip = pager.CurrentPage * pager.ItemsPerPage - pager.ItemsPerPage;
+
+ var results = new List();
+ foreach (var p in GetPosts(include, author))
+ {
+ var rank = 0;
+ var hits = 0;
+ term = term.ToLower();
+
+ if (p.Title.ToLower().Contains(term))
+ {
+ hits = Regex.Matches(p.Title.ToLower(), term).Count;
+ rank += hits * 10;
+ }
+ if (p.Description.ToLower().Contains(term))
+ {
+ hits = Regex.Matches(p.Description.ToLower(), term).Count;
+ rank += hits * 3;
+ }
+ if (p.Content.ToLower().Contains(term))
+ {
+ rank += Regex.Matches(p.Content.ToLower(), term).Count;
+ }
+
+ if (rank > 0)
+ {
+ results.Add(new SearchResult { Rank = rank, Item = await PostToItem(p, sanitize) });
+ }
+ }
+
+ results = results.OrderByDescending(r => r.Rank).ToList();
+
+ var posts = new List();
+ for (int i = 0; i < results.Count; i++)
+ {
+ posts.Add(results[i].Item);
+ }
+ pager.Configure(posts.Count);
+ return await Task.Run(() => posts.Skip(skip).Take(pager.ItemsPerPage).ToList());
+ }
+
+ public async Task GetItem(Expression> predicate, bool sanitize = false)
+ {
+ var post = _db.BlogPosts.Single(predicate);
+ var item = await PostToItem(post);
+
+ item.Author.Avatar = string.IsNullOrEmpty(item.Author.Avatar) ? Constants.DefaultAvatar : item.Author.Avatar;
+ item.Author.Email = sanitize ? Constants.DummyEmail : item.Author.Email;
+
+ post.PostViews++;
+ await _db.SaveChangesAsync();
+
+ return await Task.FromResult(item);
+ }
+
+ public async Task GetModel(string slug)
+ {
+ var model = new PostModel();
+
+ var all = _db.BlogPosts
+ .OrderByDescending(p => p.IsFeatured)
+ .ThenByDescending(p => p.Published).ToList();
+
+ if(all != null && all.Count > 0)
+ {
+ for (int i = 0; i < all.Count; i++)
+ {
+ if(all[i].Slug == slug)
+ {
+ model.Post = await PostToItem(all[i]);
+
+ if(i > 0 && all[i - 1].Published > DateTime.MinValue)
+ {
+ model.Newer = await PostToItem(all[i - 1]);
+ }
+
+ if (i + 1 < all.Count && all[i + 1].Published > DateTime.MinValue)
+ {
+ model.Older = await PostToItem(all[i + 1]);
+ }
+
+ break;
+ }
+ }
+ }
+
+ var post = _db.BlogPosts.Single(p => p.Slug == slug);
+ post.PostViews++;
+ await _db.SaveChangesAsync();
+
+ return await Task.FromResult(model);
+ }
+
+ public async Task SaveItem(PostItem item)
+ {
+ BlogPost post;
+ var field = _db.CustomFields.Where(f => f.AuthorId == 0 && f.Name == Constants.BlogCover).FirstOrDefault();
+ var cover = field == null ? "" : field.Content;
+
+ if (item.Id == 0)
+ {
+ post = new BlogPost
+ {
+ Title = item.Title,
+ Slug = item.Slug,
+ Content = item.Content,
+ Description = item.Description ?? item.Title,
+ Categories = item.Categories,
+ Cover = item.Cover ?? cover,
+ AuthorId = item.Author.Id,
+ IsFeatured = item.Featured,
+ Published = item.Published
+ };
+ _db.BlogPosts.Add(post);
+ await _db.SaveChangesAsync();
+
+ post = _db.BlogPosts.Single(p => p.Slug == post.Slug);
+ item = await PostToItem(post);
+ }
+ else
+ {
+ post = _db.BlogPosts.Single(p => p.Id == item.Id);
+
+ post.Slug = item.Slug;
+ post.Title = item.Title;
+ post.Content = item.Content;
+ post.Description = item.Description ?? item.Title;
+ post.Categories = item.Categories;
+ post.AuthorId = item.Author.Id;
+ post.Published = item.Published;
+ post.IsFeatured = item.Featured;
+ await _db.SaveChangesAsync();
+ }
+ return await Task.FromResult(item);
+ }
+
+ public async Task SaveCover(int postId, string asset)
+ {
+ var item = _db.BlogPosts.Single(p => p.Id == postId);
+ item.Cover = asset;
+
+ await _db.SaveChangesAsync();
+ }
+
+ public async Task> Categories()
+ {
+ var cats = new List();
+
+ if (_db.BlogPosts.Any())
+ {
+ foreach (var p in _db.BlogPosts.Where(p => p.Categories != null && p.Published > DateTime.MinValue))
+ {
+ var postcats = p.Categories.ToLower().Split(',');
+ if (postcats.Any())
+ {
+ foreach (var pc in postcats)
+ {
+ if (!cats.Exists(c => c.Category.ToLower() == pc.ToLower()))
+ {
+ cats.Add(new CategoryItem { Category = pc, PostCount = 1 });
+ }
+ else
+ {
+ // update post count
+ var tmp = cats.Where(c => c.Category.ToLower() == pc.ToLower()).FirstOrDefault();
+ tmp.PostCount++;
+ }
+ }
+ }
+ }
+ }
+ return await Task.FromResult(cats.OrderBy(c => c));
+ }
+
+ async Task PostToItem(BlogPost p, bool sanitize = false)
+ {
+ var post = new PostItem
+ {
+ Id = p.Id,
+ Slug = p.Slug,
+ Title = p.Title,
+ Description = p.Description,
+ Content = p.Content,
+ Categories = p.Categories,
+ Cover = p.Cover,
+ PostViews = p.PostViews,
+ Rating = p.Rating,
+ Published = p.Published,
+ Featured = p.IsFeatured,
+ Author = _db.Authors.Single(a => a.Id == p.AuthorId),
+ SocialFields = await _customFieldRepository.GetSocial(p.AuthorId)
+ };
+
+ if(post.Author != null)
+ {
+ post.Author.Avatar = string.IsNullOrEmpty(post.Author.Avatar) ?
+ AppSettings.Avatar : post.Author.Avatar;
+ post.Author.Email = sanitize ? Constants.DummyEmail : post.Author.Email;
+ }
+ return post;
+ }
+
+ public List PostListToItems(List posts)
+ {
+ return posts.Select(p => new PostItem
+ {
+ Id = p.Id,
+ Slug = p.Slug,
+ Title = p.Title,
+ Description = p.Description,
+ Content = p.Content,
+ Categories = p.Categories,
+ Cover = p.Cover,
+ PostViews = p.PostViews,
+ Rating = p.Rating,
+ Published = p.Published,
+ Featured = p.IsFeatured,
+ Author = _db.Authors.Single(a => a.Id == p.AuthorId)
+ }).Distinct().ToList();
+ }
+
+ List GetPosts(string include, int author)
+ {
+ var items = new List();
+
+ if (include.ToUpper().Contains("D") || string.IsNullOrEmpty(include))
+ {
+ var drafts = author > 0 ?
+ _db.BlogPosts.Where(p => p.Published == DateTime.MinValue && p.AuthorId == author).ToList() :
+ _db.BlogPosts.Where(p => p.Published == DateTime.MinValue).ToList();
+ items = items.Concat(drafts).ToList();
+ }
+
+ if (include.ToUpper().Contains("F") || string.IsNullOrEmpty(include))
+ {
+ var featured = author > 0 ?
+ _db.BlogPosts.Where(p => p.Published > DateTime.MinValue && p.IsFeatured && p.AuthorId == author).OrderByDescending(p => p.Published).ToList() :
+ _db.BlogPosts.Where(p => p.Published > DateTime.MinValue && p.IsFeatured).OrderByDescending(p => p.Published).ToList();
+ items = items.Concat(featured).ToList();
+ }
+
+ if (include.ToUpper().Contains("P") || string.IsNullOrEmpty(include))
+ {
+ var published = author > 0 ?
+ _db.BlogPosts.Where(p => p.Published > DateTime.MinValue && !p.IsFeatured && p.AuthorId == author).OrderByDescending(p => p.Published).ToList() :
+ _db.BlogPosts.Where(p => p.Published > DateTime.MinValue && !p.IsFeatured).OrderByDescending(p => p.Published).ToList();
+ items = items.Concat(published).ToList();
+ }
+
+ return items;
+ }
+ }
+
+ internal class SearchResult
+ {
+ public int Rank { get; set; }
+ public PostItem Item { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Blogifier.Core/Data/Repositories/Repository.cs b/src/Blogifier.Core/Data/Repositories/Repository.cs
new file mode 100644
index 000000000..cf3313d2a
--- /dev/null
+++ b/src/Blogifier.Core/Data/Repositories/Repository.cs
@@ -0,0 +1,67 @@
+using Microsoft.EntityFrameworkCore;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+
+namespace Blogifier.Core.Data
+{
+ public interface IRepository where TEntity : class
+ {
+ TEntity Single(Expression> predicate);
+
+ IEnumerable All();
+ IEnumerable Find(Expression> predicate);
+
+ void Add(TEntity entity);
+ void AddRange(IEnumerable entities);
+
+ void Remove(TEntity entity);
+ void RemoveRange(IEnumerable entities);
+ }
+
+ public class Repository : IRepository where TEntity : class
+ {
+ protected readonly DbSet _entities;
+
+ public Repository(DbContext context)
+ {
+ _entities = context.Set();
+ }
+
+ public IEnumerable All()
+ {
+ return _entities.ToList();
+ }
+
+ public IEnumerable Find(Expression> predicate)
+ {
+ return _entities.Where(predicate);
+ }
+
+ public TEntity Single(Expression> predicate)
+ {
+ return _entities.SingleOrDefault(predicate);
+ }
+
+ public void Add(TEntity entity)
+ {
+ _entities.Add(entity);
+ }
+
+ public void AddRange(IEnumerable entities)
+ {
+ _entities.AddRange(entities);
+ }
+
+ public void Remove(TEntity entity)
+ {
+ _entities.Remove(entity);
+ }
+
+ public void RemoveRange(IEnumerable entities)
+ {
+ _entities.RemoveRange(entities);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Blogifier.Core/Extensions/DateTimeExtensions.cs b/src/Blogifier.Core/Extensions/DateTimeExtensions.cs
new file mode 100644
index 000000000..16312f7d4
--- /dev/null
+++ b/src/Blogifier.Core/Extensions/DateTimeExtensions.cs
@@ -0,0 +1,45 @@
+using System;
+
+namespace Blogifier.Core
+{
+ public static class DateTimeExtensions
+ {
+ public static string ToFriendlyDateTimeString(this DateTime Date)
+ {
+ return FriendlyDate(Date) + " @ " + Date.ToString("t").ToLower();
+ }
+
+ public static string ToFriendlyShortDateString(this DateTime Date)
+ {
+ return $"{Date.ToString("MMM dd")}, {Date.Year}";
+ }
+
+ public static string ToFriendlyDateString(this DateTime Date)
+ {
+ return FriendlyDate(Date);
+ }
+
+ static string FriendlyDate(DateTime date)
+ {
+ string FormattedDate = "";
+ if (date.Date == DateTime.Today)
+ {
+ FormattedDate = "Today";
+ }
+ else if (date.Date == DateTime.Today.AddDays(-1))
+ {
+ FormattedDate = "Yesterday";
+ }
+ else if (date.Date > DateTime.Today.AddDays(-6))
+ {
+ // *** Show the Day of the week
+ FormattedDate = date.ToString("dddd").ToString();
+ }
+ else
+ {
+ FormattedDate = date.ToString("MMMM dd, yyyy");
+ }
+ return FormattedDate;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Blogifier.Core/Extensions/ServiceCollectionExtensions.cs b/src/Blogifier.Core/Extensions/ServiceCollectionExtensions.cs
new file mode 100644
index 000000000..b6655648e
--- /dev/null
+++ b/src/Blogifier.Core/Extensions/ServiceCollectionExtensions.cs
@@ -0,0 +1,134 @@
+using Askmethat.Aspnet.JsonLocalizer.Extensions;
+using Blogifier.Core.Data;
+using Blogifier.Core.Services;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Identity;
+using Microsoft.AspNetCore.Localization;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Options;
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+
+namespace Blogifier.Core.Extensions
+{
+ public static class ServiceCollectionExtensions
+ {
+ public static void AddBlogSettings(this IServiceCollection services, IConfigurationSection section) where T : class, new()
+ {
+ services.Configure(section);
+ services.AddTransient>(provider =>
+ {
+ var options = provider.GetService>();
+ return new AppService(options);
+ });
+ }
+
+ public static IServiceCollection AddBlogServices(this IServiceCollection services)
+ {
+ services.TryAddSingleton();
+
+ services.AddScoped();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+ services.AddTransient();
+
+ services.AddTransient>();
+
+ AddBlogRepositories(services);
+
+ return services;
+ }
+
+ public static IServiceCollection AddBlogDatabase(this IServiceCollection services, IConfiguration configuration)
+ {
+ var section = configuration.GetSection("Blogifier");
+
+ services.AddBlogSettings(section);
+
+ if (section.GetValue("DbProvider") == "SqlServer")
+ {
+ AppSettings.DbOptions = options => options.UseSqlServer(section.GetValue("ConnString"));
+ }
+ else if (section.GetValue("DbProvider") == "MySql")
+ {
+ AppSettings.DbOptions = options => options.UseMySql(section.GetValue("ConnString"));
+ }
+ else if (section.GetValue("DbProvider") == "Postgres")
+ {
+ AppSettings.DbOptions = options => options.UseNpgsql(section.GetValue("ConnString"));
+ }
+ else
+ {
+ AppSettings.DbOptions = options => options.UseSqlite(section.GetValue("ConnString"));
+ }
+
+ services.AddDbContext(AppSettings.DbOptions, ServiceLifetime.Scoped);
+
+ return services;
+ }
+
+ public static IServiceCollection AddBlogLocalization(this IServiceCollection services)
+ {
+ var supportedCultures = new HashSet()
+ {
+ new CultureInfo("en-US"),
+ new CultureInfo("es-ES"),
+ new CultureInfo("pt-BR"),
+ new CultureInfo("ru-RU"),
+ new CultureInfo("zh-cn"),
+ new CultureInfo("zh-tw")
+ };
+
+ services.AddJsonLocalization(options => {
+ options.DefaultCulture = new CultureInfo("en-US");
+ options.ResourcesPath = "Resources";
+ options.SupportedCultureInfos = supportedCultures;
+ });
+
+ services.Configure(options =>
+ {
+ options.DefaultRequestCulture = new RequestCulture(culture: "en-US", uiCulture: "en-US");
+ options.SupportedCultures = supportedCultures.ToArray();
+ options.SupportedUICultures = supportedCultures.ToArray();
+ });
+
+ return services;
+ }
+
+ public static IServiceCollection AddBlogSecurity(this IServiceCollection services)
+ {
+ services.AddIdentity(options => {
+ options.Password.RequireDigit = false;
+ options.Password.RequiredLength = 4;
+ options.Password.RequireNonAlphanumeric = false;
+ options.Password.RequireUppercase = false;
+ options.Password.RequireLowercase = false;
+ options.User.AllowedUserNameCharacters = null;
+ })
+ .AddEntityFrameworkStores()
+ .AddDefaultTokenProviders();
+ return services;
+ }
+
+ private static void AddBlogRepositories(IServiceCollection services)
+ {
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Blogifier.Core/Extensions/StringExtensions.cs b/src/Blogifier.Core/Extensions/StringExtensions.cs
new file mode 100644
index 000000000..f4b3bdb14
--- /dev/null
+++ b/src/Blogifier.Core/Extensions/StringExtensions.cs
@@ -0,0 +1,283 @@
+using Markdig;
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Web;
+
+namespace Blogifier.Core
+{
+ public static class StringExtensions
+ {
+ private static readonly Regex RegexStripHtml = new Regex("<[^>]*>", RegexOptions.Compiled);
+
+ public static string StripHtml(this string str)
+ {
+ return string.IsNullOrWhiteSpace(str) ? string.Empty : RegexStripHtml.Replace(str, string.Empty).Trim();
+ }
+
+ ///
+ /// Should extract title (file name) from file path or Url
+ ///
+ /// c:\foo\test.png
+ /// test.png
+ public static string ExtractTitle(this string str)
+ {
+ if (str.Contains("\\"))
+ {
+ return string.IsNullOrWhiteSpace(str) ? string.Empty : str.Substring(str.LastIndexOf("\\")).Replace("\\", "");
+ }
+ else if (str.Contains("/"))
+ {
+ return string.IsNullOrWhiteSpace(str) ? string.Empty : str.Substring(str.LastIndexOf("/")).Replace("/", "");
+ }
+ else
+ {
+ return str;
+ }
+ }
+
+ ///
+ /// Converts title to valid URL slug
+ ///
+ /// Slug
+ public static string ToSlug(this string title)
+ {
+ var str = title.ToLowerInvariant();
+ str = str.Trim('-', '_');
+
+ if (string.IsNullOrEmpty(str))
+ return string.Empty;
+
+ var bytes = Encoding.GetEncoding("utf-8").GetBytes(str);
+ str = Encoding.UTF8.GetString(bytes);
+
+ str = Regex.Replace(str, @"\s", "-", RegexOptions.Compiled);
+
+ str = Regex.Replace(str, @"([-_]){2,}", "$1", RegexOptions.Compiled);
+
+ str = RemoveIllegalCharacters(str);
+
+ return str;
+ }
+
+ public static string ToThumb(this string img)
+ {
+ if (img.IndexOf('/') < 1) return img;
+
+ var first = img.Substring(0, img.LastIndexOf('/'));
+ var second = img.Substring(img.LastIndexOf('/'));
+
+ return $"{first}/thumbs{second}";
+ }
+
+ public static string Capitalize(this string str)
+ {
+ if (string.IsNullOrEmpty(str))
+ return string.Empty;
+ char[] a = str.ToCharArray();
+ a[0] = char.ToUpper(a[0]);
+ return new string(a);
+ }
+
+ ///
+ /// Converts post body to post description
+ ///
+ /// HTML post body
+ /// Post decription as plain text
+ public static string ToDescription(this string str)
+ {
+ str = str.StripHtml();
+ return str.Length > 300 ? str.Substring(0, 300) : str;
+ }
+
+ public static string MdToHtml(this string str)
+ {
+ var mpl = new MarkdownPipelineBuilder()
+ .UsePipeTables()
+ .UseAdvancedExtensions()
+ .Build();
+
+ return Markdown.ToHtml(str, mpl);
+ }
+
+ public static bool Contains(this string source, string toCheck, StringComparison comp)
+ {
+ return source.IndexOf(toCheck, comp) >= 0;
+ }
+
+ // true if string ends with image extension
+ public static bool IsImagePath(this string str)
+ {
+ var exts = AppSettings.ImageExtensions.Split(',');
+
+ foreach (var ext in exts)
+ {
+ if(str.EndsWith(ext, StringComparison.OrdinalIgnoreCase))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // true if string is valid email address
+ public static bool IsEmail(this string str)
+ {
+ try
+ {
+ var addr = new System.Net.Mail.MailAddress(str);
+ return addr.Address == str;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ public static string ReplaceIgnoreCase(this string str, string search, string replacement)
+ {
+ string result = Regex.Replace(
+ str,
+ Regex.Escape(search),
+ replacement.Replace("$", "$$"),
+ RegexOptions.IgnoreCase
+ );
+ return result;
+ }
+
+ public static string MaskPassword(this string str)
+ {
+ var idx = str.IndexOf("password=", StringComparison.OrdinalIgnoreCase);
+
+ if (idx >= 0)
+ {
+ var idxEnd = str.IndexOf(";", idx);
+ if (idxEnd > idx)
+ {
+ return str.Substring(0, idx) + "Password=******" + str.Substring(idxEnd);
+ }
+ }
+ return str;
+ }
+
+ public static string ToPrettySize(this int value, int decimalPlaces = 0)
+ {
+ return ((long)value).ToPrettySize(decimalPlaces);
+ }
+
+ public static string ToPrettySize(this long value, int decimalPlaces = 0)
+ {
+ const long OneKb = 1024;
+ const long OneMb = OneKb * 1024;
+ const long OneGb = OneMb * 1024;
+ const long OneTb = OneGb * 1024;
+
+ var asTb = Math.Round((double)value / OneTb, decimalPlaces);
+ var asGb = Math.Round((double)value / OneGb, decimalPlaces);
+ var asMb = Math.Round((double)value / OneMb, decimalPlaces);
+ var asKb = Math.Round((double)value / OneKb, decimalPlaces);
+
+ string chosenValue = asTb > 1 ? string.Format("{0}Tb", asTb)
+ : asGb > 1 ? string.Format("{0}Gb", asGb)
+ : asMb > 1 ? string.Format("{0}Mb", asMb)
+ : asKb > 1 ? string.Format("{0}Kb", asKb)
+ : string.Format("{0}B", Math.Round((double)value, decimalPlaces));
+ return chosenValue;
+ }
+
+ #region Helper Methods
+
+ static string RemoveIllegalCharacters(string text)
+ {
+ if (string.IsNullOrEmpty(text))
+ {
+ return text;
+ }
+
+ string[] chars = new string[] {
+ ":", "/", "?", "!", "#", "[", "]", "{", "}", "@", "*", ".", ",",
+ "\"","&", "'", "~", "$"
+ };
+
+ foreach (var ch in chars)
+ {
+ text = text.Replace(ch, string.Empty);
+ }
+
+ text = text.Replace("–", "-");
+ text = text.Replace(" ", "-");
+
+ text = RemoveUnicodePunctuation(text);
+ text = RemoveDiacritics(text);
+ text = RemoveExtraHyphen(text);
+
+ return HttpUtility.HtmlEncode(text).Replace("%", string.Empty);
+ }
+
+ static string RemoveUnicodePunctuation(string text)
+ {
+ var normalized = text.Normalize(NormalizationForm.FormD);
+ var sb = new StringBuilder();
+
+ foreach (var c in
+ normalized.Where(c => CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.InitialQuotePunctuation &&
+ CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.FinalQuotePunctuation))
+ {
+ sb.Append(c);
+ }
+
+ return sb.ToString();
+ }
+
+ static string RemoveDiacritics(string text)
+ {
+ var normalized = text.Normalize(NormalizationForm.FormD);
+ var sb = new StringBuilder();
+
+ foreach (var c in
+ normalized.Where(c => CharUnicodeInfo.GetUnicodeCategory(c) != UnicodeCategory.NonSpacingMark))
+ {
+ sb.Append(c);
+ }
+
+ return sb.ToString();
+ }
+
+ static string RemoveExtraHyphen(string text)
+ {
+ if (text.Contains("--"))
+ {
+ text = text.Replace("--", "-");
+ return RemoveExtraHyphen(text);
+ }
+
+ return text;
+ }
+
+ public static string SanitizePath(this string str)
+ {
+ if (string.IsNullOrWhiteSpace(str))
+ return string.Empty;
+
+ str = str.Replace("%2E", ".").Replace("%2F", "/");
+
+ if (str.Contains("..") || str.Contains("//"))
+ throw new ApplicationException("Invalid directory path");
+
+ return str;
+ }
+
+ public static string SanitizeFileName(this string str)
+ {
+ str = str.SanitizePath();
+
+ //TODO: add filename specific validation here
+
+ return str;
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/Blogifier.Core/Helpers/ActionFilters.cs b/src/Blogifier.Core/Helpers/ActionFilters.cs
new file mode 100644
index 000000000..2bcc0f3b8
--- /dev/null
+++ b/src/Blogifier.Core/Helpers/ActionFilters.cs
@@ -0,0 +1,40 @@
+using Blogifier.Core.Data;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.Mvc.Filters;
+using Microsoft.EntityFrameworkCore;
+
+namespace Blogifier.Core.Helpers
+{
+ public class Administrator : ActionFilterAttribute
+ {
+ DbContextOptions _options;
+
+ public Administrator()
+ {
+ var builder = new DbContextOptionsBuilder();
+ AppSettings.DbOptions(builder);
+ _options = builder.Options;
+ }
+
+ public override void OnActionExecuting(ActionExecutingContext filterContext)
+ {
+ using (var context = new AppDbContext(_options))
+ {
+ var user = filterContext.HttpContext.User.Identity.Name;
+ var author = context.Authors.SingleOrDefaultAsync(a => a.AppUserName == user).Result;
+
+ if (author == null)
+ {
+ filterContext.Result = new UnauthorizedObjectResult("Unauthenticated");
+ }
+ else
+ {
+ if (!author.IsAdmin)
+ {
+ filterContext.Result = new UnauthorizedObjectResult("Unauthorized");
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Blogifier.Core/Helpers/ActiveRouteTagHelper.cs b/src/Blogifier.Core/Helpers/ActiveRouteTagHelper.cs
new file mode 100644
index 000000000..bb1d06c33
--- /dev/null
+++ b/src/Blogifier.Core/Helpers/ActiveRouteTagHelper.cs
@@ -0,0 +1,62 @@
+using Microsoft.AspNetCore.Mvc.Rendering;
+using Microsoft.AspNetCore.Mvc.ViewFeatures;
+using Microsoft.AspNetCore.Razor.TagHelpers;
+using System.Linq;
+
+namespace Blogifier.Core.Helpers
+{
+ [HtmlTargetElement(Attributes = "is-active-route")]
+ public class ActiveRouteTagHelper : TagHelper
+ {
+ [HtmlAttributeName("asp-action")]
+ public string Action { get; set; }
+
+ [HtmlAttributeName("asp-controller")]
+ public string Controller { get; set; }
+
+ [HtmlAttributeNotBound]
+ [ViewContext]
+ public ViewContext ViewContext { get; set; }
+
+ public override void Process(TagHelperContext context, TagHelperOutput output)
+ {
+ base.Process(context, output);
+
+ if (ShouldBeActive())
+ {
+ MakeActive(output);
+ }
+
+ output.Attributes.RemoveAll("is-active-route");
+ }
+
+ private bool ShouldBeActive()
+ {
+ var url = ViewContext.RouteData.Values["page"].ToString().ToLower();
+
+ if (url.EndsWith($"{Controller}/{Action}"))
+ return true;
+
+ if (url.Contains(Controller) && string.IsNullOrEmpty(Action))
+ return true;
+
+ return false;
+ }
+
+ private void MakeActive(TagHelperOutput output)
+ {
+ var classAttr = output.Attributes.FirstOrDefault(a => a.Name == "class");
+ if (classAttr == null)
+ {
+ classAttr = new TagHelperAttribute("class", "active");
+ output.Attributes.Add(classAttr);
+ }
+ else if (classAttr.Value == null || classAttr.Value.ToString().IndexOf("active") < 0)
+ {
+ output.Attributes.SetAttribute("class", classAttr.Value == null
+ ? "active"
+ : classAttr.Value.ToString() + " active");
+ }
+ }
+ }
+}
diff --git a/src/Blogifier.Core/Helpers/ModelHelper.cs b/src/Blogifier.Core/Helpers/ModelHelper.cs
new file mode 100644
index 000000000..0e87341ef
--- /dev/null
+++ b/src/Blogifier.Core/Helpers/ModelHelper.cs
@@ -0,0 +1,26 @@
+using Microsoft.AspNetCore.Mvc.ModelBinding;
+using System.Linq;
+
+namespace Blogifier.Core.Helpers
+{
+ public class ModelHelper
+ {
+ // pull validation error key to include it in error message
+ // so we can return "key: value" and not just "value"
+ public static string GetFirstValidationError(ModelStateDictionary modelState)
+ {
+ var listError = modelState.ToDictionary(
+ m => m.Key, m => m.Value.Errors.Select(s => s.ErrorMessage)
+ .FirstOrDefault(s => s != null));
+
+ foreach (var item in listError)
+ {
+ if (!string.IsNullOrEmpty(item.Value))
+ {
+ return $"{item.Key}: {item.Value}";
+ }
+ }
+ return "";
+ }
+ }
+}
diff --git a/src/Blogifier.Core/Helpers/Pager.cs b/src/Blogifier.Core/Helpers/Pager.cs
new file mode 100644
index 000000000..26a17d368
--- /dev/null
+++ b/src/Blogifier.Core/Helpers/Pager.cs
@@ -0,0 +1,55 @@
+namespace Blogifier.Core.Helpers
+{
+ public class Pager
+ {
+ public Pager(int currentPage, int itemsPerPage = 0)
+ {
+ CurrentPage = currentPage;
+ ItemsPerPage = itemsPerPage;
+
+ if (ItemsPerPage == 0)
+ ItemsPerPage = 10;
+
+ Newer = CurrentPage - 1;
+ ShowNewer = CurrentPage > 1 ? true : false;
+
+ Older = currentPage + 1;
+ }
+
+ public void Configure(int total)
+ {
+ if (total == 0)
+ return;
+
+ if (ItemsPerPage == 0)
+ ItemsPerPage = 10;
+
+ Total = total;
+ var lastItem = CurrentPage * ItemsPerPage;
+ ShowOlder = total > lastItem ? true : false;
+ if (CurrentPage < 1 || lastItem > total + ItemsPerPage)
+ {
+ NotFound = true;
+ }
+ LastPage = (total % ItemsPerPage) == 0 ? total / ItemsPerPage : (total / ItemsPerPage) + 1;
+ if (LastPage == 0) LastPage = 1;
+ }
+
+ public int CurrentPage { get; set; } = 1;
+ public int ItemsPerPage { get; set; }
+ public int Total { get; set; }
+ public bool NotFound { get; set; }
+
+ public int Newer { get; set; }
+ public bool ShowNewer { get; set; }
+
+ public int Older { get; set; }
+ public bool ShowOlder { get; set; }
+
+ public string LinkToNewer { get; set; }
+ public string LinkToOlder { get; set; }
+
+ public string RouteValue { get; set; }
+ public int LastPage { get; set; } = 1;
+ }
+}
diff --git a/src/Blogifier.Core/Helpers/SystemClock.cs b/src/Blogifier.Core/Helpers/SystemClock.cs
new file mode 100644
index 000000000..a29a3fd3a
--- /dev/null
+++ b/src/Blogifier.Core/Helpers/SystemClock.cs
@@ -0,0 +1,30 @@
+using System;
+using System.Globalization;
+
+namespace Blogifier.Core.Helpers
+{
+ public class SystemClock
+ {
+ public static DateTime Now()
+ {
+ return DateTime.UtcNow;
+ }
+
+ public static DateTime RssPubishedToDateTime(string date)
+ {
+ DateTime result = DateTime.MinValue;
+ string[] formats = { "ddd, dd MMM yyyy HH:mm:ss zzz", "ddd, d MMM yyyy HH:mm:ss zzz" };
+
+ foreach (var str in formats)
+ {
+ try
+ {
+ result = DateTime.ParseExact(date, str, DateTimeFormatInfo.InvariantInfo);
+ return result;
+ }
+ catch { }
+ }
+ return result;
+ }
+ }
+}
diff --git a/src/Blogifier.Core/Migrations/20180810003517_InitAppDb.Designer.cs b/src/Blogifier.Core/Migrations/20180810003517_InitAppDb.Designer.cs
new file mode 100644
index 000000000..8a10658fb
--- /dev/null
+++ b/src/Blogifier.Core/Migrations/20180810003517_InitAppDb.Designer.cs
@@ -0,0 +1,297 @@
+//
+using System;
+using Blogifier.Core.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+namespace Blogifier.Core.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ [Migration("20180810003517_InitAppDb")]
+ partial class InitAppDb
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "2.1.0-rtm-30799");
+
+ modelBuilder.Entity("Core.Data.AppUser", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd();
+
+ b.Property("AccessFailedCount");
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken();
+
+ b.Property("Email")
+ .HasMaxLength(256);
+
+ b.Property("EmailConfirmed");
+
+ b.Property("LockoutEnabled");
+
+ b.Property("LockoutEnd");
+
+ b.Property("NormalizedEmail")
+ .HasMaxLength(256);
+
+ b.Property("NormalizedUserName")
+ .HasMaxLength(256);
+
+ b.Property("PasswordHash");
+
+ b.Property("PhoneNumber");
+
+ b.Property("PhoneNumberConfirmed");
+
+ b.Property("SecurityStamp");
+
+ b.Property("TwoFactorEnabled");
+
+ b.Property("UserName")
+ .HasMaxLength(256);
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedEmail")
+ .HasName("EmailIndex");
+
+ b.HasIndex("NormalizedUserName")
+ .IsUnique()
+ .HasName("UserNameIndex");
+
+ b.ToTable("AspNetUsers");
+ });
+
+ modelBuilder.Entity("Core.Data.Author", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd();
+
+ b.Property("AppUserId")
+ .HasMaxLength(160);
+
+ b.Property("AppUserName")
+ .HasMaxLength(160);
+
+ b.Property("Avatar")
+ .HasMaxLength(160);
+
+ b.Property("Bio");
+
+ b.Property("Created");
+
+ b.Property("DisplayName")
+ .IsRequired()
+ .HasMaxLength(160);
+
+ b.Property("Email");
+
+ b.Property("IsAdmin");
+
+ b.HasKey("Id");
+
+ b.ToTable("Authors");
+ });
+
+ modelBuilder.Entity("Core.Data.BlogPost", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd();
+
+ b.Property("AuthorId");
+
+ b.Property("Categories")
+ .HasMaxLength(2000);
+
+ b.Property("Content")
+ .IsRequired();
+
+ b.Property("Cover")
+ .HasMaxLength(255);
+
+ b.Property("Description")
+ .IsRequired()
+ .HasMaxLength(450);
+
+ b.Property("IsFeatured");
+
+ b.Property("PostViews");
+
+ b.Property("Published");
+
+ b.Property("Rating");
+
+ b.Property("Slug")
+ .IsRequired()
+ .HasMaxLength(160);
+
+ b.Property("Title")
+ .IsRequired()
+ .HasMaxLength(160);
+
+ b.HasKey("Id");
+
+ b.ToTable("BlogPosts");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd();
+
+ b.Property("ConcurrencyStamp")
+ .IsConcurrencyToken();
+
+ b.Property("Name")
+ .HasMaxLength(256);
+
+ b.Property("NormalizedName")
+ .HasMaxLength(256);
+
+ b.HasKey("Id");
+
+ b.HasIndex("NormalizedName")
+ .IsUnique()
+ .HasName("RoleNameIndex");
+
+ b.ToTable("AspNetRoles");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd();
+
+ b.Property("ClaimType");
+
+ b.Property("ClaimValue");
+
+ b.Property("RoleId")
+ .IsRequired();
+
+ b.HasKey("Id");
+
+ b.HasIndex("RoleId");
+
+ b.ToTable("AspNetRoleClaims");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd();
+
+ b.Property("ClaimType");
+
+ b.Property("ClaimValue");
+
+ b.Property("UserId")
+ .IsRequired();
+
+ b.HasKey("Id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("AspNetUserClaims");
+ });
+
+ modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b =>
+ {
+ b.Property("LoginProvider");
+
+ b.Property