Merge remote-tracking branch 'origin/netcore/netcore' into netcore/feature/identity

# Conflicts:
#	src/Umbraco.Web.BackOffice/Controllers/BackOfficeController.cs
#	src/Umbraco.Web/Editors/BackOfficeController.cs
This commit is contained in:
Shannon
2020-05-21 15:46:43 +10:00
49 changed files with 1242 additions and 446 deletions

View File

@@ -1,6 +1,9 @@
using Microsoft.AspNetCore.Mvc;
using Umbraco.Core;
using Umbraco.Web.Common.Attributes;
using Umbraco.Web.Common.Controllers;
using Umbraco.Web.Common.Filters;
using Umbraco.Web.Security;
using Constants = Umbraco.Core.Constants;
namespace Umbraco.Web.BackOffice.Controllers
@@ -9,8 +12,30 @@ namespace Umbraco.Web.BackOffice.Controllers
//[ValidationFilter] // TODO: I don't actually think this is required with our custom Application Model conventions applied
[TypeFilter(typeof(AngularJsonOnlyConfigurationAttribute))] // TODO: This could be applied with our Application Model conventions
[IsBackOffice] // TODO: This could be applied with our Application Model conventions
public class AuthenticationController : ControllerBase
public class AuthenticationController : UmbracoApiController
{
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
// TODO: We need to import the logic from Umbraco.Web.Editors.AuthenticationController and it should not be an auto-routed api controller
public AuthenticationController(IUmbracoContextAccessor umbracoContextAccessor)
{
_umbracoContextAccessor = umbracoContextAccessor;
}
/// <summary>
/// Checks if the current user's cookie is valid and if so returns OK or a 400 (BadRequest)
/// </summary>
/// <returns></returns>
[HttpGet]
public bool IsAuthenticated()
{
var umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext();
var attempt = umbracoContext.Security.AuthorizeRequest();
if (attempt == ValidateRequestAttempt.Success)
{
return true;
}
return false;
}
}
}

View File

@@ -0,0 +1,47 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Core.Configuration;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Web.Common.Attributes;
namespace Umbraco.Web.Editors
{
[PluginController("UmbracoApi")]
public class BackOfficeAssetsController : UmbracoAuthorizedJsonController
{
private readonly IFileSystem _jsLibFileSystem;
public BackOfficeAssetsController(IIOHelper ioHelper, ILogger logger, IGlobalSettings globalSettings)
{
_jsLibFileSystem = new PhysicalFileSystem(ioHelper, logger, globalSettings.UmbracoPath + Path.DirectorySeparatorChar + "lib");
}
[HttpGet]
public object GetSupportedLocales()
{
const string momentLocaleFolder = "moment";
const string flatpickrLocaleFolder = "flatpickr/l10n";
return new
{
moment = GetLocales(momentLocaleFolder),
flatpickr = GetLocales(flatpickrLocaleFolder)
};
}
private IEnumerable<string> GetLocales(string path)
{
var cultures = _jsLibFileSystem.GetFiles(path, "*.js").ToList();
for (var i = 0; i < cultures.Count; i++)
{
cultures[i] = cultures[i]
.Substring(cultures[i].IndexOf(path, StringComparison.Ordinal) + path.Length + 1);
}
return cultures;
}
}
}

View File

@@ -33,7 +33,6 @@ namespace Umbraco.Web.BackOffice.Controllers
private readonly IRuntimeMinifier _runtimeMinifier;
private readonly IGlobalSettings _globalSettings;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IUmbracoApplicationLifetime _umbracoApplicationLifetime;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly ILocalizedTextService _textService;
private readonly IGridConfig _gridConfig;
@@ -46,7 +45,7 @@ namespace Umbraco.Web.BackOffice.Controllers
IRuntimeMinifier runtimeMinifier,
IGlobalSettings globalSettings,
IHostingEnvironment hostingEnvironment,
IUmbracoApplicationLifetime umbracoApplicationLifetime,
IUmbracoContextAccessor umbracoContextAccessor,
ILocalizedTextService textService,
IGridConfig gridConfig,
@@ -59,7 +58,6 @@ namespace Umbraco.Web.BackOffice.Controllers
_runtimeMinifier = runtimeMinifier;
_globalSettings = globalSettings;
_hostingEnvironment = hostingEnvironment;
_umbracoApplicationLifetime = umbracoApplicationLifetime;
_umbracoContextAccessor = umbracoContextAccessor;
_textService = textService;
_gridConfig = gridConfig ?? throw new ArgumentNullException(nameof(gridConfig));
@@ -130,7 +128,7 @@ namespace Umbraco.Web.BackOffice.Controllers
return new JsonNetResult { Data = nestedDictionary, Formatting = Formatting.None };
}
[UmbracoAuthorize(Order = 0)] // TODO: Re-implement UmbracoAuthorizeAttribute
[UmbracoAuthorize(Order = 0)]
[HttpGet]
public JsonNetResult GetGridConfig()
{
@@ -223,5 +221,7 @@ namespace Umbraco.Web.BackOffice.Controllers
}
return Redirect("/");
}
}
}

View File

@@ -0,0 +1,234 @@
using System.Collections.Generic;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Web.Models.ContentEditing;
using Newtonsoft.Json.Linq;
using System.Threading.Tasks;
using System.Net.Http;
using System;
using System.Linq;
using System.Text;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Core.Cache;
using Umbraco.Core.Logging;
using Umbraco.Core.Persistence;
using Umbraco.Core.Services;
using Umbraco.Core.Strings;
using Umbraco.Core.Dashboards;
using Umbraco.Web.Services;
using Umbraco.Web.BackOffice.Filters;
using Umbraco.Web.Common.Attributes;
using Umbraco.Web.Common.Controllers;
using Umbraco.Web.Common.Filters;
using Umbraco.Web.WebApi.Filters;
namespace Umbraco.Web.BackOffice.Controllers
{
//we need to fire up the controller like this to enable loading of remote css directly from this controller
[PluginController("UmbracoApi")]
[ValidationFilter]
[TypeFilter(typeof(AngularJsonOnlyConfigurationAttribute))] // TODO: This could be applied with our Application Model conventions
[IsBackOffice]
[UmbracoAuthorize]
public class DashboardController : UmbracoApiController
{
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly AppCaches _appCaches;
private readonly ILogger _logger;
private readonly IDashboardService _dashboardService;
private readonly IUmbracoVersion _umbracoVersion;
private readonly IShortStringHelper _shortStringHelper;
/// <summary>
/// Initializes a new instance of the <see cref="DashboardController"/> with all its dependencies.
/// </summary>
public DashboardController(
IGlobalSettings globalSettings,
IUmbracoContextAccessor umbracoContextAccessor,
ISqlContext sqlContext,
ServiceContext services,
AppCaches appCaches,
ILogger logger,
IRuntimeState runtimeState,
IDashboardService dashboardService,
IUmbracoVersion umbracoVersion,
IShortStringHelper shortStringHelper)
{
_umbracoContextAccessor = umbracoContextAccessor;
_appCaches = appCaches;
_logger = logger;
_dashboardService = dashboardService;
_umbracoVersion = umbracoVersion;
_shortStringHelper = shortStringHelper;
}
//we have just one instance of HttpClient shared for the entire application
private static readonly HttpClient HttpClient = new HttpClient();
//we have baseurl as a param to make previewing easier, so we can test with a dev domain from client side
[TypeFilter(typeof(ValidateAngularAntiForgeryTokenAttribute))]
public async Task<JObject> GetRemoteDashboardContent(string section, string baseUrl = "https://dashboard.umbraco.org/")
{
var user = _umbracoContextAccessor.GetRequiredUmbracoContext().Security.CurrentUser;
var allowedSections = string.Join(",", user.AllowedSections);
var language = user.Language;
var version = _umbracoVersion.SemanticVersion.ToSemanticString();
var url = string.Format(baseUrl + "{0}?section={0}&allowed={1}&lang={2}&version={3}", section, allowedSections, language, version);
var key = "umbraco-dynamic-dashboard-" + language + allowedSections.Replace(",", "-") + section;
var content = _appCaches.RuntimeCache.GetCacheItem<JObject>(key);
var result = new JObject();
if (content != null)
{
result = content;
}
else
{
//content is null, go get it
try
{
//fetch dashboard json and parse to JObject
var json = await HttpClient.GetStringAsync(url);
content = JObject.Parse(json);
result = content;
_appCaches.RuntimeCache.InsertCacheItem<JObject>(key, () => result, new TimeSpan(0, 30, 0));
}
catch (HttpRequestException ex)
{
_logger.Error<DashboardController>(ex.InnerException ?? ex, "Error getting dashboard content from {Url}", url);
//it's still new JObject() - we return it like this to avoid error codes which triggers UI warnings
_appCaches.RuntimeCache.InsertCacheItem<JObject>(key, () => result, new TimeSpan(0, 5, 0));
}
}
return result;
}
public async Task<IActionResult> GetRemoteDashboardCss(string section, string baseUrl = "https://dashboard.umbraco.org/")
{
var url = string.Format(baseUrl + "css/dashboard.css?section={0}", section);
var key = "umbraco-dynamic-dashboard-css-" + section;
var content = _appCaches.RuntimeCache.GetCacheItem<string>(key);
var result = string.Empty;
if (content != null)
{
result = content;
}
else
{
//content is null, go get it
try
{
//fetch remote css
content = await HttpClient.GetStringAsync(url);
//can't use content directly, modified closure problem
result = content;
//save server content for 30 mins
_appCaches.RuntimeCache.InsertCacheItem<string>(key, () => result, new TimeSpan(0, 30, 0));
}
catch (HttpRequestException ex)
{
_logger.Error<DashboardController>(ex.InnerException ?? ex, "Error getting dashboard CSS from {Url}", url);
//it's still string.Empty - we return it like this to avoid error codes which triggers UI warnings
_appCaches.RuntimeCache.InsertCacheItem<string>(key, () => result, new TimeSpan(0, 5, 0));
}
}
return Content(result,"text/css", Encoding.UTF8);
}
public async Task<IActionResult> GetRemoteXml(string site, string url)
{
// This is used in place of the old feedproxy.config
// Which was used to grab data from our.umbraco.com, umbraco.com or umbraco.tv
// for certain dashboards or the help drawer
var urlPrefix = string.Empty;
switch (site.ToUpper())
{
case "TV":
urlPrefix = "https://umbraco.tv/";
break;
case "OUR":
urlPrefix = "https://our.umbraco.com/";
break;
case "COM":
urlPrefix = "https://umbraco.com/";
break;
default:
return NotFound();
}
//Make remote call to fetch videos or remote dashboard feed data
var key = $"umbraco-XML-feed-{site}-{url.ToCleanString(_shortStringHelper, CleanStringType.UrlSegment)}";
var content = _appCaches.RuntimeCache.GetCacheItem<string>(key);
var result = string.Empty;
if (content != null)
{
result = content;
}
else
{
//content is null, go get it
try
{
//fetch remote css
content = await HttpClient.GetStringAsync($"{urlPrefix}{url}");
//can't use content directly, modified closure problem
result = content;
//save server content for 30 mins
_appCaches.RuntimeCache.InsertCacheItem<string>(key, () => result, new TimeSpan(0, 30, 0));
}
catch (HttpRequestException ex)
{
_logger.Error<DashboardController>(ex.InnerException ?? ex, "Error getting remote dashboard data from {UrlPrefix}{Url}", urlPrefix, url);
//it's still string.Empty - we return it like this to avoid error codes which triggers UI warnings
_appCaches.RuntimeCache.InsertCacheItem<string>(key, () => result, new TimeSpan(0, 5, 0));
}
}
return Content(result,"text/xml", Encoding.UTF8);
}
// return IDashboardSlim - we don't need sections nor access rules
[TypeFilter(typeof(ValidateAngularAntiForgeryTokenAttribute))]
[TypeFilter(typeof(OutgoingEditorModelEventAttribute))]
public IEnumerable<Tab<IDashboardSlim>> GetDashboard(string section)
{
var currentUser = _umbracoContextAccessor.GetRequiredUmbracoContext().Security.CurrentUser;
return _dashboardService.GetDashboards(section, currentUser).Select(x => new Tab<IDashboardSlim>
{
Id = x.Id,
Alias = x.Alias,
Label = x.Label,
Expanded = x.Expanded,
IsActive = x.IsActive,
Properties = x.Properties.Select(y => new DashboardSlim
{
Alias = y.Alias,
View = y.View
})
}).ToList();
}
}
}

View File

@@ -0,0 +1,275 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using Examine;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Examine;
using Umbraco.Extensions;
using Umbraco.Web.Common.Attributes;
using Umbraco.Web.Common.Exceptions;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Search;
using SearchResult = Umbraco.Web.Models.ContentEditing.SearchResult;
namespace Umbraco.Web.Editors
{
[PluginController("UmbracoApi")]
public class ExamineManagementController : UmbracoAuthorizedJsonController
{
private readonly IExamineManager _examineManager;
private readonly ILogger _logger;
private readonly IIOHelper _ioHelper;
private readonly IIndexDiagnosticsFactory _indexDiagnosticsFactory;
private readonly IAppPolicyCache _runtimeCache;
private readonly IndexRebuilder _indexRebuilder;
public ExamineManagementController(IExamineManager examineManager, ILogger logger, IIOHelper ioHelper, IIndexDiagnosticsFactory indexDiagnosticsFactory,
AppCaches appCaches,
IndexRebuilder indexRebuilder)
{
_examineManager = examineManager;
_logger = logger;
_ioHelper = ioHelper;
_indexDiagnosticsFactory = indexDiagnosticsFactory;
_runtimeCache = appCaches.RuntimeCache;
_indexRebuilder = indexRebuilder;
}
/// <summary>
/// Get the details for indexers
/// </summary>
/// <returns></returns>
public IEnumerable<ExamineIndexModel> GetIndexerDetails()
{
return _examineManager.Indexes.Select(CreateModel).OrderBy(x => x.Name.TrimEnd("Indexer"));
}
/// <summary>
/// Get the details for searchers
/// </summary>
/// <returns></returns>
public IEnumerable<ExamineSearcherModel> GetSearcherDetails()
{
var model = new List<ExamineSearcherModel>(
_examineManager.RegisteredSearchers.Select(searcher => new ExamineSearcherModel { Name = searcher.Name })
.OrderBy(x => x.Name.TrimEnd("Searcher"))); //order by name , but strip the "Searcher" from the end if it exists
return model;
}
public SearchResults GetSearchResults(string searcherName, string query, int pageIndex = 0, int pageSize = 20)
{
if (query.IsNullOrWhiteSpace())
return SearchResults.Empty();
var msg = ValidateSearcher(searcherName, out var searcher);
if (!msg.IsSuccessStatusCode())
throw new HttpResponseException(msg);
// NativeQuery will work for a single word/phrase too (but depends on the implementation) the lucene one will work.
var results = searcher.CreateQuery().NativeQuery(query).Execute(maxResults: pageSize * (pageIndex + 1));
var pagedResults = results.Skip(pageIndex * pageSize);
return new SearchResults
{
TotalRecords = results.TotalItemCount,
Results = pagedResults.Select(x => new SearchResult
{
Id = x.Id,
Score = x.Score,
//order the values by key
Values = new Dictionary<string, string>(x.Values.OrderBy(y => y.Key).ToDictionary(y => y.Key, y => y.Value))
})
};
}
/// <summary>
/// Check if the index has been rebuilt
/// </summary>
/// <param name="indexName"></param>
/// <returns></returns>
/// <remarks>
/// This is kind of rudimentary since there's no way we can know that the index has rebuilt, we
/// have a listener for the index op complete so we'll just check if that key is no longer there in the runtime cache
/// </remarks>
public ActionResult<ExamineIndexModel> PostCheckRebuildIndex(string indexName)
{
var validate = ValidateIndex(indexName, out var index);
if (!validate.IsSuccessStatusCode())
throw new HttpResponseException(validate);
validate = ValidatePopulator(index);
if (!validate.IsSuccessStatusCode())
throw new HttpResponseException(validate);
var cacheKey = "temp_indexing_op_" + indexName;
var found = _runtimeCache.Get(cacheKey);
//if its still there then it's not done
return found != null
? null
: CreateModel(index);
}
/// <summary>
/// Rebuilds the index
/// </summary>
/// <param name="indexName"></param>
/// <returns></returns>
public IActionResult PostRebuildIndex(string indexName)
{
var validate = ValidateIndex(indexName, out var index);
if (!validate.IsSuccessStatusCode())
throw new HttpResponseException(validate);
validate = ValidatePopulator(index);
if (!validate.IsSuccessStatusCode())
throw new HttpResponseException(validate);
_logger.Info<ExamineManagementController>("Rebuilding index '{IndexName}'", indexName);
//remove it in case there's a handler there already
index.IndexOperationComplete -= Indexer_IndexOperationComplete;
//now add a single handler
index.IndexOperationComplete += Indexer_IndexOperationComplete;
try
{
var cacheKey = "temp_indexing_op_" + index.Name;
//put temp val in cache which is used as a rudimentary way to know when the indexing is done
_runtimeCache.Insert(cacheKey, () => "tempValue", TimeSpan.FromMinutes(5));
_indexRebuilder.RebuildIndex(indexName);
////populate it
//foreach (var populator in _populators.Where(x => x.IsRegistered(indexName)))
// populator.Populate(index);
return new OkResult();
}
catch (Exception ex)
{
//ensure it's not listening
index.IndexOperationComplete -= Indexer_IndexOperationComplete;
_logger.Error<ExamineManagementController>(ex, "An error occurred rebuilding index");
var response = new ConflictObjectResult("The index could not be rebuilt at this time, most likely there is another thread currently writing to the index. Error: {ex}");
SetReasonPhrase(response, "Could Not Rebuild");
return response;
}
}
private ExamineIndexModel CreateModel(IIndex index)
{
var indexName = index.Name;
var indexDiag = _indexDiagnosticsFactory.Create(index);
var isHealth = indexDiag.IsHealthy();
var properties = new Dictionary<string, object>
{
[nameof(IIndexDiagnostics.DocumentCount)] = indexDiag.DocumentCount,
[nameof(IIndexDiagnostics.FieldCount)] = indexDiag.FieldCount,
};
foreach (var p in indexDiag.Metadata)
properties[p.Key] = p.Value;
var indexerModel = new ExamineIndexModel
{
Name = indexName,
HealthStatus = isHealth.Success ? (isHealth.Result ?? "Healthy") : (isHealth.Result ?? "Unhealthy"),
ProviderProperties = properties,
CanRebuild = _indexRebuilder.CanRebuild(index)
};
return indexerModel;
}
private ActionResult ValidateSearcher(string searcherName, out ISearcher searcher)
{
//try to get the searcher from the indexes
if (_examineManager.TryGetIndex(searcherName, out var index))
{
searcher = index.GetSearcher();
return new OkResult();
}
//if we didn't find anything try to find it by an explicitly declared searcher
if (_examineManager.TryGetSearcher(searcherName, out searcher))
return new OkResult();
var response1 = new BadRequestObjectResult($"No searcher found with name = {searcherName}");
SetReasonPhrase(response1, "Searcher Not Found");
return response1;
}
private ActionResult ValidatePopulator(IIndex index)
{
if (_indexRebuilder.CanRebuild(index))
return new OkResult();
var response = new BadRequestObjectResult($"The index {index.Name} cannot be rebuilt because it does not have an associated {typeof(IIndexPopulator)}");
SetReasonPhrase(response, "Index cannot be rebuilt");
return response;
}
private ActionResult ValidateIndex(string indexName, out IIndex index)
{
index = null;
if (_examineManager.TryGetIndex(indexName, out index))
{
//return Ok!
return new OkResult();
}
var response = new BadRequestObjectResult($"No index found with name = {indexName}");
SetReasonPhrase(response, "Index Not Found");
return response;
}
private void SetReasonPhrase(IActionResult response, string reasonPhrase)
{
//TODO we should update this behavior, as HTTP2 do not have ReasonPhrase. Could as well be returned in body
// https://github.com/aspnet/HttpAbstractions/issues/395
var httpResponseFeature = HttpContext.Features.Get<IHttpResponseFeature>();
if (!(httpResponseFeature is null))
{
httpResponseFeature.ReasonPhrase = reasonPhrase;
}
}
private void Indexer_IndexOperationComplete(object sender, EventArgs e)
{
var indexer = (IIndex)sender;
_logger.Debug<ExamineManagementController>("Logging operation completed for index {IndexName}", indexer.Name);
//ensure it's not listening anymore
indexer.IndexOperationComplete -= Indexer_IndexOperationComplete;
_logger
.Info<ExamineManagementController
>($"Rebuilding index '{indexer.Name}' done.");
var cacheKey = "temp_indexing_op_" + indexer.Name;
_runtimeCache.Clear(cacheKey);
}
}
}

View File

@@ -0,0 +1,63 @@
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Net.Http;
using System.Runtime.Serialization;
using System.Threading.Tasks;
using Umbraco.Core.Logging;
using Umbraco.Web.Editors;
namespace Umbraco.Web.BackOffice.Controllers
{
public class HelpController : UmbracoAuthorizedJsonController
{
private readonly ILogger _logger;
public HelpController(ILogger logger)
{
_logger = logger;
}
private static HttpClient _httpClient;
public async Task<List<HelpPage>> GetContextHelpForPage(string section, string tree, string baseUrl = "https://our.umbraco.com")
{
var url = string.Format(baseUrl + "/Umbraco/Documentation/Lessons/GetContextHelpDocs?sectionAlias={0}&treeAlias={1}", section, tree);
try
{
if (_httpClient == null)
_httpClient = new HttpClient();
//fetch dashboard json and parse to JObject
var json = await _httpClient.GetStringAsync(url);
var result = JsonConvert.DeserializeObject<List<HelpPage>>(json);
if (result != null)
return result;
}
catch (HttpRequestException rex)
{
_logger.Info(GetType(), $"Check your network connection, exception: {rex.Message}");
}
return new List<HelpPage>();
}
}
[DataContract(Name = "HelpPage")]
public class HelpPage
{
[DataMember(Name = "name")]
public string Name { get; set; }
[DataMember(Name = "description")]
public string Description { get; set; }
[DataMember(Name = "url")]
public string Url { get; set; }
[DataMember(Name = "type")]
public string Type { get; set; }
}
}

View File

@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Umbraco.Core.Media;
using Umbraco.Core.Models;
using Umbraco.Web.Common.Attributes;
using Umbraco.Web.Models;
using Umbraco.Web.Mvc;
namespace Umbraco.Web.Editors
{
/// <summary>
/// The API controller used for getting URLs for images with parameters
/// </summary>
/// <remarks>
/// <para>
/// This controller allows for retrieving URLs for processed images, such as resized, cropped,
/// or otherwise altered. These can be different based on the IImageUrlGenerator
/// implementation in use, and so the BackOffice could should not rely on hard-coded string
/// building to generate correct URLs
/// </para>
/// </remarks>
[PluginController("UmbracoApi")]
public class ImageUrlGeneratorController : UmbracoAuthorizedJsonController
{
private readonly IImageUrlGenerator _imageUrlGenerator;
public ImageUrlGeneratorController(IImageUrlGenerator imageUrlGenerator)
{
_imageUrlGenerator = imageUrlGenerator;
}
public string GetCropUrl(string mediaPath, int? width = null, int? height = null, ImageCropMode? imageCropMode = null, string animationProcessMode = null)
{
return _imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(mediaPath)
{
Width = width,
Height = height,
ImageCropMode = imageCropMode,
AnimationProcessMode = animationProcessMode
});
}
}
}

View File

@@ -0,0 +1,130 @@
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.IO;
using Umbraco.Core.Media;
using Umbraco.Core.Models;
using Umbraco.Web.BackOffice.Controllers;
using Umbraco.Web.Common.Attributes;
using Umbraco.Web.Models;
using Umbraco.Web.Mvc;
using Umbraco.Web.WebApi;
namespace Umbraco.Web.Editors
{
/// <summary>
/// A controller used to return images for media
/// </summary>
[PluginController("UmbracoApi")]
public class ImagesController : UmbracoAuthorizedApiController
{
private readonly IMediaFileSystem _mediaFileSystem;
private readonly IContentSettings _contentSettings;
private readonly IImageUrlGenerator _imageUrlGenerator;
public ImagesController(IMediaFileSystem mediaFileSystem, IContentSettings contentSettings, IImageUrlGenerator imageUrlGenerator)
{
_mediaFileSystem = mediaFileSystem;
_contentSettings = contentSettings;
_imageUrlGenerator = imageUrlGenerator;
}
/// <summary>
/// Gets the big thumbnail image for the original image path
/// </summary>
/// <param name="originalImagePath"></param>
/// <returns></returns>
/// <remarks>
/// If there is no original image is found then this will return not found.
/// </remarks>
public IActionResult GetBigThumbnail(string originalImagePath)
{
return string.IsNullOrWhiteSpace(originalImagePath)
? Ok()
: GetResized(originalImagePath, 500);
}
/// <summary>
/// Gets a resized image for the image at the given path
/// </summary>
/// <param name="imagePath"></param>
/// <param name="width"></param>
/// <returns></returns>
/// <remarks>
/// If there is no media, image property or image file is found then this will return not found.
/// </remarks>
public IActionResult GetResized(string imagePath, int width)
{
var ext = Path.GetExtension(imagePath);
// we need to check if it is an image by extension
if (_contentSettings.IsImageFile(ext) == false)
return NotFound();
//redirect to ImageProcessor thumbnail with rnd generated from last modified time of original media file
DateTimeOffset? imageLastModified = null;
try
{
imageLastModified = _mediaFileSystem.GetLastModified(imagePath);
}
catch (Exception)
{
// if we get an exception here it's probably because the image path being requested is an image that doesn't exist
// in the local media file system. This can happen if someone is storing an absolute path to an image online, which
// is perfectly legal but in that case the media file system isn't going to resolve it.
// so ignore and we won't set a last modified date.
}
var rnd = imageLastModified.HasValue ? $"&rnd={imageLastModified:yyyyMMddHHmmss}" : null;
var imageUrl = _imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(imagePath) { UpScale = false, Width = width, AnimationProcessMode = "first", ImageCropMode = ImageCropMode.Max, CacheBusterValue = rnd });
return new RedirectResult(imageUrl, false);
}
/// <summary>
/// Gets a processed image for the image at the given path
/// </summary>
/// <param name="imagePath"></param>
/// <param name="width"></param>
/// <param name="height"></param>
/// <param name="focalPointLeft"></param>
/// <param name="focalPointTop"></param>
/// <param name="animationProcessMode"></param>
/// <param name="mode"></param>
/// <param name="upscale"></param>
/// <returns></returns>
/// <remarks>
/// If there is no media, image property or image file is found then this will return not found.
/// </remarks>
public string GetProcessedImageUrl(string imagePath,
int? width = null,
int? height = null,
int? focalPointLeft = null,
int? focalPointTop = null,
string animationProcessMode = "first",
ImageCropMode mode = ImageCropMode.Max,
bool upscale = false,
string cacheBusterValue = "")
{
var options = new ImageUrlGenerationOptions(imagePath)
{
AnimationProcessMode = animationProcessMode,
CacheBusterValue = cacheBusterValue,
Height = height,
ImageCropMode = mode,
UpScale = upscale,
Width = width,
};
if (focalPointLeft.HasValue && focalPointTop.HasValue)
{
options.FocalPoint = new ImageUrlGenerationOptions.FocalPointPosition(focalPointTop.Value, focalPointLeft.Value);
}
return _imageUrlGenerator.GetImageUrl(options);
}
}
}

View File

@@ -0,0 +1,216 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Newtonsoft.Json;
using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.Hosting;
using Umbraco.Core.IO;
using Umbraco.Core.Services;
using Umbraco.Web.Common.Attributes;
using Umbraco.Web.Models;
using Umbraco.Web.Tour;
namespace Umbraco.Web.Editors
{
[PluginController("UmbracoApi")]
public class TourController : UmbracoAuthorizedJsonController
{
private readonly TourFilterCollection _filters;
private readonly IHostingEnvironment _hostingEnvironment;
private readonly ITourSettings _tourSettings;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly IContentTypeService _contentTypeService;
public TourController(
TourFilterCollection filters,
IHostingEnvironment hostingEnvironment,
ITourSettings tourSettings,
IUmbracoContextAccessor umbracoContextAccessor,
IContentTypeService contentTypeService)
{
_filters = filters;
_hostingEnvironment = hostingEnvironment;
_tourSettings = tourSettings;
_umbracoContextAccessor = umbracoContextAccessor;
_contentTypeService = contentTypeService;
}
public IEnumerable<BackOfficeTourFile> GetTours()
{
var result = new List<BackOfficeTourFile>();
if (_tourSettings.EnableTours == false)
return result;
var user = _umbracoContextAccessor.UmbracoContext.Security.CurrentUser;
if (user == null)
return result;
//get all filters that will be applied to all tour aliases
var aliasOnlyFilters = _filters.Where(x => x.PluginName == null && x.TourFileName == null).ToList();
//don't pass in any filters for core tours that have a plugin name assigned
var nonPluginFilters = _filters.Where(x => x.PluginName == null).ToList();
//add core tour files
var coreToursPath = Path.Combine(_hostingEnvironment.MapPathContentRoot(Core.Constants.SystemDirectories.Config), "BackOfficeTours");
if (Directory.Exists(coreToursPath))
{
foreach (var tourFile in Directory.EnumerateFiles(coreToursPath, "*.json"))
{
TryParseTourFile(tourFile, result, nonPluginFilters, aliasOnlyFilters);
}
}
//collect all tour files in packages
var appPlugins = _hostingEnvironment.MapPathContentRoot(Core.Constants.SystemDirectories.AppPlugins);
if (Directory.Exists(appPlugins))
{
foreach (var plugin in Directory.EnumerateDirectories(appPlugins))
{
var pluginName = Path.GetFileName(plugin.TrimEnd('\\'));
var pluginFilters = _filters.Where(x => x.PluginName != null && x.PluginName.IsMatch(pluginName))
.ToList();
//If there is any filter applied to match the plugin only (no file or tour alias) then ignore the plugin entirely
var isPluginFiltered = pluginFilters.Any(x => x.TourFileName == null && x.TourAlias == null);
if (isPluginFiltered) continue;
//combine matched package filters with filters not specific to a package
var combinedFilters = nonPluginFilters.Concat(pluginFilters).ToList();
foreach (var backofficeDir in Directory.EnumerateDirectories(plugin, "backoffice"))
{
foreach (var tourDir in Directory.EnumerateDirectories(backofficeDir, "tours"))
{
foreach (var tourFile in Directory.EnumerateFiles(tourDir, "*.json"))
{
TryParseTourFile(tourFile, result, combinedFilters, aliasOnlyFilters, pluginName);
}
}
}
}
}
//Get all allowed sections for the current user
var allowedSections = user.AllowedSections.ToList();
var toursToBeRemoved = new List<BackOfficeTourFile>();
//Checking to see if the user has access to the required tour sections, else we remove the tour
foreach (var backOfficeTourFile in result)
{
if (backOfficeTourFile.Tours != null)
{
foreach (var tour in backOfficeTourFile.Tours)
{
if (tour.RequiredSections != null)
{
foreach (var toursRequiredSection in tour.RequiredSections)
{
if (allowedSections.Contains(toursRequiredSection) == false)
{
toursToBeRemoved.Add(backOfficeTourFile);
break;
}
}
}
}
}
}
return result.Except(toursToBeRemoved).OrderBy(x => x.FileName, StringComparer.InvariantCultureIgnoreCase);
}
/// <summary>
/// Gets a tours for a specific doctype
/// </summary>
/// <param name="doctypeAlias">The documenttype alias</param>
/// <returns>A <see cref="BackOfficeTour"/></returns>
public IEnumerable<BackOfficeTour> GetToursForDoctype(string doctypeAlias)
{
var tourFiles = this.GetTours();
var doctypeAliasWithCompositions = new List<string>
{
doctypeAlias
};
var contentType = _contentTypeService.Get(doctypeAlias);
if (contentType != null)
{
doctypeAliasWithCompositions.AddRange(contentType.CompositionAliases());
}
return tourFiles.SelectMany(x => x.Tours)
.Where(x =>
{
if (string.IsNullOrEmpty(x.ContentType))
{
return false;
}
var contentTypes = x.ContentType.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(ct => ct.Trim());
return contentTypes.Intersect(doctypeAliasWithCompositions).Any();
});
}
private void TryParseTourFile(string tourFile,
ICollection<BackOfficeTourFile> result,
List<BackOfficeTourFilter> filters,
List<BackOfficeTourFilter> aliasOnlyFilters,
string pluginName = null)
{
var fileName = Path.GetFileNameWithoutExtension(tourFile);
if (fileName == null) return;
//get the filters specific to this file
var fileFilters = filters.Where(x => x.TourFileName != null && x.TourFileName.IsMatch(fileName)).ToList();
//If there is any filter applied to match the file only (no tour alias) then ignore the file entirely
var isFileFiltered = fileFilters.Any(x => x.TourAlias == null);
if (isFileFiltered) return;
//now combine all aliases to filter below
var aliasFilters = aliasOnlyFilters.Concat(filters.Where(x => x.TourAlias != null))
.Select(x => x.TourAlias)
.ToList();
try
{
var contents = System.IO.File.ReadAllText(tourFile);
var tours = JsonConvert.DeserializeObject<BackOfficeTour[]>(contents);
var backOfficeTours = tours.Where(x =>
aliasFilters.Count == 0 || aliasFilters.All(filter => filter.IsMatch(x.Alias)) == false);
var user = _umbracoContextAccessor.UmbracoContext.Security.CurrentUser;
var localizedTours = backOfficeTours.Where(x =>
string.IsNullOrWhiteSpace(x.Culture) || x.Culture.Equals(user.Language,
StringComparison.InvariantCultureIgnoreCase)).ToList();
var tour = new BackOfficeTourFile
{
FileName = Path.GetFileNameWithoutExtension(tourFile),
PluginName = pluginName,
Tours = localizedTours
};
//don't add if all of the tours are filtered
if (tour.Tours.Any())
result.Add(tour);
}
catch (IOException e)
{
throw new IOException("Error while trying to read file: " + tourFile, e);
}
catch (JsonReaderException e)
{
throw new JsonReaderException("Error while trying to parse content as tour data: " + tourFile, e);
}
}
}
}

View File

@@ -0,0 +1,28 @@
using Umbraco.Web.BackOffice.Filters;
using Umbraco.Web.Common.Attributes;
using Umbraco.Web.Common.Controllers;
using Umbraco.Web.Common.Filters;
namespace Umbraco.Web.BackOffice.Controllers
{
/// <summary>
/// Provides a base class for authorized auto-routed Umbraco API controllers.
/// </summary>
/// <remarks>
/// This controller will also append a custom header to the response if the user
/// is logged in using forms authentication which indicates the seconds remaining
/// before their timeout expires.
/// </remarks>
[IsBackOffice]
//[UmbracoUserTimeoutFilter] //TODO reintroduce
[UmbracoAuthorize]
[DisableBrowserCache]
[UmbracoWebApiRequireHttps]
//[CheckIfUserTicketDataIsStale] //TODO reintroduce
//[UnhandedExceptionLoggerConfiguration] //TODO reintroduce
//[EnableDetailedErrors] //TODO reintroduce
public abstract class UmbracoAuthorizedApiController : UmbracoApiController
{
}
}

View File

@@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Mvc;
using Umbraco.Web.BackOffice.Controllers;
using Umbraco.Web.BackOffice.Filters;
using Umbraco.Web.Common.Filters;
namespace Umbraco.Web.Editors
{
/// <summary>
/// An abstract API controller that only supports JSON and all requests must contain the correct csrf header
/// </summary>
/// <remarks>
/// Inheriting from this controller means that ALL of your methods are JSON methods that are called by Angular,
/// methods that are not called by Angular or don't contain a valid csrf header will NOT work.
/// </remarks>
[TypeFilter(typeof(ValidateAngularAntiForgeryTokenAttribute))]
[TypeFilter(typeof(AngularJsonOnlyConfigurationAttribute))] // TODO: This could be applied with our Application Model conventions
public abstract class UmbracoAuthorizedJsonController : UmbracoAuthorizedApiController
{
}
}