Compare commits
73
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca1453ab4e | ||
|
|
8962c1e636 | ||
|
|
196b29c5eb | ||
|
|
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.7")]
|
||||
[assembly: AssemblyInformationalVersion("8.18.7")]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+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)
|
||||
|
||||
@@ -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" />
|
||||
@@ -400,6 +401,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" />
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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" />
|
||||
|
||||
+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 = {
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
|
||||
@@ -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>8187</DevelopmentServerPort>
|
||||
<DevelopmentServerVPath>/</DevelopmentServerVPath>
|
||||
<IISUrl>http://localhost:8180</IISUrl>
|
||||
<IISUrl>http://localhost:8187</IISUrl>
|
||||
<NTLMAuthentication>False</NTLMAuthentication>
|
||||
<UseCustomServer>False</UseCustomServer>
|
||||
<CustomServerUrl>
|
||||
|
||||
@@ -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>
|
||||
@@ -1415,6 +1416,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 +1451,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 +2511,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>
|
||||
@@ -1425,6 +1426,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 +1463,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 +2550,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">
|
||||
|
||||
@@ -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);
|
||||
@@ -2019,6 +2019,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 +2035,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 +2080,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 +2437,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"),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -6,10 +6,8 @@ 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.WebApi;
|
||||
using Constants = Umbraco.Core.Constants;
|
||||
|
||||
namespace Umbraco.Web.Editors
|
||||
{
|
||||
@@ -62,18 +60,23 @@ namespace Umbraco.Web.Editors
|
||||
{
|
||||
var ext = Path.GetExtension(imagePath);
|
||||
|
||||
// check if imagePath is local to prevent open redirect
|
||||
if (!IsAllowed(imagePath))
|
||||
{
|
||||
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)
|
||||
{
|
||||
@@ -86,9 +89,32 @@ 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 response = Request.CreateResponse(HttpStatusCode.Found);
|
||||
response.Headers.Location = new Uri(imageUrl, UriKind.RelativeOrAbsolute);
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
private bool IsAllowed(string imagePath)
|
||||
{
|
||||
if(Uri.IsWellFormedUriString(imagePath, UriKind.Relative))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_contentSection is ContentElement contentElement)
|
||||
{
|
||||
var builder = new UriBuilder(imagePath);
|
||||
|
||||
foreach (var allowedMediaHost in contentElement.AllowedMediaHosts)
|
||||
{
|
||||
if (string.Equals(builder.Host, allowedMediaHost, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -598,6 +598,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 +647,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 +666,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 +704,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 +749,94 @@ 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 mediaItemName = fileName.ToFriendlyName();
|
||||
|
||||
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);
|
||||
}
|
||||
var createdMediaItem = mediaService.CreateMedia(mediaItemName, parentId, mediaTypeAlias, Security.CurrentUser.Id);
|
||||
|
||||
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 fileInfo = new FileInfo(file.LocalFileName);
|
||||
var fs = fileInfo.OpenReadWithRetry();
|
||||
if (fs == null) throw new InvalidOperationException("Could not acquire file stream");
|
||||
using (fs)
|
||||
{
|
||||
createdMediaItem.SetValue(Services.ContentTypeBaseServices, Constants.Conventions.Media.File, fileName, fs);
|
||||
}
|
||||
|
||||
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 +856,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;
|
||||
|
||||
@@ -120,8 +120,9 @@ 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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace Umbraco.Web.PropertyEditors
|
||||
private readonly ILogger _logger;
|
||||
private readonly IMediaService _mediaService;
|
||||
private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider;
|
||||
private readonly string _tempFolderAbsolutePath;
|
||||
|
||||
const string TemporaryImageDataAttribute = "data-tmpimg";
|
||||
|
||||
@@ -27,6 +28,7 @@ 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>
|
||||
@@ -59,7 +61,13 @@ namespace Umbraco.Web.PropertyEditors
|
||||
if (string.IsNullOrEmpty(tmpImgPath))
|
||||
continue;
|
||||
|
||||
var absoluteTempImagePath = IOHelper.MapPath(tmpImgPath);
|
||||
var absoluteTempImagePath = Path.GetFullPath(IOHelper.MapPath(tmpImgPath));
|
||||
|
||||
if (IsValidPath(absoluteTempImagePath) == false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var fileName = Path.GetFileName(absoluteTempImagePath);
|
||||
var safeFileName = fileName.ToSafeFileName();
|
||||
|
||||
@@ -139,5 +147,10 @@ namespace Umbraco.Web.PropertyEditors
|
||||
|
||||
return htmlDoc.DocumentNode.OuterHtml;
|
||||
}
|
||||
|
||||
private bool IsValidPath(string imagePath)
|
||||
{
|
||||
return imagePath.StartsWith(_tempFolderAbsolutePath, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
|
||||
{
|
||||
var maxNumber = propertyType.DataType.ConfigurationAs<MultiUrlPickerConfiguration>().MaxNumber;
|
||||
|
||||
if (inter == null)
|
||||
if (string.IsNullOrWhiteSpace(inter?.ToString()))
|
||||
{
|
||||
return maxNumber == 1 ? null : Enumerable.Empty<Link>();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
@@ -25,9 +25,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
|
||||
/// </summary>
|
||||
public NestedContentManyValueConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor, IPublishedModelFactory publishedModelFactory, IProfilingLogger proflog)
|
||||
: base(publishedSnapshotAccessor, publishedModelFactory)
|
||||
{
|
||||
_proflog = proflog;
|
||||
}
|
||||
=> _proflog = proflog;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsConverter(IPublishedPropertyType propertyType)
|
||||
@@ -37,6 +35,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
|
||||
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
|
||||
{
|
||||
var contentTypes = propertyType.DataType.ConfigurationAs<NestedContentConfiguration>().ContentTypes;
|
||||
|
||||
return contentTypes.Length == 1
|
||||
? typeof(IEnumerable<>).MakeGenericType(ModelType.For(contentTypes[0].Alias))
|
||||
: typeof(IEnumerable<IPublishedElement>);
|
||||
@@ -48,9 +47,7 @@ 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)
|
||||
@@ -64,16 +61,24 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
|
||||
: new List<IPublishedElement>();
|
||||
|
||||
var value = (string)inter;
|
||||
if (string.IsNullOrWhiteSpace(value)) return elements;
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return elements;
|
||||
}
|
||||
|
||||
var objects = JsonConvert.DeserializeObject<List<JObject>>(value);
|
||||
if (objects.Count == 0) return elements;
|
||||
if (objects.Count == 0)
|
||||
{
|
||||
return elements;
|
||||
}
|
||||
|
||||
foreach (var sourceObject in objects)
|
||||
{
|
||||
var element = ConvertToElement(sourceObject, referenceCacheLevel, preview);
|
||||
if (element != null)
|
||||
{
|
||||
elements.Add(element);
|
||||
}
|
||||
}
|
||||
|
||||
return elements;
|
||||
|
||||
+14
-13
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
@@ -24,9 +24,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
|
||||
/// </summary>
|
||||
public NestedContentSingleValueConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor, IPublishedModelFactory publishedModelFactory, IProfilingLogger proflog)
|
||||
: base(publishedSnapshotAccessor, publishedModelFactory)
|
||||
{
|
||||
_proflog = proflog;
|
||||
}
|
||||
=> _proflog = proflog;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsConverter(IPublishedPropertyType propertyType)
|
||||
@@ -36,9 +34,10 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
|
||||
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
|
||||
{
|
||||
var contentTypes = propertyType.DataType.ConfigurationAs<NestedContentConfiguration>().ContentTypes;
|
||||
return contentTypes.Length > 1
|
||||
? typeof(IPublishedElement)
|
||||
: ModelType.For(contentTypes[0].Alias);
|
||||
|
||||
return contentTypes.Length == 1
|
||||
? ModelType.For(contentTypes[0].Alias)
|
||||
: typeof(IPublishedElement);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -47,9 +46,7 @@ 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)
|
||||
@@ -57,14 +54,18 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
|
||||
using (_proflog.DebugDuration<NestedContentSingleValueConverter>($"ConvertPropertyToNestedContent ({propertyType.DataType.Id})"))
|
||||
{
|
||||
var value = (string)inter;
|
||||
if (string.IsNullOrWhiteSpace(value)) return null;
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var objects = JsonConvert.DeserializeObject<List<JObject>>(value);
|
||||
if (objects.Count == 0)
|
||||
{
|
||||
return null;
|
||||
if (objects.Count > 1)
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
// Only return the first (existing data might contain more than is currently configured)
|
||||
return ConvertToElement(objects[0], referenceCacheLevel, preview);
|
||||
}
|
||||
}
|
||||
|
||||
+18
-16
@@ -12,52 +12,54 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
|
||||
{
|
||||
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
|
||||
|
||||
protected IPublishedModelFactory PublishedModelFactory { get; }
|
||||
|
||||
protected NestedContentValueConverterBase(IPublishedSnapshotAccessor publishedSnapshotAccessor, IPublishedModelFactory publishedModelFactory)
|
||||
{
|
||||
_publishedSnapshotAccessor = publishedSnapshotAccessor;
|
||||
PublishedModelFactory = publishedModelFactory;
|
||||
}
|
||||
|
||||
protected IPublishedModelFactory PublishedModelFactory { get; }
|
||||
|
||||
public static bool IsNested(IPublishedPropertyType publishedProperty)
|
||||
{
|
||||
return publishedProperty.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.NestedContent);
|
||||
}
|
||||
=> publishedProperty.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.NestedContent);
|
||||
|
||||
public static bool IsNestedSingle(IPublishedPropertyType publishedProperty)
|
||||
private static bool IsSingle(IPublishedPropertyType publishedProperty)
|
||||
{
|
||||
if (!IsNested(publishedProperty))
|
||||
return false;
|
||||
|
||||
var config = publishedProperty.DataType.ConfigurationAs<NestedContentConfiguration>();
|
||||
|
||||
return config.MinItems == 1 && config.MaxItems == 1;
|
||||
}
|
||||
|
||||
public static bool IsNestedSingle(IPublishedPropertyType publishedProperty)
|
||||
=> IsNested(publishedProperty) && IsSingle(publishedProperty);
|
||||
|
||||
public static bool IsNestedMany(IPublishedPropertyType publishedProperty)
|
||||
{
|
||||
return IsNested(publishedProperty) && !IsNestedSingle(publishedProperty);
|
||||
}
|
||||
=> IsNested(publishedProperty) && !IsSingle(publishedProperty);
|
||||
|
||||
protected IPublishedElement ConvertToElement(JObject sourceObject, PropertyCacheLevel referenceCacheLevel, bool preview)
|
||||
{
|
||||
var elementTypeAlias = sourceObject[NestedContentPropertyEditor.ContentTypeAliasPropertyKey]?.ToObject<string>();
|
||||
if (string.IsNullOrEmpty(elementTypeAlias))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// only convert element types - content types will cause an exception when PublishedModelFactory creates the model
|
||||
// Only convert element types - content types will cause an exception when PublishedModelFactory creates the model
|
||||
var publishedContentType = _publishedSnapshotAccessor.PublishedSnapshot.Content.GetContentType(elementTypeAlias);
|
||||
if (publishedContentType == null || publishedContentType.IsElement == false)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var propertyValues = sourceObject.ToObject<Dictionary<string, object>>();
|
||||
|
||||
if (!propertyValues.TryGetValue("key", out var keyo)
|
||||
|| !Guid.TryParse(keyo.ToString(), out var key))
|
||||
if (!propertyValues.TryGetValue("key", out var keyo) || !Guid.TryParse(keyo.ToString(), out var key))
|
||||
{
|
||||
key = Guid.Empty;
|
||||
}
|
||||
|
||||
IPublishedElement element = new PublishedElement(publishedContentType, key, propertyValues, preview, referenceCacheLevel, _publishedSnapshotAccessor);
|
||||
element = PublishedModelFactory.CreateModel(element);
|
||||
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -433,10 +433,18 @@ namespace Umbraco.Web.PublishedCache.NuCache
|
||||
refreshedIdsA.Contains(x.ContentTypeId) &&
|
||||
BuildKit(x, out _)))
|
||||
{
|
||||
// replacing the node: must preserve the parents
|
||||
// replacing the node: must preserve the relations
|
||||
var node = GetHead(_contentNodes, kit.Node.Id)?.Value;
|
||||
if (node != null)
|
||||
{
|
||||
// Preserve children
|
||||
kit.Node.FirstChildContentId = node.FirstChildContentId;
|
||||
kit.Node.LastChildContentId = node.LastChildContentId;
|
||||
|
||||
// Also preserve siblings
|
||||
kit.Node.NextSiblingContentId = node.NextSiblingContentId;
|
||||
kit.Node.PreviousSiblingContentId = node.PreviousSiblingContentId;
|
||||
}
|
||||
|
||||
SetValueLocked(_contentNodes, kit.Node.Id, kit.Node);
|
||||
|
||||
|
||||
@@ -39,7 +39,9 @@ using Umbraco.Web.WebApi;
|
||||
using Current = Umbraco.Web.Composing.Current;
|
||||
using Umbraco.Web.PropertyEditors;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Telemetry;
|
||||
using Umbraco.Web.Models;
|
||||
using Umbraco.Web.Telemetry;
|
||||
|
||||
namespace Umbraco.Web.Runtime
|
||||
{
|
||||
@@ -58,6 +60,8 @@ namespace Umbraco.Web.Runtime
|
||||
|
||||
composition.ComposeWebMappingProfiles();
|
||||
|
||||
composition.RegisterUnique<ISiteIdentifierService, SiteIdentifierService>();
|
||||
|
||||
//register the install components
|
||||
//NOTE: i tried to not have these registered if we weren't installing or upgrading but post install when the site restarts
|
||||
//it still needs to use the install controller so we can't do that
|
||||
|
||||
@@ -239,7 +239,9 @@ namespace Umbraco.Web.Search
|
||||
var allLangs = _languageService.GetAllLanguages().Select(x => x.IsoCode.ToLowerInvariant()).ToList();
|
||||
|
||||
// the chars [*-_] in the query will mess everything up so let's remove those
|
||||
query = Regex.Replace(query, "[\\*\\-_]", "");
|
||||
// However we cannot just remove - and _ since these signify a space, so we instead replace them with that.
|
||||
query = Regex.Replace(query, "[\\*]", string.Empty);
|
||||
query = Regex.Replace(query, "[\\-_]", " ");
|
||||
|
||||
//check if text is surrounded by single or double quotes, if so, then exact match
|
||||
var surroundedByQuotes = Regex.IsMatch(query, "^\".*?\"$")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using System.Web.Security;
|
||||
@@ -294,21 +295,22 @@ namespace Umbraco.Web.Security
|
||||
passwordValidator.RequiredLength,
|
||||
passwordValidator.RequireNonLetterOrDigit ? 2 : 0);
|
||||
|
||||
var random = new Random();
|
||||
|
||||
var passwordChars = password.ToCharArray();
|
||||
|
||||
if (passwordValidator.RequireDigit && passwordChars.ContainsAny(Enumerable.Range(48, 58).Select(x => (char)x)))
|
||||
password += Convert.ToChar(random.Next(48, 58)); // 0-9
|
||||
using (var numberGenerator = new RNGCryptoServiceProvider())
|
||||
{
|
||||
if (passwordValidator.RequireDigit && passwordChars.ContainsAny(Enumerable.Range(48, 58).Select(x => (char)x)))
|
||||
password += Convert.ToChar(numberGenerator.GetInt32(48, 58)); // 0-9
|
||||
|
||||
if (passwordValidator.RequireLowercase && passwordChars.ContainsAny(Enumerable.Range(97, 123).Select(x => (char)x)))
|
||||
password += Convert.ToChar(random.Next(97, 123)); // a-z
|
||||
if (passwordValidator.RequireLowercase && passwordChars.ContainsAny(Enumerable.Range(97, 123).Select(x => (char)x)))
|
||||
password += Convert.ToChar(numberGenerator.GetInt32(97, 123)); // a-z
|
||||
|
||||
if (passwordValidator.RequireUppercase && passwordChars.ContainsAny(Enumerable.Range(65, 91).Select(x => (char)x)))
|
||||
password += Convert.ToChar(random.Next(65, 91)); // A-Z
|
||||
if (passwordValidator.RequireUppercase && passwordChars.ContainsAny(Enumerable.Range(65, 91).Select(x => (char)x)))
|
||||
password += Convert.ToChar(numberGenerator.GetInt32(65, 91)); // A-Z
|
||||
|
||||
if (passwordValidator.RequireNonLetterOrDigit && passwordChars.ContainsAny(Enumerable.Range(33, 48).Select(x => (char)x)))
|
||||
password += Convert.ToChar(random.Next(33, 48)); // symbols !"#$%&'()*+,-./
|
||||
if (passwordValidator.RequireNonLetterOrDigit && passwordChars.ContainsAny(Enumerable.Range(33, 48).Select(x => (char)x)))
|
||||
password += Convert.ToChar(numberGenerator.GetInt32(33, 48)); // symbols !"#$%&'()*+,-./
|
||||
}
|
||||
|
||||
return password;
|
||||
}
|
||||
|
||||
@@ -587,6 +587,13 @@ namespace Umbraco.Web.Security.Providers
|
||||
requiresFullSave = true;
|
||||
}
|
||||
|
||||
// If the last login date is default prior to setting it, it means that this value has never been set
|
||||
// and therefore there's no property data created for it yet, which means that we can't just update that property
|
||||
// and need to do a full save.
|
||||
if (member.LastLoginDate == default)
|
||||
{
|
||||
requiresFullSave = true;
|
||||
}
|
||||
member.LastLoginDate = DateTime.Now;
|
||||
|
||||
Current.Logger.Info<UmbracoMembershipProviderBase, string, string>("Login attempt succeeded for username {Username} from IP address {IpAddress}", username, GetCurrentRequestIpAddress());
|
||||
|
||||
@@ -48,13 +48,16 @@ namespace Umbraco.Web.Telemetry
|
||||
|
||||
try
|
||||
{
|
||||
// Send data to LIVE telemetry
|
||||
_httpClient.BaseAddress = new Uri("https://telemetry.umbraco.com/");
|
||||
if (_httpClient.BaseAddress == null)
|
||||
{
|
||||
// Send data to LIVE telemetry
|
||||
_httpClient.BaseAddress = new Uri("https://telemetry.umbraco.com/");
|
||||
|
||||
#if DEBUG
|
||||
// Send data to DEBUG telemetry service
|
||||
_httpClient.BaseAddress = new Uri("https://telemetry.rainbowsrock.net/");
|
||||
// Send data to DEBUG telemetry service
|
||||
_httpClient.BaseAddress = new Uri("https://telemetry.rainbowsrock.net/");
|
||||
#endif
|
||||
}
|
||||
|
||||
_httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json");
|
||||
|
||||
@@ -62,9 +65,6 @@ namespace Umbraco.Web.Telemetry
|
||||
{
|
||||
request.Content = new StringContent(JsonConvert.SerializeObject(telemetryReportData), Encoding.UTF8, "application/json"); //CONTENT-TYPE header
|
||||
|
||||
// Set a low timeout - no need to use a larger default timeout for this POST request
|
||||
_httpClient.Timeout = new TimeSpan(0, 0, 1);
|
||||
|
||||
// Make a HTTP Post to telemetry service
|
||||
// https://telemetry.umbraco.com/installs/
|
||||
// Fire & Forget, do not need to know if its a 200, 500 etc
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Xml.Linq;
|
||||
using Umbraco.Core.Configuration.UmbracoSettings;
|
||||
using Umbraco.Core.IO;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Telemetry;
|
||||
using Umbraco.Web.Install;
|
||||
|
||||
namespace Umbraco.Web.Telemetry
|
||||
{
|
||||
internal class SiteIdentifierService : ISiteIdentifierService
|
||||
{
|
||||
private readonly IUmbracoSettingsSection _settings;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public SiteIdentifierService(IUmbracoSettingsSection settings, ILogger logger)
|
||||
{
|
||||
_settings = settings;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public bool TryGetSiteIdentifier(out Guid siteIdentifier)
|
||||
{
|
||||
// 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
|
||||
|| parsedTelemetryId == Guid.Empty)
|
||||
{
|
||||
siteIdentifier = Guid.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
siteIdentifier = parsedTelemetryId;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryGetOrCreateSiteIdentifier(out Guid siteIdentifier)
|
||||
{
|
||||
if (TryGetSiteIdentifier(out var existingId))
|
||||
{
|
||||
siteIdentifier = existingId;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryCreateSiteIdentifier(out var createdId))
|
||||
{
|
||||
siteIdentifier = createdId;
|
||||
return true;
|
||||
}
|
||||
|
||||
siteIdentifier = Guid.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryCreateSiteIdentifier(out Guid createdGuid)
|
||||
{
|
||||
createdGuid = 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) is false)
|
||||
{
|
||||
_logger.Error<SiteIdentifierService>("Unable to find umbracoSettings.config file to add telemetry site identifier");
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var umbracoConfigXml = XDocument.Load(umbracoSettingsPath, LoadOptions.PreserveWhitespace);
|
||||
if (umbracoConfigXml.Root != null)
|
||||
{
|
||||
var backofficeElement = umbracoConfigXml.Root.Element("backOffice");
|
||||
if (backofficeElement is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Will add ID attribute if it does not exist
|
||||
backofficeElement.SetAttributeValue("id", createdGuid.ToString());
|
||||
|
||||
// Save file back down
|
||||
umbracoConfigXml.Save(umbracoSettingsPath, SaveOptions.DisableFormatting);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error<SiteIdentifierService>(ex, "Couldn't update umbracoSettings.config with a backoffice with a telemetry site identifier");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ namespace Umbraco.Web.Telemetry
|
||||
private readonly ITelemetryService _telemetryService;
|
||||
private BackgroundTaskRunner<IBackgroundTask> _telemetryReporterRunner;
|
||||
|
||||
public TelemetryComponent(IProfilingLogger logger, IUmbracoSettingsSection settings, ITelemetryService telemetryService)
|
||||
public TelemetryComponent(IProfilingLogger logger, ITelemetryService telemetryService)
|
||||
{
|
||||
_logger = logger;
|
||||
_telemetryService = telemetryService;
|
||||
|
||||
@@ -347,6 +347,7 @@
|
||||
<Compile Include="Models\LinkType.cs" />
|
||||
<Compile Include="Models\TemplateQuery\OperatorFactory.cs" />
|
||||
<Compile Include="Telemetry\ReportSiteTask.cs" />
|
||||
<Compile Include="Telemetry\SiteIdentifierService.cs" />
|
||||
<Compile Include="Telemetry\TelemetryComponent.cs" />
|
||||
<Compile Include="Telemetry\TelemetryComposer.cs" />
|
||||
<Compile Include="Templates\HtmlLocalLinkParser.cs" />
|
||||
|
||||
+2
-2
@@ -38,7 +38,7 @@ EndProject
|
||||
Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Web.UI.Client", "http://localhost:3961", "{3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}"
|
||||
ProjectSection(WebsiteProperties) = preProject
|
||||
UseIISExpress = "true"
|
||||
TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.5"
|
||||
TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.5.2"
|
||||
Debug.AspNetCompiler.VirtualPath = "/localhost_3961"
|
||||
Debug.AspNetCompiler.PhysicalPath = "Umbraco.Web.UI.Client\"
|
||||
Debug.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_3961\"
|
||||
@@ -60,7 +60,7 @@ Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Web.UI.Client", "ht
|
||||
EndProject
|
||||
Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Tests.AcceptanceTest", "Umbraco.Tests.AcceptanceTest\", "{9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}"
|
||||
ProjectSection(WebsiteProperties) = preProject
|
||||
TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.0"
|
||||
TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.5.2"
|
||||
Debug.AspNetCompiler.VirtualPath = "/localhost_62926"
|
||||
Debug.AspNetCompiler.PhysicalPath = "Umbraco.Tests.AcceptanceTest\"
|
||||
Debug.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_62926\"
|
||||
|
||||
Reference in New Issue
Block a user