Merge pull request #10781 from umbraco/v9/feature/merge_v8_dev_03082021

V9: Merge v8/dev 03082021
This commit is contained in:
Bjarke Berg
2021-08-04 10:40:34 +02:00
committed by GitHub
43 changed files with 638 additions and 450 deletions

View File

@@ -1,10 +1,10 @@

namespace Umbraco.Core.Dashboards
namespace Umbraco.Cms.Core.Configuration
{
public class ContentDashboardSettings
{
private const string DefaultContentDashboardPath = "cms";
/// <summary>
/// Gets a value indicating whether the content dashboard should be available to all users.
/// </summary>

View File

@@ -30,5 +30,7 @@ namespace Umbraco.Cms.Core.Configuration.Models
/// </summary>
[DefaultValue(StaticSqlPageSize)]
public int SqlPageSize { get; set; } = StaticSqlPageSize;
public bool UnPublishedContentCompression { get; set; } = false;
}
}

View File

@@ -0,0 +1,27 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
namespace Umbraco.Cms.Core.Models.ContentEditing
{
/// <summary>
/// A model for retrieving multiple content types based on their keys.
/// </summary>
[DataContract(Name = "contentTypes", Namespace = "")]
public class ContentTypesByKeys
{
/// <summary>
/// ID of the parent of the content type.
/// </summary>
[DataMember(Name = "parentId")]
[Required]
public int ParentId { get; set; }
/// <summary>
/// The id of every content type to get.
/// </summary>
[DataMember(Name = "contentTypeKeys")]
[Required]
public Guid[] ContentTypeKeys { get; set; }
}
}

View File

@@ -9,7 +9,12 @@ namespace Umbraco.Cms.Core.PropertyEditors
///
/// </remarks>
public interface IPropertyCacheCompression
{
bool IsCompressed(IReadOnlyContentBase content, string propertyTypeAlias);
{/// <summary>
/// Whether a property on the content is/should be compressed
/// </summary>
/// <param name="content">The content</param>
/// <param name="propertyTypeAlias">The property to compress or not</param>
/// <param name="published">Whether this content is the published version</param>
bool IsCompressed(IReadOnlyContentBase content, string propertyTypeAlias, bool published);
}
}

View File

@@ -4,6 +4,13 @@ namespace Umbraco.Cms.Core.PropertyEditors
{
public interface IPropertyCacheCompressionOptions
{
bool IsCompressed(IReadOnlyContentBase content, IPropertyType propertyType, IDataEditor dataEditor);
/// <summary>
/// Whether a property on the content is/should be compressed
/// </summary>
/// <param name="content">The content</param>
/// <param name="propertyType">The property to compress or not</param>
/// <param name="dataEditor">The datatype of the property to compress or not</param>
/// <param name="published">Whether this content is the published version</param>
bool IsCompressed(IReadOnlyContentBase content, IPropertyType propertyType, IDataEditor dataEditor, bool published);
}
}

View File

@@ -7,6 +7,6 @@ namespace Umbraco.Cms.Core.PropertyEditors
/// </summary>
public sealed class NoopPropertyCacheCompressionOptions : IPropertyCacheCompressionOptions
{
public bool IsCompressed(IReadOnlyContentBase content, IPropertyType propertyType, IDataEditor dataEditor) => false;
public bool IsCompressed(IReadOnlyContentBase content, IPropertyType propertyType, IDataEditor dataEditor, bool published) => false;
}
}

View File

@@ -5,7 +5,7 @@ using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors;
namespace Umbraco.Core.PropertyEditors
namespace Umbraco.Cms.Core.PropertyEditors
{
/// <summary>
@@ -16,13 +16,13 @@ namespace Umbraco.Core.PropertyEditors
private readonly IPropertyCacheCompressionOptions _compressionOptions;
private readonly IReadOnlyDictionary<int, IContentTypeComposition> _contentTypes;
private readonly PropertyEditorCollection _propertyEditors;
private readonly ConcurrentDictionary<(int contentTypeId, string propertyAlias), bool> _isCompressedCache;
private readonly ConcurrentDictionary<(int contentTypeId, string propertyAlias, bool published), bool> _isCompressedCache;
public PropertyCacheCompression(
IPropertyCacheCompressionOptions compressionOptions,
IReadOnlyDictionary<int, IContentTypeComposition> contentTypes,
PropertyEditorCollection propertyEditors,
ConcurrentDictionary<(int, string), bool> compressedStoragePropertyEditorCache)
ConcurrentDictionary<(int, string, bool), bool> compressedStoragePropertyEditorCache)
{
_compressionOptions = compressionOptions;
_contentTypes = contentTypes ?? throw new System.ArgumentNullException(nameof(contentTypes));
@@ -30,9 +30,9 @@ namespace Umbraco.Core.PropertyEditors
_isCompressedCache = compressedStoragePropertyEditorCache;
}
public bool IsCompressed(IReadOnlyContentBase content, string alias)
public bool IsCompressed(IReadOnlyContentBase content, string alias, bool published)
{
var compressedStorage = _isCompressedCache.GetOrAdd((content.ContentTypeId, alias), x =>
var compressedStorage = _isCompressedCache.GetOrAdd((content.ContentTypeId, alias, published), x =>
{
if (!_contentTypes.TryGetValue(x.contentTypeId, out var ct))
return false;
@@ -44,7 +44,7 @@ namespace Umbraco.Core.PropertyEditors
if (!_propertyEditors.TryGet(propertyType.PropertyEditorAlias, out var propertyEditor))
return false;
return _compressionOptions.IsCompressed(content, propertyType, propertyEditor);
return _compressionOptions.IsCompressed(content, propertyType, propertyEditor, published);
});
return compressedStorage;

View File

@@ -0,0 +1,20 @@
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.PropertyEditors
{
/// <summary>
/// Compress large, non published text properties
/// </summary>
public class UnPublishedContentPropertyCacheCompressionOptions : IPropertyCacheCompressionOptions
{
public bool IsCompressed(IReadOnlyContentBase content, IPropertyType propertyType, IDataEditor dataEditor, bool published)
{
if (!published && propertyType.SupportsPublishing && propertyType.ValueStorageType == ValueStorageType.Ntext)
{
//Only compress non published content that supports publishing and the property is text
return true;
}
return false;
}
}
}

View File

@@ -34,17 +34,6 @@ namespace Umbraco.Extensions
public static string Localize(this ILocalizedTextService manager, string area, string alias, string[] tokens)
=> manager.Localize(area, alias, Thread.CurrentThread.CurrentUICulture, ConvertToDictionaryVars(tokens));
/// <summary>
/// Localize using the current thread culture
/// </summary>
/// <param name="manager"></param>
/// <param name="area"></param>
/// <param name="alias"></param>
/// <param name="tokens"></param>
/// <returns></returns>
public static string Localize(this ILocalizedTextService manager, string area, string alias, IDictionary<string, string> tokens = null)
=> manager.Localize(area, alias, Thread.CurrentThread.CurrentUICulture, tokens);
/// <summary>
/// Localize a key without any variables
/// </summary>

View File

@@ -59,4 +59,8 @@
<ItemGroup>
<EmbeddedResource Include="EmbeddedResources\**\*" />
</ItemGroup>
<ItemGroup>
<Folder Include="ContentEditing" />
</ItemGroup>
</Project>

View File

@@ -9,6 +9,8 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Expressions.Create.Table
public class CreateTableOfDtoBuilder : IExecutableBuilder
{
private readonly IMigrationContext _context;
// TODO: This doesn't do anything.
private readonly DatabaseType[] _supportedDatabaseTypes;
public CreateTableOfDtoBuilder(IMigrationContext context, params DatabaseType[] supportedDatabaseTypes)

View File

@@ -211,7 +211,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade
Merge()
.To<AddCmsContentNuByteColumn>("{8DDDCD0B-D7D5-4C97-BD6A-6B38CA65752F}")
.To<UpgradedIncludeIndexes>("{4695D0C9-0729-4976-985B-048D503665D8}")
.To<UpdateCmsPropertyGroupIdSeed>("{5C424554-A32D-4852-8ED1-A13508187901}")
// to 9.0.0
.With()
.To<MigrateLogViewerQueriesFromFileToDb>("{22D801BA-A1FF-4539-BFCC-2139B55594F8}")
@@ -222,6 +222,13 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade
//FINAL
.As("{5060F3D2-88BE-4D30-8755-CF51F28EAD12}");
// This should be safe to execute again. We need it with a new name to ensure updates from all the following has executed this step.
// - 8.15 RC - Current state: {4695D0C9-0729-4976-985B-048D503665D8}
// - 8.15 Final - Current state: {5C424554-A32D-4852-8ED1-A13508187901}
// - 9.0 RC1 - Current state: {5060F3D2-88BE-4D30-8755-CF51F28EAD12}
To<UpdateCmsPropertyGroupIdSeed>("{622E5172-42E1-4662-AD80-9504AF5A4E53}");
}
}
}

View File

@@ -1,6 +1,9 @@
using NPoco;
using System.Linq;
using Umbraco.Cms.Core;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations;
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0
{
@@ -14,12 +17,49 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0
protected override void Migrate()
{
var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList();
// allow null for the `data` field
if (DatabaseType.IsSqlCe())
{
// SQLCE does not support altering NTEXT, so we have to jump through some hoops to do it
// All column ordering must remain the same as what is defined in the DTO so we need to create a temp table,
// drop orig and then re-create/copy.
Create.Table<ContentNuDtoTemp>(withoutKeysAndIndexes: true).Do();
Execute.Sql($"INSERT INTO [{TempTableName}] SELECT nodeId, published, data, rv FROM [{Constants.DatabaseSchema.Tables.NodeData}]").Do();
Delete.Table(Constants.DatabaseSchema.Tables.NodeData).Do();
Create.Table<ContentNuDto>().Do();
Execute.Sql($"INSERT INTO [{Constants.DatabaseSchema.Tables.NodeData}] SELECT nodeId, published, data, rv, NULL FROM [{TempTableName}]").Do();
}
else
{
AlterColumn<ContentNuDto>(Constants.DatabaseSchema.Tables.NodeData, "data");
}
var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList();
AddColumnIfNotExists<ContentNuDto>(columns, "dataRaw");
// allow null
AlterColumn<ContentNuDto>(Constants.DatabaseSchema.Tables.NodeData, "data");
AlterColumn<ContentNuDto>(Constants.DatabaseSchema.Tables.NodeData, "data");
}
private const string TempTableName = Constants.DatabaseSchema.TableNamePrefix + "cms" + "ContentNuTEMP";
[TableName(TempTableName)]
[ExplicitColumns]
private class ContentNuDtoTemp
{
[Column("nodeId")]
public int NodeId { get; set; }
[Column("published")]
public bool Published { get; set; }
[Column("data")]
[SpecialDbType(SpecialDbTypes.NTEXT)]
[NullSetting(NullSetting = NullSettings.Null)]
public string Data { get; set; }
[Column("rv")]
public long Rv { get; set; }
}
}
}

View File

@@ -0,0 +1,19 @@
using Umbraco.Cms.Infrastructure.Persistence;
namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_15_0
{
public class UpdateCmsPropertyGroupIdSeed : MigrationBase
{
public UpdateCmsPropertyGroupIdSeed(IMigrationContext context) : base(context)
{
}
protected override void Migrate()
{
if (DatabaseType.IsSqlCe())
{
Database.Execute(Sql("ALTER TABLE [cmsPropertyTypeGroup] ALTER COLUMN [id] IDENTITY (56,1)"));
}
}
}
}

View File

@@ -1,7 +1,7 @@
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
namespace Umbraco.Core.Models
namespace Umbraco.Cms.Core.Models
{
/// <summary>
/// Represents a media item with local crops.

View File

@@ -13,10 +13,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence
public int BulkInsertRecords<T>(IUmbracoDatabase database, IEnumerable<T> records)
{
var recordsA = records.ToArray();
if (recordsA.Length == 0) return 0;
if (!records.Any()) return 0;
return BulkInsertRecordsWithCommands(database, recordsA);
return BulkInsertRecordsWithCommands(database, records.ToArray());
}
/// <summary>

View File

@@ -12,7 +12,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Dtos
internal class PropertyTypeGroupDto
{
[Column("id")]
[PrimaryKeyColumn(IdentitySeed = 12)]
[PrimaryKeyColumn(IdentitySeed = 56)]
public int Id { get; set; }
[Column("contenttypeNodeId")]

View File

@@ -40,9 +40,10 @@ namespace Umbraco.Cms.Infrastructure.Persistence
_tableDefinition = DefinitionFactory.GetTableDefinition(pd.Type, sqlSyntaxProvider);
if (_tableDefinition == null) throw new InvalidOperationException("No table definition found for type " + pd.Type);
// only real columns, exclude result columns
// only real columns, exclude result/computed columns
// Like NPoco does: https://github.com/schotime/NPoco/blob/5117a55fde57547e928246c044fd40bd00b2d7d1/src/NPoco.SqlServer/SqlBulkCopyHelper.cs#L59
_readerColumns = pd.Columns
.Where(x => x.Value.ResultColumn == false)
.Where(x => x.Value.ResultColumn == false && x.Value.ComputedColumn == false)
.Select(x => x.Value)
.ToArray();

View File

@@ -39,6 +39,10 @@ namespace Umbraco.Cms.Infrastructure.Persistence
/// <returns>The number of records that were inserted.</returns>
private int BulkInsertRecordsSqlServer<T>(IUmbracoDatabase database, PocoData pocoData, IEnumerable<T> records)
{
// TODO: The main reason this exists is because the NPoco InsertBulk method doesn't return the number of items.
// It is worth investigating the performance of this vs NPoco's because we use a custom BulkDataReader
// which in theory should be more efficient than NPocos way of building up an in-memory DataTable.
// create command against the original database.Connection
using (var command = database.CreateCommand(database.Connection, CommandType.Text, string.Empty))
{
@@ -50,7 +54,13 @@ namespace Umbraco.Cms.Infrastructure.Persistence
var syntax = database.SqlContext.SqlSyntax as SqlServerSyntaxProvider;
if (syntax == null) throw new NotSupportedException("SqlSyntax must be SqlServerSyntaxProvider.");
using (var copy = new SqlBulkCopy(tConnection, SqlBulkCopyOptions.Default, tTransaction) { BulkCopyTimeout = 10000, DestinationTableName = tableName })
using (var copy = new SqlBulkCopy(tConnection, SqlBulkCopyOptions.Default, tTransaction)
{
BulkCopyTimeout = 0, // 0 = no bulk copy timeout. If a timeout occurs it will be an connection/command timeout.
DestinationTableName = tableName,
// be consistent with NPoco: https://github.com/schotime/NPoco/blob/5117a55fde57547e928246c044fd40bd00b2d7d1/src/NPoco.SqlServer/SqlBulkCopyHelper.cs#L50
BatchSize = 4096
})
using (var bulkReader = new PocoDataDataReader<T, SqlServerSyntaxProvider>(records, pocoData, syntax))
{
//we need to add column mappings here because otherwise columns will be matched by their order and if the order of them are different in the DB compared

View File

@@ -5,7 +5,7 @@ using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Core.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters

View File

@@ -17,13 +17,12 @@ namespace Umbraco.Cms.Persistence.SqlCe
public int BulkInsertRecords<T>(IUmbracoDatabase database, IEnumerable<T> records)
{
var recordsA = records.ToArray();
if (recordsA.Length == 0) return 0;
if (!records.Any()) return 0;
var pocoData = database.PocoDataFactory.ForType(typeof(T));
if (pocoData == null) throw new InvalidOperationException("Could not find PocoData for " + typeof(T));
return BulkInsertRecordsSqlCe(database, pocoData, recordsA);
return BulkInsertRecordsSqlCe(database, pocoData, records.ToArray());
}

View File

@@ -14,12 +14,12 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource
/// <summary>
/// Deserialize the data into a <see cref="ContentCacheDataModel"/>
/// </summary>
ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData);
ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData, bool published);
/// <summary>
/// Serializes the <see cref="ContentCacheDataModel"/>
/// Serializes the <see cref="ContentCacheDataModel"/>
/// </summary>
ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model);
ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model, bool published);
}
}

View File

@@ -24,7 +24,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource
DateFormatString = "o"
};
private readonly JsonNameTable _propertyNameTable = new DefaultJsonNameTable();
public ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData)
public ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData, bool published)
{
if (stringData == null && byteData != null)
throw new NotSupportedException($"{typeof(JsonContentNestedDataSerializer)} does not support byte[] serialization");
@@ -39,7 +39,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource
}
}
public ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model)
public ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model, bool published)
{
// note that numeric values (which are Int32) are serialized without their
// type (eg "value":1234) and JsonConvert by default deserializes them as Int64

View File

@@ -39,7 +39,8 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource
_options = defaultOptions
.WithResolver(resolver)
.WithCompression(MessagePackCompression.Lz4BlockArray);
.WithCompression(MessagePackCompression.Lz4BlockArray)
.WithSecurity(MessagePackSecurity.UntrustedData);
}
public string ToJson(byte[] bin)
@@ -48,12 +49,12 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource
return json;
}
public ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData)
public ContentCacheDataModel Deserialize(IReadOnlyContentBase content, string stringData, byte[] byteData, bool published)
{
if (byteData != null)
{
var cacheModel = MessagePackSerializer.Deserialize<ContentCacheDataModel>(byteData, _options);
Expand(content, cacheModel);
Expand(content, cacheModel, published);
return cacheModel;
}
else if (stringData != null)
@@ -61,7 +62,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource
// NOTE: We don't really support strings but it's possible if manually used (i.e. tests)
var bin = Convert.FromBase64String(stringData);
var cacheModel = MessagePackSerializer.Deserialize<ContentCacheDataModel>(bin, _options);
Expand(content, cacheModel);
Expand(content, cacheModel, published);
return cacheModel;
}
else
@@ -70,9 +71,9 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource
}
}
public ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model)
public ContentCacheDataSerializationResult Serialize(IReadOnlyContentBase content, ContentCacheDataModel model, bool published)
{
Compress(content, model);
Compress(content, model, published);
var bytes = MessagePackSerializer.Serialize(model, _options);
return new ContentCacheDataSerializationResult(null, bytes);
}
@@ -80,7 +81,9 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource
/// <summary>
/// Used during serialization to compress properties
/// </summary>
/// <param name="content"></param>
/// <param name="model"></param>
/// <param name="published"></param>
/// <remarks>
/// This will essentially 'double compress' property data. The MsgPack data as a whole will already be compressed
/// but this will go a step further and double compress property data so that it is stored in the nucache file
@@ -88,11 +91,11 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource
/// read/decompressed as a string to be displayed on the front-end. This allows for potentially a significant
/// memory savings but could also affect performance of first rendering pages while decompression occurs.
/// </remarks>
private void Compress(IReadOnlyContentBase content, ContentCacheDataModel model)
private void Compress(IReadOnlyContentBase content, ContentCacheDataModel model, bool published)
{
foreach(var propertyAliasToData in model.PropertyData)
{
if (_propertyOptions.IsCompressed(content, propertyAliasToData.Key))
if (_propertyOptions.IsCompressed(content, propertyAliasToData.Key, published))
{
foreach(var property in propertyAliasToData.Value.Where(x => x.Value != null && x.Value is string))
{
@@ -105,12 +108,14 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource
/// <summary>
/// Used during deserialization to map the property data as lazy or expand the value
/// </summary>
/// <param name="content"></param>
/// <param name="nestedData"></param>
private void Expand(IReadOnlyContentBase content, ContentCacheDataModel nestedData)
/// <param name="published"></param>
private void Expand(IReadOnlyContentBase content, ContentCacheDataModel nestedData, bool published)
{
foreach (var propertyAliasToData in nestedData.PropertyData)
{
if (_propertyOptions.IsCompressed(content, propertyAliasToData.Key))
if (_propertyOptions.IsCompressed(content, propertyAliasToData.Key,published))
{
foreach (var property in propertyAliasToData.Value.Where(x => x.Value != null))
{

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Services;
using Umbraco.Core.PropertyEditors;
namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource
{
@@ -14,7 +13,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource
private readonly IMemberTypeService _memberTypeService;
private readonly PropertyEditorCollection _propertyEditors;
private readonly IPropertyCacheCompressionOptions _compressionOptions;
private readonly ConcurrentDictionary<(int, string), bool> _isCompressedCache = new ConcurrentDictionary<(int, string), bool>();
private readonly ConcurrentDictionary<(int, string, bool), bool> _isCompressedCache = new ConcurrentDictionary<(int, string, bool), bool>();
public MsgPackContentNestedDataSerializerFactory(
IContentTypeService contentTypeService,

View File

@@ -68,7 +68,20 @@ namespace Umbraco.Extensions
throw new IndexOutOfRangeException();
}
});
builder.Services.AddSingleton<IPropertyCacheCompressionOptions, NoopPropertyCacheCompressionOptions>();
builder.Services.AddSingleton<IPropertyCacheCompressionOptions>(s =>
{
IOptions<NuCacheSettings> options = s.GetRequiredService<IOptions<NuCacheSettings>>();
if (options.Value.NuCacheSerializerType == NuCacheSerializerType.MessagePack &&
options.Value.UnPublishedContentCompression)
{
return new UnPublishedContentPropertyCacheCompressionOptions();
}
return new NoopPropertyCacheCompressionOptions();
});
builder.Services.AddSingleton(s => new ContentDataSerializer(new DictionaryOfPropertyDataSerializer()));
// add the NuCache health check (hidden from type finder)

View File

@@ -415,7 +415,7 @@ AND cmsContentNu.nodeId IS NULL
UrlSegment = content.GetUrlSegment(_shortStringHelper, _urlSegmentProviders)
};
var serialized = serializer.Serialize(ReadOnlyContentBaseAdapter.Create(content), contentCacheData);
var serialized = serializer.Serialize(ReadOnlyContentBaseAdapter.Create(content), contentCacheData, published);
var dto = new ContentNuDto
{
@@ -817,12 +817,13 @@ AND cmsContentNu.nodeId IS NULL
}
else
{
var deserializedContent = serializer.Deserialize(dto, dto.EditData, dto.EditDataRaw);
bool published = false;
var deserializedContent = serializer.Deserialize(dto, dto.EditData, dto.EditDataRaw, published);
d = new ContentData
{
Name = dto.EditName,
Published = false,
Published = published,
TemplateId = dto.EditTemplateId,
VersionId = dto.VersionId,
VersionDate = dto.EditVersionDate,
@@ -847,13 +848,14 @@ AND cmsContentNu.nodeId IS NULL
}
else
{
var deserializedContent = serializer.Deserialize(dto, dto.PubData, dto.PubDataRaw);
bool published = true;
var deserializedContent = serializer.Deserialize(dto, dto.PubData, dto.PubDataRaw, published);
p = new ContentData
{
Name = dto.PubName,
UrlSegment = deserializedContent.UrlSegment,
Published = true,
Published = published,
TemplateId = dto.PubTemplateId,
VersionId = dto.VersionId,
VersionDate = dto.PubVersionDate,
@@ -883,12 +885,13 @@ AND cmsContentNu.nodeId IS NULL
if (dto.EditData == null && dto.EditDataRaw == null)
throw new InvalidOperationException("No data for media " + dto.Id);
var deserializedMedia = serializer.Deserialize(dto, dto.EditData, dto.EditDataRaw);
bool published = true;
var deserializedMedia = serializer.Deserialize(dto, dto.EditData, dto.EditDataRaw, published);
var p = new ContentData
{
Name = dto.EditName,
Published = true,
Published = published,
TemplateId = -1,
VersionId = dto.VersionId,
VersionDate = dto.EditVersionDate,

View File

@@ -56,14 +56,14 @@ namespace Umbraco.Tests.PublishedContent
var content = Mock.Of<IReadOnlyContentBase>(x => x.ContentTypeId == 1);
var json = jsonSerializer.Serialize(content, cacheModel).StringData;
var msgPack = msgPackSerializer.Serialize(content, cacheModel).ByteData;
var json = jsonSerializer.Serialize(content, cacheModel, false).StringData;
var msgPack = msgPackSerializer.Serialize(content, cacheModel, false).ByteData;
Console.WriteLine(json);
Console.WriteLine(msgPackSerializer.ToJson(msgPack));
var jsonContent = jsonSerializer.Deserialize(content, json, null);
var msgPackContent = msgPackSerializer.Deserialize(content, null, msgPack);
var jsonContent = jsonSerializer.Deserialize(content, json, null, false);
var msgPackContent = msgPackSerializer.Deserialize(content, null, msgPack, false);
CollectionAssert.AreEqual(jsonContent.CultureData.Keys, msgPackContent.CultureData.Keys);

View File

@@ -28,12 +28,9 @@ using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Cms.Web.BackOffice.ActionResults;
using Umbraco.Cms.Web.BackOffice.Authorization;
using Umbraco.Cms.Web.BackOffice.Extensions;
using Umbraco.Cms.Web.BackOffice.Filters;
using Umbraco.Cms.Web.BackOffice.ModelBinders;
using Umbraco.Cms.Web.Common.ActionsResults;
using Umbraco.Cms.Web.Common.Attributes;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Extensions;
@@ -478,6 +475,13 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
return result;
}
private ActionResult<IDictionary<Guid, ContentItemDisplay>> GetEmptyByKeysInternal(Guid[] contentTypeKeys, int parentId)
{
using var scope = _scopeProvider.CreateScope(autoComplete: true);
var contentTypes = _contentTypeService.GetAll(contentTypeKeys).ToList();
return GetEmpties(contentTypes, parentId).ToDictionary(x => x.ContentTypeKey);
}
/// <summary>
/// Gets a collection of empty content items for all document types.
/// </summary>
@@ -486,9 +490,22 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
[OutgoingEditorModelEvent]
public ActionResult<IDictionary<Guid, ContentItemDisplay>> GetEmptyByKeys([FromQuery] Guid[] contentTypeKeys, [FromQuery] int parentId)
{
using var scope = _scopeProvider.CreateScope(autoComplete: true);
var contentTypes = _contentTypeService.GetAll(contentTypeKeys).ToList();
return GetEmpties(contentTypes, parentId).ToDictionary(x => x.ContentTypeKey);
return GetEmptyByKeysInternal(contentTypeKeys, parentId);
}
/// <summary>
/// Gets a collection of empty content items for all document types.
/// </summary>
/// <remarks>
/// This is a post request in order to support a large amount of GUIDs without hitting the URL length limit.
/// </remarks>
/// <param name="contentTypeByKeys"></param>
/// <returns></returns>
[HttpPost]
[OutgoingEditorModelEvent]
public ActionResult<IDictionary<Guid, ContentItemDisplay>> GetEmptyByKeys(ContentTypesByKeys contentTypeByKeys)
{
return GetEmptyByKeysInternal(contentTypeByKeys.ContentTypeKeys, contentTypeByKeys.ParentId);
}
[OutgoingEditorModelEvent]

View File

@@ -21,7 +21,6 @@ using Umbraco.Cms.Web.Common.Attributes;
using Umbraco.Cms.Web.Common.Authorization;
using Umbraco.Cms.Web.Common.Controllers;
using Umbraco.Cms.Web.Common.Filters;
using Umbraco.Core.Dashboards;
using Umbraco.Extensions;
using Constants = Umbraco.Cms.Core.Constants;

View File

@@ -6,7 +6,6 @@ using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
using Umbraco.Cms.Core.Routing;
using Umbraco.Cms.Web.Common.DependencyInjection;
using Umbraco.Core.Models;
namespace Umbraco.Extensions
{

View File

@@ -7,7 +7,6 @@ using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
using Umbraco.Cms.Core.Routing;
using Umbraco.Core.Models;
namespace Umbraco.Extensions
{

View File

@@ -217,9 +217,11 @@ namespace Umbraco.Extensions
}
var url = mediaItem.GetCropUrl(cropAlias: cropAlias, useCropDimensions: true);
return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url);
return CreateHtmlString(url, htmlEncode);
}
private static IHtmlContent CreateHtmlString(string url, bool htmlEncode) => htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url);
public static IHtmlContent GetCropUrl(this IUrlHelper urlHelper, IPublishedContent mediaItem, string propertyAlias, string cropAlias, bool htmlEncode = true)
{
if (mediaItem == null)
@@ -228,7 +230,7 @@ namespace Umbraco.Extensions
}
var url = mediaItem.GetCropUrl(propertyAlias: propertyAlias, cropAlias: cropAlias, useCropDimensions: true);
return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url);
return CreateHtmlString(url, htmlEncode);
}
public static IHtmlContent GetCropUrl(this IUrlHelper urlHelper,
@@ -256,7 +258,7 @@ namespace Umbraco.Extensions
var url = mediaItem.GetCropUrl(width, height, propertyAlias, cropAlias, quality, imageCropMode,
imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBuster, furtherOptions, ratioMode,
upScale);
return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url);
return CreateHtmlString(url, htmlEncode);
}
public static IHtmlContent GetCropUrl(this IUrlHelper urlHelper,
@@ -281,7 +283,7 @@ namespace Umbraco.Extensions
var url = imageUrl.GetCropUrl(imageCropperValue, width, height, cropAlias, quality, imageCropMode,
imageCropAnchor, preferFocalPoint, useCropDimensions, cacheBusterValue, furtherOptions, ratioMode,
upScale);
return htmlEncode ? new HtmlString(HttpUtility.HtmlEncode(url)) : new HtmlString(url);
return CreateHtmlString(url, htmlEncode);
}
/// <summary>

File diff suppressed because it is too large Load Diff

View File

@@ -126,7 +126,9 @@
// close all editors
if (args && !args.editor && args.editors.length === 0) {
editorCount = 0;
scope.editors = [];
scope.editors = [];
// Remove the inert attribute from the #mainwrapper
focusLockService.removeInertAttribute();
}
}));

View File

@@ -6,7 +6,7 @@
**/
angular.module("umbraco.directives")
.directive('umbImageCrop',
function ($timeout, $window, cropperHelper) {
function ($timeout, cropperHelper, windowResizeListener) {
const MAX_SCALE = 4;
@@ -26,7 +26,7 @@ angular.module("umbraco.directives")
forceUpdate: '@?'
},
link: function (scope, element, attrs, windowResizeListener) {
link: function (scope, element, attrs) {
var unsubscribe = [];
let sliderRef = null;
@@ -72,7 +72,7 @@ angular.module("umbraco.directives")
};
function updateSlider() {
if(sliderRef) {
if (sliderRef) {
// Update slider range min/max
sliderRef.noUiSlider.updateOptions({
"range": {
@@ -102,7 +102,7 @@ angular.module("umbraco.directives")
// cross-browser wheel delta
var delta = Math.max(-50, Math.min(50, (event.wheelDelta || -event.detail)));
if(sliderRef) {
if (sliderRef) {
var currentScale =sliderRef.noUiSlider.get();
var newScale = Math.min(Math.max(currentScale + delta*.001*scope.dimensions.image.ratio, scope.dimensions.scale.min), scope.dimensions.scale.max);
@@ -127,8 +127,8 @@ angular.module("umbraco.directives")
'left': (parseInt(scope.dimensions.margin.left, 10)) + 'px'
}
};
updateStyles();
updateStyles();
//elements
var $viewport = element.find(".viewport");
@@ -138,11 +138,11 @@ angular.module("umbraco.directives")
$overlay.bind("focus", function () {
$overlay.bind("DOMMouseScroll mousewheel onmousewheel", onScroll);
});
$overlay.bind("blur", function () {
$overlay.unbind("DOMMouseScroll mousewheel onmousewheel", onScroll);
});
//default constraints for drag n drop
var constraints = { left: { max: 0, min: 0 }, top: { max: 0, min: 0 } };
scope.constraints = constraints;

View File

@@ -57,7 +57,7 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) {
* Do stuff...
* });
* </pre>
*
*
* @returns {Promise} resourcePromise object.
*
*/
@@ -691,11 +691,12 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) {
getScaffoldByKeys: function (parentId, scaffoldKeys) {
return umbRequestHelper.resourcePromise(
$http.get(
$http.post(
umbRequestHelper.getApiUrl(
"contentApiBaseUrl",
"GetEmptyByKeys",
{ contentTypeKeys: scaffoldKeys, parentId: parentId })),
"GetEmptyByKeys"),
{ contentTypeKeys: scaffoldKeys, parentId: parentId }
),
'Failed to retrieve data for empty content items ids' + scaffoldKeys.join(", "))
.then(function (result) {
Object.keys(result).map(function(key) {
@@ -804,7 +805,7 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) {
else if (options.orderDirection === "desc") {
options.orderDirection = "Descending";
}
//converts the value to a js bool
function toBool(v) {
if (Utilities.isNumber(v)) {

View File

@@ -487,7 +487,7 @@
* @returns {Object | null} Scaffold model for the that content type. Or null if the scaffolding model dosnt exist in this context.
*/
getScaffoldFromKey: function (contentTypeKey) {
return this.scaffolds.find(o => o.contentTypeKey === contentTypeKey);
return this.scaffolds.find(o => o.contentTypeKey === contentTypeKey) || null;
},
/**
@@ -499,7 +499,7 @@
* @returns {Object | null} Scaffold model for the that content type. Or null if the scaffolding model dosnt exist in this context.
*/
getScaffoldFromAlias: function (contentTypeAlias) {
return this.scaffolds.find(o => o.contentTypeAlias === contentTypeAlias);
return this.scaffolds.find(o => o.contentTypeAlias === contentTypeAlias) || null;
},
/**
@@ -609,10 +609,14 @@
blockObject.settingsData = settingsData;
// make basics from scaffold
blockObject.settings = Utilities.copy(settingsScaffold);
ensureUdiAndKey(blockObject.settings, settingsUdi);
if (settingsScaffold !== null) {// We might not have settingsScaffold
blockObject.settings = Utilities.copy(settingsScaffold);
ensureUdiAndKey(blockObject.settings, settingsUdi);
mapToElementModel(blockObject.settings, settingsData);
mapToElementModel(blockObject.settings, settingsData);
} else {
blockObject.settings = null;
}
// add settings content-app
appendSettingsContentApp(blockObject.content, this.__labels.settingsName);

View File

@@ -22,7 +22,7 @@
*/
angular.module('umbraco.services')
.factory('localizationService', function ($http, $q, eventsService, $window, $filter, userService) {
.factory('localizationService', function ($http, $q, eventsService) {
// TODO: This should be injected as server vars
var url = "LocalizedText";
@@ -61,14 +61,14 @@ angular.module('umbraco.services')
return "[" + alias + "]";
}
var service = {
// loads the language resource file from the server
initLocalizedResources: function () {
// TODO: This promise handling is super ugly, we should just be returnning the promise from $http and returning inner values.
// TODO: This promise handling is super ugly, we should just be returnning the promise from $http and returning inner values.
var deferred = $q.defer();
@@ -120,7 +120,7 @@ angular.module('umbraco.services')
* @description
* Helper to tokenize and compile a localization string
* @param {String} value the value to tokenize
* @param {Object} scope the $scope object
* @param {Object} scope the $scope object
* @returns {String} tokenized resource string
*/
tokenize: function (value, scope) {
@@ -138,8 +138,8 @@ angular.module('umbraco.services')
}
return value;
},
/**
* @ngdoc method
* @name umbraco.services.localizationService#tokenReplace
@@ -148,19 +148,19 @@ angular.module('umbraco.services')
* @description
* Helper to replace tokens
* @param {String} value the text-string to manipulate
* @param {Array} tekens An array of tokens values
* @param {Array} tekens An array of tokens values
* @returns {String} Replaced test-string
*/
tokenReplace: function (text, tokens) {
if (tokens) {
for (var i = 0; i < tokens.length; i++) {
text = text.replace("%" + i + "%", tokens[i]);
text = text.replace("%" + i + "%", _.escape(tokens[i]));
}
}
return text;
},
/**
* @ngdoc method
* @name umbraco.services.localizationService#localize
@@ -168,16 +168,16 @@ angular.module('umbraco.services')
*
* @description
* Checks the dictionary for a localized resource string
* @param {String} value the area/key to localize in the format of 'section_key'
* @param {String} value the area/key to localize in the format of 'section_key'
* alternatively if no section is set such as 'key' then we assume the key is to be looked in
* the 'general' section
*
*
* @param {Array} tokens if specified this array will be sent as parameter values
* This replaces %0% and %1% etc in the dictionary key value with the passed in strings
*
* @param {String} fallbackValue if specified this string will be returned if no matching
*
* @param {String} fallbackValue if specified this string will be returned if no matching
* entry was found in the dictionary
*
*
* @returns {String} localized resource string
*/
localize: function (value, tokens, fallbackValue) {
@@ -194,7 +194,7 @@ angular.module('umbraco.services')
* @description
* Checks the dictionary for multipe localized resource strings at once, preventing the need for nested promises
* with localizationService.localize
*
*
* ##Usage
* <pre>
* localizationService.localizeMany(["speechBubbles_templateErrorHeader", "speechBubbles_templateErrorText"]).then(function(data){
@@ -203,11 +203,11 @@ angular.module('umbraco.services')
* notificationService.error(header, message);
* });
* </pre>
*
* @param {Array} keys is an array of strings of the area/key to localize in the format of 'section_key'
*
* @param {Array} keys is an array of strings of the area/key to localize in the format of 'section_key'
* alternatively if no section is set such as 'key' then we assume the key is to be looked in
* the 'general' section
*
*
* @returns {Array} An array of localized resource string in the same order
*/
localizeMany: function(keys) {
@@ -234,18 +234,18 @@ angular.module('umbraco.services')
* @description
* Checks the dictionary for multipe localized resource strings at once & concats them to a single string
* Which was not possible with localizationSerivce.localize() due to returning a promise
*
*
* ##Usage
* <pre>
* localizationService.concat(["speechBubbles_templateErrorHeader", "speechBubbles_templateErrorText"]).then(function(data){
* var combinedText = data;
* });
* </pre>
*
* @param {Array} keys is an array of strings of the area/key to localize in the format of 'section_key'
*
* @param {Array} keys is an array of strings of the area/key to localize in the format of 'section_key'
* alternatively if no section is set such as 'key' then we assume the key is to be looked in
* the 'general' section
*
*
* @returns {String} An concatenated string of localized resource string passed into the function in the same order
*/
concat: function(keys) {
@@ -280,7 +280,7 @@ angular.module('umbraco.services')
* @description
* Checks the dictionary for multipe localized resource strings at once & formats a tokenized message
* Which was not possible with localizationSerivce.localize() due to returning a promise
*
*
* ##Usage
* <pre>
* localizationService.format(["template_insert", "template_insertSections"], "%0% %1%").then(function(data){
@@ -288,14 +288,14 @@ angular.module('umbraco.services')
* var formattedResult = data;
* });
* </pre>
*
* @param {Array} keys is an array of strings of the area/key to localize in the format of 'section_key'
*
* @param {Array} keys is an array of strings of the area/key to localize in the format of 'section_key'
* alternatively if no section is set such as 'key' then we assume the key is to be looked in
* the 'general' section
*
*
* @param {String} message is the string you wish to replace containing tokens in the format of %0% and %1%
* with the localized resource strings
*
*
* @returns {String} An concatenated string of localized resource string passed into the function in the same order
*/
format: function(keys, message){
@@ -330,7 +330,7 @@ angular.module('umbraco.services')
resourceFileLoadStatus = "none";
resourceLoadingPromise = [];
});
// return the local instance when called
return service;

View File

@@ -11,7 +11,7 @@
*/
function windowResizeListener($rootScope) {
var WinReszier = (function () {
var WinResizer = (function () {
var registered = [];
var inited = false;
var resize = _.debounce(function(ev) {
@@ -51,7 +51,7 @@ function windowResizeListener($rootScope) {
* @param {Function} cb
*/
register: function (cb) {
WinReszier.register(cb);
WinResizer.register(cb);
},
/**
@@ -59,9 +59,9 @@ function windowResizeListener($rootScope) {
* @param {Function} cb
*/
unregister: function(cb) {
WinReszier.unregister(cb);
WinResizer.unregister(cb);
}
};
}
angular.module('umbraco.services').factory('windowResizeListener', windowResizeListener);
angular.module('umbraco.services').factory('windowResizeListener', windowResizeListener);

View File

@@ -154,9 +154,10 @@
currentForm.$dirty = false;
}
});
$scope.dialog.confirmDiscardChanges = false;
vm.saveState = "success";
vm.saveSuccces = true;
}, function(error){
vm.saveState = "error";
vm.saveError = error;

View File

@@ -98,6 +98,10 @@
vm.model.value.forEach(mediaEntry => updateMediaEntryData(mediaEntry));
// set the onValueChanged callback, this will tell us if the media picker model changed on the server
// once the data is submitted. If so we need to re-initialize
vm.model.onValueChanged = onServerValueChanged;
userService.getCurrentUser().then(function (userData) {
if (!vm.model.config.startNodeId) {
@@ -120,6 +124,15 @@
};
function onServerValueChanged(newVal, oldVal) {
if(newVal === null || !Array.isArray(newVal)) {
newVal = [];
vm.model.value = newVal;
}
vm.model.value.forEach(mediaEntry => updateMediaEntryData(mediaEntry));
}
function setDirty() {
if (vm.propertyForm) {
vm.propertyForm.$setDirty();
@@ -259,7 +272,7 @@
function setActiveMedia(mediaEntryOrNull) {
vm.activeMediaEntry = mediaEntryOrNull;
}
function editMedia(mediaEntry, options, $event) {
if($event)

View File

@@ -80,7 +80,7 @@
<RazorCompileOnBuild>false</RazorCompileOnBuild>
<RazorCompileOnPublish>false</RazorCompileOnPublish>
</PropertyGroup>
<PropertyGroup>
<BellePath>$(ProjectDir)wwwroot/umbraco</BellePath>
<JsonSchemaPath>$(ProjectDir)umbraco/config/appsettings-schema.json</JsonSchemaPath>
@@ -107,7 +107,7 @@
<Target Name="JsonSchemaBuild">
<!-- <Exec WorkingDirectory="$(ProjectDir)/../../" Command="powershell -ExecutionPolicy RemoteSigned -Command '&amp;dotnet run &#45;&#45;project $pwd/src/JsonSchema/JsonSchema.csproj -c Release &#45;&#45; &#45;&#45;outputFile $pwd/src/Umbraco.Web.UI.NetCore/$(JsonSchemaPath)'" />-->
</Target>
<!-- clean Belle when cleaning and rebuilding, but only in Visual Studio -->
<Target Name="CleanPreconditions" AfterTargets="Clean" Condition="'$(UmbracoBuild)' == ''">
<Message Text="-CleanPreconditions-" Importance="high" />