Merge remote-tracking branch 'origin/netcore/netcore' into netcore/feature/get-remaining-seconds

# Conflicts:
#	src/Umbraco.Web/Editors/AuthenticationController.cs
This commit is contained in:
Shannon
2020-06-09 10:41:57 +10:00
110 changed files with 1945 additions and 799 deletions

View File

@@ -0,0 +1,126 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json.Linq;
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.Events;
using Umbraco.Core.Models;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Implement;
using Umbraco.Web.PropertyEditors;
namespace Umbraco.Web.Compose
{
public class NestedContentPropertyComponent : IComponent
{
public void Initialize()
{
ContentService.Copying += ContentService_Copying;
ContentService.Saving += ContentService_Saving;
}
private void ContentService_Copying(IContentService sender, CopyEventArgs<IContent> e)
{
// When a content node contains nested content property
// Check if the copied node contains a nested content
var nestedContentProps = e.Copy.Properties.Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.NestedContent);
UpdateNestedContentProperties(nestedContentProps, false);
}
private void ContentService_Saving(IContentService sender, ContentSavingEventArgs e)
{
// One or more content nodes could be saved in a bulk publish
foreach (var entity in e.SavedEntities)
{
// When a content node contains nested content property
// Check if the copied node contains a nested content
var nestedContentProps = entity.Properties.Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.NestedContent);
UpdateNestedContentProperties(nestedContentProps, true);
}
}
public void Terminate()
{
ContentService.Copying -= ContentService_Copying;
ContentService.Saving -= ContentService_Saving;
}
private void UpdateNestedContentProperties(IEnumerable<IProperty> nestedContentProps, bool onlyMissingKeys)
{
// Each NC Property on a doctype
foreach (var nestedContentProp in nestedContentProps)
{
// A NC Prop may have one or more values due to cultures
var propVals = nestedContentProp.Values;
foreach (var cultureVal in propVals)
{
// Remove keys from published value & any nested NC's
var updatedPublishedVal = CreateNestedContentKeys(cultureVal.PublishedValue?.ToString(), onlyMissingKeys);
cultureVal.PublishedValue = updatedPublishedVal;
// Remove keys from edited/draft value & any nested NC's
var updatedEditedVal = CreateNestedContentKeys(cultureVal.EditedValue?.ToString(), onlyMissingKeys);
cultureVal.EditedValue = updatedEditedVal;
}
}
}
// internal for tests
internal string CreateNestedContentKeys(string rawJson, bool onlyMissingKeys, Func<Guid> createGuid = null)
{
// used so we can test nicely
if (createGuid == null)
createGuid = () => Guid.NewGuid();
if (string.IsNullOrWhiteSpace(rawJson) || !rawJson.DetectIsJson())
return rawJson;
// Parse JSON
var complexEditorValue = JToken.Parse(rawJson);
UpdateNestedContentKeysRecursively(complexEditorValue, onlyMissingKeys, createGuid);
return complexEditorValue.ToString();
}
private void UpdateNestedContentKeysRecursively(JToken json, bool onlyMissingKeys, Func<Guid> createGuid)
{
// check if this is NC
var isNestedContent = json.SelectTokens($"$..['{NestedContentPropertyEditor.ContentTypeAliasPropertyKey}']", false).Any();
// select all values (flatten)
var allProperties = json.SelectTokens("$..*").OfType<JValue>().Select(x => x.Parent as JProperty).WhereNotNull().ToList();
foreach (var prop in allProperties)
{
if (prop.Name == NestedContentPropertyEditor.ContentTypeAliasPropertyKey)
{
// get it's sibling 'key' property
var ncKeyVal = prop.Parent["key"] as JValue;
// TODO: This bool seems odd, if the key is null, shouldn't we fill it in regardless of onlyMissingKeys?
if ((onlyMissingKeys && ncKeyVal == null) || (!onlyMissingKeys && ncKeyVal != null))
{
// create or replace
prop.Parent["key"] = createGuid().ToString();
}
}
else if (!isNestedContent || prop.Name != "key")
{
// this is an arbitrary property that could contain a nested complex editor
var propVal = prop.Value?.ToString();
// check if this might contain a nested NC
if (!propVal.IsNullOrWhiteSpace() && propVal.DetectIsJson() && propVal.InvariantContains(NestedContentPropertyEditor.ContentTypeAliasPropertyKey))
{
// recurse
var parsed = JToken.Parse(propVal);
UpdateNestedContentKeysRecursively(parsed, onlyMissingKeys, createGuid);
// set the value to the updated one
prop.Value = parsed.ToString();
}
}
}
}
}
}

View File

@@ -0,0 +1,9 @@
using Umbraco.Core;
using Umbraco.Core.Composing;
namespace Umbraco.Web.Compose
{
[RuntimeLevel(MinLevel = RuntimeLevel.Run)]
public class NestedContentPropertyComposer : ComponentComposer<NestedContentPropertyComponent>, ICoreComposer
{ }
}

View File

@@ -41,16 +41,6 @@ namespace Umbraco.Web
/// <summary>
/// Gets the back office tree collection builder
/// </summary>
/// <param name="composition"></param>
/// <returns></returns>
public static TreeCollectionBuilder Trees(this Composition composition)
=> composition.WithCollectionBuilder<TreeCollectionBuilder>();
#endregion
#region Uniques

View File

@@ -88,7 +88,7 @@ namespace Umbraco.Web.Editors
[WebApi.UmbracoAuthorize(requireApproval: false)]
public IDictionary<string, object> GetPasswordConfig(int userId)
{
return _passwordConfiguration.GetConfiguration(userId != UmbracoContext.Security.CurrentUser.Id);
return _passwordConfiguration.GetConfiguration(userId != Security.CurrentUser.Id);
}
/// <summary>
@@ -169,7 +169,7 @@ namespace Umbraco.Web.Editors
[SetAngularAntiForgeryTokens]
public UserDetail GetCurrentInvitedUser()
{
var user = UmbracoContext.Security.CurrentUser;
var user = Security.CurrentUser;
if (user.IsApproved)
{
@@ -193,7 +193,7 @@ namespace Umbraco.Web.Editors
[ValidateAngularAntiForgeryToken]
public async Task<Dictionary<string, string>> GetCurrentUserLinkedLogins()
{
var identityUser = await UserManager.FindByIdAsync(UmbracoContext.Security.GetUserId().ResultOr(0).ToString());
var identityUser = await UserManager.FindByIdAsync(Security.GetUserId().ResultOr(0).ToString());
return identityUser.Logins.ToDictionary(x => x.LoginProvider, x => x.ProviderKey);
}

View File

@@ -172,14 +172,7 @@ namespace Umbraco.Web.Editors
// "imagesApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl<ImagesController>(
// controller => controller.GetBigThumbnail(""))
// },
{
"sectionApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl<SectionController>(
controller => controller.GetSections())
},
{
"treeApplicationApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl<ApplicationTreeController>(
controller => controller.GetApplicationTrees(null, null, null, TreeUse.None))
},
{
"contentTypeApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl<ContentTypeController>(
controller => controller.GetAllowedChildren(0))
@@ -196,7 +189,7 @@ namespace Umbraco.Web.Editors
"macroApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl<MacrosController>(
controller => controller.Create(null))
},
{
"currentUserApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl<CurrentUserController>(
controller => controller.PostChangePassword(null))
@@ -250,10 +243,10 @@ namespace Umbraco.Web.Editors
"memberGroupApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl<MemberGroupController>(
controller => controller.GetAllGroups())
},
{
"updateCheckApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl<UpdateCheckController>(
controller => controller.GetCheck())
},
// {
// "updateCheckApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl<UpdateCheckController>(
// controller => controller.GetCheck())
// },
// {
// "templateApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl<TemplateController>(
// controller => controller.GetById(0))
@@ -296,10 +289,10 @@ namespace Umbraco.Web.Editors
// "publishedStatusBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl<PublishedStatusController>(
// controller => controller.GetPublishedStatusUrl())
// },
{
"dictionaryApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl<DictionaryController>(
controller => controller.DeleteById(int.MaxValue))
},
// {
// "dictionaryApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl<DictionaryController>(
// controller => controller.DeleteById(int.MaxValue))
// },
// {
// "publishedSnapshotCacheStatusBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl<PublishedSnapshotCacheStatusController>(
// controller => controller.GetStatus())
@@ -330,10 +323,10 @@ namespace Umbraco.Web.Editors
// "webProfilingBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl<WebProfilingController>(
// controller => controller.GetStatus())
// },
{
"tinyMceApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl<TinyMceController>(
controller => controller.UploadImage())
},
// {
// "tinyMceApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl<TinyMceController>(
// controller => controller.UploadImage())
// },
//TODO Reintroduce
// {
// "imageUrlGeneratorApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl<ImageUrlGeneratorController>(

View File

@@ -1,264 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration;
using Umbraco.Core.Logging;
using Umbraco.Core.Mapping;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence;
using Umbraco.Core.Services;
using Umbraco.Core.Strings;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Mvc;
using Umbraco.Web.Routing;
using Umbraco.Web.WebApi;
using Umbraco.Web.WebApi.Filters;
using Constants = Umbraco.Core.Constants;
namespace Umbraco.Web.Editors
{
/// <inheritdoc />
/// <summary>
/// The API controller used for editing dictionary items
/// </summary>
/// <remarks>
/// The security for this controller is defined to allow full CRUD access to dictionary if the user has access to either:
/// Dictionary
/// </remarks>
[PluginController("UmbracoApi")]
[UmbracoTreeAuthorize(Constants.Trees.Dictionary)]
[EnableOverrideAuthorization]
public class DictionaryController : BackOfficeNotificationsController
{
public DictionaryController(
IGlobalSettings globalSettings,
IUmbracoContextAccessor umbracoContextAccessor,
ISqlContext sqlContext,
ServiceContext services,
AppCaches appCaches,
IProfilingLogger logger,
IRuntimeState runtimeState,
IShortStringHelper shortStringHelper,
UmbracoMapper umbracoMapper,
IPublishedUrlProvider publishedUrlProvider)
: base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, shortStringHelper, umbracoMapper, publishedUrlProvider)
{
}
/// <summary>
/// Deletes a data type with a given ID
/// </summary>
/// <param name="id"></param>
/// <returns><see cref="HttpResponseMessage"/></returns>
[HttpDelete]
[HttpPost]
public HttpResponseMessage DeleteById(int id)
{
var foundDictionary = Services.LocalizationService.GetDictionaryItemById(id);
if (foundDictionary == null)
throw new HttpResponseException(HttpStatusCode.NotFound);
var foundDictionaryDescendants = Services.LocalizationService.GetDictionaryItemDescendants(foundDictionary.Key);
foreach (var dictionaryItem in foundDictionaryDescendants)
{
Services.LocalizationService.Delete(dictionaryItem, Security.CurrentUser.Id);
}
Services.LocalizationService.Delete(foundDictionary, Security.CurrentUser.Id);
return Request.CreateResponse(HttpStatusCode.OK);
}
/// <summary>
/// Creates a new dictionary item
/// </summary>
/// <param name="parentId">
/// The parent id.
/// </param>
/// <param name="key">
/// The key.
/// </param>
/// <returns>
/// The <see cref="HttpResponseMessage"/>.
/// </returns>
[HttpPost]
public HttpResponseMessage Create(int parentId, string key)
{
if (string.IsNullOrEmpty(key))
return Request
.CreateNotificationValidationErrorResponse("Key can not be empty."); // TODO: translate
if (Services.LocalizationService.DictionaryItemExists(key))
{
var message = Services.TextService.Localize(
"dictionaryItem/changeKeyError",
Security.CurrentUser.GetUserCulture(Services.TextService, GlobalSettings),
new Dictionary<string, string> { { "0", key } });
return Request.CreateNotificationValidationErrorResponse(message);
}
try
{
Guid? parentGuid = null;
if (parentId > 0)
parentGuid = Services.LocalizationService.GetDictionaryItemById(parentId).Key;
var item = Services.LocalizationService.CreateDictionaryItemWithIdentity(
key,
parentGuid,
string.Empty);
return Request.CreateResponse(HttpStatusCode.OK, item.Id);
}
catch (Exception ex)
{
Logger.Error(GetType(), ex, "Error creating dictionary with {Name} under {ParentId}", key, parentId);
return Request.CreateNotificationValidationErrorResponse("Error creating dictionary item");
}
}
/// <summary>
/// Gets a dictionary item by id
/// </summary>
/// <param name="id">
/// The id.
/// </param>
/// <returns>
/// The <see cref="DictionaryDisplay"/>.
/// </returns>
/// <exception cref="HttpResponseException">
/// Returns a not found response when dictionary item does not exist
/// </exception>
public DictionaryDisplay GetById(int id)
{
var dictionary = Services.LocalizationService.GetDictionaryItemById(id);
if (dictionary == null)
throw new HttpResponseException(HttpStatusCode.NotFound);
return Mapper.Map<IDictionaryItem, DictionaryDisplay>(dictionary);
}
/// <summary>
/// Saves a dictionary item
/// </summary>
/// <param name="dictionary">
/// The dictionary.
/// </param>
/// <returns>
/// The <see cref="DictionaryDisplay"/>.
/// </returns>
public DictionaryDisplay PostSave(DictionarySave dictionary)
{
var dictionaryItem =
Services.LocalizationService.GetDictionaryItemById(int.Parse(dictionary.Id.ToString()));
if (dictionaryItem == null)
throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse("Dictionary item does not exist"));
var userCulture = Security.CurrentUser.GetUserCulture(Services.TextService, GlobalSettings);
if (dictionary.NameIsDirty)
{
// if the name (key) has changed, we need to check if the new key does not exist
var dictionaryByKey = Services.LocalizationService.GetDictionaryItemByKey(dictionary.Name);
if (dictionaryByKey != null && dictionaryItem.Id != dictionaryByKey.Id)
{
var message = Services.TextService.Localize(
"dictionaryItem/changeKeyError",
userCulture,
new Dictionary<string, string> { { "0", dictionary.Name } });
ModelState.AddModelError("Name", message);
throw new HttpResponseException(Request.CreateValidationErrorResponse(ModelState));
}
dictionaryItem.ItemKey = dictionary.Name;
}
foreach (var translation in dictionary.Translations)
{
Services.LocalizationService.AddOrUpdateDictionaryValue(dictionaryItem,
Services.LocalizationService.GetLanguageById(translation.LanguageId), translation.Translation);
}
try
{
Services.LocalizationService.Save(dictionaryItem);
var model = Mapper.Map<IDictionaryItem, DictionaryDisplay>(dictionaryItem);
model.Notifications.Add(new BackOfficeNotification(
Services.TextService.Localize("speechBubbles/dictionaryItemSaved", userCulture), string.Empty,
NotificationStyle.Success));
return model;
}
catch (Exception ex)
{
Logger.Error(GetType(), ex, "Error saving dictionary with {Name} under {ParentId}", dictionary.Name, dictionary.ParentId);
throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse("Something went wrong saving dictionary"));
}
}
/// <summary>
/// Retrieves a list with all dictionary items
/// </summary>
/// <returns>
/// The <see cref="IEnumerable{T}"/>.
/// </returns>
public IEnumerable<DictionaryOverviewDisplay> GetList()
{
var list = new List<DictionaryOverviewDisplay>();
const int level = 0;
foreach (var dictionaryItem in Services.LocalizationService.GetRootDictionaryItems().OrderBy(ItemSort()))
{
var item = Mapper.Map<IDictionaryItem, DictionaryOverviewDisplay>(dictionaryItem);
item.Level = 0;
list.Add(item);
GetChildItemsForList(dictionaryItem, level + 1, list);
}
return list;
}
/// <summary>
/// Get child items for list.
/// </summary>
/// <param name="dictionaryItem">
/// The dictionary item.
/// </param>
/// <param name="level">
/// The level.
/// </param>
/// <param name="list">
/// The list.
/// </param>
private void GetChildItemsForList(IDictionaryItem dictionaryItem, int level, ICollection<DictionaryOverviewDisplay> list)
{
foreach (var childItem in Services.LocalizationService.GetDictionaryItemChildren(dictionaryItem.Key).OrderBy(ItemSort()))
{
var item = Mapper.Map<IDictionaryItem, DictionaryOverviewDisplay>(childItem);
item.Level = level;
list.Add(item);
GetChildItemsForList(childItem, level + 1, list);
}
}
private static Func<IDictionaryItem, string> ItemSort() => item => item.ItemKey;
}
}

View File

@@ -10,6 +10,7 @@ using Umbraco.Core.Models;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Security;
namespace Umbraco.Web.Editors.Filters
{
@@ -18,13 +19,13 @@ namespace Umbraco.Web.Editors.Filters
/// </summary>
internal abstract class ContentModelValidator
{
protected IUmbracoContextAccessor UmbracoContextAccessor { get; }
protected IWebSecurity WebSecurity { get; }
protected ILogger Logger { get; }
protected ContentModelValidator(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor)
protected ContentModelValidator(ILogger logger, IWebSecurity webSecurity)
{
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
UmbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor));
WebSecurity = webSecurity ?? throw new ArgumentNullException(nameof(webSecurity));
}
}
@@ -45,7 +46,7 @@ namespace Umbraco.Web.Editors.Filters
{
private readonly ILocalizedTextService _textService;
protected ContentModelValidator(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService) : base(logger, umbracoContextAccessor)
protected ContentModelValidator(ILogger logger, IWebSecurity webSecurity, ILocalizedTextService textService) : base(logger, webSecurity)
{
_textService = textService ?? throw new ArgumentNullException(nameof(textService));
}

View File

@@ -2,6 +2,7 @@
using Umbraco.Core.Models;
using Umbraco.Core.Services;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Security;
namespace Umbraco.Web.Editors.Filters
{
@@ -10,7 +11,7 @@ namespace Umbraco.Web.Editors.Filters
/// </summary>
internal class ContentSaveModelValidator : ContentModelValidator<IContent, ContentItemSave, ContentVariantSave>
{
public ContentSaveModelValidator(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService) : base(logger, umbracoContextAccessor, textService)
public ContentSaveModelValidator(ILogger logger, IWebSecurity webSecurity, ILocalizedTextService textService) : base(logger, webSecurity, textService)
{
}
}

View File

@@ -25,19 +25,19 @@ namespace Umbraco.Web.Editors.Filters
internal sealed class ContentSaveValidationAttribute : ActionFilterAttribute
{
private readonly ILogger _logger;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly IWebSecurity _webSecurity;
private readonly ILocalizedTextService _textService;
private readonly IContentService _contentService;
private readonly IUserService _userService;
private readonly IEntityService _entityService;
public ContentSaveValidationAttribute(): this(Current.Logger, Current.UmbracoContextAccessor, Current.Services.TextService, Current.Services.ContentService, Current.Services.UserService, Current.Services.EntityService)
public ContentSaveValidationAttribute(): this(Current.Logger, Current.UmbracoContextAccessor.UmbracoContext.Security, Current.Services.TextService, Current.Services.ContentService, Current.Services.UserService, Current.Services.EntityService)
{ }
public ContentSaveValidationAttribute(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService, IContentService contentService, IUserService userService, IEntityService entityService)
public ContentSaveValidationAttribute(ILogger logger, IWebSecurity webSecurity, ILocalizedTextService textService, IContentService contentService, IUserService userService, IEntityService entityService)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor));
_webSecurity = webSecurity ?? throw new ArgumentNullException(nameof(webSecurity));
_textService = textService ?? throw new ArgumentNullException(nameof(textService));
_contentService = contentService ?? throw new ArgumentNullException(nameof(contentService));
_userService = userService ?? throw new ArgumentNullException(nameof(userService));
@@ -47,11 +47,11 @@ namespace Umbraco.Web.Editors.Filters
public override void OnActionExecuting(HttpActionContext actionContext)
{
var model = (ContentItemSave)actionContext.ActionArguments["contentItem"];
var contentItemValidator = new ContentSaveModelValidator(_logger, _umbracoContextAccessor, _textService);
var contentItemValidator = new ContentSaveModelValidator(_logger, _webSecurity, _textService);
if (!ValidateAtLeastOneVariantIsBeingSaved(model, actionContext)) return;
if (!contentItemValidator.ValidateExistingContent(model, actionContext)) return;
if (!ValidateUserAccess(model, actionContext, _umbracoContextAccessor.UmbracoContext.Security)) return;
if (!ValidateUserAccess(model, actionContext, _webSecurity)) return;
//validate for each variant that is being updated
foreach (var variant in model.Variants.Where(x => x.Save))

View File

@@ -9,6 +9,7 @@ using Umbraco.Core.Models;
using Umbraco.Core.Services;
using Umbraco.Web.Composing;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Security;
using Umbraco.Web.WebApi;
namespace Umbraco.Web.Editors.Filters
@@ -19,19 +20,19 @@ namespace Umbraco.Web.Editors.Filters
internal class MediaItemSaveValidationAttribute : ActionFilterAttribute
{
private readonly ILogger _logger;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly IWebSecurity _webSecurity;
private readonly ILocalizedTextService _textService;
private readonly IMediaService _mediaService;
private readonly IEntityService _entityService;
public MediaItemSaveValidationAttribute() : this(Current.Logger, Current.UmbracoContextAccessor, Current.Services.TextService, Current.Services.MediaService, Current.Services.EntityService)
public MediaItemSaveValidationAttribute() : this(Current.Logger, Current.UmbracoContextAccessor.UmbracoContext.Security, Current.Services.TextService, Current.Services.MediaService, Current.Services.EntityService)
{
}
public MediaItemSaveValidationAttribute(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService, IMediaService mediaService, IEntityService entityService)
public MediaItemSaveValidationAttribute(ILogger logger, IWebSecurity webSecurity, ILocalizedTextService textService, IMediaService mediaService, IEntityService entityService)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor));
_webSecurity = webSecurity ?? throw new ArgumentNullException(nameof(webSecurity));
_textService = textService ?? throw new ArgumentNullException(nameof(textService));
_mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService));
_entityService = entityService ?? throw new ArgumentNullException(nameof(entityService));
@@ -40,7 +41,7 @@ namespace Umbraco.Web.Editors.Filters
public override void OnActionExecuting(HttpActionContext actionContext)
{
var model = (MediaItemSave)actionContext.ActionArguments["contentItem"];
var contentItemValidator = new MediaSaveModelValidator(_logger, _umbracoContextAccessor, _textService);
var contentItemValidator = new MediaSaveModelValidator(_logger, _webSecurity, _textService);
if (ValidateUserAccess(model, actionContext))
{
@@ -90,7 +91,7 @@ namespace Umbraco.Web.Editors.Filters
if (MediaController.CheckPermissions(
actionContext.Request.Properties,
_umbracoContextAccessor.UmbracoContext.Security.CurrentUser,
_webSecurity.CurrentUser,
_mediaService, _entityService,
contentIdToCheck, contentToCheck) == false)
{

View File

@@ -2,6 +2,7 @@
using Umbraco.Core.Models;
using Umbraco.Core.Services;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Security;
namespace Umbraco.Web.Editors.Filters
{
@@ -10,7 +11,7 @@ namespace Umbraco.Web.Editors.Filters
/// </summary>
internal class MediaSaveModelValidator : ContentModelValidator<IMedia, MediaItemSave, IContentProperties<ContentPropertyBasic>>
{
public MediaSaveModelValidator(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService) : base(logger, umbracoContextAccessor, textService)
public MediaSaveModelValidator(ILogger logger, IWebSecurity webSecurity, ILocalizedTextService textService) : base(logger, webSecurity, textService)
{
}
}

View File

@@ -11,6 +11,7 @@ using Umbraco.Core.Models;
using Umbraco.Core.Services;
using Umbraco.Core.Strings;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Security;
namespace Umbraco.Web.Editors.Filters
{
@@ -25,12 +26,12 @@ namespace Umbraco.Web.Editors.Filters
public MemberSaveModelValidator(
ILogger logger,
IUmbracoContextAccessor umbracoContextAccessor,
IWebSecurity webSecurity,
ILocalizedTextService textService,
IMemberTypeService memberTypeService,
IMemberService memberService,
IShortStringHelper shortStringHelper)
: base(logger, umbracoContextAccessor, textService)
: base(logger, webSecurity, textService)
{
_memberTypeService = memberTypeService ?? throw new ArgumentNullException(nameof(memberTypeService));
_memberService = memberService ?? throw new ArgumentNullException(nameof(memberService));
@@ -101,7 +102,7 @@ namespace Umbraco.Web.Editors.Filters
//if the user doesn't have access to sensitive values, then we need to validate the incoming properties to check
//if a sensitive value is being submitted.
if (UmbracoContextAccessor.UmbracoContext.Security.CurrentUser.HasAccessToSensitiveData() == false)
if (WebSecurity.CurrentUser.HasAccessToSensitiveData() == false)
{
var contentType = _memberTypeService.Get(model.PersistedContent.ContentTypeId);
var sensitiveProperties = contentType

View File

@@ -6,6 +6,7 @@ using Umbraco.Core.Services;
using Umbraco.Core.Strings;
using Umbraco.Web.Composing;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Security;
namespace Umbraco.Web.Editors.Filters
{
@@ -15,20 +16,20 @@ namespace Umbraco.Web.Editors.Filters
internal class MemberSaveValidationAttribute : ActionFilterAttribute
{
private readonly ILogger _logger;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly IWebSecurity _webSecurity;
private readonly ILocalizedTextService _textService;
private readonly IMemberTypeService _memberTypeService;
private readonly IMemberService _memberService;
private readonly IShortStringHelper _shortStringHelper;
public MemberSaveValidationAttribute()
: this(Current.Logger, Current.UmbracoContextAccessor, Current.Services.TextService, Current.Services.MemberTypeService, Current.Services.MemberService, Current.ShortStringHelper)
: this(Current.Logger, Current.UmbracoContextAccessor.UmbracoContext.Security, Current.Services.TextService, Current.Services.MemberTypeService, Current.Services.MemberService, Current.ShortStringHelper)
{ }
public MemberSaveValidationAttribute(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService, IMemberTypeService memberTypeService, IMemberService memberService, IShortStringHelper shortStringHelper)
public MemberSaveValidationAttribute(ILogger logger, IWebSecurity webSecurity, ILocalizedTextService textService, IMemberTypeService memberTypeService, IMemberService memberService, IShortStringHelper shortStringHelper)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor));
_webSecurity = webSecurity ?? throw new ArgumentNullException(nameof(webSecurity));
_textService = textService ?? throw new ArgumentNullException(nameof(textService));
_memberTypeService = memberTypeService ?? throw new ArgumentNullException(nameof(memberTypeService));
_memberService = memberService ?? throw new ArgumentNullException(nameof(memberService));
@@ -38,7 +39,7 @@ namespace Umbraco.Web.Editors.Filters
public override void OnActionExecuting(HttpActionContext actionContext)
{
var model = (MemberSave)actionContext.ActionArguments["contentItem"];
var contentItemValidator = new MemberSaveModelValidator(_logger, _umbracoContextAccessor,_textService, _memberTypeService, _memberService, _shortStringHelper);
var contentItemValidator = new MemberSaveModelValidator(_logger, _webSecurity, _textService, _memberTypeService, _memberService, _shortStringHelper);
//now do each validation step
if (contentItemValidator.ValidateExistingContent(model, actionContext))
if (contentItemValidator.ValidateProperties(model, model, actionContext))

View File

@@ -136,7 +136,7 @@ namespace Umbraco.Web.Editors
var ctId = Convert.ToInt32(contentTypeSave.Id);
var ct = ctId > 0 ? Services.MemberTypeService.Get(ctId) : null;
if (UmbracoContext.Security.CurrentUser.HasAccessToSensitiveData() == false)
if (Security.CurrentUser.HasAccessToSensitiveData() == false)
{
//We need to validate if any properties on the contentTypeSave have had their IsSensitiveValue changed,
//and if so, we need to check if the current user has access to sensitive values. If not, we have to return an error

View File

@@ -1,106 +0,0 @@
using System.Collections.Generic;
using Umbraco.Web.Mvc;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence;
using Umbraco.Core.Services;
using Umbraco.Core.Strings;
using Umbraco.Web.Trees;
using Section = Umbraco.Web.Models.ContentEditing.Section;
using Umbraco.Web.Models.Trees;
using Umbraco.Web.Services;
using Umbraco.Core.Mapping;
using Umbraco.Web.Routing;
namespace Umbraco.Web.Editors
{
/// <summary>
/// The API controller used for using the list of sections
/// </summary>
[PluginController("UmbracoApi")]
public class SectionController : UmbracoAuthorizedJsonController
{
private readonly IDashboardService _dashboardService;
private readonly ISectionService _sectionService;
private readonly ITreeService _treeService;
public SectionController(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState,
IDashboardService dashboardService, ISectionService sectionService, ITreeService treeService, IShortStringHelper shortStringHelper, UmbracoMapper umbracoMapper, IPublishedUrlProvider publishedUrlProvider)
: base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, shortStringHelper, umbracoMapper, publishedUrlProvider)
{
_dashboardService = dashboardService;
_sectionService = sectionService;
_treeService = treeService;
}
public IEnumerable<Section> GetSections()
{
var sections = _sectionService.GetAllowedSections(Security.GetUserId().ResultOr(0));
var sectionModels = sections.Select(Mapper.Map<Section>).ToArray();
// this is a bit nasty since we'll be proxying via the app tree controller but we sort of have to do that
// since tree's by nature are controllers and require request contextual data
var appTreeController = new ApplicationTreeController(GlobalSettings, UmbracoContextAccessor, SqlContext, Services, AppCaches, Logger, RuntimeState, _treeService, _sectionService, Mapper, PublishedUrlProvider)
{
ControllerContext = ControllerContext
};
var dashboards = _dashboardService.GetDashboards(Security.CurrentUser);
//now we can add metadata for each section so that the UI knows if there's actually anything at all to render for
//a dashboard for a given section, then the UI can deal with it accordingly (i.e. redirect to the first tree)
foreach (var section in sectionModels)
{
var hasDashboards = dashboards.TryGetValue(section.Alias, out var dashboardsForSection) && dashboardsForSection.Any();
if (hasDashboards) continue;
// get the first tree in the section and get its root node route path
var sectionRoot = appTreeController.GetApplicationTrees(section.Alias, null, null).Result;
section.RoutePath = GetRoutePathForFirstTree(sectionRoot);
}
return sectionModels;
}
/// <summary>
/// Returns the first non root/group node's route path
/// </summary>
/// <param name="rootNode"></param>
/// <returns></returns>
private string GetRoutePathForFirstTree(TreeRootNode rootNode)
{
if (!rootNode.IsContainer || !rootNode.ContainsTrees)
return rootNode.RoutePath;
foreach(var node in rootNode.Children)
{
if (node is TreeRootNode groupRoot)
return GetRoutePathForFirstTree(groupRoot);//recurse to get the first tree in the group
else
return node.RoutePath;
}
return string.Empty;
}
/// <summary>
/// Returns all the sections that the user has access to
/// </summary>
/// <returns></returns>
public IEnumerable<Section> GetAllSections()
{
var sections = _sectionService.GetSections();
var mapped = sections.Select(Mapper.Map<Section>);
if (Security.CurrentUser.IsAdmin())
return mapped;
return mapped.Where(x => Security.CurrentUser.AllowedSections.Contains(x.Alias)).ToArray();
}
}
}

View File

@@ -1,129 +0,0 @@
using System;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Mapping;
using Umbraco.Core.Persistence;
using Umbraco.Core.Services;
using Umbraco.Core.Strings;
using Umbraco.Web.Mvc;
using Umbraco.Web.Routing;
using Umbraco.Web.WebApi;
using Umbraco.Web.WebApi.Filters;
using Constants = Umbraco.Core.Constants;
namespace Umbraco.Web.Editors
{
[PluginController("UmbracoApi")]
[UmbracoApplicationAuthorize(
Constants.Applications.Content,
Constants.Applications.Media,
Constants.Applications.Members)]
public class TinyMceController : UmbracoAuthorizedApiController
{
private readonly IIOHelper _ioHelper;
private readonly IShortStringHelper _shortStringHelper;
private readonly IContentSettings _contentSettings;
public TinyMceController(
IGlobalSettings globalSettings,
IUmbracoContextAccessor umbracoContextAccessor,
ISqlContext sqlContext,
ServiceContext services,
AppCaches appCaches,
IProfilingLogger logger,
IRuntimeState runtimeState,
UmbracoMapper umbracoMapper,
IShortStringHelper shortStringHelper,
IContentSettings contentSettings,
IIOHelper ioHelper,
IPublishedUrlProvider publishedUrlProvider)
: base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoMapper, publishedUrlProvider)
{
_shortStringHelper = shortStringHelper ?? throw new ArgumentNullException(nameof(shortStringHelper));
_contentSettings = contentSettings ?? throw new ArgumentNullException(nameof(contentSettings));
_ioHelper = ioHelper ?? throw new ArgumentNullException(nameof(ioHelper));
}
[HttpPost]
public async Task<HttpResponseMessage> UploadImage()
{
if (Request.Content.IsMimeMultipartContent() == false)
{
throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
}
// Create an unique folder path to help with concurrent users to avoid filename clash
var imageTempPath = _ioHelper.MapPath(Constants.SystemDirectories.TempImageUploads + "/" + Guid.NewGuid().ToString());
// Temp folderpath (Files come in as bodypart & will need to move/saved into imgTempPath
var folderPath = _ioHelper.MapPath(Constants.SystemDirectories.TempFileUploads);
// Ensure image temp path exists
if(Directory.Exists(imageTempPath) == false)
{
Directory.CreateDirectory(imageTempPath);
}
// File uploaded will be saved as bodypart into TEMP folder
var provider = new MultipartFormDataStreamProvider(folderPath);
var result = await Request.Content.ReadAsMultipartAsync(provider);
// Must have a file
if (result.FileData.Count == 0)
{
return Request.CreateResponse(HttpStatusCode.NotFound);
}
// Should only have one file
if (result.FileData.Count > 1)
{
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, "Only one file can be uploaded at a time");
}
// Really we should only have one file per request to this endpoint
var file = result.FileData[0];
var fileName = file.Headers.ContentDisposition.FileName.Trim(new[] { '\"' }).TrimEnd();
var safeFileName = fileName.ToSafeFileName(_shortStringHelper);
var ext = safeFileName.Substring(safeFileName.LastIndexOf('.') + 1).ToLower();
if (_contentSettings.IsFileAllowedForUpload(ext) == false || _contentSettings.ImageFileTypes.Contains(ext) == false)
{
// Throw some error - to say can't upload this IMG type
return Request.CreateErrorResponse(HttpStatusCode.BadRequest, "This is not an image filetype extension that is approved");
}
//var mediaItemName = fileName.ToFriendlyName();
var currentFile = file.LocalFileName;
var newFilePath = imageTempPath + Path.DirectorySeparatorChar + safeFileName;
var relativeNewFilePath = _ioHelper.GetRelativePath(newFilePath);
try
{
// Move the file from bodypart to a real filename
// This is what we return from this API so RTE updates img src path
// Until we fully persist & save the media item when persisting
// If we find <img data-temp-img /> data attribute
File.Move(currentFile, newFilePath);
}
catch (Exception ex)
{
// IOException, PathTooLong, DirectoryNotFound, UnathorizedAccess
Logger.Error<TinyMceController>(ex, "Error when trying to move {CurrentFilePath} to {NewFilePath}", currentFile, newFilePath);
return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, $"Error when trying to move {currentFile} to {newFilePath}", ex);
}
return Request.CreateResponse(HttpStatusCode.OK, new { tmpLocation = relativeNewFilePath });
}
}
}

View File

@@ -1,83 +0,0 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using System.Web.Http.Filters;
using Semver;
using Umbraco.Core;
using Umbraco.Web.Composing;
using Umbraco.Core.Configuration;
using Umbraco.Core.Models;
using Umbraco.Core.Services;
using Umbraco.Web.Models;
using Umbraco.Web.Mvc;
namespace Umbraco.Web.Editors
{
[PluginController("UmbracoApi")]
public class UpdateCheckController : UmbracoAuthorizedJsonController
{
private readonly IUpgradeService _upgradeService;
private readonly IUmbracoVersion _umbracoVersion;
public UpdateCheckController() { }
public UpdateCheckController(IUpgradeService upgradeService, IUmbracoVersion umbracoVersion)
{
_upgradeService = upgradeService;
_umbracoVersion = umbracoVersion;
}
[UpdateCheckResponseFilter]
public async Task<UpgradeCheckResponse> GetCheck()
{
var updChkCookie = Request.Headers.GetCookies("UMB_UPDCHK").FirstOrDefault();
var updateCheckCookie = updChkCookie != null ? updChkCookie["UMB_UPDCHK"].Value : "";
if (GlobalSettings.VersionCheckPeriod > 0 && string.IsNullOrEmpty(updateCheckCookie) && Security.CurrentUser.IsAdmin())
{
try
{
var version = new SemVersion(_umbracoVersion.Current.Major, _umbracoVersion.Current.Minor,
_umbracoVersion.Current.Build, _umbracoVersion.Comment);
var result = await _upgradeService.CheckUpgrade(version);
return new UpgradeCheckResponse(result.UpgradeType, result.Comment, result.UpgradeUrl, _umbracoVersion);
}
catch
{
//We don't want to crash due to this
return null;
}
}
return null;
}
/// <summary>
/// Adds the cookie response if it was successful
/// </summary>
/// <remarks>
/// A filter is required because we are returning an object from the get method and not an HttpResponseMessage
/// </remarks>
internal class UpdateCheckResponseFilter : ActionFilterAttribute
{
public override void OnActionExecuted(HttpActionExecutedContext context)
{
if (context.Response == null) return;
var objectContent = context.Response.Content as ObjectContent;
if (objectContent == null || objectContent.Value == null) return;
//there is a result, set the outgoing cookie
var cookie = new CookieHeaderValue("UMB_UPDCHK", "1")
{
Path = "/",
Expires = DateTimeOffset.Now.AddDays(Current.Configs.Global().VersionCheckPeriod),
HttpOnly = true,
Secure = Current.Configs.Global().UseHttps
};
context.Response.Headers.AddCookies(new[] { cookie });
}
}
}
}

View File

@@ -92,7 +92,7 @@ namespace Umbraco.Web.Editors
/// <returns></returns>
public string[] GetCurrentUserAvatarUrls()
{
var urls = UmbracoContext.Security.CurrentUser.GetUserAvatarUrls(AppCaches.RuntimeCache, _mediaFileSystem, _imageUrlGenerator);
var urls = Security.CurrentUser.GetUserAvatarUrls(AppCaches.RuntimeCache, _mediaFileSystem, _imageUrlGenerator);
if (urls == null)
throw new HttpResponseException(Request.CreateErrorResponse(HttpStatusCode.BadRequest, "Could not access Gravatar endpoint"));

View File

@@ -7,7 +7,7 @@ using Umbraco.Core;
namespace Umbraco.Web
{
// Migrated to .NET Core (as FormCollectionExtensions)
public static class FormDataCollectionExtensions
{
/// <summary>

View File

@@ -25,7 +25,10 @@ namespace Umbraco.Web.Mvc
//we have no choice but to instantiate the controller
var instance = factory.CreateController(requestContext, controllerName);
return instance?.GetType();
var controllerType = instance?.GetType();
factory.ReleaseController(instance);
return controllerType;
}
}
}

View File

@@ -82,7 +82,10 @@ namespace Umbraco.Web.Mvc
//we have no choice but to instantiate the controller
var instance = factory.CreateController(requestContext, controllerName);
return instance?.GetType();
var controllerType = instance?.GetType();
factory.ReleaseController(instance);
return controllerType;
}
return GetControllerType(requestContext, controllerName);

View File

@@ -4,6 +4,7 @@ using System.Web.Mvc;
using Umbraco.Core;
using Umbraco.Web.Composing;
using Umbraco.Core.Configuration;
using Umbraco.Web.Security;
namespace Umbraco.Web.Mvc
{
@@ -12,25 +13,23 @@ namespace Umbraco.Web.Mvc
public sealed class UmbracoAuthorizeAttribute : AuthorizeAttribute
{
// see note in HttpInstallAuthorizeAttribute
private readonly IUmbracoContext _umbracoContext;
private readonly IWebSecurity _webSecurity;
private readonly IRuntimeState _runtimeState;
private readonly string _redirectUrl;
private IRuntimeState RuntimeState => _runtimeState ?? Current.RuntimeState;
private IUmbracoContext UmbracoContext => _umbracoContext ?? Current.UmbracoContext;
private IWebSecurity WebSecurity => _webSecurity ?? Current.UmbracoContext.Security;
/// <summary>
/// THIS SHOULD BE ONLY USED FOR UNIT TESTS
/// </summary>
/// <param name="umbracoContext"></param>
/// <param name="webSecurity"></param>
/// <param name="runtimeState"></param>
public UmbracoAuthorizeAttribute(IUmbracoContext umbracoContext, IRuntimeState runtimeState)
public UmbracoAuthorizeAttribute(IWebSecurity webSecurity, IRuntimeState runtimeState)
{
if (umbracoContext == null) throw new ArgumentNullException(nameof(umbracoContext));
if (runtimeState == null) throw new ArgumentNullException(nameof(runtimeState));
_umbracoContext = umbracoContext;
_runtimeState = runtimeState;
_webSecurity = webSecurity ?? throw new ArgumentNullException(nameof(webSecurity));
_runtimeState = runtimeState ?? throw new ArgumentNullException(nameof(runtimeState));
}
/// <summary>
@@ -75,7 +74,7 @@ namespace Umbraco.Web.Mvc
// otherwise we need to ensure that a user is logged in
return RuntimeState.Level == RuntimeLevel.Install
|| RuntimeState.Level == RuntimeLevel.Upgrade
|| UmbracoContext.Security.ValidateCurrentUser();
|| WebSecurity.ValidateCurrentUser();
}
catch (Exception)
{

View File

@@ -1,292 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Routing;
using System.Web.Mvc;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration;
using Umbraco.Core.Logging;
using Umbraco.Core.Mapping;
using Umbraco.Core.Persistence;
using Umbraco.Core.Services;
using Umbraco.Web.Models.Trees;
using Umbraco.Web.Mvc;
using Umbraco.Web.Routing;
using Umbraco.Web.Services;
using Umbraco.Web.WebApi;
using Umbraco.Web.WebApi.Filters;
using Constants = Umbraco.Core.Constants;
namespace Umbraco.Web.Trees
{
/// <summary>
/// Used to return tree root nodes
/// </summary>
[AngularJsonOnlyConfiguration]
[PluginController("UmbracoTrees")]
public class ApplicationTreeController : UmbracoAuthorizedApiController
{
private readonly ITreeService _treeService;
private readonly ISectionService _sectionService;
public ApplicationTreeController(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor,
ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger,
IRuntimeState runtimeState, ITreeService treeService, ISectionService sectionService, UmbracoMapper umbracoMapper, IPublishedUrlProvider publishedUrlProvider)
: base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoMapper, publishedUrlProvider)
{
_treeService = treeService;
_sectionService = sectionService;
}
/// <summary>
/// Returns the tree nodes for an application
/// </summary>
/// <param name="application">The application to load tree for</param>
/// <param name="tree">An optional single tree alias, if specified will only load the single tree for the request app</param>
/// <param name="queryStrings"></param>
/// <param name="use">Tree use.</param>
/// <returns></returns>
public async Task<TreeRootNode> GetApplicationTrees(string application, string tree, [System.Web.Http.ModelBinding.ModelBinder(typeof(HttpQueryStringModelBinder))]FormDataCollection queryStrings, TreeUse use = TreeUse.Main)
{
application = application.CleanForXss();
if (string.IsNullOrEmpty(application))
throw new HttpResponseException(HttpStatusCode.NotFound);
var section = _sectionService.GetByAlias(application);
if (section == null)
throw new HttpResponseException(HttpStatusCode.NotFound);
//find all tree definitions that have the current application alias
var groupedTrees = _treeService.GetBySectionGrouped(application, use);
var allTrees = groupedTrees.Values.SelectMany(x => x).ToList();
if (allTrees.Count == 0)
{
//if there are no trees defined for this section but the section is defined then we can have a simple
//full screen section without trees
var name = Services.TextService.Localize("sections/" + application);
return TreeRootNode.CreateSingleTreeRoot(Constants.System.RootString, null, null, name, TreeNodeCollection.Empty, true);
}
// handle request for a specific tree / or when there is only one tree
if (!tree.IsNullOrWhiteSpace() || allTrees.Count == 1)
{
var t = tree.IsNullOrWhiteSpace()
? allTrees[0]
: allTrees.FirstOrDefault(x => x.TreeAlias == tree);
if (t == null)
throw new HttpResponseException(HttpStatusCode.NotFound);
var treeRootNode = await GetTreeRootNode(t, Constants.System.Root, queryStrings);
if (treeRootNode != null)
return treeRootNode;
throw new HttpResponseException(HttpStatusCode.NotFound);
}
// handle requests for all trees
// for only 1 group
if (groupedTrees.Count == 1)
{
var nodes = new TreeNodeCollection();
foreach (var t in allTrees)
{
var node = await TryGetRootNode(t, queryStrings);
if (node != null)
nodes.Add(node);
}
var name = Services.TextService.Localize("sections/" + application);
if (nodes.Count > 0)
{
var treeRootNode = TreeRootNode.CreateMultiTreeRoot(nodes);
treeRootNode.Name = name;
return treeRootNode;
}
// otherwise it's a section with all empty trees, aka a fullscreen section
// todo is this true? what if we just failed to TryGetRootNode on all of them? SD: Yes it's true but we should check the result of TryGetRootNode and throw?
return TreeRootNode.CreateSingleTreeRoot(Constants.System.RootString, null, null, name, TreeNodeCollection.Empty, true);
}
// for many groups
var treeRootNodes = new List<TreeRootNode>();
foreach (var (groupName, trees) in groupedTrees)
{
var nodes = new TreeNodeCollection();
foreach (var t in trees)
{
var node = await TryGetRootNode(t, queryStrings);
if (node != null)
nodes.Add(node);
}
if (nodes.Count == 0)
continue;
// no name => third party
// use localization key treeHeaders/thirdPartyGroup
// todo this is an odd convention
var name = groupName.IsNullOrWhiteSpace() ? "thirdPartyGroup" : groupName;
var groupRootNode = TreeRootNode.CreateGroupNode(nodes, application);
groupRootNode.Name = Services.TextService.Localize("treeHeaders/" + name);
treeRootNodes.Add(groupRootNode);
}
return TreeRootNode.CreateGroupedMultiTreeRoot(new TreeNodeCollection(treeRootNodes.OrderBy(x => x.Name)));
}
/// <summary>
/// Tries to get the root node of a tree.
/// </summary>
/// <remarks>
/// <para>Returns null if the root node could not be obtained due to an HttpResponseException,
/// which probably indicates that the user isn't authorized to view that tree.</para>
/// </remarks>
private async Task<TreeNode> TryGetRootNode(Tree tree, FormDataCollection querystring)
{
if (tree == null) throw new ArgumentNullException(nameof(tree));
try
{
return await GetRootNode(tree, querystring);
}
catch (HttpResponseException)
{
// if this occurs its because the user isn't authorized to view that tree,
// in this case since we are loading multiple trees we will just return
// null so that it's not added to the list.
return null;
}
}
/// <summary>
/// Get the tree root node of a tree.
/// </summary>
private async Task<TreeRootNode> GetTreeRootNode(Tree tree, int id, FormDataCollection querystring)
{
if (tree == null) throw new ArgumentNullException(nameof(tree));
var children = await GetChildren(tree, id, querystring);
var rootNode = await GetRootNode(tree, querystring);
var sectionRoot = TreeRootNode.CreateSingleTreeRoot(
Constants.System.RootString,
rootNode.ChildNodesUrl,
rootNode.MenuUrl,
rootNode.Name,
children,
tree.IsSingleNodeTree);
// assign the route path based on the root node, this means it will route there when the
// section is navigated to and no dashboards will be available for this section
sectionRoot.RoutePath = rootNode.RoutePath;
sectionRoot.Path = rootNode.Path;
foreach (var d in rootNode.AdditionalData)
sectionRoot.AdditionalData[d.Key] = d.Value;
return sectionRoot;
}
/// <summary>
/// Gets the root node of a tree.
/// </summary>
private async Task<TreeNode> GetRootNode(Tree tree, FormDataCollection querystring)
{
if (tree == null) throw new ArgumentNullException(nameof(tree));
var controller = (TreeController) await GetApiControllerProxy(tree.TreeControllerType, "GetRootNode", querystring);
var rootNode = controller.GetRootNode(querystring);
if (rootNode == null)
throw new InvalidOperationException($"Failed to get root node for tree \"{tree.TreeAlias}\".");
return rootNode;
}
/// <summary>
/// Get the child nodes of a tree node.
/// </summary>
private async Task<TreeNodeCollection> GetChildren(Tree tree, int id, FormDataCollection querystring)
{
if (tree == null) throw new ArgumentNullException(nameof(tree));
// the method we proxy has an 'id' parameter which is *not* in the querystring,
// we need to add it for the proxy to work (else, it does not find the method,
// when trying to run auth filters etc).
var d = querystring?.ToDictionary(x => x.Key, x => x.Value) ?? new Dictionary<string, string>();
d["id"] = null;
var proxyQuerystring = new FormDataCollection(d);
var controller = (TreeController) await GetApiControllerProxy(tree.TreeControllerType, "GetNodes", proxyQuerystring);
return controller.GetNodes(id.ToInvariantString(), querystring);
}
/// <summary>
/// Gets a proxy to a controller for a specified action.
/// </summary>
/// <param name="controllerType">The type of the controller.</param>
/// <param name="action">The action.</param>
/// <param name="querystring">The querystring.</param>
/// <returns>An instance of the controller.</returns>
/// <remarks>
/// <para>Creates an instance of the <paramref name="controllerType"/> and initializes it with a route
/// and context etc. so it can execute the specified <paramref name="action"/>. Runs the authorization
/// filters for that action, to ensure that the user has permission to execute it.</para>
/// </remarks>
private async Task<object> GetApiControllerProxy(Type controllerType, string action, FormDataCollection querystring)
{
// note: this is all required in order to execute the auth-filters for the sub request, we
// need to "trick" web-api into thinking that it is actually executing the proxied controller.
var context = ControllerContext;
// get the controller
var controller = (ApiController) DependencyResolver.Current.GetService(controllerType)
?? throw new Exception($"Failed to create controller of type {controllerType.FullName}.");
// create the proxy URL for the controller action
var proxyUrl = context.Request.RequestUri.GetLeftPart(UriPartial.Authority)
+ context.Request.GetUrlHelper().GetUmbracoApiService(action, controllerType)
+ "?" + querystring.ToQueryString();
// create proxy route data specifying the action & controller to execute
var proxyRoute = new HttpRouteData(
context.RouteData.Route,
new HttpRouteValueDictionary(new { action, controller = ControllerExtensions.GetControllerName(controllerType) }));
// create a proxy request
var proxyRequest = new HttpRequestMessage(HttpMethod.Get, proxyUrl);
// create a proxy controller context
var proxyContext = new HttpControllerContext(context.Configuration, proxyRoute, proxyRequest)
{
ControllerDescriptor = new HttpControllerDescriptor(context.ControllerDescriptor.Configuration, ControllerExtensions.GetControllerName(controllerType), controllerType),
RequestContext = context.RequestContext,
Controller = controller
};
// wire everything
controller.ControllerContext = proxyContext;
controller.Request = proxyContext.Request;
controller.RequestContext.RouteData = proxyRoute;
// auth
var authResult = await controller.ControllerContext.InvokeAuthorizationFiltersForRequest();
if (authResult != null)
throw new HttpResponseException(authResult);
return controller;
}
}
}

View File

@@ -1,45 +0,0 @@
using System.Net.Http.Formatting;
using Umbraco.Core;
using Umbraco.Web.Models.Trees;
using Umbraco.Web.Mvc;
using Umbraco.Web.WebApi.Filters;
using Constants = Umbraco.Core.Constants;
namespace Umbraco.Web.Trees
{
[UmbracoTreeAuthorize(Constants.Trees.Languages)]
[Tree(Constants.Applications.Settings, Constants.Trees.Languages, SortOrder = 11, TreeGroup = Constants.Trees.Groups.Settings)]
[PluginController("UmbracoTrees")]
[CoreTree]
public class LanguageTreeController : TreeController
{
protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings)
{
//We don't have any child nodes & only use the root node to load a custom UI
return new TreeNodeCollection();
}
protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings)
{
//We don't have any menu item options (such as create/delete/reload) & only use the root node to load a custom UI
return null;
}
/// <summary>
/// Helper method to create a root model for a tree
/// </summary>
/// <returns></returns>
protected override TreeNode CreateRootNode(FormDataCollection queryStrings)
{
var root = base.CreateRootNode(queryStrings);
//this will load in a custom UI instead of the dashboard for the root node
root.RoutePath = $"{Constants.Applications.Settings}/{Constants.Trees.Languages}/overview";
root.Icon = "icon-globe";
root.HasChildren = false;
root.MenuUrl = null;
return root;
}
}
}

View File

@@ -3,6 +3,7 @@ using Umbraco.Web.Models.Trees;
namespace Umbraco.Web.Trees
{
// Migrated to .NET Core
public class MenuRenderingEventArgs : TreeRenderingEventArgs
{
/// <summary>

View File

@@ -5,6 +5,7 @@ namespace Umbraco.Web.Trees
/// <summary>
/// Identifies a section tree.
/// </summary>
/// // Migrated to .NET Core
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class TreeAttribute : Attribute, ITree
{

View File

@@ -1,57 +0,0 @@
using System;
using System.Collections.Generic;
using Umbraco.Core;
using Umbraco.Core.Composing;
namespace Umbraco.Web.Trees
{
/// <summary>
/// Builds a <see cref="TreeCollection"/>.
/// </summary>
public class TreeCollectionBuilder : ICollectionBuilder<TreeCollection, Tree>
{
private readonly List<Tree> _trees = new List<Tree>();
public TreeCollection CreateCollection(IFactory factory) => new TreeCollection(_trees);
public void RegisterWith(IRegister register) => register.Register(CreateCollection, Lifetime.Singleton);
/// <summary>
/// Registers a custom tree definition
/// </summary>
/// <param name="treeDefinition"></param>
/// <remarks>
/// This is useful if a developer wishes to have a single tree controller for different tree aliases. In this case the tree controller
/// cannot be decorated with the TreeAttribute (since then it will be auto-registered).
/// </remarks>
public void AddTree(Tree treeDefinition)
{
if (treeDefinition == null) throw new ArgumentNullException(nameof(treeDefinition));
_trees.Add(treeDefinition);
}
public void AddTreeController<TController>()
where TController : TreeControllerBase
=> AddTreeController(typeof(TController));
public void AddTreeController(Type controllerType)
{
if (!typeof(TreeControllerBase).IsAssignableFrom(controllerType))
throw new ArgumentException($"Type {controllerType} does not inherit from {typeof(TreeControllerBase).FullName}.");
// not all TreeControllerBase are meant to be used here,
// ignore those that don't have the attribute
var attribute = controllerType.GetCustomAttribute<TreeAttribute>(false);
if (attribute == null) return;
var tree = new Tree(attribute.SortOrder, attribute.SectionAlias, attribute.TreeGroup, attribute.TreeAlias, attribute.TreeTitle, attribute.TreeUse, controllerType, attribute.IsSingleNodeTree);
_trees.Add(tree);
}
public void AddTreeControllers(IEnumerable<Type> controllerTypes)
{
foreach (var controllerType in controllerTypes)
AddTreeController(controllerType);
}
}
}

View File

@@ -14,6 +14,7 @@ namespace Umbraco.Web.Trees
/// <summary>
/// The base controller for all tree requests
/// </summary>
/// // Migrated to .NET Core
public abstract class TreeController : TreeControllerBase
{
private static readonly ConcurrentDictionary<Type, TreeAttribute> TreeAttributeCache = new ConcurrentDictionary<Type, TreeAttribute>();

View File

@@ -27,6 +27,7 @@ namespace Umbraco.Web.Trees
/// <remarks>
/// Developers should generally inherit from TreeController.
/// </remarks>
/// Migrated to .NET Core
[AngularJsonOnlyConfiguration]
public abstract class TreeControllerBase : UmbracoAuthorizedApiController, ITree
{

View File

@@ -3,6 +3,7 @@ using Umbraco.Web.Models.Trees;
namespace Umbraco.Web.Trees
{
// Migrated to .NET Core
public class TreeNodeRenderingEventArgs : TreeRenderingEventArgs
{
public TreeNode Node { get; private set; }

View File

@@ -3,6 +3,7 @@ using Umbraco.Web.Models.Trees;
namespace Umbraco.Web.Trees
{
// Migrated to .NET Core
public class TreeNodesRenderingEventArgs : TreeRenderingEventArgs
{
public TreeNodeCollection Nodes { get; private set; }

View File

@@ -3,6 +3,7 @@
/// <summary>
/// Common query string parameters used for tree query strings
/// </summary>
/// Migrated to .NET Core
internal struct TreeQueryStringParameters
{
public const string Use = "use";

View File

@@ -3,6 +3,7 @@ using System.Net.Http.Formatting;
namespace Umbraco.Web.Trees
{
// Migrated to .NET Core
public class TreeRenderingEventArgs : EventArgs
{
public FormDataCollection QueryStrings { get; private set; }

View File

@@ -8,6 +8,7 @@ using Umbraco.Core;
namespace Umbraco.Web.Trees
{
// Migrated to .NET Core
public static class UrlHelperExtensions
{
public static string GetTreeUrl(this UrlHelper urlHelper, Type treeType, string nodeId, FormDataCollection queryStrings)

View File

@@ -144,6 +144,7 @@
<Compile Include="Compose\AuditEventsComposer.cs" />
<Compile Include="Compose\BackOfficeUserAuditEventsComponent.cs" />
<Compile Include="Compose\BackOfficeUserAuditEventsComposer.cs" />
<Compile Include="Compose\NestedContentPropertyComponent.cs" />
<Compile Include="Composing\LightInject\LightInjectContainer.cs" />
<Compile Include="Macros\MacroRenderer.cs" />
<Compile Include="Macros\MemberUserKeyProvider.cs" />
@@ -159,7 +160,6 @@
<Compile Include="Editors\Filters\ContentSaveModelValidator.cs" />
<Compile Include="Editors\Filters\MediaSaveModelValidator.cs" />
<Compile Include="Editors\MacrosController.cs" />
<Compile Include="Editors\TinyMceController.cs" />
<Compile Include="HttpContextAccessorExtensions.cs" />
<Compile Include="HttpContextExtensions.cs" />
<Compile Include="ImageCropperTemplateCoreExtensions.cs" />
@@ -185,6 +185,7 @@
<Compile Include="AspNet\AspNetHttpContextAccessor.cs" />
<Compile Include="AspNet\AspNetIpResolver.cs" />
<Compile Include="AspNet\AspNetPasswordHasher.cs" />
<Compile Include="Compose\NestedContentPropertyComposer.cs" />
<Compile Include="RoutableDocumentFilter.cs" />
<Compile Include="Runtime\AspNetUmbracoBootPermissionChecker.cs" />
<Compile Include="Security\BackOfficeSignInManager.cs" />
@@ -201,7 +202,6 @@
<Compile Include="Security\UserAwarePasswordHasher.cs" />
<Compile Include="StringExtensions.cs" />
<Compile Include="Trees\ITreeNodeController.cs" />
<Compile Include="Trees\TreeCollectionBuilder.cs" />
<Compile Include="UmbracoContext.cs" />
<Compile Include="UmbracoContextFactory.cs" />
<Compile Include="Mvc\UmbracoVirtualNodeByUdiRouteHandler.cs" />
@@ -231,7 +231,6 @@
<Compile Include="WebApi\HttpActionContextExtensions.cs" />
<Compile Include="WebApi\SerializeVersionAttribute.cs" />
<Compile Include="WebApi\TrimModelBinder.cs" />
<Compile Include="Editors\DictionaryController.cs" />
<Compile Include="Editors\Filters\MemberSaveModelValidator.cs" />
<Compile Include="Editors\FromJsonPathAttribute.cs" />
<Compile Include="Editors\Filters\IsCurrentUserModelFilterAttribute.cs" />
@@ -330,7 +329,6 @@
<Compile Include="Mvc\UmbracoViewPage.cs" />
<Compile Include="Editors\MacroRenderingController.cs" />
<Compile Include="Editors\MemberTypeController.cs" />
<Compile Include="Editors\UpdateCheckController.cs" />
<Compile Include="Editors\EntityController.cs" />
<Compile Include="Editors\MemberController.cs" />
<Compile Include="Editors\CurrentUserController.cs" />
@@ -342,7 +340,6 @@
<Compile Include="Security\UmbracoBackOfficeCookieAuthOptions.cs" />
<Compile Include="Trees\DataTypeTreeController.cs" />
<Compile Include="Trees\FileSystemTreeController.cs" />
<Compile Include="Trees\LanguageTreeController.cs" />
<Compile Include="Trees\MemberTreeController.cs" />
<Compile Include="Trees\MenuRenderingEventArgs.cs" />
<Compile Include="Trees\TemplatesTreeController.cs" />
@@ -373,7 +370,6 @@
<Compile Include="Models\RegisterModel.cs" />
<Compile Include="Editors\MediaTypeController.cs" />
<Compile Include="Security\MembershipHelper.cs" />
<Compile Include="Editors\SectionController.cs" />
<Compile Include="Editors\UmbracoAuthorizedJsonController.cs" />
<Compile Include="HttpCookieExtensions.cs" />
<Compile Include="FormDataCollectionExtensions.cs" />
@@ -383,7 +379,6 @@
<Compile Include="Trees\TreeNodeRenderingEventArgs.cs" />
<Compile Include="Trees\TreeNodesRenderingEventArgs.cs" />
<Compile Include="Trees\TreeQueryStringParameters.cs" />
<Compile Include="Trees\ApplicationTreeController.cs" />
<Compile Include="Editors\BackOfficeController.cs" />
<Compile Include="Security\Providers\MembersMembershipProvider.cs" />
<Compile Include="Security\Providers\MembersRoleProvider.cs" />

View File

@@ -19,24 +19,22 @@ namespace Umbraco.Web.WebApi
internal static bool Enable = true;
// TODO: inject!
private readonly IUmbracoContext _umbracoContext;
private readonly IWebSecurity _webSecurity;
private readonly IRuntimeState _runtimeState;
private IRuntimeState RuntimeState => _runtimeState ?? Current.RuntimeState;
private IUmbracoContext UmbracoContext => _umbracoContext ?? Current.UmbracoContext;
private IWebSecurity WebSecurity => _webSecurity ?? Current.UmbracoContext.Security;
/// <summary>
/// THIS SHOULD BE ONLY USED FOR UNIT TESTS
/// </summary>
/// <param name="umbracoContext"></param>
/// <param name="webSecurity"></param>
/// <param name="runtimeState"></param>
public UmbracoAuthorizeAttribute(IUmbracoContext umbracoContext, IRuntimeState runtimeState)
public UmbracoAuthorizeAttribute(IWebSecurity webSecurity, IRuntimeState runtimeState)
{
if (umbracoContext == null) throw new ArgumentNullException(nameof(umbracoContext));
if (runtimeState == null) throw new ArgumentNullException(nameof(runtimeState));
_umbracoContext = umbracoContext;
_runtimeState = runtimeState;
_webSecurity = webSecurity ?? throw new ArgumentNullException(nameof(webSecurity));
_runtimeState = runtimeState ?? throw new ArgumentNullException(nameof(runtimeState));
}
public UmbracoAuthorizeAttribute() : this(true)
@@ -60,7 +58,7 @@ namespace Umbraco.Web.WebApi
// otherwise we need to ensure that a user is logged in
return RuntimeState.Level == RuntimeLevel.Install
|| RuntimeState.Level == RuntimeLevel.Upgrade
|| UmbracoContext.Security.ValidateCurrentUser(false, _requireApproval) == ValidateRequestAttempt.Success;
|| WebSecurity.ValidateCurrentUser(false, _requireApproval) == ValidateRequestAttempt.Success;
}
catch (Exception)
{