Compare commits
119
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8f71af646 | ||
|
|
fa4fe477f8 | ||
|
|
241a0d8093 | ||
|
|
ec52b545b4 | ||
|
|
4ed960caa2 | ||
|
|
d0c1f23661 | ||
|
|
9700dc7266 | ||
|
|
a2684069b1 | ||
|
|
3ce0854e47 | ||
|
|
aa86a68bad | ||
|
|
323cecfd58 | ||
|
|
a908c995f8 | ||
|
|
851026d145 | ||
|
|
fee470c731 | ||
|
|
a242a4200c | ||
|
|
104ba5f2bc | ||
|
|
d9d2b66e85 | ||
|
|
8771cc707e | ||
|
|
376856c347 | ||
|
|
e688f5a2db | ||
|
|
02146722f7 | ||
|
|
4c73f8a83b | ||
|
|
3b35c424a5 | ||
|
|
42aaa7dd4d | ||
|
|
365621023a | ||
|
|
865a3880b9 | ||
|
|
1751ff8a8c | ||
|
|
7d4a786a7c | ||
|
|
72e996f54d | ||
|
|
e8346caa8c | ||
|
|
fe076b65f4 | ||
|
|
bb3bfdb54d | ||
|
|
04a872fe42 | ||
|
|
9f80baa50b | ||
|
|
dac9986ed2 | ||
|
|
1e1affd328 | ||
|
|
ca6d92277a | ||
|
|
c8427274b4 | ||
|
|
40ef4e804d | ||
|
|
53395db518 | ||
|
|
7217aa9cd8 | ||
|
|
3cbff23404 | ||
|
|
13bff13110 | ||
|
|
9799d722f6 | ||
|
|
7c9364a532 | ||
|
|
ca1453ab4e | ||
|
|
8962c1e636 | ||
|
|
196b29c5eb | ||
|
|
309e7fe371 | ||
|
|
7e4060e90c | ||
|
|
2d1e40ed5e | ||
|
|
f4fdd07eff | ||
|
|
3ab1957a74 | ||
|
|
352ef47225 | ||
|
|
c0c106a107 | ||
|
|
65e3f001c0 | ||
|
|
db5a0bad7c | ||
|
|
f29b6d93e6 | ||
|
|
b934fb7d90 | ||
|
|
d0f9c9e931 | ||
|
|
263b9c74f5 | ||
|
|
cbc8724df7 | ||
|
|
a4dcdbc5cd | ||
|
|
21d5f1b754 | ||
|
|
907a5a69d3 | ||
|
|
087e682f79 | ||
|
|
6c7053c449 | ||
|
|
211c86ccbc | ||
|
|
22df4190bb | ||
|
|
7d99708065 | ||
|
|
59a6abaa84 | ||
|
|
ee1fe5f2d8 | ||
|
|
74f17eb3f6 | ||
|
|
796eb9b25a | ||
|
|
76bcf58953 | ||
|
|
e8c18cda56 | ||
|
|
9daa15f1fc | ||
|
|
400f323db4 | ||
|
|
2c0a380d8b | ||
|
|
8a73bbb900 | ||
|
|
99629e4712 | ||
|
|
27ae938099 | ||
|
|
edb69eea36 | ||
|
|
30bb416ff8 | ||
|
|
a9b8090d6b | ||
|
|
76ecb295cc | ||
|
|
ce817551a5 | ||
|
|
32cd9fed49 | ||
|
|
cabb91dbe1 | ||
|
|
633bc8fd8a | ||
|
|
9de4cf4444 | ||
|
|
ffe9a14664 | ||
|
|
1fb4b905c2 | ||
|
|
4b8a16af7c | ||
|
|
f6c4af745f | ||
|
|
6dd8cb496b | ||
|
|
8d6394e8e3 | ||
|
|
159f92d6f8 | ||
|
|
75f8c09e85 | ||
|
|
62de668380 | ||
|
|
96349e4754 | ||
|
|
1ec98139c6 | ||
|
|
7bb85c9ed6 | ||
|
|
25ac6b275e | ||
|
|
db8fe55d08 | ||
|
|
86fcb8e715 | ||
|
|
c85486b866 | ||
|
|
4e2302ab6e | ||
|
|
a87e7a12d0 | ||
|
|
2fc8f62195 | ||
|
|
586c4e8db3 | ||
|
|
5c663fd0f0 | ||
|
|
4f89cb1a34 | ||
|
|
424db16ab1 | ||
|
|
e9be3dd4a5 | ||
|
|
8ff1d49346 | ||
|
|
1473e41a20 | ||
|
|
2038898f4c | ||
|
|
40c08f81c4 |
@@ -30,7 +30,7 @@
|
||||
<dependency id="Microsoft.AspNet.WebApi.Client" version="[5.2.7,5.999999)" />
|
||||
<dependency id="Microsoft.Owin" version="[4.0.1,4.999999)" />
|
||||
<dependency id="MiniProfiler" version="[4.0.138,4.999999)" />
|
||||
<dependency id="Newtonsoft.Json" version="[12.0.1,12.999999)" />
|
||||
<dependency id="Newtonsoft.Json" version="[12.0.1,13.999999)" />
|
||||
<dependency id="Semver" version="[2.0.4,2.999999)" />
|
||||
<dependency id="Serilog" version="[2.10.0,2.999999)" />
|
||||
<dependency id="Serilog.Enrichers.Process" version="[2.0.2,2.999999)" />
|
||||
|
||||
@@ -15,8 +15,8 @@
|
||||
<div class="level{{item.level}}"><span class="xref">{{item.name.0.value}}</span></div>
|
||||
</div>
|
||||
{{/inheritance.0}}
|
||||
<h6><strong>{{__global.namespace}}</strong>:{{namespace}}</h6>
|
||||
<h6><strong>{{__global.assembly}}</strong>:{{assemblies.0}}.dll</h6>
|
||||
<h6><strong>{{__global.namespace}}</strong>: {{{namespace.specName.0.value}}}</h6>
|
||||
<h6><strong>{{__global.assembly}}</strong>: {{assemblies.0}}.dll</h6>
|
||||
<h5 id="{{id}}_syntax">{{__global.syntax}}</h5>
|
||||
<div class="codewrapper">
|
||||
<pre><code class="lang-{{_lang}} hljs">{{syntax.content.0.value}}</code></pre>
|
||||
|
||||
+2
-2
@@ -18,5 +18,5 @@ using System.Resources;
|
||||
[assembly: AssemblyVersion("8.0.0")]
|
||||
|
||||
// these are FYI and changed automatically
|
||||
[assembly: AssemblyFileVersion("8.18.0")]
|
||||
[assembly: AssemblyInformationalVersion("8.18.0-rc")]
|
||||
[assembly: AssemblyFileVersion("8.18.14")]
|
||||
[assembly: AssemblyInformationalVersion("8.18.14")]
|
||||
|
||||
@@ -79,7 +79,7 @@ namespace Umbraco.Core.Compose
|
||||
get
|
||||
{
|
||||
var httpContext = HttpContext.Current == null ? (HttpContextBase) null : new HttpContextWrapper(HttpContext.Current);
|
||||
var ip = httpContext.GetCurrentRequestIpAddress();
|
||||
var ip = httpContext?.GetCurrentRequestIpAddress();
|
||||
if (ip == null || ip.ToLowerInvariant().StartsWith("unknown")) ip = "";
|
||||
return ip;
|
||||
}
|
||||
|
||||
@@ -83,6 +83,8 @@ namespace Umbraco.Core.Composing.CompositionExtensions
|
||||
|
||||
composition.RegisterUnique<ITelemetryService, TelemetryService>();
|
||||
composition.RegisterUnique<IHtmlSanitizer, NoopHtmlSanitizer>();
|
||||
composition.RegisterUnique<IMarkdownSanitizer, NoopMarkdownSanitizer>();
|
||||
composition.RegisterUnique<IFileStreamSecurityValidator, FileStreamSecurityValidator>();
|
||||
|
||||
return composition;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Configuration;
|
||||
using Umbraco.Core.Macros;
|
||||
|
||||
@@ -34,6 +35,9 @@ namespace Umbraco.Core.Configuration.UmbracoSettings
|
||||
[ConfigurationProperty("disallowedUploadFiles")]
|
||||
internal CommaDelimitedConfigurationElement DisallowedUploadFiles => GetOptionalDelimitedElement("disallowedUploadFiles", new[] {"ashx", "aspx", "ascx", "config", "cshtml", "vbhtml", "asmx", "air", "axd", "xamlx"});
|
||||
|
||||
[ConfigurationProperty("allowedMediaHosts")]
|
||||
internal CommaDelimitedConfigurationElement AllowedMediaHosts => GetOptionalDelimitedElement("allowedMediaHosts", new string[0]);
|
||||
|
||||
[ConfigurationProperty("allowedUploadFiles")]
|
||||
internal CommaDelimitedConfigurationElement AllowedUploadFiles => GetOptionalDelimitedElement("allowedUploadFiles", new string[0]);
|
||||
|
||||
@@ -49,6 +53,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings
|
||||
[ConfigurationProperty("hideBackofficeLogo")]
|
||||
internal InnerTextConfigurationElement<bool> HideBackOfficeLogo => GetOptionalTextElement("hideBackofficeLogo", false);
|
||||
|
||||
|
||||
string IContentSection.NotificationEmailAddress => Notifications.NotificationEmailAddress;
|
||||
|
||||
bool IContentSection.DisableHtmlEmail => Notifications.DisableHtmlEmail;
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
|
||||
namespace Umbraco.Core
|
||||
{
|
||||
public static class CryptoServiceProviderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates a random int withing a specified range.
|
||||
/// </summary>
|
||||
/// <param name="provider">Random bytes provider.</param>
|
||||
/// <param name="minValue">The minimum value of the resulting int.</param>
|
||||
/// <param name="maxValue">The maximum value of the resulting int.</param>
|
||||
/// <returns>A random integer that falls withing the specified range.</returns>
|
||||
public static int GetInt32(this RNGCryptoServiceProvider provider, int minValue, int maxValue)
|
||||
{
|
||||
var randomBytes = new byte[4];
|
||||
provider.GetBytes(randomBytes);
|
||||
|
||||
var randomInt = Math.Abs(BitConverter.ToInt32(randomBytes, 0));
|
||||
// We call do mod to ensure that the value is within the specified range.
|
||||
return randomInt % (maxValue - minValue + 1) + minValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ namespace Umbraco.Core.Logging.Viewer
|
||||
/// <returns></returns>
|
||||
public string GetLogLevel()
|
||||
{
|
||||
var logLevel = Enum.GetValues(typeof(LogEventLevel)).Cast<LogEventLevel>().Where(Log.Logger.IsEnabled)?.Min() ?? null;
|
||||
var logLevel = Enum.GetValues(typeof(LogEventLevel)).Cast<LogEventLevel>().Where(Log.Logger.IsEnabled).DefaultIfEmpty(LogEventLevel.Information)?.Min() ?? null;
|
||||
return logLevel?.ToString() ?? "";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
using Umbraco.Core.Migrations.Install;
|
||||
using System.Linq;
|
||||
using NPoco;
|
||||
using Umbraco.Core.Migrations.Install;
|
||||
|
||||
namespace Umbraco.Core.Migrations.Upgrade.Common
|
||||
{
|
||||
@@ -14,9 +16,20 @@ namespace Umbraco.Core.Migrations.Upgrade.Common
|
||||
Delete.KeysAndIndexes(Constants.DatabaseSchema.Tables.KeyValue).Do();
|
||||
Delete.KeysAndIndexes(Constants.DatabaseSchema.Tables.PropertyData).Do();
|
||||
|
||||
var existingTables = SqlSyntax.GetTablesInSchema(Context.Database).ToHashSet();
|
||||
|
||||
// re-create *all* keys and indexes
|
||||
foreach (var x in DatabaseSchemaCreator.OrderedTables)
|
||||
Create.KeysAndIndexes(x).Do();
|
||||
foreach (var entityClass in DatabaseSchemaCreator.OrderedTables)
|
||||
{
|
||||
var tableNameAttribute = entityClass.FirstAttribute<TableNameAttribute>();
|
||||
if (tableNameAttribute == null)
|
||||
continue;
|
||||
|
||||
if (!existingTables.Contains(tableNameAttribute.Value))
|
||||
continue;
|
||||
|
||||
Create.KeysAndIndexes(entityClass).Do();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,6 +213,7 @@ namespace Umbraco.Core.Migrations.Upgrade
|
||||
|
||||
//FINAL
|
||||
To<AddContentVersionCleanupFeature>("{8BAF5E6C-DCB7-41AE-824F-4215AE4F1F98}");
|
||||
To<NoopMigration>("{03482BB0-CF13-475C-845E-ECB8319DBE3C}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,9 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
|
||||
var links = new List<LinkDto>();
|
||||
foreach (var relatedLink in relatedLinks)
|
||||
{
|
||||
if (relatedLink == null)
|
||||
continue;
|
||||
|
||||
GuidUdi udi = null;
|
||||
if (relatedLink.IsInternal)
|
||||
{
|
||||
|
||||
@@ -1,17 +1,37 @@
|
||||
using System.Runtime.Serialization;
|
||||
using Umbraco.Core.Models.Entities;
|
||||
|
||||
namespace Umbraco.Core.Models.ContentEditing
|
||||
{
|
||||
[DataContract(Name = "historyCleanup", Namespace = "")]
|
||||
public class HistoryCleanup
|
||||
public class HistoryCleanup : BeingDirtyBase
|
||||
{
|
||||
private bool _preventCleanup;
|
||||
|
||||
[DataMember(Name = "preventCleanup")]
|
||||
public bool PreventCleanup { get; set; }
|
||||
public bool PreventCleanup
|
||||
{
|
||||
get => _preventCleanup;
|
||||
set => SetPropertyValueAndDetectChanges(value, ref _preventCleanup, nameof(PreventCleanup));
|
||||
}
|
||||
|
||||
private int? _keepAllVersionsNewerThanDays;
|
||||
|
||||
[DataMember(Name = "keepAllVersionsNewerThanDays")]
|
||||
public int? KeepAllVersionsNewerThanDays { get; set; }
|
||||
public int? KeepAllVersionsNewerThanDays
|
||||
{
|
||||
get => _keepAllVersionsNewerThanDays;
|
||||
set => SetPropertyValueAndDetectChanges(value, ref _keepAllVersionsNewerThanDays, nameof(KeepAllVersionsNewerThanDays));
|
||||
}
|
||||
|
||||
private int? _keepLatestVersionPerDayForDays;
|
||||
|
||||
[DataMember(Name = "keepLatestVersionPerDayForDays")]
|
||||
public int? KeepLatestVersionPerDayForDays { get; set; }
|
||||
public int? KeepLatestVersionPerDayForDays
|
||||
{
|
||||
get => _keepLatestVersionPerDayForDays;
|
||||
set => SetPropertyValueAndDetectChanges(value, ref _keepLatestVersionPerDayForDays,
|
||||
nameof(KeepLatestVersionPerDayForDays));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
using Umbraco.Core.Models.ContentEditing;
|
||||
|
||||
@@ -50,8 +50,6 @@ namespace Umbraco.Core.Models
|
||||
/// <inheritdoc />
|
||||
public override bool SupportsPublishing => SupportsPublishingConst;
|
||||
|
||||
|
||||
|
||||
//Custom comparer for enumerable
|
||||
private static readonly DelegateEqualityComparer<IEnumerable<ITemplate>> TemplateComparer = new DelegateEqualityComparer<IEnumerable<ITemplate>>(
|
||||
(templates, enumerable) => templates.UnsortedSequenceEqual(enumerable),
|
||||
@@ -98,7 +96,14 @@ namespace Umbraco.Core.Models
|
||||
}
|
||||
}
|
||||
|
||||
public HistoryCleanup HistoryCleanup { get; set; }
|
||||
private HistoryCleanup _historyCleanup;
|
||||
|
||||
public HistoryCleanup HistoryCleanup
|
||||
{
|
||||
get => _historyCleanup;
|
||||
set => SetPropertyValueAndDetectChanges(value, ref _historyCleanup, nameof(HistoryCleanup));
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Determines if AllowedTemplates contains templateId
|
||||
@@ -165,5 +170,15 @@ namespace Umbraco.Core.Models
|
||||
|
||||
/// <inheritdoc />
|
||||
IContentType IContentType.DeepCloneWithResetIdentities(string newAlias) => (IContentType)DeepCloneWithResetIdentities(newAlias);
|
||||
|
||||
|
||||
public override bool IsDirty()
|
||||
{
|
||||
bool dirtyEntity = base.IsDirty();
|
||||
|
||||
bool dirtyHistoryCleanup = HistoryCleanup.IsDirty();
|
||||
|
||||
return dirtyEntity || dirtyHistoryCleanup;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace Umbraco.Core.Models.Entities
|
||||
public abstract class BeingDirtyBase : IRememberBeingDirty
|
||||
{
|
||||
private bool _withChanges = true; // should we track changes?
|
||||
private bool _withChangedEvent = true; // should we fire the event when changes are registered
|
||||
private Dictionary<string, bool> _currentChanges; // which properties have changed?
|
||||
private Dictionary<string, bool> _savedChanges; // which properties had changed at last commit?
|
||||
|
||||
@@ -117,7 +118,10 @@ namespace Umbraco.Core.Models.Entities
|
||||
|
||||
_currentChanges[propertyName] = true;
|
||||
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
if (_withChangedEvent)
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -136,6 +140,22 @@ namespace Umbraco.Core.Models.Entities
|
||||
_withChanges = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disables the property changed event.
|
||||
/// </summary>
|
||||
protected void DisablePropertyChangedEvent()
|
||||
{
|
||||
_withChangedEvent = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables the property changed event.
|
||||
/// </summary>
|
||||
protected void EnablePropertyChangedEvent()
|
||||
{
|
||||
_withChangedEvent = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a property value, detects changes and manages the dirty flag.
|
||||
/// </summary>
|
||||
|
||||
@@ -217,6 +217,15 @@ namespace Umbraco.Core.Packaging
|
||||
Directory.CreateDirectory(IOHelper.MapPath(_mediaFolderPath));
|
||||
|
||||
var packPath = _mediaFolderPath.EnsureEndsWith('/') + (definition.Name + "_" + definition.Version).Replace(' ', '_') + ".zip";
|
||||
|
||||
var absolutePackPath = Path.GetFullPath(IOHelper.MapPath(packPath));
|
||||
var expectedRoot = Path.GetFullPath(IOHelper.MapPath(_mediaFolderPath.EnsureEndsWith('/')));
|
||||
|
||||
if (absolutePackPath.StartsWith(expectedRoot) == false)
|
||||
{
|
||||
throw new IOException("Invalid path due to the package name");
|
||||
}
|
||||
|
||||
ZipPackage(temporaryPath, IOHelper.MapPath(packPath));
|
||||
|
||||
//we need to update the package path and save it
|
||||
|
||||
@@ -71,7 +71,7 @@ namespace Umbraco.Core.Persistence.Factories
|
||||
}
|
||||
|
||||
if (entity.HasIdentity)
|
||||
dto.Id = short.Parse(entity.Id.ToString());
|
||||
dto.Id = entity.Id.SafeCast<int>();
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using NPoco;
|
||||
using System.Data.Common;
|
||||
|
||||
namespace Umbraco.Core.Persistence
|
||||
{
|
||||
@@ -20,5 +21,7 @@ namespace Umbraco.Core.Persistence
|
||||
/// Gets a value indicating whether the database is currently in a transaction.
|
||||
/// </summary>
|
||||
bool InTransaction { get; }
|
||||
|
||||
void AddParameter(DbCommand cmd, PocoColumn column, object value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,8 +103,8 @@ namespace Umbraco.Core.Persistence
|
||||
|
||||
if (string.IsNullOrWhiteSpace(programFiles)) return;
|
||||
|
||||
// detect 14, 13, 12, 11
|
||||
for (var i = 14; i > 10; i--)
|
||||
// detect (17, 16) 15, 14, 13, 12, 11 - future-proofing by a couple of versions
|
||||
for (var i = 17; i > 10; i--)
|
||||
{
|
||||
var exe = Path.Combine(programFiles, $@"Microsoft SQL Server\{i}0\Tools\Binn\SqlLocalDB.exe");
|
||||
if (File.Exists(exe) == false) continue;
|
||||
|
||||
@@ -156,7 +156,8 @@ namespace Umbraco.Core.Persistence
|
||||
var recordValues = new string[columns.Length];
|
||||
for (var columnIndex = 0; columnIndex < columns.Length; columnIndex++)
|
||||
{
|
||||
database.AddParameter(command, columns[columnIndex].Value.GetValue(record));
|
||||
var column = columns[columnIndex].Value;
|
||||
database.AddParameter(command, column, column.GetValue(record));
|
||||
recordValues[columnIndex] = prefix + parameterIndex++;
|
||||
}
|
||||
recordsValues[commandRecordIndex] = "(" + string.Join(",", recordValues) + ")";
|
||||
|
||||
@@ -1128,18 +1128,24 @@ AND umbracoNode.id <> @id",
|
||||
}
|
||||
}
|
||||
|
||||
//Now bulk update the table DocumentCultureVariationDto, once for edited = true, another for edited = false
|
||||
// Now bulk update the table DocumentCultureVariationDto, once for edited = true, another for edited = false
|
||||
foreach (var editValue in toUpdate.GroupBy(x => x.Edited))
|
||||
{
|
||||
Database.Execute(Sql().Update<DocumentCultureVariationDto>(u => u.Set(x => x.Edited, editValue.Key))
|
||||
.WhereIn<DocumentCultureVariationDto>(x => x.Id, editValue.Select(x => x.Id)));
|
||||
foreach (var ids in editValue.Select(x => x.Id).InGroupsOf(Constants.Sql.MaxParameterCount))
|
||||
{
|
||||
Database.Execute(Sql().Update<DocumentCultureVariationDto>(u => u.Set(x => x.Edited, editValue.Key))
|
||||
.WhereIn<DocumentCultureVariationDto>(x => x.Id, ids));
|
||||
}
|
||||
}
|
||||
|
||||
//Now bulk update the umbracoDocument table
|
||||
// Now bulk update the umbracoDocument table
|
||||
foreach (var editValue in editedDocument.GroupBy(x => x.Value))
|
||||
{
|
||||
Database.Execute(Sql().Update<DocumentDto>(u => u.Set(x => x.Edited, editValue.Key))
|
||||
.WhereIn<DocumentDto>(x => x.NodeId, editValue.Select(x => x.Key)));
|
||||
foreach (var ids in editValue.Select(x => x.Key).InGroupsOf(Constants.Sql.MaxParameterCount))
|
||||
{
|
||||
Database.Execute(Sql().Update<DocumentDto>(u => u.Set(x => x.Edited, editValue.Key))
|
||||
.WhereIn<DocumentDto>(x => x.NodeId, ids));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -174,13 +174,13 @@ JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID");
|
||||
var urlHash = url.GenerateHash<SHA1>();
|
||||
var sql = GetBaseQuery(false)
|
||||
.Where<RedirectUrlDto>(x => x.Url == url && x.UrlHash == urlHash &&
|
||||
(x.Culture == culture.ToLower() || x.Culture == string.Empty))
|
||||
(x.Culture == culture.ToLower() || x.Culture == null || x.Culture == string.Empty))
|
||||
.OrderByDescending<RedirectUrlDto>(x => x.CreateDateUtc);
|
||||
var dtos = Database.Fetch<RedirectUrlDto>(sql);
|
||||
var dto = dtos.FirstOrDefault(f => f.Culture == culture.ToLower());
|
||||
|
||||
if (dto == null)
|
||||
dto = dtos.FirstOrDefault(f => f.Culture == string.Empty);
|
||||
dto = dtos.FirstOrDefault(f => string.IsNullOrWhiteSpace(f.Culture));
|
||||
|
||||
return dto == null ? null : Map(dto);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using System.Data.SqlClient;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using NPoco;
|
||||
using StackExchange.Profiling;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Persistence.FaultHandling;
|
||||
using Umbraco.Core.Persistence.Mappers;
|
||||
|
||||
namespace Umbraco.Core.Persistence
|
||||
{
|
||||
@@ -88,6 +90,22 @@ namespace Umbraco.Core.Persistence
|
||||
return command;
|
||||
}
|
||||
|
||||
public void AddParameter(DbCommand cmd, PocoColumn column, object value)
|
||||
{
|
||||
if (DatabaseType.IsSqlServer())
|
||||
{
|
||||
if (column.ColumnType == typeof(byte[]) && value == null)
|
||||
{
|
||||
value = new SqlParameter("_bytes", SqlDbType.VarBinary, -1)
|
||||
{
|
||||
Value = DBNull.Value
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
base.AddParameter(cmd, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Testing, Debugging and Troubleshooting
|
||||
|
||||
@@ -145,16 +145,25 @@ namespace Umbraco.Core.PropertyEditors
|
||||
/// <returns></returns>
|
||||
internal Attempt<object> TryConvertValueToCrlType(object value)
|
||||
{
|
||||
if (value is JToken jsonValue)
|
||||
if (value is JValue jsonValue)
|
||||
{
|
||||
if (jsonValue is JContainer && jsonValue.HasValues == false)
|
||||
// Calling the "ToString(Formatting)" method on a string value results in a new
|
||||
// string enclosed in double quotes (which we don't want), as the method is declared
|
||||
// in the JToken class. Calling either of the "ToString()" or "ToString(CultureInfo)"
|
||||
// methods hits the overridden methods in the JValue class, which correctly doesn't
|
||||
// enclose the value in double quotes.
|
||||
value = jsonValue.ToString(CultureInfo.InvariantCulture);
|
||||
}
|
||||
else if (value is JToken jsonToken)
|
||||
{
|
||||
if (jsonToken is JContainer && jsonToken.HasValues == false)
|
||||
{
|
||||
// Empty JSON array/object
|
||||
value = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
value = jsonValue.ToString(Formatting.None);
|
||||
value = jsonToken.ToString(Formatting.None);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -100,20 +100,20 @@ namespace Umbraco.Core.Security
|
||||
if (userService == null) throw new ArgumentNullException("userService");
|
||||
if (entityService == null) throw new ArgumentNullException("entityService");
|
||||
|
||||
bool? hasPathAccess = null;
|
||||
bool hasPathAccess;
|
||||
entity = null;
|
||||
|
||||
if (nodeId == Constants.System.Root)
|
||||
hasPathAccess = user.HasContentRootAccess(entityService, appCaches);
|
||||
else if (nodeId == Constants.System.RecycleBinContent)
|
||||
hasPathAccess = user.HasContentBinAccess(entityService, appCaches);
|
||||
|
||||
if (hasPathAccess.HasValue)
|
||||
return hasPathAccess.Value ? ContentAccess.Granted : ContentAccess.Denied;
|
||||
|
||||
entity = entityService.Get(nodeId, UmbracoObjectTypes.Document);
|
||||
if (entity == null) return ContentAccess.NotFound;
|
||||
hasPathAccess = user.HasContentPathAccess(entity, entityService, appCaches);
|
||||
else
|
||||
{
|
||||
entity = entityService.Get(nodeId, UmbracoObjectTypes.Document);
|
||||
if (entity == null)
|
||||
return ContentAccess.NotFound;
|
||||
hasPathAccess = user.HasContentPathAccess(entity, entityService, appCaches);
|
||||
}
|
||||
|
||||
if (hasPathAccess == false)
|
||||
return ContentAccess.Denied;
|
||||
@@ -121,8 +121,11 @@ namespace Umbraco.Core.Security
|
||||
if (permissionsToCheck == null || permissionsToCheck.Length == 0)
|
||||
return ContentAccess.Granted;
|
||||
|
||||
// if there is no entity for this id, then just use the id as the path (i.e. -1 or -20)
|
||||
var path = entity == null ? nodeId.ToString() : entity.Path;
|
||||
|
||||
//get the implicit/inherited permissions for the user for this path
|
||||
return CheckPermissionsPath(entity.Path, user, userService, permissionsToCheck)
|
||||
return CheckPermissionsPath(path, user, userService, permissionsToCheck)
|
||||
? ContentAccess.Granted
|
||||
: ContentAccess.Denied;
|
||||
}
|
||||
@@ -153,20 +156,20 @@ namespace Umbraco.Core.Security
|
||||
if (contentService == null) throw new ArgumentNullException("contentService");
|
||||
if (entityService == null) throw new ArgumentNullException("entityService");
|
||||
|
||||
bool? hasPathAccess = null;
|
||||
bool hasPathAccess;
|
||||
contentItem = null;
|
||||
|
||||
if (nodeId == Constants.System.Root)
|
||||
hasPathAccess = user.HasContentRootAccess(entityService, appCaches);
|
||||
else if (nodeId == Constants.System.RecycleBinContent)
|
||||
hasPathAccess = user.HasContentBinAccess(entityService, appCaches);
|
||||
|
||||
if (hasPathAccess.HasValue)
|
||||
return hasPathAccess.Value ? ContentAccess.Granted : ContentAccess.Denied;
|
||||
|
||||
contentItem = contentService.GetById(nodeId);
|
||||
if (contentItem == null) return ContentAccess.NotFound;
|
||||
hasPathAccess = user.HasPathAccess(contentItem, entityService, appCaches);
|
||||
else
|
||||
{
|
||||
contentItem = contentService.GetById(nodeId);
|
||||
if (contentItem == null)
|
||||
return ContentAccess.NotFound;
|
||||
hasPathAccess = user.HasPathAccess(contentItem, entityService, appCaches);
|
||||
}
|
||||
|
||||
if (hasPathAccess == false)
|
||||
return ContentAccess.Denied;
|
||||
@@ -174,16 +177,18 @@ namespace Umbraco.Core.Security
|
||||
if (permissionsToCheck == null || permissionsToCheck.Length == 0)
|
||||
return ContentAccess.Granted;
|
||||
|
||||
// if there is no content item for this id, then just use the id as the path (i.e. -1 or -20)
|
||||
var path = contentItem == null ? nodeId.ToString() : contentItem.Path;
|
||||
|
||||
//get the implicit/inherited permissions for the user for this path
|
||||
return CheckPermissionsPath(contentItem.Path, user, userService, permissionsToCheck)
|
||||
return CheckPermissionsPath(path, user, userService, permissionsToCheck)
|
||||
? ContentAccess.Granted
|
||||
: ContentAccess.Denied;
|
||||
}
|
||||
|
||||
private static bool CheckPermissionsPath(string path, IUser user, IUserService userService, params char[] permissionsToCheck)
|
||||
{
|
||||
//get the implicit/inherited permissions for the user for this path,
|
||||
//if there is no content item for this id, than just use the id as the path (i.e. -1 or -20)
|
||||
//get the implicit/inherited permissions for the user for this path
|
||||
var permission = userService.GetPermissionsForPath(user, path);
|
||||
|
||||
var allowed = true;
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace Umbraco.Core.Security
|
||||
{
|
||||
public class FileStreamSecurityValidator : IFileStreamSecurityValidator
|
||||
{
|
||||
private readonly IEnumerable<IFileStreamSecurityAnalyzer> _fileAnalyzers;
|
||||
|
||||
public FileStreamSecurityValidator(IEnumerable<IFileStreamSecurityAnalyzer> fileAnalyzers)
|
||||
{
|
||||
_fileAnalyzers = fileAnalyzers;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes whether the file content is considered safe with registered IFileStreamSecurityAnalyzers
|
||||
/// </summary>
|
||||
/// <param name="fileStream">Needs to be a Read seekable stream</param>
|
||||
/// <returns>Whether the file is considered safe after running the necessary analyzers</returns>
|
||||
public bool IsConsideredSafe(Stream fileStream)
|
||||
{
|
||||
foreach (var fileAnalyzer in _fileAnalyzers)
|
||||
{
|
||||
fileStream.Seek(0, SeekOrigin.Begin);
|
||||
if (!fileAnalyzer.ShouldHandle(fileStream))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
fileStream.Seek(0, SeekOrigin.Begin);
|
||||
if (fileAnalyzer.IsConsideredSafe(fileStream) == false)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
fileStream.Seek(0, SeekOrigin.Begin);
|
||||
// If no analyzer we consider the file to be safe as the implementer has the possibility to add additional analyzers
|
||||
// Or all analyzers deem te file to be safe
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.IO;
|
||||
|
||||
namespace Umbraco.Core.Security
|
||||
{
|
||||
public interface IFileStreamSecurityAnalyzer
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether the analyzer should process the file
|
||||
/// The implementation should be considerably faster than IsConsideredSafe
|
||||
/// </summary>
|
||||
/// <param name="fileStream"></param>
|
||||
/// <returns></returns>
|
||||
bool ShouldHandle(Stream fileStream);
|
||||
|
||||
/// <summary>
|
||||
/// Analyzes whether the file content is considered safe
|
||||
/// </summary>
|
||||
/// <param name="fileStream">Needs to be a Read/Write seekable stream</param>
|
||||
/// <returns>Whether the file is considered safe</returns>
|
||||
bool IsConsideredSafe(Stream fileStream);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.IO;
|
||||
|
||||
namespace Umbraco.Core.Security
|
||||
{
|
||||
public interface IFileStreamSecurityValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// Analyzes whether the file content is considered safe with registered IFileStreamSecurityAnalyzers
|
||||
/// </summary>
|
||||
/// <param name="fileStream">Needs to be a Read seekable stream</param>
|
||||
/// <returns>Whether the file is considered safe after running the necessary analyzers</returns>
|
||||
bool IsConsideredSafe(Stream fileStream);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace Umbraco.Core.Security
|
||||
{
|
||||
public interface IMarkdownSanitizer
|
||||
{
|
||||
/// <summary>
|
||||
/// Sanitizes Markdown
|
||||
/// </summary>
|
||||
/// <param name="markdown">Markdown to be sanitized</param>
|
||||
/// <returns>Sanitized Markdown</returns>
|
||||
string Sanitize(string markdown);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -965,7 +965,7 @@ namespace Umbraco.Core.Security
|
||||
protected string GetCurrentRequestIpAddress()
|
||||
{
|
||||
var httpContext = HttpContext.Current == null ? (HttpContextBase) null : new HttpContextWrapper(HttpContext.Current);
|
||||
return httpContext.GetCurrentRequestIpAddress();
|
||||
return httpContext?.GetCurrentRequestIpAddress();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
namespace Umbraco.Core.Security
|
||||
{
|
||||
public class NoopMarkdownSanitizer : IMarkdownSanitizer
|
||||
{
|
||||
public string Sanitize(string markdown)
|
||||
{
|
||||
return markdown;
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
@@ -704,7 +704,29 @@ namespace Umbraco.Core.Services.Implement
|
||||
}
|
||||
|
||||
copy.ParentId = containerId;
|
||||
var saveEventArgs = new SaveEventArgs<TItem>(copy);
|
||||
if (OnSavingCancelled(scope, saveEventArgs))
|
||||
{
|
||||
scope.Complete();
|
||||
return OperationResult.Attempt.Fail<MoveOperationStatusType, TItem>(MoveOperationStatusType.FailedCancelledByEvent, evtMsgs); // causes rollback
|
||||
}
|
||||
|
||||
Repository.Save(copy);
|
||||
|
||||
// handle events for the copied node
|
||||
// figure out impacted content types
|
||||
var changes = ComposeContentTypeChanges(copy).ToArray();
|
||||
var args = changes.ToEventArgs();
|
||||
|
||||
OnUowRefreshedEntity(args);
|
||||
|
||||
OnChanged(scope, args);
|
||||
saveEventArgs.CanCancel = false;
|
||||
OnSaved(scope, saveEventArgs);
|
||||
|
||||
// Since this overload doesn't accept a user id, set the super users.
|
||||
Audit(AuditType.Save, Constants.Security.SuperUserId, copy.Id);
|
||||
|
||||
scope.Complete();
|
||||
}
|
||||
catch (DataOperationException<MoveOperationStatusType> ex)
|
||||
|
||||
@@ -231,7 +231,8 @@ namespace Umbraco.Core.Services.Implement
|
||||
{
|
||||
using (var scope = ScopeProvider.CreateScope())
|
||||
{
|
||||
if (scope.Events.DispatchCancelable(SavingDictionaryItem, this, new SaveEventArgs<IDictionaryItem>(dictionaryItem)))
|
||||
var saveEventArgs = new SaveEventArgs<IDictionaryItem>(dictionaryItem);
|
||||
if (scope.Events.DispatchCancelable(SavingDictionaryItem, this, saveEventArgs))
|
||||
{
|
||||
scope.Complete();
|
||||
return;
|
||||
@@ -243,7 +244,10 @@ namespace Umbraco.Core.Services.Implement
|
||||
// ensure the lazy Language callback is assigned
|
||||
|
||||
EnsureDictionaryItemLanguageCallback(dictionaryItem);
|
||||
scope.Events.Dispatch(SavedDictionaryItem, this, new SaveEventArgs<IDictionaryItem>(dictionaryItem, false));
|
||||
scope.Events.Dispatch(SavedDictionaryItem, this, new SaveEventArgs<IDictionaryItem>(dictionaryItem, false)
|
||||
{
|
||||
EventState = saveEventArgs.EventState
|
||||
});
|
||||
|
||||
Audit(AuditType.Save, "Save DictionaryItem", userId, dictionaryItem.Id, "DictionaryItem");
|
||||
scope.Complete();
|
||||
|
||||
@@ -343,11 +343,14 @@ namespace Umbraco.Core.Strings
|
||||
var isUpper = char.IsUpper(c); // false for digits, symbols...
|
||||
//var isLower = char.IsLower(c); // false for digits, symbols...
|
||||
|
||||
// what should I do with surrogates?
|
||||
// no idea, really, so they are not supported at the moment
|
||||
// what should I do with surrogates? - E.g emojis like 🎈
|
||||
// no idea, really, so they are not supported at the moment and we just continue
|
||||
var isPair = char.IsSurrogate(c);
|
||||
if (isPair)
|
||||
throw new NotSupportedException("Surrogate pairs are not supported.");
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
switch (state)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
|
||||
namespace Umbraco.Core.Telemetry
|
||||
{
|
||||
/// <summary>
|
||||
/// Used to get and create the site identifier
|
||||
/// </summary>
|
||||
public interface ISiteIdentifierService
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the site identifier
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
bool TryGetSiteIdentifier(out Guid siteIdentifier);
|
||||
|
||||
/// <summary>
|
||||
/// Tries to get the site identifier or otherwise create it if it doesn't exist.
|
||||
/// </summary>
|
||||
/// <param name="siteIdentifier"></param>
|
||||
/// <returns></returns>
|
||||
bool TryGetOrCreateSiteIdentifier(out Guid siteIdentifier);
|
||||
|
||||
/// <summary>
|
||||
/// Creates the site identifier and writes it to config.
|
||||
/// </summary>
|
||||
bool TryCreateSiteIdentifier(out Guid createdGuid);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Umbraco.Core.Configuration;
|
||||
using Umbraco.Core.Configuration.UmbracoSettings;
|
||||
using Umbraco.Core.Manifest;
|
||||
using Umbraco.Core.Telemetry.Models;
|
||||
|
||||
@@ -10,7 +9,7 @@ namespace Umbraco.Core.Telemetry
|
||||
/// <inheritdoc/>
|
||||
internal class TelemetryService : ITelemetryService
|
||||
{
|
||||
private readonly IUmbracoSettingsSection _settings;
|
||||
private readonly ISiteIdentifierService _siteIdentifierService;
|
||||
private readonly ManifestParser _manifestParser;
|
||||
|
||||
/// <summary>
|
||||
@@ -18,16 +17,16 @@ namespace Umbraco.Core.Telemetry
|
||||
/// </summary>
|
||||
public TelemetryService(
|
||||
ManifestParser manifestParser,
|
||||
IUmbracoSettingsSection settings)
|
||||
ISiteIdentifierService siteIdentifierService)
|
||||
{
|
||||
_manifestParser = manifestParser;
|
||||
_settings = settings;
|
||||
_siteIdentifierService = siteIdentifierService;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool TryGetTelemetryReportData(out TelemetryReportData telemetryReportData)
|
||||
{
|
||||
if (TryGetTelemetryId(out Guid telemetryId) is false)
|
||||
if (_siteIdentifierService.TryGetOrCreateSiteIdentifier(out Guid telemetryId) is false)
|
||||
{
|
||||
telemetryReportData = null;
|
||||
return false;
|
||||
@@ -42,20 +41,6 @@ namespace Umbraco.Core.Telemetry
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryGetTelemetryId(out Guid telemetryId)
|
||||
{
|
||||
// Parse telemetry string as a GUID & verify its a GUID and not some random string
|
||||
// since users may have messed with or decided to empty the app setting or put in something random
|
||||
if (Guid.TryParse(_settings.BackOffice.Id, out var parsedTelemetryId) is false)
|
||||
{
|
||||
telemetryId = Guid.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
telemetryId = parsedTelemetryId;
|
||||
return true;
|
||||
}
|
||||
|
||||
private IEnumerable<PackageTelemetry> GetPackageTelemetry()
|
||||
{
|
||||
List<PackageTelemetry> packages = new ();
|
||||
|
||||
@@ -136,6 +136,7 @@
|
||||
<Compile Include="Collections\EventClearingObservableCollection.cs" />
|
||||
<Compile Include="Constants-Sql.cs" />
|
||||
<Compile Include="Constants-SqlTemplates.cs" />
|
||||
<Compile Include="CryptoServiceProviderExtensions.cs" />
|
||||
<Compile Include="Events\UnattendedInstallEventArgs.cs" />
|
||||
<Compile Include="Help\HelpPageSettings.cs" />
|
||||
<Compile Include="Help\IHelpPageSettings.cs" />
|
||||
@@ -196,8 +197,13 @@
|
||||
<Compile Include="PropertyEditors\PropertyCacheCompression.cs" />
|
||||
<Compile Include="PropertyEditors\IPropertyCacheCompression.cs" />
|
||||
<Compile Include="PropertyEditors\UnPublishedContentPropertyCacheCompressionOptions.cs" />
|
||||
<Compile Include="Security\FileStreamSecurityValidator.cs" />
|
||||
<Compile Include="Security\IFileStreamSecurityAnalyzer.cs" />
|
||||
<Compile Include="Security\IFileStreamSecurityValidator.cs" />
|
||||
<Compile Include="Security\IHtmlSanitizer.cs" />
|
||||
<Compile Include="Security\IMarkdownSanitizer.cs" />
|
||||
<Compile Include="Security\NoopHtmlSanitizer.cs" />
|
||||
<Compile Include="Security\NoopMarkdownSanitizer.cs" />
|
||||
<Compile Include="Serialization\AutoInterningStringConverter.cs" />
|
||||
<Compile Include="Serialization\AutoInterningStringKeyCaseInsensitiveDictionaryConverter.cs" />
|
||||
<Compile Include="PropertyEditors\EyeDropperColorPickerConfiguration.cs" />
|
||||
@@ -400,6 +406,7 @@
|
||||
<Compile Include="Services\DateTypeServiceExtensions.cs" />
|
||||
<Compile Include="Services\PropertyValidationService.cs" />
|
||||
<Compile Include="Composing\TypeCollectionBuilderBase.cs" />
|
||||
<Compile Include="Telemetry\ISiteIdentifierService.cs" />
|
||||
<Compile Include="Telemetry\ITelemetryService.cs" />
|
||||
<Compile Include="Telemetry\Models\PackageTelemetry.cs" />
|
||||
<Compile Include="Telemetry\Models\TelemetryReportData.cs" />
|
||||
|
||||
@@ -5,10 +5,10 @@ using System.Threading;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.Configuration.UmbracoSettings;
|
||||
using Umbraco.Core.Events;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.Membership;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Core.Services.Changes;
|
||||
using Umbraco.Tests.TestHelpers.Entities;
|
||||
@@ -16,6 +16,7 @@ using Umbraco.Tests.Testing;
|
||||
using Umbraco.Tests.Testing.Objects.Accessors;
|
||||
using Umbraco.Web;
|
||||
using Umbraco.Web.Cache;
|
||||
using Umbraco.Web.PropertyEditors;
|
||||
using Umbraco.Web.PublishedCache;
|
||||
using Umbraco.Web.Routing;
|
||||
|
||||
@@ -238,5 +239,37 @@ namespace Umbraco.Tests.Cache
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void GroupsDataTypeEvents()
|
||||
{
|
||||
IDataType[] dataTypes = new[]
|
||||
{
|
||||
new DataType(new LabelPropertyEditor(Logger)) { Name = "Label (string)", DatabaseType = ValueStorageType.Ntext },
|
||||
new DataType(new LabelPropertyEditor(Logger)) { Name = "Label (integer)", DatabaseType = ValueStorageType.Integer },
|
||||
new DataType(new RadioButtonsPropertyEditor(Logger, Current.Services.TextService)) { Name = "Radiobox" },
|
||||
};
|
||||
|
||||
var definitions = dataTypes.SelectMany(x => new IEventDefinition[]
|
||||
{
|
||||
new EventDefinition<IDataTypeService, SaveEventArgs<IDataType>>(null, Current.Services.DataTypeService, new SaveEventArgs<IDataType>(x), "Saved"),
|
||||
});
|
||||
|
||||
var result = DistributedCacheBinder.GetGroupedEventList(definitions);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.AreEqual(dataTypes.Length, definitions.Count(), "Precondition is we have many definitions");
|
||||
Assert.AreEqual(1, result.Count(), "Unexpected number of reduced definitions");
|
||||
|
||||
foreach (var eventDefinition in result)
|
||||
{
|
||||
if (eventDefinition.Args is SaveEventArgs<IDataType> eventArgs)
|
||||
{
|
||||
Assert.AreEqual(dataTypes.Length, eventArgs.SavedEntities.Count());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.ContentEditing;
|
||||
using Umbraco.Tests.TestHelpers.Entities;
|
||||
using Umbraco.Tests.Testing;
|
||||
|
||||
namespace Umbraco.Tests.Models
|
||||
{
|
||||
[TestFixture]
|
||||
public class ContentTypeHistoryCleanupTests : UmbracoTestBase
|
||||
{
|
||||
[Test]
|
||||
public void Changing_Keep_all_Makes_ContentType_Dirty()
|
||||
{
|
||||
var contentType = MockedContentTypes.CreateBasicContentType();
|
||||
|
||||
Assert.IsFalse(contentType.IsDirty());
|
||||
|
||||
var newValue = 2;
|
||||
contentType.HistoryCleanup.KeepAllVersionsNewerThanDays = newValue;
|
||||
Assert.IsTrue(contentType.IsDirty());
|
||||
Assert.AreEqual(newValue, contentType.HistoryCleanup.KeepAllVersionsNewerThanDays);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Changing_Keep_latest_Makes_ContentType_Dirty()
|
||||
{
|
||||
var contentType = MockedContentTypes.CreateBasicContentType();
|
||||
|
||||
Assert.IsFalse(contentType.IsDirty());
|
||||
|
||||
var newValue = 2;
|
||||
contentType.HistoryCleanup.KeepLatestVersionPerDayForDays = newValue;
|
||||
Assert.IsTrue(contentType.IsDirty());
|
||||
Assert.AreEqual(newValue, contentType.HistoryCleanup.KeepLatestVersionPerDayForDays);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Changing_Prevent_Cleanup_Makes_ContentType_Dirty()
|
||||
{
|
||||
var contentType = MockedContentTypes.CreateBasicContentType();
|
||||
|
||||
Assert.IsFalse(contentType.IsDirty());
|
||||
|
||||
var newValue = true;
|
||||
contentType.HistoryCleanup.PreventCleanup = newValue;
|
||||
Assert.IsTrue(contentType.IsDirty());
|
||||
Assert.AreEqual(newValue, contentType.HistoryCleanup.PreventCleanup);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Replacing_History_Cleanup_Registers_As_Dirty()
|
||||
{
|
||||
var contentType = MockedContentTypes.CreateBasicContentType();
|
||||
Assert.IsFalse(contentType.IsDirty());
|
||||
|
||||
contentType.HistoryCleanup = new HistoryCleanup();
|
||||
|
||||
Assert.IsTrue(contentType.IsDirty());
|
||||
Assert.IsTrue(contentType.IsPropertyDirty(nameof(contentType.HistoryCleanup)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Replacing_History_Cleanup_Removes_Old_Dirty_History_Properties()
|
||||
{
|
||||
var contentType = MockedContentTypes.CreateBasicContentType();
|
||||
|
||||
contentType.Alias = "NewValue";
|
||||
contentType.HistoryCleanup.KeepAllVersionsNewerThanDays = 2;
|
||||
|
||||
contentType.PropertyChanged += (sender, args) =>
|
||||
{
|
||||
// Ensure that property changed is only invoked for history cleanup
|
||||
Assert.AreEqual(nameof(contentType.HistoryCleanup), args.PropertyName);
|
||||
};
|
||||
|
||||
// Since we're replacing the entire HistoryCleanup the changed property is no longer dirty, the entire HistoryCleanup is
|
||||
contentType.HistoryCleanup = new HistoryCleanup();
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.IsTrue(contentType.IsDirty());
|
||||
Assert.IsFalse(contentType.WasDirty());
|
||||
Assert.AreEqual(2, contentType.GetDirtyProperties().Count());
|
||||
Assert.IsTrue(contentType.IsPropertyDirty(nameof(contentType.HistoryCleanup)));
|
||||
Assert.IsTrue(contentType.IsPropertyDirty(nameof(contentType.Alias)));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Old_History_Cleanup_Reference_Doesnt_Make_Content_Type_Dirty()
|
||||
{
|
||||
var contentType = MockedContentTypes.CreateBasicContentType();
|
||||
var oldHistoryCleanup = contentType.HistoryCleanup;
|
||||
|
||||
contentType.HistoryCleanup = new HistoryCleanup();
|
||||
contentType.ResetDirtyProperties();
|
||||
contentType.ResetWereDirtyProperties();
|
||||
|
||||
oldHistoryCleanup.KeepAllVersionsNewerThanDays = 2;
|
||||
|
||||
Assert.IsFalse(contentType.IsDirty());
|
||||
Assert.IsFalse(contentType.WasDirty());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ using Umbraco.Web.Models;
|
||||
using Umbraco.Web;
|
||||
using Umbraco.Web.PropertyEditors;
|
||||
using System.Text;
|
||||
using Umbraco.Core.Security;
|
||||
|
||||
namespace Umbraco.Tests.PropertyEditors
|
||||
{
|
||||
@@ -95,7 +96,16 @@ namespace Umbraco.Tests.PropertyEditors
|
||||
}
|
||||
};
|
||||
var dataTypeService = new TestObjects.TestDataTypeService(
|
||||
new DataType(new ImageCropperPropertyEditor(Mock.Of<ILogger>(), mediaFileSystem, Mock.Of<IContentSection>(), Mock.Of<IDataTypeService>())) { Id = 1, Configuration = imageCropperConfiguration });
|
||||
new DataType(new ImageCropperPropertyEditor(
|
||||
Mock.Of<ILogger>(),
|
||||
mediaFileSystem,
|
||||
Mock.Of<IContentSection>(),
|
||||
Mock.Of<IDataTypeService>(),
|
||||
Mock.Of<IFileStreamSecurityValidator>()))
|
||||
{
|
||||
Id = 1,
|
||||
Configuration = imageCropperConfiguration
|
||||
});
|
||||
|
||||
var factory = new PublishedContentTypeFactory(Mock.Of<IPublishedModelFactory>(), new PropertyValueConverterCollection(Array.Empty<IPropertyValueConverter>()), dataTypeService);
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using Umbraco.Web.Routing;
|
||||
|
||||
namespace Umbraco.Tests.Routing
|
||||
{
|
||||
|
||||
[TestFixture]
|
||||
public class WebPathTests
|
||||
{
|
||||
|
||||
[Test]
|
||||
[TestCase("ftp://hello.com/", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("file:///hello.com/", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("ws://hello.com/", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("wss://hello.com/", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("https://hello.com:8080/", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("http://hello.com:8080/", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("https://hello.com/path", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("http://hello.com/path", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("https://hello.com/path?query=param", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("http://hello.com/path?query=param", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("https://hello.com/path#fragment", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("http://hello.com/path#fragment", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("https://hello.com/path?query=param#fragment", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("http://hello.com/path?query=param#fragment", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("https://hello.com:8080/path?query=param#fragment", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("http://hello.com:8080/path?query=param#fragment", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("//hello.com:8080/path?query=param#fragment", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("//hello.com:8080/path", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("//hello.com:8080", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("//hello.com", UriKind.Absolute, ExpectedResult = true)]
|
||||
[TestCase("/test/test.jpg", UriKind.Absolute, ExpectedResult = false)]
|
||||
[TestCase("/test", UriKind.Absolute, ExpectedResult = false)]
|
||||
[TestCase("test", UriKind.Absolute, ExpectedResult = false)]
|
||||
[TestCase("", UriKind.Absolute, ExpectedResult = false)]
|
||||
[TestCase(null, UriKind.Absolute, ExpectedResult = false)]
|
||||
[TestCase("this is not welformed", UriKind.Absolute, ExpectedResult = false)]
|
||||
[TestCase("ftp://hello.com/", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("file:///hello.com/", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("ws://hello.com/", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("wss://hello.com/", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("https://hello.com:8080/", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("http://hello.com:8080/", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("https://hello.com/path", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("http://hello.com/path", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("https://hello.com/path?query=param", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("http://hello.com/path?query=param", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("https://hello.com/path#fragment", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("http://hello.com/path#fragment", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("https://hello.com/path?query=param#fragment", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("http://hello.com/path?query=param#fragment", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("https://hello.com:8080/path?query=param#fragment", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("http://hello.com:8080/path?query=param#fragment", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("//hello.com:8080/path?query=param#fragment", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("//hello.com:8080/path", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("//hello.com:8080", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("//hello.com", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("/test/test.jpg", UriKind.Relative, ExpectedResult = true)]
|
||||
[TestCase("/test", UriKind.Relative, ExpectedResult = true)]
|
||||
[TestCase("test", UriKind.Relative, ExpectedResult = true)]
|
||||
[TestCase("", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase(null, UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("this is not welformed", UriKind.Relative, ExpectedResult = false)]
|
||||
[TestCase("ftp://hello.com/", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("file:///hello.com/", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("ws://hello.com/", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("wss://hello.com/", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("https://hello.com:8080/", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("http://hello.com:8080/", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("https://hello.com/path", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("http://hello.com/path", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("https://hello.com/path?query=param", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("http://hello.com/path?query=param", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("https://hello.com/path#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("http://hello.com/path#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("https://hello.com/path?query=param#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("http://hello.com/path?query=param#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("https://hello.com:8080/path?query=param#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("http://hello.com:8080/path?query=param#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("//hello.com:8080/path?query=param#fragment", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("//hello.com:8080/path", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("//hello.com:8080", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("//hello.com", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("/test/test.jpg", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("/test", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("test", UriKind.RelativeOrAbsolute, ExpectedResult = true)]
|
||||
[TestCase("", UriKind.RelativeOrAbsolute, ExpectedResult = false)]
|
||||
[TestCase(null, UriKind.RelativeOrAbsolute, ExpectedResult = false)]
|
||||
[TestCase("this is not welformed", UriKind.RelativeOrAbsolute, ExpectedResult = false)]
|
||||
public bool IsWellFormedWebPath(string? webPath, UriKind uriKind)
|
||||
{
|
||||
return WebPath.IsWellFormedWebPath(webPath, uriKind);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Core.Security;
|
||||
|
||||
namespace Umbraco.Tests.Security
|
||||
{
|
||||
[TestFixture]
|
||||
public class FileStreamSecurityValidatorTests
|
||||
{
|
||||
|
||||
[Test]
|
||||
public void IsConsideredSafe_True_NoAnalyzersPresent()
|
||||
{
|
||||
// Arrange
|
||||
var sut = new FileStreamSecurityValidator(Enumerable.Empty<IFileStreamSecurityAnalyzer>());
|
||||
|
||||
using var memoryStream = new MemoryStream();
|
||||
using var streamWriter = new StreamWriter(memoryStream);
|
||||
streamWriter.Write("TestContent");
|
||||
streamWriter.Flush();
|
||||
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
// Act
|
||||
var validationResult = sut.IsConsideredSafe(memoryStream);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(validationResult);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsConsideredSafe_True_NoAnalyzerMatchesType()
|
||||
{
|
||||
// Arrange
|
||||
var analyzerOne = new Mock<IFileStreamSecurityAnalyzer>();
|
||||
analyzerOne.Setup(analyzer => analyzer.ShouldHandle(It.IsAny<Stream>()))
|
||||
.Returns(false);
|
||||
var analyzerTwo = new Mock<IFileStreamSecurityAnalyzer>();
|
||||
analyzerTwo.Setup(analyzer => analyzer.ShouldHandle(It.IsAny<Stream>()))
|
||||
.Returns(false);
|
||||
|
||||
var sut = new FileStreamSecurityValidator(new List<IFileStreamSecurityAnalyzer>{analyzerOne.Object,analyzerTwo.Object});
|
||||
|
||||
using var memoryStream = new MemoryStream();
|
||||
using var streamWriter = new StreamWriter(memoryStream);
|
||||
streamWriter.Write("TestContent");
|
||||
streamWriter.Flush();
|
||||
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
// Act
|
||||
var validationResult = sut.IsConsideredSafe(memoryStream);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(validationResult);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsConsideredSafe_True_AllMatchingAnalyzersReturnTrue()
|
||||
{
|
||||
// Arrange
|
||||
var matchingAnalyzerOne = new Mock<IFileStreamSecurityAnalyzer>();
|
||||
matchingAnalyzerOne.Setup(analyzer => analyzer.ShouldHandle(It.IsAny<Stream>()))
|
||||
.Returns(true);
|
||||
matchingAnalyzerOne.Setup(analyzer => analyzer.IsConsideredSafe(It.IsAny<Stream>()))
|
||||
.Returns(true);
|
||||
|
||||
var matchingAnalyzerTwo = new Mock<IFileStreamSecurityAnalyzer>();
|
||||
matchingAnalyzerTwo.Setup(analyzer => analyzer.ShouldHandle(It.IsAny<Stream>()))
|
||||
.Returns(true);
|
||||
matchingAnalyzerTwo.Setup(analyzer => analyzer.IsConsideredSafe(It.IsAny<Stream>()))
|
||||
.Returns(true);
|
||||
|
||||
var unmatchedAnalyzer = new Mock<IFileStreamSecurityAnalyzer>();
|
||||
unmatchedAnalyzer.Setup(analyzer => analyzer.ShouldHandle(It.IsAny<Stream>()))
|
||||
.Returns(false);
|
||||
|
||||
var sut = new FileStreamSecurityValidator(new List<IFileStreamSecurityAnalyzer>{matchingAnalyzerOne.Object,matchingAnalyzerTwo.Object});
|
||||
|
||||
using var memoryStream = new MemoryStream();
|
||||
using var streamWriter = new StreamWriter(memoryStream);
|
||||
streamWriter.Write("TestContent");
|
||||
streamWriter.Flush();
|
||||
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
// Act
|
||||
var validationResult = sut.IsConsideredSafe(memoryStream);
|
||||
|
||||
// Assert
|
||||
Assert.IsTrue(validationResult);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void IsConsideredSafe_False_AnyMatchingAnalyzersReturnFalse()
|
||||
{
|
||||
// Arrange
|
||||
var saveMatchingAnalyzer = new Mock<IFileStreamSecurityAnalyzer>();
|
||||
saveMatchingAnalyzer.Setup(analyzer => analyzer.ShouldHandle(It.IsAny<Stream>()))
|
||||
.Returns(true);
|
||||
saveMatchingAnalyzer.Setup(analyzer => analyzer.IsConsideredSafe(It.IsAny<Stream>()))
|
||||
.Returns(true);
|
||||
|
||||
var unsafeMatchingAnalyzer = new Mock<IFileStreamSecurityAnalyzer>();
|
||||
unsafeMatchingAnalyzer.Setup(analyzer => analyzer.ShouldHandle(It.IsAny<Stream>()))
|
||||
.Returns(true);
|
||||
unsafeMatchingAnalyzer.Setup(analyzer => analyzer.IsConsideredSafe(It.IsAny<Stream>()))
|
||||
.Returns(false);
|
||||
|
||||
var unmatchedAnalyzer = new Mock<IFileStreamSecurityAnalyzer>();
|
||||
unmatchedAnalyzer.Setup(analyzer => analyzer.ShouldHandle(It.IsAny<Stream>()))
|
||||
.Returns(false);
|
||||
|
||||
var sut = new FileStreamSecurityValidator(new List<IFileStreamSecurityAnalyzer>{saveMatchingAnalyzer.Object,unsafeMatchingAnalyzer.Object});
|
||||
|
||||
using var memoryStream = new MemoryStream();
|
||||
using var streamWriter = new StreamWriter(memoryStream);
|
||||
streamWriter.Write("TestContent");
|
||||
streamWriter.Flush();
|
||||
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
// Act
|
||||
var validationResult = sut.IsConsideredSafe(memoryStream);
|
||||
|
||||
// Assert
|
||||
Assert.IsFalse(validationResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,6 +116,11 @@ namespace Umbraco.Tests.Testing
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void AddParameter(DbCommand cmd, PocoColumn column, object value)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public DbCommand CreateCommand(DbConnection connection, CommandType commandType, string sql, params object[] args)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
|
||||
@@ -145,6 +145,7 @@
|
||||
<Compile Include="ModelsBuilder\StringExtensions.cs" />
|
||||
<Compile Include="ModelsBuilder\UmbracoApplicationTests.cs" />
|
||||
<Compile Include="Models\ContentScheduleTests.cs" />
|
||||
<Compile Include="Models\ContentTypeHistoryCleanupTests.cs" />
|
||||
<Compile Include="Models\CultureImpactTests.cs" />
|
||||
<Compile Include="Models\ImageProcessorImageUrlGeneratorTest.cs" />
|
||||
<Compile Include="Models\PathValidationTests.cs" />
|
||||
@@ -169,9 +170,11 @@
|
||||
<Compile Include="PublishedContent\NuCacheTests.cs" />
|
||||
<Compile Include="Routing\MediaUrlProviderTests.cs" />
|
||||
<Compile Include="Routing\RoutableDocumentFilterTests.cs" />
|
||||
<Compile Include="Routing\WebPathTests.cs" />
|
||||
<Compile Include="Runtimes\StandaloneTests.cs" />
|
||||
<Compile Include="Routing\GetContentUrlsTests.cs" />
|
||||
<Compile Include="Scheduling\ContentVersionCleanup_Tests_UnitTests.cs" />
|
||||
<Compile Include="Security\FileStreamSecurityValidatorTests.cs" />
|
||||
<Compile Include="Serialization\AutoInterningStringConverterTests.cs" />
|
||||
<Compile Include="Scoping\ScopeUnitTests.cs" />
|
||||
<Compile Include="Services\AmbiguousEventTests.cs" />
|
||||
|
||||
+29684
-16413
File diff suppressed because it is too large
Load Diff
@@ -10,23 +10,23 @@
|
||||
"watch": "gulp watch"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.00.0"
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ace-builds": "1.4.2",
|
||||
"angular": "1.8.2",
|
||||
"angular-animate": "1.7.5",
|
||||
"angular-aria": "1.7.9",
|
||||
"angular-chart.js": "^1.1.1",
|
||||
"angular-cookies": "1.7.5",
|
||||
"angular-dynamic-locale": "0.1.37",
|
||||
"angular-i18n": "1.7.5",
|
||||
"angular-animate": "1.8.2",
|
||||
"angular-aria": "1.8.2",
|
||||
"angular-chart.js": "1.1.1",
|
||||
"angular-cookies": "1.8.2",
|
||||
"angular-dynamic-locale": "0.1.38",
|
||||
"angular-i18n": "1.8.2",
|
||||
"angular-local-storage": "0.7.1",
|
||||
"angular-messages": "1.7.5",
|
||||
"angular-mocks": "1.7.5",
|
||||
"angular-route": "1.7.5",
|
||||
"angular-sanitize": "1.7.5",
|
||||
"angular-touch": "1.7.5",
|
||||
"angular-messages": "1.8.2",
|
||||
"angular-mocks": "1.8.2",
|
||||
"angular-route": "1.8.2",
|
||||
"angular-sanitize": "1.8.2",
|
||||
"angular-touch": "1.8.2",
|
||||
"angular-ui-sortable": "0.19.0",
|
||||
"animejs": "2.2.0",
|
||||
"bootstrap-social": "5.1.1",
|
||||
@@ -35,15 +35,14 @@
|
||||
"diff": "3.5.0",
|
||||
"flatpickr": "4.6.9",
|
||||
"font-awesome": "4.7.0",
|
||||
"jquery": "^3.6.0",
|
||||
"jquery-ui-dist": "1.12.1",
|
||||
"jquery": "3.6.1",
|
||||
"jquery-ui-dist": "1.13.2",
|
||||
"jquery-ui-touch-punch": "0.2.3",
|
||||
"lazyload-js": "1.0.0",
|
||||
"moment": "2.22.2",
|
||||
"moment": "2.29.4",
|
||||
"ng-file-upload": "12.2.13",
|
||||
"nouislider": "15.5.0",
|
||||
"npm": "^6.14.7",
|
||||
"signalr": "2.4.0",
|
||||
"signalr": "2.4.3",
|
||||
"spectrum-colorpicker2": "2.0.8",
|
||||
"tinymce": "4.9.11",
|
||||
"typeahead.js": "0.11.1",
|
||||
|
||||
+15
-7
@@ -182,23 +182,31 @@
|
||||
}
|
||||
);
|
||||
|
||||
$scope.propertyEditorDisabled = function (property) {
|
||||
$scope.propertyEditorInherited = function (property) {
|
||||
if (property.unlockInvariantValue) {
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
|
||||
var contentLanguage = $scope.content.language;
|
||||
|
||||
var canEditCulture = !contentLanguage ||
|
||||
// If the property culture equals the content culture it can be edited
|
||||
property.culture === contentLanguage.culture ||
|
||||
// A culture-invariant property can only be edited by the default language variant
|
||||
(property.culture == null && contentLanguage.isDefault);
|
||||
// If the property culture equals the content culture it can be edited
|
||||
property.culture === contentLanguage.culture ||
|
||||
// A culture-invariant property can only be edited by the default language variant
|
||||
(property.culture == null && contentLanguage.isDefault);
|
||||
|
||||
var canEditSegment = property.segment === $scope.content.segment;
|
||||
|
||||
return !canEditCulture || !canEditSegment;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.propertyEditorDisabled = function (property) {
|
||||
if (property.readonly) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $scope.propertyEditorInherited(property);
|
||||
};
|
||||
}
|
||||
|
||||
var directive = {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* @description
|
||||
* This Resource perfomrs actions to common authentication tasks for the Umbraco backoffice user
|
||||
*
|
||||
* @requires $q
|
||||
* @requires $q
|
||||
* @requires $http
|
||||
* @requires umbRequestHelper
|
||||
* @requires angularHelper
|
||||
@@ -28,7 +28,7 @@ function authResource($q, $http, umbRequestHelper, angularHelper) {
|
||||
* });
|
||||
* </pre>
|
||||
* @returns {Promise} resourcePromise object
|
||||
*
|
||||
*
|
||||
*/
|
||||
get2FAProviders: function () {
|
||||
|
||||
@@ -118,7 +118,7 @@ function authResource($q, $http, umbRequestHelper, angularHelper) {
|
||||
* .then(function(data) {
|
||||
* //Do stuff for login...
|
||||
* });
|
||||
* </pre>
|
||||
* </pre>
|
||||
* @param {string} login Username of backoffice user
|
||||
* @param {string} password Password of backoffice user
|
||||
* @returns {Promise} resourcePromise object
|
||||
@@ -147,9 +147,9 @@ function authResource($q, $http, umbRequestHelper, angularHelper) {
|
||||
* There are not parameters for this since when the user has clicked on their invite email they will be partially
|
||||
* logged in (but they will not be approved) so we need to use this method to verify the non approved logged in user's details.
|
||||
* Using the getCurrentUser will not work since that only works for approved users
|
||||
* @returns {}
|
||||
* @returns {}
|
||||
*/
|
||||
getCurrentInvitedUser: function () {
|
||||
getCurrentInvitedUser: function () {
|
||||
return umbRequestHelper.resourcePromise(
|
||||
$http.get(
|
||||
umbRequestHelper.getApiUrl(
|
||||
@@ -173,7 +173,7 @@ function authResource($q, $http, umbRequestHelper, angularHelper) {
|
||||
* .then(function(data) {
|
||||
* //Do stuff for password reset request...
|
||||
* });
|
||||
* </pre>
|
||||
* </pre>
|
||||
* @param {string} email Email address of backoffice user
|
||||
* @returns {Promise} resourcePromise object
|
||||
*
|
||||
@@ -203,7 +203,7 @@ function authResource($q, $http, umbRequestHelper, angularHelper) {
|
||||
"PostRequestPasswordReset"), {
|
||||
email: email
|
||||
}),
|
||||
'Request password reset failed for email ' + email);
|
||||
'An email with password reset instructions will be sent to the specified address if it matched our records');
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -220,7 +220,7 @@ function authResource($q, $http, umbRequestHelper, angularHelper) {
|
||||
* .then(function(data) {
|
||||
* //Allow reset of password
|
||||
* });
|
||||
* </pre>
|
||||
* </pre>
|
||||
* @param {integer} userId User Id
|
||||
* @param {string} resetCode Password reset code
|
||||
* @returns {Promise} resourcePromise object
|
||||
@@ -251,14 +251,14 @@ function authResource($q, $http, umbRequestHelper, angularHelper) {
|
||||
}),
|
||||
'Password reset code validation failed for userId ' + userId + ', code' + resetCode);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name umbraco.resources.currentUserResource#getMembershipProviderConfig
|
||||
* @methodOf umbraco.resources.currentUserResource
|
||||
*
|
||||
* @description
|
||||
* Gets the configuration of the user membership provider which is used to configure the change password form
|
||||
* Gets the configuration of the user membership provider which is used to configure the change password form
|
||||
*/
|
||||
getMembershipProviderConfig: function () {
|
||||
return umbRequestHelper.resourcePromise(
|
||||
@@ -283,7 +283,7 @@ function authResource($q, $http, umbRequestHelper, angularHelper) {
|
||||
* .then(function(data) {
|
||||
* //Password set
|
||||
* });
|
||||
* </pre>
|
||||
* </pre>
|
||||
* @param {integer} userId User Id
|
||||
* @param {string} password New password
|
||||
* @param {string} confirmPassword Confirmation of new password
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* @ngdoc service
|
||||
* @name umbraco.services.mediaHelper
|
||||
* @description A helper object used for dealing with media items
|
||||
**/
|
||||
function mediaHelper(umbRequestHelper, $log) {
|
||||
* @ngdoc service
|
||||
* @name umbraco.services.mediaHelper
|
||||
* @description A helper object used for dealing with media items
|
||||
**/
|
||||
function mediaHelper(umbRequestHelper, $http, $log) {
|
||||
|
||||
//container of fileresolvers
|
||||
var _mediaFileResolvers = {};
|
||||
@@ -304,11 +304,6 @@ function mediaHelper(umbRequestHelper, $log) {
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
// Check if file is a svg
|
||||
if (this.getFileExtension(imagePath) === "svg") {
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
// If the path is not an image we cannot get a thumb
|
||||
if (!this.detectIfImageByExtension(imagePath)) {
|
||||
return null;
|
||||
@@ -399,6 +394,61 @@ function mediaHelper(umbRequestHelper, $log) {
|
||||
var lowered = filePath.toLowerCase();
|
||||
var ext = lowered.substr(lowered.lastIndexOf(".") + 1);
|
||||
return ext;
|
||||
},
|
||||
|
||||
/**
|
||||
* @ngdoc function
|
||||
* @name umbraco.services.mediaHelper#getProcessedImageUrl
|
||||
* @methodOf umbraco.services.mediaHelper
|
||||
* @function
|
||||
*
|
||||
* @description
|
||||
* Returns image URL with configured crop and other processing parameters.
|
||||
*
|
||||
* @param {string} imagePath Raw image path
|
||||
* @param {object} options Object describing image generation parameters:
|
||||
* {
|
||||
* width: <int>
|
||||
* height: <int>
|
||||
* focalPoint: {
|
||||
* left: <int>
|
||||
* top: <int>
|
||||
* },
|
||||
* mode: <string>
|
||||
* cacheBusterValue: <string>
|
||||
* crop: {
|
||||
* x1: <int>
|
||||
* x2: <int>
|
||||
* y1: <int>
|
||||
* y2: <int>
|
||||
* },
|
||||
* }
|
||||
*/
|
||||
getProcessedImageUrl: function (imagePath, options) {
|
||||
|
||||
if (!options) {
|
||||
return imagePath;
|
||||
}
|
||||
|
||||
return umbRequestHelper.resourcePromise(
|
||||
$http.get(
|
||||
umbRequestHelper.getApiUrl(
|
||||
"imagesApiBaseUrl",
|
||||
"GetProcessedImageUrl",
|
||||
{
|
||||
imagePath,
|
||||
width: options.width,
|
||||
height: options.height,
|
||||
focalPointLeft: options.focalPoint ? options.focalPoint.left : null,
|
||||
focalPointTop: options.focalPoint ? options.focalPoint.top : null,
|
||||
mode: options.mode,
|
||||
cacheBusterValue: options.cacheBusterValue,
|
||||
cropX1: options.crop ? options.crop.x1 : null,
|
||||
cropX2: options.crop ? options.crop.x2 : null,
|
||||
cropY1: options.crop ? options.crop.y1 : null,
|
||||
cropY2: options.crop ? options.crop.y2 : null
|
||||
})),
|
||||
"Failed to retrieve processed image URL for image: " + imagePath);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
* A service containing all logic for all of the Umbraco TinyMCE plugins
|
||||
*/
|
||||
function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, stylesheetResource, macroResource, macroService,
|
||||
$routeParams, umbRequestHelper, angularHelper, userService, editorService, entityResource, eventsService, localStorageService) {
|
||||
$routeParams, umbRequestHelper, angularHelper, userService, editorService, entityResource, eventsService, localStorageService, mediaHelper) {
|
||||
|
||||
//These are absolutely required in order for the macros to render inline
|
||||
//we put these as extended elements because they get merged on top of the normal allowed elements by tiny mce
|
||||
var extendedValidElements = "@[id|class|style],-div[id|dir|class|align|style],ins[datetime|cite],-ul[class|style],-li[class|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align],span[id|class|style|lang]";
|
||||
var extendedValidElements = "@[id|class|style],-div[id|dir|class|align|style],ins[datetime|cite],-ul[class|style],-li[class|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align],span[id|class|style|lang],figure,figcaption";
|
||||
var fallbackStyles = [{ title: "Page header", block: "h2" }, { title: "Section header", block: "h3" }, { title: "Paragraph header", block: "h4" }, { title: "Normal", block: "p" }, { title: "Quote", block: "blockquote" }, { title: "Code", block: "code" }];
|
||||
// these languages are available for localization
|
||||
var availableLanguages = [
|
||||
@@ -222,9 +222,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
|
||||
}
|
||||
|
||||
function uploadImageHandler(blobInfo, success, failure, progress){
|
||||
let xhr, formData;
|
||||
|
||||
xhr = new XMLHttpRequest();
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('POST', Umbraco.Sys.ServerVariables.umbracoUrls.tinyMceApiBaseUrl + 'UploadImage');
|
||||
|
||||
xhr.onloadstart = function(e) {
|
||||
@@ -248,17 +246,29 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
|
||||
};
|
||||
|
||||
xhr.onload = function () {
|
||||
let json;
|
||||
|
||||
if (xhr.status < 200 || xhr.status >= 300) {
|
||||
failure('HTTP Error: ' + xhr.status);
|
||||
return;
|
||||
}
|
||||
|
||||
json = JSON.parse(xhr.responseText);
|
||||
const data = xhr.responseText;
|
||||
|
||||
if (!data.length > 1) {
|
||||
failure('Unrecognized text string: ' + data);
|
||||
return;
|
||||
}
|
||||
|
||||
let json = {};
|
||||
|
||||
try {
|
||||
json = JSON.parse(data);
|
||||
} catch (e) {
|
||||
failure('Invalid JSON: ' + data + ' - ' + e.message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!json || typeof json.tmpLocation !== 'string') {
|
||||
failure('Invalid JSON: ' + xhr.responseText);
|
||||
failure('Invalid JSON: ' + data);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -271,7 +281,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
|
||||
success(blobInfo.blobUri());
|
||||
};
|
||||
|
||||
formData = new FormData();
|
||||
const formData = new FormData();
|
||||
formData.append('file', blobInfo.blob(), blobInfo.blob().name);
|
||||
|
||||
xhr.send(formData);
|
||||
@@ -292,21 +302,25 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
|
||||
}
|
||||
|
||||
function sizeImageInEditor(editor, imageDomElement, imgUrl) {
|
||||
|
||||
var size = editor.dom.getSize(imageDomElement);
|
||||
|
||||
if (editor.settings.maxImageSize && editor.settings.maxImageSize !== 0) {
|
||||
var newSize = imageHelper.scaleToMaxSize(editor.settings.maxImageSize, size.w, size.h);
|
||||
|
||||
|
||||
editor.dom.setAttrib(imageDomElement, 'width', newSize.width);
|
||||
editor.dom.setAttrib(imageDomElement, 'height', newSize.height);
|
||||
|
||||
// Images inserted via Media Picker will have a URL we can use for ImageResizer QueryStrings
|
||||
// Images pasted/dragged in are not persisted to media until saved & thus will need to be added
|
||||
if(imgUrl){
|
||||
var src = imgUrl + "?width=" + newSize.width + "&height=" + newSize.height;
|
||||
editor.dom.setAttrib(imageDomElement, 'data-mce-src', src);
|
||||
if (imgUrl) {
|
||||
mediaHelper.getProcessedImageUrl(imgUrl,
|
||||
{
|
||||
width: newSize.width,
|
||||
height: newSize.height
|
||||
})
|
||||
.then(function (resizedImgUrl) {
|
||||
editor.dom.setAttrib(imageDomElement, 'data-mce-src', resizedImgUrl);
|
||||
});
|
||||
}
|
||||
|
||||
editor.execCommand("mceAutoResize", false, null, null);
|
||||
@@ -431,7 +445,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
|
||||
|
||||
|
||||
if (args.htmlId) {
|
||||
config.selector = "#" + args.htmlId;
|
||||
config.selector = `[id="${args.htmlId}"]`;
|
||||
} else if (args.target) {
|
||||
config.target = args.target;
|
||||
}
|
||||
@@ -651,21 +665,19 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
|
||||
stateSelector: 'img[data-udi]',
|
||||
onclick: function () {
|
||||
|
||||
|
||||
var selectedElm = editor.selection.getNode(),
|
||||
currentTarget,
|
||||
imgDomElement;
|
||||
currentTarget;
|
||||
|
||||
if (selectedElm.nodeName === 'IMG') {
|
||||
var img = $(selectedElm);
|
||||
imgDomElement = selectedElm;
|
||||
|
||||
var hasUdi = img.attr("data-udi") ? true : false;
|
||||
var hasDataTmpImg = img.attr("data-tmpimg") ? true : false;
|
||||
|
||||
currentTarget = {
|
||||
altText: img.attr("alt"),
|
||||
url: img.attr("src")
|
||||
url: img.attr("src"),
|
||||
caption: img.attr('data-caption')
|
||||
};
|
||||
|
||||
if (hasUdi) {
|
||||
@@ -682,85 +694,80 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
|
||||
userService.getCurrentUser().then(function (userData) {
|
||||
if (callback) {
|
||||
angularHelper.safeApply($rootScope, function() {
|
||||
callback(currentTarget, userData, imgDomElement);
|
||||
callback(currentTarget, userData);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
insertMediaInEditor: function (editor, img, imgDomElement) {
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name umbraco.services.tinyMceService#insetMediaInEditor
|
||||
* @methodOf umbraco.services.tinyMceService
|
||||
*
|
||||
* @description
|
||||
* Inserts the image element in tinymce plugin
|
||||
*
|
||||
* @param {Object} editor the TinyMCE editor instance
|
||||
*/
|
||||
insertMediaInEditor: function (editor, img) {
|
||||
if (img) {
|
||||
// imgElement is only definied if updating an image
|
||||
// if null/undefinied then its a BRAND new image
|
||||
if(imgDomElement){
|
||||
// Check if the img src has changed
|
||||
// If it has we will need to do some resizing/recalc again
|
||||
var hasImageSrcChanged = false;
|
||||
// We need to create a NEW DOM <img> element to insert
|
||||
// setting an attribute of ID to __mcenew, so we can gather a reference to the node, to be able to update its size accordingly to the size of the image.
|
||||
var data = {
|
||||
alt: img.altText || "",
|
||||
src: (img.url) ? img.url : "nothing.jpg",
|
||||
id: "__mcenew",
|
||||
"data-udi": img.udi,
|
||||
"data-caption": img.caption
|
||||
};
|
||||
var newImage = editor.dom.createHTML('img', data);
|
||||
var parentElement = editor.selection.getNode().parentElement;
|
||||
|
||||
if(img.url !== editor.dom.getAttrib(imgDomElement, "src")){
|
||||
hasImageSrcChanged = true;
|
||||
if (img.caption) {
|
||||
var figCaption = editor.dom.createHTML('figcaption', {}, img.caption);
|
||||
var combined = newImage + figCaption;
|
||||
|
||||
if (parentElement.nodeName !== 'FIGURE') {
|
||||
var fragment = editor.dom.createHTML('figure', {}, combined);
|
||||
editor.selection.setContent(fragment);
|
||||
}
|
||||
|
||||
// If null/undefinied it will remove the attribute
|
||||
editor.dom.setAttrib(imgDomElement, "alt", img.altText);
|
||||
|
||||
// It's possible to pick a NEW image - so need to ensure this gets updated
|
||||
if(img.udi){
|
||||
editor.dom.setAttrib(imgDomElement, "data-udi", img.udi);
|
||||
else {
|
||||
parentElement.innerHTML = combined;
|
||||
}
|
||||
|
||||
// It's possible to pick a NEW image - so need to ensure this gets updated
|
||||
if(img.url){
|
||||
editor.dom.setAttrib(imgDomElement, "src", img.url);
|
||||
}
|
||||
|
||||
// Remove width & height attributes (ONLY if imgSrc changed)
|
||||
// So native image size is used as this needed to re-calc width & height
|
||||
// For the function sizeImageInEditor() & apply the image resizing querystrings etc..
|
||||
if(hasImageSrcChanged){
|
||||
editor.dom.setAttrib(imgDomElement, "width", null);
|
||||
editor.dom.setAttrib(imgDomElement, "height", null);
|
||||
|
||||
//Re-calc the image dimensions
|
||||
sizeImageInEditor(editor, imgDomElement, img.url);
|
||||
}
|
||||
|
||||
} else{
|
||||
// We need to create a NEW DOM <img> element to insert
|
||||
// setting an attribute of ID to __mcenew, so we can gather a reference to the node, to be able to update its size accordingly to the size of the image.
|
||||
var data = {
|
||||
alt: img.altText || "",
|
||||
src: (img.url) ? img.url : "nothing.jpg",
|
||||
id: "__mcenew",
|
||||
"data-udi": img.udi
|
||||
};
|
||||
|
||||
editor.selection.setContent(editor.dom.createHTML('img', data));
|
||||
|
||||
// Using settimeout to wait for a DoM-render, so we can find the new element by ID.
|
||||
$timeout(function () {
|
||||
|
||||
var imgElm = editor.dom.get("__mcenew");
|
||||
editor.dom.setAttrib(imgElm, "id", null);
|
||||
|
||||
// When image is loaded we are ready to call sizeImageInEditor.
|
||||
var onImageLoaded = function() {
|
||||
sizeImageInEditor(editor, imgElm, img.url);
|
||||
editor.fire("Change");
|
||||
}
|
||||
|
||||
// Check if image already is loaded.
|
||||
if(imgElm.complete === true) {
|
||||
onImageLoaded();
|
||||
} else {
|
||||
imgElm.onload = onImageLoaded;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
else {
|
||||
//if caption is removed, remove the figure element
|
||||
if (parentElement.nodeName === 'FIGURE') {
|
||||
parentElement.parentElement.innerHTML = newImage;
|
||||
}
|
||||
else {
|
||||
editor.selection.setContent(newImage);
|
||||
}
|
||||
}
|
||||
|
||||
// Using settimeout to wait for a DoM-render, so we can find the new element by ID.
|
||||
$timeout(function () {
|
||||
|
||||
var imgElm = editor.dom.get("__mcenew");
|
||||
editor.dom.setAttrib(imgElm, "id", null);
|
||||
|
||||
// When image is loaded we are ready to call sizeImageInEditor.
|
||||
var onImageLoaded = function() {
|
||||
sizeImageInEditor(editor, imgElm, img.url);
|
||||
editor.fire("Change");
|
||||
}
|
||||
|
||||
// Check if image already is loaded.
|
||||
if(imgElm.complete === true) {
|
||||
onImageLoaded();
|
||||
} else {
|
||||
imgElm.onload = onImageLoaded;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1243,7 +1250,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
|
||||
|
||||
// the href might be an external url, so check the value for an anchor/qs
|
||||
// href has the anchor re-appended later, hence the reset here to avoid duplicating the anchor
|
||||
if (!target.anchor) {
|
||||
if (!target.anchor && href) {
|
||||
var urlParts = href.split(/(#|\?)/);
|
||||
if (urlParts.length === 3) {
|
||||
href = urlParts[0];
|
||||
@@ -1444,7 +1451,8 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
|
||||
// Then we need to add an event listener to the editor
|
||||
// That will update native browser drag & drop events
|
||||
// To update the icon to show you can NOT drop something into the editor
|
||||
var toolbarItems = args.editor.settings.toolbar.split(" ");
|
||||
|
||||
var toolbarItems = args.editor.settings.toolbar === false ? [] : args.editor.settings.toolbar.split(" ");
|
||||
if(isMediaPickerEnabled(toolbarItems) === false){
|
||||
// Wire up the event listener
|
||||
args.editor.on('dragend dragover draggesture dragdrop drop drag', function (e) {
|
||||
@@ -1602,17 +1610,27 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
|
||||
syncContent();
|
||||
});
|
||||
|
||||
// When the element is removed from the DOM, we need to terminate
|
||||
// any active watchers to ensure scopes are disposed and do not leak.
|
||||
// No need to sync content as that has already happened.
|
||||
args.editor.on('remove', () => stopWatch());
|
||||
|
||||
args.editor.on('ObjectResized', function (e) {
|
||||
var qs = "?width=" + e.width + "&height=" + e.height + "&mode=max";
|
||||
var srcAttr = $(e.target).attr("src");
|
||||
var path = srcAttr.split("?")[0];
|
||||
$(e.target).attr("data-mce-src", path + qs);
|
||||
mediaHelper.getProcessedImageUrl(path, {
|
||||
width: e.width,
|
||||
height: e.height,
|
||||
mode: "max"
|
||||
}).then(function (resizedPath) {
|
||||
$(e.target).attr("data-mce-src", resizedPath);
|
||||
});
|
||||
|
||||
syncContent();
|
||||
});
|
||||
|
||||
args.editor.on('Dirty', function (e) {
|
||||
syncContent(); // Set model.value to the RTE's content
|
||||
syncContent(); // Set model.value to the RTE's content
|
||||
});
|
||||
|
||||
let self = this;
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
});
|
||||
|
||||
var saveProperties = _.map(realProperties, function (p) {
|
||||
var saveProperty = _.pick(p, 'id', 'alias', 'description', 'validation', 'label', 'sortOrder', 'dataTypeId', 'groupId', 'memberCanEdit', 'showOnMemberProfile', 'isSensitiveData', 'allowCultureVariant', 'allowSegmentVariant', 'labelOnTop');
|
||||
var saveProperty = _.pick(p, 'id', 'alias', 'description', 'validation', 'label', 'sortOrder', 'dataTypeId', 'dataTypeKey', 'groupId', 'memberCanEdit', 'showOnMemberProfile', 'isSensitiveData', 'allowCultureVariant', 'allowSegmentVariant', 'labelOnTop');
|
||||
return saveProperty;
|
||||
});
|
||||
|
||||
|
||||
@@ -128,13 +128,13 @@
|
||||
|
||||
<!-- Links -->
|
||||
<div class="umb-help-section" data-element="help-links" ng-if="vm.hasAccessToSettings">
|
||||
<a data-element="help-link-umbraco-tv" class="umb-help-badge" href="https://umbraco.tv?utm_source=core&utm_medium=help&utm_content=link&utm_campaign=tv" target="_blank" rel="noopener">
|
||||
<a data-element="help-link-umbraco-tv" class="umb-help-badge" href="https://umbra.co/ulb" target="_blank" rel="noopener">
|
||||
<umb-icon icon="icon-tv-old" class="umb-help-badge__icon"></umb-icon>
|
||||
<div class="umb-help-badge__title">
|
||||
<localize key="help_umbracoTv">Visit umbraco.tv</localize>
|
||||
<localize key="help_umbracoLearningBase">Watch our free tutorial videos</localize>
|
||||
</div>
|
||||
<small>
|
||||
<localize key="help_theBestUmbracoVideoTutorials">The best Umbraco video tutorials</localize>
|
||||
<localize key="help_umbracoLearningBaseDescription">on the Umbraco Learning Base</localize>
|
||||
</small>
|
||||
</a>
|
||||
|
||||
|
||||
+53
-46
@@ -3,25 +3,35 @@ angular.module("umbraco")
|
||||
function ($scope, localizationService, entityResource, editorService, overlayService, eventsService, mediaHelper) {
|
||||
|
||||
var unsubscribe = [];
|
||||
var vm = this;
|
||||
|
||||
|
||||
const vm = this;
|
||||
|
||||
vm.loading = true;
|
||||
vm.model = $scope.model;
|
||||
vm.mediaEntry = vm.model.mediaEntry;
|
||||
vm.currentCrop = null;
|
||||
|
||||
localizationService.localizeMany([
|
||||
vm.model.createFlow ? "general_cancel" : "general_close",
|
||||
vm.model.createFlow ? "general_create" : "buttons_submitChanges"
|
||||
]).then(function (data) {
|
||||
vm.closeLabel = data[0];
|
||||
vm.submitLabel = data[1];
|
||||
});
|
||||
|
||||
vm.title = "";
|
||||
|
||||
vm.focalPointChanged = focalPointChanged;
|
||||
vm.onImageLoaded = onImageLoaded;
|
||||
vm.openMedia = openMedia;
|
||||
vm.repickMedia = repickMedia;
|
||||
vm.selectCrop = selectCrop;
|
||||
vm.deselectCrop = deselectCrop;
|
||||
vm.resetCrop = resetCrop;
|
||||
vm.submitAndClose = submitAndClose;
|
||||
vm.close = close;
|
||||
|
||||
function init() {
|
||||
|
||||
localizationService.localizeMany([
|
||||
vm.model.createFlow ? "general_cancel" : "general_close",
|
||||
vm.model.createFlow ? "general_create" : "buttons_submitChanges"
|
||||
]).then(data => {
|
||||
vm.closeLabel = data[0];
|
||||
vm.submitLabel = data[1];
|
||||
});
|
||||
|
||||
updateMedia();
|
||||
|
||||
unsubscribe.push(eventsService.on("editors.media.saved", function(name, args) {
|
||||
@@ -35,6 +45,7 @@ angular.module("umbraco")
|
||||
function updateMedia() {
|
||||
|
||||
vm.loading = true;
|
||||
|
||||
entityResource.getById(vm.mediaEntry.mediaKey, "Media").then(function (mediaEntity) {
|
||||
vm.media = mediaEntity;
|
||||
vm.imageSrc = mediaHelper.resolveFileFromEntity(mediaEntity, true);
|
||||
@@ -42,11 +53,11 @@ angular.module("umbraco")
|
||||
vm.hasDimensions = false;
|
||||
vm.isCroppable = false;
|
||||
|
||||
localizationService.localize("mediaPicker_editMediaEntryLabel", [vm.media.name, vm.model.documentName]).then(function (data) {
|
||||
localizationService.localize("mediaPicker_editMediaEntryLabel", [vm.media.name, vm.model.documentName]).then(data => {
|
||||
vm.title = data;
|
||||
});
|
||||
}, function () {
|
||||
localizationService.localize("mediaPicker_deletedItem").then(function (localized) {
|
||||
localizationService.localize("mediaPicker_deletedItem").then(localized => {
|
||||
vm.media = {
|
||||
name: localized,
|
||||
icon: "icon-picture",
|
||||
@@ -58,15 +69,12 @@ angular.module("umbraco")
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
vm.onImageLoaded = onImageLoaded;
|
||||
|
||||
function onImageLoaded(isCroppable, hasDimensions) {
|
||||
vm.isCroppable = isCroppable;
|
||||
vm.hasDimensions = hasDimensions;
|
||||
};
|
||||
|
||||
|
||||
vm.repickMedia = repickMedia;
|
||||
}
|
||||
|
||||
function repickMedia() {
|
||||
vm.model.propertyEditor.changeMediaFor(vm.model.mediaEntry, onMediaReplaced);
|
||||
}
|
||||
@@ -82,24 +90,23 @@ angular.module("umbraco")
|
||||
//
|
||||
updateMedia();
|
||||
}
|
||||
|
||||
vm.openMedia = openMedia;
|
||||
|
||||
function openMedia() {
|
||||
|
||||
var mediaEditor = {
|
||||
const mediaEditor = {
|
||||
id: vm.mediaEntry.mediaKey,
|
||||
submit: function () {
|
||||
submit: () => {
|
||||
editorService.close();
|
||||
},
|
||||
close: function () {
|
||||
close: () => {
|
||||
editorService.close();
|
||||
}
|
||||
};
|
||||
|
||||
editorService.mediaEditor(mediaEditor);
|
||||
}
|
||||
|
||||
|
||||
vm.focalPointChanged = function(left, top) {
|
||||
function focalPointChanged(left, top) {
|
||||
//update the model focalpoint value
|
||||
vm.mediaEntry.focalPoint = {
|
||||
left: left,
|
||||
@@ -109,22 +116,17 @@ angular.module("umbraco")
|
||||
//set form to dirty to track changes
|
||||
setDirty();
|
||||
}
|
||||
|
||||
|
||||
|
||||
vm.selectCrop = selectCrop;
|
||||
|
||||
function selectCrop(targetCrop) {
|
||||
vm.currentCrop = targetCrop;
|
||||
setDirty();
|
||||
// TODO: start watchin values of crop, first when changed set to dirty.
|
||||
};
|
||||
|
||||
vm.deselectCrop = deselectCrop;
|
||||
}
|
||||
|
||||
function deselectCrop() {
|
||||
vm.currentCrop = null;
|
||||
};
|
||||
|
||||
vm.resetCrop = resetCrop;
|
||||
}
|
||||
|
||||
function resetCrop() {
|
||||
if (vm.currentCrop) {
|
||||
$scope.$evalAsync( () => {
|
||||
@@ -138,18 +140,22 @@ angular.module("umbraco")
|
||||
vm.imageCropperForm.$setDirty();
|
||||
}
|
||||
|
||||
|
||||
vm.submitAndClose = function () {
|
||||
function submitAndClose() {
|
||||
if (vm.model && vm.model.submit) {
|
||||
vm.model.submit(vm.model);
|
||||
}
|
||||
}
|
||||
|
||||
vm.close = function () {
|
||||
if (vm.model && vm.model.close) {
|
||||
if (vm.model.createFlow === true || vm.imageCropperForm.$dirty === true) {
|
||||
var labels = vm.model.createFlow === true ? ["mediaPicker_confirmCancelMediaEntryCreationHeadline", "mediaPicker_confirmCancelMediaEntryCreationMessage"] : ["prompt_discardChanges", "mediaPicker_confirmCancelMediaEntryHasChanges"];
|
||||
localizationService.localizeMany(labels).then(function (localizations) {
|
||||
|
||||
function close() {
|
||||
if (vm.model && vm.model.close)
|
||||
{
|
||||
if (vm.model.createFlow === true || vm.imageCropperForm.$dirty === true)
|
||||
{
|
||||
const labelKeys = vm.model.createFlow === true
|
||||
? ["mediaPicker_confirmCancelMediaEntryCreationHeadline", "mediaPicker_confirmCancelMediaEntryCreationMessage"]
|
||||
: ["prompt_discardChanges", "mediaPicker_confirmCancelMediaEntryHasChanges"];
|
||||
|
||||
localizationService.localizeMany(labelKeys).then(localizations => {
|
||||
const confirm = {
|
||||
title: localizations[0],
|
||||
view: "default",
|
||||
@@ -157,11 +163,11 @@ angular.module("umbraco")
|
||||
submitButtonLabelKey: "general_discard",
|
||||
submitButtonStyle: "danger",
|
||||
closeButtonLabelKey: "prompt_stay",
|
||||
submit: function () {
|
||||
submit: () => {
|
||||
overlayService.close();
|
||||
vm.model.close(vm.model);
|
||||
},
|
||||
close: function () {
|
||||
close: () => {
|
||||
overlayService.close();
|
||||
}
|
||||
};
|
||||
@@ -175,6 +181,7 @@ angular.module("umbraco")
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
$scope.$on("$destroy", function () {
|
||||
unsubscribe.forEach(x => x());
|
||||
});
|
||||
|
||||
@@ -195,7 +195,7 @@
|
||||
</div>
|
||||
|
||||
<div ng-messages="vm.requestPasswordResetForm.$error" class="control-group" ng-show="vm.requestPasswordResetForm.$invalid">
|
||||
<p ng-message="auth" class="text-error" role="alert" tabindex="0">{{vm.errorMsg}}</p>
|
||||
<p ng-message="auth" class="text-info" role="alert" tabindex="0">{{vm.errorMsg}}</p>
|
||||
</div>
|
||||
|
||||
<div class="control-group" ng-show="vm.showEmailResetConfirmation">
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
data-element="property-{{property.alias}}"
|
||||
ng-repeat="property in tab.properties track by property.alias"
|
||||
property="property"
|
||||
show-inherit="propertyEditorDisabled(property)"
|
||||
show-inherit="propertyEditorInherited(property)"
|
||||
inherits-from="defaultVariant.displayName">
|
||||
|
||||
<div ng-class="{'o-40 cursor-not-allowed': propertyEditorDisabled(property) }">
|
||||
@@ -41,7 +41,7 @@
|
||||
data-element="property-{{property.alias}}"
|
||||
ng-repeat="property in group.properties track by property.alias"
|
||||
property="property"
|
||||
show-inherit="propertyEditorDisabled(property)"
|
||||
show-inherit="propertyEditorInherited(property)"
|
||||
inherits-from="defaultVariant.displayName">
|
||||
|
||||
<div ng-class="{'o-40 cursor-not-allowed': propertyEditorDisabled(property) }">
|
||||
|
||||
+2
-2
@@ -45,10 +45,10 @@
|
||||
show-inherit="vm.model.variants.length > 1 && !property.culture && !activeVariant.language.isDefault"
|
||||
inherits-from="defaultVariant.language.name">
|
||||
|
||||
<div ng-class="{'o-40 cursor-not-allowed': vm.model.variants.length > 1 && !activeVariant.language.isDefault && !property.culture && !property.unlockInvariantValue}">
|
||||
<div ng-class="{'o-40 cursor-not-allowed': property.readonly || (vm.model.variants.length > 1 && !activeVariant.language.isDefault && !property.culture && !property.unlockInvariantValue)}">
|
||||
<umb-property-editor
|
||||
model="property"
|
||||
preview="vm.model.variants.length > 1 && !activeVariant.language.isDefault && !property.culture && !property.unlockInvariantValue">
|
||||
preview="property.readonly || (vm.model.variants.length > 1 && !activeVariant.language.isDefault && !property.culture && !property.unlockInvariantValue)">
|
||||
</umb-property-editor>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</li>
|
||||
<li class="umb-content-grid__details-item" ng-repeat="property in contentProperties">
|
||||
<div class="umb-content-grid__details-label">{{ property.header }}:</div>
|
||||
<div class="umb-content-grid__details-value">{{ item[property.alias] }}</div>
|
||||
<div class="umb-content-grid__details-value">{{ item[property.alias].expression ? item[property.alias].expression({value: item[property.alias].value}) : item[property.alias] }}</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
@@ -57,10 +57,10 @@
|
||||
</umb-variant-state>
|
||||
</div>
|
||||
<div class="umb-table-cell" ng-repeat="column in vm.itemProperties track by column.alias">
|
||||
<span title="{{column.header}}: {{item[column.alias]}}">
|
||||
<span title="{{column.header}}: {{item[column.alias].expression ? item[column.alias].expression({value: item[column.alias].value}) : item[column.alias]}}">
|
||||
|
||||
<div ng-if="!column.isSensitive">
|
||||
{{item[column.alias]}}
|
||||
{{item[column.alias].expression ? item[column.alias].expression({value: item[column.alias].value}) : item[column.alias]}}
|
||||
</div>
|
||||
|
||||
<em ng-show="column.isSensitive" class="muted">
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<localize key="settingsDashboard_bulletPointTwo">Ask a question in the <a class="btn-link -underline" href="https://our.umbraco.com/forum" target="_blank" rel="noopener">Community Forum</a></localize>
|
||||
</li>
|
||||
<li>
|
||||
<localize key="settingsDashboard_bulletPointThree">Watch our <a class="btn-link -underline" href="https://umbraco.tv" target="_blank" rel="noopener">tutorial videos</a> (some are free, some require a subscription)</localize>
|
||||
<localize key="settingsDashboard_bulletPointTutorials">Watch our free <a class="btn-link -underline" href="https://umbra.co/ulb" target="_blank" rel="noopener">tutorial videos on the Umbraco Learning Base</a></localize>
|
||||
</li>
|
||||
<li>
|
||||
<localize key="settingsDashboard_bulletPointFour">Find out about our <a class="btn-link -underline" href="https://umbraco.com/products/" target="_blank" rel="noopener">productivity boosting tools and commercial support</a></localize>
|
||||
|
||||
@@ -158,7 +158,7 @@ function MemberEditController($scope, $routeParams, $location, $http, $q, appSta
|
||||
//it's a child item, just sync the ui node to the parent
|
||||
navigationService.syncTree({ tree: "member", path: path.substring(0, path.lastIndexOf(",")).split(","), forceReload: initialLoad !== true });
|
||||
|
||||
//if this is a child of a list view and it's the initial load of the editor, we need to get the tree node
|
||||
//if this is a child of a list view and it's the initial load of the editor, we need to get the tree node
|
||||
// from the server so that we can load in the actions menu.
|
||||
umbRequestHelper.resourcePromise(
|
||||
$http.get(content.treeNodeUrl),
|
||||
@@ -189,7 +189,7 @@ function MemberEditController($scope, $routeParams, $location, $http, $q, appSta
|
||||
$scope.page.saveButtonState = "busy";
|
||||
|
||||
//anytime a user is changing a member's password without the oldPassword, we are in effect resetting it so we need to set that flag here
|
||||
var passwordProp = _.find(contentEditingHelper.getAllProps($scope.content), function (e) { return e.alias === '_umb_password' });
|
||||
var passwordProp = _.find($scope.content.membershipProperties, function (e) { return e.alias === '_umb_password' });
|
||||
if (passwordProp && passwordProp.value && (typeof passwordProp.value.reset !== 'undefined') && !passwordProp.value.reset) {
|
||||
//so if the admin is not explicitly resetting the password, flag it for resetting if a new password is being entered
|
||||
passwordProp.value.reset = !passwordProp.value.oldPassword && passwordProp.config.allowManuallyChangingPassword;
|
||||
|
||||
@@ -667,6 +667,7 @@ angular.module("umbraco")
|
||||
$scope.removeControl = function (cell, $index) {
|
||||
$scope.currentControl = null;
|
||||
cell.controls.splice($index, 1);
|
||||
currentForm.$setDirty();
|
||||
};
|
||||
|
||||
$scope.percentage = function (spans) {
|
||||
|
||||
+21
-7
@@ -146,6 +146,7 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time
|
||||
}
|
||||
|
||||
var listParamsForCurrent = $routeParams.id == $routeParams.list;
|
||||
|
||||
$scope.options = {
|
||||
useInfiniteEditor: $scope.model.config.useInfiniteEditor === true,
|
||||
pageSize: $scope.model.config.pageSize ? $scope.model.config.pageSize : 10,
|
||||
@@ -169,6 +170,7 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time
|
||||
allowBulkDelete: $scope.model.config.bulkActionPermissions.allowBulkDelete,
|
||||
cultureName: $routeParams.cculture ? $routeParams.cculture : $routeParams.mculture
|
||||
};
|
||||
|
||||
_.each($scope.options.includeProperties, function (property) {
|
||||
property.nameExp = !!property.nameTemplate
|
||||
? $interpolate(property.nameTemplate)
|
||||
@@ -268,12 +270,13 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time
|
||||
|
||||
$scope.getContent = function (contentId) {
|
||||
$scope.reloadView($scope.contentId, true);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.reloadView = function (id, reloadActiveNode) {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.viewLoaded = false;
|
||||
$scope.folders = [];
|
||||
|
||||
@@ -327,7 +330,7 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time
|
||||
$scope.options.pageNumber = 1;
|
||||
$scope.reloadView($scope.contentId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$scope.onSearchStartTyping = function() {
|
||||
$scope.viewLoaded = false;
|
||||
@@ -689,9 +692,18 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time
|
||||
}
|
||||
|
||||
if (e.nameExp) {
|
||||
var newValue = e.nameExp({ value });
|
||||
if (newValue && (newValue = newValue.trim())) {
|
||||
value = newValue;
|
||||
if (/{{.*\s*\w+\s*\|\s*\w+\s*.*}}/.test(e.nameTemplate)) { //check whether the name template has a filter
|
||||
value = {
|
||||
value,
|
||||
expression: e.nameExp
|
||||
};
|
||||
}
|
||||
else {
|
||||
var newValue = e.nameExp({ value });
|
||||
|
||||
if (newValue && (newValue = newValue.trim())) {
|
||||
value = newValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -708,13 +720,15 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time
|
||||
}
|
||||
|
||||
function initView() {
|
||||
|
||||
var id = $routeParams.id;
|
||||
if (id === undefined) {
|
||||
// no ID found in route params - don't list anything as we don't know for sure where we are
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.contentId = id;
|
||||
|
||||
// Get current id for node to load it's children
|
||||
$scope.contentId = editorState.current ? editorState.current.id : id;
|
||||
$scope.isTrashed = editorState.current ? editorState.current.trashed : id === "-20" || id === "-21";
|
||||
|
||||
$scope.options.allowBulkPublish = $scope.options.allowBulkPublish && !$scope.isTrashed;
|
||||
|
||||
@@ -348,9 +348,9 @@
|
||||
<WebProjectProperties>
|
||||
<UseIIS>False</UseIIS>
|
||||
<AutoAssignPort>True</AutoAssignPort>
|
||||
<DevelopmentServerPort>8180</DevelopmentServerPort>
|
||||
<DevelopmentServerPort>8189</DevelopmentServerPort>
|
||||
<DevelopmentServerVPath>/</DevelopmentServerVPath>
|
||||
<IISUrl>http://localhost:8180</IISUrl>
|
||||
<IISUrl>http://localhost:8189</IISUrl>
|
||||
<NTLMAuthentication>False</NTLMAuthentication>
|
||||
<UseCustomServer>False</UseCustomServer>
|
||||
<CustomServerUrl>
|
||||
|
||||
@@ -339,6 +339,7 @@
|
||||
<key alias="renameFolderFailed">Omdøbning af mappen med id %0% fejlede</key>
|
||||
<key alias="dragAndDropYourFilesIntoTheArea">Træk dine filer ind i dropzonen for, at uploade dem til mediebiblioteket.</key>
|
||||
<key alias="uploadNotAllowed">Upload er ikke tiladt på denne lokation</key>
|
||||
<key alias="fileSecurityValidationFailure">En eller flere fil sikkerhedsvalideringer har fejlet</key>
|
||||
</area>
|
||||
<area alias="member">
|
||||
<key alias="createNewMember">Opret et nyt medlem</key>
|
||||
|
||||
@@ -329,6 +329,7 @@
|
||||
<key alias="orClickHereToUpload">or click here to choose files</key>
|
||||
<key alias="dragFilesHereToUpload">You can drag files here to upload</key>
|
||||
<key alias="disallowedFileType">Cannot upload this file, it does not have an approved file type</key>
|
||||
<key alias="disallowedMediaType">Cannot upload this file, the media type with alias '%0%' is not allowed here</key>
|
||||
<key alias="invalidFileName">Cannot upload this file, it does not have a valid file name</key>
|
||||
<key alias="maxFileSize">Max file size is</key>
|
||||
<key alias="mediaRoot">Media root</key>
|
||||
@@ -337,6 +338,7 @@
|
||||
<key alias="createFolderFailed">Failed to create a folder under parent id %0%</key>
|
||||
<key alias="renameFolderFailed">Failed to rename the folder with id %0%</key>
|
||||
<key alias="dragAndDropYourFilesIntoTheArea">Drag and drop your file(s) into the area</key>
|
||||
<key alias="fileSecurityValidationFailure">One or more file security validations have failed</key>
|
||||
</area>
|
||||
<area alias="member">
|
||||
<key alias="createNewMember">Create a new member</key>
|
||||
@@ -1415,6 +1417,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
|
||||
<key alias="theBestUmbracoVideoTutorials">The best Umbraco video tutorials</key>
|
||||
<key alias="umbracoForum">Visit our.umbraco.com</key>
|
||||
<key alias="umbracoTv">Visit umbraco.tv</key>
|
||||
<key alias="umbracoLearningBase">Watch our free tutorial videos</key>
|
||||
<key alias="umbracoLearningBaseDescription">on the Umbraco Learning Base</key>
|
||||
</area>
|
||||
<area alias="settings">
|
||||
<key alias="defaulttemplate">Default template</key>
|
||||
@@ -1448,6 +1452,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
|
||||
<key alias="invalidUserPermissionsText">Insufficient user permissions, could not complete the operation</key>
|
||||
<key alias="operationCancelledHeader">Cancelled</key>
|
||||
<key alias="operationCancelledText">Operation was cancelled by a 3rd party add-in</key>
|
||||
<key alias="folderUploadNotAllowed">This file is being uploaded as part of a folder, but creating a new folder is not allowed here</key>
|
||||
<key alias="folderCreationNotAllowed">Creating a new folder is not allowed here</key>
|
||||
<key alias="contentPublishedFailedByEvent">Publishing was cancelled by a 3rd party add-in</key>
|
||||
<key alias="contentTypeDublicatePropertyType">Property type already exists</key>
|
||||
<key alias="contentTypePropertyTypeCreated">Property type created</key>
|
||||
@@ -2506,9 +2512,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
|
||||
Ask a question in the <a class="btn-link -underline" href="https://our.umbraco.com/forum" target="_blank" rel="noopener">Community Forum</a>
|
||||
]]>
|
||||
</key>
|
||||
<key alias="bulletPointThree">
|
||||
<key alias="bulletPointTutorials">
|
||||
<![CDATA[
|
||||
Watch our <a class="btn-link -underline" href="https://umbraco.tv" target="_blank" rel="noopener">tutorial videos</a> (some are free, some require a subscription)
|
||||
Watch our free <a class="btn-link -underline" href="https://umbra.co/ulb" target="_blank" rel="noopener">tutorial videos on the Umbraco Learning Base</a>
|
||||
]]>
|
||||
</key>
|
||||
<key alias="bulletPointFour">
|
||||
|
||||
@@ -331,8 +331,9 @@
|
||||
<area alias="media">
|
||||
<key alias="clickToUpload">Click to upload</key>
|
||||
<key alias="orClickHereToUpload">or click here to choose files</key>
|
||||
<key alias="dragFilesHereToUpload">You can drag files here to upload.</key>
|
||||
<key alias="dragFilesHereToUpload">You can drag files here to upload.</key>
|
||||
<key alias="disallowedFileType">Cannot upload this file, it does not have an approved file type</key>
|
||||
<key alias="disallowedMediaType">Cannot upload this file, the media type with alias '%0%' is not allowed here</key>
|
||||
<key alias="invalidFileName">Cannot upload this file, it does not have a valid file name</key>
|
||||
<key alias="maxFileSize">Max file size is</key>
|
||||
<key alias="mediaRoot">Media root</key>
|
||||
@@ -343,6 +344,7 @@
|
||||
<key alias="renameFolderFailed">Failed to rename the folder with id %0%</key>
|
||||
<key alias="dragAndDropYourFilesIntoTheArea">Drag and drop your file(s) into the area</key>
|
||||
<key alias="uploadNotAllowed">Upload is not allowed in this location.</key>
|
||||
<key alias="fileSecurityValidationFailure">One or more file security validations have failed</key>
|
||||
</area>
|
||||
<area alias="member">
|
||||
<key alias="createNewMember">Create a new member</key>
|
||||
@@ -1425,6 +1427,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
|
||||
<key alias="theBestUmbracoVideoTutorials">The best Umbraco video tutorials</key>
|
||||
<key alias="umbracoForum">Visit our.umbraco.com</key>
|
||||
<key alias="umbracoTv">Visit umbraco.tv</key>
|
||||
<key alias="umbracoLearningBase">Watch our free tutorial videos</key>
|
||||
<key alias="umbracoLearningBaseDescription">on the Umbraco Learning Base</key>
|
||||
</area>
|
||||
<area alias="settings">
|
||||
<key alias="defaulttemplate">Default template</key>
|
||||
@@ -1460,6 +1464,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
|
||||
<key alias="invalidUserPermissionsText">Insufficient user permissions, could not complete the operation</key>
|
||||
<key alias="operationCancelledHeader">Cancelled</key>
|
||||
<key alias="operationCancelledText">Operation was cancelled by a 3rd party add-in</key>
|
||||
<key alias="folderUploadNotAllowed">This file is being uploaded as part of a folder, but creating a new folder is not allowed here</key>
|
||||
<key alias="folderCreationNotAllowed">Creating a new folder is not allowed here</key>
|
||||
<key alias="contentPublishedFailedByEvent">Publishing was cancelled by a 3rd party add-in</key>
|
||||
<key alias="contentTypeDublicatePropertyType">Property type already exists</key>
|
||||
<key alias="contentTypePropertyTypeCreated">Property type created</key>
|
||||
<key alias="contentTypePropertyTypeCreatedText"><![CDATA[Name: %0% <br /> DataType: %1%]]></key>
|
||||
@@ -2544,9 +2551,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
|
||||
Ask a question in the <a class="btn-link -underline" href="https://our.umbraco.com/forum" target="_blank" rel="noopener">Community Forum</a>
|
||||
]]>
|
||||
</key>
|
||||
<key alias="bulletPointThree">
|
||||
<key alias="bulletPointTutorials">
|
||||
<![CDATA[
|
||||
Watch our <a class="btn-link -underline" href="https://umbraco.tv" target="_blank" rel="noopener">tutorial videos</a> (some are free, some require a subscription)
|
||||
Watch our free <a class="btn-link -underline" href="https://umbra.co/ulb" target="_blank" rel="noopener">tutorial videos on the Umbraco Learning Base</a>
|
||||
]]>
|
||||
</key>
|
||||
<key alias="bulletPointFour">
|
||||
|
||||
@@ -336,6 +336,7 @@
|
||||
<key alias="renameFolderFailed">Kan de map met id %0% niet hernoemen</key>
|
||||
<key alias="dragAndDropYourFilesIntoTheArea">Sleep en zet je bestand(en) neer in dit gebied</key>
|
||||
<key alias="uploadNotAllowed">Upload is niet toegelaten in deze locatie.</key>
|
||||
<key alias="fileSecurityValidationFailure">Een of meerdere veiligheid validaties zijn gefaald voor het bestand</key>
|
||||
</area>
|
||||
<area alias="member">
|
||||
<key alias="createNewMember">Maak nieuw lid aan</key>
|
||||
|
||||
@@ -6,7 +6,8 @@ namespace Umbraco.Web.Actions
|
||||
{
|
||||
public class ActionCreateBlueprintFromContent : IAction
|
||||
{
|
||||
public char Letter => 'ï';
|
||||
public const char ActionLetter = 'ï';
|
||||
public char Letter => ActionLetter;
|
||||
public bool ShowInNotifier => false;
|
||||
public bool CanBePermissionAssigned => true;
|
||||
public string Icon => "blueprint";
|
||||
|
||||
@@ -121,33 +121,35 @@ namespace Umbraco.Web.Cache
|
||||
{
|
||||
var groupedEvents = new List<IEventDefinition>();
|
||||
|
||||
var grouped = events.GroupBy(x => x.GetType());
|
||||
|
||||
foreach (var group in grouped)
|
||||
foreach (var group in events.GroupBy(x => x.GetType()))
|
||||
{
|
||||
if (group.Key == typeof(EventDefinition<IContentTypeService, SaveEventArgs<IContentType>>))
|
||||
{
|
||||
GroupSaveEvents<IContentTypeService, IContentType>(groupedEvents, group);
|
||||
groupedEvents.AddRange(GroupSaveEvents<IContentTypeService, IContentType>(group));
|
||||
}
|
||||
else if (group.Key == typeof(EventDefinition<IContentTypeService, ContentTypeChange<IContentType>.EventArgs>))
|
||||
{
|
||||
GroupChangeEvents<IContentTypeService, IContentType>(groupedEvents, group);
|
||||
groupedEvents.AddRange(GroupChangeEvents<IContentTypeService, IContentType>(group));
|
||||
}
|
||||
else if (group.Key == typeof(EventDefinition<IMediaTypeService, SaveEventArgs<IMediaType>>))
|
||||
{
|
||||
GroupSaveEvents<IMediaTypeService, IMediaType>(groupedEvents, group);
|
||||
groupedEvents.AddRange(GroupSaveEvents<IMediaTypeService, IMediaType>(group));
|
||||
}
|
||||
else if (group.Key == typeof(EventDefinition<IMediaTypeService, ContentTypeChange<IMediaType>.EventArgs>))
|
||||
{
|
||||
GroupChangeEvents<IMediaTypeService, IMediaType>(groupedEvents, group);
|
||||
groupedEvents.AddRange(GroupChangeEvents<IMediaTypeService, IMediaType>(group));
|
||||
}
|
||||
else if (group.Key == typeof(EventDefinition<IMemberTypeService, SaveEventArgs<IMemberType>>))
|
||||
{
|
||||
GroupSaveEvents<IMemberTypeService, IMemberType>(groupedEvents, group);
|
||||
groupedEvents.AddRange(GroupSaveEvents<IMemberTypeService, IMemberType>(group));
|
||||
}
|
||||
else if (group.Key == typeof(EventDefinition<IMemberTypeService, ContentTypeChange<IMemberType>.EventArgs>))
|
||||
{
|
||||
GroupChangeEvents<IMemberTypeService, IMemberType>(groupedEvents, group);
|
||||
groupedEvents.AddRange(GroupChangeEvents<IMemberTypeService, IMemberType>(group));
|
||||
}
|
||||
else if (group.Key == typeof(EventDefinition<IDataTypeService, SaveEventArgs<IDataType>>))
|
||||
{
|
||||
groupedEvents.AddRange(GroupSaveEvents<IDataTypeService, IDataType>(group));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -158,35 +160,28 @@ namespace Umbraco.Web.Cache
|
||||
return groupedEvents;
|
||||
}
|
||||
|
||||
private static void GroupSaveEvents<TService, TType>(List<IEventDefinition> groupedEvents, IGrouping<Type, IEventDefinition> group)
|
||||
where TService : IContentTypeBaseService
|
||||
where TType : IContentTypeBase
|
||||
private static IEnumerable<IEventDefinition> GroupSaveEvents<TSender, TEntity>(IEnumerable<IEventDefinition> events)
|
||||
{
|
||||
var groupedGroups = group.GroupBy(x => (x.EventName, x.Sender));
|
||||
|
||||
foreach (var groupedGroup in groupedGroups)
|
||||
foreach (var group in events.GroupBy(x => (x.EventName, x.Sender)))
|
||||
{
|
||||
groupedEvents.Add(new EventDefinition<TService, SaveEventArgs<TType>>(
|
||||
yield return new EventDefinition<TSender, SaveEventArgs<TEntity>>(
|
||||
null,
|
||||
(TService)groupedGroup.Key.Sender,
|
||||
new SaveEventArgs<TType>(groupedGroup.SelectMany(x => ((SaveEventArgs<TType>)x.Args).SavedEntities)),
|
||||
groupedGroup.Key.EventName));
|
||||
(TSender)group.Key.Sender,
|
||||
new SaveEventArgs<TEntity>(group.SelectMany(x => ((SaveEventArgs<TEntity>)x.Args).SavedEntities)),
|
||||
group.Key.EventName);
|
||||
}
|
||||
}
|
||||
|
||||
private static void GroupChangeEvents<TService, TType>(List<IEventDefinition> groupedEvents, IGrouping<Type, IEventDefinition> group)
|
||||
where TService : IContentTypeBaseService
|
||||
where TType : class, IContentTypeComposition
|
||||
private static IEnumerable<IEventDefinition> GroupChangeEvents<TSender, TEntity>(IEnumerable<IEventDefinition> events)
|
||||
where TEntity : class, IContentTypeComposition
|
||||
{
|
||||
var groupedGroups = group.GroupBy(x => (x.EventName, x.Sender));
|
||||
|
||||
foreach (var groupedGroup in groupedGroups)
|
||||
foreach (var group in events.GroupBy(x => (x.EventName, x.Sender)))
|
||||
{
|
||||
groupedEvents.Add(new EventDefinition<TService, ContentTypeChange<TType>.EventArgs>(
|
||||
yield return new EventDefinition<TSender, ContentTypeChange<TEntity>.EventArgs>(
|
||||
null,
|
||||
(TService)groupedGroup.Key.Sender,
|
||||
new ContentTypeChange<TType>.EventArgs(groupedGroup.SelectMany(x => ((ContentTypeChange<TType>.EventArgs)x.Args).Changes)),
|
||||
groupedGroup.Key.EventName));
|
||||
(TSender)group.Key.Sender,
|
||||
new ContentTypeChange<TEntity>.EventArgs(group.SelectMany(x => ((ContentTypeChange<TEntity>.EventArgs)x.Args).Changes)),
|
||||
group.Key.EventName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,16 +245,10 @@ namespace Umbraco.Web.Cache
|
||||
#region DataTypeService
|
||||
|
||||
private void DataTypeService_Saved(IDataTypeService sender, SaveEventArgs<IDataType> e)
|
||||
{
|
||||
foreach (var entity in e.SavedEntities)
|
||||
_distributedCache.RefreshDataTypeCache(entity);
|
||||
}
|
||||
=> _distributedCache.RefreshDataTypeCache(e.SavedEntities);
|
||||
|
||||
private void DataTypeService_Deleted(IDataTypeService sender, DeleteEventArgs<IDataType> e)
|
||||
{
|
||||
foreach (var entity in e.DeletedEntities)
|
||||
_distributedCache.RemoveDataTypeCache(entity);
|
||||
}
|
||||
=> _distributedCache.RemoveDataTypeCache(e.DeletedEntities);
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Services.Changes;
|
||||
|
||||
@@ -86,20 +89,34 @@ namespace Umbraco.Web.Cache
|
||||
|
||||
#region DataTypeCache
|
||||
|
||||
[Obsolete("Use the overload accepting IEnumerable instead.")]
|
||||
public static void RefreshDataTypeCache(this DistributedCache dc, IDataType dataType)
|
||||
{
|
||||
if (dataType == null) return;
|
||||
var payloads = new[] { new DataTypeCacheRefresher.JsonPayload(dataType.Id, dataType.Key, false) };
|
||||
dc.RefreshByPayload(DataTypeCacheRefresher.UniqueId, payloads);
|
||||
if (dataType == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
dc.RefreshDataTypeCache(dataType.Yield());
|
||||
}
|
||||
|
||||
public static void RefreshDataTypeCache(this DistributedCache dc, IEnumerable<IDataType> dataTypes)
|
||||
=> dc.RefreshByPayload(DataTypeCacheRefresher.UniqueId, dataTypes?.Select(x => new DataTypeCacheRefresher.JsonPayload(x.Id, x.Key, false)));
|
||||
|
||||
[Obsolete("Use the overload accepting IEnumerable instead.")]
|
||||
public static void RemoveDataTypeCache(this DistributedCache dc, IDataType dataType)
|
||||
{
|
||||
if (dataType == null) return;
|
||||
var payloads = new[] { new DataTypeCacheRefresher.JsonPayload(dataType.Id, dataType.Key, true) };
|
||||
dc.RefreshByPayload(DataTypeCacheRefresher.UniqueId, payloads);
|
||||
if (dataType == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
dc.RemoveDataTypeCache(dataType.Yield());
|
||||
}
|
||||
|
||||
public static void RemoveDataTypeCache(this DistributedCache dc, IEnumerable<IDataType> dataTypes)
|
||||
=> dc.RefreshByPayload(DataTypeCacheRefresher.UniqueId, dataTypes?.Select(x => new DataTypeCacheRefresher.JsonPayload(x.Id, x.Key, true)));
|
||||
|
||||
#endregion
|
||||
|
||||
#region ContentCache
|
||||
|
||||
@@ -335,7 +335,12 @@ namespace Umbraco.Web.Editors
|
||||
{
|
||||
throw new HttpResponseException(HttpStatusCode.BadRequest);
|
||||
}
|
||||
|
||||
var identityUser = await SignInManager.UserManager.FindByEmailAsync(model.Email);
|
||||
|
||||
Random random = new Random();
|
||||
await Task.Delay(random.Next(400, 2500)); // To randomize response time preventing user enumeration
|
||||
|
||||
if (identityUser != null)
|
||||
{
|
||||
var user = Services.UserService.GetByEmail(model.Email);
|
||||
@@ -349,11 +354,20 @@ namespace Umbraco.Web.Editors
|
||||
UserExtensions.GetUserCulture(identityUser.Culture, Services.TextService, GlobalSettings),
|
||||
new[] { identityUser.UserName, callbackUrl });
|
||||
|
||||
await UserManager.SendEmailAsync(identityUser.Id,
|
||||
Services.TextService.Localize("login", "resetPasswordEmailCopySubject",
|
||||
// Ensure the culture of the found user is used for the email!
|
||||
UserExtensions.GetUserCulture(identityUser.Culture, Services.TextService, GlobalSettings)),
|
||||
message);
|
||||
try
|
||||
{
|
||||
await UserManager.SendEmailAsync(identityUser.Id,
|
||||
Services.TextService.Localize("login", "resetPasswordEmailCopySubject",
|
||||
// Ensure the culture of the found user is used for the email!
|
||||
UserExtensions.GetUserCulture(identityUser.Culture, Services.TextService,
|
||||
GlobalSettings)),
|
||||
message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error<AuthenticationController>(ex, "Error sending email, please check your SMTP configuration: {ErrorMessage}", ex.Message);
|
||||
return Request.CreateResponse(HttpStatusCode.OK);
|
||||
}
|
||||
|
||||
UserManager.RaiseForgotPasswordRequestedEvent(user.Id);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ using Umbraco.Web.Models.ContentEditing;
|
||||
|
||||
namespace Umbraco.Web.Editors.Binders
|
||||
{
|
||||
internal class BlueprintItemBinder : ContentItemBinder
|
||||
public class BlueprintItemBinder : ContentItemBinder
|
||||
{
|
||||
public BlueprintItemBinder()
|
||||
{
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace Umbraco.Web.Editors.Binders
|
||||
/// <summary>
|
||||
/// The model binder for <see cref="T:Umbraco.Web.Models.ContentEditing.ContentItemSave" />
|
||||
/// </summary>
|
||||
internal class ContentItemBinder : IModelBinder
|
||||
public class ContentItemBinder : IModelBinder
|
||||
{
|
||||
public ContentItemBinder() : this(Current.Services)
|
||||
{
|
||||
|
||||
@@ -306,7 +306,7 @@ namespace Umbraco.Web.Editors
|
||||
switch (type)
|
||||
{
|
||||
case Core.Constants.Trees.PartialViews:
|
||||
if (IsDirectory(virtualPath, SystemDirectories.PartialViews))
|
||||
if (IsDirectory(virtualPath, SystemDirectories.PartialViews, Current.FileSystems.PartialViewsFileSystem))
|
||||
{
|
||||
Services.FileService.DeletePartialViewFolder(virtualPath);
|
||||
return Request.CreateResponse(HttpStatusCode.OK);
|
||||
@@ -318,7 +318,7 @@ namespace Umbraco.Web.Editors
|
||||
return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No Partial View or folder found with the specified path");
|
||||
|
||||
case Core.Constants.Trees.PartialViewMacros:
|
||||
if (IsDirectory(virtualPath, SystemDirectories.MacroPartials))
|
||||
if (IsDirectory(virtualPath, SystemDirectories.MacroPartials, Current.FileSystems.MacroPartialsFileSystem))
|
||||
{
|
||||
Services.FileService.DeletePartialViewMacroFolder(virtualPath);
|
||||
return Request.CreateResponse(HttpStatusCode.OK);
|
||||
@@ -330,7 +330,7 @@ namespace Umbraco.Web.Editors
|
||||
return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No Partial View Macro or folder found with the specified path");
|
||||
|
||||
case Core.Constants.Trees.Scripts:
|
||||
if (IsDirectory(virtualPath, SystemDirectories.Scripts))
|
||||
if (IsDirectory(virtualPath, SystemDirectories.Scripts, Current.FileSystems.ScriptsFileSystem))
|
||||
{
|
||||
Services.FileService.DeleteScriptFolder(virtualPath);
|
||||
return Request.CreateResponse(HttpStatusCode.OK);
|
||||
@@ -343,7 +343,7 @@ namespace Umbraco.Web.Editors
|
||||
return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No Script or folder found with the specified path");
|
||||
|
||||
case Core.Constants.Trees.Stylesheets:
|
||||
if (IsDirectory(virtualPath, SystemDirectories.Css))
|
||||
if (IsDirectory(virtualPath, SystemDirectories.Css, Current.FileSystems.StylesheetsFileSystem))
|
||||
{
|
||||
Services.FileService.DeleteStyleSheetFolder(virtualPath);
|
||||
return Request.CreateResponse(HttpStatusCode.OK);
|
||||
@@ -634,14 +634,23 @@ namespace Umbraco.Web.Editors
|
||||
return value;
|
||||
}
|
||||
|
||||
private bool IsDirectory(string virtualPath, string systemDirectory)
|
||||
private bool IsDirectory(string virtualPath, string systemDirectory, IFileSystem fileSystem)
|
||||
{
|
||||
var path = IOHelper.MapPath(systemDirectory + "/" + virtualPath);
|
||||
var dirInfo = new DirectoryInfo(path);
|
||||
|
||||
// If you turn off indexing in Windows this will have the attribute:
|
||||
// `FileAttributes.Directory | FileAttributes.NotContentIndexed`
|
||||
return (dirInfo.Attributes & FileAttributes.Directory) != 0;
|
||||
// If it's a physical filesystem check with directory info
|
||||
if (fileSystem.CanAddPhysical)
|
||||
{
|
||||
var dirInfo = new DirectoryInfo(path);
|
||||
|
||||
// If you turn off indexing in Windows this will have the attribute:
|
||||
// `FileAttributes.Directory | FileAttributes.NotContentIndexed`
|
||||
return (dirInfo.Attributes & FileAttributes.Directory) != 0;
|
||||
}
|
||||
|
||||
// Otherwise check the filesystem abstraction to see if the folder exists
|
||||
// Since this is used for delete, it presumably exists if we're trying to delete it
|
||||
return fileSystem.DirectoryExists(path);
|
||||
}
|
||||
|
||||
// this is an internal class for passing stylesheet data from the client to the controller while editing
|
||||
|
||||
@@ -376,7 +376,7 @@ namespace Umbraco.Web.Editors
|
||||
[HttpPost]
|
||||
public IDictionary<string, ContentItemDisplay> GetEmptyByAliases(ContentTypesByAliases contentTypesByAliases)
|
||||
{
|
||||
// It's important to do this operation within a scope to reduce the amount of readlock queries.
|
||||
// It's important to do this operation within a scope to reduce the amount of readlock queries.
|
||||
using var scope = _scopeProvider.CreateScope(autoComplete: true);
|
||||
var contentTypes = contentTypesByAliases.ContentTypeAliases.Select(alias => Services.ContentTypeService.Get(alias));
|
||||
return GetEmpties(contentTypes, contentTypesByAliases.ParentId).ToDictionary(x => x.ContentTypeAlias);
|
||||
@@ -657,6 +657,7 @@ namespace Umbraco.Web.Editors
|
||||
/// <param name="contentId">The content id to copy</param>
|
||||
/// <param name="name">The name of the blueprint</param>
|
||||
/// <returns></returns>
|
||||
[EnsureUserPermissionForContent("contentId", ActionCreateBlueprintFromContent.ActionLetter)]
|
||||
[HttpPost]
|
||||
public SimpleNotificationModel CreateBlueprintFromContent([FromUri] int contentId, [FromUri] string name)
|
||||
{
|
||||
@@ -696,8 +697,9 @@ namespace Umbraco.Web.Editors
|
||||
/// Saves content
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[UmbracoTreeAuthorize(Constants.Trees.DocumentTypes)]
|
||||
[FileUploadCleanupFilter]
|
||||
[ContentSaveValidation]
|
||||
[ContentSaveValidation(skipUserAccessValidation:true)] // skip user access validation because we "only" require Settings access to create new blueprints from scratch
|
||||
public ContentItemDisplay PostSaveBlueprint([ModelBinder(typeof(BlueprintItemBinder))] ContentItemSave contentItem)
|
||||
{
|
||||
var contentItemDisplay = PostSaveInternal(contentItem,
|
||||
@@ -1586,6 +1588,7 @@ namespace Umbraco.Web.Editors
|
||||
|
||||
}
|
||||
|
||||
[UmbracoTreeAuthorize(Constants.Trees.DocumentTypes)]
|
||||
[HttpDelete]
|
||||
[HttpPost]
|
||||
public HttpResponseMessage DeleteBlueprint(int id)
|
||||
@@ -2019,6 +2022,8 @@ namespace Umbraco.Web.Editors
|
||||
|
||||
var variantIndex = 0;
|
||||
|
||||
var defaultCulture = _allLangs.Value.Values.FirstOrDefault(x => x.IsDefault)?.IsoCode;
|
||||
|
||||
//loop through each variant, set the correct name and property values
|
||||
foreach (var variant in contentSave.Variants)
|
||||
{
|
||||
@@ -2033,6 +2038,12 @@ namespace Umbraco.Web.Editors
|
||||
if (variant.Culture.IsNullOrWhiteSpace())
|
||||
throw new InvalidOperationException($"Cannot set culture name without a culture.");
|
||||
contentSave.PersistedContent.SetCultureName(variant.Name, variant.Culture);
|
||||
|
||||
// If the variant culture is the default culture we also want to update the name on the Content itself.
|
||||
if (variant.Culture.Equals(defaultCulture, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
contentSave.PersistedContent.Name = variant.Name;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2072,6 +2083,12 @@ namespace Umbraco.Web.Editors
|
||||
variantIndex++;
|
||||
}
|
||||
|
||||
// Map IsDirty cultures to edited cultures, to make it easier to verify changes on specific variants on Saving and Saved events.
|
||||
var editedCultures = contentSave.PersistedContent.CultureInfos.Values
|
||||
.Where(x => x.IsDirty())
|
||||
.Select(x => x.Culture);
|
||||
contentSave.PersistedContent.SetCultureEdited(editedCultures);
|
||||
|
||||
// handle template
|
||||
if (string.IsNullOrWhiteSpace(contentSave.TemplateAlias)) // cleared: clear if not already null
|
||||
{
|
||||
@@ -2423,7 +2440,7 @@ namespace Umbraco.Web.Editors
|
||||
Items = results.Select(x => new ContentVersionMetaViewModel(x))
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
[HttpPost]
|
||||
[EnsureUserPermissionForContent("contentId", ActionUpdate.ActionLetter)]
|
||||
public HttpResponseMessage PostSetContentVersionPreventCleanup(int contentId, int versionId, bool preventCleanup)
|
||||
|
||||
@@ -521,7 +521,7 @@ namespace Umbraco.Web.Editors
|
||||
[HttpPost]
|
||||
public HttpResponseMessage Import(string file)
|
||||
{
|
||||
var filePath = Path.Combine(IOHelper.MapPath(SystemDirectories.Data), file);
|
||||
var filePath = Path.Combine(IOHelper.MapPath(SystemDirectories.TempFileUploads), file);
|
||||
if (string.IsNullOrEmpty(file) || !System.IO.File.Exists(filePath))
|
||||
{
|
||||
return Request.CreateResponse(HttpStatusCode.NotFound);
|
||||
@@ -576,35 +576,35 @@ namespace Umbraco.Web.Editors
|
||||
var fileName = file.Headers.ContentDisposition.FileName.Trim(Constants.CharArrays.DoubleQuote);
|
||||
var ext = fileName.Substring(fileName.LastIndexOf('.') + 1).ToLower();
|
||||
|
||||
var destFileName = Path.Combine(root, fileName);
|
||||
if (Path.GetFullPath(destFileName).StartsWith(Path.GetFullPath(root)))
|
||||
var destFilePath = Path.Combine(root, fileName);
|
||||
if (Path.GetFullPath(destFilePath).StartsWith(Path.GetFullPath(root)))
|
||||
{
|
||||
try
|
||||
{
|
||||
// due to a bug before 8.7.0 we didn't delete temp files, so we need to make sure to delete before
|
||||
// moving else you get errors and the upload fails without a message in the UI (there's a JS error)
|
||||
if(System.IO.File.Exists(destFileName))
|
||||
System.IO.File.Delete(destFileName);
|
||||
if(System.IO.File.Exists(destFilePath))
|
||||
System.IO.File.Delete(destFilePath);
|
||||
|
||||
// renaming the file because MultipartFormDataStreamProvider has created a random fileName instead of using the name from the
|
||||
// content-disposition for more than 6 years now. Creating a CustomMultipartDataStreamProvider deriving from MultipartFormDataStreamProvider
|
||||
// seems like a cleaner option, but I'm not sure where to put it and renaming only takes one line of code.
|
||||
System.IO.File.Move(result.FileData[0].LocalFileName, destFileName);
|
||||
System.IO.File.Move(result.FileData[0].LocalFileName, destFilePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Error<ContentTypeController, string>(ex, "Error uploading udt file to App_Data: {File}", destFileName);
|
||||
Logger.Error<ContentTypeController, string>(ex, "Error uploading udt file to App_Data: {File}", destFilePath);
|
||||
}
|
||||
|
||||
if (ext.InvariantEquals("udt"))
|
||||
{
|
||||
model.TempFileName = destFileName;
|
||||
model.TempFileName = fileName;
|
||||
|
||||
var xd = new XmlDocument
|
||||
{
|
||||
XmlResolver = null
|
||||
};
|
||||
xd.Load(model.TempFileName);
|
||||
xd.Load(destFilePath);
|
||||
|
||||
model.Alias = xd.DocumentElement?.SelectSingleNode("//DocumentType/Info/Alias")?.FirstChild.Value;
|
||||
model.Name = xd.DocumentElement?.SelectSingleNode("//DocumentType/Info/Name")?.FirstChild.Value;
|
||||
@@ -612,7 +612,7 @@ namespace Umbraco.Web.Editors
|
||||
else
|
||||
{
|
||||
// Cleanup the temp file
|
||||
System.IO.File.Delete(destFileName);
|
||||
System.IO.File.Delete(destFilePath);
|
||||
model.Notifications.Add(new Notification(
|
||||
Services.TextService.Localize("speechBubbles", "operationFailedHeader"),
|
||||
Services.TextService.Localize("media", "disallowedFileType"),
|
||||
|
||||
@@ -12,6 +12,8 @@ using Umbraco.Web.WebApi;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Security;
|
||||
using Umbraco.Web.Composing;
|
||||
using Umbraco.Web.Security;
|
||||
using Umbraco.Web.WebApi.Filters;
|
||||
|
||||
@@ -155,7 +157,9 @@ namespace Umbraco.Web.Editors
|
||||
public async Task<HttpResponseMessage> PostSetAvatar()
|
||||
{
|
||||
//borrow the logic from the user controller
|
||||
return await UsersController.PostSetAvatarInternal(Request, Services.UserService, AppCaches.RuntimeCache, Security.GetUserId().ResultOr(0));
|
||||
// This is unbelievable... This controller has no real constructor using DI, instead everything is resolved from the Current service locator.
|
||||
// So I guess we'll just do that again here
|
||||
return await UsersController.PostSetAvatarInternal(Request, Services.UserService, Current.Factory.GetInstance<IFileStreamSecurityValidator>(), AppCaches.RuntimeCache, Security.GetUserId().ResultOr(0));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -20,6 +20,7 @@ using Umbraco.Core.Dashboards;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Web.Services;
|
||||
using System.Web.Http;
|
||||
using Umbraco.Core.Telemetry;
|
||||
|
||||
namespace Umbraco.Web.Editors
|
||||
{
|
||||
@@ -34,18 +35,28 @@ namespace Umbraco.Web.Editors
|
||||
{
|
||||
private readonly IDashboardService _dashboardService;
|
||||
private readonly IContentDashboardSettings _dashboardSettings;
|
||||
private readonly ISiteIdentifierService _siteIdentifierService;
|
||||
|
||||
/// <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, IProfilingLogger logger,
|
||||
IRuntimeState runtimeState, IDashboardService dashboardService, UmbracoHelper umbracoHelper,
|
||||
IContentDashboardSettings dashboardSettings)
|
||||
public DashboardController(
|
||||
IGlobalSettings globalSettings,
|
||||
IUmbracoContextAccessor umbracoContextAccessor,
|
||||
ISqlContext sqlContext,
|
||||
ServiceContext services,
|
||||
AppCaches appCaches,
|
||||
IProfilingLogger logger,
|
||||
IRuntimeState runtimeState,
|
||||
IDashboardService dashboardService,
|
||||
UmbracoHelper umbracoHelper,
|
||||
IContentDashboardSettings dashboardSettings,
|
||||
ISiteIdentifierService siteIdentifierService)
|
||||
: base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper)
|
||||
{
|
||||
_dashboardService = dashboardService;
|
||||
_dashboardSettings = dashboardSettings;
|
||||
_siteIdentifierService = siteIdentifierService;
|
||||
}
|
||||
|
||||
//we have just one instance of HttpClient shared for the entire application
|
||||
@@ -60,17 +71,19 @@ namespace Umbraco.Web.Editors
|
||||
var language = user.Language;
|
||||
var version = UmbracoVersion.SemanticVersion.ToSemanticString();
|
||||
var isAdmin = user.IsAdmin();
|
||||
_siteIdentifierService.TryGetOrCreateSiteIdentifier(out var siteIdentifier);
|
||||
|
||||
VerifyDashboardSource(baseUrl);
|
||||
|
||||
var url = string.Format("{0}{1}?section={2}&allowed={3}&lang={4}&version={5}&admin={6}",
|
||||
var url = string.Format("{0}{1}?section={2}&allowed={3}&lang={4}&version={5}&admin={6}&siteid={7}",
|
||||
baseUrl,
|
||||
_dashboardSettings.ContentDashboardPath,
|
||||
section,
|
||||
allowedSections,
|
||||
language,
|
||||
version,
|
||||
isAdmin);
|
||||
isAdmin,
|
||||
siteIdentifier.ToString());
|
||||
var key = "umbraco-dynamic-dashboard-" + language + allowedSections.Replace(",", "-") + section;
|
||||
|
||||
var content = AppCaches.RuntimeCache.GetCacheItem<JObject>(key);
|
||||
|
||||
@@ -16,13 +16,14 @@ using Umbraco.Web.Actions;
|
||||
using Umbraco.Web.Composing;
|
||||
using Umbraco.Web.Models.ContentEditing;
|
||||
using Umbraco.Web.Security;
|
||||
using Umbraco.Web.WebApi;
|
||||
|
||||
namespace Umbraco.Web.Editors.Filters
|
||||
{
|
||||
/// <summary>
|
||||
/// Validates the incoming <see cref="ContentItemSave"/> model along with if the user is allowed to perform the operation
|
||||
/// </summary>
|
||||
internal sealed class ContentSaveValidationAttribute : ActionFilterAttribute
|
||||
public sealed class ContentSaveValidationAttribute : ActionFilterAttribute
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
|
||||
@@ -31,11 +32,12 @@ namespace Umbraco.Web.Editors.Filters
|
||||
private readonly IUserService _userService;
|
||||
private readonly IEntityService _entityService;
|
||||
private readonly AppCaches _appCaches;
|
||||
private readonly bool _skipUserAccessValidation;
|
||||
|
||||
public ContentSaveValidationAttribute(): this(Current.Logger, Current.UmbracoContextAccessor, Current.Services.TextService, Current.Services.ContentService, Current.Services.UserService, Current.Services.EntityService, Current.AppCaches)
|
||||
public ContentSaveValidationAttribute(bool skipUserAccessValidation = false): this(Current.Logger, Current.UmbracoContextAccessor, Current.Services.TextService, Current.Services.ContentService, Current.Services.UserService, Current.Services.EntityService, Current.AppCaches, skipUserAccessValidation)
|
||||
{ }
|
||||
|
||||
public ContentSaveValidationAttribute(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService, IContentService contentService, IUserService userService, IEntityService entityService, AppCaches appCaches)
|
||||
public ContentSaveValidationAttribute(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService, IContentService contentService, IUserService userService, IEntityService entityService, AppCaches appCaches, bool skipUserAccessValidation)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor));
|
||||
@@ -44,6 +46,7 @@ namespace Umbraco.Web.Editors.Filters
|
||||
_userService = userService ?? throw new ArgumentNullException(nameof(userService));
|
||||
_entityService = entityService ?? throw new ArgumentNullException(nameof(entityService));
|
||||
_appCaches = appCaches;
|
||||
_skipUserAccessValidation = skipUserAccessValidation;
|
||||
}
|
||||
|
||||
public override void OnActionExecuting(HttpActionContext actionContext)
|
||||
@@ -53,8 +56,8 @@ namespace Umbraco.Web.Editors.Filters
|
||||
|
||||
if (!ValidateAtLeastOneVariantIsBeingSaved(model, actionContext)) return;
|
||||
if (!contentItemValidator.ValidateExistingContent(model, actionContext)) return;
|
||||
if (!ValidateUserAccess(model, actionContext, _umbracoContextAccessor.UmbracoContext.Security)) return;
|
||||
|
||||
if (!_skipUserAccessValidation && !ValidateUserAccess(model, actionContext, _umbracoContextAccessor.UmbracoContext.Security)) return;
|
||||
|
||||
//validate for each variant that is being updated
|
||||
foreach (var variant in model.Variants.Where(x => x.Save))
|
||||
{
|
||||
@@ -87,7 +90,7 @@ namespace Umbraco.Web.Editors.Filters
|
||||
/// <param name="contentItem"></param>
|
||||
/// <param name="webSecurity"></param>
|
||||
private bool ValidateUserAccess(ContentItemSave contentItem, HttpActionContext actionContext, WebSecurity webSecurity)
|
||||
{
|
||||
{
|
||||
|
||||
//We now need to validate that the user is allowed to be doing what they are doing.
|
||||
//Based on the action we need to check different permissions.
|
||||
@@ -117,7 +120,7 @@ namespace Umbraco.Web.Editors.Filters
|
||||
break;
|
||||
case ContentSaveAction.Schedule:
|
||||
permissionToCheck.Add(ActionUpdate.ActionLetter);
|
||||
permissionToCheck.Add(ActionToPublish.ActionLetter);
|
||||
permissionToCheck.Add(ActionPublish.ActionLetter);
|
||||
contentToCheck = contentItem.PersistedContent;
|
||||
contentIdToCheck = contentToCheck.Id;
|
||||
break;
|
||||
@@ -171,7 +174,7 @@ namespace Umbraco.Web.Editors.Filters
|
||||
}
|
||||
break;
|
||||
case ContentSaveAction.ScheduleNew:
|
||||
|
||||
|
||||
permissionToCheck.Add(ActionNew.ActionLetter);
|
||||
permissionToCheck.Add(ActionUpdate.ActionLetter);
|
||||
permissionToCheck.Add(ActionPublish.ActionLetter);
|
||||
@@ -217,6 +220,9 @@ namespace Umbraco.Web.Editors.Filters
|
||||
if (accessResult == ContentPermissionsHelper.ContentAccess.NotFound)
|
||||
throw new HttpResponseException(HttpStatusCode.NotFound);
|
||||
|
||||
if(accessResult == ContentPermissionsHelper.ContentAccess.Denied)
|
||||
actionContext.Response = actionContext.Request.CreateUserNoAccessResponse();
|
||||
|
||||
return accessResult == ContentPermissionsHelper.ContentAccess.Granted;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
using System.IO;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Web;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.Configuration.UmbracoSettings;
|
||||
using Umbraco.Core.IO;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Web.Media;
|
||||
using Umbraco.Web.Mvc;
|
||||
using Umbraco.Web.Routing;
|
||||
using Umbraco.Web.WebApi;
|
||||
using Constants = Umbraco.Core.Constants;
|
||||
|
||||
namespace Umbraco.Web.Editors
|
||||
{
|
||||
@@ -60,20 +60,29 @@ namespace Umbraco.Web.Editors
|
||||
/// </remarks>
|
||||
public HttpResponseMessage GetResized(string imagePath, int width)
|
||||
{
|
||||
// We have to use HttpUtility to encode the path here, for non-ASCII characters
|
||||
// We cannot use the WebUtility, as we only want to encode the path, and not the entire string
|
||||
var encodedImagePath = HttpUtility.UrlPathEncode(imagePath);
|
||||
|
||||
var ext = Path.GetExtension(imagePath);
|
||||
|
||||
// check if imagePath is local to prevent open redirect
|
||||
if (!IsAllowed(encodedImagePath))
|
||||
{
|
||||
return Request.CreateResponse(HttpStatusCode.Unauthorized);
|
||||
}
|
||||
|
||||
// we need to check if it is an image by extension
|
||||
if (_contentSection.IsImageFile(ext) == false)
|
||||
return Request.CreateResponse(HttpStatusCode.NotFound);
|
||||
|
||||
//redirect to ImageProcessor thumbnail with rnd generated from last modified time of original media file
|
||||
var response = Request.CreateResponse(HttpStatusCode.Found);
|
||||
|
||||
DateTimeOffset? imageLastModified = null;
|
||||
try
|
||||
{
|
||||
imageLastModified = _mediaFileSystem.GetLastModified(imagePath);
|
||||
|
||||
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
@@ -84,11 +93,89 @@ namespace Umbraco.Web.Editors
|
||||
}
|
||||
|
||||
var rnd = imageLastModified.HasValue ? $"&rnd={imageLastModified:yyyyMMddHHmmss}" : null;
|
||||
var imageUrl = _imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(imagePath) { UpScale = false, Width = width, AnimationProcessMode = "first", ImageCropMode = "max", CacheBusterValue = rnd });
|
||||
var imageUrl = _imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(encodedImagePath) { UpScale = false, Width = width, AnimationProcessMode = "first", ImageCropMode = "max", CacheBusterValue = rnd });
|
||||
|
||||
var response = Request.CreateResponse(HttpStatusCode.Found);
|
||||
response.Headers.Location = new Uri(imageUrl, UriKind.RelativeOrAbsolute);
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
private bool IsAllowed(string encodedImagePath)
|
||||
{
|
||||
|
||||
if(WebPath.IsWellFormedWebPath(encodedImagePath, UriKind.Relative))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_contentSection is ContentElement contentElement)
|
||||
{
|
||||
var builder = new UriBuilder(encodedImagePath);
|
||||
|
||||
foreach (var allowedMediaHost in contentElement.AllowedMediaHosts)
|
||||
{
|
||||
if (string.Equals(builder.Host, allowedMediaHost, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 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="mode"></param>
|
||||
/// <param name="cacheBusterValue"></param>
|
||||
/// <param name="cropX1"></param>
|
||||
/// <param name="cropX2"></param>
|
||||
/// <param name="cropY1"></param>
|
||||
/// <param name="cropY2"></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,
|
||||
decimal? focalPointLeft = null,
|
||||
decimal? focalPointTop = null,
|
||||
string mode = "max",
|
||||
string cacheBusterValue = "",
|
||||
decimal? cropX1 = null,
|
||||
decimal? cropX2 = null,
|
||||
decimal? cropY1 = null,
|
||||
decimal? cropY2 = null)
|
||||
{
|
||||
var options = new ImageUrlGenerationOptions(imagePath)
|
||||
{
|
||||
Width = width,
|
||||
Height = height,
|
||||
ImageCropMode = mode,
|
||||
CacheBusterValue = cacheBusterValue
|
||||
};
|
||||
|
||||
if (focalPointLeft.HasValue && focalPointTop.HasValue)
|
||||
{
|
||||
options.FocalPoint =
|
||||
new ImageUrlGenerationOptions.FocalPointPosition(focalPointLeft.Value, focalPointTop.Value);
|
||||
}
|
||||
else if (cropX1.HasValue && cropX2.HasValue && cropY1.HasValue && cropY2.HasValue)
|
||||
{
|
||||
options.Crop =
|
||||
new ImageUrlGenerationOptions.CropCoordinates(cropX1.Value, cropY1.Value, cropX2.Value, cropY2.Value);
|
||||
}
|
||||
|
||||
return _imageUrlGenerator.GetImageUrl(options);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ using Umbraco.Core.Models;
|
||||
using Umbraco.Web.Mvc;
|
||||
using Umbraco.Web.WebApi;
|
||||
using Umbraco.Web.WebApi.Filters;
|
||||
using Constants = Umbraco.Core.Constants;
|
||||
using Language = Umbraco.Web.Models.ContentEditing.Language;
|
||||
|
||||
namespace Umbraco.Web.Editors
|
||||
@@ -18,6 +19,7 @@ namespace Umbraco.Web.Editors
|
||||
/// </summary>
|
||||
[PluginController("UmbracoApi")]
|
||||
[PrefixlessBodyModelValidator]
|
||||
|
||||
public class LanguageController : UmbracoAuthorizedJsonController
|
||||
{
|
||||
/// <summary>
|
||||
@@ -25,6 +27,7 @@ namespace Umbraco.Web.Editors
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
[HttpGet]
|
||||
[UmbracoTreeAuthorize(Constants.Trees.Languages)]
|
||||
public IDictionary<string, string> GetAllCultures()
|
||||
{
|
||||
// get cultures - new-ing instances to get proper display name,
|
||||
@@ -50,6 +53,7 @@ namespace Umbraco.Web.Editors
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[UmbracoTreeAuthorize(Constants.Trees.Languages)]
|
||||
public Language GetLanguage(int id)
|
||||
{
|
||||
var lang = Services.LocalizationService.GetLanguageById(id);
|
||||
|
||||
@@ -37,6 +37,7 @@ using Umbraco.Web.ContentApps;
|
||||
using Umbraco.Web.Editors.Binders;
|
||||
using Umbraco.Web.Editors.Filters;
|
||||
using Umbraco.Core.Models.Entities;
|
||||
using Umbraco.Core.Security;
|
||||
|
||||
namespace Umbraco.Web.Editors
|
||||
{
|
||||
@@ -49,10 +50,39 @@ namespace Umbraco.Web.Editors
|
||||
[MediaControllerControllerConfiguration]
|
||||
public class MediaController : ContentControllerBase
|
||||
{
|
||||
public MediaController(PropertyEditorCollection propertyEditors, IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper)
|
||||
[Obsolete("Use the constructor specifying all dependencies instead.")]
|
||||
public MediaController(PropertyEditorCollection propertyEditors, IGlobalSettings globalSettings,
|
||||
IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services,
|
||||
AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper)
|
||||
: this(propertyEditors,
|
||||
globalSettings,
|
||||
umbracoContextAccessor,
|
||||
sqlContext,
|
||||
services,
|
||||
appCaches,
|
||||
logger,
|
||||
runtimeState,
|
||||
umbracoHelper,
|
||||
Current.Factory.GetInstance<IFileStreamSecurityValidator>())
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public MediaController(
|
||||
PropertyEditorCollection propertyEditors,
|
||||
IGlobalSettings globalSettings,
|
||||
IUmbracoContextAccessor umbracoContextAccessor,
|
||||
ISqlContext sqlContext,
|
||||
ServiceContext services,
|
||||
AppCaches appCaches,
|
||||
IProfilingLogger logger,
|
||||
IRuntimeState runtimeState,
|
||||
UmbracoHelper umbracoHelper,
|
||||
IFileStreamSecurityValidator fileStreamSecurityValidator)
|
||||
: base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper)
|
||||
{
|
||||
_propertyEditors = propertyEditors ?? throw new ArgumentNullException(nameof(propertyEditors));
|
||||
_fileStreamSecurityValidator = fileStreamSecurityValidator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -236,6 +266,7 @@ namespace Umbraco.Web.Editors
|
||||
|
||||
private int[] _userStartNodes;
|
||||
private readonly PropertyEditorCollection _propertyEditors;
|
||||
private readonly IFileStreamSecurityValidator _fileStreamSecurityValidator;
|
||||
|
||||
protected int[] UserStartNodes
|
||||
{
|
||||
@@ -598,6 +629,13 @@ namespace Umbraco.Web.Editors
|
||||
{
|
||||
var intParentId = GetParentIdAsInt(folder.ParentId, validatePermissions:true);
|
||||
|
||||
var isFolderAllowed = IsFolderCreationAllowedHere(intParentId);
|
||||
if (isFolderAllowed == false)
|
||||
{
|
||||
throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse(
|
||||
Services.TextService.Localize("speechBubbles", "folderCreationNotAllowed")));
|
||||
}
|
||||
|
||||
var mediaService = Services.MediaService;
|
||||
|
||||
var f = mediaService.CreateMedia(folder.Name, intParentId, Constants.Conventions.MediaTypes.Folder);
|
||||
@@ -640,10 +678,16 @@ namespace Umbraco.Web.Editors
|
||||
|
||||
var tempFiles = new PostedFiles();
|
||||
var mediaService = Services.MediaService;
|
||||
var localizedTextService = Services.TextService;
|
||||
|
||||
//in case we pass a path with a folder in it, we will create it and upload media to it.
|
||||
if (result.FormData.ContainsKey("path"))
|
||||
{
|
||||
if (!IsFolderCreationAllowedHere(parentId))
|
||||
{
|
||||
AddCancelMessage(tempFiles, message: "speechBubbles/folderUploadNotAllowed");
|
||||
return Request.CreateResponse(HttpStatusCode.OK, tempFiles);
|
||||
}
|
||||
|
||||
var folders = result.FormData["path"].Split(Constants.CharArrays.ForwardSlash);
|
||||
|
||||
@@ -653,7 +697,7 @@ namespace Umbraco.Web.Editors
|
||||
IMedia folderMediaItem;
|
||||
|
||||
//if uploading directly to media root and not a subfolder
|
||||
if (parentId == -1)
|
||||
if (parentId == Constants.System.Root)
|
||||
{
|
||||
//look for matching folder
|
||||
folderMediaItem =
|
||||
@@ -691,6 +735,44 @@ namespace Umbraco.Web.Editors
|
||||
}
|
||||
}
|
||||
|
||||
var mediaTypeAlias = string.Empty;
|
||||
var allMediaTypes = Services.MediaTypeService.GetAll().ToList();
|
||||
var allowedContentTypes = new HashSet<IMediaType>();
|
||||
|
||||
if (parentId != Constants.System.Root)
|
||||
{
|
||||
var mediaFolderItem = mediaService.GetById(parentId);
|
||||
var mediaFolderType = allMediaTypes.FirstOrDefault(x => x.Alias == mediaFolderItem.ContentType.Alias);
|
||||
|
||||
if (mediaFolderType != null)
|
||||
{
|
||||
IMediaType mediaTypeItem = null;
|
||||
|
||||
foreach (ContentTypeSort allowedContentType in mediaFolderType.AllowedContentTypes)
|
||||
{
|
||||
IMediaType checkMediaTypeItem = allMediaTypes.FirstOrDefault(x => x.Id == allowedContentType.Id.Value);
|
||||
allowedContentTypes.Add(checkMediaTypeItem);
|
||||
|
||||
var fileProperty = checkMediaTypeItem?.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == Constants.Conventions.Media.File);
|
||||
if (fileProperty != null)
|
||||
{
|
||||
mediaTypeItem = checkMediaTypeItem;
|
||||
}
|
||||
}
|
||||
|
||||
//Only set the permission-based mediaType if we only allow 1 specific file under this parent.
|
||||
if (allowedContentTypes.Count == 1 && mediaTypeItem != null)
|
||||
{
|
||||
mediaTypeAlias = mediaTypeItem.Alias;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var typesAllowedAtRoot = allMediaTypes.Where(x => x.AllowedAsRoot).ToList();
|
||||
allowedContentTypes.UnionWith(typesAllowedAtRoot);
|
||||
}
|
||||
|
||||
//get the files
|
||||
foreach (var file in result.FileData)
|
||||
{
|
||||
@@ -698,81 +780,105 @@ namespace Umbraco.Web.Editors
|
||||
var safeFileName = fileName.ToSafeFileName();
|
||||
var ext = safeFileName.Substring(safeFileName.LastIndexOf('.') + 1).ToLower();
|
||||
|
||||
if (Current.Configs.Settings().Content.IsFileAllowedForUpload(ext))
|
||||
if (!Current.Configs.Settings().Content.IsFileAllowedForUpload(ext))
|
||||
{
|
||||
var mediaType = Constants.Conventions.MediaTypes.File;
|
||||
tempFiles.Notifications.Add(new Notification(
|
||||
localizedTextService.Localize("speechBubbles", "operationFailedHeader"),
|
||||
localizedTextService.Localize("media", "disallowedFileType"),
|
||||
NotificationStyle.Warning));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(mediaTypeAlias))
|
||||
{
|
||||
mediaTypeAlias = Constants.Conventions.MediaTypes.File;
|
||||
|
||||
if (result.FormData["contentTypeAlias"] == Constants.Conventions.MediaTypes.AutoSelect)
|
||||
{
|
||||
var mediaTypes = Services.MediaTypeService.GetAll();
|
||||
// Look up MediaTypes
|
||||
foreach (var mediaTypeItem in mediaTypes)
|
||||
foreach (var mediaTypeItem in allMediaTypes)
|
||||
{
|
||||
var fileProperty = mediaTypeItem.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == "umbracoFile");
|
||||
if (fileProperty != null) {
|
||||
var dataTypeKey = fileProperty.DataTypeKey;
|
||||
var dataType = Services.DataTypeService.GetDataType(dataTypeKey);
|
||||
|
||||
if (dataType != null && dataType.Configuration is IFileExtensionsConfig fileExtensionsConfig) {
|
||||
var fileExtensions = fileExtensionsConfig.FileExtensions;
|
||||
if (fileExtensions != null)
|
||||
{
|
||||
if (fileExtensions.Where(x => x.Value == ext).Count() != 0)
|
||||
{
|
||||
mediaType = mediaTypeItem.Alias;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
var fileProperty = mediaTypeItem.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == Constants.Conventions.Media.File);
|
||||
if (fileProperty == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var dataTypeKey = fileProperty.DataTypeKey;
|
||||
var dataType = Services.DataTypeService.GetDataType(dataTypeKey);
|
||||
|
||||
if (dataType == null || dataType.Configuration is not IFileExtensionsConfig fileExtensionsConfig)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var fileExtensions = fileExtensionsConfig.FileExtensions;
|
||||
if (fileExtensions == null || fileExtensions.All(x => x.Value != ext))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
mediaTypeAlias = mediaTypeItem.Alias;
|
||||
break;
|
||||
}
|
||||
|
||||
// If media type is still File then let's check if it's an image.
|
||||
if (mediaType == Constants.Conventions.MediaTypes.File && Current.Configs.Settings().Content.ImageFileTypes.Contains(ext))
|
||||
if (mediaTypeAlias == Constants.Conventions.MediaTypes.File && Current.Configs.Settings().Content.ImageFileTypes.Contains(ext))
|
||||
{
|
||||
mediaType = Constants.Conventions.MediaTypes.Image;
|
||||
mediaTypeAlias = Constants.Conventions.MediaTypes.Image;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
mediaType = result.FormData["contentTypeAlias"];
|
||||
mediaTypeAlias = result.FormData["contentTypeAlias"];
|
||||
}
|
||||
}
|
||||
|
||||
var mediaItemName = fileName.ToFriendlyName();
|
||||
if (allowedContentTypes.Any(x => x.Alias == mediaTypeAlias) == false)
|
||||
{
|
||||
tempFiles.Notifications.Add(new Notification(
|
||||
localizedTextService.Localize("speechBubbles", "operationFailedHeader"),
|
||||
localizedTextService.Localize("media", "disallowedMediaType", new[] { mediaTypeAlias }),
|
||||
NotificationStyle.Warning));
|
||||
continue;
|
||||
}
|
||||
|
||||
var f = mediaService.CreateMedia(mediaItemName, parentId, mediaType, Security.CurrentUser.Id);
|
||||
var fileInfo = new FileInfo(file.LocalFileName);
|
||||
var mediaFileStream = fileInfo.OpenReadWithRetry();
|
||||
if (mediaFileStream == null)
|
||||
{
|
||||
throw new InvalidOperationException("Could not acquire file stream");
|
||||
}
|
||||
|
||||
var fileInfo = new FileInfo(file.LocalFileName);
|
||||
var fs = fileInfo.OpenReadWithRetry();
|
||||
if (fs == null) throw new InvalidOperationException("Could not acquire file stream");
|
||||
using (fs)
|
||||
{
|
||||
f.SetValue(Services.ContentTypeBaseServices, Constants.Conventions.Media.File,fileName, fs);
|
||||
}
|
||||
if (_fileStreamSecurityValidator != null && _fileStreamSecurityValidator.IsConsideredSafe(mediaFileStream) is false)
|
||||
{
|
||||
tempFiles.Notifications.Add(new Notification(
|
||||
localizedTextService.Localize("speechBubbles", "operationFailedHeader"),
|
||||
localizedTextService.Localize("media", "fileSecurityValidationFailure"),
|
||||
NotificationStyle.Warning));
|
||||
continue;
|
||||
}
|
||||
|
||||
var saveResult = mediaService.Save(f, Security.CurrentUser.Id);
|
||||
if (saveResult == false)
|
||||
{
|
||||
AddCancelMessage(tempFiles,
|
||||
message: Services.TextService.Localize("speechBubbles", "operationCancelledText") + " -- " + mediaItemName);
|
||||
}
|
||||
else
|
||||
{
|
||||
tempFiles.UploadedFiles.Add(new ContentPropertyFile
|
||||
{
|
||||
FileName = fileName,
|
||||
PropertyAlias = Constants.Conventions.Media.File,
|
||||
TempFilePath = file.LocalFileName
|
||||
});
|
||||
}
|
||||
var mediaItemName = fileName.ToFriendlyName();
|
||||
var createdMediaItem = mediaService.CreateMedia(mediaItemName, parentId, mediaTypeAlias, Security.CurrentUser.Id);
|
||||
using (mediaFileStream)
|
||||
{
|
||||
createdMediaItem.SetValue(Services.ContentTypeBaseServices, Constants.Conventions.Media.File, fileName, mediaFileStream);
|
||||
}
|
||||
|
||||
var saveResult = mediaService.Save(createdMediaItem, Security.CurrentUser.Id);
|
||||
if (saveResult == false)
|
||||
{
|
||||
AddCancelMessage(tempFiles, message: "speechBubbles/operationCancelledText" + " -- " + mediaItemName);
|
||||
}
|
||||
else
|
||||
{
|
||||
tempFiles.Notifications.Add(new Notification(
|
||||
Services.TextService.Localize("speechBubbles", "operationFailedHeader"),
|
||||
Services.TextService.Localize("media", "disallowedFileType"),
|
||||
NotificationStyle.Warning));
|
||||
tempFiles.UploadedFiles.Add(new ContentPropertyFile
|
||||
{
|
||||
FileName = fileName,
|
||||
PropertyAlias = Constants.Conventions.Media.File,
|
||||
TempFilePath = file.LocalFileName
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -792,6 +898,29 @@ namespace Umbraco.Web.Editors
|
||||
return Request.CreateResponse(HttpStatusCode.OK, tempFiles);
|
||||
}
|
||||
|
||||
private bool IsFolderCreationAllowedHere(int parentId)
|
||||
{
|
||||
var allMediaTypes = Services.MediaTypeService.GetAll().ToList();
|
||||
var isFolderAllowed = false;
|
||||
if (parentId == Constants.System.Root)
|
||||
{
|
||||
var typesAllowedAtRoot = allMediaTypes.Where(ct => ct.AllowedAsRoot).ToList();
|
||||
isFolderAllowed = typesAllowedAtRoot.Any(x => x.Alias == Constants.Conventions.MediaTypes.Folder);
|
||||
}
|
||||
else
|
||||
{
|
||||
var parentMediaType = Services.MediaService.GetById(parentId);
|
||||
var mediaFolderType = allMediaTypes.FirstOrDefault(x => x.Alias == parentMediaType.ContentType.Alias);
|
||||
if (mediaFolderType != null)
|
||||
{
|
||||
isFolderAllowed =
|
||||
mediaFolderType.AllowedContentTypes.Any(x => x.Alias == Constants.Conventions.MediaTypes.Folder);
|
||||
}
|
||||
}
|
||||
|
||||
return isFolderAllowed;
|
||||
}
|
||||
|
||||
private IMedia FindInChildren(int mediaId, string nameToFind, string contentTypeAlias)
|
||||
{
|
||||
const int pageSize = 500;
|
||||
|
||||
@@ -5,9 +5,11 @@ using Umbraco.Web.Composing;
|
||||
using Umbraco.Web.PublishedCache;
|
||||
using Umbraco.Web.PublishedCache.NuCache;
|
||||
using Umbraco.Web.WebApi;
|
||||
using Umbraco.Web.WebApi.Filters;
|
||||
|
||||
namespace Umbraco.Web.Editors
|
||||
{
|
||||
[UmbracoApplicationAuthorize(Core.Constants.Applications.Settings)]
|
||||
public class NuCacheStatusController : UmbracoAuthorizedApiController
|
||||
{
|
||||
private readonly IPublishedSnapshotService _publishedSnapshotService;
|
||||
|
||||
@@ -25,6 +25,8 @@ namespace Umbraco.Web.Editors
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private static bool IsEnabled => Current.Configs.Settings().WebRouting.DisableRedirectUrlTracking == false;
|
||||
|
||||
/// <summary>
|
||||
/// Returns true/false of whether redirect tracking is enabled or not
|
||||
/// </summary>
|
||||
@@ -32,9 +34,8 @@ namespace Umbraco.Web.Editors
|
||||
[HttpGet]
|
||||
public IHttpActionResult GetEnableState()
|
||||
{
|
||||
var enabled = Current.Configs.Settings().WebRouting.DisableRedirectUrlTracking == false;
|
||||
var userIsAdmin = UmbracoContext.Security.CurrentUser.IsAdmin();
|
||||
return Ok(new { enabled, userIsAdmin });
|
||||
return Ok(new { enabled = IsEnabled, userIsAdmin });
|
||||
}
|
||||
|
||||
//add paging
|
||||
@@ -83,6 +84,11 @@ namespace Umbraco.Web.Editors
|
||||
[HttpPost]
|
||||
public IHttpActionResult DeleteRedirectUrl(Guid id)
|
||||
{
|
||||
if (IsEnabled is false)
|
||||
{
|
||||
return BadRequest("Redirect tracking is disabled.");
|
||||
}
|
||||
|
||||
var redirectUrlService = Services.RedirectUrlService;
|
||||
redirectUrlService.Delete(id);
|
||||
return Ok();
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Linq;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Web.Models.ContentEditing;
|
||||
using Umbraco.Web.Mvc;
|
||||
using Umbraco.Web.WebApi.Filters;
|
||||
|
||||
namespace Umbraco.Web.Editors
|
||||
{
|
||||
@@ -10,6 +11,8 @@ namespace Umbraco.Web.Editors
|
||||
/// The API controller used for retrieving available stylesheets
|
||||
/// </summary>
|
||||
[PluginController("UmbracoApi")]
|
||||
// This is a bit wierd, but if you have access to the content section, you can load a rich text editor, and thus need to get the rules.
|
||||
[UmbracoApplicationAuthorize(Core.Constants.Applications.Content)]
|
||||
public class StylesheetController : UmbracoAuthorizedJsonController
|
||||
{
|
||||
public IEnumerable<Stylesheet> GetAll()
|
||||
@@ -32,4 +35,4 @@ namespace Umbraco.Web.Editors
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
@@ -28,6 +29,18 @@ namespace Umbraco.Web.Editors
|
||||
private IMediaService _mediaService;
|
||||
private IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider;
|
||||
|
||||
private readonly Dictionary<string, string> _fileContentTypeMappings = new()
|
||||
{
|
||||
{ "image/png", "png" },
|
||||
{ "image/jpeg", "jpg" },
|
||||
{ "image/gif", "gif" },
|
||||
{ "image/bmp", "bmp" },
|
||||
{ "image/x-icon", "ico" },
|
||||
{ "image/svg+xml", "svg" },
|
||||
{ "image/tiff", "tiff" },
|
||||
{ "image/webp", "webp" },
|
||||
};
|
||||
|
||||
|
||||
public TinyMceController(IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider)
|
||||
{
|
||||
@@ -76,7 +89,20 @@ namespace Umbraco.Web.Editors
|
||||
var file = result.FileData[0];
|
||||
var fileName = file.Headers.ContentDisposition.FileName.Trim(Constants.CharArrays.DoubleQuote).TrimEnd();
|
||||
var safeFileName = fileName.ToSafeFileName();
|
||||
var ext = safeFileName.Substring(safeFileName.LastIndexOf('.') + 1).ToLower();
|
||||
string ext;
|
||||
var fileExtensionIndex = safeFileName.LastIndexOf('.');
|
||||
if (fileExtensionIndex is not -1)
|
||||
{
|
||||
ext = safeFileName.Substring(fileExtensionIndex + 1).ToLowerInvariant();
|
||||
}
|
||||
else
|
||||
{
|
||||
_fileContentTypeMappings.TryGetValue(file.Headers.ContentType.MediaType, out var fileExtension);
|
||||
ext = fileExtension ?? string.Empty;
|
||||
|
||||
// safeFileName will not have a file extension, so we need to add it back
|
||||
safeFileName += $".{ext}";
|
||||
}
|
||||
|
||||
if (Current.Configs.Settings().Content.IsFileAllowedForUpload(ext) == false || Current.Configs.Settings().Content.ImageFileTypes.Contains(ext) == false)
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
@@ -49,6 +50,7 @@ namespace Umbraco.Web.Editors
|
||||
public class UsersController : UmbracoAuthorizedJsonController
|
||||
{
|
||||
private readonly IUmbracoSettingsSection _umbracoSettingsSection;
|
||||
private readonly IFileStreamSecurityValidator _fileStreamSecurityValidator;
|
||||
|
||||
public UsersController(
|
||||
IGlobalSettings globalSettings,
|
||||
@@ -59,10 +61,27 @@ namespace Umbraco.Web.Editors
|
||||
IProfilingLogger logger,
|
||||
IRuntimeState runtimeState,
|
||||
UmbracoHelper umbracoHelper,
|
||||
IUmbracoSettingsSection umbracoSettingsSection)
|
||||
IUmbracoSettingsSection umbracoSettingsSection,
|
||||
IFileStreamSecurityValidator fileStreamSecurityValidator)
|
||||
: base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper)
|
||||
{
|
||||
_umbracoSettingsSection = umbracoSettingsSection;
|
||||
_fileStreamSecurityValidator = fileStreamSecurityValidator;
|
||||
}
|
||||
|
||||
[Obsolete("Use the overload specifying all dependencies instead")]
|
||||
public UsersController(
|
||||
IGlobalSettings globalSettings,
|
||||
IUmbracoContextAccessor umbracoContextAccessor,
|
||||
ISqlContext sqlContext,
|
||||
ServiceContext services,
|
||||
AppCaches appCaches,
|
||||
IProfilingLogger logger,
|
||||
IRuntimeState runtimeState,
|
||||
UmbracoHelper umbracoHelper,
|
||||
IUmbracoSettingsSection umbracoSettingsSection)
|
||||
: this(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper, umbracoSettingsSection, Current.Factory.GetInstance<IFileStreamSecurityValidator>())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -83,10 +102,15 @@ namespace Umbraco.Web.Editors
|
||||
[AdminUsersAuthorize]
|
||||
public async Task<HttpResponseMessage> PostSetAvatar(int id)
|
||||
{
|
||||
return await PostSetAvatarInternal(Request, Services.UserService, AppCaches.RuntimeCache, id);
|
||||
return await PostSetAvatarInternal(Request, Services.UserService, _fileStreamSecurityValidator, AppCaches.RuntimeCache, id);
|
||||
}
|
||||
|
||||
internal static async Task<HttpResponseMessage> PostSetAvatarInternal(HttpRequestMessage request, IUserService userService, IAppCache cache, int id)
|
||||
internal static async Task<HttpResponseMessage> PostSetAvatarInternal(
|
||||
HttpRequestMessage request,
|
||||
IUserService userService,
|
||||
IFileStreamSecurityValidator fileStreamSecurityValidator,
|
||||
IAppCache cache,
|
||||
int id)
|
||||
{
|
||||
if (request.Content.IsMimeMultipartContent() == false)
|
||||
{
|
||||
@@ -120,14 +144,20 @@ namespace Umbraco.Web.Editors
|
||||
var fileName = file.Headers.ContentDisposition.FileName.Trim(Constants.CharArrays.DoubleQuote).TrimEnd();
|
||||
var safeFileName = fileName.ToSafeFileName();
|
||||
var ext = safeFileName.Substring(safeFileName.LastIndexOf('.') + 1).ToLower();
|
||||
const string allowedAvatarFileTypes = "jpeg,jpg,gif,bmp,png,tiff,tif,webp";
|
||||
|
||||
if (Current.Configs.Settings().Content.DisallowedUploadFiles.Contains(ext) == false)
|
||||
if (allowedAvatarFileTypes.Contains(ext) == true && Current.Configs.Settings().Content.DisallowedUploadFiles.Contains(ext) == false)
|
||||
{
|
||||
//generate a path of known data, we don't want this path to be guessable
|
||||
user.Avatar = "UserAvatars/" + (user.Id + safeFileName).GenerateHash<SHA1>() + "." + ext;
|
||||
|
||||
using (var fs = System.IO.File.OpenRead(file.LocalFileName))
|
||||
{
|
||||
if (fileStreamSecurityValidator.IsConsideredSafe(fs) is false)
|
||||
{
|
||||
return request.CreateValidationErrorResponse("The uploaded file is not permitted due to security reasons");
|
||||
}
|
||||
|
||||
Current.MediaFileSystem.AddFile(user.Avatar, fs, true);
|
||||
}
|
||||
|
||||
@@ -549,7 +579,7 @@ namespace Umbraco.Web.Editors
|
||||
var emailBody = Services.TextService.Localize("user", "inviteEmailCopyFormat",
|
||||
//Ensure the culture of the found user is used for the email!
|
||||
UserExtensions.GetUserCulture(to.Language, Services.TextService, GlobalSettings),
|
||||
new[] { userDisplay.Name, from, message, inviteUri.ToString(), fromEmail });
|
||||
new[] { userDisplay.Name, from, Regex.Replace(WebUtility.HtmlEncode(message),"(\r\n|\r|\n)", "<br/>"), inviteUri.ToString(), fromEmail });
|
||||
|
||||
await UserManager.EmailService.SendAsync(
|
||||
//send the special UmbracoEmailMessage which configures it's own sender
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Umbraco.Core.Configuration.UmbracoSettings;
|
||||
using Umbraco.Core.IO;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Telemetry;
|
||||
using Umbraco.Web.Install.Models;
|
||||
|
||||
namespace Umbraco.Web.Install.InstallSteps
|
||||
@@ -14,54 +10,20 @@ namespace Umbraco.Web.Install.InstallSteps
|
||||
PerformsAppRestart = false)]
|
||||
internal class TelemetryIdentifierStep : InstallSetupStep<object>
|
||||
{
|
||||
private readonly IProfilingLogger _logger;
|
||||
private readonly IUmbracoSettingsSection _settings;
|
||||
private readonly ISiteIdentifierService _siteIdentifierService;
|
||||
|
||||
public TelemetryIdentifierStep(IProfilingLogger logger, IUmbracoSettingsSection settings)
|
||||
public TelemetryIdentifierStep(
|
||||
IUmbracoSettingsSection settings,
|
||||
ISiteIdentifierService siteIdentifierService)
|
||||
{
|
||||
_logger = logger;
|
||||
_settings = settings;
|
||||
_siteIdentifierService = siteIdentifierService;
|
||||
}
|
||||
|
||||
public override Task<InstallSetupResult> ExecuteAsync(object model)
|
||||
{
|
||||
// Generate GUID
|
||||
var telemetrySiteIdentifier = Guid.NewGuid();
|
||||
|
||||
// Modify the XML to add a new GUID site identifier
|
||||
// hack: ensure this does not trigger a restart
|
||||
using (ChangesMonitor.Suspended())
|
||||
{
|
||||
var umbracoSettingsPath = IOHelper.MapPath(SystemFiles.UmbracoSettings);
|
||||
if(File.Exists(umbracoSettingsPath) == false)
|
||||
{
|
||||
// Log an error
|
||||
_logger.Error<TelemetryIdentifierStep>("Unable to find umbracoSettings.config file to add telemetry site identifier");
|
||||
return Task.FromResult<InstallSetupResult>(null);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var umbracoConfigXml = XDocument.Load(umbracoSettingsPath, LoadOptions.PreserveWhitespace);
|
||||
if (umbracoConfigXml.Root != null)
|
||||
{
|
||||
var backofficeElement = umbracoConfigXml.Root.Element("backOffice");
|
||||
if (backofficeElement == null)
|
||||
return Task.FromResult<InstallSetupResult>(null);
|
||||
|
||||
// Will add ID attribute if it does not exist
|
||||
backofficeElement.SetAttributeValue("id", telemetrySiteIdentifier.ToString());
|
||||
|
||||
// Save file back down
|
||||
umbracoConfigXml.Save(umbracoSettingsPath, SaveOptions.DisableFormatting);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error<TelemetryIdentifierStep>(ex, "Couldn't update umbracoSettings.config with a backoffice with a telemetry site identifier");
|
||||
}
|
||||
}
|
||||
|
||||
_siteIdentifierService.TryCreateSiteIdentifier(out _);
|
||||
return Task.FromResult<InstallSetupResult>(null);
|
||||
}
|
||||
|
||||
|
||||
@@ -87,8 +87,7 @@ namespace Umbraco.Web.Models.Mapping
|
||||
{
|
||||
MapSaveToTypeBase<DocumentTypeSave, PropertyTypeBasic>(source, target, context);
|
||||
MapComposition(source, target, alias => _contentTypeService.Get(alias));
|
||||
|
||||
target.HistoryCleanup = source.HistoryCleanup;
|
||||
MapHistoryCleanup(source, target);
|
||||
|
||||
target.AllowedTemplates = source.AllowedTemplates
|
||||
.Where(x => x != null)
|
||||
@@ -99,6 +98,34 @@ namespace Umbraco.Web.Models.Mapping
|
||||
target.SetDefaultTemplate(source.DefaultTemplate == null ? null : _fileService.GetTemplate(source.DefaultTemplate));
|
||||
}
|
||||
|
||||
private static void MapHistoryCleanup(DocumentTypeSave source, IContentType target)
|
||||
{
|
||||
// If source history cleanup is null we don't have to map all properties
|
||||
if (source.HistoryCleanup is null)
|
||||
{
|
||||
target.HistoryCleanup = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// We need to reset the dirty properties, because it is otherwise true, just because the json serializer has set properties
|
||||
target.HistoryCleanup.ResetDirtyProperties(false);
|
||||
if (target.HistoryCleanup.PreventCleanup != source.HistoryCleanup.PreventCleanup)
|
||||
{
|
||||
target.HistoryCleanup.PreventCleanup = source.HistoryCleanup.PreventCleanup;
|
||||
}
|
||||
|
||||
if (target.HistoryCleanup.KeepAllVersionsNewerThanDays != source.HistoryCleanup.KeepAllVersionsNewerThanDays)
|
||||
{
|
||||
target.HistoryCleanup.KeepAllVersionsNewerThanDays = source.HistoryCleanup.KeepAllVersionsNewerThanDays;
|
||||
}
|
||||
|
||||
if (target.HistoryCleanup.KeepLatestVersionPerDayForDays !=
|
||||
source.HistoryCleanup.KeepLatestVersionPerDayForDays)
|
||||
{
|
||||
target.HistoryCleanup.KeepLatestVersionPerDayForDays = source.HistoryCleanup.KeepLatestVersionPerDayForDays;
|
||||
}
|
||||
}
|
||||
|
||||
// no MapAll - take care
|
||||
private void Map(MediaTypeSave source, IMediaType target, MapperContext context)
|
||||
{
|
||||
@@ -253,7 +280,12 @@ namespace Umbraco.Web.Models.Mapping
|
||||
target.Id = source.Id;
|
||||
|
||||
if (source.GroupId > 0)
|
||||
target.PropertyGroupId = new Lazy<int>(() => source.GroupId, false);
|
||||
{
|
||||
if (target.PropertyGroupId?.Value != source.GroupId)
|
||||
{
|
||||
target.PropertyGroupId = new Lazy<int>(() => source.GroupId, false);
|
||||
}
|
||||
}
|
||||
|
||||
target.Alias = source.Alias;
|
||||
target.Description = source.Description;
|
||||
@@ -476,13 +508,23 @@ namespace Umbraco.Web.Models.Mapping
|
||||
|
||||
// ensure no duplicate alias, then assign the group properties collection
|
||||
EnsureUniqueAliases(destProperties);
|
||||
destGroup.PropertyTypes = new PropertyTypeCollection(isPublishing, destProperties);
|
||||
|
||||
if (destGroup.PropertyTypes.SupportsPublishing != isPublishing || destGroup.PropertyTypes.SequenceEqual(destProperties) is false)
|
||||
{
|
||||
destGroup.PropertyTypes = new PropertyTypeCollection(isPublishing, destProperties);
|
||||
}
|
||||
|
||||
destGroups.Add(destGroup);
|
||||
}
|
||||
|
||||
// ensure no duplicate name, then assign the groups collection
|
||||
EnsureUniqueAliases(destGroups);
|
||||
target.PropertyGroups = new PropertyGroupCollection(destGroups);
|
||||
|
||||
|
||||
if (target.PropertyGroups.SequenceEqual(destGroups) is false)
|
||||
{
|
||||
target.PropertyGroups = new PropertyGroupCollection(destGroups);
|
||||
}
|
||||
|
||||
// because the property groups collection was rebuilt, there is no need to remove
|
||||
// the old groups - they are just gone and will be cleared by the repository
|
||||
|
||||
@@ -96,7 +96,7 @@ namespace Umbraco.Web.Models.Mapping
|
||||
// Umbraco.Code.MapAll -Udi -Trashed
|
||||
private static void Map(IUser source, EntityBasic target, MapperContext context)
|
||||
{
|
||||
target.Alias = source.Username;
|
||||
target.Alias = source.Key.ToString();
|
||||
target.Icon = Constants.Icons.User;
|
||||
target.Id = source.Id;
|
||||
target.Key = source.Key;
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.Configuration.UmbracoSettings;
|
||||
using Umbraco.Core.IO;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Core.Security;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Web.Media;
|
||||
|
||||
@@ -22,16 +24,27 @@ namespace Umbraco.Web.PropertyEditors
|
||||
{
|
||||
private readonly IMediaFileSystem _mediaFileSystem;
|
||||
private readonly IContentSection _contentSection;
|
||||
private readonly IFileStreamSecurityValidator _fileStreamSecurityValidator;
|
||||
private readonly UploadAutoFillProperties _uploadAutoFillProperties;
|
||||
|
||||
public FileUploadPropertyEditor(ILogger logger, IMediaFileSystem mediaFileSystem, IContentSection contentSection)
|
||||
public FileUploadPropertyEditor(
|
||||
ILogger logger,
|
||||
IMediaFileSystem mediaFileSystem,
|
||||
IContentSection contentSection,
|
||||
IFileStreamSecurityValidator fileStreamSecurityValidator)
|
||||
: base(logger)
|
||||
{
|
||||
_mediaFileSystem = mediaFileSystem ?? throw new ArgumentNullException(nameof(mediaFileSystem));
|
||||
_contentSection = contentSection;
|
||||
_fileStreamSecurityValidator = fileStreamSecurityValidator;
|
||||
_uploadAutoFillProperties = new UploadAutoFillProperties(_mediaFileSystem, logger, contentSection);
|
||||
}
|
||||
|
||||
[Obsolete("Use constructor that accepts IFileStreamSecurityValidator.")]
|
||||
public FileUploadPropertyEditor(ILogger logger, IMediaFileSystem mediaFileSystem, IContentSection contentSection)
|
||||
: this(logger, mediaFileSystem, contentSection, Current.Factory.GetInstance<IFileStreamSecurityValidator>())
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override IConfigurationEditor CreateConfigurationEditor() => new FileUploadConfigurationEditor();
|
||||
@@ -42,7 +55,7 @@ namespace Umbraco.Web.PropertyEditors
|
||||
/// <returns>The corresponding property value editor.</returns>
|
||||
protected override IDataValueEditor CreateValueEditor()
|
||||
{
|
||||
var editor = new FileUploadPropertyValueEditor(Attribute, _mediaFileSystem);
|
||||
var editor = new FileUploadPropertyValueEditor(Attribute, _mediaFileSystem, _fileStreamSecurityValidator);
|
||||
editor.Validators.Add(new UploadFileTypeValidator());
|
||||
return editor;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using Umbraco.Core;
|
||||
using Umbraco.Core.IO;
|
||||
using Umbraco.Core.Models.Editors;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Core.Security;
|
||||
|
||||
namespace Umbraco.Web.PropertyEditors
|
||||
{
|
||||
@@ -16,11 +17,16 @@ namespace Umbraco.Web.PropertyEditors
|
||||
internal class FileUploadPropertyValueEditor : DataValueEditor
|
||||
{
|
||||
private readonly IMediaFileSystem _mediaFileSystem;
|
||||
private readonly IFileStreamSecurityValidator _fileStreamSecurityValidator;
|
||||
|
||||
public FileUploadPropertyValueEditor(DataEditorAttribute attribute, IMediaFileSystem mediaFileSystem)
|
||||
public FileUploadPropertyValueEditor(
|
||||
DataEditorAttribute attribute,
|
||||
IMediaFileSystem mediaFileSystem,
|
||||
IFileStreamSecurityValidator fileStreamSecurityValidator)
|
||||
: base(attribute)
|
||||
{
|
||||
_mediaFileSystem = mediaFileSystem ?? throw new ArgumentNullException(nameof(mediaFileSystem));
|
||||
_fileStreamSecurityValidator = fileStreamSecurityValidator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -109,6 +115,11 @@ namespace Umbraco.Web.PropertyEditors
|
||||
|
||||
using (var filestream = File.OpenRead(file.TempFilePath))
|
||||
{
|
||||
if (_fileStreamSecurityValidator.IsConsideredSafe(filestream) is false)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// TODO: Here it would make sense to do the auto-fill properties stuff but the API doesn't allow us to do that right
|
||||
// since we'd need to be able to return values for other properties from these methods
|
||||
|
||||
|
||||
@@ -4,11 +4,13 @@ using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.Configuration.UmbracoSettings;
|
||||
using Umbraco.Core.IO;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Core.Security;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Web.Media;
|
||||
|
||||
@@ -30,29 +32,42 @@ namespace Umbraco.Web.PropertyEditors
|
||||
private readonly IMediaFileSystem _mediaFileSystem;
|
||||
private readonly IContentSection _contentSettings;
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly IFileStreamSecurityValidator _fileStreamSecurityValidator;
|
||||
private readonly UploadAutoFillProperties _autoFillProperties;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ImageCropperPropertyEditor"/> class.
|
||||
/// </summary>
|
||||
public ImageCropperPropertyEditor(ILogger logger, IMediaFileSystem mediaFileSystem, IContentSection contentSettings, IDataTypeService dataTypeService)
|
||||
public ImageCropperPropertyEditor(
|
||||
ILogger logger,
|
||||
IMediaFileSystem mediaFileSystem,
|
||||
IContentSection contentSettings,
|
||||
IDataTypeService dataTypeService,
|
||||
IFileStreamSecurityValidator fileStreamSecurityValidator)
|
||||
: base(logger)
|
||||
{
|
||||
_mediaFileSystem = mediaFileSystem ?? throw new ArgumentNullException(nameof(mediaFileSystem));
|
||||
_contentSettings = contentSettings ?? throw new ArgumentNullException(nameof(contentSettings));
|
||||
_dataTypeService = dataTypeService;
|
||||
_fileStreamSecurityValidator = fileStreamSecurityValidator;
|
||||
|
||||
// TODO: inject?
|
||||
_autoFillProperties = new UploadAutoFillProperties(_mediaFileSystem, logger, _contentSettings);
|
||||
}
|
||||
|
||||
[Obsolete("Use constructor that accepts IFileStreamSecurityValidator.")]
|
||||
public ImageCropperPropertyEditor(ILogger logger, IMediaFileSystem mediaFileSystem, IContentSection contentSettings, IDataTypeService dataTypeService)
|
||||
: this(logger, mediaFileSystem, contentSettings, dataTypeService, Current.Factory.GetInstance<IFileStreamSecurityValidator>())
|
||||
{
|
||||
}
|
||||
|
||||
public string GetMediaPath(object value) => GetFileSrcFromPropertyValue(value, out _, false);
|
||||
|
||||
/// <summary>
|
||||
/// Creates the corresponding property value editor.
|
||||
/// </summary>
|
||||
/// <returns>The corresponding property value editor.</returns>
|
||||
protected override IDataValueEditor CreateValueEditor() => new ImageCropperPropertyValueEditor(Attribute, Logger, _mediaFileSystem);
|
||||
protected override IDataValueEditor CreateValueEditor() => new ImageCropperPropertyValueEditor(Attribute, Logger, _mediaFileSystem, _fileStreamSecurityValidator);
|
||||
|
||||
/// <summary>
|
||||
/// Creates the corresponding preValue editor.
|
||||
@@ -131,7 +146,7 @@ namespace Umbraco.Web.PropertyEditors
|
||||
/// Returns the "src" property from the json structure if the value is formatted correctly
|
||||
/// </summary>
|
||||
/// <param name="propVal"></param>
|
||||
/// <param name="deserializedValue">The deserialized <see cref="JObject"/> value</param>
|
||||
/// <param name="deserializedValue">The deserialized <see cref="JObject"/> value</param>
|
||||
/// <param name="relative">Should the path returned be the application relative path</param>
|
||||
/// <returns></returns>
|
||||
private string GetFileSrcFromPropertyValue(object propVal, out JObject deserializedValue, bool relative = true)
|
||||
|
||||
@@ -8,6 +8,7 @@ using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.Editors;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Core.PropertyEditors.ValueConverters;
|
||||
using Umbraco.Core.Security;
|
||||
using Umbraco.Core.Services;
|
||||
using File = System.IO.File;
|
||||
|
||||
@@ -20,12 +21,18 @@ namespace Umbraco.Web.PropertyEditors
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IMediaFileSystem _mediaFileSystem;
|
||||
private readonly IFileStreamSecurityValidator _fileStreamSecurityValidator;
|
||||
|
||||
public ImageCropperPropertyValueEditor(DataEditorAttribute attribute, ILogger logger, IMediaFileSystem mediaFileSystem)
|
||||
public ImageCropperPropertyValueEditor(
|
||||
DataEditorAttribute attribute,
|
||||
ILogger logger,
|
||||
IMediaFileSystem mediaFileSystem,
|
||||
IFileStreamSecurityValidator fileStreamSecurityValidator)
|
||||
: base(attribute)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_mediaFileSystem = mediaFileSystem ?? throw new ArgumentNullException(nameof(mediaFileSystem));
|
||||
_fileStreamSecurityValidator = fileStreamSecurityValidator;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -166,6 +173,11 @@ namespace Umbraco.Web.PropertyEditors
|
||||
// TODO: Here it would make sense to do the auto-fill properties stuff but the API doesn't allow us to do that right
|
||||
// since we'd need to be able to return values for other properties from these methods
|
||||
|
||||
if (_fileStreamSecurityValidator.IsConsideredSafe(filestream) is false)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
_mediaFileSystem.AddFile(filepath, filestream, true); // must overwrite!
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Models.Editors;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Core.Security;
|
||||
|
||||
namespace Umbraco.Web.PropertyEditors
|
||||
{
|
||||
internal class MarkDownPropertyValueEditor : DataValueEditor
|
||||
{
|
||||
private readonly IMarkdownSanitizer _markdownSanitizer;
|
||||
|
||||
public MarkDownPropertyValueEditor(DataEditorAttribute attribute, IMarkdownSanitizer markdownSanitizer) : base(attribute)
|
||||
{
|
||||
_markdownSanitizer = markdownSanitizer;
|
||||
}
|
||||
|
||||
public override object FromEditor(ContentPropertyData editorValue, object currentValue)
|
||||
{
|
||||
var editorValueString = editorValue.Value?.ToString();
|
||||
if (string.IsNullOrWhiteSpace(editorValueString))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var sanitized = _markdownSanitizer.Sanitize(editorValueString);
|
||||
|
||||
return sanitized.NullOrWhiteSpaceAsNull();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
using Umbraco.Core;
|
||||
using System;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Core.Security;
|
||||
|
||||
namespace Umbraco.Web.PropertyEditors
|
||||
{
|
||||
@@ -16,14 +19,31 @@ namespace Umbraco.Web.PropertyEditors
|
||||
Icon = "icon-code")]
|
||||
public class MarkdownPropertyEditor : DataEditor
|
||||
{
|
||||
private readonly IMarkdownSanitizer _markdownSanitizer;
|
||||
|
||||
[Obsolete("Use non obsolete constructor")]
|
||||
public MarkdownPropertyEditor(ILogger logger)
|
||||
: this(logger, Current.Factory.GetInstance<IMarkdownSanitizer>())
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MarkdownPropertyEditor"/> class.
|
||||
/// </summary>
|
||||
public MarkdownPropertyEditor(ILogger logger)
|
||||
public MarkdownPropertyEditor(ILogger logger, IMarkdownSanitizer markdownSanitizer)
|
||||
: base(logger)
|
||||
{ }
|
||||
{
|
||||
_markdownSanitizer = markdownSanitizer;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override IConfigurationEditor CreateConfigurationEditor() => new MarkdownConfigurationEditor();
|
||||
|
||||
/// <summary>
|
||||
/// Create a custom value editor
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected override IDataValueEditor CreateValueEditor() => new MarkDownPropertyValueEditor(Attribute, _markdownSanitizer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,10 @@ namespace Umbraco.Web.PropertyEditors
|
||||
private readonly ILogger _logger;
|
||||
private readonly IMediaService _mediaService;
|
||||
private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider;
|
||||
private readonly string _tempFolderAbsolutePath;
|
||||
|
||||
// An array to contain a list of URLs that we have already processed to avoid dupes
|
||||
private static readonly Dictionary<string, GuidUdi> UploadedImages = new();
|
||||
|
||||
const string TemporaryImageDataAttribute = "data-tmpimg";
|
||||
|
||||
@@ -27,6 +31,69 @@ namespace Umbraco.Web.PropertyEditors
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
_mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService));
|
||||
_contentTypeBaseServiceProvider = contentTypeBaseServiceProvider ?? throw new ArgumentNullException(nameof(contentTypeBaseServiceProvider));
|
||||
_tempFolderAbsolutePath = Path.GetFullPath(IOHelper.MapPath(SystemDirectories.TempImageUploads));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used by the RTE (and grid RTE) for converting inline base64 images to Media items
|
||||
/// </summary>
|
||||
/// <param name="html"></param>
|
||||
/// <param name="mediaParentFolder"></param>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="imageUrlGenerator"></param>
|
||||
/// <returns></returns>
|
||||
internal string FindAndPersistBase64Images(string html, Guid mediaParentFolder, int userId, IImageUrlGenerator imageUrlGenerator)
|
||||
{
|
||||
// Find all img's that has data-tmpimg attribute
|
||||
// Use HTML Agility Pack - https://html-agility-pack.net
|
||||
var htmlDoc = new HtmlDocument();
|
||||
htmlDoc.LoadHtml(html);
|
||||
|
||||
var imagesWithDataUris = htmlDoc.DocumentNode.SelectNodes("//img");
|
||||
if (imagesWithDataUris == null || imagesWithDataUris.Count == 0)
|
||||
return html;
|
||||
|
||||
foreach (var img in imagesWithDataUris)
|
||||
{
|
||||
var srcValue = img.GetAttributeValue("src", string.Empty);
|
||||
|
||||
// Ignore src-less images
|
||||
if (string.IsNullOrEmpty(srcValue))
|
||||
continue;
|
||||
|
||||
// Take only images that have a "data:image" uri into consideration
|
||||
if (!srcValue.StartsWith("data:image"))
|
||||
continue;
|
||||
|
||||
// Create tmp image by scanning the srcValue
|
||||
// the value will look like "data:image/jpg;base64,abc" where the first part
|
||||
// is the mimetype and the second (after the comma) is the image blob
|
||||
var tokens = srcValue.Split(',');
|
||||
var dataUriInfo = tokens[0];
|
||||
var mimeType = dataUriInfo.Split(';')[0].Replace("data:", string.Empty);
|
||||
var base64imageString = tokens[1];
|
||||
|
||||
// Create an unique folder path to help with concurrent users to avoid filename clash
|
||||
var imageTempPath = IOHelper.MapPath(SystemDirectories.TempImageUploads + IOHelper.DirSepChar + Guid.NewGuid().ToString());
|
||||
|
||||
// Ensure image temp path exists
|
||||
if (Directory.Exists(imageTempPath) == false)
|
||||
Directory.CreateDirectory(imageTempPath);
|
||||
|
||||
// To get the filename, we simply manipulate the mimetype into a filename
|
||||
var filePath = mimeType.Replace('/', '.');
|
||||
var tmpImgPath = imageTempPath + IOHelper.DirSepChar + filePath;
|
||||
var absoluteTmpImgPath = IOHelper.MapPath(tmpImgPath);
|
||||
|
||||
// Convert the base64 content to a byte array and save the bytes directly to a file
|
||||
// this method should work for most use-cases
|
||||
System.IO.File.WriteAllBytes(absoluteTmpImgPath, Convert.FromBase64String(base64imageString));
|
||||
|
||||
// When the temp file has been created, we can persist it
|
||||
PersistMediaItem(mediaParentFolder, userId, img, tmpImgPath, imageUrlGenerator);
|
||||
}
|
||||
|
||||
return htmlDoc.DocumentNode.OuterHtml;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -35,6 +102,7 @@ namespace Umbraco.Web.PropertyEditors
|
||||
/// <param name="html"></param>
|
||||
/// <param name="mediaParentFolder"></param>
|
||||
/// <param name="userId"></param>
|
||||
/// <param name="imageUrlGenerator"></param>
|
||||
/// <returns></returns>
|
||||
internal string FindAndPersistPastedTempImages(string html, Guid mediaParentFolder, int userId, IImageUrlGenerator imageUrlGenerator)
|
||||
{
|
||||
@@ -47,10 +115,6 @@ namespace Umbraco.Web.PropertyEditors
|
||||
if (tmpImages == null || tmpImages.Count == 0)
|
||||
return html;
|
||||
|
||||
// An array to contain a list of URLs that
|
||||
// we have already processed to avoid dupes
|
||||
var uploadedImages = new Dictionary<string, GuidUdi>();
|
||||
|
||||
foreach (var img in tmpImages)
|
||||
{
|
||||
// The data attribute contains the path to the tmp img to persist as a media item
|
||||
@@ -59,85 +123,101 @@ namespace Umbraco.Web.PropertyEditors
|
||||
if (string.IsNullOrEmpty(tmpImgPath))
|
||||
continue;
|
||||
|
||||
var absoluteTempImagePath = IOHelper.MapPath(tmpImgPath);
|
||||
var fileName = Path.GetFileName(absoluteTempImagePath);
|
||||
var safeFileName = fileName.ToSafeFileName();
|
||||
|
||||
var mediaItemName = safeFileName.ToFriendlyName();
|
||||
IMedia mediaFile;
|
||||
GuidUdi udi;
|
||||
|
||||
if (uploadedImages.ContainsKey(tmpImgPath) == false)
|
||||
{
|
||||
if (mediaParentFolder == Guid.Empty)
|
||||
mediaFile = _mediaService.CreateMedia(mediaItemName, Constants.System.Root, Constants.Conventions.MediaTypes.Image, userId);
|
||||
else
|
||||
mediaFile = _mediaService.CreateMedia(mediaItemName, mediaParentFolder, Constants.Conventions.MediaTypes.Image, userId);
|
||||
|
||||
var fileInfo = new FileInfo(absoluteTempImagePath);
|
||||
|
||||
var fileStream = fileInfo.OpenReadWithRetry();
|
||||
if (fileStream == null) throw new InvalidOperationException("Could not acquire file stream");
|
||||
using (fileStream)
|
||||
{
|
||||
mediaFile.SetValue(_contentTypeBaseServiceProvider, Constants.Conventions.Media.File, safeFileName, fileStream);
|
||||
}
|
||||
|
||||
_mediaService.Save(mediaFile, userId);
|
||||
|
||||
udi = mediaFile.GetUdi();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Already been uploaded & we have it's UDI
|
||||
udi = uploadedImages[tmpImgPath];
|
||||
}
|
||||
|
||||
// Add the UDI to the img element as new data attribute
|
||||
img.SetAttributeValue("data-udi", udi.ToString());
|
||||
|
||||
// Get the new persisted image URL
|
||||
var mediaTyped = _umbracoContextAccessor?.UmbracoContext?.Media.GetById(udi.Guid);
|
||||
if (mediaTyped == null)
|
||||
throw new PanicException($"Could not find media by id {udi.Guid} or there was no UmbracoContext available.");
|
||||
|
||||
var location = mediaTyped.Url();
|
||||
|
||||
// Find the width & height attributes as we need to set the imageprocessor QueryString
|
||||
var width = img.GetAttributeValue("width", int.MinValue);
|
||||
var height = img.GetAttributeValue("height", int.MinValue);
|
||||
|
||||
if (width != int.MinValue && height != int.MinValue)
|
||||
{
|
||||
location = imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(location) { ImageCropMode = "max", Width = width, Height = height });
|
||||
}
|
||||
|
||||
img.SetAttributeValue("src", location);
|
||||
|
||||
// Remove the data attribute (so we do not re-process this)
|
||||
img.Attributes.Remove(TemporaryImageDataAttribute);
|
||||
|
||||
// Add to the dictionary to avoid dupes
|
||||
if (uploadedImages.ContainsKey(tmpImgPath) == false)
|
||||
{
|
||||
uploadedImages.Add(tmpImgPath, udi);
|
||||
|
||||
// Delete folder & image now its saved in media
|
||||
// The folder should contain one image - as a unique guid folder created
|
||||
// for each image uploaded from TinyMceController
|
||||
var folderName = Path.GetDirectoryName(absoluteTempImagePath);
|
||||
try
|
||||
{
|
||||
Directory.Delete(folderName, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error<string>(typeof(HtmlImageSourceParser), ex, "Could not delete temp file or folder {FileName}", absoluteTempImagePath);
|
||||
}
|
||||
}
|
||||
PersistMediaItem(mediaParentFolder, userId, img, tmpImgPath, imageUrlGenerator);
|
||||
}
|
||||
|
||||
return htmlDoc.DocumentNode.OuterHtml;
|
||||
}
|
||||
|
||||
private void PersistMediaItem(Guid mediaParentFolder, int userId, HtmlNode img, string tmpImgPath, IImageUrlGenerator imageUrlGenerator)
|
||||
{
|
||||
var absoluteTempImagePath = Path.GetFullPath(IOHelper.MapPath(tmpImgPath));
|
||||
|
||||
if (IsValidPath(absoluteTempImagePath) == false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var fileName = Path.GetFileName(absoluteTempImagePath);
|
||||
var safeFileName = fileName.ToSafeFileName();
|
||||
|
||||
var mediaItemName = safeFileName.ToFriendlyName();
|
||||
IMedia mediaFile;
|
||||
GuidUdi udi;
|
||||
|
||||
if (UploadedImages.ContainsKey(tmpImgPath) == false)
|
||||
{
|
||||
if (mediaParentFolder == Guid.Empty)
|
||||
mediaFile = _mediaService.CreateMedia(mediaItemName, Constants.System.Root, Constants.Conventions.MediaTypes.Image, userId);
|
||||
else
|
||||
mediaFile = _mediaService.CreateMedia(mediaItemName, mediaParentFolder, Constants.Conventions.MediaTypes.Image, userId);
|
||||
|
||||
var fileInfo = new FileInfo(absoluteTempImagePath);
|
||||
|
||||
var fileStream = fileInfo.OpenReadWithRetry();
|
||||
if (fileStream == null) throw new InvalidOperationException("Could not acquire file stream");
|
||||
using (fileStream)
|
||||
{
|
||||
mediaFile.SetValue(_contentTypeBaseServiceProvider, Constants.Conventions.Media.File, safeFileName, fileStream);
|
||||
}
|
||||
|
||||
_mediaService.Save(mediaFile, userId);
|
||||
|
||||
udi = mediaFile.GetUdi();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Already been uploaded & we have it's UDI
|
||||
udi = UploadedImages[tmpImgPath];
|
||||
}
|
||||
|
||||
// Add the UDI to the img element as new data attribute
|
||||
img.SetAttributeValue("data-udi", udi.ToString());
|
||||
|
||||
// Get the new persisted image URL
|
||||
var mediaTyped = _umbracoContextAccessor?.UmbracoContext?.Media.GetById(udi.Guid);
|
||||
if (mediaTyped == null)
|
||||
throw new PanicException($"Could not find media by id {udi.Guid} or there was no UmbracoContext available.");
|
||||
|
||||
var location = mediaTyped.Url();
|
||||
|
||||
// Find the width & height attributes as we need to set the imageprocessor QueryString
|
||||
var width = img.GetAttributeValue("width", int.MinValue);
|
||||
var height = img.GetAttributeValue("height", int.MinValue);
|
||||
|
||||
if (width != int.MinValue && height != int.MinValue)
|
||||
{
|
||||
location = imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(location) { ImageCropMode = "max", Width = width, Height = height });
|
||||
}
|
||||
|
||||
img.SetAttributeValue("src", location);
|
||||
|
||||
// Remove the data attribute (so we do not re-process this)
|
||||
img.Attributes.Remove(TemporaryImageDataAttribute);
|
||||
|
||||
// Add to the dictionary to avoid dupes
|
||||
if (UploadedImages.ContainsKey(tmpImgPath) == false)
|
||||
{
|
||||
UploadedImages.Add(tmpImgPath, udi);
|
||||
|
||||
// Delete folder & image now its saved in media
|
||||
// The folder should contain one image - as a unique guid folder created
|
||||
// for each image uploaded from TinyMceController
|
||||
var folderName = Path.GetDirectoryName(absoluteTempImagePath);
|
||||
try
|
||||
{
|
||||
Directory.Delete(folderName, true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error<string>(typeof(HtmlImageSourceParser), ex, "Could not delete temp file or folder {FileName}", absoluteTempImagePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsValidPath(string imagePath)
|
||||
{
|
||||
return imagePath.StartsWith(_tempFolderAbsolutePath, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +174,8 @@ namespace Umbraco.Web.PropertyEditors
|
||||
var mediaParent = config?.MediaParentId;
|
||||
var mediaParentId = mediaParent == null ? Guid.Empty : mediaParent.Guid;
|
||||
|
||||
var parseAndSavedTempImages = _pastedImages.FindAndPersistPastedTempImages(editorValue.Value.ToString(), mediaParentId, userId, _imageUrlGenerator);
|
||||
var parseAndSaveDataUriImages = _pastedImages.FindAndPersistBase64Images(editorValue.Value.ToString(), mediaParentId, userId, _imageUrlGenerator);
|
||||
var parseAndSavedTempImages = _pastedImages.FindAndPersistPastedTempImages(parseAndSaveDataUriImages, mediaParentId, userId, _imageUrlGenerator);
|
||||
var editorValueWithMediaUrlsRemoved = _imageSourceParser.RemoveImageSources(parseAndSavedTempImages);
|
||||
var parsed = MacroTagParser.FormatRichTextContentForPersistence(editorValueWithMediaUrlsRemoved);
|
||||
var sanitized = _htmlSanitizer.Sanitize(parsed);
|
||||
|
||||
@@ -49,11 +49,14 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
|
||||
public Type GetModelType(Guid contentTypeKey)
|
||||
{
|
||||
var publishedContentType = GetContentType(contentTypeKey);
|
||||
if (publishedContentType != null)
|
||||
if (publishedContentType != null && publishedContentType.IsElement)
|
||||
{
|
||||
var modelType = ModelType.For(publishedContentType.Alias);
|
||||
|
||||
return _publishedModelFactory.MapModelType(modelType);
|
||||
// TODO Get the model type without having to construct a list
|
||||
var listType = _publishedModelFactory.CreateModelList(publishedContentType.Alias).GetType();
|
||||
if (listType.GenericTypeArguments.Length == 1)
|
||||
{
|
||||
return listType.GenericTypeArguments[0];
|
||||
}
|
||||
}
|
||||
|
||||
return typeof(IPublishedElement);
|
||||
|
||||
+86
-27
@@ -10,7 +10,6 @@ using Umbraco.Core.PropertyEditors.ValueConverters;
|
||||
|
||||
namespace Umbraco.Web.PropertyEditors.ValueConverters
|
||||
{
|
||||
|
||||
[DefaultPropertyValueConverter(typeof(JsonValueConverter))]
|
||||
public class BlockListPropertyValueConverter : PropertyValueConverterBase
|
||||
{
|
||||
@@ -30,7 +29,8 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
|
||||
=> propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.BlockList);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof(BlockListModel);
|
||||
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
|
||||
=> typeof(BlockListModel);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
|
||||
@@ -38,24 +38,27 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview)
|
||||
{
|
||||
return source?.ToString();
|
||||
}
|
||||
=> source?.ToString();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
|
||||
{
|
||||
// NOTE: The intermediate object is just a json string, we don't actually convert from source -> intermediate since source is always just a json string
|
||||
|
||||
// NOTE: The intermediate object is just a JSON string, we don't actually convert from source -> intermediate since source is always just a JSON string
|
||||
using (_proflog.DebugDuration<BlockListPropertyValueConverter>($"ConvertPropertyToBlockList ({propertyType.DataType.Id})"))
|
||||
{
|
||||
var value = (string)inter;
|
||||
|
||||
// Short-circuit on empty values
|
||||
if (string.IsNullOrWhiteSpace(value)) return BlockListModel.Empty;
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return BlockListModel.Empty;
|
||||
}
|
||||
|
||||
var converted = _blockListEditorDataConverter.Deserialize(value);
|
||||
if (converted.BlockValue.ContentData.Count == 0) return BlockListModel.Empty;
|
||||
if (converted.BlockValue.ContentData.Count == 0)
|
||||
{
|
||||
return BlockListModel.Empty;
|
||||
}
|
||||
|
||||
var blockListLayout = converted.Layout.ToObject<IEnumerable<BlockListLayoutItem>>();
|
||||
|
||||
@@ -68,77 +71,133 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
|
||||
var contentPublishedElements = new Dictionary<Guid, IPublishedElement>();
|
||||
foreach (var data in converted.BlockValue.ContentData)
|
||||
{
|
||||
if (!blockConfigMap.ContainsKey(data.ContentTypeKey)) continue;
|
||||
if (!blockConfigMap.ContainsKey(data.ContentTypeKey))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var element = _blockConverter.ConvertToElement(data, referenceCacheLevel, preview);
|
||||
if (element == null) continue;
|
||||
if (element == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
contentPublishedElements[element.Key] = element;
|
||||
}
|
||||
|
||||
// If there are no content elements, it doesn't matter what is stored in layout
|
||||
if (contentPublishedElements.Count == 0) return BlockListModel.Empty;
|
||||
if (contentPublishedElements.Count == 0)
|
||||
{
|
||||
return BlockListModel.Empty;
|
||||
}
|
||||
|
||||
// Convert the settings data
|
||||
var settingsPublishedElements = new Dictionary<Guid, IPublishedElement>();
|
||||
foreach (var data in converted.BlockValue.SettingsData)
|
||||
{
|
||||
if (!validSettingsElementTypes.Contains(data.ContentTypeKey)) continue;
|
||||
if (!validSettingsElementTypes.Contains(data.ContentTypeKey))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var element = _blockConverter.ConvertToElement(data, referenceCacheLevel, preview);
|
||||
if (element == null) continue;
|
||||
if (element == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
settingsPublishedElements[element.Key] = element;
|
||||
}
|
||||
|
||||
var layout = new List<BlockListItem>();
|
||||
// Cache constructors locally (it's tied to the current IPublishedSnapshot and IPublishedModelFactory)
|
||||
var blockListItemActivator = new BlockListItemActivator(_blockConverter);
|
||||
|
||||
var list = new List<BlockListItem>();
|
||||
foreach (var layoutItem in blockListLayout)
|
||||
{
|
||||
// Get the content reference
|
||||
var contentGuidUdi = (GuidUdi)layoutItem.ContentUdi;
|
||||
if (!contentPublishedElements.TryGetValue(contentGuidUdi.Guid, out var contentData))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!contentData.ContentType.TryGetKey(out var contentTypeKey))
|
||||
{
|
||||
throw new InvalidOperationException("The content type was not of type " + typeof(IPublishedContentType2));
|
||||
}
|
||||
|
||||
if (!blockConfigMap.TryGetValue(contentTypeKey, out var blockConfig))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get the setting reference
|
||||
IPublishedElement settingsData = null;
|
||||
var settingGuidUdi = layoutItem.SettingsUdi != null ? (GuidUdi)layoutItem.SettingsUdi : null;
|
||||
var settingGuidUdi = (GuidUdi)layoutItem.SettingsUdi;
|
||||
if (settingGuidUdi != null)
|
||||
{
|
||||
settingsPublishedElements.TryGetValue(settingGuidUdi.Guid, out settingsData);
|
||||
}
|
||||
|
||||
// This can happen if they have a settings type, save content, remove the settings type, and display the front-end page before saving the content again
|
||||
// We also ensure that the content types match, since maybe the settings type has been changed after this has been persisted
|
||||
if (settingsData != null)
|
||||
{
|
||||
if (!settingsData.ContentType.TryGetKey(out var settingsElementTypeKey))
|
||||
{
|
||||
throw new InvalidOperationException("The settings element type was not of type " + typeof(IPublishedContentType2));
|
||||
}
|
||||
|
||||
if (!blockConfig.SettingsElementTypeKey.HasValue || settingsElementTypeKey != blockConfig.SettingsElementTypeKey)
|
||||
{
|
||||
settingsData = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Get settings type from configuration
|
||||
var settingsType = blockConfig.SettingsElementTypeKey.HasValue
|
||||
? _blockConverter.GetModelType(blockConfig.SettingsElementTypeKey.Value)
|
||||
: typeof(IPublishedElement);
|
||||
// Create instance (use content/settings type from configuration)
|
||||
var layoutRef = blockListItemActivator.CreateInstance(blockConfig.ContentElementTypeKey, blockConfig.SettingsElementTypeKey, contentGuidUdi, contentData, settingGuidUdi, settingsData);
|
||||
|
||||
// TODO: This should be optimized/cached, as calling Activator.CreateInstance is slow
|
||||
var layoutType = typeof(BlockListItem<,>).MakeGenericType(contentData.GetType(), settingsType);
|
||||
var layoutRef = (BlockListItem)Activator.CreateInstance(layoutType, contentGuidUdi, contentData, settingGuidUdi, settingsData);
|
||||
|
||||
layout.Add(layoutRef);
|
||||
list.Add(layoutRef);
|
||||
}
|
||||
|
||||
var model = new BlockListModel(layout);
|
||||
return model;
|
||||
return new BlockListModel(list);
|
||||
}
|
||||
}
|
||||
|
||||
private class BlockListItemActivator
|
||||
{
|
||||
private readonly BlockEditorConverter _blockConverter;
|
||||
private readonly Dictionary<(Guid, Guid?), Func<Udi, IPublishedElement, Udi, IPublishedElement, BlockListItem>> _contructorCache = new();
|
||||
|
||||
public BlockListItemActivator(BlockEditorConverter blockConverter)
|
||||
=> _blockConverter = blockConverter;
|
||||
|
||||
public BlockListItem CreateInstance(Guid contentTypeKey, Guid? settingsTypeKey, Udi contentUdi, IPublishedElement contentData, Udi settingsUdi, IPublishedElement settingsData)
|
||||
{
|
||||
if (!_contructorCache.TryGetValue((contentTypeKey, settingsTypeKey), out var constructor))
|
||||
{
|
||||
constructor = _contructorCache[(contentTypeKey, settingsTypeKey)] = EmitConstructor(contentTypeKey, settingsTypeKey);
|
||||
}
|
||||
|
||||
return constructor(contentUdi, contentData, settingsUdi, settingsData);
|
||||
}
|
||||
|
||||
private Func<Udi, IPublishedElement, Udi, IPublishedElement, BlockListItem> EmitConstructor(Guid contentTypeKey, Guid? settingsTypeKey)
|
||||
{
|
||||
var contentType = _blockConverter.GetModelType(contentTypeKey);
|
||||
var settingsType = settingsTypeKey.HasValue ? _blockConverter.GetModelType(settingsTypeKey.Value) : typeof(IPublishedElement);
|
||||
var type = typeof(BlockListItem<,>).MakeGenericType(contentType, settingsType);
|
||||
|
||||
var constructor = type.GetConstructor(new[] { typeof(Udi), contentType, typeof(Udi), settingsType });
|
||||
if (constructor == null)
|
||||
{
|
||||
throw new InvalidOperationException($"Could not find the required public constructor on {type}.");
|
||||
}
|
||||
|
||||
// We use unsafe here, because we know the contructor parameter count and types match
|
||||
return ReflectionUtilities.EmitConstructorUnsafe<Func<Udi, IPublishedElement, Udi, IPublishedElement, BlockListItem>>(constructor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user