Merge remote-tracking branch 'origin/v8/8.8' into v8/8.9

# Conflicts:
#	src/SolutionInfo.cs
#	src/Umbraco.Web.UI/Umbraco/Views/Default.cshtml
This commit is contained in:
Shannon
2020-09-24 14:19:58 +10:00
45 changed files with 948 additions and 356 deletions

View File

@@ -128,15 +128,16 @@ namespace Umbraco.Web.Cache
public static void RefreshMemberCache(this DistributedCache dc, params IMember[] members)
{
dc.Refresh(MemberCacheRefresher.UniqueId, x => x.Id, members);
if (members.Length == 0) return;
dc.RefreshByPayload(MemberCacheRefresher.UniqueId, members.Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username)));
}
public static void RemoveMemberCache(this DistributedCache dc, params IMember[] members)
{
dc.Remove(MemberCacheRefresher.UniqueId, x => x.Id, members);
if (members.Length == 0) return;
dc.RefreshByPayload(MemberCacheRefresher.UniqueId, members.Select(x => new MemberCacheRefresher.JsonPayload(x.Id, x.Username)));
}
#endregion
#region MemberGroupCache

View File

@@ -1,4 +1,6 @@
using System;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using Umbraco.Core.Cache;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence.Repositories;
@@ -7,14 +9,30 @@ using Umbraco.Core.Services;
namespace Umbraco.Web.Cache
{
public sealed class MemberCacheRefresher : TypedCacheRefresherBase<MemberCacheRefresher, IMember>
public sealed class MemberCacheRefresher : PayloadCacheRefresherBase<MemberCacheRefresher, MemberCacheRefresher.JsonPayload>
{
private readonly IdkMap _idkMap;
private readonly LegacyMemberCacheRefresher _legacyMemberRefresher;
public MemberCacheRefresher(AppCaches appCaches, IdkMap idkMap)
: base(appCaches)
{
_idkMap = idkMap;
_legacyMemberRefresher = new LegacyMemberCacheRefresher(this, appCaches);
}
public class JsonPayload
{
[JsonConstructor]
public JsonPayload(int id, string username)
{
Id = id;
Username = username;
}
public int Id { get; }
public string Username { get; }
}
#region Define
@@ -31,38 +49,45 @@ namespace Umbraco.Web.Cache
#region Refresher
public override void Refresh(JsonPayload[] payloads)
{
ClearCache(payloads);
base.Refresh(payloads);
}
public override void Refresh(int id)
{
ClearCache(id);
ClearCache(new JsonPayload(id, null));
base.Refresh(id);
}
public override void Remove(int id)
{
ClearCache(id);
ClearCache(new JsonPayload(id, null));
base.Remove(id);
}
public override void Refresh(IMember instance)
{
ClearCache(instance.Id);
base.Refresh(instance);
}
[Obsolete("This is no longer used and will be removed from the codebase in the future")]
public void Refresh(IMember instance) => _legacyMemberRefresher.Refresh(instance);
public override void Remove(IMember instance)
{
ClearCache(instance.Id);
base.Remove(instance);
}
[Obsolete("This is no longer used and will be removed from the codebase in the future")]
public void Remove(IMember instance) => _legacyMemberRefresher.Remove(instance);
private void ClearCache(int id)
private void ClearCache(params JsonPayload[] payloads)
{
_idkMap.ClearCache(id);
AppCaches.ClearPartialViewCache();
var memberCache = AppCaches.IsolatedCaches.Get<IMember>();
if (memberCache)
memberCache.Result.Clear(RepositoryCacheKeys.GetKey<IMember>(id));
foreach (var p in payloads)
{
_idkMap.ClearCache(p.Id);
if (memberCache)
{
memberCache.Result.Clear(RepositoryCacheKeys.GetKey<IMember>(p.Id));
memberCache.Result.Clear(RepositoryCacheKeys.GetKey<IMember>(p.Username));
}
}
}
#endregion
@@ -75,5 +100,38 @@ namespace Umbraco.Web.Cache
}
#endregion
#region Backwards Compat
// TODO: this is here purely for backwards compat but should be removed in netcore
private class LegacyMemberCacheRefresher : TypedCacheRefresherBase<MemberCacheRefresher, IMember>
{
private readonly MemberCacheRefresher _parent;
public LegacyMemberCacheRefresher(MemberCacheRefresher parent, AppCaches appCaches) : base(appCaches)
{
_parent = parent;
}
public override Guid RefresherUniqueId => _parent.RefresherUniqueId;
public override string Name => _parent.Name;
protected override MemberCacheRefresher This => _parent;
public override void Refresh(IMember instance)
{
_parent.ClearCache(new JsonPayload(instance.Id, instance.Username));
base.Refresh(instance.Id);
}
public override void Remove(IMember instance)
{
_parent.ClearCache(new JsonPayload(instance.Id, instance.Username));
base.Remove(instance);
}
}
#endregion
}
}

View File

@@ -35,7 +35,19 @@ namespace Umbraco.Web.Compose
}
public void Terminate()
{ }
{
//BackOfficeUserManager.AccountLocked -= ;
//BackOfficeUserManager.AccountUnlocked -= ;
BackOfficeUserManager.ForgotPasswordRequested -= OnForgotPasswordRequest;
BackOfficeUserManager.ForgotPasswordChangedSuccess -= OnForgotPasswordChange;
BackOfficeUserManager.LoginFailed -= OnLoginFailed;
//BackOfficeUserManager.LoginRequiresVerification -= ;
BackOfficeUserManager.LoginSuccess -= OnLoginSuccess;
BackOfficeUserManager.LogoutSuccess -= OnLogoutSuccess;
BackOfficeUserManager.PasswordChanged -= OnPasswordChanged;
BackOfficeUserManager.PasswordReset -= OnPasswordReset;
//BackOfficeUserManager.ResetAccessFailedCount -= ;
}
private IUser GetPerformingUser(int userId)
{

View File

@@ -32,42 +32,78 @@ namespace Umbraco.Web.Compose
public void Initialize()
{
//Send notifications for the send to publish action
ContentService.SentToPublish += (sender, args) => _notifier.Notify(_actions.GetAction<ActionToPublish>(), args.Entity);
ContentService.SentToPublish += ContentService_SentToPublish;
//Send notifications for the published action
ContentService.Published += (sender, args) => _notifier.Notify(_actions.GetAction<ActionPublish>(), args.PublishedEntities.ToArray());
ContentService.Published += ContentService_Published;
//Send notifications for the saved action
ContentService.Sorted += (sender, args) => ContentServiceSorted(_notifier, sender, args, _actions);
ContentService.Sorted += ContentService_Sorted;
//Send notifications for the update and created actions
ContentService.Saved += (sender, args) => ContentServiceSaved(_notifier, sender, args, _actions);
ContentService.Saved += ContentService_Saved;
//Send notifications for the unpublish action
ContentService.Unpublished += (sender, args) => _notifier.Notify(_actions.GetAction<ActionUnpublish>(), args.PublishedEntities.ToArray());
ContentService.Unpublished += ContentService_Unpublished;
//Send notifications for the move/move to recycle bin and restore actions
ContentService.Moved += (sender, args) => ContentServiceMoved(_notifier, sender, args, _actions);
ContentService.Moved += ContentService_Moved;
//Send notifications for the delete action when content is moved to the recycle bin
ContentService.Trashed += (sender, args) => _notifier.Notify(_actions.GetAction<ActionDelete>(), args.MoveInfoCollection.Select(m => m.Entity).ToArray());
ContentService.Trashed += ContentService_Trashed;
//Send notifications for the copy action
ContentService.Copied += (sender, args) => _notifier.Notify(_actions.GetAction<ActionCopy>(), args.Original);
ContentService.Copied += ContentService_Copied;
//Send notifications for the rollback action
ContentService.RolledBack += (sender, args) => _notifier.Notify(_actions.GetAction<ActionRollback>(), args.Entity);
ContentService.RolledBack += ContentService_RolledBack;
//Send notifications for the public access changed action
PublicAccessService.Saved += (sender, args) => PublicAccessServiceSaved(_notifier, sender, args, _contentService, _actions);
UserService.UserGroupPermissionsAssigned += (sender, args) => UserServiceUserGroupPermissionsAssigned(_notifier, sender, args, _contentService, _actions);
PublicAccessService.Saved += PublicAccessService_Saved;
UserService.UserGroupPermissionsAssigned += UserService_UserGroupPermissionsAssigned;
}
public void Terminate()
{ }
{
ContentService.SentToPublish -= ContentService_SentToPublish;
ContentService.Published -= ContentService_Published;
ContentService.Sorted -= ContentService_Sorted;
ContentService.Saved -= ContentService_Saved;
ContentService.Unpublished -= ContentService_Unpublished;
ContentService.Moved -= ContentService_Moved;
ContentService.Trashed -= ContentService_Trashed;
ContentService.Copied -= ContentService_Copied;
ContentService.RolledBack -= ContentService_RolledBack;
PublicAccessService.Saved -= PublicAccessService_Saved;
UserService.UserGroupPermissionsAssigned -= UserService_UserGroupPermissionsAssigned;
}
private void ContentServiceSorted(Notifier notifier, IContentService sender, Core.Events.SaveEventArgs<IContent> args, ActionCollection actions)
private void UserService_UserGroupPermissionsAssigned(IUserService sender, Core.Events.SaveEventArgs<EntityPermission> args)
=> UserServiceUserGroupPermissionsAssigned(args, _contentService);
private void PublicAccessService_Saved(IPublicAccessService sender, Core.Events.SaveEventArgs<PublicAccessEntry> args)
=> PublicAccessServiceSaved(args, _contentService);
private void ContentService_RolledBack(IContentService sender, Core.Events.RollbackEventArgs<IContent> args)
=> _notifier.Notify(_actions.GetAction<ActionRollback>(), args.Entity);
private void ContentService_Copied(IContentService sender, Core.Events.CopyEventArgs<IContent> args)
=> _notifier.Notify(_actions.GetAction<ActionCopy>(), args.Original);
private void ContentService_Trashed(IContentService sender, Core.Events.MoveEventArgs<IContent> args)
=> _notifier.Notify(_actions.GetAction<ActionDelete>(), args.MoveInfoCollection.Select(m => m.Entity).ToArray());
private void ContentService_Moved(IContentService sender, Core.Events.MoveEventArgs<IContent> args)
=> ContentServiceMoved(args);
private void ContentService_Unpublished(IContentService sender, Core.Events.PublishEventArgs<IContent> args)
=> _notifier.Notify(_actions.GetAction<ActionUnpublish>(), args.PublishedEntities.ToArray());
private void ContentService_Saved(IContentService sender, Core.Events.ContentSavedEventArgs args)
=> ContentServiceSaved(args);
private void ContentService_Sorted(IContentService sender, Core.Events.SaveEventArgs<IContent> args)
=> ContentServiceSorted(sender, args);
private void ContentService_Published(IContentService sender, Core.Events.ContentPublishedEventArgs args)
=> _notifier.Notify(_actions.GetAction<ActionPublish>(), args.PublishedEntities.ToArray());
private void ContentService_SentToPublish(IContentService sender, Core.Events.SendToPublishEventArgs<IContent> args)
=> _notifier.Notify(_actions.GetAction<ActionToPublish>(), args.Entity);
private void ContentServiceSorted(IContentService sender, Core.Events.SaveEventArgs<IContent> args)
{
var parentId = args.SavedEntities.Select(x => x.ParentId).Distinct().ToList();
if (parentId.Count != 1) return; // this shouldn't happen, for sorting all entities will have the same parent id
@@ -79,10 +115,10 @@ namespace Umbraco.Web.Compose
var parent = sender.GetById(parentId[0]);
if (parent == null) return; // this shouldn't happen
notifier.Notify(actions.GetAction<ActionSort>(), new[] { parent });
_notifier.Notify(_actions.GetAction<ActionSort>(), new[] { parent });
}
private void ContentServiceSaved(Notifier notifier, IContentService sender, Core.Events.SaveEventArgs<IContent> args, ActionCollection actions)
private void ContentServiceSaved(Core.Events.SaveEventArgs<IContent> args)
{
var newEntities = new List<IContent>();
var updatedEntities = new List<IContent>();
@@ -102,21 +138,21 @@ namespace Umbraco.Web.Compose
updatedEntities.Add(entity);
}
}
notifier.Notify(actions.GetAction<ActionNew>(), newEntities.ToArray());
notifier.Notify(actions.GetAction<ActionUpdate>(), updatedEntities.ToArray());
_notifier.Notify(_actions.GetAction<ActionNew>(), newEntities.ToArray());
_notifier.Notify(_actions.GetAction<ActionUpdate>(), updatedEntities.ToArray());
}
private void UserServiceUserGroupPermissionsAssigned(Notifier notifier, IUserService sender, Core.Events.SaveEventArgs<EntityPermission> args, IContentService contentService, ActionCollection actions)
private void UserServiceUserGroupPermissionsAssigned(Core.Events.SaveEventArgs<EntityPermission> args, IContentService contentService)
{
var entities = contentService.GetByIds(args.SavedEntities.Select(e => e.EntityId)).ToArray();
if(entities.Any() == false)
if (entities.Any() == false)
{
return;
}
notifier.Notify(actions.GetAction<ActionRights>(), entities);
_notifier.Notify(_actions.GetAction<ActionRights>(), entities);
}
private void ContentServiceMoved(Notifier notifier, IContentService sender, Core.Events.MoveEventArgs<IContent> args, ActionCollection actions)
private void ContentServiceMoved(Core.Events.MoveEventArgs<IContent> args)
{
// notify about the move for all moved items
_notifier.Notify(_actions.GetAction<ActionMove>(), args.MoveInfoCollection.Select(m => m.Entity).ToArray());
@@ -126,22 +162,22 @@ namespace Umbraco.Web.Compose
.Where(m => m.OriginalPath.Contains(Constants.System.RecycleBinContentString))
.Select(m => m.Entity)
.ToArray();
if(restoredEntities.Any())
if (restoredEntities.Any())
{
_notifier.Notify(_actions.GetAction<ActionRestore>(), restoredEntities);
}
}
private void PublicAccessServiceSaved(Notifier notifier, IPublicAccessService sender, Core.Events.SaveEventArgs<PublicAccessEntry> args, IContentService contentService, ActionCollection actions)
private void PublicAccessServiceSaved(Core.Events.SaveEventArgs<PublicAccessEntry> args, IContentService contentService)
{
var entities = contentService.GetByIds(args.SavedEntities.Select(e => e.ProtectedNodeId)).ToArray();
if(entities.Any() == false)
if (entities.Any() == false)
{
return;
}
notifier.Notify(actions.GetAction<ActionProtect>(), entities);
_notifier.Notify(_actions.GetAction<ActionProtect>(), entities);
}
/// <summary>
/// This class is used to send the notifications
/// </summary>

View File

@@ -14,7 +14,9 @@ namespace Umbraco.Web.Compose
}
public void Terminate()
{ }
{
MemberGroupService.Saved -= MemberGroupService_Saved;
}
static void MemberGroupService_Saved(IMemberGroupService sender, Core.Events.SaveEventArgs<Core.Models.IMemberGroup> e)
{

View File

@@ -146,6 +146,9 @@ namespace Umbraco.Web.Composing
public static ISectionService SectionService
=> Factory.GetInstance<ISectionService>();
public static IIconService IconService
=> Factory.GetInstance<IIconService>();
#endregion
#region Web Constants

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -14,7 +13,6 @@ using Newtonsoft.Json;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Manifest;
using Umbraco.Core.Models.Identity;
@@ -25,6 +23,7 @@ using Umbraco.Web.Composing;
using Umbraco.Web.Features;
using Umbraco.Web.JavaScript;
using Umbraco.Web.Security;
using Umbraco.Web.Services;
using Constants = Umbraco.Core.Constants;
using JArray = Newtonsoft.Json.Linq.JArray;
@@ -41,15 +40,52 @@ namespace Umbraco.Web.Editors
private readonly ManifestParser _manifestParser;
private readonly UmbracoFeatures _features;
private readonly IRuntimeState _runtimeState;
private readonly IIconService _iconService;
private BackOfficeUserManager<BackOfficeIdentityUser> _userManager;
private BackOfficeSignInManager _signInManager;
public BackOfficeController(ManifestParser manifestParser, UmbracoFeatures features, IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ServiceContext services, AppCaches appCaches, IProfilingLogger profilingLogger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper)
[Obsolete("Use the constructor that injects IIconService.")]
public BackOfficeController(
ManifestParser manifestParser,
UmbracoFeatures features,
IGlobalSettings globalSettings,
IUmbracoContextAccessor umbracoContextAccessor,
ServiceContext services,
AppCaches appCaches,
IProfilingLogger profilingLogger,
IRuntimeState runtimeState,
UmbracoHelper umbracoHelper)
: this(manifestParser,
features,
globalSettings,
umbracoContextAccessor,
services,
appCaches,
profilingLogger,
runtimeState,
umbracoHelper,
Current.IconService)
{
}
public BackOfficeController(
ManifestParser manifestParser,
UmbracoFeatures features,
IGlobalSettings globalSettings,
IUmbracoContextAccessor umbracoContextAccessor,
ServiceContext services,
AppCaches appCaches,
IProfilingLogger profilingLogger,
IRuntimeState runtimeState,
UmbracoHelper umbracoHelper,
IIconService iconService)
: base(globalSettings, umbracoContextAccessor, services, appCaches, profilingLogger, umbracoHelper)
{
_manifestParser = manifestParser;
_features = features;
_runtimeState = runtimeState;
_iconService = iconService;
}
protected BackOfficeSignInManager SignInManager => _signInManager ?? (_signInManager = OwinContext.GetBackOfficeSignInManager());
@@ -64,9 +100,10 @@ namespace Umbraco.Web.Editors
/// <returns></returns>
public async Task<ActionResult> Default()
{
var backofficeModel = new BackOfficeModel(_features, GlobalSettings, _iconService);
return await RenderDefaultOrProcessExternalLoginAsync(
() => View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml", new BackOfficeModel(_features, GlobalSettings)),
() => View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml", new BackOfficeModel(_features, GlobalSettings)));
() => View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml", backofficeModel),
() => View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/Default.cshtml", backofficeModel));
}
[HttpGet]
@@ -149,7 +186,7 @@ namespace Umbraco.Web.Editors
{
return await RenderDefaultOrProcessExternalLoginAsync(
//The default view to render when there is no external login info or errors
() => View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/AuthorizeUpgrade.cshtml", new BackOfficeModel(_features, GlobalSettings)),
() => View(GlobalSettings.Path.EnsureEndsWith('/') + "Views/AuthorizeUpgrade.cshtml", new BackOfficeModel(_features, GlobalSettings, _iconService)),
//The ActionResult to perform if external login is successful
() => Redirect("/"));
}

View File

@@ -1,4 +1,7 @@
using Umbraco.Core.Configuration;
using System;
using Umbraco.Core.Configuration;
using Umbraco.Core.Services;
using Umbraco.Web.Composing;
using Umbraco.Web.Features;
namespace Umbraco.Web.Editors
@@ -6,13 +9,24 @@ namespace Umbraco.Web.Editors
public class BackOfficeModel
{
public BackOfficeModel(UmbracoFeatures features, IGlobalSettings globalSettings)
[Obsolete("Use the overload that injects IIconService.")]
public BackOfficeModel(UmbracoFeatures features, IGlobalSettings globalSettings) : this(features, globalSettings, Current.IconService)
{
}
public BackOfficeModel(UmbracoFeatures features, IGlobalSettings globalSettings, IIconService iconService)
{
Features = features;
GlobalSettings = globalSettings;
IconCheckData = iconService.GetIcon("icon-check")?.SvgString;
IconDeleteData = iconService.GetIcon("icon-delete")?.SvgString;
}
public UmbracoFeatures Features { get; }
public IGlobalSettings GlobalSettings { get; }
public string IconCheckData { get; }
public string IconDeleteData { get; }
}
}

View File

@@ -1,6 +1,9 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using Umbraco.Core.Configuration;
using Umbraco.Core.Models;
using Umbraco.Core.Services;
using Umbraco.Web.Composing;
using Umbraco.Web.Features;
namespace Umbraco.Web.Editors
@@ -10,7 +13,21 @@ namespace Umbraco.Web.Editors
private readonly UmbracoFeatures _features;
public IEnumerable<ILanguage> Languages { get; }
public BackOfficePreviewModel(UmbracoFeatures features, IGlobalSettings globalSettings, IEnumerable<ILanguage> languages) : base(features, globalSettings)
[Obsolete("Use the overload that injects IIconService.")]
public BackOfficePreviewModel(
UmbracoFeatures features,
IGlobalSettings globalSettings,
IEnumerable<ILanguage> languages)
: this(features, globalSettings, languages, Current.IconService)
{
}
public BackOfficePreviewModel(
UmbracoFeatures features,
IGlobalSettings globalSettings,
IEnumerable<ILanguage> languages,
IIconService iconService)
: base(features, globalSettings, iconService)
{
_features = features;
Languages = languages;

View File

@@ -1,21 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Collections.Generic;
using Umbraco.Core.Models;
using Umbraco.Core.Services;
using Umbraco.Web.Mvc;
using Umbraco.Web.WebApi;
using Umbraco.Core.Logging;
using Umbraco.Web.Models;
using System.IO;
using Umbraco.Core;
using Umbraco.Core.IO;
using Ganss.XSS;
using Umbraco.Core.Cache;
namespace Umbraco.Web.Editors
{
[PluginController("UmbracoApi")]
public class IconController : UmbracoAuthorizedApiController
{
private readonly IIconService _iconService;
public IconController(IIconService iconService)
{
_iconService = iconService;
}
/// <summary>
/// Gets an IconModel containing the icon name and SvgString according to an icon name found at the global icons path
@@ -24,76 +23,16 @@ namespace Umbraco.Web.Editors
/// <returns></returns>
public IconModel GetIcon(string iconName)
{
return string.IsNullOrWhiteSpace(iconName)
? null
: CreateIconModel(iconName.StripFileExtension(), IOHelper.MapPath($"{GlobalSettings.IconsPath}/{iconName}.svg"));
}
/// <summary>
/// Gets an IconModel using values from a FileInfo model
/// </summary>
/// <param name="fileInfo"></param>
/// <returns></returns>
public IconModel GetIcon(FileInfo fileInfo)
{
return fileInfo == null || string.IsNullOrWhiteSpace(fileInfo.Name)
? null
: CreateIconModel(fileInfo.Name.StripFileExtension(), fileInfo.FullName);
return _iconService.GetIcon(iconName);
}
/// <summary>
/// Gets a list of all svg icons found at at the global icons path.
/// </summary>
/// <returns></returns>
public List<IconModel> GetAllIcons()
public IList<IconModel> GetAllIcons()
{
var icons = new List<IconModel>();
var directory = new DirectoryInfo(IOHelper.MapPath($"{GlobalSettings.IconsPath}/"));
var iconNames = directory.GetFiles("*.svg");
iconNames.OrderBy(f => f.Name).ToList().ForEach(iconInfo =>
{
var icon = GetIcon(iconInfo);
if (icon != null)
{
icons.Add(icon);
}
});
return icons;
}
/// <summary>
/// Gets an IconModel containing the icon name and SvgString
/// </summary>
/// <param name="iconName"></param>
/// <param name="iconPath"></param>
/// <returns></returns>
private IconModel CreateIconModel(string iconName, string iconPath)
{
var sanitizer = new HtmlSanitizer();
sanitizer.AllowedAttributes.UnionWith(Core.Constants.SvgSanitizer.Attributes);
sanitizer.AllowedCssProperties.UnionWith(Core.Constants.SvgSanitizer.Attributes);
sanitizer.AllowedTags.UnionWith(Core.Constants.SvgSanitizer.Tags);
try
{
var svgContent = File.ReadAllText(iconPath);
var sanitizedString = sanitizer.Sanitize(svgContent);
var svg = new IconModel
{
Name = iconName,
SvgString = sanitizedString
};
return svg;
}
catch
{
return null;
}
return _iconService.GetAllIcons();
}
}
}

View File

@@ -12,6 +12,7 @@ using Umbraco.Web.JavaScript;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Mvc;
using Umbraco.Web.PublishedCache;
using Umbraco.Web.Services;
using Constants = Umbraco.Core.Constants;
namespace Umbraco.Web.Editors
@@ -24,19 +25,39 @@ namespace Umbraco.Web.Editors
private readonly IPublishedSnapshotService _publishedSnapshotService;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly ILocalizationService _localizationService;
private readonly IIconService _iconService;
[Obsolete("Use the constructor that injects IIconService.")]
public PreviewController(
UmbracoFeatures features,
IGlobalSettings globalSettings,
IPublishedSnapshotService publishedSnapshotService,
IUmbracoContextAccessor umbracoContextAccessor,
ILocalizationService localizationService)
:this(features,
globalSettings,
publishedSnapshotService,
umbracoContextAccessor,
localizationService,
Current.IconService)
{
}
public PreviewController(
UmbracoFeatures features,
IGlobalSettings globalSettings,
IPublishedSnapshotService publishedSnapshotService,
IUmbracoContextAccessor umbracoContextAccessor,
ILocalizationService localizationService,
IIconService iconService)
{
_features = features;
_globalSettings = globalSettings;
_publishedSnapshotService = publishedSnapshotService;
_umbracoContextAccessor = umbracoContextAccessor;
_localizationService = localizationService;
_iconService = iconService;
}
[UmbracoAuthorize(redirectToUmbracoLogin: true)]
@@ -45,7 +66,7 @@ namespace Umbraco.Web.Editors
{
var availableLanguages = _localizationService.GetAllLanguages();
var model = new BackOfficePreviewModel(_features, _globalSettings, availableLanguages);
var model = new BackOfficePreviewModel(_features, _globalSettings, availableLanguages, _iconService);
if (model.PreviewExtendedHeaderView.IsNullOrWhiteSpace() == false)
{

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Web;
using Umbraco.Core.Composing;
using Umbraco.Core.Logging;
@@ -9,6 +10,7 @@ namespace Umbraco.Web.Logging
{
private readonly WebProfiler _profiler;
private readonly bool _profile;
private readonly List<Action> _terminate = new List<Action>();
public WebProfilerComponent(IProfiler profiler, ILogger logger)
{
@@ -35,15 +37,23 @@ namespace Umbraco.Web.Logging
}
public void Terminate()
{ }
{
UmbracoApplicationBase.ApplicationInit -= InitializeApplication;
foreach (var t in _terminate) t();
}
private void InitializeApplication(object sender, EventArgs args)
{
if (!(sender is HttpApplication app)) return;
// for *each* application (this will run more than once)
app.BeginRequest += (s, a) => _profiler.UmbracoApplicationBeginRequest(s, a);
app.EndRequest += (s, a) => _profiler.UmbracoApplicationEndRequest(s, a);
void beginRequest(object s, EventArgs a) => _profiler.UmbracoApplicationBeginRequest(s, a);
app.BeginRequest += beginRequest;
_terminate.Add(() => app.BeginRequest -= beginRequest);
void endRequest(object s, EventArgs a) => _profiler.UmbracoApplicationEndRequest(s, a);
app.EndRequest += endRequest;
_terminate.Add(() => app.EndRequest -= endRequest);
}
}
}

View File

@@ -1,8 +0,0 @@
namespace Umbraco.Web.Models
{
public class IconModel
{
public string Name { get; set; }
public string SvgString { get; set; }
}
}

View File

@@ -1,7 +1,11 @@
using System.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core.Composing;
using Umbraco.Core.Events;
using Umbraco.Core.Models;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Implement;
namespace Umbraco.Web.PropertyEditors
@@ -9,6 +13,7 @@ namespace Umbraco.Web.PropertyEditors
internal sealed class PropertyEditorsComponent : IComponent
{
private readonly PropertyEditorCollection _propertyEditors;
private readonly List<Action> _terminate = new List<Action>();
public PropertyEditorsComponent(PropertyEditorCollection propertyEditors)
{
@@ -27,32 +32,48 @@ namespace Umbraco.Web.PropertyEditors
}
public void Terminate()
{ }
private static void Initialize(FileUploadPropertyEditor fileUpload)
{
MediaService.Saving += fileUpload.MediaServiceSaving;
ContentService.Copied += fileUpload.ContentServiceCopied;
MediaService.Deleted += (sender, args)
=> args.MediaFilesToDelete.AddRange(fileUpload.ServiceDeleted(args.DeletedEntities.Cast<ContentBase>()));
ContentService.Deleted += (sender, args)
=> args.MediaFilesToDelete.AddRange(fileUpload.ServiceDeleted(args.DeletedEntities.Cast<ContentBase>()));
MemberService.Deleted += (sender, args)
=> args.MediaFilesToDelete.AddRange(fileUpload.ServiceDeleted(args.DeletedEntities.Cast<ContentBase>()));
foreach (var t in _terminate) t();
}
private static void Initialize(ImageCropperPropertyEditor imageCropper)
private void Initialize(FileUploadPropertyEditor fileUpload)
{
MediaService.Saving += fileUpload.MediaServiceSaving;
_terminate.Add(() => MediaService.Saving -= fileUpload.MediaServiceSaving);
ContentService.Copied += fileUpload.ContentServiceCopied;
_terminate.Add(() => ContentService.Copied -= fileUpload.ContentServiceCopied);
void mediaServiceDeleted(IMediaService sender, DeleteEventArgs<IMedia> args) => args.MediaFilesToDelete.AddRange(fileUpload.ServiceDeleted(args.DeletedEntities.Cast<ContentBase>()));
MediaService.Deleted += mediaServiceDeleted;
_terminate.Add(() => MediaService.Deleted -= mediaServiceDeleted);
void contentServiceDeleted(IContentService sender, DeleteEventArgs<IContent> args) => args.MediaFilesToDelete.AddRange(fileUpload.ServiceDeleted(args.DeletedEntities.Cast<ContentBase>()));
ContentService.Deleted += contentServiceDeleted;
_terminate.Add(() => ContentService.Deleted -= contentServiceDeleted);
void memberServiceDeleted(IMemberService sender, DeleteEventArgs<IMember> args) => args.MediaFilesToDelete.AddRange(fileUpload.ServiceDeleted(args.DeletedEntities.Cast<ContentBase>()));
MemberService.Deleted += memberServiceDeleted;
_terminate.Add(() => MemberService.Deleted -= memberServiceDeleted);
}
private void Initialize(ImageCropperPropertyEditor imageCropper)
{
MediaService.Saving += imageCropper.MediaServiceSaving;
_terminate.Add(() => MediaService.Saving -= imageCropper.MediaServiceSaving);
ContentService.Copied += imageCropper.ContentServiceCopied;
_terminate.Add(() => ContentService.Copied -= imageCropper.ContentServiceCopied);
MediaService.Deleted += (sender, args)
=> args.MediaFilesToDelete.AddRange(imageCropper.ServiceDeleted(args.DeletedEntities.Cast<ContentBase>()));
ContentService.Deleted += (sender, args)
=> args.MediaFilesToDelete.AddRange(imageCropper.ServiceDeleted(args.DeletedEntities.Cast<ContentBase>()));
MemberService.Deleted += (sender, args)
=> args.MediaFilesToDelete.AddRange(imageCropper.ServiceDeleted(args.DeletedEntities.Cast<ContentBase>()));
void mediaServiceDeleted(IMediaService sender, DeleteEventArgs<IMedia> args) => args.MediaFilesToDelete.AddRange(imageCropper.ServiceDeleted(args.DeletedEntities.Cast<ContentBase>()));
MediaService.Deleted += mediaServiceDeleted;
_terminate.Add(() => MediaService.Deleted -= mediaServiceDeleted);
void contentServiceDeleted(IContentService sender, DeleteEventArgs<IContent> args) => args.MediaFilesToDelete.AddRange(imageCropper.ServiceDeleted(args.DeletedEntities.Cast<ContentBase>()));
ContentService.Deleted += contentServiceDeleted;
_terminate.Add(() => ContentService.Deleted -= contentServiceDeleted);
void memberServiceDeleted(IMemberService sender, DeleteEventArgs<IMember> args) => args.MediaFilesToDelete.AddRange(imageCropper.ServiceDeleted(args.DeletedEntities.Cast<ContentBase>()));
MemberService.Deleted += memberServiceDeleted;
_terminate.Add(() => MemberService.Deleted -= memberServiceDeleted);
}
}
}

View File

@@ -52,7 +52,12 @@ namespace Umbraco.Web.Routing
}
public void Terminate()
{ }
{
ContentService.Publishing -= ContentService_Publishing;
ContentService.Published -= ContentService_Published;
ContentService.Moving -= ContentService_Moving;
ContentService.Moved -= ContentService_Moved;
}
private void ContentService_Publishing(IContentService sender, PublishEventArgs<IContent> args)
{

View File

@@ -137,8 +137,8 @@ namespace Umbraco.Web.Runtime
composition.RegisterUnique<IEventMessagesAccessor, HybridEventMessagesAccessor>();
composition.RegisterUnique<ITreeService, TreeService>();
composition.RegisterUnique<ISectionService, SectionService>();
composition.RegisterUnique<IDashboardService, DashboardService>();
composition.RegisterUnique<IIconService, IconService>();
composition.RegisterUnique<IExamineManager>(factory => ExamineManager.Instance);

View File

@@ -0,0 +1,32 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Umbraco.Web.Scheduling
{
/// <summary>
/// A simple task that executes a delegate synchronously
/// </summary>
internal class SimpleTask : IBackgroundTask
{
private readonly Action _action;
public SimpleTask(Action action)
{
_action = action;
}
public bool IsAsync => false;
public void Run() => _action();
public Task RunAsync(CancellationToken token)
{
throw new NotImplementedException();
}
public void Dispose()
{
}
}
}

View File

@@ -17,9 +17,13 @@ using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Examine.LuceneEngine.Directories;
using Umbraco.Core.Composing;
using System.ComponentModel;
using System.Threading;
using Umbraco.Web.Scheduling;
namespace Umbraco.Web.Search
{
public sealed class ExamineComponent : Umbraco.Core.Composing.IComponent
{
private readonly IExamineManager _examineManager;
@@ -34,7 +38,8 @@ namespace Umbraco.Web.Search
private readonly IMainDom _mainDom;
private readonly IProfilingLogger _logger;
private readonly IUmbracoIndexesCreator _indexCreator;
private readonly BackgroundTaskRunner<IBackgroundTask> _indexItemTaskRunner;
// the default enlist priority is 100
// enlist with a lower priority to ensure that anything "default" runs after us
@@ -62,6 +67,7 @@ namespace Umbraco.Web.Search
_mainDom = mainDom;
_logger = profilingLogger;
_indexCreator = indexCreator;
_indexItemTaskRunner = new BackgroundTaskRunner<IBackgroundTask>(_logger);
}
public void Initialize()
@@ -117,7 +123,13 @@ namespace Umbraco.Web.Search
}
public void Terminate()
{ }
{
ContentCacheRefresher.CacheUpdated -= ContentCacheRefresherUpdated;
ContentTypeCacheRefresher.CacheUpdated -= ContentTypeCacheRefresherUpdated;
MediaCacheRefresher.CacheUpdated -= MediaCacheRefresherUpdated;
MemberCacheRefresher.CacheUpdated -= MemberCacheRefresherUpdated;
LanguageCacheRefresher.CacheUpdated -= LanguageCacheRefresherUpdated;
}
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("This method should not be used and will be removed in future versions, rebuilding indexes can be done with the IndexRebuilder or the BackgroundIndexRebuilder")]
@@ -574,12 +586,18 @@ namespace Umbraco.Web.Search
}
}
/// <summary>
/// An action that will execute at the end of the Scope being completed
/// </summary>
private abstract class DeferedAction
{
public virtual void Execute()
{ }
}
/// <summary>
/// Re-indexes an <see cref="IContent"/> item on a background thread
/// </summary>
private class DeferedReIndexForContent : DeferedAction
{
private readonly ExamineComponent _examineComponent;
@@ -600,21 +618,32 @@ namespace Umbraco.Web.Search
public static void Execute(ExamineComponent examineComponent, IContent content, bool isPublished)
{
foreach (var index in examineComponent._examineManager.Indexes.OfType<IUmbracoIndex>()
//filter the indexers
.Where(x => isPublished || !x.PublishedValuesOnly)
.Where(x => x.EnableDefaultEventHandler))
// perform the ValueSet lookup on a background thread
examineComponent._indexItemTaskRunner.Add(new SimpleTask(() =>
{
//for content we have a different builder for published vs unpublished
var builder = index.PublishedValuesOnly
? examineComponent._publishedContentValueSetBuilder
: (IValueSetBuilder<IContent>)examineComponent._contentValueSetBuilder;
// for content we have a different builder for published vs unpublished
// we don't want to build more value sets than is needed so we'll lazily build 2 one for published one for non-published
var builders = new Dictionary<bool, Lazy<List<ValueSet>>>
{
[true] = new Lazy<List<ValueSet>>(() => examineComponent._publishedContentValueSetBuilder.GetValueSets(content).ToList()),
[false] = new Lazy<List<ValueSet>>(() => examineComponent._contentValueSetBuilder.GetValueSets(content).ToList())
};
index.IndexItems(builder.GetValueSets(content));
}
foreach (var index in examineComponent._examineManager.Indexes.OfType<IUmbracoIndex>()
//filter the indexers
.Where(x => isPublished || !x.PublishedValuesOnly)
.Where(x => x.EnableDefaultEventHandler))
{
var valueSet = builders[index.PublishedValuesOnly].Value;
index.IndexItems(valueSet);
}
}));
}
}
/// <summary>
/// Re-indexes an <see cref="IMedia"/> item on a background thread
/// </summary>
private class DeferedReIndexForMedia : DeferedAction
{
private readonly ExamineComponent _examineComponent;
@@ -635,18 +664,25 @@ namespace Umbraco.Web.Search
public static void Execute(ExamineComponent examineComponent, IMedia media, bool isPublished)
{
var valueSet = examineComponent._mediaValueSetBuilder.GetValueSets(media).ToList();
foreach (var index in examineComponent._examineManager.Indexes.OfType<IUmbracoIndex>()
//filter the indexers
.Where(x => isPublished || !x.PublishedValuesOnly)
.Where(x => x.EnableDefaultEventHandler))
// perform the ValueSet lookup on a background thread
examineComponent._indexItemTaskRunner.Add(new SimpleTask(() =>
{
index.IndexItems(valueSet);
}
var valueSet = examineComponent._mediaValueSetBuilder.GetValueSets(media).ToList();
foreach (var index in examineComponent._examineManager.Indexes.OfType<IUmbracoIndex>()
//filter the indexers
.Where(x => isPublished || !x.PublishedValuesOnly)
.Where(x => x.EnableDefaultEventHandler))
{
index.IndexItems(valueSet);
}
}));
}
}
/// <summary>
/// Re-indexes an <see cref="IMember"/> item on a background thread
/// </summary>
private class DeferedReIndexForMember : DeferedAction
{
private readonly ExamineComponent _examineComponent;
@@ -665,13 +701,17 @@ namespace Umbraco.Web.Search
public static void Execute(ExamineComponent examineComponent, IMember member)
{
var valueSet = examineComponent._memberValueSetBuilder.GetValueSets(member).ToList();
foreach (var index in examineComponent._examineManager.Indexes.OfType<IUmbracoIndex>()
//filter the indexers
.Where(x => x.EnableDefaultEventHandler))
// perform the ValueSet lookup on a background thread
examineComponent._indexItemTaskRunner.Add(new SimpleTask(() =>
{
index.IndexItems(valueSet);
}
var valueSet = examineComponent._memberValueSetBuilder.GetValueSets(member).ToList();
foreach (var index in examineComponent._examineManager.Indexes.OfType<IUmbracoIndex>()
//filter the indexers
.Where(x => x.EnableDefaultEventHandler))
{
index.IndexItems(valueSet);
}
}));
}
}

View File

@@ -309,8 +309,9 @@ namespace Umbraco.Web.Security
{
return false;
}
//Set member online
var member = provider.GetUser(username, true);
// Get the member, do not set to online - this is done implicitly as part of ValidateUser which is consistent with
// how the .NET framework SqlMembershipProvider works. Passing in true will just cause more unnecessary SQL queries/locks.
var member = provider.GetUser(username, false);
if (member == null)
{
//this should not happen
@@ -826,33 +827,17 @@ namespace Umbraco.Web.Security
/// <returns></returns>
private IMember GetCurrentPersistedMember()
{
return _appCaches.RequestCache.GetCacheItem<IMember>(
GetCacheKey("GetCurrentPersistedMember"), () =>
{
var provider = _membershipProvider;
var provider = _membershipProvider;
if (provider.IsUmbracoMembershipProvider() == false)
{
throw new NotSupportedException("An IMember model can only be retrieved when using the built-in Umbraco membership providers");
}
var username = provider.GetCurrentUserName();
var member = _memberService.GetByUsername(username);
return member;
});
}
private static string GetCacheKey(string key, params object[] additional)
{
var sb = new StringBuilder();
sb.Append(typeof(MembershipHelper).Name);
sb.Append("-");
sb.Append(key);
foreach (var s in additional)
if (provider.IsUmbracoMembershipProvider() == false)
{
sb.Append("-");
sb.Append(s);
throw new NotSupportedException("An IMember model can only be retrieved when using the built-in Umbraco membership providers");
}
return sb.ToString();
var username = provider.GetCurrentUserName();
// The result of this is cached by the MemberRepository
var member = _memberService.GetByUsername(username);
return member;
}
}

View File

@@ -348,15 +348,16 @@ namespace Umbraco.Web.Security.Providers
if (userIsOnline)
{
member.LastLoginDate = DateTime.Now;
member.UpdateDate = DateTime.Now;
//don't raise events for this! It just sets the member dates, if we do raise events this will
// cause all distributed cache to execute - which will clear out some caches we don't want.
// http://issues.umbraco.org/issue/U4-3451
// when upgrading from 7.2 to 7.3 trying to save will throw
if (UmbracoVersion.Current >= new Version(7, 3, 0, 0))
MemberService.Save(member, false);
{
var now = DateTime.Now;
// update the database data directly instead of a full member save which requires DB locks
MemberService.SetLastLogin(username, now);
member.LastLoginDate = now;
member.UpdateDate = now;
}
}
return ConvertToMembershipUser(member);
@@ -555,6 +556,8 @@ namespace Umbraco.Web.Security.Providers
var authenticated = CheckPassword(password, member.RawPasswordValue);
var requiresFullSave = false;
if (authenticated == false)
{
// TODO: Increment login attempts - lock if too many.
@@ -574,6 +577,8 @@ namespace Umbraco.Web.Security.Providers
{
Current.Logger.Info<UmbracoMembershipProviderBase>("Login attempt failed for username {Username} from IP address {IpAddress}", username, GetCurrentRequestIpAddress());
}
requiresFullSave = true;
}
else
{
@@ -581,6 +586,7 @@ namespace Umbraco.Web.Security.Providers
{
//we have successfully logged in, reset the AccessFailedCount
member.FailedPasswordAttempts = 0;
requiresFullSave = true;
}
member.LastLoginDate = DateTime.Now;
@@ -588,15 +594,23 @@ namespace Umbraco.Web.Security.Providers
Current.Logger.Info<UmbracoMembershipProviderBase>("Login attempt succeeded for username {Username} from IP address {IpAddress}", username, GetCurrentRequestIpAddress());
}
//don't raise events for this! It just sets the member dates, if we do raise events this will
// don't raise events for this! It just sets the member dates, if we do raise events this will
// cause all distributed cache to execute - which will clear out some caches we don't want.
// http://issues.umbraco.org/issue/U4-3451
// TODO: In v8 we aren't going to have an overload to disable events, so we'll need to make a different method
// for this type of thing (i.e. UpdateLastLogin or similar).
// when upgrading from 7.2 to 7.3 trying to save will throw
if (UmbracoVersion.Current >= new Version(7, 3, 0, 0))
MemberService.Save(member, false);
if (requiresFullSave)
{
// when upgrading from 7.2 to 7.3 trying to save will throw
if (UmbracoVersion.Current >= new Version(7, 3, 0, 0))
MemberService.Save(member, false);
}
else
{
// set the last login date without full save (fast, no locks)
MemberService.SetLastLogin(member.Username, member.LastLoginDate);
}
return new ValidateUserResult
{

View File

@@ -0,0 +1,95 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Ganss.XSS;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.IO;
using Umbraco.Core.Models;
using Umbraco.Core.Services;
namespace Umbraco.Web.Services
{
public class IconService : IIconService
{
private readonly IGlobalSettings _globalSettings;
public IconService(IGlobalSettings globalSettings)
{
_globalSettings = globalSettings;
}
/// <inheritdoc />
public IList<IconModel> GetAllIcons()
{
var icons = new List<IconModel>();
var directory = new DirectoryInfo(IOHelper.MapPath($"{_globalSettings.IconsPath}/"));
var iconNames = directory.GetFiles("*.svg");
iconNames.OrderBy(f => f.Name).ToList().ForEach(iconInfo =>
{
var icon = GetIcon(iconInfo);
if (icon != null)
{
icons.Add(icon);
}
});
return icons;
}
/// <inheritdoc />
public IconModel GetIcon(string iconName)
{
return string.IsNullOrWhiteSpace(iconName)
? null
: CreateIconModel(iconName.StripFileExtension(), IOHelper.MapPath($"{_globalSettings.IconsPath}/{iconName}.svg"));
}
/// <summary>
/// Gets an IconModel using values from a FileInfo model
/// </summary>
/// <param name="fileInfo"></param>
/// <returns></returns>
private IconModel GetIcon(FileInfo fileInfo)
{
return fileInfo == null || string.IsNullOrWhiteSpace(fileInfo.Name)
? null
: CreateIconModel(fileInfo.Name.StripFileExtension(), fileInfo.FullName);
}
/// <summary>
/// Gets an IconModel containing the icon name and SvgString
/// </summary>
/// <param name="iconName"></param>
/// <param name="iconPath"></param>
/// <returns></returns>
private IconModel CreateIconModel(string iconName, string iconPath)
{
var sanitizer = new HtmlSanitizer();
sanitizer.AllowedAttributes.UnionWith(Constants.SvgSanitizer.Attributes);
sanitizer.AllowedCssProperties.UnionWith(Constants.SvgSanitizer.Attributes);
sanitizer.AllowedTags.UnionWith(Constants.SvgSanitizer.Tags);
try
{
var svgContent = System.IO.File.ReadAllText(iconPath);
var sanitizedString = sanitizer.Sanitize(svgContent);
var svg = new IconModel
{
Name = iconName,
SvgString = sanitizedString
};
return svg;
}
catch
{
return null;
}
}
}
}

View File

@@ -240,7 +240,6 @@
<Compile Include="Models\ContentEditing\LinkDisplay.cs" />
<Compile Include="Models\ContentEditing\MacroDisplay.cs" />
<Compile Include="Models\ContentEditing\MacroParameterDisplay.cs" />
<Compile Include="Models\IconModel.cs" />
<Compile Include="Models\ContentEditing\UrlAndAnchors.cs" />
<Compile Include="Models\ImageProcessorImageUrlGenerator.cs" />
<Compile Include="Models\Mapping\CommonMapper.cs" />
@@ -278,6 +277,7 @@
<Compile Include="Routing\IPublishedRouter.cs" />
<Compile Include="Routing\MediaUrlProviderCollection.cs" />
<Compile Include="Routing\MediaUrlProviderCollectionBuilder.cs" />
<Compile Include="Scheduling\SimpleTask.cs" />
<Compile Include="Scheduling\TempFileCleanup.cs" />
<Compile Include="Search\BackgroundIndexRebuilder.cs" />
<Compile Include="Search\ExamineFinalComponent.cs" />
@@ -291,6 +291,7 @@
<Compile Include="Security\SignOutAuditEventArgs.cs" />
<Compile Include="Security\UserInviteEventArgs.cs" />
<Compile Include="Services\DashboardService.cs" />
<Compile Include="Services\IconService.cs" />
<Compile Include="Services\IDashboardService.cs" />
<Compile Include="Models\Link.cs" />
<Compile Include="Models\LinkType.cs" />