@@ -101,7 +101,7 @@ public FileCountResult CountFilesGrouped(MediaFilesFilter filter)
Guard.NotNull(filter, nameof(filter));

// Base db query
var q = _searcher.PrepareFilterQuery(filter);
var q = _searcher.ApplyFilterQuery(filter);

// Get ids of untrackable folders, 'cause no orphan check can be made for them.
var untrackableFolderIds = _folderService.GetRootNode()
@@ -196,9 +196,7 @@ public MediaFileInfo GetFileByPath(string path, MediaLoadFlags flags = MediaLoad

if (_helper.TokenizePath(path, out var tokens))
{
var table = _fileRepo.Table;

// TODO: (mm) (mc) LoadFlags > Blob | Tags | Tracks
var table = _searcher.ApplyLoadFlags(_fileRepo.Table, flags);

var entity = table.FirstOrDefault(x => x.FolderId == tokens.Folder.Id && x.Name == tokens.FileName);
if (entity != null)
@@ -296,7 +294,7 @@ protected internal virtual bool CheckUniqueFileName(MediaPathData pathData)
var query = _searcher.PrepareQuery(q, MediaLoadFlags.AsNoTracking).Select(x => x.Name);
var files = new HashSet<string>(query.ToList(), StringComparer.CurrentCultureIgnoreCase);

if (InternalCheckUniqueFileName(pathData.FileTitle, pathData.Extension, files, out var uniqueName))
if (_helper.CheckUniqueFileName(pathData.FileTitle, pathData.Extension, files, out var uniqueName))
{
pathData.FileName = uniqueName;
return true;
@@ -305,35 +303,6 @@ protected internal virtual bool CheckUniqueFileName(MediaPathData pathData)
return false;
}

private bool InternalCheckUniqueFileName(string title, string ext, string destFileName, out string uniqueName)
{
return InternalCheckUniqueFileName(title, ext, new HashSet<string>( new[] { destFileName }, StringComparer.CurrentCultureIgnoreCase), out uniqueName);
}

private bool InternalCheckUniqueFileName(string title, string ext, HashSet<string> destFileNames, out string uniqueName)
{
uniqueName = null;

if (destFileNames.Count == 0)
{
return false;
}

int i = 1;
while (true)
{
var test = string.Concat(title, "-", i, ".", ext.TrimStart('.'));
if (!destFileNames.Contains(test))
{
// Found our gap
uniqueName = test;
return true;
}

i++;
}
}

public string CombinePaths(params string[] paths)
{
return FolderService.NormalizePath(Path.Combine(paths), false);
@@ -887,10 +856,10 @@ public MediaFileInfo MoveFile(MediaFile file, string destinationFileName, Duplic
{
case DuplicateFileHandling.ThrowError:
var fullPath = destPathData.FullPath;
InternalCheckUniqueFileName(destPathData.FileTitle, destPathData.Extension, dupe.Name, out _);
_helper.CheckUniqueFileName(destPathData.FileTitle, destPathData.Extension, dupe.Name, out _);
throw _exceptionFactory.DuplicateFile(fullPath, ConvertMediaFile(dupe, destPathData.Folder), destPathData.FullPath);
case DuplicateFileHandling.Rename:
if (InternalCheckUniqueFileName(destPathData.FileTitle, destPathData.Extension, dupe.Name, out var uniqueName))
if (_helper.CheckUniqueFileName(destPathData.FileTitle, destPathData.Extension, dupe.Name, out var uniqueName))
{
nameChanged = true;
destPathData.FileName = uniqueName;
@@ -16,11 +16,23 @@
using System.Runtime.CompilerServices;
using SmartStore.Core.Domain.Messages;
using System.Diagnostics;
using System.Globalization;
using NReco.PdfGenerator;
using SmartStore.Core;

namespace SmartStore.Services.Media.Migration
{
public class MediaMigrator
{
public class FileMin
{
public int Id { get; set; }
public string Name { get; set; }
public bool IsDupe { get; set; }
public string UniqueName { get; set; }
public int UniqueIndex { get; set; }
}

internal static bool Executed;
internal const string MigrationName = "MediaManager";

@@ -164,12 +176,17 @@ public void MigrateDownloads(SmartObjectContext ctx)
if (stub == null)
continue;

if (stub.UseDownloadUrl || stub.Filename == "undefined" || string.IsNullOrEmpty(stub.Filename) || string.IsNullOrEmpty(stub.Extension))
if (stub.UseDownloadUrl || string.IsNullOrEmpty(stub.Extension))
{
// Something weird has happened in the past
continue;
}

}

if (stub.Filename == "undefined" || string.IsNullOrEmpty(stub.Filename))
{
stub.Filename = stub.Id.ToString(CultureInfo.InvariantCulture);
}

var isMailAttachment = false;
if (messageTemplatesDict.TryGetValue(stub.Id, out var mt))
{
@@ -304,9 +321,85 @@ public void MigrateDownloads(SmartObjectContext ctx)

public void MigrateMediaFiles(SmartObjectContext ctx)
{
var query = ctx.Set<MediaFile>()
string prevName = null;
int fileIndex = 0;

var query = ctx.Set<MediaFile>().OrderBy(x => x.Name);
var totalCount = query.Count();
var pageSize = Math.Max(1000, Math.Min(5000, totalCount / 200));
var pageIndex = 0;

using (var scope = new DbContextScope(ctx,
hooksEnabled: false,
autoCommit: false,
proxyCreation: false,
validateOnSave: false,
lazyLoading: false))
{
while (true)
{
var files = new PagedList<MediaFile>(query, pageIndex, pageSize);
if (files.Count == 0)
break;

foreach (var file in files)
{
if (file.Version > 0)
continue;

if (file.Extension.IsEmpty())
{
file.Extension = MimeTypes.MapMimeTypeToExtension(file.MimeType);
}

var name = file.Name;
var fixedName = name;
if (name.IsEmpty())
{
name = fixedName = file.Id.ToString(System.Globalization.CultureInfo.InvariantCulture);
}
else
{
name = fixedName = file.Name.Truncate(292).ToValidFileName();
if (name == prevName)
{
// Make file name unique
fixedName = name + "-" + ++fileIndex;
}
else
{
fileIndex = 0;
}
}

prevName = name;

file.Name = fixedName + "." + file.Extension;
file.CreatedOnUtc = file.UpdatedOnUtc;
file.Version = 1;

ProcessMediaFile(file);
}

// Save to DB
int num = scope.Commit();

// Breathe
ctx.DetachEntities<MediaFile>(deep: true);

if (!files.HasNextPage)
break;

pageIndex++;
}
}
}

public void MigrateMediaFiles_Old(SmartObjectContext ctx)
{
var query = ctx.Set<MediaFile>();
//.Where(x => x.Version == 0)
.Include(x => x.MediaStorage);
//.Include(x => x.MediaStorage);

var pager = new FastPager<MediaFile>(query, 1000);

@@ -395,18 +488,7 @@ void ProcessFolder(IFolder folder, int mediaFolderId)

ProcessMediaFile(file);

if (!_isFsProvider)
{
using (var stream = uploadedFile.OpenRead())
{
file.MediaStorage = new MediaStorage { Data = stream.ToByteArray() };
}
}
else
{
newFiles.Add(new FilePair { MediaFile = file, UploadedFile = uploadedFile });
}

newFiles.Add(new FilePair { MediaFile = file, UploadedFile = uploadedFile });
fileSet.Add(file);
}

@@ -417,9 +499,9 @@ void ProcessFolder(IFolder folder, int mediaFolderId)
int num = scope.Commit();

// Copy/Move files
if (_isFsProvider)
foreach (var newFile in newFiles)
{
foreach (var newFile in newFiles)
if (_isFsProvider)
{
var newPath = GetStoragePath(newFile.MediaFile);
if (!_mediaFileSystem.FileExists(newPath))
@@ -428,6 +510,16 @@ void ProcessFolder(IFolder folder, int mediaFolderId)
_mediaFileSystem.CopyFile(newFile.UploadedFile.Path, newPath);
}
}
else
{
_mediaStorageProvider.Save(newFile.MediaFile, newFile.UploadedFile.OpenRead());
}
}

if (!_isFsProvider)
{
// MediaFile.MediaStorageId has been updated, we need to save again.
num = scope.Commit();
}
}
catch
@@ -482,6 +574,12 @@ private void ProcessMediaFile(MediaFile file)
file.Height = size.Height;
file.PixelSize = size.Width * size.Height;
}
catch
{
// Don't attempt again
file.Width = 0;
file.Height = 0;
}
finally
{
stream.Dispose();
@@ -21,7 +21,7 @@ public void Migrate()
var setFolders = _db.Set<MediaFolder>();

// Insert new Albums: Catalog & Content
// Entity has an unique index of ParentId and Name.
// Entity has a unique index of ParentId and Name.
var catalogAlbum = setFolders.FirstOrDefault(x => x.ParentId == null && x.Name == SystemAlbumProvider.Catalog) as MediaAlbum;
if (catalogAlbum == null)
{
@@ -41,12 +41,13 @@ public void Migrate()
// Load all db albums into a dictionary (Key = AlbumName)
var albums = setFolders.OfType<MediaAlbum>().ToDictionary(x => x.Name);

// Reorganize all files (product/brand/category >> catalog && news/blog/forum >> content)
// Reorganize files (product/category/brand >> catalog)
foreach (var oldName in new[] { "product", "category", "brand" })
{
UpdateFolderId(oldName, catalogAlbum);
}

// Reorganize files (news/blog/forum >> content)
foreach (var oldName in new[] { "blog", "news", "forum" })
{
UpdateFolderId(oldName, contentAlbum);
@@ -61,7 +62,11 @@ public void Migrate()
.Select(x => x.Value)
.Where(x => namesToDelete.Contains(x.Name))
.ToList();
setFolders.RemoveRange(toDelete);

if (toDelete.Any())
{
setFolders.RemoveRange(toDelete);
}

_db.SaveChanges();

@@ -10,7 +10,7 @@ public interface IMediaSearcher
{
IPagedList<MediaFile> SearchFiles(MediaSearchQuery query, MediaLoadFlags flags = MediaLoadFlags.AsNoTracking);
IQueryable<MediaFile> PrepareQuery(MediaSearchQuery query, MediaLoadFlags flags);
IQueryable<MediaFile> PrepareFilterQuery(MediaFilesFilter filter);
IQueryable<MediaFile> ApplyFilterQuery(MediaFilesFilter filter, IQueryable<MediaFile> sourceQuery = null);
IQueryable<MediaFile> ApplyLoadFlags(IQueryable<MediaFile> query, MediaLoadFlags flags);
}
}
@@ -66,7 +66,7 @@ public partial class MediaSearchQuery : MediaFilesFilter
public int PageSize { get; set; } = int.MaxValue;

[JsonProperty("sortBy")]
public string SortBy { get; set; } = nameof(MediaFile.Name);
public string SortBy { get; set; } = nameof(MediaFile.Id);

[JsonProperty("sortDesc")]
public bool SortDesc { get; set; }
@@ -35,7 +35,7 @@ public virtual IQueryable<MediaFile> PrepareQuery(MediaSearchQuery query, MediaL
{
Guard.NotNull(query, nameof(query));

var q = PrepareFilterQuery(query);
var q = _fileRepo.Table;
bool? shouldIncludeDeleted = false;

// Folder
@@ -81,57 +81,76 @@ public virtual IQueryable<MediaFile> PrepareQuery(MediaSearchQuery query, MediaL
q = q.Where(x => x.FolderId == null);
}
}
else
{
// (perf) Composite index
q = q.Where(x => x.FolderId == null || x.FolderId.HasValue);
}

q = ApplyFilterQuery(query, q);

if (query.Deleted == null && shouldIncludeDeleted.HasValue)
{
q = q.Where(x => x.Deleted == shouldIncludeDeleted.Value);
}

// Sorting
var ordering = query.SortBy;
if (ordering.HasValue())
{
if (query.SortDesc) ordering += " descending";
q = q.OrderBy(ordering);
}
var ordering = query.SortBy.NullEmpty() ?? "Id";
if (query.SortDesc) ordering += " descending";
q = q.OrderBy(ordering);

return ApplyLoadFlags(q, flags);
}

public virtual IQueryable<MediaFile> PrepareFilterQuery(MediaFilesFilter filter)
public virtual IQueryable<MediaFile> ApplyFilterQuery(MediaFilesFilter filter, IQueryable<MediaFile> sourceQuery = null)
{
Guard.NotNull(filter, nameof(filter));

var q = _fileRepo.Table;
var q = sourceQuery ?? _fileRepo.Table;

// Deleted
if (filter.Deleted != null)
{
q = q.Where(x => x.Deleted == filter.Deleted.Value);
}

// Hidden
if (filter.Hidden != null)
// Term
if (filter.Term.HasValue() && filter.Term != "*")
{
q = q.Where(x => x.Hidden == filter.Hidden.Value);
// Convert file pattern to SQL 'LIKE' expression
q = ApplySearchTerm(q, filter.Term, filter.IncludeAltForTerm, filter.ExactMatch);
}

// MimeType
if (filter.MimeTypes != null && filter.MimeTypes.Length > 0)
// MediaType
if (filter.MediaTypes != null && filter.MediaTypes.Length > 0)
{
q = q.Where(x => filter.MimeTypes.Contains(x.MimeType));
if (filter.MediaTypes.Length == 1)
{
var right = filter.MediaTypes[0];
q = q.Where(x => x.MediaType == right);
}
else if (filter.MediaTypes.Length > 1)
{
q = q.Where(x => filter.MediaTypes.Contains(x.MediaType));
}
else
{
// (perf) Composite index
q = q.Where(x => !string.IsNullOrEmpty(x.MediaType));
}
}

// Extension
if (filter.Extensions != null && filter.Extensions.Length > 0)
{
q = q.Where(x => filter.Extensions.Contains(x.Extension));
}

// MediaType
if (filter.MediaTypes != null && filter.MediaTypes.Length > 0)
{
q = q.Where(x => filter.MediaTypes.Contains(x.MediaType));
if (filter.Extensions.Length == 1)
{
var right = filter.Extensions[0];
q = q.Where(x => x.Extension == right);
}
else if (filter.Extensions.Length > 1)
{
q = q.Where(x => filter.Extensions.Contains(x.Extension));
}
else
{
// (perf) Composite index
q = q.Where(x => !string.IsNullOrEmpty(x.Extension));
}
}

// Tags
@@ -140,7 +159,7 @@ public virtual IQueryable<MediaFile> PrepareFilterQuery(MediaFilesFilter filter)
q = q.Where(x => x.Tags.Any(t => filter.Tags.Contains(t.Id)));
}

// Extensions
// Image dimension
if (filter.Dimensions != null && filter.Dimensions.Length > 0)
{
var predicates = new List<Expression<Func<MediaFile, bool>>>(5);
@@ -170,13 +189,36 @@ public virtual IQueryable<MediaFile> PrepareFilterQuery(MediaFilesFilter filter)
}
}

// Term
if (filter.Term.HasValue() && filter.Term != "*")
// Deleted
if (filter.Deleted != null)
{
// Convert file pattern to SQL 'LIKE' expression
q = ApplySearchTerm(q, filter.Term, filter.IncludeAltForTerm, filter.ExactMatch);
q = q.Where(x => x.Deleted == filter.Deleted.Value);
}

#region Currently unindexed

// MimeType
if (filter.MimeTypes != null && filter.MimeTypes.Length > 0)
{
if (filter.MimeTypes.Length == 1)
{
var right = filter.MimeTypes[0];
q = q.Where(x => x.MimeType == right);
}
else if (filter.MimeTypes.Length > 1)
{
q = q.Where(x => filter.MimeTypes.Contains(x.MimeType));
}
}

// Hidden
if (filter.Hidden != null)
{
q = q.Where(x => x.Hidden == filter.Hidden.Value);
}

#endregion

return q;
}

@@ -93,18 +93,18 @@ public Stream OpenRead(MediaFile mediaFile)
{
Guard.NotNull(mediaFile, nameof(mediaFile));

if (_isSqlServer)
{
if (mediaFile.MediaStorageId > 0)
{
return CreateBlobStream(mediaFile.MediaStorageId.Value);
}

return null;
}
else
{
return mediaFile.MediaStorage?.Data?.ToStream();
if (_isSqlServer)
{
if (mediaFile.MediaStorageId > 0)
{
return CreateBlobStream(mediaFile.MediaStorageId.Value);
}

return null;
}
else
{
return mediaFile.MediaStorage?.Data?.ToStream();
}
}

@@ -142,18 +142,31 @@ public void Save(MediaFile mediaFile, Stream stream)
{
Guard.NotNull(mediaFile, nameof(mediaFile));

if (_isSqlServer)
{
SaveFast(mediaFile, stream);
}
if (stream == null)
{
mediaFile.ApplyBlob(null);
}
else
{
SaveInternal(mediaFile, stream);
}

_mediaFileRepo.Update(mediaFile);
}

private void SaveInternal(MediaFile mediaFile, Stream stream)
{
using (stream)
{
byte[] buffer;
using (stream ?? new MemoryStream())
if (_isSqlServer)
{
buffer = stream.ToByteArray();
SaveFast(mediaFile, stream);
}
else
{
var buffer = stream.ToByteArray();
mediaFile.ApplyBlob(buffer);
}
mediaFile.ApplyBlob(buffer);
}

_mediaFileRepo.Update(mediaFile);
@@ -163,30 +176,52 @@ public async Task SaveAsync(MediaFile mediaFile, Stream stream)
{
Guard.NotNull(mediaFile, nameof(mediaFile));

if (_isSqlServer)
if (stream == null)
{
SaveFast(mediaFile, stream);
mediaFile.ApplyBlob(null);
}
else
{
byte[] buffer;
using (stream ?? new MemoryStream())
await SaveInternalAsync(mediaFile, stream);
}

_mediaFileRepo.Update(mediaFile);
}

private async Task SaveInternalAsync(MediaFile mediaFile, Stream stream)
{
using (stream)
{
if (_isSqlServer)
{
SaveFast(mediaFile, stream);
}
else
{
buffer = await stream.ToByteArrayAsync();
var buffer = await stream.ToByteArrayAsync();
mediaFile.ApplyBlob(buffer);
}
mediaFile.ApplyBlob(buffer);
}

_mediaFileRepo.Update(mediaFile);
}

private int SaveFast(MediaFile mediaFile, Stream stream)
{
var sql = "INSERT INTO [MediaStorage] (Data) Values(@p0)";
var storageId = ((DbContext)_dbContext).InsertInto(sql, stream);
mediaFile.MediaStorageId = storageId;

return storageId;
if (mediaFile.MediaStorageId == null)
{
// Insert new blob
var sql = "INSERT INTO [MediaStorage] (Data) Values(@p0)";
mediaFile.MediaStorageId = ((DbContext)_dbContext).InsertInto(sql, stream);
return mediaFile.MediaStorageId.Value;
}
else
{
// Update existing blob
var sql = "UPDATE [MediaStorage] SET [Data] = @p0 WHERE Id = @p1";
_dbContext.ExecuteSqlCommand(sql, false, null, stream, mediaFile.MediaStorageId.Value);
return mediaFile.MediaStorageId.Value;
}
}

public void Remove(params MediaFile[] mediaFiles)
@@ -221,8 +256,10 @@ public void MoveTo(ISupportsMediaMoving target, MediaMoverContext context, Media
// Remove picture binary from DB
try
{
mediaFile.MediaStorageId = null;
mediaFile.MediaStorage = null;
_mediaStorageRepo.Delete(mediaFile.MediaStorageId.Value);
//mediaFile.MediaStorageId = null;
//mediaFile.MediaStorage = null;

_mediaFileRepo.Update(mediaFile);
}
catch { }
@@ -116,8 +116,12 @@ public void Save(MediaFile mediaFile, Stream stream)

if (stream != null)
{
// create folder if it does not exist yet
_fileSystem.CreateFolder(Path.GetDirectoryName(filePath));
// Create folder if it does not exist yet
var dir = Path.GetDirectoryName(filePath);
if (!_fileSystem.FolderExists(dir))
{
_fileSystem.CreateFolder(dir);
}

using (stream)
{
@@ -141,6 +145,13 @@ public async Task SaveAsync(MediaFile mediaFile, Stream stream)

if (stream != null)
{
// Create folder if it does not exist yet
var dir = Path.GetDirectoryName(filePath);
if (!_fileSystem.FolderExists(dir))
{
_fileSystem.CreateFolder(dir);
}

using (stream)
{
await _fileSystem.SaveStreamAsync(filePath, stream);
@@ -206,12 +217,20 @@ public void Receive(MediaMoverContext context, MediaFile mediaFile, Stream strea

if (!_fileSystem.FileExists(filePath))
{
// TBD: (mc) We only save the file if it doesn't exist yet.
// This should save time and bandwidth in the case where the target
// is a cloud based file system (like Azure BLOB).
// In such a scenario it'd be advisable to copy the files manually
// with other - maybe more performant - tools before performing the provider switch.
using (stream)
// TBD: (mc) We only save the file if it doesn't exist yet.
// This should save time and bandwidth in the case where the target
// is a cloud based file system (like Azure BLOB).
// In such a scenario it'd be advisable to copy the files manually
// with other - maybe more performant - tools before performing the provider switch.

// Create folder if it does not exist yet
var dir = Path.GetDirectoryName(filePath);
if (!_fileSystem.FolderExists(dir))
{
_fileSystem.CreateFolder(dir);
}

using (stream)
{
_fileSystem.SaveStream(filePath, stream);
}
@@ -233,12 +252,20 @@ public async Task ReceiveAsync(MediaMoverContext context, MediaFile mediaFile, S

if (!_fileSystem.FileExists(filePath))
{
// TBD: (mc) We only save the file if it doesn't exist yet.
// This should save time and bandwidth in the case where the target
// is a cloud based file system (like Azure BLOB).
// In such a scenario it'd be advisable to copy the files manually
// with other - maybe more performant - tools before performing the provider switch.
using (stream)
// TBD: (mc) We only save the file if it doesn't exist yet.
// This should save time and bandwidth in the case where the target
// is a cloud based file system (like Azure BLOB).
// In such a scenario it'd be advisable to copy the files manually
// with other - maybe more performant - tools before performing the provider switch.

// Create folder if it does not exist yet
var dir = Path.GetDirectoryName(filePath);
if (!_fileSystem.FolderExists(dir))
{
_fileSystem.CreateFolder(dir);
}

using (stream)
{
await _fileSystem.SaveStreamAsync(filePath, stream);
}
@@ -1,61 +1,35 @@
using System;
using System.Data.Entity;
using System.Linq;
using System.Web.UI;
using SmartStore.Core;
using SmartStore.Core.Data;
using SmartStore.Core.Domain.Media;
using SmartStore.Core.Domain.Messages;
using SmartStore.Core.Localization;
using SmartStore.Core.Logging;
using SmartStore.Core.Plugins;
using SmartStore.Data.Utilities;

namespace SmartStore.Services.Media.Storage
{
public class MediaMover : IMediaMover
{
private const int PAGE_SIZE = 100;
private const int PAGE_SIZE = 50;

private readonly IRepository<MediaFile> _pictureRepository;
private readonly IRepository<MediaFile> _mediaFileRepo;
private readonly ICommonServices _services;
private readonly ILogger _logger;

public MediaMover(
IRepository<MediaFile> pictureRepository,
ICommonServices services,
ILogger logger)
public MediaMover(IRepository<MediaFile> mediaFileRepo, ICommonServices services, ILogger logger)
{
_pictureRepository = pictureRepository;
_mediaFileRepo = mediaFileRepo;
_services = services;
_logger = logger;
}

public Localizer T { get; set; } = NullLocalizer.Instance;

protected virtual void PageEntities<TEntity>(IOrderedQueryable<TEntity> query, Action<TEntity> moveEntity) where TEntity : BaseEntity, IHasMedia
{
var pageIndex = 0;
IPagedList<TEntity> entities = null;

do
{
if (entities != null)
{
// detach all entities from previous page to save memory
_services.DbContext.DetachEntities(entities);
entities.Clear();
entities = null;
}

// load max 100 entities at once
entities = new PagedList<TEntity>(query, pageIndex++, PAGE_SIZE);

entities.Each(x => moveEntity(x));

// save the current batch to database
_services.DbContext.SaveChanges();
}
while (entities.HasNextPage);
}

public virtual bool Move(Provider<IMediaStorageProvider> sourceProvider, Provider<IMediaStorageProvider> targetProvider)
{
Guard.NotNull(sourceProvider, nameof(sourceProvider));
@@ -68,7 +42,7 @@ public virtual bool Move(Provider<IMediaStorageProvider> sourceProvider, Provide
var source = sourceProvider.Value as ISupportsMediaMoving;
var target = targetProvider.Value as ISupportsMediaMoving;

// source and target must support media storage moving
// Source and target must support media storage moving
if (source == null)
{
throw new ArgumentException(T("Admin.Media.StorageMovingNotSupported", sourceProvider.Metadata.SystemName));
@@ -79,45 +53,51 @@ public virtual bool Move(Provider<IMediaStorageProvider> sourceProvider, Provide
throw new ArgumentException(T("Admin.Media.StorageMovingNotSupported", targetProvider.Metadata.SystemName));
}

// source and target provider must not be equal
// Source and target provider must not be equal
if (sourceProvider.Metadata.SystemName.IsCaseInsensitiveEqual(targetProvider.Metadata.SystemName))
{
throw new ArgumentException(T("Admin.Media.CannotMoveToSameProvider"));
}

// we are about to process data in chunks but want to commit ALL at once when ALL chunks have been processed successfully.
// autoDetectChanges true required for newly inserted binary data.
// We are about to process data in chunks but want to commit ALL at once after ALL chunks have been processed successfully.
// AutoDetectChanges true required for newly inserted binary data.
using (var scope = new DbContextScope(ctx: _services.DbContext,
autoDetectChanges: true,
proxyCreation: false,
validateOnSave: false,
autoCommit: false))
{
using (var transaction = _services.DbContext.BeginTransaction())
using (var transaction = scope.DbContext.BeginTransaction())
{
try
{
// Files
var queryFiles = _pictureRepository.Table.OrderBy(x => x.Id);
var pager = new FastPager<MediaFile>(_mediaFileRepo.Table, PAGE_SIZE);
while (pager.ReadNextPage(out var files))
{
foreach (var file in files)
{
// Move item from source to target
source.MoveTo(target, context, file);

file.UpdatedOnUtc = utcNow;
++context.MovedItems;
}

PageEntities(queryFiles, file =>
{
// move item from source to target
source.MoveTo(target, context, file);
scope.DbContext.SaveChanges();

file.UpdatedOnUtc = utcNow;
++context.MovedItems;
});
// Detach all entities from previous page to save memory
scope.DbContext.DetachEntities(files, deep: true);
}

transaction.Commit();
transaction.Commit();
success = true;
}
catch (Exception exception)
{
success = false;
transaction.Rollback();

_services.Notifier.Error(exception.Message);
_services.Notifier.Error(exception);
_logger.Error(exception);
}
}
@@ -70,15 +70,14 @@ private void HookObject(BaseEntity entity, IHookedEntity entry, bool beforeSave)
}

var state = entry.InitialState;
var actions = new HashSet<MediaTrack>();

foreach (var prop in properties)
{
if (beforeSave)
{
if (entry.Entry.TryGetModifiedProperty(_dbContext, prop.Name, out object prevValue))
{
var actions = new HashSet<MediaTrack>();

// Untrack the previous file relation (if not null)
TryAddTrack(prop.Album, entry.Entity, prop.Name, prevValue, MediaTrackOperation.Untrack, actions);

@@ -98,7 +97,7 @@ private void HookObject(BaseEntity entity, IHookedEntity entry, bool beforeSave)
TryAddTrack(prop.Album, entry.Entity, prop.Name, value, state == EntityState.Added ? MediaTrackOperation.Track : MediaTrackOperation.Untrack);
break;
case EntityState.Modified:
if (_actionsTemp.TryGetValue(entry.Entity, out var actions))
if (_actionsTemp.TryGetValue(entry.Entity, out actions))
{
_actionsUnit.AddRange(actions);
}
@@ -280,7 +280,7 @@ protected virtual void CreateAttachments(QueuedEmail queuedEmail, MessageContext

if (fileIds.Any())
{
var files = _mediaService.GetFilesByIds(fileIds, MediaLoadFlags.WithBlob);
var files = _mediaService.GetFilesByIds(fileIds);
foreach (var file in files)
{
queuedEmail.Attachments.Add(new QueuedEmailAttachment
@@ -252,13 +252,21 @@ public partial interface IOrderService
/// Search recurring payments
/// </summary>
/// <param name="customerId">The customer identifier; 0 to load all records</param>
/// <param name="storeId">The store identifier; 0 to load all records</param>
/// <param name="storeId">The store identifier; 0 to load all records</param>
/// <param name="initialOrderId">The initial order identifier; 0 to load all records</param>
/// <param name="initialOrderStatus">Initial order status identifier; null to load all records</param>
/// <param name="showHidden">A value indicating whether to show hidden records</param>
/// <param name="pageIndex">Page index</param>
/// <param name="pageSize">Page size</param>
/// <returns>Recurring payment collection</returns>
IList<RecurringPayment> SearchRecurringPayments(int storeId,
int customerId, int initialOrderId, OrderStatus? initialOrderStatus, bool showHidden = false);
IPagedList<RecurringPayment> SearchRecurringPayments(
int storeId,
int customerId,
int initialOrderId,
OrderStatus? initialOrderStatus,
bool showHidden = false,
int pageIndex = 0,
int pageSize = int.MaxValue);

#endregion

@@ -429,13 +429,21 @@ public virtual void UpdateRecurringPayment(RecurringPayment recurringPayment)
_eventPublisher.PublishOrderUpdated(recurringPayment.InitialOrder);
}

public virtual IList<RecurringPayment> SearchRecurringPayments(int storeId,
int customerId, int initialOrderId, OrderStatus? initialOrderStatus,
bool showHidden = false)
public virtual IPagedList<RecurringPayment> SearchRecurringPayments(
int storeId,
int customerId,
int initialOrderId,
OrderStatus? initialOrderStatus,
bool showHidden = false,
int pageIndex = 0,
int pageSize = int.MaxValue)
{
int? initialOrderStatusId = null;

if (initialOrderStatus.HasValue)
{
initialOrderStatusId = (int)initialOrderStatus.Value;
}

var query1 = from rp in _recurringPaymentRepository.Table
join c in _customerRepository.Table on rp.InitialOrder.CustomerId equals c.Id
@@ -454,9 +462,8 @@ public virtual void UpdateRecurringPayment(RecurringPayment recurringPayment)
where query1.Contains(rp.Id)
orderby rp.StartDateUtc, rp.Id
select rp;

var recurringPayments = query2.ToList();
return recurringPayments;

return new PagedList<RecurringPayment>(query2, pageIndex, pageSize);
}

#endregion
@@ -8,6 +8,7 @@
using SmartStore.Core.Logging;
using SmartStore.Core.Plugins;
using SmartStore.Core.Security;
using SmartStore.Data.Setup;

namespace SmartStore.Services.Security
{
@@ -43,8 +44,12 @@ public void Start(HttpContextBase httpContext)
var removeUnusedPermissions = true;
var providers = new List<IPermissionProvider>();

if (PluginManager.PluginChangeDetected || !_permissionRepository.TableUntracked.Any())
if (PluginManager.PluginChangeDetected || DbMigrationContext.Current.GetAppliedMigrations().Any() || !_permissionRepository.TableUntracked.Any())
{
// INFO: even if no plugin has changed: directly after a DB migration this code block MUST run. It seems awkward
// that pending migrations exist when binaries has not changed. But after a manual DB reset for a migration rerun
// nobody touches the binaries usually.

// Standard permission provider and all plugin providers.
var types = _typeFinder.FindClassesOfType<IPermissionProvider>(ignoreInactivePlugins: true).ToList();
foreach (var type in types)
@@ -12,6 +12,7 @@
using SmartStore.Core.Localization;
using SmartStore.Core.Logging;
using SmartStore.Core.Security;
using SmartStore.Data.Utilities;
using SmartStore.Services.Customers;
using SmartStore.Services.Localization;

@@ -308,11 +309,17 @@ public virtual void InstallPermissions(IPermissionProvider[] permissionProviders
{
if (existingRoles == null)
{
var allRoles = _customerService.Value.GetAllCustomerRoles(true);
existingRoles = new Dictionary<string, CustomerRole>();

existingRoles = allRoles
.Where(x => !string.IsNullOrEmpty(x.SystemName))
.ToDictionarySafe(x => x.SystemName, x => x);
var rolesQuery = _customerService.Value.GetAllCustomerRoles(true).SourceQuery;
rolesQuery = rolesQuery.Where(x => !string.IsNullOrEmpty(x.SystemName));

var rolesPager = new FastPager<CustomerRole>(rolesQuery, 500);

while (rolesPager.ReadNextPage(out var roles))
{
roles.Each(x => existingRoles[x.SystemName] = x);
}
}

if (!existingRoles.TryGetValue(roleName, out var role))
@@ -360,14 +367,17 @@ public virtual void InstallPermissions(IPermissionProvider[] permissionProviders
// Remove permissions no longer supported by providers.
if (removeUnusedPermissions)
{
var toDelete = existing.Except(providerPermissions);
var toDelete = existing.Except(providerPermissions).ToList();
if (toDelete.Any())
{
clearCache = true;

var entities = _permissionRepository.Table.Where(x => toDelete.Contains(x.SystemName)).ToList();
entities.Each(x => _permissionRepository.Delete(x));
scope.Commit();
foreach (var chunk in toDelete.Slice(500))
{
var entities = _permissionRepository.Table.Where(x => chunk.Contains(x.SystemName)).ToList();
entities.Each(x => _permissionRepository.Delete(x));
scope.Commit();
}

if (log)
{
@@ -185,6 +185,7 @@
</Compile>
<Compile Include="Blogs\BlogMessageFactoryExtensions.cs" />
<Compile Include="Cart\Rules\Impl\CartProductCountRule.cs" />
<Compile Include="Cart\Rules\Impl\WeekdayRule.cs" />
<Compile Include="Cart\Rules\Impl\ProductFromCategoryInCartRule.cs" />
<Compile Include="Cart\Rules\Impl\ProductFromManufacturerInCartRule.cs" />
<Compile Include="Cart\Rules\Impl\ProductOnWishlistRule.cs" />
@@ -1,8 +1,8 @@
FriendlyName: Login and Pay with Amazon
SystemName: SmartStore.AmazonPay
Group: Payment
Version: 4.0.0
MinAppVersion: 4.0.0
Version: 4.0.1
MinAppVersion: 4.0.1
Author: SmartStore AG
DisplayOrder: 1
FileName: SmartStore.AmazonPay.dll
@@ -1,8 +1,8 @@
FriendlyName: Clickatell SMS Provider
SystemName: SmartStore.Clickatell
Group: Mobile
Version: 4.0.0
MinAppVersion: 4.0.0
Version: 4.0.1
MinAppVersion: 4.0.1
DisplayOrder: 1
FileName: SmartStore.Clickatell.dll
ResourceRootKey: Plugins.Sms.Clickatell
@@ -1,8 +1,8 @@
FriendlyName: Smartstore Developer Tools (MiniProfiler and other goodies)
SystemName: SmartStore.DevTools
Group: Developer
Version: 4.0.0
MinAppVersion: 4.0.0
Version: 4.0.1
MinAppVersion: 4.0.1
DisplayOrder: 1
FileName: SmartStore.DevTools.dll
ResourceRootKey: Plugins.Developer.DevTools
@@ -1,8 +1,8 @@
FriendlyName: Facebook Login
SystemName: SmartStore.FacebookAuth
Group: Security
Version: 4.0.0
MinAppVersion: 4.0.0
Version: 4.0.1
MinAppVersion: 4.0.1
DisplayOrder: 5
FileName: SmartStore.FacebookAuth.dll
ResourceRootKey: Plugins.ExternalAuth.Facebook
@@ -9,6 +9,7 @@
using SmartStore.Core.Logging;
using SmartStore.Core.Plugins;
using SmartStore.GoogleAnalytics.Models;
using SmartStore.GoogleAnalytics.Services;
using SmartStore.Services.Catalog;
using SmartStore.Services.Configuration;
using SmartStore.Services.Customers;
@@ -58,7 +59,7 @@ public ActionResult Configure(GoogleAnalyticsSettings settings)
return View(model);
}

[HttpPost, AdminAuthorize, ChildActionOnly, ValidateInput(false)]
[HttpPost, AdminAuthorize, ChildActionOnly, ValidateInput(false), FormValueRequired("save")]
public ActionResult Configure(ConfigurationModel model, FormCollection form)
{
var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData);
@@ -83,6 +84,23 @@ public ActionResult Configure(ConfigurationModel model, FormCollection form)
return RedirectToConfiguration("SmartStore.GoogleAnalytics");
}

[AdminAuthorize, HttpPost]
[ActionName("Configure"), FormValueRequired("restore-scripts")]
public ActionResult RestoreScripts()
{
var storeDependingSettingHelper = new StoreDependingSettingHelper(ViewData);
var storeScope = this.GetActiveStoreScopeConfiguration(Services.StoreService, Services.WorkContext);
var settings = Services.Settings.LoadSetting<GoogleAnalyticsSettings>(storeScope);

settings.TrackingScript = GoogleAnalyticsScriptHelper.GetTrackingScript();
settings.EcommerceScript = GoogleAnalyticsScriptHelper.GetEcommerceScript();
settings.EcommerceDetailScript = GoogleAnalyticsScriptHelper.GetEcommerceDetailScript();

_settingService.SaveSetting(settings, storeScope);

return RedirectToConfiguration("SmartStore.GoogleAnalytics", true);
}

[ChildActionOnly]
public ActionResult PublicInfo(string widgetZone)
{
@@ -147,13 +165,15 @@ private string GetStorageScript()
{
// If no consent to analytical cookies was given, set storage to none.
var script = @"
ga('set', 'storage', 'none');
ga('set', 'clientId', '{0}');
{
'storage': 'none',
'clientId': '" + _workContext.CurrentCustomer.CustomerGuid + @"',
'storeGac': false
}
";

script = script + "\n";
script = script.FormatWith(_workContext.CurrentCustomer.CustomerGuid);


return script;
}

@@ -164,7 +184,7 @@ private string GetTrackingScript(bool cookiesAllowed)
script = script.Replace("{GOOGLEID}", settings.GoogleId);
script = script.Replace("{ECOMMERCE}", "");
script = script.Replace("{OPTOUTCOOKIE}", GetOptOutCookieScript());
script = script.Replace("{STORAGETYPE}", cookiesAllowed ? "" : GetStorageScript());
script = script.Replace("{STORAGETYPE}", cookiesAllowed ? "'auto'" : GetStorageScript());

return script;
}
@@ -231,7 +251,7 @@ private string GetEcommerceScript(Order order, bool cookiesAllowed)
script = script.Replace("{ECOMMERCE}", ecScript);

// If no consent to third party cookies was given, set storage to none.
script = script.Replace("{STORAGETYPE}", cookiesAllowed ? "" : GetStorageScript());
script = script.Replace("{STORAGETYPE}", cookiesAllowed ? "'auto'" : GetStorageScript());
}

return script;
@@ -1,8 +1,8 @@
FriendlyName: Google Analytics
SystemName: SmartStore.GoogleAnalytics
Group: Analytics
Version: 4.0.0
MinAppVersion: 4.0.0
Version: 4.0.1.1
MinAppVersion: 4.0.1
DisplayOrder: 1
FileName: SmartStore.GoogleAnalytics.dll
ResourceRootKey: Plugins.Widgets.GoogleAnalytics
@@ -1,62 +1,18 @@
using System.Collections.Generic;
using System.Web.Routing;
using SmartStore.Core.Plugins;
using SmartStore.GoogleAnalytics.Services;
using SmartStore.Services.Cms;
using SmartStore.Services.Configuration;
using SmartStore.Services.Localization;

namespace SmartStore.GoogleAnalytics
{
/// <summary>
/// Google Analytics Plugin
/// </summary>
public class GoogleAnalyticPlugin : BasePlugin, IWidget, IConfigurable, ICookiePublisher
{
#region Scripts

private const string TRACKING_SCRIPT = @"<!-- Google code for Analytics tracking -->
<script>
{OPTOUTCOOKIE}
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', '{GOOGLEID}', 'auto');
ga('set', 'anonymizeIp', true);
ga('send', 'pageview');
{STORAGETYPE}
{ECOMMERCE}
</script>";

private const string ECOMMERCE_SCRIPT = @"ga('require', 'ecommerce');
ga('ecommerce:addTransaction', {
'id': '{ORDERID}',
'affiliation': '{SITE}',
'revenue': '{TOTAL}',
'shipping': '{SHIP}',
'tax': '{TAX}',
'currency': '{CURRENCY}'
});
{DETAILS}
ga('ecommerce:send');";

private const string ECOMMERCE_DETAIL_SCRIPT = @"ga('ecommerce:addItem', {
'id': '{ORDERID}',
'name': '{PRODUCTNAME}',
'sku': '{PRODUCTSKU}',
'category': '{CATEGORYNAME}',
'price': '{UNITPRICE}',
'quantity': '{QUANTITY}'
});";

#endregion

/// <summary>
/// Google Analytics Plugin
/// </summary>
public class GoogleAnalyticPlugin : BasePlugin, IWidget, IConfigurable, ICookiePublisher
{
private readonly ISettingService _settingService;
private readonly GoogleAnalyticsSettings _googleAnalyticsSettings;
private readonly ILocalizationService _localizationService;
@@ -135,15 +91,15 @@ public CookieInfo GetCookieInfo()
return cookieInfo;
}

public override void Install()
{
var settings = new GoogleAnalyticsSettings
{
GoogleId = "UA-0000000-0",
TrackingScript = TRACKING_SCRIPT,
EcommerceScript = ECOMMERCE_SCRIPT,
EcommerceDetailScript = ECOMMERCE_DETAIL_SCRIPT
};
public override void Install()
{
var settings = new GoogleAnalyticsSettings
{
GoogleId = "UA-0000000-0",
TrackingScript = GoogleAnalyticsScriptHelper.GetTrackingScript(),
EcommerceScript = GoogleAnalyticsScriptHelper.GetEcommerceScript(),
EcommerceDetailScript = GoogleAnalyticsScriptHelper.GetEcommerceDetailScript()
};

_settingService.SaveSetting(settings);
_localizationService.ImportPluginResourcesFromXml(this.PluginDescriptor);
@@ -51,4 +51,10 @@
<LocaleResource Name="CookieInfo">
<Value>Google Analytics hilft uns das Nutzerverhalten unserer Webseite zu analysieren und aufgrund dessen zu verbessern, um Ihnen die bestmögliche Nutzererfahrung zu bieten.</Value>
</LocaleResource>
<LocaleResource Name="RestoreScripts">
<Value>Scripte wiederherstellen</Value>
</LocaleResource>
<LocaleResource Name="RestoreScripts.Hint">
<Value>Stellt die Original-Scripte wieder her. Nach einem Shop-Update sollte diese Funktion genutzt werden, um sicher zu gehen, dass die aktuellsten Scripte verwendet werden.</Value>
</LocaleResource>
</Language>
@@ -51,4 +51,10 @@
<LocaleResource Name="CookieInfo">
<Value>Google Analytics helps us analyze and improve the user behavior of our website to provide you with the best possible user experience.</Value>
</LocaleResource>
<LocaleResource Name="RestoreScripts">
<Value>Restore scripts</Value>
</LocaleResource>
<LocaleResource Name="RestoreScripts.Hint">
<Value>Restores the original scripts. After a store update this function should be used to make sure that the latest scripts are used.</Value>
</LocaleResource>
</Language>
@@ -0,0 +1,60 @@
namespace SmartStore.GoogleAnalytics.Services
{
/// <summary>
/// Provides ready to use scripts for plugin settings.
/// </summary>
/// <remarks>
/// Code formatting (everything is squeezed to the edge) was done intentionally like this.
/// Else whitespace would be copied into the setting properties and effect the configuration page in a negative way.
/// </remarks>
public partial class GoogleAnalyticsScriptHelper
{
internal static string GetTrackingScript()
{
return @"<!-- Google code for Analytics tracking -->
<script>
{OPTOUTCOOKIE}
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','//www.google-analytics.com/analytics.js','ga');
ga('create', '{GOOGLEID}', {STORAGETYPE});
ga('set', 'anonymizeIp', true);
ga('send', 'pageview');
{ECOMMERCE}
</script>";
}

internal static string GetEcommerceScript()
{
return @"ga('require', 'ecommerce');
ga('ecommerce:addTransaction', {
'id': '{ORDERID}',
'affiliation': '{SITE}',
'revenue': '{TOTAL}',
'shipping': '{SHIP}',
'tax': '{TAX}',
'currency': '{CURRENCY}'
});
{DETAILS}
ga('ecommerce:send');";
}

internal static string GetEcommerceDetailScript()
{
return @"ga('ecommerce:addItem', {
'id': '{ORDERID}',
'name': '{PRODUCTNAME}',
'sku': '{PRODUCTSKU}',
'category': '{CATEGORYNAME}',
'price': '{UNITPRICE}',
'quantity': '{QUANTITY}'
});";
}
}
}
@@ -141,6 +141,7 @@
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="RouteProvider.cs" />
<Compile Include="GoogleAnalyticsSettings.cs" />
<Compile Include="Services\GoogleAnalyticsScriptHelper.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Libraries\SmartStore.Core\SmartStore.Core.csproj">
@@ -41,7 +41,7 @@
<td class="adminTitle">
@Html.SmartLabelFor(model => model.TrackingScript)
</td>
<td class="adminData">
<td class="adminData wide">
@Html.SettingEditorFor(model => model.TrackingScript,
Html.TextAreaFor(model => model.TrackingScript, new { style = "height: 300px;" }))
@Html.ValidationMessageFor(model => model.TrackingScript)
@@ -51,7 +51,7 @@
<td class="adminTitle">
@Html.SmartLabelFor(model => model.EcommerceScript)
</td>
<td class="adminData">
<td class="adminData wide">
@Html.SettingEditorFor(model => model.EcommerceScript,
Html.TextAreaFor(model => model.EcommerceScript, new { style = "height: 300px;" }))
@Html.ValidationMessageFor(model => model.EcommerceScript)
@@ -61,11 +61,23 @@
<td class="adminTitle">
@Html.SmartLabelFor(model => model.EcommerceDetailScript)
</td>
<td class="adminData">
<td class="adminData wide">
@Html.SettingEditorFor(model => model.EcommerceDetailScript,
Html.TextAreaFor(model => model.EcommerceDetailScript, new { style = "height: 200px;" }))
@Html.ValidationMessageFor(model => model.EcommerceDetailScript)
</td>
</tr>
<tr>
<td class="adminTitle">
</td>
<td class="adminData">
<button type="submit" name="restore-scripts" class="btn btn-danger" value="restore-scripts"
title="@T("Plugins.Widgets.GoogleAnalytics.RestoreScripts.Hint")"
onclick="return confirm(@T("Admin.Common.AreYouSure").JsText);">
<i class="far fa-bolt"></i>
<span>@T("Plugins.Widgets.GoogleAnalytics.RestoreScripts")</span>
</button>
</td>
</tr>
</table>
}
@@ -1,8 +1,8 @@
FriendlyName: Google Merchant Center (GMC) feed
SystemName: SmartStore.GoogleMerchantCenter
Group: Marketing
Version: 4.0.0
MinAppVersion: 4.0.0
Version: 4.0.1
MinAppVersion: 4.0.1
Author: SmartStore AG
DisplayOrder: 1
FileName: SmartStore.GoogleMerchantCenter.dll
@@ -2,8 +2,8 @@
Description: Contains common offline payment methods like Direct Debit, Invoice, Prepayment etc.
Group: Payment
SystemName: SmartStore.OfflinePayment
Version: 4.0.0
MinAppVersion: 4.0.0
Version: 4.0.1
MinAppVersion: 4.0.1
DisplayOrder: 0
FileName: SmartStore.OfflinePayment.dll
ResourceRootKey: Plugins.SmartStore.OfflinePayment
@@ -2,8 +2,8 @@
Description: Provides the PayPal payment methods PayPal Standard, PayPal Direct, PayPal Express and PayPal PLUS.
SystemName: SmartStore.PayPal
Group: Payment
Version: 4.0.0
MinAppVersion: 4.0.0
Version: 4.0.1
MinAppVersion: 4.0.1
DisplayOrder: 1
FileName: SmartStore.PayPal.dll
ResourceRootKey: Plugins.SmartStore.PayPal
@@ -2,8 +2,8 @@
Description: Provides shipping methods for fixed rate shipping and computation based on weight.
SystemName: SmartStore.Shipping
Group: Shipping
Version: 4.0.0
MinAppVersion: 4.0.0
Version: 4.0.1
MinAppVersion: 4.0.1
DisplayOrder: 1
FileName: SmartStore.Shipping.dll
ResourceRootKey: Plugins.SmartStore.Shipping
@@ -1,8 +1,8 @@
FriendlyName: Shipping by weight
SystemName: SmartStore.ShippingByWeight
Group: Shipping
Version: 4.0.0
MinAppVersion: 4.0.0
Version: 4.0.1
MinAppVersion: 4.0.1
DisplayOrder: 1
FileName: SmartStore.ShippingByWeight.dll
ResourceRootKey: Plugins.Shipping.ByWeight
@@ -2,8 +2,8 @@
Description: Contains default tax providers like FixedRate, ByRegion etc.
Group: Tax
SystemName: SmartStore.Tax
Version: 4.0.0
MinAppVersion: 4.0.0
Version: 4.0.1
MinAppVersion: 4.0.1
DisplayOrder: 0
FileName: SmartStore.Tax.dll
ResourceRootKey: Plugins.SmartStore.Tax
@@ -1,8 +1,8 @@
FriendlyName: Smartstore Web API
SystemName: Smartstore.WebApi
Group: Api
Version: 4.0.0
MinAppVersion: 4.0.0
Version: 4.0.1
MinAppVersion: 4.0.1
Author: SmartStore AG
DisplayOrder: 1
FileName: SmartStore.WebApi.dll
@@ -489,8 +489,8 @@ internal class LocalizationModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<LanguageService>().As<ILanguageService>().InstancePerRequest();

builder.RegisterType<LanguageService>().As<ILanguageService>().InstancePerRequest();
builder.RegisterType<TelerikLocalizationServiceFactory>().As<Telerik.Web.Mvc.Infrastructure.ILocalizationServiceFactory>().InstancePerRequest();
builder.RegisterType<LocalizationService>().As<ILocalizationService>().InstancePerRequest();

@@ -501,7 +501,7 @@ protected override void Load(ContainerBuilder builder)
builder.RegisterType<LocalizationFileResolver>().As<ILocalizationFileResolver>().InstancePerRequest();
builder.RegisterType<LocalizedEntityService>().As<ILocalizedEntityService>().InstancePerRequest();
builder.RegisterType<LocalizedEntityHelper>().InstancePerRequest();
}
}

protected override void AttachToComponentRegistration(IComponentRegistry componentRegistry, IComponentRegistration registration)
{
@@ -1112,7 +1112,6 @@ private string ProviderTypeToKnownGroupName(Type implType)
}

#endregion

}

internal class TasksModule : Module
@@ -182,35 +182,24 @@ internal static string GetUserThemeChoiceFromCookie(this HttpContextBase context

internal static void SetUserThemeChoiceInCookie(this HttpContextBase context, string value)
{
if (context == null)
return;

var cookie = context.Request.Cookies.Get("sm.UserThemeChoice");

if (value.HasValue() && cookie == null)
if (context?.Request == null)
{
cookie = new HttpCookie("sm.UserThemeChoice")
{
HttpOnly = true,
Expires = DateTime.UtcNow.AddYears(1)
};
return;
}

if (value.HasValue())
if (value.IsEmpty())
{
cookie.Value = value;
context.Request.Cookies.Set(cookie);
context.Request.Cookies.Remove("sm.UserThemeChoice");
return;
}

if (value.IsEmpty() && cookie != null)
{
cookie.Expires = DateTime.UtcNow.AddYears(-10);
}
var cookie = context.Request.Cookies.Get("sm.UserThemeChoice") ?? new HttpCookie("sm.UserThemeChoice");
cookie.Value = value;
cookie.Expires = DateTime.UtcNow.AddYears(1);
cookie.HttpOnly = true;
cookie.Secure = context.Request.IsHttps();

if (cookie != null)
{
context.Response.SetCookie(cookie);
}
context.Request.Cookies.Set(cookie);
}

internal static HttpCookie GetPreviewModeCookie(this HttpContextBase context, bool createIfMissing)
@@ -18,8 +18,9 @@ public class PublicStoreAllowNavigationAttribute : FilterAttribute, IAuthorizati
new Tuple<string, string>("SmartStore.Web.Controllers.CustomerController", "PasswordRecoveryConfirm"),
new Tuple<string, string>("SmartStore.Web.Controllers.CustomerController", "AccountActivation"),
new Tuple<string, string>("SmartStore.Web.Controllers.CustomerController", "CheckUsernameAvailability"),
new Tuple<string, string>("SmartStore.Web.Controllers.MenuController", "OffCanvas")
};
new Tuple<string, string>("SmartStore.Web.Controllers.MenuController", "OffCanvas"),
new Tuple<string, string>("SmartStore.Web.Controllers.ShoppingCartController", "CartSummary")
};

public Lazy<IPermissionService> PermissionService { get; set; }

@@ -1,16 +1,14 @@
using SmartStore.Core.Domain.Common;
using SmartStore.Web.Framework.Modelling;
using SmartStore.Web.Framework.Pdf;

namespace SmartStore.Web.Framework.Pdf
{
public partial class PdfReceiptHeaderFooterModel : ModelBase
public partial class PdfReceiptHeaderFooterModel : ModelBase
{
public int StoreId { get; set; }

public string StoreName { get; set; }
public string LogoUrl { get; set; }
public string StoreUrl { get; set; }
public int LogoId { get; set; }

public CompanyInformationSettings MerchantCompanyInfo { get; set; }
public BankConnectionSettings MerchantBankAccount { get; set; }
@@ -0,0 +1,47 @@
using System;
using System.Web;
using SmartStore.Core.Infrastructure;
using SmartStore.Core.Plugins;
using SmartStore.Services.Localization;

namespace SmartStore.Web.Framework.Plugins
{
/// <summary>
/// Checks whether any plugin has changed and refreshes all plugin locale resources.
/// </summary>
public sealed class PluginStarter : IPostApplicationStart
{
private readonly IPluginFinder _pluginFinder;
private readonly ILocalizationService _locService;

public PluginStarter(IPluginFinder pluginFinder, ILocalizationService locService)
{
_pluginFinder = pluginFinder;
_locService = locService;
}

public int Order => 100;
public bool ThrowOnError => false;
public int MaxAttempts => 1;

public void Start(HttpContextBase httpContext)
{
//if (!PluginManager.PluginChangeDetected)
// return;

var descriptors = _pluginFinder.GetPluginDescriptors(true);
foreach (var d in descriptors)
{
var hasher = _locService.CreatePluginResourcesHasher(d);
if (hasher.HasChanged)
{
_locService.ImportPluginResourcesFromXml(d, null, false);
}
}
}

public void OnFail(Exception exception, bool willRetry)
{
}
}
}
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Json;
@@ -61,7 +62,8 @@ public override void OnActionExecuting(ActionExecutingContext filterContext)
}
else
{
foreach (var error in result.ErrorCodes)
// Do not log 'missing input'. Could be a regular case.
foreach (var error in result.ErrorCodes.Where(x => x.HasValue() && x != "missing-input-response"))
{
Logger.Error("Error while getting Google Recaptcha response: " + error);
}
@@ -76,7 +78,7 @@ public override void OnActionExecuting(ActionExecutingContext filterContext)
Logger.ErrorsAll(ex);
}

// This will push the result value into a parameter in our Action.
// This will push the result value into a parameter in our action method.
filterContext.ActionParameters["captchaValid"] = valid;

filterContext.ActionParameters["captchaError"] = !valid && CaptchaSettings.Value.CanDisplayCaptcha
@@ -236,6 +236,7 @@
<Compile Include="Modelling\Results\PermissiveRedirectResult.cs" />
<Compile Include="Modelling\Results\RootActionViewResult.cs" />
<Compile Include="Plugins\PluginRazorHost.cs" />
<Compile Include="Plugins\PluginStarter.cs" />
<Compile Include="Security\Honeypot\HtmlHoneypotExtensions.cs" />
<Compile Include="Security\Honeypot\ValidateHoneypotAttribute.cs" />
<Compile Include="Security\Honeypot\Honeypot.cs" />
@@ -43,7 +43,13 @@ public bool IsResponsive
set;
}

public string Breakpoint
public bool HideSingleItem
{
get;
set;
}

public string Breakpoint
{
get;
set;
@@ -25,6 +25,12 @@ public TabStripBuilder<TModel> Responsive(bool value, string breakpoint = "<lg")
return this;
}

public TabStripBuilder<TModel> HideSingleItem(bool value)
{
base.Component.HideSingleItem = value;
return this;
}

public TabStripBuilder<TModel> TabContentHeaderContent(string value)
{
if (value.IsEmpty())
@@ -104,6 +104,11 @@ protected override void WriteHtmlCore(HtmlTextWriter writer)
ulAttrs.AppendCssClass("nav-tabs nav-tabs-line");
}

if (tab.HideSingleItem && tab.Items.Count == 1)
{
ulAttrs.AppendCssClass("d-none");
}

if (isStacked)
{
ulAttrs.AppendCssClass("flex-row flex-lg-column");
@@ -128,8 +128,8 @@ public static void AppendCssFileParts(this IPageAssetsBuilder builder, ResourceL

public static void AddLinkPart(this IPageAssetsBuilder builder, string rel, string href, object htmlAttributes)
{
var attrs = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
builder.AddLinkPart(rel, href, attrs);
var attrs = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
builder.AddLinkPart(rel, href, attrs);
}

public static void AddLinkPart(this IPageAssetsBuilder builder, string rel, string href, string type = null, string media = null, string sizes = null, string hreflang = null)
@@ -19,6 +19,7 @@
using SmartStore.Services;
using SmartStore.Services.Directory;
using SmartStore.Services.Localization;
using SmartStore.Web.Framework.WebApi.Security;

namespace SmartStore.Web.Framework.WebApi
{
@@ -43,6 +44,7 @@ protected internal HttpResponseException ExceptionNotExpanded<TProperty>(Express
return new HttpResponseException(response);
}

[WebApiAuthenticate]
public override HttpResponseMessage HandleUnmappedRequest(ODataPath odataPath)
{
if (odataPath.PathTemplate.IsCaseInsensitiveEqual("~/entityset/key/property") ||
@@ -172,6 +174,7 @@ public virtual ICommonServices Services
set;
}

[WebApiAuthenticate]
public override IQueryable<TEntity> Get()
{
if (!ModelState.IsValid)
@@ -344,6 +347,7 @@ protected internal virtual TProperty GetExpandedProperty<TProperty>(int key, Exp
return SingleResult.Create(query);
}

[WebApiAuthenticate]
public override HttpResponseMessage Post(TEntity entity)
{
var response = Request.CreateResponse(HttpStatusCode.OK, CreateEntity(entity));
@@ -380,6 +384,7 @@ protected internal virtual void Insert(TEntity entity)
Repository.Insert(entity);
}

[WebApiAuthenticate]
public override HttpResponseMessage Put(int key, TEntity update)
{
return Request.CreateResponse(HttpStatusCode.OK, UpdateEntity(key, update));
@@ -411,6 +416,7 @@ protected internal virtual void Update(TEntity entity)
Repository.Update(entity);
}

[WebApiAuthenticate]
public override HttpResponseMessage Patch(int key, Delta<TEntity> patch)
{
return Request.CreateResponse(HttpStatusCode.OK, PatchEntity(key, patch));
@@ -431,6 +437,7 @@ protected override TEntity PatchEntity(int key, Delta<TEntity> patch)
return entity;
}

[WebApiAuthenticate]
public override void Delete(int key)
{
if (!ModelState.IsValid)
@@ -187,9 +187,12 @@ protected virtual Customer GetGuestCustomer()
// Set visitor cookie
if ( _httpContext?.Response != null)
{
visitorCookie = new HttpCookie(VisitorCookieName);
visitorCookie.HttpOnly = true;
//visitorCookie.Secure = true;
visitorCookie = new HttpCookie(VisitorCookieName)
{
HttpOnly = true,
Secure = _httpContext.Request.IsHttps()
};

visitorCookie.Value = customer.CustomerGuid.ToString();
if (customer.CustomerGuid == Guid.Empty)
{
@@ -22,6 +22,7 @@ $section-header-height: 70px;
background-color: $gray-100;
padding: 1rem;
margin-bottom: $alert-margin-bottom;
border-radius: $border-radius;
}

pre {
@@ -276,6 +277,8 @@ body {
padding: calc(var(--content-padding-y) / 2) var(--content-padding-x);
background: #fff;
box-shadow: 0 8px 16px 0 rgba(#32325d, .1), 0 2px 7px 0 rgba(#000, .07);
--shadow-color-intensity: 1;
box-shadow: $box-shadow-sm-var;

.modal-open & {
// add the killed scrollbar width
@@ -390,14 +393,14 @@ body {
text-align: center;
color: #fff;
background: rgba(#fff, 0);
opacity: 0.8;
transition: opacity 0.1s linear, background-color 0.1s linear;
transition: color 0.1s linear, opacity 0.1s linear, background-color 0.1s linear;
color: mix($indigo, #fff, 22%);
}

.nav-link:hover,
&.show .nav-link {
background: rgba(#fff, .15);
opacity: 1;
color: #fff;
}

.navbar-icon {
@@ -407,7 +410,6 @@ body {
font-size: 20px;
height: 22px;
line-height: 22px;
color: #fff;
}

.navbar-label {
@@ -273,7 +273,7 @@
margin: 0;
}

& li {
li {
display: inline-block;
user-select: none;

@@ -287,19 +287,23 @@
}
}

& .total-amount {
.total-amount {
text-decoration: none !important;
display: inline-block;
user-select: text;
}

& .legend {
.legend {
border-radius: 7px;
display: inline-block;
height: 11px;
margin-right: .3rem;
width: 11px;
}

.hidden > .legend {
background-color: $gray-400 !important;
}
}

.chevron {
@@ -315,6 +319,122 @@
transform: rotate(180deg);
}
}

.chart-tooltip {
opacity: 1;
position: absolute;
background: rgba(#000, 0.8);
color: $light;
border-radius: $border-radius-sm;
transition: all 0.2s ease-in-out;
pointer-events: none;
transform: translateX(-50%);
z-index: 10;
padding: 0.4rem 0.7rem 0.5rem 0.7rem;

&:after {
content: "";
position: absolute;
width: 0;
height: 0;
background-color: transparent;
}

&.bottom:after,
&.top:not(.right):not(.left):after {
border-left: 9px solid transparent;
border-right: 9px solid transparent;
transform: translateX(-50%);
transition: left 0.1s ease-in-out;
left: 50%;
}

&.bottom {
transform: translate(-50%, calc(-100% - 15px));

&:after {
border-top: 9px solid rgba($black, 0.8);
top: 100%;
}

&.left {
transform: translate(-10%, calc(-100% - 15px));

&:after {
left: 10%;
}
}

&.right {
transform: translate(-90%, calc(-100% - 15px));

&:after {
left: 90%;
}
}
}

&.top:not(.right):not(.left) {
transform: translate(-50%, 15px);

&:after {
border-bottom: 9px solid rgba($black, 0.8);
top: -9px;
}
}

&.left:not(.bottom):after,
&.right:not(.bottom):after {
border-top: 9px solid transparent;
border-bottom: 9px solid transparent;
transform: translateY(-50%);
transition: top 0.1s ease-in-out;
top: 50%;
}

&.left:not(.bottom) {
transform: translate(15px, -50%);

&:after {
border-right: 9px solid rgba($black, 0.8);
left: -9px;
}
}

&.right:not(.bottom) {
transform: translate(calc(-100% - 15px), -50%);

&:after {
border-left: 9px solid rgba($black, 0.8);
left: 100%;
}
}

.chart-tooltip-indicator {
display: inline-block;
width: 11px;
height: 11px;
margin-right: 0.33rem;
border-radius: 50%;
margin-bottom: -1px;
}

.chart-tooltip-title {
font-size: $font-size-sm;
line-height: initial;
font-weight: $font-weight-bold;
padding-bottom: 0.2rem;
}

.chart-tooltip-body {
font-size: $font-size-xs;
line-height: initial;

&:not(:last-child) {
padding-bottom: 0.1rem;
}
}
}
}

.report-incomplete-orders {
@@ -479,6 +599,7 @@
grid-row: 4 / 5;
}
}

.table-prevent-overflow {
tr > th,
tr > td {
@@ -118,9 +118,20 @@ $border-radius-lg: 0.3rem;

// Shadows

$box-shadow-sm-var: 0 0 .75rem 0 rgba(#000, .05);
$box-shadow-var: 0 1rem 2.25rem rgba(#32325d, .03), 0 0.3125rem 1rem rgba(#000, .12);
$box-shadow-lg-var: 0 1rem 3rem rgba(#000, .125);
//$box-shadow-sm-var: 0 0 .75rem 0 rgba(#000, .05);
//$box-shadow-var: 0 1rem 2.25rem rgba(#32325d, .03), 0 0.3125rem 1rem rgba(#000, .12);
//$box-shadow-lg-var: 0 1rem 3rem rgba(#000, .125);

$box-shadow-sm-var: 0 calc(1px * var(--shadow-color-yoffset, 1)) 1px rgba(var(--shadow-color-rgb), calc(var(--shadow-color-intensity, 1) * 0.06)),
0 calc(2px * var(--shadow-color-yoffset, 1)) 4px rgba(var(--shadow-color-rgb), calc(var(--shadow-color-intensity, 1) * 0.06)),
0 calc(3px * var(--shadow-color-yoffset, 1)) 8px rgba(var(--shadow-color-rgb), calc(var(--shadow-color-intensity, 1) * 0.1));
$box-shadow-var: 0 calc(1px * var(--shadow-color-yoffset, 1)) 2px rgba(var(--shadow-color-rgb), calc(var(--shadow-color-intensity, 1) * 0.06)),
0 calc(3px * var(--shadow-color-yoffset, 1)) 6px rgba(var(--shadow-color-rgb), calc(var(--shadow-color-intensity, 1) * 0.08)),
0 calc(6px * var(--shadow-color-yoffset, 1)) 16px rgba(var(--shadow-color-rgb), calc(var(--shadow-color-intensity, 1) * 0.12));
$box-shadow-lg-var: 0 calc(1px * var(--shadow-color-yoffset, 1)) 2px rgba(var(--shadow-color-rgb), calc(var(--shadow-color-intensity, 1) * 0.06)),
0 calc(3px * var(--shadow-color-yoffset, 1)) 8px rgba(var(--shadow-color-rgb), calc(var(--shadow-color-intensity, 1) * 0.06)),
0 calc(8px * var(--shadow-color-yoffset, 1)) 20px rgba(var(--shadow-color-rgb), calc(var(--shadow-color-intensity, 1) * 0.1)),
0 calc(16px * var(--shadow-color-yoffset, 1)) 42px rgba(var(--shadow-color-rgb), calc(var(--shadow-color-intensity, 1) * 0.12));

$box-shadow-sm: var(--box-shadow);
$box-shadow: var(--box-shadow);
@@ -166,8 +177,10 @@ $btn-border-radius-lg: $input-border-radius-lg; //.1875rem;
$btn-border-radius-sm: $input-border-radius-sm; //.125rem;
$btn-disabled-opacity: 0.5;

$dropdown-box-shadow: $box-shadow-var; // 0 3px 12px rgba(27,31,35, 0.15); //0 2px 6px rgba(#000, .15);
$dropdown-border-color: rgba(#000, 0.05);
//$dropdown-box-shadow: $box-shadow-var; // 0 3px 12px rgba(27,31,35, 0.15); //0 2px 6px rgba(#000, .15);
//$dropdown-border-color: rgba(#000, 0.05);
$dropdown-box-shadow: $box-shadow-lg-var;
$dropdown-border-color: $gray-300;
$dropdown-link-color: $body-color; //$gray-800;
$dropdown-link-hover-color: darken($dropdown-link-color, 5%);
$dropdown-link-hover-bg: lighten($gray-200, 3%);
@@ -199,8 +212,8 @@ $modal-header-padding: 1.25rem;
$modal-content-border-width: 0;
$modal-content-border-radius: $border-radius;
$modal-backdrop-opacity: .75;
$modal-content-box-shadow-xs: 0 12px 26px rgba(#32325d, .12), 0 3px 10px rgba(#000, .08);
$modal-content-box-shadow-sm-up: 0 50px 100px rgba(#32325d, .12), 0 15px 35px rgba(#32325d, .27), 0 5px 15px rgba(#000, .25);
$modal-content-box-shadow-xs: $box-shadow-var;
$modal-content-box-shadow-sm-up: $box-shadow-lg-var;


// Embeds (TODO: Use from v4.3 onwards)
@@ -874,7 +874,7 @@ public ActionResult MaintenanceDeleteImageCache()
{
_imageCache.Value.Clear();

// get rid of cached image metadata
// Get rid of cached image metadata.
_services.Cache.Clear();

return RedirectToAction("Maintenance");
@@ -883,17 +883,21 @@ public ActionResult MaintenanceDeleteImageCache()
[HttpPost, ActionName("Maintenance")]
[FormValueRequired("delete-guests")]
[Permission(Permissions.System.Maintenance.Execute)]
public async Task<ActionResult> MaintenanceDeleteGuests(MaintenanceModel model)
public ActionResult MaintenanceDeleteGuests(MaintenanceModel model)
{
DateTime? startDateValue = (model.DeleteGuests.StartDate == null) ? null
: (DateTime?)_dateTimeHelper.Value.ConvertToUtcTime(model.DeleteGuests.StartDate.Value, _dateTimeHelper.Value.CurrentTimeZone);
DateTime? startDateValue = model.DeleteGuests.StartDate == null
? null
: (DateTime?)_dateTimeHelper.Value.ConvertToUtcTime(model.DeleteGuests.StartDate.Value, _dateTimeHelper.Value.CurrentTimeZone);

DateTime? endDateValue = (model.DeleteGuests.EndDate == null) ? null
: (DateTime?)_dateTimeHelper.Value.ConvertToUtcTime(model.DeleteGuests.EndDate.Value, _dateTimeHelper.Value.CurrentTimeZone).AddDays(1);
DateTime? endDateValue = model.DeleteGuests.EndDate == null
? null
: (DateTime?)_dateTimeHelper.Value.ConvertToUtcTime(model.DeleteGuests.EndDate.Value, _dateTimeHelper.Value.CurrentTimeZone).AddDays(1);

model.DeleteGuests.NumberOfDeletedCustomers = await _customerService.DeleteGuestCustomersAsync(startDateValue, endDateValue, model.DeleteGuests.OnlyWithoutShoppingCart);
var numberOfDeletedCustomers = _customerService.DeleteGuestCustomers(startDateValue, endDateValue, model.DeleteGuests.OnlyWithoutShoppingCart);

return View(model);
NotifyInfo(T("Admin.System.Maintenance.DeleteGuests.TotalDeleted", numberOfDeletedCustomers));

return RedirectToAction("Maintenance");
}

[HttpPost, ActionName("Maintenance")]

Large diffs are not rendered by default.

@@ -10,6 +10,7 @@
using SmartStore.Core.Domain.Tax;
using SmartStore.Core.Logging;
using SmartStore.Core.Security;
using SmartStore.Data.Utilities;
using SmartStore.Rules;
using SmartStore.Services.Customers;
using SmartStore.Services.Tasks;
@@ -50,27 +51,41 @@ public class CustomerRoleController : AdminControllerBase
_adminAreaSettings = adminAreaSettings;
}

// Ajax.
public ActionResult AllCustomerRoles(string label, string selectedIds)
// AJAX.
public ActionResult AllCustomerRoles(string label, string selectedIds, bool? includeSystemRoles)
{
var customerRoles = _customerService.GetAllCustomerRoles(true);
var rolesQuery = _customerService.GetAllCustomerRoles(true).SourceQuery;

if (!(includeSystemRoles ?? true))
{
rolesQuery = rolesQuery.Where(x => !x.IsSystemRole);
}

var rolesPager = new FastPager<CustomerRole>(rolesQuery, 500);
var customerRoles = new List<CustomerRole>();
var ids = selectedIds.ToIntArray();

if (label.HasValue())
while (rolesPager.ReadNextPage(out var roles))
{
customerRoles.Insert(0, new CustomerRole { Name = label, Id = 0 });
customerRoles.AddRange(roles);
}

var list =
from c in customerRoles
select new
var list = customerRoles
.OrderBy(x => x.Name)
.Select(x => new
{
id = c.Id.ToString(),
text = c.Name,
selected = ids.Contains(c.Id)
};
id = x.Id.ToString(),
text = x.Name,
selected = ids.Contains(x.Id)
})
.ToList();

if (label.HasValue())
{
list.Insert(0, new { id = "0", text = label, selected = false });
}

return new JsonResult { Data = list.ToList(), JsonRequestBehavior = JsonRequestBehavior.AllowGet };
return new JsonResult { Data = list, JsonRequestBehavior = JsonRequestBehavior.AllowGet };
}

#region List / Create / Edit / Delete
@@ -83,25 +98,20 @@ public ActionResult Index()
[Permission(Permissions.Customer.Role.Read)]
public ActionResult List()
{
var customerRoles = _customerService.GetAllCustomerRoles(true);
var gridModel = new GridModel<CustomerRoleModel>
{
Data = customerRoles.Select(x => x.ToModel()),
Total = customerRoles.Count()
};
return View(gridModel);
ViewData["GridPageSize"] = _adminAreaSettings.GridPageSize;

return View();
}

[HttpPost, GridAction(EnableCustomBinding = true)]
[Permission(Permissions.Customer.Role.Read)]
public ActionResult List(GridCommand command)
{
var model = new GridModel<CustomerRoleModel>();

var customerRoles = _customerService.GetAllCustomerRoles(true);
var customerRoles = _customerService.GetAllCustomerRoles(true, command.Page - 1, command.PageSize);

model.Data = customerRoles.Select(x => x.ToModel());
model.Total = customerRoles.Count();
model.Total = customerRoles.TotalCount;

return new JsonResult
{
@@ -596,14 +596,8 @@ private void PrepareProfileModelForEdit(ExportProfileModel model, ExportProfile
}
else if (model.Provider.EntityType == ExportEntityType.Customer)
{
var allCustomerRoles = _customerService.GetAllCustomerRoles(true);
var allCountries = _countryService.GetAllCountries(true);

model.Filter.AvailableCustomerRoles = allCustomerRoles
.OrderBy(x => x.Name)
.Select(x => new SelectListItem { Text = x.Name, Value = x.Id.ToString() })
.ToList();

model.Filter.AvailableCountries = allCountries
.Select(x => new SelectListItem { Text = x.GetLocalized(y => y.Name, language, true, false), Value = x.Id.ToString() })
.ToList();
@@ -618,13 +612,6 @@ private void PrepareProfileModelForEdit(ExportProfileModel model, ExportProfile
}
else if (model.Provider.EntityType == ExportEntityType.ShoppingCartItem)
{
var allCustomerRoles = _customerService.GetAllCustomerRoles(true);

model.Filter.AvailableCustomerRoles = allCustomerRoles
.OrderBy(x => x.Name)
.Select(x => new SelectListItem { Text = x.Name, Value = x.Id.ToString() })
.ToList();

model.Filter.AvailableShoppingCartTypes = ShoppingCartType.ShoppingCart.ToSelectList(false).ToList();
}

@@ -711,7 +711,7 @@ public JsonResult FileUpload(int id)
NotifyError(error);
}

return Json(new { success, tempFile, error });
return Json(new { success, tempFile, error, name = postedFile.FileName, ext = postedFile.FileExtension });
}

[Permission(Permissions.Configuration.Import.Read)]
@@ -256,6 +256,7 @@ public ActionResult Create()
}

[HttpPost, ParameterBasedOnFormName("save-continue", "continueEditing")]
[ValidateInput(false)]
[Permission(Permissions.Catalog.Manufacturer.Create)]
public ActionResult Create(ManufacturerModel model, bool continueEditing, FormCollection form)
{
@@ -140,6 +140,7 @@ public ActionResult Create()
//default values
model.Published = true;
model.AllowComments = true;
model.CreatedOn = DateTime.UtcNow;
return View(model);
}

@@ -153,7 +154,7 @@ public ActionResult Create(NewsItemModel model, bool continueEditing, FormCollec

newsItem.StartDateUtc = model.StartDate;
newsItem.EndDateUtc = model.EndDate;
newsItem.CreatedOnUtc = DateTime.UtcNow;
newsItem.CreatedOnUtc = model.CreatedOn;
_newsService.InsertNews(newsItem);

// Search engine name.
@@ -188,6 +189,7 @@ public ActionResult Edit(int id)
var model = newsItem.ToModel();
model.StartDate = newsItem.StartDateUtc;
model.EndDate = newsItem.EndDateUtc;
model.CreatedOn = newsItem.CreatedOnUtc;

//stores
PrepareStoresMappingModel(model, newsItem, false);
@@ -209,6 +211,7 @@ public ActionResult Edit(NewsItemModel model, bool continueEditing, FormCollecti

newsItem.StartDateUtc = model.StartDate;
newsItem.EndDateUtc = model.EndDate;
newsItem.CreatedOnUtc = model.CreatedOn;
_newsService.UpdateNews(newsItem);

// Search engine name.