diff --git a/Blogifier.sln b/Blogifier.sln index ae896696c..f74e47d9a 100644 --- a/Blogifier.sln +++ b/Blogifier.sln @@ -42,6 +42,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Blogifier", "src\Blogifier\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Blogifier.Widgets", "src\Blogifier.Widgets\Blogifier.Widgets.csproj", "{19384C5F-D421-4A41-AD66-8812B48F48AC}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Blogifier.Core", "src\Blogifier.Core\Blogifier.Core.csproj", "{B5E6D870-731D-4E48-B772-3751EAEBA77E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Blogifier.Core.Tests", "src\Blogifier.Core.Tests\Blogifier.Core.Tests.csproj", "{F16D9352-B301-4643-B1AE-E16AC82ADA10}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -56,6 +60,14 @@ Global {19384C5F-D421-4A41-AD66-8812B48F48AC}.Debug|Any CPU.Build.0 = Debug|Any CPU {19384C5F-D421-4A41-AD66-8812B48F48AC}.Release|Any CPU.ActiveCfg = Release|Any CPU {19384C5F-D421-4A41-AD66-8812B48F48AC}.Release|Any CPU.Build.0 = Release|Any CPU + {B5E6D870-731D-4E48-B772-3751EAEBA77E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B5E6D870-731D-4E48-B772-3751EAEBA77E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B5E6D870-731D-4E48-B772-3751EAEBA77E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B5E6D870-731D-4E48-B772-3751EAEBA77E}.Release|Any CPU.Build.0 = Release|Any CPU + {F16D9352-B301-4643-B1AE-E16AC82ADA10}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F16D9352-B301-4643-B1AE-E16AC82ADA10}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F16D9352-B301-4643-B1AE-E16AC82ADA10}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F16D9352-B301-4643-B1AE-E16AC82ADA10}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -63,6 +75,8 @@ Global GlobalSection(NestedProjects) = preSolution {0DC52C2B-3803-4BB3-A5A2-6371E94700D7} = {39614650-0D6E-4502-B87D-184366060F59} {19384C5F-D421-4A41-AD66-8812B48F48AC} = {39614650-0D6E-4502-B87D-184366060F59} + {B5E6D870-731D-4E48-B772-3751EAEBA77E} = {39614650-0D6E-4502-B87D-184366060F59} + {F16D9352-B301-4643-B1AE-E16AC82ADA10} = {39614650-0D6E-4502-B87D-184366060F59} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D601BCCC-A1D9-415B-94B5-B3C0EF4AAF0B} diff --git a/README.md b/README.md index adaa0297a..1c5f1c32e 100644 --- a/README.md +++ b/README.md @@ -23,14 +23,14 @@ via theming engine and Blazor components. ## Other Projects -Blogifier uses `Blogifier.Core` library hosted in this same -repository and published to `Nuget.org` package gallery. +Blogifier publishes `Blogifier.Core` library to the Nuget gallery. +This package used by Blogifier.SPA and can be used by other applications. -![blogifier-diagram](https://user-images.githubusercontent.com/1932785/78852494-1465b100-79e2-11ea-81f4-2d7c51f89702.png) +![blogifier-dgm](https://user-images.githubusercontent.com/1932785/81506457-1611e580-92bc-11ea-927e-b826c56ba21b.png) ## Demo site The [demo site](http://blogifier.net) is a playground to check out Blogifier features. You can write and publish posts, upload files and test application before install. -![dashboard-3](https://user-images.githubusercontent.com/1932785/77836549-1481c900-7125-11ea-812f-9bd5343274f9.png) +![screenshot](https://user-images.githubusercontent.com/1932785/81506584-faf3a580-92bc-11ea-92c9-fd0802d9e977.png) The [developer's blog](http://rtur.net/blog). \ No newline at end of file diff --git a/src/Blogifier.Core.Tests/Blog.db b/src/Blogifier.Core.Tests/Blog.db new file mode 100644 index 000000000..3f3574051 Binary files /dev/null and b/src/Blogifier.Core.Tests/Blog.db differ diff --git a/src/Blogifier.Core.Tests/Blogifier.Core.Tests.csproj b/src/Blogifier.Core.Tests/Blogifier.Core.Tests.csproj new file mode 100644 index 000000000..72fd9b758 --- /dev/null +++ b/src/Blogifier.Core.Tests/Blogifier.Core.Tests.csproj @@ -0,0 +1,35 @@ + + + + netcoreapp3.1 + + false + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + + \ No newline at end of file diff --git a/src/Blogifier.Core.Tests/Extensions/StringExtensionsTests.cs b/src/Blogifier.Core.Tests/Extensions/StringExtensionsTests.cs new file mode 100644 index 000000000..03a63a21c --- /dev/null +++ b/src/Blogifier.Core.Tests/Extensions/StringExtensionsTests.cs @@ -0,0 +1,26 @@ +using Xunit; + +namespace Blogifier.Core.Tests.Extensions +{ + public class StringExtensionsTests + { + [Theory] + [InlineData("tes't, #one", "test-one")] + [InlineData("{test [two?", "test-two")] + [InlineData("test$ ~three!", "test-three")] + [InlineData("Тест* для& --Кирил/лицы", "тест-для-кириллицы")] + public void ShouldRemoveIlligalChars(string title, string slug) + { + Assert.Equal(title.ToSlug(), slug); + } + + [Theory] + [InlineData("http://foo/bar/img.jpg", "http://foo/bar/thumbs/img.jpg")] + [InlineData("foo/bar//img-foo.jpg", "foo/bar//thumbs/img-foo.jpg")] + [InlineData("foo/bar/img.one.png", "foo/bar/thumbs/img.one.png")] + public void ShouldConvertImgPathToTumbPath(string img, string thumb) + { + Assert.Equal(img.ToThumb(), thumb); + } + } +} \ No newline at end of file diff --git a/src/Blogifier.Core.Tests/Repositories/AuthorRepositoryTests.cs b/src/Blogifier.Core.Tests/Repositories/AuthorRepositoryTests.cs new file mode 100644 index 000000000..1569400f1 --- /dev/null +++ b/src/Blogifier.Core.Tests/Repositories/AuthorRepositoryTests.cs @@ -0,0 +1,159 @@ +using Blogifier.Core.Data; +using Blogifier.Core.Helpers; +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace Blogifier.Core.Tests.Repositories +{ + public class AuthorRepositoryTests + { + private readonly IEnumerable _authors = Enumerable.Range(1, 12) + .Select(i => new Author + { + Id = i, + AppUserName = $"test{i}" + }); + + [Fact] + public async Task Can_Save_New_Author() + { + // arrange + var dbName = Guid.NewGuid().ToString(); + var db = GetMemoryDb(dbName); + var sut = new AuthorRepository(db); + + var author = new Author + { + Id = 25, + AppUserName = "Test25" + }; + + // act + await sut.Save(author); + var result = await sut.GetItem(a => a.AppUserName == "Test25"); + ClearMemoryDb(dbName); + + // assert + Assert.NotNull(result); + Assert.True(result.Id == 25); + } + + [Fact] + public async Task Can_Remove_Author() + { + // arrange + var dbName = Guid.NewGuid().ToString(); + var db = GetMemoryDb(dbName); + var sut = new AuthorRepository(db); + var author = new Author { Id = 25, AppUserName = "Test25" }; + + // act + await sut.Save(author); + var result1 = await sut.GetItem(a => a.Id == 25); + await sut.Remove(25); + var result2 = await sut.GetItem(a => a.Id == 25); + ClearMemoryDb(dbName); + + // assert + Assert.NotNull(result1); + Assert.Null(result2); + } + + [Fact] + public async Task GetItem_By_Matching_Id_Returns_1_Result() + { + // arrange + var dbName = Guid.NewGuid().ToString(); + var db = GetMemoryDb(dbName); + var sut = new AuthorRepository(db); + + // act + var result = await sut.GetItem(x => x.AppUserName == "test1"); + ClearMemoryDb(dbName); + + // assert + Assert.NotNull(result); + Assert.True(result.Id == 1); + } + + [Fact] + public async Task GetItems_By_NotMatching_Id_Returns_0_Results() + { + // arrange + var dbName = Guid.NewGuid().ToString(); + var db = GetMemoryDb(dbName); + var sut = new AuthorRepository(db); + var pager = new Pager(1); + + // act + var result = await sut.GetList(x => x.Id == 123, pager); + ClearMemoryDb(dbName); + + // assert + Assert.Empty(result); + } + + [Fact] + public async Task GetItems_By_Matching_Id_Returns_1_Result() + { + // arrange + var dbName = Guid.NewGuid().ToString(); + var db = GetMemoryDb(dbName); + var sut = new AuthorRepository(db); + var pager = new Pager(1); + + // act + var result = await sut.GetList(x => x.Id == 1, pager); + ClearMemoryDb(dbName); + + // assert + Assert.True(result.Count() == 1); + } + + [Fact] + public async Task Get_First_Page_Returns_10_Results() + { + // arrange + var dbName = Guid.NewGuid().ToString(); + var db = GetMemoryDb(dbName); + var sut = new AuthorRepository(db); + var pager = new Pager(1); + + // act + var result = await sut.GetList(x => x.Id > 0, pager); + ClearMemoryDb(dbName); + + // assert + Assert.True(result.Count() == 10); + } + + private AppDbContext GetMemoryDb(string dbName) + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(dbName).Options; + + var context = new AppDbContext(options); + + context.Authors.AddRange(_authors); + context.SaveChanges(); + + return context; + } + + private void ClearMemoryDb(string dbName) + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(dbName).Options; + + using (var context = new AppDbContext(options)) + { + context.RemoveRange(_authors); + context.SaveChanges(); + } + } + } +} \ No newline at end of file diff --git a/src/Blogifier.Core.Tests/Repositories/CustomFieldsRepositoryTests.cs b/src/Blogifier.Core.Tests/Repositories/CustomFieldsRepositoryTests.cs new file mode 100644 index 000000000..774e4b3b4 --- /dev/null +++ b/src/Blogifier.Core.Tests/Repositories/CustomFieldsRepositoryTests.cs @@ -0,0 +1,67 @@ +using Blogifier.Core.Data; +using Microsoft.EntityFrameworkCore; +using System.Threading.Tasks; +using Xunit; + +namespace Blogifier.Core.Tests.Repositories +{ + public class CustomFieldsRepositoryTests + { + [Fact] + public async Task CanSaveAndGetCustomField() + { + var db = GetSut(); + var sut = new CustomFieldRepository(db); + + sut.Add(new CustomField { AuthorId = 1, Name = "social|facebook|1", Content = "http://your.facebook.page.com" }); + await db.SaveChangesAsync(); + + var result = sut.Single(f => f.Name.Contains("social|facebook")); + Assert.NotNull(result); + + sut.Remove(result); + await db.SaveChangesAsync(); + + result = sut.Single(f => f.Name.Contains("social|facebook")); + Assert.Null(result); + } + + [Fact] + public async Task CanSaveAndGetSocialField() + { + var db = GetSut(); + var sut = new CustomFieldRepository(db); + + await sut.SaveSocial(new SocialField { + AuthorId = 0, + Title = "Facebook", + Icon = "fa-facebook", + Name = "social|facebook|1", + Rank = 1, + Content = "http://your.facebook.page.com" + }); + + var socials = await sut.GetSocial(); + Assert.NotNull(socials); + + var result = sut.Single(f => f.Name.Contains("social|facebook")); + Assert.NotNull(result); + + sut.Remove(result); + await db.SaveChangesAsync(); + + result = sut.Single(f => f.Name.Contains("social|facebook")); + Assert.Null(result); + } + + private AppDbContext GetSut() + { + var options = new DbContextOptionsBuilder() + .UseSqlite("DataSource=Blog.db").Options; + + var context = new AppDbContext(options); + + return context; + } + } +} \ No newline at end of file diff --git a/src/Blogifier.Core.Tests/Repositories/NewsletterRepositoryTests.cs b/src/Blogifier.Core.Tests/Repositories/NewsletterRepositoryTests.cs new file mode 100644 index 000000000..665787bc0 --- /dev/null +++ b/src/Blogifier.Core.Tests/Repositories/NewsletterRepositoryTests.cs @@ -0,0 +1,44 @@ +using Blogifier.Core.Data; +using Blogifier.Core.Helpers; +using Microsoft.EntityFrameworkCore; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace Blogifier.Core.Tests.Repositories +{ + public class NewsletterRepositoryTests + { + [Fact] + public async Task CanGetAndRemoveNewsletterFromDb() + { + var email = "test@test.com"; + var db = GetSut(); + var sut = new NewsletterRepository(db); + + sut.Add(new Newsletter { Email = email, Ip = "1.2.3", Created = SystemClock.Now() }); + db.SaveChanges(); + + var result = await sut.GetList(x => x.Id > 0, new Pager(1)); + Assert.NotNull(result); + int count = result.Count(); + + var existing = sut.Single(x => x.Email == email); + db.Newsletters.Remove(existing); + db.SaveChanges(); + + result = await sut.GetList(x => x.Id > 0, new Pager(1)); + Assert.True(result.Count() == count - 1); + } + + private AppDbContext GetSut() + { + var options = new DbContextOptionsBuilder() + .UseSqlite("DataSource=Blog.db").Options; + + var context = new AppDbContext(options); + + return context; + } + } +} \ No newline at end of file diff --git a/src/Blogifier.Core.Tests/Services/DataServiceTests.cs b/src/Blogifier.Core.Tests/Services/DataServiceTests.cs new file mode 100644 index 000000000..1b1575924 --- /dev/null +++ b/src/Blogifier.Core.Tests/Services/DataServiceTests.cs @@ -0,0 +1,43 @@ +using Blogifier.Core.Data; +using Blogifier.Core.Helpers; +using Blogifier.Core.Services; +using Microsoft.EntityFrameworkCore; +using System; +using System.Linq; +using System.Threading.Tasks; +using Xunit; + +namespace Blogifier.Core.Tests.Services +{ + public class DataServiceTests + { + [Fact] + public async Task GetList_All_Published_Returns_Result() + { + // arrange + var sut = GetSut(); + + // act + var result = await sut.BlogPosts.GetList(x => x.Published > DateTime.MinValue, new Pager(1)); + + // assert + Assert.True(result.ToList().Count > 0); + } + + private IDataService GetSut() + { + var options = new DbContextOptionsBuilder() + .UseSqlite("DataSource=Blog.db").Options; + + var context = new AppDbContext(options); + var customFieldRepository = new CustomFieldRepository(context); + + IPostRepository posts = new PostRepository(context, customFieldRepository); + IAuthorRepository authors = new AuthorRepository(context); + INewsletterRepository letters = new NewsletterRepository(context); + ICustomFieldRepository custom = new CustomFieldRepository(context); + + return new DataService(context, posts, authors, null, null, custom, letters); + } + } +} \ No newline at end of file diff --git a/src/Blogifier.Core.Tests/Services/EmailServiceTests.cs b/src/Blogifier.Core.Tests/Services/EmailServiceTests.cs new file mode 100644 index 000000000..b635042d9 --- /dev/null +++ b/src/Blogifier.Core.Tests/Services/EmailServiceTests.cs @@ -0,0 +1,74 @@ +using Blogifier.Core.Data; +using Blogifier.Core.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Moq; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Xunit; + +namespace Core.Tests.Services +{ + public class EmailServiceTests + { + private readonly string email = "test@test.com"; + + [Fact] + public async Task CanSendEmail() + { + var sut = GetSut(); + + bool expected = await sut.SendEmail(email, "test", "testing blogifier"); + + Assert.False(expected); + } + + [Fact] + public async Task CanSendNewsletters() + { + var sut = GetSut(); + var emails = new List { email }; + + BlogPost post = new BlogPost + { + Title = "test", + Description = "test", + Content = "test", + Slug = "test", + Published = DateTime.Now.AddDays(-2), + Cover = "", + AuthorId = 1 + }; + + int expected = await sut.SendNewsletters(post, emails, "http://blogifier.net"); + + Assert.Equal(0, expected); + } + + private SendGridService GetSut() + { + var data = new Mock(); + + var options = new DbContextOptionsBuilder() + .UseSqlite("DataSource=Blog.db").Options; + + var context = new AppDbContext(options); + var customFieldRepository = new CustomFieldRepository(context); + + IPostRepository posts = new PostRepository(context, customFieldRepository); + IAuthorRepository authors = new AuthorRepository(context); + INewsletterRepository letters = new NewsletterRepository(context); + ICustomFieldRepository custom = new CustomFieldRepository(context); + + IDataService ds = new DataService(context, posts, authors, null, null, custom, letters); + + var logger = new Mock>(); + var storage = new Mock(); + var config = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build(); + + return new SendGridService(ds, config, logger.Object, storage.Object); + } + } +} diff --git a/src/Blogifier.Core.Tests/Services/ImportServiceTests.cs b/src/Blogifier.Core.Tests/Services/ImportServiceTests.cs new file mode 100644 index 000000000..a58f62fc3 --- /dev/null +++ b/src/Blogifier.Core.Tests/Services/ImportServiceTests.cs @@ -0,0 +1,142 @@ +using Blogifier.Core.Data; +using Blogifier.Core.Helpers; +using Blogifier.Core.Services; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Threading.Tasks; +using Xunit; + +namespace Core.Tests.Services +{ + public class ImportServiceTests + { + private readonly Mock _unitOfWork = new Mock(); + private readonly Mock _authorRepository = new Mock(); + private readonly Mock _postsRepository = new Mock(); + private readonly Mock _storageService = new Mock(); + + static string _separator = Path.DirectorySeparatorChar.ToString(); + + [Fact] + public async Task CanImportFromRssFeed() + { + SetupDependencies(); + + var sut = GetSut(); + + var fileName = $"{GetAppRoot()}{_separator}_init{_separator}_test{_separator}be3.xml"; + var result = await sut.Import(fileName, "admin"); + + Assert.NotNull(result); + Assert.NotEmpty(result); + } + + private ImportService GetSut() + { + return new ImportService(_unitOfWork.Object, _storageService.Object); + } + + private void SetupDependencies() + { + var author = new Author { Id = 1, AppUserName = "admin" }; + var postItem = new PostItem { Author = author, Title = "dotnet core", Description = "test@test.com" }; + var items = new List(); + items.Add(postItem); + + _postsRepository + .Setup(x => x.GetList(It.IsAny>>(), It.IsAny())) + .Returns(Task.FromResult(items.AsEnumerable())); + _unitOfWork.Setup(x => x.BlogPosts).Returns(_postsRepository.Object); + + _authorRepository + .Setup(x => x.GetItem(It.IsAny>>(), false)) + .Returns(Task.FromResult(new Author + { + Id = 1, + AppUserName = "admin", + Email = "test@test.com" + })); + _unitOfWork.Setup(x => x.Authors).Returns(_authorRepository.Object); + + _storageService + .Setup(x => x.UploadFromWeb(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((Uri u, string s1, string s2) => Task.FromResult(new AssetItem { Url = u.ToString() })); + } + + string GetAppRoot() + { + Assembly assembly; + var assemblyName = "Blogifier.Core.Tests"; + + assembly = Assembly.Load(new AssemblyName(assemblyName)); + + var uri = new UriBuilder(assembly.CodeBase); + var path = Uri.UnescapeDataString(uri.Path); + var root = Path.GetDirectoryName(path); + root = root.Substring(0, root.IndexOf(assemblyName)); + root = root.Replace($"tests{_separator}", $"src{_separator}"); + + root = Path.Combine(root, "Blogifier"); + root = Path.Combine(root, "wwwroot"); + root = Path.Combine(root, "data"); + + return root; + } + + private AppDbContext GetDb(string dbName) + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(dbName).Options; + + var context = new AppDbContext(options); + + //context.Seed() + //context.Users.Add(new AppUser { Id = "admin", UserName = "admin" }); + //context.SaveChanges(); + + return context; + } + + public AppDbContext Context => InMemoryContext(); + private AppDbContext InMemoryContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(Guid.NewGuid().ToString()) + .EnableSensitiveDataLogging() + .Options; + var context = new AppDbContext(options); + + return context; + } + + public static UserManager TestUserManager(IUserStore store = null) where TUser : class + { + store = store ?? new Mock>().Object; + var options = new Mock>(); + var idOptions = new IdentityOptions(); + idOptions.Lockout.AllowedForNewUsers = false; + options.Setup(o => o.Value).Returns(idOptions); + var userValidators = new List>(); + var validator = new Mock>(); + userValidators.Add(validator.Object); + var pwdValidators = new List>(); + pwdValidators.Add(new PasswordValidator()); + var userManager = new UserManager(store, options.Object, new PasswordHasher(), + userValidators, pwdValidators, new UpperInvariantLookupNormalizer(), + new IdentityErrorDescriber(), null, + new Mock>>().Object); + validator.Setup(v => v.ValidateAsync(userManager, It.IsAny())) + .Returns(Task.FromResult(IdentityResult.Success)).Verifiable(); + return userManager; + } + } +} \ No newline at end of file diff --git a/src/Blogifier.Core.Tests/Services/StorageServiceTests.cs b/src/Blogifier.Core.Tests/Services/StorageServiceTests.cs new file mode 100644 index 000000000..1d6a38e51 --- /dev/null +++ b/src/Blogifier.Core.Tests/Services/StorageServiceTests.cs @@ -0,0 +1,134 @@ +using Blogifier.Core; +using Blogifier.Core.Data; +using Blogifier.Core.Helpers; +using Blogifier.Core.Services; +using Microsoft.Extensions.Logging; +using Moq; +using System; +using System.Threading.Tasks; +using Xunit; + +namespace Core.Tests.Services +{ + public class StorageServiceTests + { + IStorageService _storage; + static string _img = "cover.png"; + static Uri _uri1 = new Uri("http://blogifier.net/admin/img/" + _img); + static Uri _uri2 = new Uri("http://dnbe.net/v01/images/mp3player.png"); + static string _separator = System.IO.Path.DirectorySeparatorChar.ToString(); + private readonly Mock> _logger = new Mock>(); + + public StorageServiceTests() + { + _storage = new StorageService(null, _logger.Object); + } + + [Fact] + public void HaveValidLocation() + { + var path = string.Format("Blogifier{0}wwwroot{0}data", _separator); + Assert.EndsWith(path, _storage.Location); + } + + [Fact] + public void CanGetThemes() + { + var themes = _storage.GetThemes(); + + Assert.NotNull(themes); + } + + [Fact] + public async Task CanGetAssets() + { + // create folders and files + _storage.CreateFolder("foo"); + _storage.CreateFolder("foo/bar"); + + await _storage.UploadFromWeb(_uri1, "/", "foo"); + await _storage.UploadFromWeb(_uri2, "/", "foo/bar"); + + // get all files from folder structure + var assets = _storage.GetAssets("foo"); + + Assert.NotNull(assets); + Assert.NotEmpty(assets); + + // cleanup + _storage.DeleteFolder("foo"); + + var folder = System.IO.Path.Combine(_storage.Location, "foo"); + Assert.False(System.IO.Directory.Exists(folder)); + } + + [Fact] + public async Task CanFindAssets() + { + AppSettings.ImageExtensions = "png,jpg,gif,bmp,tiff"; + + var pager = new Pager(1); + var assets = await _storage.Find(null, pager, ""); + + Assert.NotNull(assets); + Assert.NotEmpty(assets); + } + + [Fact] + public async Task CanCreateDeleteFile() + { + var result = await _storage.UploadFromWeb(_uri1, "/"); + Assert.True(System.IO.File.Exists(result.Path)); + + _storage.DeleteFile(_img); + Assert.False(System.IO.File.Exists(result.Path)); + } + + [Fact] + public async Task CanCreateDeleteFolder() + { + var folder = System.IO.Path.Combine(_storage.Location, "foo"); + + _storage.CreateFolder("foo"); + Assert.True(System.IO.Directory.Exists(folder)); + + // not just emply folder + await _storage.UploadFromWeb(_uri1, "/", "foo"); + + _storage.DeleteFolder(folder); + Assert.False(System.IO.Directory.Exists(folder)); + } + + [Theory] + [InlineData("http://blogifier.net/admin/img/cover.png")] + [InlineData("https://raw.githubusercontent.com/blogifierdotnet/Design/master/v1.5/01.jpg")] + public async Task CanCreateCoverImagesWithThumbnails(string img) + { + AssetItem result = await _storage.UploadFromWeb(new Uri(img), "/"); + Assert.True(System.IO.File.Exists(result.Path)); + + string thumbPath = result.Path.Replace(result.Title, $"thumbs\\{result.Title}"); + Assert.True(System.IO.File.Exists(thumbPath)); + + _storage.DeleteFile(result.Title); + Assert.False(System.IO.File.Exists(result.Path)); + + _storage.DeleteFile(thumbPath); + Assert.False(System.IO.File.Exists(thumbPath)); + } + + [Theory] + [InlineData("http://blogifier.net/admin/img/avatar.png")] + public async Task SmallImagesShouldNotCreateThumbnails(string img) + { + AssetItem result = await _storage.UploadFromWeb(new Uri(img), "/"); + Assert.True(System.IO.File.Exists(result.Path)); + + string thumbPath = result.Path.Replace(result.Title, $"thumbs\\{result.Title}"); + Assert.False(System.IO.File.Exists(thumbPath)); + + _storage.DeleteFile(result.Title); + Assert.False(System.IO.File.Exists(result.Path)); + } + } +} \ No newline at end of file diff --git a/src/Blogifier.Core.Tests/Services/SyndicationServiceTests.cs b/src/Blogifier.Core.Tests/Services/SyndicationServiceTests.cs new file mode 100644 index 000000000..391644df3 --- /dev/null +++ b/src/Blogifier.Core.Tests/Services/SyndicationServiceTests.cs @@ -0,0 +1,55 @@ +using Blogifier.Core.Data; +using Blogifier.Core.Services; +using Microsoft.Extensions.Logging; +using Moq; +using System; +using System.Linq.Expressions; +using System.Threading.Tasks; + +namespace Core.Tests.Services +{ + public class SyndicationServiceTests + { + private readonly Mock _syndicationService = new Mock(); + private readonly Mock _unitOfWork = new Mock(); + private readonly Mock authorRepository = new Mock(); + private readonly Mock postRepository = new Mock(); + private readonly Mock> _logger = new Mock>(); + private readonly IStorageService _storage; + + static string _separator = System.IO.Path.DirectorySeparatorChar.ToString(); + + public SyndicationServiceTests() + { + _storage = new StorageService(null, _logger.Object); + } + + private FeedService GetSut() + { + return new FeedService(_unitOfWork.Object, _storage); // _storageService.Object); + } + + private void SetupDependencies() + { + authorRepository + .Setup(x => x.GetItem(It.IsAny>>(), false)) + .Returns(Task.FromResult(new Author + { + Id = 1, + AppUserName = "admin" + //Email = "admin@us.com" + })); + _unitOfWork.Setup(x => x.Authors).Returns(authorRepository.Object); + + postRepository + .Setup(x => x.Single(It.IsAny>>())) + .Returns(new BlogPost + { + Id = 1, + Title = "Post one", + Slug = "post-one" + }); + _unitOfWork.Setup(x => x.BlogPosts).Returns(postRepository.Object); + } + } +} diff --git a/src/Blogifier.Core.Tests/Services/WebServiceTests.cs b/src/Blogifier.Core.Tests/Services/WebServiceTests.cs new file mode 100644 index 000000000..3722fe470 --- /dev/null +++ b/src/Blogifier.Core.Tests/Services/WebServiceTests.cs @@ -0,0 +1,29 @@ +using Blogifier.Core.Services; +using Microsoft.Extensions.Configuration; +using Moq; +using System.Threading.Tasks; +using Xunit; + +namespace Core.Tests.Services +{ + public class WebServiceTests + { + private readonly Mock _unitOfWork = new Mock(); + private readonly Mock _config = new Mock(); + + private WebService GetSut() + { + return new WebService(_unitOfWork.Object, _config.Object); + } + + [Fact] + public async Task CanImportFromRssFeed() + { + var sut = GetSut(); + + var result = await sut.GetNotifications(); + + Assert.NotNull(result); + } + } +} diff --git a/src/Blogifier.Core.Tests/appsettings.json b/src/Blogifier.Core.Tests/appsettings.json new file mode 100644 index 000000000..9358b46c1 --- /dev/null +++ b/src/Blogifier.Core.Tests/appsettings.json @@ -0,0 +1,19 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "Blogifier": { + "DbProvider": "SQLite", + "ConnString": "DataSource=Blog.db", + + "SendGridApiKey": "YOUR-SENDGRID-API-KEY", + "SendGridEmailFrom": "admin@blog.com", + "SendGridEmailFromName": "Blog admin", + + "DemoMode": false + } +} \ No newline at end of file diff --git a/src/Blogifier.Core/AppConfig.cs b/src/Blogifier.Core/AppConfig.cs new file mode 100644 index 000000000..98f3a7cdf --- /dev/null +++ b/src/Blogifier.Core/AppConfig.cs @@ -0,0 +1,50 @@ +using Blogifier.Core.Data; +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; + +namespace Blogifier.Core +{ + public static class AppConfig + { + public static IEnumerable GetAssemblies(bool includeApp = false) + { + var assemblies = new List(); + try + { + foreach (string dll in Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.dll", SearchOption.TopDirectoryOnly)) + { + try + { + var assembly = Assembly.LoadFile(dll); + + if ((dll.Contains("Blogifier.dll")) && includeApp) + { + assemblies.Add(assembly); + continue; + } + + var product = assembly.GetCustomAttribute().Product; + if (product.StartsWith("Blogifier.")) + { + assemblies.Add(assembly); + } + } + catch { } + } + } + catch { } + return assemblies; + } + + public static void SetSettings(AppItem app) + { + AppSettings.Avatar = app.Avatar; + AppSettings.DemoMode = app.DemoMode; + AppSettings.ImageExtensions = app.ImageExtensions; + AppSettings.ImportTypes = app.ImportTypes; + AppSettings.SeedData = app.SeedData; + } + } +} \ No newline at end of file diff --git a/src/Blogifier.Core/AppResources.cs b/src/Blogifier.Core/AppResources.cs new file mode 100644 index 000000000..cf0ed5fc5 --- /dev/null +++ b/src/Blogifier.Core/AppResources.cs @@ -0,0 +1,13 @@ +namespace Blogifier.Core +{ + public class Resources + { + public static string Created { get; set; } = "Created"; + public static string Updated { get; set; } = "Updated"; + public static string Saved { get; set; } = "Saved"; + public static string Removed { get; set; } = "Removed"; + + public static string UserExists { get; set; } = "This user already exists"; + + } +} \ No newline at end of file diff --git a/src/Blogifier.Core/AppSettings.cs b/src/Blogifier.Core/AppSettings.cs new file mode 100644 index 000000000..f912cf48e --- /dev/null +++ b/src/Blogifier.Core/AppSettings.cs @@ -0,0 +1,44 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Reflection; + +namespace Blogifier.Core +{ + public class AppSettings + { + public static string Avatar { get; set; } + public static bool DemoMode { get; set; } + public static string ImageExtensions { get; set; } + public static string ImportTypes { get; set; } + public static bool SeedData { get; set; } + + public static string SiteRoot { get; set; } = "/"; + + public static string WebRootPath { get; set; } + public static string ContentRootPath { get; set; } + + public static int ThumbWidth { get; set; } = 432; + public static int ThumbHeight { get; set; } = 200; + + public static Action DbOptions { get; set; } + + public static string Version + { + get + { + return typeof(AppSettings) + .GetTypeInfo() + .Assembly + .GetCustomAttribute() + .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("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Core.Data.AppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Core.Data.AppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Core.Data.AppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Core.Data.AppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Blogifier.Core/Migrations/20180810003517_InitAppDb.cs b/src/Blogifier.Core/Migrations/20180810003517_InitAppDb.cs new file mode 100644 index 000000000..7a3cd124f --- /dev/null +++ b/src/Blogifier.Core/Migrations/20180810003517_InitAppDb.cs @@ -0,0 +1,292 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Blogifier.Core.Migrations +{ + public partial class InitAppDb : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(nullable: false), + Name = table.Column(maxLength: 256, nullable: true), + NormalizedName = table.Column(maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(nullable: false), + UserName = table.Column(maxLength: 256, nullable: true), + NormalizedUserName = table.Column(maxLength: 256, nullable: true), + Email = table.Column(maxLength: 256, nullable: true), + NormalizedEmail = table.Column(maxLength: 256, nullable: true), + EmailConfirmed = table.Column(nullable: false), + PasswordHash = table.Column(nullable: true), + SecurityStamp = table.Column(nullable: true), + ConcurrencyStamp = table.Column(nullable: true), + PhoneNumber = table.Column(nullable: true), + PhoneNumberConfirmed = table.Column(nullable: false), + TwoFactorEnabled = table.Column(nullable: false), + LockoutEnd = table.Column(nullable: true), + LockoutEnabled = table.Column(nullable: false), + AccessFailedCount = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Authors", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true) + .Annotation("SqlServer:ValueGenerationStrategy", + SqlServerValueGenerationStrategy.IdentityColumn) + .Annotation("MySql:ValueGenerationStrategy", + MySqlValueGenerationStrategy.IdentityColumn) + .Annotation("Npgsql:ValueGenerationStrategy", + NpgsqlValueGenerationStrategy.SerialColumn), + AppUserId = table.Column(maxLength: 160, nullable: true), + AppUserName = table.Column(maxLength: 160, nullable: true), + Email = table.Column(nullable: true), + DisplayName = table.Column(maxLength: 160, nullable: false), + Bio = table.Column(nullable: true), + Avatar = table.Column(maxLength: 160, nullable: true), + IsAdmin = table.Column(nullable: false), + Created = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Authors", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "BlogPosts", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true) + .Annotation("SqlServer:ValueGenerationStrategy", + SqlServerValueGenerationStrategy.IdentityColumn) + .Annotation("MySql:ValueGenerationStrategy", + MySqlValueGenerationStrategy.IdentityColumn) + .Annotation("Npgsql:ValueGenerationStrategy", + NpgsqlValueGenerationStrategy.SerialColumn), + AuthorId = table.Column(nullable: false), + Title = table.Column(maxLength: 160, nullable: false), + Slug = table.Column(maxLength: 160, nullable: false), + Description = table.Column(maxLength: 450, nullable: false), + Content = table.Column(nullable: false), + Categories = table.Column(maxLength: 2000, nullable: true), + Cover = table.Column(maxLength: 255, nullable: true), + PostViews = table.Column(nullable: false), + Rating = table.Column(nullable: false), + IsFeatured = table.Column(nullable: false), + Published = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_BlogPosts", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true) + .Annotation("SqlServer:ValueGenerationStrategy", + SqlServerValueGenerationStrategy.IdentityColumn) + .Annotation("MySql:ValueGenerationStrategy", + MySqlValueGenerationStrategy.IdentityColumn) + .Annotation("Npgsql:ValueGenerationStrategy", + NpgsqlValueGenerationStrategy.SerialColumn), + RoleId = table.Column(nullable: false), + ClaimType = table.Column(nullable: true), + ClaimValue = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true) + .Annotation("SqlServer:ValueGenerationStrategy", + SqlServerValueGenerationStrategy.IdentityColumn) + .Annotation("MySql:ValueGenerationStrategy", + MySqlValueGenerationStrategy.IdentityColumn) + .Annotation("Npgsql:ValueGenerationStrategy", + NpgsqlValueGenerationStrategy.SerialColumn), + UserId = table.Column(nullable: false), + ClaimType = table.Column(nullable: true), + ClaimValue = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(nullable: false), + ProviderKey = table.Column(nullable: false), + ProviderDisplayName = table.Column(nullable: true), + UserId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(nullable: false), + RoleId = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(nullable: false), + LoginProvider = table.Column(nullable: false), + Name = table.Column(nullable: false), + Value = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "Authors"); + + migrationBuilder.DropTable( + name: "BlogPosts"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + } + } +} diff --git a/src/Blogifier.Core/Migrations/20180915222836_Notifications.Designer.cs b/src/Blogifier.Core/Migrations/20180915222836_Notifications.Designer.cs new file mode 100644 index 000000000..399ec8c64 --- /dev/null +++ b/src/Blogifier.Core/Migrations/20180915222836_Notifications.Designer.cs @@ -0,0 +1,319 @@ +// +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("20180915222836_Notifications")] + partial class Notifications + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.3-rtm-32065"); + + 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("Core.Data.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("AlertType"); + + b.Property("AuthorId"); + + b.Property("Content"); + + b.Property("DateNotified"); + + b.Property("Notifier"); + + b.HasKey("Id"); + + b.ToTable("Notifications"); + }); + + 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("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Core.Data.AppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Core.Data.AppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Core.Data.AppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Core.Data.AppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Blogifier.Core/Migrations/20180915222836_Notifications.cs b/src/Blogifier.Core/Migrations/20180915222836_Notifications.cs new file mode 100644 index 000000000..d53499fa4 --- /dev/null +++ b/src/Blogifier.Core/Migrations/20180915222836_Notifications.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Blogifier.Core.Migrations +{ + public partial class Notifications : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Notifications", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true) + .Annotation("SqlServer:ValueGenerationStrategy", + SqlServerValueGenerationStrategy.IdentityColumn) + .Annotation("MySql:ValueGenerationStrategy", + MySqlValueGenerationStrategy.IdentityColumn) + .Annotation("Npgsql:ValueGenerationStrategy", + NpgsqlValueGenerationStrategy.SerialColumn), + AuthorId = table.Column(nullable: false), + AlertType = table.Column(nullable: false), + Content = table.Column(nullable: true), + DateNotified = table.Column(nullable: false), + Notifier = table.Column(nullable: true), + Active = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Notifications", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Notifications"); + } + } +} diff --git a/src/Blogifier.Core/Migrations/20180917014904_HtmlWidgets.Designer.cs b/src/Blogifier.Core/Migrations/20180917014904_HtmlWidgets.Designer.cs new file mode 100644 index 000000000..29b396582 --- /dev/null +++ b/src/Blogifier.Core/Migrations/20180917014904_HtmlWidgets.Designer.cs @@ -0,0 +1,337 @@ +// +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("20180917014904_HtmlWidgets")] + partial class HtmlWidgets + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.3-rtm-32065"); + + 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("Core.Data.HtmlWidget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Author"); + + b.Property("Content"); + + b.Property("Name"); + + b.Property("Theme"); + + b.HasKey("Id"); + + b.ToTable("HtmlWidtes"); + }); + + modelBuilder.Entity("Core.Data.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("AlertType"); + + b.Property("AuthorId"); + + b.Property("Content"); + + b.Property("DateNotified"); + + b.Property("Notifier"); + + b.HasKey("Id"); + + b.ToTable("Notifications"); + }); + + 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("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Core.Data.AppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Core.Data.AppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Core.Data.AppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Core.Data.AppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Blogifier.Core/Migrations/20180917014904_HtmlWidgets.cs b/src/Blogifier.Core/Migrations/20180917014904_HtmlWidgets.cs new file mode 100644 index 000000000..30bc35984 --- /dev/null +++ b/src/Blogifier.Core/Migrations/20180917014904_HtmlWidgets.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Blogifier.Core.Migrations +{ + public partial class HtmlWidgets : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "HtmlWidtes", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true) + .Annotation("SqlServer:ValueGenerationStrategy", + SqlServerValueGenerationStrategy.IdentityColumn) + .Annotation("MySql:ValueGenerationStrategy", + MySqlValueGenerationStrategy.IdentityColumn) + .Annotation("Npgsql:ValueGenerationStrategy", + NpgsqlValueGenerationStrategy.SerialColumn), + Name = table.Column(nullable: true), + Theme = table.Column(nullable: true), + Author = table.Column(nullable: true), + Content = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_HtmlWidtes", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "HtmlWidtes"); + } + } +} diff --git a/src/Blogifier.Core/Migrations/20181013050615_CustomFields.Designer.cs b/src/Blogifier.Core/Migrations/20181013050615_CustomFields.Designer.cs new file mode 100644 index 000000000..03f00980a --- /dev/null +++ b/src/Blogifier.Core/Migrations/20181013050615_CustomFields.Designer.cs @@ -0,0 +1,353 @@ +// +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("20181013050615_CustomFields")] + partial class CustomFields + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.4-rtm-31024"); + + 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("Core.Data.CustomField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuthorId"); + + b.Property("Content"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.ToTable("CustomFields"); + }); + + modelBuilder.Entity("Core.Data.HtmlWidget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Author"); + + b.Property("Content"); + + b.Property("Name"); + + b.Property("Theme"); + + b.HasKey("Id"); + + b.ToTable("HtmlWidtes"); + }); + + modelBuilder.Entity("Core.Data.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("AlertType"); + + b.Property("AuthorId"); + + b.Property("Content"); + + b.Property("DateNotified"); + + b.Property("Notifier"); + + b.HasKey("Id"); + + b.ToTable("Notifications"); + }); + + 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("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Core.Data.AppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Core.Data.AppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Core.Data.AppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Core.Data.AppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Blogifier.Core/Migrations/20181013050615_CustomFields.cs b/src/Blogifier.Core/Migrations/20181013050615_CustomFields.cs new file mode 100644 index 000000000..614ea4c30 --- /dev/null +++ b/src/Blogifier.Core/Migrations/20181013050615_CustomFields.cs @@ -0,0 +1,39 @@ +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Blogifier.Core.Migrations +{ + public partial class CustomFields : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "CustomFields", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true) + .Annotation("SqlServer:ValueGenerationStrategy", + SqlServerValueGenerationStrategy.IdentityColumn) + .Annotation("MySql:ValueGenerationStrategy", + MySqlValueGenerationStrategy.IdentityColumn) + .Annotation("Npgsql:ValueGenerationStrategy", + NpgsqlValueGenerationStrategy.SerialColumn), + AuthorId = table.Column(nullable: false), + Name = table.Column(nullable: true), + Content = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_CustomFields", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "CustomFields"); + } + } +} diff --git a/src/Blogifier.Core/Migrations/20181220174710_Newsletters.Designer.cs b/src/Blogifier.Core/Migrations/20181220174710_Newsletters.Designer.cs new file mode 100644 index 000000000..2b1108ae2 --- /dev/null +++ b/src/Blogifier.Core/Migrations/20181220174710_Newsletters.Designer.cs @@ -0,0 +1,347 @@ +// +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("20181220174710_Newsletters")] + partial class Newsletters + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.4-rtm-31024"); + + 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("Core.Data.CustomField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuthorId"); + + b.Property("Content"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.ToTable("CustomFields"); + }); + + modelBuilder.Entity("Core.Data.Newsletter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Email"); + + b.HasKey("Id"); + + b.ToTable("Newsletters"); + }); + + modelBuilder.Entity("Core.Data.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("AlertType"); + + b.Property("AuthorId"); + + b.Property("Content"); + + b.Property("DateNotified"); + + b.Property("Notifier"); + + b.HasKey("Id"); + + b.ToTable("Notifications"); + }); + + 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("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Core.Data.AppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Core.Data.AppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Core.Data.AppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Core.Data.AppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Blogifier.Core/Migrations/20181220174710_Newsletters.cs b/src/Blogifier.Core/Migrations/20181220174710_Newsletters.cs new file mode 100644 index 000000000..be1ed8a24 --- /dev/null +++ b/src/Blogifier.Core/Migrations/20181220174710_Newsletters.cs @@ -0,0 +1,62 @@ +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Blogifier.Core.Migrations +{ + public partial class Newsletters : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "HtmlWidtes"); + + migrationBuilder.CreateTable( + name: "Newsletters", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true) + .Annotation("SqlServer:ValueGenerationStrategy", + SqlServerValueGenerationStrategy.IdentityColumn) + .Annotation("MySql:ValueGenerationStrategy", + MySqlValueGenerationStrategy.IdentityColumn) + .Annotation("Npgsql:ValueGenerationStrategy", + NpgsqlValueGenerationStrategy.SerialColumn), + Email = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Newsletters", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Newsletters"); + + migrationBuilder.CreateTable( + name: "HtmlWidtes", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true) + .Annotation("SqlServer:ValueGenerationStrategy", + SqlServerValueGenerationStrategy.IdentityColumn) + .Annotation("MySql:ValueGenerationStrategy", + MySqlValueGenerationStrategy.IdentityColumn) + .Annotation("Npgsql:ValueGenerationStrategy", + NpgsqlValueGenerationStrategy.SerialColumn), + Author = table.Column(nullable: true), + Content = table.Column(nullable: true), + Name = table.Column(nullable: true), + Theme = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_HtmlWidtes", x => x.Id); + }); + } + } +} diff --git a/src/Blogifier.Core/Migrations/20181220175110_RebuildHtmlWidgets.Designer.cs b/src/Blogifier.Core/Migrations/20181220175110_RebuildHtmlWidgets.Designer.cs new file mode 100644 index 000000000..1e1ae4665 --- /dev/null +++ b/src/Blogifier.Core/Migrations/20181220175110_RebuildHtmlWidgets.Designer.cs @@ -0,0 +1,365 @@ +// +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("20181220175110_RebuildHtmlWidgets")] + partial class RebuildHtmlWidgets + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.4-rtm-31024"); + + 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("Core.Data.CustomField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AuthorId"); + + b.Property("Content"); + + b.Property("Name"); + + b.HasKey("Id"); + + b.ToTable("CustomFields"); + }); + + modelBuilder.Entity("Core.Data.HtmlWidget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Author"); + + b.Property("Content"); + + b.Property("Name"); + + b.Property("Theme"); + + b.HasKey("Id"); + + b.ToTable("HtmlWidgets"); + }); + + modelBuilder.Entity("Core.Data.Newsletter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Email"); + + b.HasKey("Id"); + + b.ToTable("Newsletters"); + }); + + modelBuilder.Entity("Core.Data.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Active"); + + b.Property("AlertType"); + + b.Property("AuthorId"); + + b.Property("Content"); + + b.Property("DateNotified"); + + b.Property("Notifier"); + + b.HasKey("Id"); + + b.ToTable("Notifications"); + }); + + 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("ProviderKey"); + + b.Property("ProviderDisplayName"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId"); + + b.Property("RoleId"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId"); + + b.Property("LoginProvider"); + + b.Property("Name"); + + b.Property("Value"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Core.Data.AppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Core.Data.AppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Core.Data.AppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Core.Data.AppUser") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Blogifier.Core/Migrations/20181220175110_RebuildHtmlWidgets.cs b/src/Blogifier.Core/Migrations/20181220175110_RebuildHtmlWidgets.cs new file mode 100644 index 000000000..ce7b495b7 --- /dev/null +++ b/src/Blogifier.Core/Migrations/20181220175110_RebuildHtmlWidgets.cs @@ -0,0 +1,40 @@ +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Blogifier.Core.Migrations +{ + public partial class RebuildHtmlWidgets : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "HtmlWidgets", + columns: table => new + { + Id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true) + .Annotation("SqlServer:ValueGenerationStrategy", + SqlServerValueGenerationStrategy.IdentityColumn) + .Annotation("MySql:ValueGenerationStrategy", + MySqlValueGenerationStrategy.IdentityColumn) + .Annotation("Npgsql:ValueGenerationStrategy", + NpgsqlValueGenerationStrategy.SerialColumn), + Name = table.Column(nullable: true), + Theme = table.Column(nullable: true), + Author = table.Column(nullable: true), + Content = table.Column(nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_HtmlWidgets", x => x.Id); + }); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "HtmlWidgets"); + } + } +} diff --git a/src/Blogifier.Core/Migrations/20200411175525_NewsletterIpDate.Designer.cs b/src/Blogifier.Core/Migrations/20200411175525_NewsletterIpDate.Designer.cs new file mode 100644 index 000000000..3e8986db6 --- /dev/null +++ b/src/Blogifier.Core/Migrations/20200411175525_NewsletterIpDate.Designer.cs @@ -0,0 +1,449 @@ +// +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("20200411175525_NewsletterIpDate")] + partial class NewsletterIpDate + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.3"); + + modelBuilder.Entity("Blogifier.Core.Data.AppUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Blogifier.Core.Data.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserName") + .HasColumnType("TEXT") + .HasMaxLength(160); + + b.Property("Avatar") + .HasColumnType("TEXT") + .HasMaxLength(160); + + b.Property("Bio") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(160); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("IsAdmin") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Authors"); + }); + + modelBuilder.Entity("Blogifier.Core.Data.BlogPost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("Categories") + .HasColumnType("TEXT") + .HasMaxLength(2000); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Cover") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(450); + + b.Property("IsFeatured") + .HasColumnType("INTEGER"); + + b.Property("PostViews") + .HasColumnType("INTEGER"); + + b.Property("Published") + .HasColumnType("TEXT"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(160); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(160); + + b.HasKey("Id"); + + b.ToTable("BlogPosts"); + }); + + modelBuilder.Entity("Blogifier.Core.Data.CustomField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("CustomFields"); + }); + + modelBuilder.Entity("Blogifier.Core.Data.HtmlWidget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Author") + .HasColumnType("TEXT"); + + b.Property("Content") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Theme") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("HtmlWidgets"); + }); + + modelBuilder.Entity("Blogifier.Core.Data.Newsletter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + //b.Property("Created") + // .ValueGeneratedOnAddOrUpdate() + // .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("Ip") + .HasColumnType("TEXT") + .HasMaxLength(80); + + b.HasKey("Id"); + + b.ToTable("Newsletters"); + }); + + modelBuilder.Entity("Blogifier.Core.Data.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AlertType") + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .HasColumnType("TEXT"); + + b.Property("DateNotified") + .HasColumnType("TEXT"); + + b.Property("Notifier") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasColumnType("TEXT") + .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() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Blogifier.Core.Data.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Blogifier.Core.Data.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Blogifier.Core.Data.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Blogifier.Core.Data.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Blogifier.Core/Migrations/20200411175525_NewsletterIpDate.cs b/src/Blogifier.Core/Migrations/20200411175525_NewsletterIpDate.cs new file mode 100644 index 000000000..6cfd72861 --- /dev/null +++ b/src/Blogifier.Core/Migrations/20200411175525_NewsletterIpDate.cs @@ -0,0 +1,33 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Blogifier.Core.Migrations +{ + public partial class NewsletterIpDate : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Created", + table: "Newsletters", + nullable: true); + + migrationBuilder.AddColumn( + name: "Ip", + table: "Newsletters", + nullable: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Created", + table: "Newsletters"); + + migrationBuilder.DropColumn( + name: "Ip", + table: "Newsletters"); + + } + } +} diff --git a/src/Blogifier.Core/Migrations/AppDbContextModelSnapshot.cs b/src/Blogifier.Core/Migrations/AppDbContextModelSnapshot.cs new file mode 100644 index 000000000..bca81f970 --- /dev/null +++ b/src/Blogifier.Core/Migrations/AppDbContextModelSnapshot.cs @@ -0,0 +1,447 @@ +// +using System; +using Blogifier.Core.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Blogifier.Core.Migrations +{ + [DbContext(typeof(AppDbContext))] + partial class AppDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.3"); + + modelBuilder.Entity("Blogifier.Core.Data.AppUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessFailedCount") + .HasColumnType("INTEGER"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("EmailConfirmed") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnabled") + .HasColumnType("INTEGER"); + + b.Property("LockoutEnd") + .HasColumnType("TEXT"); + + b.Property("NormalizedEmail") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("NormalizedUserName") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("PasswordHash") + .HasColumnType("TEXT"); + + b.Property("PhoneNumber") + .HasColumnType("TEXT"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("INTEGER"); + + b.Property("SecurityStamp") + .HasColumnType("TEXT"); + + b.Property("TwoFactorEnabled") + .HasColumnType("INTEGER"); + + b.Property("UserName") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasName("UserNameIndex"); + + b.ToTable("AspNetUsers"); + }); + + modelBuilder.Entity("Blogifier.Core.Data.Author", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AppUserName") + .HasColumnType("TEXT") + .HasMaxLength(160); + + b.Property("Avatar") + .HasColumnType("TEXT") + .HasMaxLength(160); + + b.Property("Bio") + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("DisplayName") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(160); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("IsAdmin") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Authors"); + }); + + modelBuilder.Entity("Blogifier.Core.Data.BlogPost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("Categories") + .HasColumnType("TEXT") + .HasMaxLength(2000); + + b.Property("Content") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Cover") + .HasColumnType("TEXT") + .HasMaxLength(255); + + b.Property("Description") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(450); + + b.Property("IsFeatured") + .HasColumnType("INTEGER"); + + b.Property("PostViews") + .HasColumnType("INTEGER"); + + b.Property("Published") + .HasColumnType("TEXT"); + + b.Property("Rating") + .HasColumnType("REAL"); + + b.Property("Slug") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(160); + + b.Property("Title") + .IsRequired() + .HasColumnType("TEXT") + .HasMaxLength(160); + + b.HasKey("Id"); + + b.ToTable("BlogPosts"); + }); + + modelBuilder.Entity("Blogifier.Core.Data.CustomField", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("CustomFields"); + }); + + modelBuilder.Entity("Blogifier.Core.Data.HtmlWidget", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Author") + .HasColumnType("TEXT"); + + b.Property("Content") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Theme") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("HtmlWidgets"); + }); + + modelBuilder.Entity("Blogifier.Core.Data.Newsletter", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Created") + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("Ip") + .HasColumnType("TEXT") + .HasMaxLength(80); + + b.HasKey("Id"); + + b.ToTable("Newsletters"); + }); + + modelBuilder.Entity("Blogifier.Core.Data.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("AlertType") + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("Content") + .HasColumnType("TEXT"); + + b.Property("DateNotified") + .HasColumnType("TEXT"); + + b.Property("Notifier") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT") + .HasMaxLength(256); + + b.Property("NormalizedName") + .HasColumnType("TEXT") + .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() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClaimType") + .HasColumnType("TEXT"); + + b.Property("ClaimValue") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("ProviderKey") + .HasColumnType("TEXT"); + + b.Property("ProviderDisplayName") + .HasColumnType("TEXT"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("LoginProvider") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Blogifier.Core.Data.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Blogifier.Core.Data.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Blogifier.Core.Data.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Blogifier.Core.Data.AppUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Blogifier.Core/Services/AppService.cs b/src/Blogifier.Core/Services/AppService.cs new file mode 100644 index 000000000..a75f5fac5 --- /dev/null +++ b/src/Blogifier.Core/Services/AppService.cs @@ -0,0 +1,21 @@ +using Microsoft.Extensions.Options; + +namespace Blogifier.Core.Services +{ + public interface IAppService : IOptionsSnapshot where T : class, new() + { + } + + public class AppService : IAppService where T : class, new() + { + private readonly IOptionsMonitor _options; + + public AppService(IOptionsMonitor options) + { + _options = options; + } + + public T Value => _options.CurrentValue; + public T Get(string name) => _options.Get(name); + } +} \ No newline at end of file diff --git a/src/Blogifier.Core/Services/DataService.cs b/src/Blogifier.Core/Services/DataService.cs new file mode 100644 index 000000000..00da15f96 --- /dev/null +++ b/src/Blogifier.Core/Services/DataService.cs @@ -0,0 +1,58 @@ +using Blogifier.Core.Data; +using System; + +namespace Blogifier.Core.Services +{ + public interface IDataService : IDisposable + { + IPostRepository BlogPosts { get; } + IAuthorRepository Authors { get; } + INotificationRepository Notifications { get; } + IHtmlWidgetRepository HtmlWidgets { get; } + ICustomFieldRepository CustomFields { get; } + INewsletterRepository Newsletters { get; } + + int Complete(); + } + + public class DataService : IDataService + { + private readonly AppDbContext _db; + + public DataService( + AppDbContext db, + IPostRepository postRepository, + IAuthorRepository authorRepository, + INotificationRepository notificationRepository, + IHtmlWidgetRepository htmlWidgetRepository, + ICustomFieldRepository customFieldRepository, + INewsletterRepository newsletterRepository) + { + _db = db; + + BlogPosts = postRepository; + Authors = authorRepository; + Notifications = notificationRepository; + HtmlWidgets = htmlWidgetRepository; + CustomFields = customFieldRepository; + Newsletters = newsletterRepository; + } + + public IPostRepository BlogPosts { get; private set; } + public IAuthorRepository Authors { get; private set; } + public INotificationRepository Notifications { get; private set; } + public IHtmlWidgetRepository HtmlWidgets { get; private set; } + public ICustomFieldRepository CustomFields { get; private set; } + public INewsletterRepository Newsletters { get; private set; } + + public int Complete() + { + return _db.SaveChanges(); + } + + public void Dispose() + { + _db.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/Blogifier.Core/Services/FeedService.cs b/src/Blogifier.Core/Services/FeedService.cs new file mode 100644 index 000000000..dcd3cba2e --- /dev/null +++ b/src/Blogifier.Core/Services/FeedService.cs @@ -0,0 +1,88 @@ +using Blogifier.Core.Helpers; +using Microsoft.SyndicationFeed; +using Microsoft.SyndicationFeed.Atom; +using Microsoft.SyndicationFeed.Rss; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Xml; + +namespace Blogifier.Core.Services +{ + public interface IFeedService + { + Task> GetEntries(string type, string host); + Task GetWriter(string type, string host, XmlWriter xmlWriter); + } + + public class FeedService : IFeedService + { + IDataService _db; + + public FeedService(IDataService db, IStorageService storage) + { + _db = db; + } + + public async Task> GetEntries(string type, string host) + { + var items = new List(); + var posts = await _db.BlogPosts.GetList(p => p.Published > DateTime.MinValue, new Pager(1)); + + foreach (var post in posts) + { + var item = new AtomEntry + { + Title = post.Title, + Description = post.Content, + Id = $"{host}/posts/{post.Slug}", + Published = post.Published, + LastUpdated = post.Published, + ContentType = "html", + }; + + if (!string.IsNullOrEmpty(post.Categories)) + { + foreach (string category in post.Categories.Split(',')) + { + item.AddCategory(new SyndicationCategory(category)); + } + } + + item.AddContributor(new SyndicationPerson(post.Author.DisplayName, post.Author.Email)); + item.AddLink(new SyndicationLink(new Uri(item.Id))); + items.Add(item); + } + + return await Task.FromResult(items); + } + + public async Task GetWriter(string type, string host, XmlWriter xmlWriter) + { + var lastPost = _db.BlogPosts.All().OrderByDescending(p => p.Published).FirstOrDefault(); + var blog = await _db.CustomFields.GetBlogSettings(); + + if (lastPost == null) + return null; + + if (type.Equals("rss", StringComparison.OrdinalIgnoreCase)) + { + var rss = new RssFeedWriter(xmlWriter); + await rss.WriteTitle(blog.Title); + await rss.WriteDescription(blog.Description); + await rss.WriteGenerator("Blogifier"); + await rss.WriteValue("link", host); + return rss; + } + + var atom = new AtomFeedWriter(xmlWriter); + await atom.WriteTitle(blog.Title); + await atom.WriteId(host); + await atom.WriteSubtitle(blog.Description); + await atom.WriteGenerator("Blogifier", "https://github.com/blogifierdotnet/Blogifier", "1.0"); + await atom.WriteValue("updated", lastPost.Published.ToString("yyyy-MM-ddTHH:mm:ssZ")); + return atom; + } + } +} \ No newline at end of file diff --git a/src/Blogifier.Core/Services/ImportService.cs b/src/Blogifier.Core/Services/ImportService.cs new file mode 100644 index 000000000..cb4376a01 --- /dev/null +++ b/src/Blogifier.Core/Services/ImportService.cs @@ -0,0 +1,301 @@ +using Blogifier.Core.Data; +using Microsoft.AspNetCore.Http; +using Microsoft.SyndicationFeed; +using Microsoft.SyndicationFeed.Rss; +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using System.Xml; +using System.Xml.Linq; + +namespace Blogifier.Core.Services +{ + public interface IImportService + { + Task> Import(IFormFile file, string user, string webRoot = "/"); + Task> Import(string fileName, string user, string webRoot = "/"); + } + + public class ImportService : IImportService + { + IDataService _db; + IStorageService _ss; + List _msgs; + string _usr; + string _url; + string _webRoot; + + public ImportService(IDataService db, IStorageService ss) + { + _db = db; + _ss = ss; + _msgs = new List(); + } + + public async Task> Import(IFormFile file, string usr, string webRoot = "/") + { + _usr = usr; + _webRoot = webRoot; + return await ImportFeed(new StreamReader(file.OpenReadStream(), Encoding.UTF8)); + } + + public async Task> Import(string fileName, string usr, string webRoot = "/") + { + _usr = usr; + _webRoot = webRoot; + return await ImportFeed(new StreamReader(fileName, Encoding.UTF8)); + } + + async Task> ImportFeed(StreamReader reader) + { + using (var xmlReader = XmlReader.Create(reader, new XmlReaderSettings() { })) + { + var feedReader = new RssFeedReader(xmlReader); + + while (await feedReader.Read()) + { + if (feedReader.ElementType == SyndicationElementType.Link) + { + var link = await feedReader.ReadLink(); + _url = link.Uri.ToString(); + + if (_url.ToLower().EndsWith("/rss")) + _url = _url.Substring(0, _url.Length - 4); + + if (_url.EndsWith("/")) + _url = _url.Substring(0, _url.Length - 1); + } + + if (feedReader.ElementType == SyndicationElementType.Item) + { + try + { + var item = await feedReader.ReadItem(); + + PostItem post = new PostItem + { + Author = await _db.Authors.GetItem(a => a.AppUserName == _usr), + Title = item.Title, + Description = item.Title, + Content = item.Description, + Slug = await GetSlug(item.Title), + Published = item.Published.DateTime, + Status = SaveStatus.Publishing + }; + + if(item.Categories != null) + { + var blogCats = new List(); + foreach (var cat in item.Categories) + { + blogCats.Add(cat.Name); + } + post.Categories = string.Join(",", blogCats); + } + + _msgs.Add(new ImportMessage { ImportType = ImportType.Post, Status = Status.Success, Message = post.Title }); + + await ImportPost(post); + } + catch (Exception ex) + { + _msgs.Add(new ImportMessage { ImportType = ImportType.Post, Status = Status.Error, Message = ex.Message }); + } + } + } + } + return await Task.FromResult(_msgs); + } + + async Task ImportPost(PostItem post) + { + await ImportImages(post); + await ImportFiles(post); + + var converter = new ReverseMarkdown.Converter(); + post.Content = converter.Convert(post.Content); + + var blog = await _db.CustomFields.GetBlogSettings(); + post.Cover = blog.Cover; + + await _db.BlogPosts.SaveItem(post); + } + + async Task ImportImages(PostItem post) + { + var links = new List(); + string rgx = @"]*?src\s*=\s*[""']?([^'"" >]+?)[ '""][^>]*?>"; + + if (string.IsNullOrEmpty(post.Content)) + return; + + var matches = Regex.Matches(post.Content, rgx, RegexOptions.IgnoreCase | RegexOptions.Singleline); + + if (matches != null) + { + foreach (Match m in matches) + { + var uri = ""; + try + { + var tag = m.Groups[0].Value; + var path = string.Format("{0}/{1}", post.Published.Year, post.Published.Month); + + uri = Regex.Match(tag, "", RegexOptions.IgnoreCase).Groups[1].Value; + + uri = ValidateUrl(uri); + + AssetItem asset; + if (uri.Contains("data:image")) + { + asset = await _ss.UploadBase64Image(uri, _webRoot, path); + } + else + { + asset = await _ss.UploadFromWeb(new Uri(uri), _webRoot, path); + } + + var mdTag = $"![{asset.Title}]({_webRoot}{asset.Url})"; + + post.Content = post.Content.ReplaceIgnoreCase(tag, mdTag); + + _msgs.Add(new ImportMessage + { + ImportType = ImportType.Image, + Status = Status.Success, + Message = $"{tag} -> {mdTag}" + }); + } + catch (Exception ex) + { + _msgs.Add(new ImportMessage + { + ImportType = ImportType.Image, + Status = Status.Error, + Message = $"{m.Groups[0].Value} -> {uri} ->{ex.Message}" + }); + } + } + } + } + + async Task ImportFiles(PostItem post) + { + var links = new List(); + var rgx = @"(?i)]*?>(?.*?)"; + string[] exts = AppSettings.ImportTypes.Split(','); + + if (string.IsNullOrEmpty(post.Content)) + return; + + MatchCollection matches = Regex.Matches(post.Content, rgx, RegexOptions.IgnoreCase | RegexOptions.Singleline); + + if (matches != null) + { + foreach (Match m in matches) + { + try + { + var tag = m.Value; + var src = XElement.Parse(tag).Attribute("href").Value; + var mdTag = ""; + + foreach (var ext in exts) + { + if (src.ToLower().EndsWith($".{ext}")) + { + var uri = ValidateUrl(src); + var path = string.Format("{0}/{1}", post.Published.Year, post.Published.Month); + var asset = await _ss.UploadFromWeb(new Uri(uri), _webRoot, path); + + mdTag = $"[{asset.Title}]({_webRoot}{asset.Url})"; + + post.Content = post.Content.ReplaceIgnoreCase(m.Value, mdTag); + + _msgs.Add(new ImportMessage + { + ImportType = ImportType.Attachement, + Status = Status.Success, + Message = $"{tag} -> {mdTag}" + }); + } + } + } + catch (Exception ex) + { + _msgs.Add(new ImportMessage { + ImportType = ImportType.Attachement, + Status = Status.Error, + Message = $"{m.Value} -> {ex.Message}" + }); + } + } + } + } + + async Task GetSlug(string title) + { + string slug = title.ToSlug(); + BlogPost post; + + post = _db.BlogPosts.Single(p => p.Slug == slug); + + if (post == null) + return await Task.FromResult(slug); + + for (int i = 2; i < 100; i++) + { + post = _db.BlogPosts.Single(p => p.Slug == $"{slug}{i}"); + + if (post == null) + { + return await Task.FromResult(slug + i.ToString()); + } + } + + return await Task.FromResult(slug); + } + + string ValidateUrl(string link) + { + var url = link; + + if (url.StartsWith("~")) + { + url = url.Replace("~", _url); + } + if (url.StartsWith("/")) + { + url = string.Concat(_url, url); + } + return url; + } + } + + public class ImportMessage + { + public ImportType ImportType { get; set; } + public Status Status { get; set; } + public string Message { get; set; } + } + + public class ImportAsset + { + public AssetType AssetType { get; set; } + public string Tag { get; set; } + public string Src { get; set; } + } + + public enum Status + { + Success, Warning, Error + } + + public enum ImportType + { + Post, Image, Attachement + } +} \ No newline at end of file diff --git a/src/Blogifier.Core/Services/NotificationService.cs b/src/Blogifier.Core/Services/NotificationService.cs new file mode 100644 index 000000000..97caa906b --- /dev/null +++ b/src/Blogifier.Core/Services/NotificationService.cs @@ -0,0 +1,69 @@ +using Blogifier.Core.Data; +using Blogifier.Core.Helpers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace Blogifier.Core.Services +{ + public interface INotificationService + { + Task PullSystemNotifications(); + Task AddNotification(AlertType aType, int authorId, string notifier, string content); + } + + public class NotificationService : INotificationService + { + static DateTime _checkPoint; + IDataService _db; + IWebService _web; + + public NotificationService(IDataService db, IWebService web) + { + _db = db; + _web = web; + } + + public async Task AddNotification(AlertType aType, int authorId, string notifier, string content) + { + var existing = _db.Notifications.Single(n => + n.AlertType == aType && + n.Notifier == notifier && + n.Content == content + ); + + if(existing == null) + { + _db.Notifications.Add(new Notification + { + AlertType = aType, + AuthorId = authorId, + Notifier = notifier, + Content = content, + Active = true, + DateNotified = SystemClock.Now() + }); + _db.Complete(); + } + return await Task.FromResult(0); + } + + public async Task PullSystemNotifications() + { + if (SystemClock.Now() >= _checkPoint) + { + _checkPoint = SystemClock.Now().AddMinutes(30); + var messages = await _web.GetNotifications(); + + if(messages != null && messages.Count > 0) + { + foreach (var msg in messages) + { + await AddNotification(AlertType.System, 0, "Blogifier", msg); + } + } + } + } + } +} \ No newline at end of file diff --git a/src/Blogifier.Core/Services/SendGridService.cs b/src/Blogifier.Core/Services/SendGridService.cs new file mode 100644 index 000000000..5ed045755 --- /dev/null +++ b/src/Blogifier.Core/Services/SendGridService.cs @@ -0,0 +1,116 @@ +using Blogifier.Core.Data; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using SendGrid; +using SendGrid.Helpers.Mail; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Blogifier.Core.Services +{ + public interface IEmailService + { + Task SendNewsletters(BlogPost postItem, List emails, string siteUrl); + Task SendEmail(string to, string subject, string content); + } + + public class SendGridService : IEmailService + { + private readonly IConfiguration _config; + private readonly ILogger _logger; + private readonly IStorageService _storage; + private readonly IDataService _db; + + public SendGridService(IDataService db, IConfiguration config, ILogger logger, IStorageService storage) + { + _db = db; + _config = config; + _logger = logger; + _storage = storage; + } + + public async Task SendNewsletters(BlogPost post, List emails, string siteUrl) + { + int sendCount = 0; + try + { + var blog = await _db.CustomFields.GetBlogSettings(); + var author = _db.Authors.Single(a => a.Id == post.AuthorId); + + foreach (var email in emails) + { + var subject = post.Title; + var content = _storage.GetHtmlTemplate("newsletter") ?? "

{3}

"; + + var htmlContent = string.Format(content, + blog.Title, // 0 + blog.Logo, // 1 + blog.Cover, // 2 + post.Title, // 3 + post.Description, // 4 + post.Content, // 5 + post.Slug, // 6 + post.Published, // 7 + post.Cover, // 8 + author.DisplayName, // 9 + siteUrl); // 10 + + if (await SendEmail(email, subject, htmlContent)) + { + sendCount++; + } + } + } + catch (Exception ex) + { + _logger.LogError(ex.Message); + } + + return sendCount; + } + + public async Task SendEmail(string to, string subject, string content) + { + var section = _config.GetSection(Constants.ConfigSectionKey); + + if(section != null) + { + var apiKey = section.GetValue("SendGridApiKey"); + + if (!string.IsNullOrEmpty(apiKey) && apiKey != "YOUR-SENDGRID-API-KEY") + { + try + { + var client = new SendGridClient(apiKey); + + var fromEmail = section.GetValue("SendGridEmailFrom") ?? "admin@blog.com"; + var fromName = section.GetValue("SendGridEmailFromName") ?? "Blog admin"; + var from = new EmailAddress(fromEmail, fromName); + + var msg = MailHelper.CreateSingleEmail(from, new EmailAddress(to), subject, content.StripHtml(), content); + var response = await client.SendEmailAsync(msg); + + if(response.StatusCode == System.Net.HttpStatusCode.Unauthorized) + { + _logger.LogError("SendGrid service returned 'Unauthorized' - please verfiy SendGrid API key in configuration file"); + return false; + } + } + catch (Exception ex) + { + _logger.LogError(ex.Message); + return false; + } + } + else + { + _logger.LogError("Email sevice is not configured"); + return false; + } + } + await Task.CompletedTask; + return true; + } + } +} diff --git a/src/Blogifier.Core/Services/StorageService.cs b/src/Blogifier.Core/Services/StorageService.cs new file mode 100644 index 000000000..5ad9e5fbe --- /dev/null +++ b/src/Blogifier.Core/Services/StorageService.cs @@ -0,0 +1,663 @@ +using Blogifier.Core.Data; +using Blogifier.Core.Helpers; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Reflection; +using System.Threading.Tasks; + +namespace Blogifier.Core.Services +{ + public interface IStorageService + { + string Location { get; } + + void CreateFolder(string path); + void DeleteFolder(string path); + void DeleteAuthor(string name); + + Task UploadFormFile(IFormFile file, string root, string path = ""); + Task UploadBase64Image(string baseImg, string root, string path = ""); + Task UploadFromWeb(Uri requestUri, string root, string path = ""); + void DeleteFile(string path); + + IList GetAssets(string path); + IList GetThemes(); + bool SelectTheme(string theme); + + string GetHtmlTemplate(string template); + + string GetThemeData(string theme); + Task SaveThemeData(ThemeDataModel model, bool isActive); + + Task> Find(Func predicate, Pager pager, string path = "", bool sanitize = false); + + Task Reset(); + } + + public class StorageService : IStorageService + { + private readonly string _blogSlug; + private readonly string _separator = Path.DirectorySeparatorChar.ToString(); + private readonly string _uploadFolder = "data"; + private readonly string _thumbs = "thumbs"; + private readonly ILogger _logger; + + public StorageService(IHttpContextAccessor httpContext, ILogger logger) + { + if(httpContext == null || httpContext.HttpContext == null) + { + _blogSlug = ""; + } + else + { + _blogSlug = httpContext.HttpContext.User.Identity.Name; + } + + _logger = logger; + + if (!Directory.Exists(Location)) + CreateFolder(""); + } + + public string Location + { + get + { + var path = AppSettings.WebRootPath ?? Path.Combine(GetAppRoot(), "wwwroot"); + + path = Path.Combine(path, _uploadFolder.Replace("/", Path.DirectorySeparatorChar.ToString())); + + if (!string.IsNullOrEmpty(_blogSlug)) + { + path = Path.Combine(path, _blogSlug); + } + return path; + } + } + + public IList GetAssets(string path) + { + path = path.Replace("/", _separator); + try + { + var dir = string.IsNullOrEmpty(path) ? Location : Path.Combine(Location, path); + var info = new DirectoryInfo(dir); + + FileInfo[] files = info.GetFiles("*", SearchOption.AllDirectories) + .OrderByDescending(p => p.CreationTime).ToArray(); + + if(files != null && files.Any()) + { + var assets = new List(); + + foreach (FileInfo file in files) + { + assets.Add(file.FullName); + } + return assets; + } + } + catch (Exception ex) + { + _logger.LogError(ex.Message); + } + return null; + } + + public IList GetThemes() + { + var items = new List(); + var dir = Path.Combine(GetAppRoot(), $"wwwroot{_separator}themes"); + try + { + foreach (string d in Directory.GetDirectories(dir)) + { + if(!d.EndsWith("_active")) + items.Add(Path.GetFileName(d)); + } + } + catch { } + return items; + } + + public bool SelectTheme(string theme) + { + var dir = Path.Combine(GetAppRoot(), $"wwwroot{_separator}themes"); + string temp = $"{dir}{_separator}_temp"; + string active = $"{dir}{_separator}_active"; + string source = $"{dir}{_separator}{theme}"; + + try + { + // backup + if (Directory.Exists(active)) + Directory.Move(active, temp); + + Directory.CreateDirectory(active); + + CopyFilesRecursively(new DirectoryInfo(source), new DirectoryInfo(active)); + + Directory.Delete(temp, true); + + return true; + } + catch (Exception ex) + { + try + { + // restore and cleanup + if (Directory.Exists(temp)) + { + if (Directory.Exists(active)) + Directory.Delete(active, true); + + Directory.Move(temp, active); + } + } + catch { } + + _logger.LogError($"Error replacing theme in the file system: {ex.Message}"); + return false; + } + } + + static void CopyFilesRecursively(DirectoryInfo source, DirectoryInfo target) + { + foreach (DirectoryInfo dir in source.GetDirectories()) + CopyFilesRecursively(dir, target.CreateSubdirectory(dir.Name)); + foreach (FileInfo file in source.GetFiles()) + file.CopyTo(Path.Combine(target.FullName, file.Name)); + } + + public string GetThemeData(string theme) + { + string jsonFile = $"{AppSettings.WebRootPath}{_separator}themes{_separator}{theme}{_separator}assets{_separator}{Constants.ThemeDataFile}"; + if (File.Exists(jsonFile)) + { + using (StreamReader r = new StreamReader(jsonFile)) + { + return r.ReadToEnd(); + } + } + return ""; + } + + public async Task SaveThemeData(ThemeDataModel model, bool isActive) + { + if (!GetThemes().Contains(model.Theme)) + { + var msg = $"Theme \"{model.Theme}\" does not exist"; + _logger.LogError(msg); + throw new ApplicationException(msg); + } + try + { + var tmpObj = JContainer.Parse(model.Data); + } + catch (Exception ex) + { + _logger.LogError(ex.Message); + throw new ApplicationException(ex.Message); + } + string jsonFile = $"{AppSettings.WebRootPath}{_separator}themes{_separator}{model.Theme}{_separator}assets{_separator}{Constants.ThemeDataFile}"; + if (File.Exists(jsonFile)) + { + File.Delete(jsonFile); + File.WriteAllText(jsonFile, model.Data); + } + if (isActive) + { + jsonFile = $"{AppSettings.WebRootPath}{_separator}themes{_separator}_active{_separator}assets{_separator}{Constants.ThemeDataFile}"; + if (File.Exists(jsonFile)) + { + File.Delete(jsonFile); + File.WriteAllText(jsonFile, model.Data); + } + } + + await Task.CompletedTask; + } + + public string GetHtmlTemplate(string template) + { + string content = "

Not found

"; + try + { + var path = AppSettings.WebRootPath ?? Path.Combine(GetAppRoot(), "wwwroot"); + path = Path.Combine(path, "templates"); + path = Path.Combine(path, $"{template}.html"); + + if (File.Exists(path)) + { + content = File.ReadAllText(path); + } + } + catch (Exception ex) + { + _logger.LogError(ex.Message); + } + return content; + } + + public async Task UploadFormFile(IFormFile file, string root, string path = "") + { + path = path.Replace("/", _separator); + + VerifyPath(path); + + var fileName = GetFileName(file.FileName); + var filePath = string.IsNullOrEmpty(path) ? + Path.Combine(Location, fileName) : + Path.Combine(Location, path + _separator + fileName); + var thumbFolder = string.IsNullOrEmpty(path) ? + Path.Combine(Location, _thumbs) : + Path.Combine(Location, $"{path}{_separator}{_thumbs}"); + + using (var fileStream = new FileStream(filePath, FileMode.Create)) + { + await file.CopyToAsync(fileStream); + + Stream stream = file.OpenReadStream(); + SaveThumbnail(stream, thumbFolder, fileName); + + return new AssetItem + { + Title = fileName, + Path = TrimFilePath(filePath), + Url = GetUrl(filePath, root) + }; + } + } + + public async Task UploadBase64Image(string baseImg, string root, string path = "") + { + path = path.Replace("/", _separator); + var fileName = ""; + + VerifyPath(path); + + Random rnd = new Random(); + + if (baseImg.StartsWith("data:image/png;base64,")) + { + fileName = string.Format("{0}.png", rnd.Next(1000, 9999)); + baseImg = baseImg.Replace("data:image/png;base64,", ""); + } + if (baseImg.StartsWith("data:image/jpeg;base64,")) + { + fileName = string.Format("{0}.jpeg", rnd.Next(1000, 9999)); + baseImg = baseImg.Replace("data:image/jpeg;base64,", ""); + } + if (baseImg.StartsWith("data:image/gif;base64,")) + { + fileName = string.Format("{0}.gif", rnd.Next(1000, 9999)); + baseImg = baseImg.Replace("data:image/gif;base64,", ""); + } + + var filePath = string.IsNullOrEmpty(path) ? + Path.Combine(Location, fileName) : + Path.Combine(Location, path + _separator + fileName); + + byte[] bytes = Convert.FromBase64String(baseImg); + + await File.WriteAllBytesAsync(filePath, Convert.FromBase64String(baseImg)); + + return new AssetItem + { + Title = fileName, + Path = filePath, + Url = GetUrl(filePath, root) + }; + } + + public async Task UploadFromWeb(Uri requestUri, string root, string path = "") + { + path = path.Replace("/", _separator); + + VerifyPath(path); + + var fileName = TitleFromUri(requestUri); + var filePath = string.IsNullOrEmpty(path) ? + Path.Combine(Location, fileName) : + Path.Combine(Location, path + _separator + fileName); + var thumbFolder = string.IsNullOrEmpty(path) ? + Path.Combine(Location, _thumbs) : + Path.Combine(Location, $"{path}{_separator}{_thumbs}"); + + using (var client = new HttpClient()) + { + using (var request = new HttpRequestMessage(HttpMethod.Get, requestUri)) + { + using ( + Stream contentStream = await (await client.SendAsync(request)).Content.ReadAsStreamAsync(), + stream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, 3145728, true)) + { + await contentStream.CopyToAsync(stream); + SaveThumbnail(contentStream, thumbFolder, fileName); + + return new AssetItem + { + Title = fileName, + Path = filePath, + Url = GetUrl(filePath, root) + }; + } + } + } + } + + public bool SaveThumbnail(Stream resourceImage, string thumbFolder, string fileName) + { + try + { + Image image = Image.FromStream(resourceImage); + + if (image.Width < AppSettings.ThumbWidth) + return false; + + if (!Directory.Exists(thumbFolder)) + Directory.CreateDirectory(thumbFolder); + + Image thumb = image.GetThumbnailImage(AppSettings.ThumbWidth, AppSettings.ThumbHeight, () => false, IntPtr.Zero); + thumb.Save(Path.Combine(thumbFolder, fileName)); + return true; + } + catch { return false; } + } + + public async Task> Find(Func predicate, Pager pager, string path = "", bool sanitize = false) + { + var skip = pager.CurrentPage * pager.ItemsPerPage - pager.ItemsPerPage; + var files = GetAssets(path); + var items = MapFilesToAssets(files); + + if (predicate != null) + items = items.Where(predicate).ToList(); + + pager.Configure(items.Count); + + var page = items.Skip(skip).Take(pager.ItemsPerPage).ToList(); + + if (sanitize) + { + foreach (var p in page) + { + p.Path = p.Path.Replace(Location, ""); + } + } + + return await Task.FromResult(page); + } + + public void CreateFolder(string path) + { + var dir = GetFullPath(path); + + if (!Directory.Exists(dir)) + Directory.CreateDirectory(dir); + } + + public void DeleteFolder(string path) + { + var dir = GetFullPath(path); + + if (Directory.Exists(dir)) + Directory.Delete(dir, true); + } + + public void DeleteAuthor(string name) + { + var dir = Path.GetFullPath(Path.Combine(Location, @"..\")); + dir = Path.Combine(dir, name); + + if (Directory.Exists(dir)) + Directory.Delete(dir, true); + } + + public void DeleteFile(string path) + { + path = path.SanitizeFileName(); + path = path.Replace("/", _separator); + path = path.Replace($"{_uploadFolder}{_separator}{_blogSlug}{_separator}", ""); + File.Delete(GetFullPath(path)); + } + + public async Task Reset() + { + try + { + var dirs = Directory.GetDirectories(Location); + foreach (var dir in dirs) + { + if (!dir.EndsWith("_init")) + { + Directory.Delete(dir, true); + } + } + var srcLoc = Path.Combine(Location, "_init"); + + foreach (string dirPath in Directory.GetDirectories(srcLoc, "*", + SearchOption.AllDirectories)) + Directory.CreateDirectory(dirPath.Replace(srcLoc, Location)); + + foreach (string newPath in Directory.GetFiles(srcLoc, "*.*", + SearchOption.AllDirectories)) + File.Copy(newPath, newPath.Replace(srcLoc, Location), true); + + await Task.CompletedTask; + } + catch { } + } + + void VerifyPath(string path) + { + path = path.SanitizePath(); + + if (!string.IsNullOrEmpty(path)) + { + var dir = Path.Combine(Location, path); + + if (!Directory.Exists(dir)) + { + CreateFolder(dir); + } + } + } + + string TrimFilePath(string path) + { + var p = path.Replace(AppSettings.WebRootPath, ""); + if (p.StartsWith("\\")) p = p.Substring(1); + return p; + } + + string GetFullPath(string path) + { + if (string.IsNullOrEmpty(path)) + return Location; + else + return Path.Combine(Location, path.Replace("/", _separator)); + } + + string GetFileName(string fileName) + { + // some browsers pass uploaded file name as short file name + // and others include the path; remove path part if needed + if (fileName.Contains(_separator)) + { + fileName = fileName.Substring(fileName.LastIndexOf(_separator)); + fileName = fileName.Replace(_separator, ""); + } + // when drag-and-drop or copy image to TinyMce editor + // it uses "mceclip0" as file name; randomize it for multiple uploads + if (fileName.StartsWith("mceclip0")) + { + Random rnd = new Random(); + fileName = fileName.Replace("mceclip0", rnd.Next(100000, 999999).ToString()); + } + return fileName.SanitizeFileName(); + } + + string GetUrl(string path, string root) + { + var url = path.ReplaceIgnoreCase(Location, "").Replace(_separator, "/"); + return string.Concat(_uploadFolder, "/", _blogSlug, url); + } + + string GetAppRoot() + { + // normal application run + if(!string.IsNullOrEmpty(AppSettings.ContentRootPath)) + return AppSettings.ContentRootPath; + + // unit tests of seed data load + Assembly assembly; + var assemblyName = "Blogifier.Core.Tests"; + try + { + assembly = Assembly.Load(new AssemblyName(assemblyName)); + } + catch + { + assemblyName = "Blogifier.Core"; + assembly = Assembly.Load(new AssemblyName(assemblyName)); + } + + var uri = new UriBuilder(assembly.CodeBase); + var path = Uri.UnescapeDataString(uri.Path); + var root = Path.GetDirectoryName(path); + root = root.Substring(0, root.IndexOf(assemblyName)); + + if (root.EndsWith($"tests{_separator}")) + { + root = root.Replace($"tests{_separator}", $"src{_separator}"); + } + + return Path.Combine(root, "Blogifier"); + } + + string TitleFromUri(Uri uri) + { + var title = uri.ToString().ToLower(); + title = title.Replace("%2f", "/"); + + if (title.EndsWith(".axdx")) + { + title = title.Replace(".axdx", ""); + } + if (title.Contains("image.axd?picture=")) + { + title = title.Substring(title.IndexOf("image.axd?picture=") + 18); + } + if (title.Contains("file.axd?file=")) + { + title = title.Substring(title.IndexOf("file.axd?file=") + 14); + } + if (title.Contains("encrypted-tbn") || title.Contains("base64,")) + { + Random rnd = new Random(); + title = string.Format("{0}.png", rnd.Next(1000, 9999)); + } + + if (title.Contains("/")) + { + title = title.Substring(title.LastIndexOf("/")); + } + + title = title.Replace(" ", "-"); + + return title.Replace("/", "").SanitizeFileName(); + } + + List MapFilesToAssets(IList assets) + { + var items = new List(); + + if (assets != null && assets.Any()) + { + foreach (var asset in assets) + { + // Azure puts web sites under "wwwroot" folder + var path = asset.Replace($"wwwroot{_separator}wwwroot", "wwwroot", StringComparison.OrdinalIgnoreCase); + + items.Add(new AssetItem + { + Path = asset, + Url = pathToUrl(path), + Title = pathToTitle(path), + Image = pathToImage(path) + }); + } + } + return items; + } + + string pathToUrl(string path) + { + return path.Substring(path.IndexOf("wwwroot") + 8) + .Replace(_separator, "/"); + } + + string pathToTitle(string path) + { + var title = path; + + if(title.LastIndexOf(_separator) > 0) + title = title.Substring(title.LastIndexOf(_separator)); + + if(title.IndexOf('.') > 0) + title = title.Substring(1, title.LastIndexOf('.') - 1); + + return title; + } + + string pathToImage(string path) + { + if(path.IsImagePath()) + return pathToUrl(path); + + var ext = "blank.png"; + + if (path.EndsWith(".xml", StringComparison.OrdinalIgnoreCase)) + ext = "xml.png"; + + if (path.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) + ext = "zip.png"; + + if (path.EndsWith(".txt", StringComparison.OrdinalIgnoreCase)) + ext = "txt.png"; + + if (path.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase)) + ext = "pdf.png"; + + if (path.EndsWith(".doc", StringComparison.OrdinalIgnoreCase) || + path.EndsWith(".docx", StringComparison.OrdinalIgnoreCase)) + ext = "doc.png"; + + if (path.EndsWith(".xls", StringComparison.OrdinalIgnoreCase) || + path.EndsWith(".xlsx", StringComparison.OrdinalIgnoreCase)) + ext = "xls.png"; + + // video/audio formats fro HTML5 tags + + if (path.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase) + || path.EndsWith(".webm", StringComparison.OrdinalIgnoreCase) + || path.EndsWith(".ogv", StringComparison.OrdinalIgnoreCase)) + ext = "video.png"; + + if (path.EndsWith(".mp3", StringComparison.OrdinalIgnoreCase) + || path.EndsWith(".wav", StringComparison.OrdinalIgnoreCase) + || path.EndsWith(".ogg", StringComparison.OrdinalIgnoreCase)) + ext = "audio.png"; + + return $"lib/img/doctypes/{ext}"; + } + } +} \ No newline at end of file diff --git a/src/Blogifier.Core/Services/WebService.cs b/src/Blogifier.Core/Services/WebService.cs new file mode 100644 index 000000000..63edbc352 --- /dev/null +++ b/src/Blogifier.Core/Services/WebService.cs @@ -0,0 +1,171 @@ +using Microsoft.Extensions.Configuration; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; + +namespace Blogifier.Core.Services +{ + public interface IWebService + { + Task CheckForLatestRelease(); + Task DownloadLatestRelease(); + Task> GetNotifications(); + } + + public class WebService : IWebService + { + IDataService _db; + IConfiguration _config; + static HttpClient client = new HttpClient(); + + public WebService(IDataService db, IConfiguration config) + { + _db = db; + _config = config; + + // required by Github + if (!client.DefaultRequestHeaders.Contains("User-Agent")) + { + client.DefaultRequestHeaders.Add("User-Agent", "Blogifier"); + } + } + + public async Task CheckForLatestRelease() + { + string result = ""; + + HttpResponseMessage response = await client.GetAsync(getGithubRepoUrl()); + + if (response.IsSuccessStatusCode) + { + var repo = await response.Content.ReadAsAsync(); + + int current, latest; + + int.TryParse(AppSettings.Version.Replace(".", "").Substring(0, 2), out current); + int.TryParse(repo.tag_name.ReplaceIgnoreCase("v", "").Replace(".", "").Substring(0, 2), out latest); + + // at least version 2.1.x.x for auto-upgrade + if(current > 20 && current < latest) + { + var dwnUrl = repo.assets[0].browser_download_url; + result = $"The new Blogifier {repo.name} is available for download"; + + var field = _db.CustomFields.Single(f => f.Name == Constants.NewestVersion && f.AuthorId == 0); + + if (field == null) + { + _db.CustomFields.Add(new Data.CustomField + { + AuthorId = 0, + Name = Constants.NewestVersion, + Content = latest.ToString() + }); + _db.Complete(); + } + else + { + if (int.Parse(field.Content) < latest) + { + await _db.CustomFields.SaveCustomValue(Constants.NewestVersion, latest.ToString()); + } + } + } + } + + return await Task.FromResult(result); + } + + public async Task DownloadLatestRelease() + { + var msg = ""; + try + { + HttpResponseMessage response = await client.GetAsync(getGithubRepoUrl()); + if (response.IsSuccessStatusCode) + { + var repo = await response.Content.ReadAsAsync(); + var zipUrl = repo.assets[0].browser_download_url; + var zipPath = $"{Constants.UpgradeDirectory}{Path.DirectorySeparatorChar.ToString()}{repo.tag_name}.zip"; + + using (var client = new HttpClient()) + { + using (var result = await client.GetAsync(zipUrl)) + { + if (result.IsSuccessStatusCode) + { + var zipBites = await result.Content.ReadAsByteArrayAsync(); + + if (!Directory.Exists(Constants.UpgradeDirectory)) + Directory.CreateDirectory(Constants.UpgradeDirectory); + + File.WriteAllBytes(zipPath, zipBites); + + ZipFile.ExtractToDirectory(zipPath, Constants.UpgradeDirectory); + } + } + } + } + } + catch (System.Exception ex) + { + msg = ex.Message; + } + + return await Task.FromResult(msg); + } + + public async Task> GetNotifications() + { + var notifications = new List(); + HttpResponseMessage response = await client.GetAsync(getGithubNotificationsUrl()); + + if (response.IsSuccessStatusCode) + { + var folder = await response.Content.ReadAsAsync>(); + if(folder != null && folder.Count > 0) + { + foreach (var file in folder) + { + var message = ReadFileFromUrl(file.download_url); + notifications.Add(message); + } + } + } + return await Task.FromResult(notifications); + } + + string ReadFileFromUrl(string url) + { + var webRequest = WebRequest.Create(url); + + using (var response = webRequest.GetResponse()) + using (var content = response.GetResponseStream()) + using (var reader = new StreamReader(content)) + { + return reader.ReadToEnd(); + } + } + + string getGithubRepoUrl() + { + var section = _config.GetSection(Constants.ConfigSectionKey); + + return (section != null && !string.IsNullOrEmpty(section.GetValue(Constants.ConfigRepoKey))) ? + section.GetValue(Constants.ConfigRepoKey) : + "https://api.github.com/repos/blogifierdotnet/Blogifier/releases/latest"; + } + + string getGithubNotificationsUrl() + { + var section = _config.GetSection(Constants.ConfigSectionKey); + + return (section != null && !string.IsNullOrEmpty(section.GetValue(Constants.ConfigNotificationsKey))) ? + section.GetValue(Constants.ConfigNotificationsKey) : + "https://api.github.com/repos/blogifierdotnet/Upgrade/contents/Notifications"; + } + } +} \ No newline at end of file diff --git a/src/Blogifier.Widgets/Blogifier.Widgets.csproj b/src/Blogifier.Widgets/Blogifier.Widgets.csproj index b2762eada..8dd82c69a 100644 --- a/src/Blogifier.Widgets/Blogifier.Widgets.csproj +++ b/src/Blogifier.Widgets/Blogifier.Widgets.csproj @@ -7,7 +7,6 @@ - @@ -18,4 +17,8 @@ + + + + \ No newline at end of file diff --git a/src/Blogifier/Blog.db b/src/Blogifier/Blog.db index 21ca73f48..b56f6d4f4 100644 Binary files a/src/Blogifier/Blog.db and b/src/Blogifier/Blog.db differ diff --git a/src/Blogifier/icon.png b/src/Blogifier/icon.png new file mode 100644 index 000000000..8e2f35f92 Binary files /dev/null and b/src/Blogifier/icon.png differ diff --git a/src/Blogifier/wwwroot/data/_init/_test/be3.xml b/src/Blogifier/wwwroot/data/_init/_test/be3.xml new file mode 100644 index 000000000..d651fbd79 --- /dev/null +++ b/src/Blogifier/wwwroot/data/_init/_test/be3.xml @@ -0,0 +1,88 @@ + + + + RTur.net + .NET and Open Source: better together + http://www.rtur.net/blog/ + http://www.rssboard.org/rss-specification + BlogEngine.NET 3.2.2.3 + en-US + http://www.rtur.net/blog/opml.axd + http://www.dotnetblogengine.net/syndication.axd + RTur.net + RTur.net + 0.000000 + 0.000000 + + Consolidating Blogifier and Core + <p><img style="width: 160px; float: left;" src="http://www.rtur.net/blog/image.axd?picture=/2017/Consolidation.png" alt="" />For a while <a href="https://github.com/blogifierdotnet/Blogifier" target="_blank">Blogifier</a> tried wearing two hats - been platform (Core) and application at the same time. In theory it made sense, in reality you need large team for this kind of multitasking. Worrying about all the scenarios we tried to support made any change to application become a major headache and slow us down big time. Also didn't help that developers seems confused about this double-headed beast we were building&nbsp; So it came time to decide on what we want to be - a platform for others build their applications or an application in its own right. And it turns out we like building applications more than platforms, so this is direction Blogifier taking now going forward. Here are first steps we took:</p> +<ul> +<li>Taking down "Blogifier" repository</li> +<li>Renaming "Blogifier.Core" to "Blogifier". This is our main repository with most developers following it</li> +<li>Moving application code to renamed "Blgofier" (currently, only dev branch)</li> +</ul> +<p>We going to move forward Blogifier as blogging application, simple, beautiful and powerful, that will be pleasure to work with and easy to customize and extend. And having simplified and consolidated code base will let us adopt and evolve much quicker.</p> +<p>If you used Core for your own application - the latest code tagged and available <a href="https://github.com/blogifierdotnet/Blogifier/releases/tag/pre-merge" target="_blank">here</a>.</p> + http://www.rtur.net/blog/post/2017/11/25/consolidating-blogifier-and-core + http://www.rtur.net/blog/post/2017/11/25/consolidating-blogifier-and-core#comment + http://www.rtur.net/blog/post.aspx?id=8e861a2a-b720-4a5c-b172-78c37e2a8a7e + Sat, 25 Nov 2017 06:49:00 -0800 + Blogifier + rtur.net + http://www.rtur.net/blog/pingback.axd + http://www.rtur.net/blog/post.aspx?id=8e861a2a-b720-4a5c-b172-78c37e2a8a7e + 0 + http://www.rtur.net/blog/trackback.axd?id=8e861a2a-b720-4a5c-b172-78c37e2a8a7e + http://www.rtur.net/blog/post/2017/11/25/consolidating-blogifier-and-core#comment + http://www.rtur.net/blog/syndication.axd?post=8e861a2a-b720-4a5c-b172-78c37e2a8a7e + + + The Failure of Communication + <p><img src="http://www.rtur.net/blog/image.axd?picture=/2017/Miscommunication.PNG" alt="" width="102" height="85" />Explaining is hard. Things that seem crystal clear to you can be completely foreign to others, and when I started planning on Blogifier, concept looked very natural and not even worth explanation. Seriously, the whole &ldquo;architectural&rdquo; diagram would look something like this &ndash; web application (Blogifier) using component (Blogifier.Core) to encapsulate common blogging functionality. </p> +<div style="display: inline-block !important;"><img src="http://www.rtur.net/blog/image.axd?picture=/2017/Diagram.PNG" alt="" width="361" height="246" /></div> +<p>Nothing hard about it, right? Wrong, I bump into people over and over telling me they have no idea what I&rsquo;m trying to accomplish. Different people. Smart people. To make things worse, the Core component did a good job pretending to be an application on its own &ndash; it has sample app that only needed for testing, but people took it and ran away.</p> +<p>I&rsquo;m hoping to fix this miscommunication soon. We just released <a title="Blogifier.Core Nuget Package" href="https://www.nuget.org/packages/Blogifier.Core" target="_blank">Blogifier.Core 1.2</a>, and this will hopefully be the last &ldquo;Core&rdquo; to worry about. Moving forward, this will be what it was supposed to be all along &ndash; just a component like jQuery or Bootstrap so you can build your application on top of it.</p> +<p>The application we are building is Blogifier. That is, just &ldquo;Blogifier&rdquo;, without &ldquo;Core&rdquo;. It is, accidently, a blog. It references Core so it does not have to deal with all the common plumbing, like database, services, APIs and so on. We going to add all the bells and whistles to make it fun modern application everyone would love and wanted to use. It will run on Windows or Linux, have install and, later, in-place upgrade to the latest version. The work just started, but we already have <a title="Blogifier on GIthub" href="https://github.com/blogifierdotnet/Blogifier" target="_blank">code repository</a> integrated with <a title="Blogifier Demo" href="http://bfier.azurewebsites.net" target="_blank">demo site</a>, so if you are interested in progress and want to make your opinion heard please follow the trail.</p> +<p>Hope this clears things a little bit, if not &ndash; don&rsquo;t hesitate to ask any questions.</p> + http://www.rtur.net/blog/post/2017/09/25/the-failure-of-communication + http://www.rtur.net/blog/post/2017/09/25/the-failure-of-communication#comment + http://www.rtur.net/blog/post.aspx?id=c68038e8-ae89-4fc4-8bbe-84734ff36eab + Mon, 25 Sep 2017 09:04:00 -0700 + Blogifier + rtur.net + http://www.rtur.net/blog/pingback.axd + http://www.rtur.net/blog/post.aspx?id=c68038e8-ae89-4fc4-8bbe-84734ff36eab + 0 + http://www.rtur.net/blog/trackback.axd?id=c68038e8-ae89-4fc4-8bbe-84734ff36eab + http://www.rtur.net/blog/post/2017/09/25/the-failure-of-communication#comment + http://www.rtur.net/blog/syndication.axd?post=c68038e8-ae89-4fc4-8bbe-84734ff36eab + + + Blogifier Release 1.1 + <p><img src="http://www.rtur.net/blog/image.axd?picture=/2017/logo-64.png" alt="" />It took just a few weeks to move Blogifier to release 1.1. Although changes are mostly cosmetic, it adds some really nice polishing touches to core functionality and significantly improves UX. Check out below what exactly was added in this new version. </p> +<ul style="list-style-type: circle;"> +<li>Separate setup page for new blog registration</li> +<li>Search in admin on posts and files</li> +<li>Filters for posts and files</li> +<li>Actions (publish, delete etc.) on multiple selected items in posts and files</li> +<li>Public APIs for 3rd party apps or SPA-style themes</li> +</ul> +<p>This should make managing large blogs a lot easier and improve experience overall.</p> +<p><img style="margin-bottom: 15px;" src="http://www.rtur.net/blog/image.axd?picture=/2017/filters.PNG" alt="" /></p> +<p>How do I upgrade from 1.0? Same way you upgrade any Nuget package: via package manager in Visual Studio or VS Code.</p> +<p><img style="margin-bottom: 15px;" src="http://www.rtur.net/blog/image.axd?picture=/2017/nuget.PNG" alt="" /></p> + http://www.rtur.net/blog/post/2017/08/12/blogifier-release-1-1 + http://www.rtur.net/blog/post/2017/08/12/blogifier-release-1-1#comment + http://www.rtur.net/blog/post.aspx?id=d51578aa-06e1-4d3a-a030-8c777644b891 + Sat, 12 Aug 2017 14:43:00 -0700 + Blogifier + rtur.net + http://www.rtur.net/blog/pingback.axd + http://www.rtur.net/blog/post.aspx?id=d51578aa-06e1-4d3a-a030-8c777644b891 + 0 + http://www.rtur.net/blog/trackback.axd?id=d51578aa-06e1-4d3a-a030-8c777644b891 + http://www.rtur.net/blog/post/2017/08/12/blogifier-release-1-1#comment + http://www.rtur.net/blog/syndication.axd?post=d51578aa-06e1-4d3a-a030-8c777644b891 + + + \ No newline at end of file diff --git a/src/Blogifier/wwwroot/data/_init/admin/admin-editor.png b/src/Blogifier/wwwroot/data/_init/admin/admin-editor.png new file mode 100644 index 000000000..19c521bcd Binary files /dev/null and b/src/Blogifier/wwwroot/data/_init/admin/admin-editor.png differ diff --git a/src/Blogifier/wwwroot/data/_init/admin/admin-files.png b/src/Blogifier/wwwroot/data/_init/admin/admin-files.png new file mode 100644 index 000000000..f69277cc6 Binary files /dev/null and b/src/Blogifier/wwwroot/data/_init/admin/admin-files.png differ diff --git a/src/Blogifier/wwwroot/data/_init/admin/avatar.png b/src/Blogifier/wwwroot/data/_init/admin/avatar.png new file mode 100644 index 000000000..d492cd2de Binary files /dev/null and b/src/Blogifier/wwwroot/data/_init/admin/avatar.png differ diff --git a/src/Blogifier/wwwroot/data/_init/admin/cover-blog.png b/src/Blogifier/wwwroot/data/_init/admin/cover-blog.png new file mode 100644 index 000000000..32b3650da Binary files /dev/null and b/src/Blogifier/wwwroot/data/_init/admin/cover-blog.png differ diff --git a/src/Blogifier/wwwroot/data/_init/admin/cover-desk.jpg b/src/Blogifier/wwwroot/data/_init/admin/cover-desk.jpg new file mode 100644 index 000000000..dad4eaed3 Binary files /dev/null and b/src/Blogifier/wwwroot/data/_init/admin/cover-desk.jpg differ diff --git a/src/Blogifier/wwwroot/data/_init/admin/cover-globe.png b/src/Blogifier/wwwroot/data/_init/admin/cover-globe.png new file mode 100644 index 000000000..85a624468 Binary files /dev/null and b/src/Blogifier/wwwroot/data/_init/admin/cover-globe.png differ diff --git a/src/Blogifier/wwwroot/data/_init/demo/demo-cover.jpg b/src/Blogifier/wwwroot/data/_init/demo/demo-cover.jpg new file mode 100644 index 000000000..42ca95ae0 Binary files /dev/null and b/src/Blogifier/wwwroot/data/_init/demo/demo-cover.jpg differ diff --git a/src/Blogifier/wwwroot/data/_test/be3.xml b/src/Blogifier/wwwroot/data/_test/be3.xml new file mode 100644 index 000000000..d651fbd79 --- /dev/null +++ b/src/Blogifier/wwwroot/data/_test/be3.xml @@ -0,0 +1,88 @@ + + + + RTur.net + .NET and Open Source: better together + http://www.rtur.net/blog/ + http://www.rssboard.org/rss-specification + BlogEngine.NET 3.2.2.3 + en-US + http://www.rtur.net/blog/opml.axd + http://www.dotnetblogengine.net/syndication.axd + RTur.net + RTur.net + 0.000000 + 0.000000 + + Consolidating Blogifier and Core + <p><img style="width: 160px; float: left;" src="http://www.rtur.net/blog/image.axd?picture=/2017/Consolidation.png" alt="" />For a while <a href="https://github.com/blogifierdotnet/Blogifier" target="_blank">Blogifier</a> tried wearing two hats - been platform (Core) and application at the same time. In theory it made sense, in reality you need large team for this kind of multitasking. Worrying about all the scenarios we tried to support made any change to application become a major headache and slow us down big time. Also didn't help that developers seems confused about this double-headed beast we were building&nbsp; So it came time to decide on what we want to be - a platform for others build their applications or an application in its own right. And it turns out we like building applications more than platforms, so this is direction Blogifier taking now going forward. Here are first steps we took:</p> +<ul> +<li>Taking down "Blogifier" repository</li> +<li>Renaming "Blogifier.Core" to "Blogifier". This is our main repository with most developers following it</li> +<li>Moving application code to renamed "Blgofier" (currently, only dev branch)</li> +</ul> +<p>We going to move forward Blogifier as blogging application, simple, beautiful and powerful, that will be pleasure to work with and easy to customize and extend. And having simplified and consolidated code base will let us adopt and evolve much quicker.</p> +<p>If you used Core for your own application - the latest code tagged and available <a href="https://github.com/blogifierdotnet/Blogifier/releases/tag/pre-merge" target="_blank">here</a>.</p> + http://www.rtur.net/blog/post/2017/11/25/consolidating-blogifier-and-core + http://www.rtur.net/blog/post/2017/11/25/consolidating-blogifier-and-core#comment + http://www.rtur.net/blog/post.aspx?id=8e861a2a-b720-4a5c-b172-78c37e2a8a7e + Sat, 25 Nov 2017 06:49:00 -0800 + Blogifier + rtur.net + http://www.rtur.net/blog/pingback.axd + http://www.rtur.net/blog/post.aspx?id=8e861a2a-b720-4a5c-b172-78c37e2a8a7e + 0 + http://www.rtur.net/blog/trackback.axd?id=8e861a2a-b720-4a5c-b172-78c37e2a8a7e + http://www.rtur.net/blog/post/2017/11/25/consolidating-blogifier-and-core#comment + http://www.rtur.net/blog/syndication.axd?post=8e861a2a-b720-4a5c-b172-78c37e2a8a7e + + + The Failure of Communication + <p><img src="http://www.rtur.net/blog/image.axd?picture=/2017/Miscommunication.PNG" alt="" width="102" height="85" />Explaining is hard. Things that seem crystal clear to you can be completely foreign to others, and when I started planning on Blogifier, concept looked very natural and not even worth explanation. Seriously, the whole &ldquo;architectural&rdquo; diagram would look something like this &ndash; web application (Blogifier) using component (Blogifier.Core) to encapsulate common blogging functionality. </p> +<div style="display: inline-block !important;"><img src="http://www.rtur.net/blog/image.axd?picture=/2017/Diagram.PNG" alt="" width="361" height="246" /></div> +<p>Nothing hard about it, right? Wrong, I bump into people over and over telling me they have no idea what I&rsquo;m trying to accomplish. Different people. Smart people. To make things worse, the Core component did a good job pretending to be an application on its own &ndash; it has sample app that only needed for testing, but people took it and ran away.</p> +<p>I&rsquo;m hoping to fix this miscommunication soon. We just released <a title="Blogifier.Core Nuget Package" href="https://www.nuget.org/packages/Blogifier.Core" target="_blank">Blogifier.Core 1.2</a>, and this will hopefully be the last &ldquo;Core&rdquo; to worry about. Moving forward, this will be what it was supposed to be all along &ndash; just a component like jQuery or Bootstrap so you can build your application on top of it.</p> +<p>The application we are building is Blogifier. That is, just &ldquo;Blogifier&rdquo;, without &ldquo;Core&rdquo;. It is, accidently, a blog. It references Core so it does not have to deal with all the common plumbing, like database, services, APIs and so on. We going to add all the bells and whistles to make it fun modern application everyone would love and wanted to use. It will run on Windows or Linux, have install and, later, in-place upgrade to the latest version. The work just started, but we already have <a title="Blogifier on GIthub" href="https://github.com/blogifierdotnet/Blogifier" target="_blank">code repository</a> integrated with <a title="Blogifier Demo" href="http://bfier.azurewebsites.net" target="_blank">demo site</a>, so if you are interested in progress and want to make your opinion heard please follow the trail.</p> +<p>Hope this clears things a little bit, if not &ndash; don&rsquo;t hesitate to ask any questions.</p> + http://www.rtur.net/blog/post/2017/09/25/the-failure-of-communication + http://www.rtur.net/blog/post/2017/09/25/the-failure-of-communication#comment + http://www.rtur.net/blog/post.aspx?id=c68038e8-ae89-4fc4-8bbe-84734ff36eab + Mon, 25 Sep 2017 09:04:00 -0700 + Blogifier + rtur.net + http://www.rtur.net/blog/pingback.axd + http://www.rtur.net/blog/post.aspx?id=c68038e8-ae89-4fc4-8bbe-84734ff36eab + 0 + http://www.rtur.net/blog/trackback.axd?id=c68038e8-ae89-4fc4-8bbe-84734ff36eab + http://www.rtur.net/blog/post/2017/09/25/the-failure-of-communication#comment + http://www.rtur.net/blog/syndication.axd?post=c68038e8-ae89-4fc4-8bbe-84734ff36eab + + + Blogifier Release 1.1 + <p><img src="http://www.rtur.net/blog/image.axd?picture=/2017/logo-64.png" alt="" />It took just a few weeks to move Blogifier to release 1.1. Although changes are mostly cosmetic, it adds some really nice polishing touches to core functionality and significantly improves UX. Check out below what exactly was added in this new version. </p> +<ul style="list-style-type: circle;"> +<li>Separate setup page for new blog registration</li> +<li>Search in admin on posts and files</li> +<li>Filters for posts and files</li> +<li>Actions (publish, delete etc.) on multiple selected items in posts and files</li> +<li>Public APIs for 3rd party apps or SPA-style themes</li> +</ul> +<p>This should make managing large blogs a lot easier and improve experience overall.</p> +<p><img style="margin-bottom: 15px;" src="http://www.rtur.net/blog/image.axd?picture=/2017/filters.PNG" alt="" /></p> +<p>How do I upgrade from 1.0? Same way you upgrade any Nuget package: via package manager in Visual Studio or VS Code.</p> +<p><img style="margin-bottom: 15px;" src="http://www.rtur.net/blog/image.axd?picture=/2017/nuget.PNG" alt="" /></p> + http://www.rtur.net/blog/post/2017/08/12/blogifier-release-1-1 + http://www.rtur.net/blog/post/2017/08/12/blogifier-release-1-1#comment + http://www.rtur.net/blog/post.aspx?id=d51578aa-06e1-4d3a-a030-8c777644b891 + Sat, 12 Aug 2017 14:43:00 -0700 + Blogifier + rtur.net + http://www.rtur.net/blog/pingback.axd + http://www.rtur.net/blog/post.aspx?id=d51578aa-06e1-4d3a-a030-8c777644b891 + 0 + http://www.rtur.net/blog/trackback.axd?id=d51578aa-06e1-4d3a-a030-8c777644b891 + http://www.rtur.net/blog/post/2017/08/12/blogifier-release-1-1#comment + http://www.rtur.net/blog/syndication.axd?post=d51578aa-06e1-4d3a-a030-8c777644b891 + + + \ No newline at end of file diff --git a/src/Blogifier/wwwroot/data/thumbs/cover.png b/src/Blogifier/wwwroot/data/thumbs/cover.png new file mode 100644 index 000000000..984ac6692 Binary files /dev/null and b/src/Blogifier/wwwroot/data/thumbs/cover.png differ