diff --git a/build/NuSpecs/UmbracoCms.nuspec b/build/NuSpecs/UmbracoCms.nuspec
index 7a7f672ea6..3f649b5116 100644
--- a/build/NuSpecs/UmbracoCms.nuspec
+++ b/build/NuSpecs/UmbracoCms.nuspec
@@ -40,7 +40,6 @@
-
diff --git a/src/Umbraco.Core/Constants-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs
index 7f6f6e875f..2930cf1fff 100644
--- a/src/Umbraco.Core/Constants-Conventions.cs
+++ b/src/Umbraco.Core/Constants-Conventions.cs
@@ -23,9 +23,6 @@ namespace Umbraco.Core
{
public const string MemberUsernameRuleType = "MemberUsername";
public const string MemberRoleRuleType = "MemberRole";
-
- [Obsolete("No longer supported, this is here for backwards compatibility only")]
- public const string MemberIdRuleType = "MemberId";
}
diff --git a/src/Umbraco.Core/Constants-ObjectTypes.cs b/src/Umbraco.Core/Constants-ObjectTypes.cs
index 790c143bbf..4bf944e1e1 100644
--- a/src/Umbraco.Core/Constants-ObjectTypes.cs
+++ b/src/Umbraco.Core/Constants-ObjectTypes.cs
@@ -71,9 +71,6 @@ namespace Umbraco.Core
[Obsolete("This no longer exists in the database")]
internal const string Stylesheet = "9F68DA4F-A3A8-44C2-8226-DCBD125E4840";
- [Obsolete("This no longer exists in the database")]
- internal const string StylesheetProperty = "5555da4f-a123-42b2-4488-dcdfb25e4111";
-
// ReSharper restore MemberHidesStaticFromOuterClass
}
diff --git a/src/Umbraco.Core/EnumerableExtensions.cs b/src/Umbraco.Core/EnumerableExtensions.cs
index 0d199d1d0d..c455fadad7 100644
--- a/src/Umbraco.Core/EnumerableExtensions.cs
+++ b/src/Umbraco.Core/EnumerableExtensions.cs
@@ -92,18 +92,7 @@ namespace Umbraco.Core
}
}
- /// The flatten list.
- /// The items.
- /// The select child.
- /// Item type
- /// list of TItem
- [EditorBrowsable(EditorBrowsableState.Never)]
- [Obsolete("Do not use, use SelectRecursive instead which has far less potential of re-iterating an iterator which may cause significantly more SQL queries")]
- public static IEnumerable FlattenList(this IEnumerable e, Func> f)
- {
- return e.SelectMany(c => f(c).FlattenList(f)).Concat(e);
- }
-
+
///
/// Returns true if all items in the other collection exist in this collection
///
diff --git a/src/Umbraco.Core/HttpContextExtensions.cs b/src/Umbraco.Core/HttpContextExtensions.cs
index e370b055a4..22eb4d1917 100644
--- a/src/Umbraco.Core/HttpContextExtensions.cs
+++ b/src/Umbraco.Core/HttpContextExtensions.cs
@@ -24,11 +24,19 @@ namespace Umbraco.Core
{
return "Unknown, httpContext is null";
}
- if (httpContext.Request == null)
+
+ HttpRequestBase request;
+ try
+ {
+ // is not null - throws
+ request = httpContext.Request;
+ }
+ catch
{
return "Unknown, httpContext.Request is null";
}
- if (httpContext.Request.ServerVariables == null)
+
+ if (request.ServerVariables == null)
{
return "Unknown, httpContext.Request.ServerVariables is null";
}
@@ -37,16 +45,16 @@ namespace Umbraco.Core
try
{
- var ipAddress = httpContext.Request.ServerVariables["HTTP_X_FORWARDED_FOR"];
+ var ipAddress = request.ServerVariables["HTTP_X_FORWARDED_FOR"];
if (string.IsNullOrEmpty(ipAddress))
- return httpContext.Request.UserHostAddress;
+ return request.UserHostAddress;
var addresses = ipAddress.Split(',');
if (addresses.Length != 0)
return addresses[0];
- return httpContext.Request.UserHostAddress;
+ return request.UserHostAddress;
}
catch (System.Exception ex)
{
diff --git a/src/Umbraco.Core/Logging/DisposableTimer.cs b/src/Umbraco.Core/Logging/DisposableTimer.cs
index db530e5339..ed98e5cfab 100644
--- a/src/Umbraco.Core/Logging/DisposableTimer.cs
+++ b/src/Umbraco.Core/Logging/DisposableTimer.cs
@@ -30,17 +30,17 @@ namespace Umbraco.Core.Logging
_endMessage = endMessage;
_failMessage = failMessage;
_thresholdMilliseconds = thresholdMilliseconds < 0 ? 0 : thresholdMilliseconds;
- _timingId = Guid.NewGuid().ToString("N");
+ _timingId = Guid.NewGuid().ToString("N").Substring(0, 7); // keep it short-ish
if (thresholdMilliseconds == 0)
{
switch (_level)
{
case LogLevel.Debug:
- logger.Debug(loggerType, "[Timing {TimingId}] {StartMessage}", _timingId, startMessage);
+ logger.Debug(loggerType, "{StartMessage} [Timing {TimingId}]", startMessage, _timingId);
break;
case LogLevel.Information:
- logger.Info(loggerType, "[Timing {TimingId}] {StartMessage}", _timingId, startMessage);
+ logger.Info(loggerType, "{StartMessage} [Timing {TimingId}]", startMessage, _timingId);
break;
default:
throw new ArgumentOutOfRangeException(nameof(level));
@@ -84,15 +84,15 @@ namespace Umbraco.Core.Logging
{
if (_failed)
{
- _logger.Error(_loggerType, _failException, "[Timing {TimingId}] {FailMessage} ({TimingDuration}ms)", _timingId, _failMessage, Stopwatch.ElapsedMilliseconds);
+ _logger.Error(_loggerType, _failException, "{FailMessage} ({Duration}ms) [Timing {TimingId}]", _failMessage, Stopwatch.ElapsedMilliseconds, _timingId);
}
else switch (_level)
{
case LogLevel.Debug:
- _logger.Debug(_loggerType, "[Timing {TimingId}] {EndMessage} ({TimingDuration}ms)", _timingId, _endMessage, Stopwatch.ElapsedMilliseconds);
+ _logger.Debug(_loggerType, "{EndMessage} ({Duration}ms) [Timing {TimingId}]", _endMessage, Stopwatch.ElapsedMilliseconds, _timingId);
break;
case LogLevel.Information:
- _logger.Info(_loggerType, "[Timing {TimingId}] {EndMessage} ({TimingDuration}ms)", _timingId, _endMessage, Stopwatch.ElapsedMilliseconds);
+ _logger.Info(_loggerType, "{EndMessage} ({Duration}ms) [Timing {TimingId}]", _endMessage, Stopwatch.ElapsedMilliseconds, _timingId);
break;
// filtered in the ctor
//default:
diff --git a/src/Umbraco.Core/Logging/LogHttpRequest.cs b/src/Umbraco.Core/Logging/LogHttpRequest.cs
new file mode 100644
index 0000000000..34c1918b76
--- /dev/null
+++ b/src/Umbraco.Core/Logging/LogHttpRequest.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Web;
+
+namespace Umbraco.Core.Logging
+{
+ public static class LogHttpRequest
+ {
+ static readonly string RequestIdItemName = typeof(LogHttpRequest).Name + "+RequestId";
+
+ ///
+ /// Retrieve the id assigned to the currently-executing HTTP request, if any.
+ ///
+ /// The request id.
+ /// true if there is a request in progress; false otherwise.
+ public static bool TryGetCurrentHttpRequestId(out Guid requestId)
+ {
+ if (HttpContext.Current == null)
+ {
+ requestId = default(Guid);
+ return false;
+ }
+
+ var requestIdItem = HttpContext.Current.Items[RequestIdItemName];
+ if (requestIdItem == null)
+ HttpContext.Current.Items[RequestIdItemName] = requestId = Guid.NewGuid();
+ else
+ requestId = (Guid)requestIdItem;
+
+ return true;
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Logging/Serilog/Enrichers/HttpRequestIdEnricher.cs b/src/Umbraco.Core/Logging/Serilog/Enrichers/HttpRequestIdEnricher.cs
new file mode 100644
index 0000000000..2099698b6f
--- /dev/null
+++ b/src/Umbraco.Core/Logging/Serilog/Enrichers/HttpRequestIdEnricher.cs
@@ -0,0 +1,36 @@
+using System;
+using Serilog.Core;
+using Serilog.Events;
+
+namespace Umbraco.Core.Logging.Serilog.Enrichers
+{
+ ///
+ /// Enrich log events with a HttpRequestId GUID.
+ /// Original source - https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpRequestIdEnricher.cs
+ /// Nupkg: 'Serilog.Web.Classic' contains handlers & extra bits we do not want
+ ///
+ internal class HttpRequestIdEnricher : ILogEventEnricher
+ {
+ ///
+ /// The property name added to enriched log events.
+ ///
+ public const string HttpRequestIdPropertyName = "HttpRequestId";
+
+ ///
+ /// Enrich the log event with an id assigned to the currently-executing HTTP request, if any.
+ ///
+ /// The log event to enrich.
+ /// Factory for creating new properties to add to the event.
+ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
+ {
+ if (logEvent == null) throw new ArgumentNullException("logEvent");
+
+ Guid requestId;
+ if (!LogHttpRequest.TryGetCurrentHttpRequestId(out requestId))
+ return;
+
+ var requestIdProperty = new LogEventProperty(HttpRequestIdPropertyName, new ScalarValue(requestId));
+ logEvent.AddPropertyIfAbsent(requestIdProperty);
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Logging/Serilog/Enrichers/HttpRequestNumberEnricher.cs b/src/Umbraco.Core/Logging/Serilog/Enrichers/HttpRequestNumberEnricher.cs
new file mode 100644
index 0000000000..48415cccbc
--- /dev/null
+++ b/src/Umbraco.Core/Logging/Serilog/Enrichers/HttpRequestNumberEnricher.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Threading;
+using System.Web;
+using Serilog.Core;
+using Serilog.Events;
+
+namespace Umbraco.Core.Logging.Serilog.Enrichers
+{
+ ///
+ /// Enrich log events with a HttpRequestNumber unique within the current
+ /// logging session.
+ /// Original source - https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpRequestNumberEnricher.cs
+ /// Nupkg: 'Serilog.Web.Classic' contains handlers & extra bits we do not want
+ ///
+ internal class HttpRequestNumberEnricher : ILogEventEnricher
+ {
+ ///
+ /// The property name added to enriched log events.
+ ///
+ public const string HttpRequestNumberPropertyName = "HttpRequestNumber";
+
+ static int _lastRequestNumber;
+ static readonly string RequestNumberItemName = typeof(HttpRequestNumberEnricher).Name + "+RequestNumber";
+
+ ///
+ /// Enrich the log event with the number assigned to the currently-executing HTTP request, if any.
+ ///
+ /// The log event to enrich.
+ /// Factory for creating new properties to add to the event.
+ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
+ {
+ if (logEvent == null) throw new ArgumentNullException("logEvent");
+
+ if (HttpContext.Current == null)
+ return;
+
+ int requestNumber;
+ var requestNumberItem = HttpContext.Current.Items[RequestNumberItemName];
+ if (requestNumberItem == null)
+ HttpContext.Current.Items[RequestNumberItemName] = requestNumber = Interlocked.Increment(ref _lastRequestNumber);
+ else
+ requestNumber = (int)requestNumberItem;
+
+ var requestNumberProperty = new LogEventProperty(HttpRequestNumberPropertyName, new ScalarValue(requestNumber));
+ logEvent.AddPropertyIfAbsent(requestNumberProperty);
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Logging/Serilog/Enrichers/HttpSessionIdEnricher.cs b/src/Umbraco.Core/Logging/Serilog/Enrichers/HttpSessionIdEnricher.cs
new file mode 100644
index 0000000000..d2fbfd4627
--- /dev/null
+++ b/src/Umbraco.Core/Logging/Serilog/Enrichers/HttpSessionIdEnricher.cs
@@ -0,0 +1,39 @@
+using Serilog.Core;
+using Serilog.Events;
+using System;
+using System.Web;
+
+namespace Umbraco.Core.Logging.Serilog.Enrichers
+{
+ ///
+ /// Enrich log events with the HttpSessionId property.
+ /// Original source - https://github.com/serilog-web/classic/blob/master/src/SerilogWeb.Classic/Classic/Enrichers/HttpSessionIdEnricher.cs
+ /// Nupkg: 'Serilog.Web.Classic' contains handlers & extra bits we do not want
+ ///
+ internal class HttpSessionIdEnricher : ILogEventEnricher
+ {
+ ///
+ /// The property name added to enriched log events.
+ ///
+ public const string HttpSessionIdPropertyName = "HttpSessionId";
+
+ ///
+ /// Enrich the log event with the current ASP.NET session id, if sessions are enabled.
+ /// The log event to enrich.
+ /// Factory for creating new properties to add to the event.
+ public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
+ {
+ if (logEvent == null) throw new ArgumentNullException("logEvent");
+
+ if (HttpContext.Current == null)
+ return;
+
+ if (HttpContext.Current.Session == null)
+ return;
+
+ var sessionId = HttpContext.Current.Session.SessionID;
+ var sessionIdProperty = new LogEventProperty(HttpSessionIdPropertyName, new ScalarValue(sessionId));
+ logEvent.AddPropertyIfAbsent(sessionIdProperty);
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Logging/Serilog/Log4NetLevelMapperEnricher.cs b/src/Umbraco.Core/Logging/Serilog/Enrichers/Log4NetLevelMapperEnricher.cs
similarity index 96%
rename from src/Umbraco.Core/Logging/Serilog/Log4NetLevelMapperEnricher.cs
rename to src/Umbraco.Core/Logging/Serilog/Enrichers/Log4NetLevelMapperEnricher.cs
index 1424fa0b55..0c255fa8b4 100644
--- a/src/Umbraco.Core/Logging/Serilog/Log4NetLevelMapperEnricher.cs
+++ b/src/Umbraco.Core/Logging/Serilog/Enrichers/Log4NetLevelMapperEnricher.cs
@@ -1,7 +1,7 @@
using Serilog.Core;
using Serilog.Events;
-namespace Umbraco.Core.Logging.Serilog
+namespace Umbraco.Core.Logging.Serilog.Enrichers
{
///
/// This is used to create a new property in Logs called 'Log4NetLevel'
diff --git a/src/Umbraco.Core/Logging/Serilog/LoggerConfigExtensions.cs b/src/Umbraco.Core/Logging/Serilog/LoggerConfigExtensions.cs
index 8861c808df..2d333ed916 100644
--- a/src/Umbraco.Core/Logging/Serilog/LoggerConfigExtensions.cs
+++ b/src/Umbraco.Core/Logging/Serilog/LoggerConfigExtensions.cs
@@ -3,6 +3,7 @@ using System.Web;
using Serilog;
using Serilog.Events;
using Serilog.Formatting.Compact;
+using Umbraco.Core.Logging.Serilog.Enrichers;
namespace Umbraco.Core.Logging.Serilog
{
@@ -21,7 +22,7 @@ namespace Umbraco.Core.Logging.Serilog
//Set this environment variable - so that it can be used in external config file
//add key="serilog:write-to:RollingFile.pathFormat" value="%BASEDIR%\logs\log.txt" />
Environment.SetEnvironmentVariable("BASEDIR", AppDomain.CurrentDomain.BaseDirectory, EnvironmentVariableTarget.Process);
-
+
logConfig.MinimumLevel.Verbose() //Set to highest level of logging (as any sinks may want to restrict it to Errors only)
.Enrich.WithProcessId()
.Enrich.WithProcessName()
@@ -29,8 +30,11 @@ namespace Umbraco.Core.Logging.Serilog
.Enrich.WithProperty("AppDomainId", AppDomain.CurrentDomain.Id)
.Enrich.WithProperty("AppDomainAppId", HttpRuntime.AppDomainAppId.ReplaceNonAlphanumericChars(string.Empty))
.Enrich.WithProperty("MachineName", Environment.MachineName)
- .Enrich.With();
-
+ .Enrich.With()
+ .Enrich.With()
+ .Enrich.With()
+ .Enrich.With();
+
return logConfig;
}
diff --git a/src/Umbraco.Core/Manifest/ContentAppDefinitionConverter.cs b/src/Umbraco.Core/Manifest/ContentAppDefinitionConverter.cs
new file mode 100644
index 0000000000..87f104d90e
--- /dev/null
+++ b/src/Umbraco.Core/Manifest/ContentAppDefinitionConverter.cs
@@ -0,0 +1,16 @@
+using System;
+using Newtonsoft.Json.Linq;
+using Umbraco.Core.Models.ContentEditing;
+using Umbraco.Core.Serialization;
+
+namespace Umbraco.Core.Manifest
+{
+ ///
+ /// Implements a json read converter for .
+ ///
+ internal class ContentAppDefinitionConverter : JsonReadConverter
+ {
+ protected override IContentAppDefinition Create(Type objectType, string path, JObject jObject)
+ => new ManifestContentAppDefinition();
+ }
+}
diff --git a/src/Umbraco.Core/Manifest/ManifestContentAppDefinition.cs b/src/Umbraco.Core/Manifest/ManifestContentAppDefinition.cs
new file mode 100644
index 0000000000..6b8534a88f
--- /dev/null
+++ b/src/Umbraco.Core/Manifest/ManifestContentAppDefinition.cs
@@ -0,0 +1,176 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Runtime.Serialization;
+using System.Text.RegularExpressions;
+using Umbraco.Core.IO;
+using Umbraco.Core.Models;
+using Umbraco.Core.Models.ContentEditing;
+
+namespace Umbraco.Core.Manifest
+{
+ // contentApps: [
+ // {
+ // name: 'App Name', // required
+ // alias: 'appAlias', // required
+ // weight: 0, // optional, default is 0, use values between -99 and +99
+ // icon: 'icon.app', // required
+ // view: 'path/view.htm', // required
+ // show: [ // optional, default is always show
+ // '-content/foo', // hide for content type 'foo'
+ // '+content/*', // show for all other content types
+ // '+media/*' // show for all media types
+ // ]
+ // },
+ // ...
+ // ]
+
+ ///
+ /// Represents a content app definition, parsed from a manifest.
+ ///
+ [DataContract(Name = "appdef", Namespace = "")]
+ public class ManifestContentAppDefinition : IContentAppDefinition
+ {
+ private string _view;
+ private ContentApp _app;
+ private ShowRule[] _showRules;
+
+ ///
+ /// Gets or sets the name of the content app.
+ ///
+ [DataMember(Name = "name")]
+ public string Name { get; set; }
+
+ ///
+ /// Gets or sets the unique alias of the content app.
+ ///
+ ///
+ /// Must be a valid javascript identifier, ie no spaces etc.
+ ///
+ [DataMember(Name = "alias")]
+ public string Alias { get; set; }
+
+ ///
+ /// Gets or sets the weight of the content app.
+ ///
+ [DataMember(Name = "weight")]
+ public int Weight { get; set; }
+
+ ///
+ /// Gets or sets the icon of the content app.
+ ///
+ ///
+ /// Must be a valid helveticons class name (see http://hlvticons.ch/).
+ ///
+ [DataMember(Name = "icon")]
+ public string Icon { get; set; }
+
+ ///
+ /// Gets or sets the view for rendering the content app.
+ ///
+ [DataMember(Name = "view")]
+ public string View
+ {
+ get => _view;
+ set => _view = IOHelper.ResolveVirtualUrl(value);
+ }
+
+ ///
+ /// Gets or sets the list of 'show' conditions for the content app.
+ ///
+ [DataMember(Name = "show")]
+ public string[] Show { get; set; } = Array.Empty();
+
+ ///
+ public ContentApp GetContentAppFor(object o)
+ {
+ string partA, partB;
+
+ switch (o)
+ {
+ case IContent content:
+ partA = "content";
+ partB = content.ContentType.Alias;
+ break;
+
+ case IMedia media:
+ partA = "media";
+ partB = media.ContentType.Alias;
+ break;
+
+ default:
+ return null;
+ }
+
+ var rules = _showRules ?? (_showRules = ShowRule.Parse(Show).ToArray());
+
+ // if no 'show' is specified, then always display the content app
+ if (rules.Length > 0)
+ {
+ var ok = false;
+
+ // else iterate over each entry
+ foreach (var rule in rules)
+ {
+ // if the entry does not apply, skip it
+ if (!rule.Matches(partA, partB))
+ continue;
+
+ // if the entry applies,
+ // if it's an exclude entry, exit, do not display the content app
+ if (!rule.Show)
+ return null;
+
+ // else break - ok to display
+ ok = true;
+ break;
+ }
+
+ // when 'show' is specified, default is to *not* show the content app
+ if (!ok)
+ return null;
+ }
+
+ // content app can be displayed
+ return _app ?? (_app = new ContentApp
+ {
+ Alias = Alias,
+ Name = Name,
+ Icon = Icon,
+ View = View,
+ Weight = Weight
+ });
+ }
+
+ private class ShowRule
+ {
+ private static readonly Regex ShowRegex = new Regex("^([+-])?([a-z]+)/([a-z0-9_]+|\\*)$", RegexOptions.Compiled | RegexOptions.IgnoreCase);
+
+ public bool Show { get; private set; }
+ public string PartA { get; private set; }
+ public string PartB { get; private set; }
+
+ public bool Matches(string partA, string partB)
+ {
+ return (PartA == "*" || PartA.InvariantEquals(partA)) && (PartB == "*" || PartB.InvariantEquals(partB));
+ }
+
+ public static IEnumerable Parse(string[] rules)
+ {
+ foreach (var rule in rules)
+ {
+ var match = ShowRegex.Match(rule);
+ if (!match.Success)
+ throw new FormatException($"Illegal 'show' entry \"{rule}\" in manifest.");
+
+ yield return new ShowRule
+ {
+ Show = match.Groups[1].Value != "-",
+ PartA = match.Groups[2].Value,
+ PartB = match.Groups[3].Value
+ };
+ }
+ }
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Manifest/ManifestParser.cs b/src/Umbraco.Core/Manifest/ManifestParser.cs
index e2363e314f..125dee5c05 100644
--- a/src/Umbraco.Core/Manifest/ManifestParser.cs
+++ b/src/Umbraco.Core/Manifest/ManifestParser.cs
@@ -8,6 +8,7 @@ using Umbraco.Core.Cache;
using Umbraco.Core.Exceptions;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
+using Umbraco.Core.Models.ContentEditing;
using Umbraco.Core.PropertyEditors;
namespace Umbraco.Core.Manifest
@@ -98,6 +99,7 @@ namespace Umbraco.Core.Manifest
var propertyEditors = new List();
var parameterEditors = new List();
var gridEditors = new List();
+ var contentApps = new List();
foreach (var manifest in manifests)
{
@@ -106,6 +108,7 @@ namespace Umbraco.Core.Manifest
if (manifest.PropertyEditors != null) propertyEditors.AddRange(manifest.PropertyEditors);
if (manifest.ParameterEditors != null) parameterEditors.AddRange(manifest.ParameterEditors);
if (manifest.GridEditors != null) gridEditors.AddRange(manifest.GridEditors);
+ if (manifest.ContentApps != null) contentApps.AddRange(manifest.ContentApps);
}
return new PackageManifest
@@ -114,7 +117,8 @@ namespace Umbraco.Core.Manifest
Stylesheets = stylesheets.ToArray(),
PropertyEditors = propertyEditors.ToArray(),
ParameterEditors = parameterEditors.ToArray(),
- GridEditors = gridEditors.ToArray()
+ GridEditors = gridEditors.ToArray(),
+ ContentApps = contentApps.ToArray()
};
}
@@ -146,7 +150,8 @@ namespace Umbraco.Core.Manifest
var manifest = JsonConvert.DeserializeObject(text,
new DataEditorConverter(_logger),
- new ValueValidatorConverter(_validators));
+ new ValueValidatorConverter(_validators),
+ new ContentAppDefinitionConverter());
// scripts and stylesheets are raw string, must process here
for (var i = 0; i < manifest.Scripts.Length; i++)
diff --git a/src/Umbraco.Core/Manifest/PackageManifest.cs b/src/Umbraco.Core/Manifest/PackageManifest.cs
index a1702cc58b..32dae46a9a 100644
--- a/src/Umbraco.Core/Manifest/PackageManifest.cs
+++ b/src/Umbraco.Core/Manifest/PackageManifest.cs
@@ -1,5 +1,6 @@
using System;
using Newtonsoft.Json;
+using Umbraco.Core.Models.ContentEditing;
using Umbraco.Core.PropertyEditors;
namespace Umbraco.Core.Manifest
@@ -23,5 +24,8 @@ namespace Umbraco.Core.Manifest
[JsonProperty("gridEditors")]
public GridEditor[] GridEditors { get; set; } = Array.Empty();
+
+ [JsonProperty("contentApps")]
+ public IContentAppDefinition[] ContentApps { get; set; } = Array.Empty();
}
}
diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs
index 5ec259ade4..620c040bfe 100644
--- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs
+++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs
@@ -152,6 +152,7 @@ namespace Umbraco.Core.Migrations.Install
_database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.MemberTypes, Name = "MemberTypes" });
_database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.MemberTree, Name = "MemberTree" });
_database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.Domains, Name = "Domains" });
+ _database.Insert(Constants.DatabaseSchema.Tables.Lock, "id", false, new LockDto { Id = Constants.Locks.Languages, Name = "Languages" });
}
private void CreateContentTypeData()
diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs
index 036fc99fa4..e28193ac5e 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs
@@ -130,6 +130,7 @@ namespace Umbraco.Core.Migrations.Upgrade
.Chain("{5F4597F4-A4E0-4AFE-90B5-6D2F896830EB}"); // to next
// resume at {5F4597F4-A4E0-4AFE-90B5-6D2F896830EB} ...
+ Chain("{B19BF0F2-E1C6-4AEB-A146-BC559D97A2C6}");
//FINAL
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RefactorVariantsModel.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RefactorVariantsModel.cs
new file mode 100644
index 0000000000..aa498583ff
--- /dev/null
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/RefactorVariantsModel.cs
@@ -0,0 +1,79 @@
+using Umbraco.Core.Persistence;
+using Umbraco.Core.Persistence.Dtos;
+
+namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
+{
+ public class RefactorVariantsModel : MigrationBase
+ {
+ public RefactorVariantsModel(IMigrationContext context)
+ : base(context)
+ { }
+
+ public override void Migrate()
+ {
+ Delete.Column("edited").FromTable(Constants.DatabaseSchema.Tables.ContentVersionCultureVariation).Do();
+
+
+ // add available column
+ AddColumn("available", out var sqls);
+
+ // so far, only those cultures that were available had records in the table
+ Update.Table(DocumentCultureVariationDto.TableName).Set(new { available = true }).AllRows().Do();
+
+ foreach (var sql in sqls) Execute.Sql(sql).Do();
+
+
+ // add published column
+ AddColumn("published", out sqls);
+
+ // make it false by default
+ Update.Table(DocumentCultureVariationDto.TableName).Set(new { published = false }).AllRows().Do();
+
+ // now figure out whether these available cultures are published, too
+ var getPublished = Sql()
+ .Select(x => x.NodeId)
+ .AndSelect(x => x.LanguageId)
+ .From()
+ .InnerJoin().On((node, cv) => node.NodeId == cv.NodeId)
+ .InnerJoin().On((cv, dv) => cv.Id == dv.Id && dv.Published)
+ .InnerJoin().On((cv, ccv) => cv.Id == ccv.VersionId);
+
+ foreach (var dto in Database.Fetch(getPublished))
+ Database.Execute(Sql()
+ .Update(u => u.Set(x => x.Published, true))
+ .Where(x => x.NodeId == dto.NodeId && x.LanguageId == dto.LanguageId));
+
+ foreach (var sql in sqls) Execute.Sql(sql).Do();
+
+ // so far, it was kinda impossible to make a culture unavailable again,
+ // so we *should* not have anything published but not available - ignore
+
+
+ // add name column
+ AddColumn("name");
+
+ // so far, every record in the table mapped to an available culture
+ var getNames = Sql()
+ .Select(x => x.NodeId)
+ .AndSelect(x => x.LanguageId, x => x.Name)
+ .From()
+ .InnerJoin().On((node, cv) => node.NodeId == cv.NodeId && cv.Current)
+ .InnerJoin().On((cv, ccv) => cv.Id == ccv.VersionId);
+
+ foreach (var dto in Database.Fetch(getNames))
+ Database.Execute(Sql()
+ .Update(u => u.Set(x => x.Name, dto.Name))
+ .Where(x => x.NodeId == dto.NodeId && x.LanguageId == dto.LanguageId));
+ }
+
+ // ReSharper disable once ClassNeverInstantiated.Local
+ // ReSharper disable UnusedAutoPropertyAccessor.Local
+ private class TempDto
+ {
+ public int NodeId { get; set; }
+ public int LanguageId { get; set; }
+ public string Name { get; set; }
+ }
+ // ReSharper restore UnusedAutoPropertyAccessor.Local
+ }
+}
diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs
index dd379e02f8..238d87b186 100644
--- a/src/Umbraco.Core/Models/Content.cs
+++ b/src/Umbraco.Core/Models/Content.cs
@@ -269,7 +269,7 @@ namespace Umbraco.Core.Models
if (_publishInfos == null)
_publishInfos = new Dictionary(StringComparer.OrdinalIgnoreCase);
- _publishInfos[culture] = (name, date);
+ _publishInfos[culture.ToLowerInvariant()] = (name, date);
}
private void ClearPublishInfos()
@@ -294,7 +294,7 @@ namespace Umbraco.Core.Models
throw new ArgumentNullOrEmptyException(nameof(culture));
if (_editedCultures == null)
_editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase);
- _editedCultures.Add(culture);
+ _editedCultures.Add(culture.ToLowerInvariant());
}
// sets all publish edited
diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs
index 56a31cd76d..bf2fd580d9 100644
--- a/src/Umbraco.Core/Models/ContentBase.cs
+++ b/src/Umbraco.Core/Models/ContentBase.cs
@@ -167,7 +167,7 @@ namespace Umbraco.Core.Models
}
///
- public DateTime? GetCultureDate(string culture)
+ public DateTime? GetUpdateDate(string culture)
{
if (culture.IsNullOrWhiteSpace()) return null;
if (!ContentTypeBase.VariesByCulture()) return null;
@@ -202,6 +202,12 @@ namespace Umbraco.Core.Models
}
}
+ internal void TouchCulture(string culture)
+ {
+ if (ContentTypeBase.VariesByCulture() && _cultureInfos != null && _cultureInfos.TryGetValue(culture, out var infos))
+ _cultureInfos[culture] = (infos.Name, DateTime.Now);
+ }
+
protected void ClearCultureInfos()
{
_cultureInfos = null;
diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentApp.cs b/src/Umbraco.Core/Models/ContentEditing/ContentApp.cs
new file mode 100644
index 0000000000..bf28c28c9e
--- /dev/null
+++ b/src/Umbraco.Core/Models/ContentEditing/ContentApp.cs
@@ -0,0 +1,72 @@
+using System.Runtime.Serialization;
+
+namespace Umbraco.Core.Models.ContentEditing
+{
+ ///
+ /// Represents a content app.
+ ///
+ ///
+ /// Content apps are editor extensions.
+ ///
+ [DataContract(Name = "app", Namespace = "")]
+ public class ContentApp
+ {
+ ///
+ /// Gets the name of the content app.
+ ///
+ [DataMember(Name = "name")]
+ public string Name { get; set; }
+
+ ///
+ /// Gets the unique alias of the content app.
+ ///
+ ///
+ /// Must be a valid javascript identifier, ie no spaces etc.
+ ///
+ [DataMember(Name = "alias")]
+ public string Alias { get; set; }
+
+ ///
+ /// Gets or sets the weight of the content app.
+ ///
+ ///
+ /// Content apps are ordered by weight, from left (lowest values) to right (highest values).
+ /// Some built-in apps have special weights: listview is -666, content is -100 and infos is +100.
+ /// The default weight is 0, meaning somewhere in-between content and infos, but weight could
+ /// be used for ordering between user-level apps, or anything really.
+ ///
+ [DataMember(Name = "weight")]
+ public int Weight { get; set; }
+
+ ///
+ /// Gets the icon of the content app.
+ ///
+ ///
+ /// Must be a valid helveticons class name (see http://hlvticons.ch/).
+ ///
+ [DataMember(Name = "icon")]
+ public string Icon { get; set; }
+
+ ///
+ /// Gets the view for rendering the content app.
+ ///
+ [DataMember(Name = "view")]
+ public string View { get; set; }
+
+ ///
+ /// The view model specific to this app
+ ///
+ [DataMember(Name = "viewModel")]
+ public object ViewModel { get; set; }
+
+ ///
+ /// Gets a value indicating whether the app is active.
+ ///
+ ///
+ /// Normally reserved for Angular to deal with but in some cases this can be set on the server side.
+ ///
+ [DataMember(Name = "active")]
+ public bool Active { get; set; }
+ }
+}
+
diff --git a/src/Umbraco.Core/Models/ContentEditing/IContentAppDefinition.cs b/src/Umbraco.Core/Models/ContentEditing/IContentAppDefinition.cs
new file mode 100644
index 0000000000..5e0c421742
--- /dev/null
+++ b/src/Umbraco.Core/Models/ContentEditing/IContentAppDefinition.cs
@@ -0,0 +1,20 @@
+namespace Umbraco.Core.Models.ContentEditing
+{
+ ///
+ /// Represents a content app definition.
+ ///
+ public interface IContentAppDefinition
+ {
+ ///
+ /// Gets the content app for an object.
+ ///
+ /// The source object.
+ /// The content app for the object, or null.
+ ///
+ /// The definition must determine, based on , whether
+ /// the content app should be displayed or not, and return either a
+ /// instance, or null.
+ ///
+ ContentApp GetContentAppFor(object source);
+ }
+}
diff --git a/src/Umbraco.Core/Models/DataType.cs b/src/Umbraco.Core/Models/DataType.cs
index 9668864588..4f0d0d6c31 100644
--- a/src/Umbraco.Core/Models/DataType.cs
+++ b/src/Umbraco.Core/Models/DataType.cs
@@ -30,6 +30,9 @@ namespace Umbraco.Core.Models
{
_editor = editor ?? throw new ArgumentNullException(nameof(editor));
ParentId = parentId;
+
+ // set a default configuration
+ Configuration = _editor.GetConfigurationEditor().DefaultConfigurationObject;
}
private static PropertySelectors Selectors => _selectors ?? (_selectors = new PropertySelectors());
diff --git a/src/Umbraco.Core/Models/Entities/DocumentEntitySlim.cs b/src/Umbraco.Core/Models/Entities/DocumentEntitySlim.cs
index 5b63ad81c5..39ece5fa10 100644
--- a/src/Umbraco.Core/Models/Entities/DocumentEntitySlim.cs
+++ b/src/Umbraco.Core/Models/Entities/DocumentEntitySlim.cs
@@ -1,4 +1,5 @@
using System.Collections.Generic;
+using System.Linq;
namespace Umbraco.Core.Models.Entities
{
@@ -8,13 +9,32 @@ namespace Umbraco.Core.Models.Entities
public class DocumentEntitySlim : ContentEntitySlim, IDocumentEntitySlim
{
private static readonly IReadOnlyDictionary Empty = new Dictionary();
+
private IReadOnlyDictionary _cultureNames;
+ private IEnumerable _publishedCultures;
+ private IEnumerable _editedCultures;
+
+ ///
public IReadOnlyDictionary CultureNames
{
get => _cultureNames ?? Empty;
set => _cultureNames = value;
}
+ ///
+ public IEnumerable PublishedCultures
+ {
+ get => _publishedCultures ?? Enumerable.Empty();
+ set => _publishedCultures = value;
+ }
+
+ ///
+ public IEnumerable EditedCultures
+ {
+ get => _editedCultures ?? Enumerable.Empty();
+ set => _editedCultures = value;
+ }
+
public ContentVariation Variations { get; set; }
///
@@ -22,5 +42,6 @@ namespace Umbraco.Core.Models.Entities
///
public bool Edited { get; set; }
+
}
}
diff --git a/src/Umbraco.Core/Models/Entities/IContentEntitySlim.cs b/src/Umbraco.Core/Models/Entities/IContentEntitySlim.cs
index dc986a4cd9..9ab557b02c 100644
--- a/src/Umbraco.Core/Models/Entities/IContentEntitySlim.cs
+++ b/src/Umbraco.Core/Models/Entities/IContentEntitySlim.cs
@@ -20,4 +20,4 @@
///
string ContentTypeThumbnail { get; }
}
-}
\ No newline at end of file
+}
diff --git a/src/Umbraco.Core/Models/Entities/IDocumentEntitySlim.cs b/src/Umbraco.Core/Models/Entities/IDocumentEntitySlim.cs
index 6b72fd4a2b..38fd9a02f1 100644
--- a/src/Umbraco.Core/Models/Entities/IDocumentEntitySlim.cs
+++ b/src/Umbraco.Core/Models/Entities/IDocumentEntitySlim.cs
@@ -7,26 +7,35 @@ namespace Umbraco.Core.Models.Entities
///
public interface IDocumentEntitySlim : IContentEntitySlim
{
- //fixme we need to supply more information than this and change this property name. This will need to include Published/Editor per variation since we need this information for the tree
+ ///
+ /// Gets the variant name for each culture
+ ///
IReadOnlyDictionary CultureNames { get; }
+ ///
+ /// Gets the published cultures.
+ ///
+ IEnumerable PublishedCultures { get; }
+
+ ///
+ /// Gets the edited cultures.
+ ///
+ IEnumerable EditedCultures { get; }
+
+ ///
+ /// Gets the content variation of the content type.
+ ///
ContentVariation Variations { get; }
///
- /// At least one variation is published
+ /// Gets a value indicating whether the content is published.
///
- ///
- /// If the document is invariant, this simply means there is a published version
- ///
- bool Published { get; set; }
+ bool Published { get; }
///
- /// At least one variation has pending changes
+ /// Gets a value indicating whether the content has been edited.
///
- ///
- /// If the document is invariant, this simply means there is pending changes
- ///
- bool Edited { get; set; }
+ bool Edited { get; }
}
}
diff --git a/src/Umbraco.Core/Models/IContentBase.cs b/src/Umbraco.Core/Models/IContentBase.cs
index de1b2666d5..460bd521d4 100644
--- a/src/Umbraco.Core/Models/IContentBase.cs
+++ b/src/Umbraco.Core/Models/IContentBase.cs
@@ -80,13 +80,13 @@ namespace Umbraco.Core.Models
bool IsCultureAvailable(string culture);
///
- /// Gets the date a culture was created.
+ /// Gets the date a culture was updated.
///
///
/// When is null, returns null.
/// If the specified culture is not available, returns null.
///
- DateTime? GetCultureDate(string culture);
+ DateTime? GetUpdateDate(string culture);
///
/// List of properties, which make up all the data available for this Content object
diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedElementModel.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedElementModel.cs
index e0514b38d7..882109f908 100644
--- a/src/Umbraco.Core/Models/PublishedContent/PublishedElementModel.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/PublishedElementModel.cs
@@ -6,7 +6,7 @@
///
/// Every strongly-typed property set class should inherit from PublishedElementModel
/// (or inherit from a class that inherits from... etc.) so they are picked by the factory.
- public class PublishedElementModel : PublishedElementWrapped
+ public abstract class PublishedElementModel : PublishedElementWrapped
{
///
///
diff --git a/src/Umbraco.Core/Persistence/Dtos/ContentVersionCultureVariationDto.cs b/src/Umbraco.Core/Persistence/Dtos/ContentVersionCultureVariationDto.cs
index a4e51b913e..1dca820a6c 100644
--- a/src/Umbraco.Core/Persistence/Dtos/ContentVersionCultureVariationDto.cs
+++ b/src/Umbraco.Core/Persistence/Dtos/ContentVersionCultureVariationDto.cs
@@ -10,7 +10,7 @@ namespace Umbraco.Core.Persistence.Dtos
internal class ContentVersionCultureVariationDto
{
public const string TableName = Constants.DatabaseSchema.Tables.ContentVersionCultureVariation;
- private int? _publishedUserId;
+ private int? _updateUserId;
[Column("id")]
[PrimaryKeyColumn]
@@ -33,16 +33,12 @@ namespace Umbraco.Core.Persistence.Dtos
[Column("name")]
public string Name { get; set; }
- [Column("date")]
- public DateTime Date { get; set; }
+ [Column("date")] // fixme: db rename to 'updateDate'
+ public DateTime UpdateDate { get; set; }
- // fixme want?
- [Column("availableUserId")]
+ [Column("availableUserId")] // fixme: db rename to 'updateDate'
[ForeignKey(typeof(UserDto))]
[NullSetting(NullSetting = NullSettings.Null)]
- public int? PublishedUserId { get => _publishedUserId == 0 ? null : _publishedUserId; set => _publishedUserId = value; } //return null if zero
-
- [Column("edited")]
- public bool Edited { get; set; }
+ public int? UpdateUserId { get => _updateUserId == 0 ? null : _updateUserId; set => _updateUserId = value; } //return null if zero
}
}
diff --git a/src/Umbraco.Core/Persistence/Dtos/ContentVersionDto.cs b/src/Umbraco.Core/Persistence/Dtos/ContentVersionDto.cs
index 3ec40c74b3..a13bf921d9 100644
--- a/src/Umbraco.Core/Persistence/Dtos/ContentVersionDto.cs
+++ b/src/Umbraco.Core/Persistence/Dtos/ContentVersionDto.cs
@@ -21,11 +21,11 @@ namespace Umbraco.Core.Persistence.Dtos
[ForeignKey(typeof(ContentDto))]
public int NodeId { get; set; }
- [Column("versionDate")]
+ [Column("versionDate")] // fixme: db rename to 'updateDate'
[Constraint(Default = SystemMethods.CurrentDateTime)]
public DateTime VersionDate { get; set; }
- [Column("userId")]
+ [Column("userId")] // fixme: db rename to 'updateUserId'
[ForeignKey(typeof(UserDto))]
[NullSetting(NullSetting = NullSettings.Null)]
public int? UserId { get => _userId == 0 ? null : _userId; set => _userId = value; } //return null if zero
diff --git a/src/Umbraco.Core/Persistence/Dtos/DocumentCultureVariationDto.cs b/src/Umbraco.Core/Persistence/Dtos/DocumentCultureVariationDto.cs
index 78e819e714..ed61ea5622 100644
--- a/src/Umbraco.Core/Persistence/Dtos/DocumentCultureVariationDto.cs
+++ b/src/Umbraco.Core/Persistence/Dtos/DocumentCultureVariationDto.cs
@@ -28,7 +28,25 @@ namespace Umbraco.Core.Persistence.Dtos
[Ignore]
public string Culture { get; set; }
+ // authority on whether a culture has been edited
[Column("edited")]
public bool Edited { get; set; }
+
+ // de-normalized for perfs
+ // (means there is a current content version culture variation for the language)
+ [Column("available")]
+ public bool Available { get; set; }
+
+ // de-normalized for perfs
+ // (means there is a published content version culture variation for the language)
+ [Column("published")]
+ public bool Published { get; set; }
+
+ // de-normalized for perfs
+ // (when available, copies name from current content version culture variation for the language)
+ // (otherwise, it's the published one, 'cos we need to have one)
+ [Column("name")]
+ [NullSetting(NullSetting = NullSettings.Null)]
+ public string Name { get; set; }
}
}
diff --git a/src/Umbraco.Core/Persistence/Dtos/NodeDto.cs b/src/Umbraco.Core/Persistence/Dtos/NodeDto.cs
index 10450f2bf4..56da821360 100644
--- a/src/Umbraco.Core/Persistence/Dtos/NodeDto.cs
+++ b/src/Umbraco.Core/Persistence/Dtos/NodeDto.cs
@@ -45,7 +45,7 @@ namespace Umbraco.Core.Persistence.Dtos
[Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Trashed")]
public bool Trashed { get; set; }
- [Column("nodeUser")] // fixme dbfix rename userId
+ [Column("nodeUser")] // fixme: db rename to 'createUserId'
[ForeignKey(typeof(UserDto))]
[NullSetting(NullSetting = NullSettings.Null)]
public int? UserId { get => _userId == 0 ? null : _userId; set => _userId = value; } //return null if zero
@@ -54,10 +54,10 @@ namespace Umbraco.Core.Persistence.Dtos
[NullSetting(NullSetting = NullSettings.Null)]
public string Text { get; set; }
- [Column("nodeObjectType")] // fixme dbfix rename objectType
+ [Column("nodeObjectType")] // fixme: db rename to 'objectType'
[NullSetting(NullSetting = NullSettings.Null)]
[Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ObjectType")]
- public Guid? NodeObjectType { get; set; } // fixme dbfix rename ObjectType
+ public Guid? NodeObjectType { get; set; }
[Column("createDate")]
[Constraint(Default = SystemMethods.CurrentDateTime)]
diff --git a/src/Umbraco.Core/Persistence/NPocoDatabaseTypeExtensions.cs b/src/Umbraco.Core/Persistence/NPocoDatabaseTypeExtensions.cs
index e40e6dedd3..bd44c095fa 100644
--- a/src/Umbraco.Core/Persistence/NPocoDatabaseTypeExtensions.cs
+++ b/src/Umbraco.Core/Persistence/NPocoDatabaseTypeExtensions.cs
@@ -22,6 +22,11 @@ namespace Umbraco.Core.Persistence
return databaseType is NPoco.DatabaseTypes.SqlServer2008DatabaseType;
}
+ public static bool IsSqlServer2012OrLater(this DatabaseType databaseType)
+ {
+ return databaseType is NPoco.DatabaseTypes.SqlServer2012DatabaseType;
+ }
+
public static bool IsSqlCe(this DatabaseType databaseType)
{
return databaseType is NPoco.DatabaseTypes.SqlServerCEDatabaseType;
diff --git a/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs b/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs
index 9c1f0d9a07..d97c748b6f 100644
--- a/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs
+++ b/src/Umbraco.Core/Persistence/NPocoSqlExtensions.cs
@@ -7,7 +7,6 @@ using System.Reflection;
using System.Text;
using NPoco;
using Umbraco.Core.Persistence.Querying;
-using Umbraco.Core.Persistence.SqlSyntax;
namespace Umbraco.Core.Persistence
{
@@ -74,12 +73,10 @@ namespace Umbraco.Core.Persistence
/// The Sql statement.
public static Sql Where(this Sql sql, Expression> predicate, string alias = null)
{
- var expresionist = new PocoToSqlExpressionVisitor(sql.SqlContext, alias);
- var whereExpression = expresionist.Visit(predicate);
- sql.Where(whereExpression, expresionist.GetSqlParameters());
- return sql;
+ var (s, a) = sql.SqlContext.Visit(predicate, alias);
+ return sql.Where(s, a);
}
-
+
///
/// Appends a WHERE clause to the Sql statement.
///
@@ -92,10 +89,8 @@ namespace Umbraco.Core.Persistence
/// The Sql statement.
public static Sql Where(this Sql sql, Expression> predicate, string alias1 = null, string alias2 = null)
{
- var expresionist = new PocoToSqlExpressionVisitor(sql.SqlContext, alias1, alias2);
- var whereExpression = expresionist.Visit(predicate);
- sql.Where(whereExpression, expresionist.GetSqlParameters());
- return sql;
+ var (s, a) = sql.SqlContext.Visit(predicate, alias1, alias2);
+ return sql.Where(s, a);
}
///
@@ -108,7 +103,7 @@ namespace Umbraco.Core.Persistence
/// The Sql statement.
public static Sql WhereIn(this Sql sql, Expression> field, IEnumerable values)
{
- var fieldName = GetFieldName(field, sql.SqlContext.SqlSyntax);
+ var fieldName = sql.SqlContext.SqlSyntax.GetFieldName(field);
sql.Where(fieldName + " IN (@values)", new { values });
return sql;
}
@@ -136,7 +131,7 @@ namespace Umbraco.Core.Persistence
/// The Sql statement.
public static Sql WhereNotIn(this Sql sql, Expression> field, IEnumerable values)
{
- var fieldName = GetFieldName(field, sql.SqlContext.SqlSyntax);
+ var fieldName = sql.SqlContext.SqlSyntax.GetFieldName(field);
sql.Where(fieldName + " NOT IN (@values)", new { values });
return sql;
}
@@ -164,7 +159,8 @@ namespace Umbraco.Core.Persistence
/// The Sql statement.
public static Sql WhereAnyIn(this Sql sql, Expression>[] fields, IEnumerable values)
{
- var fieldNames = fields.Select(x => GetFieldName(x, sql.SqlContext.SqlSyntax)).ToArray();
+ var sqlSyntax = sql.SqlContext.SqlSyntax;
+ var fieldNames = fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray();
var sb = new StringBuilder();
sb.Append("(");
for (var i = 0; i < fieldNames.Length; i++)
@@ -180,7 +176,7 @@ namespace Umbraco.Core.Persistence
private static Sql WhereIn(this Sql sql, Expression> fieldSelector, Sql valuesSql, bool not)
{
- var fieldName = GetFieldName(fieldSelector, sql.SqlContext.SqlSyntax);
+ var fieldName = sql.SqlContext.SqlSyntax.GetFieldName(fieldSelector);
sql.Where(fieldName + (not ? " NOT" : "") +" IN (" + valuesSql.SQL + ")", valuesSql.Arguments);
return sql;
}
@@ -274,7 +270,7 @@ namespace Umbraco.Core.Persistence
/// The Sql statement.
public static Sql OrderBy(this Sql sql, Expression> field)
{
- return sql.OrderBy("(" + GetFieldName(field, sql.SqlContext.SqlSyntax) + ")");
+ return sql.OrderBy("(" + sql.SqlContext.SqlSyntax.GetFieldName(field) + ")");
}
///
@@ -286,9 +282,10 @@ namespace Umbraco.Core.Persistence
/// The Sql statement.
public static Sql OrderBy(this Sql sql, params Expression>[] fields)
{
+ var sqlSyntax = sql.SqlContext.SqlSyntax;
var columns = fields.Length == 0
? sql.GetColumns(withAlias: false)
- : fields.Select(x => GetFieldName(x, sql.SqlContext.SqlSyntax)).ToArray();
+ : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray();
return sql.OrderBy(columns);
}
@@ -301,7 +298,7 @@ namespace Umbraco.Core.Persistence
/// The Sql statement.
public static Sql OrderByDescending(this Sql sql, Expression> field)
{
- return sql.OrderBy("(" + GetFieldName(field, sql.SqlContext.SqlSyntax) + ") DESC");
+ return sql.OrderBy("(" + sql.SqlContext.SqlSyntax.GetFieldName(field) + ") DESC");
}
///
@@ -313,9 +310,10 @@ namespace Umbraco.Core.Persistence
/// The Sql statement.
public static Sql OrderByDescending(this Sql sql, params Expression>[] fields)
{
+ var sqlSyntax = sql.SqlContext.SqlSyntax;
var columns = fields.Length == 0
? sql.GetColumns(withAlias: false)
- : fields.Select(x => GetFieldName(x, sql.SqlContext.SqlSyntax)).ToArray();
+ : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray();
return sql.OrderBy(columns.Select(x => x + " DESC"));
}
@@ -339,7 +337,7 @@ namespace Umbraco.Core.Persistence
/// The Sql statement.
public static Sql GroupBy(this Sql sql, Expression> field)
{
- return sql.GroupBy(GetFieldName(field, sql.SqlContext.SqlSyntax));
+ return sql.GroupBy(sql.SqlContext.SqlSyntax.GetFieldName(field));
}
///
@@ -351,9 +349,10 @@ namespace Umbraco.Core.Persistence
/// The Sql statement.
public static Sql GroupBy(this Sql sql, params Expression>[] fields)
{
+ var sqlSyntax = sql.SqlContext.SqlSyntax;
var columns = fields.Length == 0
? sql.GetColumns(withAlias: false)
- : fields.Select(x => GetFieldName(x, sql.SqlContext.SqlSyntax)).ToArray();
+ : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray();
return sql.GroupBy(columns);
}
@@ -366,9 +365,10 @@ namespace Umbraco.Core.Persistence
/// The Sql statement.
public static Sql AndBy(this Sql sql, params Expression>[] fields)
{
+ var sqlSyntax = sql.SqlContext.SqlSyntax;
var columns = fields.Length == 0
? sql.GetColumns(withAlias: false)
- : fields.Select(x => GetFieldName(x, sql.SqlContext.SqlSyntax)).ToArray();
+ : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray();
return sql.Append(", " + string.Join(", ", columns));
}
@@ -381,9 +381,10 @@ namespace Umbraco.Core.Persistence
/// The Sql statement.
public static Sql AndByDescending(this Sql sql, params Expression>[] fields)
{
+ var sqlSyntax = sql.SqlContext.SqlSyntax;
var columns = fields.Length == 0
? sql.GetColumns(withAlias: false)
- : fields.Select(x => GetFieldName(x, sql.SqlContext.SqlSyntax)).ToArray();
+ : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray();
return sql.Append(", " + string.Join(", ", columns.Select(x => x + " DESC")));
}
@@ -391,6 +392,23 @@ namespace Umbraco.Core.Persistence
#region Joins
+ ///
+ /// Appends a CROSS JOIN clause to the Sql statement.
+ ///
+ /// The type of the Dto.
+ /// The Sql statement.
+ /// An optional alias for the joined table.
+ /// The Sql statement.
+ public static Sql CrossJoin(this Sql sql, string alias = null)
+ {
+ var type = typeof(TDto);
+ var tableName = type.GetTableName();
+ var join = sql.SqlContext.SqlSyntax.GetQuotedTableName(tableName);
+ if (alias != null) join += " " + sql.SqlContext.SqlSyntax.GetQuotedTableName(alias);
+
+ return sql.Append("CROSS JOIN " + join);
+ }
+
///
/// Appends an INNER JOIN clause to the Sql statement.
///
@@ -532,6 +550,25 @@ namespace Umbraco.Core.Persistence
return sqlJoin.On(onExpression, expresionist.GetSqlParameters());
}
+ ///
+ /// Appends an ON clause to a SqlJoin statement.
+ ///
+ /// The type of Dto 1.
+ /// The type of Dto 2.
+ /// The type of Dto 3.
+ /// The SqlJoin statement.
+ /// A predicate to transform and use as the ON clause body.
+ /// An optional alias for Dto 1 table.
+ /// An optional alias for Dto 2 table.
+ /// An optional alias for Dto 3 table.
+ /// The Sql statement.
+ public static Sql On(this Sql.SqlJoinClause sqlJoin, Expression> predicate, string aliasLeft = null, string aliasRight = null, string aliasOther = null)
+ {
+ var expresionist = new PocoToSqlExpressionVisitor(sqlJoin.SqlContext, aliasLeft, aliasRight, aliasOther);
+ var onExpression = expresionist.Visit(predicate);
+ return sqlJoin.On(onExpression, expresionist.GetSqlParameters());
+ }
+
#endregion
#region Select
@@ -572,9 +609,10 @@ namespace Umbraco.Core.Persistence
public static Sql SelectCount(this Sql sql, params Expression>[] fields)
{
if (sql == null) throw new ArgumentNullException(nameof(sql));
+ var sqlSyntax = sql.SqlContext.SqlSyntax;
var columns = fields.Length == 0
? sql.GetColumns(withAlias: false)
- : fields.Select(x => GetFieldName(x, sql.SqlContext.SqlSyntax)).ToArray();
+ : fields.Select(x => sqlSyntax.GetFieldName(x)).ToArray();
return sql.Select("COUNT (" + string.Join(", ", columns) + ")");
}
@@ -906,7 +944,7 @@ namespace Umbraco.Core.Persistence
public SqlUpd Set(Expression> fieldSelector, object value)
{
- var fieldName = GetFieldName(fieldSelector, _sqlContext.SqlSyntax);
+ var fieldName = _sqlContext.SqlSyntax.GetFieldName(fieldSelector);
_setExpressions.Add(new Tuple(fieldName, value));
return this;
}
@@ -1062,17 +1100,6 @@ namespace Umbraco.Core.Persistence
return string.IsNullOrWhiteSpace(attr?.Name) ? column.Name : attr.Name;
}
- private static string GetFieldName(Expression> fieldSelector, ISqlSyntaxProvider sqlSyntax)
- {
- var field = ExpressionHelper.FindProperty(fieldSelector).Item1 as PropertyInfo;
- var fieldName = field.GetColumnName();
-
- var type = typeof (TDto);
- var tableName = type.GetTableName();
-
- return sqlSyntax.GetQuotedTableName(tableName) + "." + sqlSyntax.GetQuotedColumnName(fieldName);
- }
-
internal static void WriteToConsole(this Sql sql)
{
Console.WriteLine(sql.SQL);
diff --git a/src/Umbraco.Core/Persistence/Querying/PocoToSqlExpressionVisitor.cs b/src/Umbraco.Core/Persistence/Querying/PocoToSqlExpressionVisitor.cs
index 4d33977c72..971b65c220 100644
--- a/src/Umbraco.Core/Persistence/Querying/PocoToSqlExpressionVisitor.cs
+++ b/src/Umbraco.Core/Persistence/Querying/PocoToSqlExpressionVisitor.cs
@@ -167,4 +167,93 @@ namespace Umbraco.Core.Persistence.Querying
}
}
+ ///
+ /// Represents an expression tree parser used to turn strongly typed expressions into SQL statements.
+ ///
+ /// The type of DTO 1.
+ /// The type of DTO 2.
+ /// The type of DTO 3.
+ /// This visitor is stateful and cannot be reused.
+ internal class PocoToSqlExpressionVisitor : ExpressionVisitorBase
+ {
+ private readonly PocoData _pocoData1, _pocoData2, _pocoData3;
+ private readonly string _alias1, _alias2, _alias3;
+ private string _parameterName1, _parameterName2, _parameterName3;
+
+ public PocoToSqlExpressionVisitor(ISqlContext sqlContext, string alias1, string alias2, string alias3)
+ : base(sqlContext.SqlSyntax)
+ {
+ _pocoData1 = sqlContext.PocoDataFactory.ForType(typeof(TDto1));
+ _pocoData2 = sqlContext.PocoDataFactory.ForType(typeof(TDto2));
+ _pocoData3 = sqlContext.PocoDataFactory.ForType(typeof(TDto3));
+ _alias1 = alias1;
+ _alias2 = alias2;
+ _alias3 = alias3;
+ }
+
+ protected override string VisitLambda(LambdaExpression lambda)
+ {
+ if (lambda.Parameters.Count == 3)
+ {
+ _parameterName1 = lambda.Parameters[0].Name;
+ _parameterName2 = lambda.Parameters[1].Name;
+ _parameterName3 = lambda.Parameters[2].Name;
+ }
+ else if (lambda.Parameters.Count == 2)
+ {
+ _parameterName1 = lambda.Parameters[0].Name;
+ _parameterName2 = lambda.Parameters[1].Name;
+ }
+ else
+ {
+ _parameterName1 = _parameterName2 = null;
+ }
+ return base.VisitLambda(lambda);
+ }
+
+ protected override string VisitMemberAccess(MemberExpression m)
+ {
+ if (m.Expression != null)
+ {
+ if (m.Expression.NodeType == ExpressionType.Parameter)
+ {
+ var pex = (ParameterExpression)m.Expression;
+
+ if (pex.Name == _parameterName1)
+ return Visited ? string.Empty : GetFieldName(_pocoData1, m.Member.Name, _alias1);
+
+ if (pex.Name == _parameterName2)
+ return Visited ? string.Empty : GetFieldName(_pocoData2, m.Member.Name, _alias2);
+
+ if (pex.Name == _parameterName3)
+ return Visited ? string.Empty : GetFieldName(_pocoData3, m.Member.Name, _alias3);
+ }
+ else if (m.Expression.NodeType == ExpressionType.Convert)
+ {
+ // here: which _pd should we use?!
+ throw new NotSupportedException();
+ //return Visited ? string.Empty : GetFieldName(_pd, m.Member.Name);
+ }
+ }
+
+ var member = Expression.Convert(m, typeof(object));
+ var lambda = Expression.Lambda>(member);
+ var getter = lambda.Compile();
+ var o = getter();
+
+ SqlParameters.Add(o);
+
+ // execute if not already compiled
+ return Visited ? string.Empty : "@" + (SqlParameters.Count - 1);
+ }
+
+ protected virtual string GetFieldName(PocoData pocoData, string name, string alias)
+ {
+ var column = pocoData.Columns.FirstOrDefault(x => x.Value.MemberInfoData.Name == name);
+ var tableName = SqlSyntax.GetQuotedTableName(alias ?? pocoData.TableInfo.TableName);
+ var columnName = SqlSyntax.GetQuotedColumnName(column.Value.ColumnName);
+
+ return tableName + "." + columnName;
+ }
+ }
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs
index fea0a61589..f7341d112b 100644
--- a/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/IContentRepository.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using Umbraco.Core.Models.Entities;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Umbraco.Core.Persistence.Querying;
+using Umbraco.Core.Services;
namespace Umbraco.Core.Persistence.Repositories
{
@@ -67,7 +68,8 @@ namespace Umbraco.Core.Persistence.Repositories
///
/// Gets paged content items.
///
+ /// Here, can be null but cannot.
IEnumerable GetPage(IQuery query, long pageIndex, int pageSize, out long totalRecords,
- string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter = null);
+ IQuery filter, Ordering ordering);
}
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/ITemplateRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ITemplateRepository.cs
index 83d4148689..4bab445bee 100644
--- a/src/Umbraco.Core/Persistence/Repositories/ITemplateRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/ITemplateRepository.cs
@@ -18,25 +18,6 @@ namespace Umbraco.Core.Persistence.Repositories
IEnumerable GetDescendants(int masterTemplateId);
IEnumerable GetDescendants(string alias);
- ///
- /// Returns a template as a template node which can be traversed (parent, children)
- ///
- ///
- ///
- [Obsolete("Use GetDescendants instead")]
- [EditorBrowsable(EditorBrowsableState.Never)]
- TemplateNode GetTemplateNode(string alias);
-
- ///
- /// Given a template node in a tree, this will find the template node with the given alias if it is found in the hierarchy, otherwise null
- ///
- ///
- ///
- ///
- [Obsolete("Use GetDescendants instead")]
- [EditorBrowsable(EditorBrowsableState.Never)]
- TemplateNode FindTemplateInTree(TemplateNode anyNode, string alias);
-
///
/// This checks what the default rendering engine is set in config but then also ensures that there isn't already
/// a template that exists in the opposite rendering engine's template folder, then returns the appropriate
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs
index 2e0139aa30..6ace73fbc3 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs
@@ -232,16 +232,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
#endregion
- public abstract IEnumerable GetPage(IQuery query, long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection, bool orderBySystemField, IQuery filter = null);
-
- // sql: the main sql
- // filterSql: a filtering ? fixme different from v7?
- // orderBy: the name of an ordering field
- // orderDirection: direction for orderBy
- // orderBySystemField: whether orderBy is a system field or a custom field (property value)
- private Sql PrepareSqlForPage(Sql sql, Sql filterSql, string orderBy, Direction orderDirection, bool orderBySystemField)
+ private Sql PreparePageSql(Sql sql, Sql filterSql, Ordering ordering)
{
- if (filterSql == null && string.IsNullOrEmpty(orderBy)) return sql;
+ // non-filtering, non-ordering = nothing to do
+ if (filterSql == null && ordering.IsEmpty) return sql;
// preserve original
var psql = new Sql(sql.SqlContext, sql.SQL, sql.Arguments);
@@ -251,74 +245,131 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
psql.Append(filterSql);
// non-sorting, we're done
- if (string.IsNullOrEmpty(orderBy))
+ if (ordering.IsEmpty)
return psql;
- // else apply sort
- var dbfield = orderBySystemField
- ? GetOrderBySystemField(ref psql, orderBy)
- : GetOrderByNonSystemField(ref psql, orderBy);
-
- if (orderDirection == Direction.Ascending)
- psql.OrderBy(dbfield);
- else
- psql.OrderByDescending(dbfield);
+ // else apply ordering
+ ApplyOrdering(ref psql, ordering);
// no matter what we always MUST order the result also by umbracoNode.id to ensure that all records being ordered by are unique.
// if we do not do this then we end up with issues where we are ordering by a field that has duplicate values (i.e. the 'text' column
// is empty for many nodes) - see: http://issues.umbraco.org/issue/U4-8831
- dbfield = GetDatabaseFieldNameForOrderBy("umbracoNode", "id");
- if (orderBySystemField == false || orderBy.InvariantEquals(dbfield) == false)
+ var dbfield = GetQuotedFieldName("umbracoNode", "id");
+ (dbfield, _) = SqlContext.Visit(x => x.NodeId); // fixme?!
+ if (ordering.IsCustomField || !ordering.OrderBy.InvariantEquals("id"))
{
- // get alias, if aliased
- var matches = SqlContext.SqlSyntax.AliasRegex.Matches(sql.SQL);
- var match = matches.Cast().FirstOrDefault(m => m.Groups[1].Value.InvariantEquals(dbfield));
- if (match != null) dbfield = match.Groups[2].Value;
-
- // add field
- psql.OrderBy(dbfield);
+ psql.OrderBy(GetAliasedField(dbfield, sql)); // fixme why aliased?
}
// create prepared sql
// ensure it's single-line as NPoco PagingHelper has issues with multi-lines
- psql = new Sql(psql.SqlContext, psql.SQL.ToSingleLine(), psql.Arguments);
+ psql = Sql(psql.SQL.ToSingleLine(), psql.Arguments);
return psql;
}
- private string GetOrderBySystemField(ref Sql sql, string orderBy)
+ private void ApplyOrdering(ref Sql sql, Ordering ordering)
{
- // get the database field eg "[table].[column]"
- var dbfield = GetDatabaseFieldNameForOrderBy(orderBy);
+ if (sql == null) throw new ArgumentNullException(nameof(sql));
+ if (ordering == null) throw new ArgumentNullException(nameof(ordering));
- // for SqlServer pagination to work, the "order by" field needs to be the alias eg if
- // the select statement has "umbracoNode.text AS NodeDto__Text" then the order field needs
- // to be "NodeDto__Text" and NOT "umbracoNode.text".
- // not sure about SqlCE nor MySql, so better do it too. initially thought about patching
- // NPoco but that would be expensive and not 100% possible, so better give NPoco proper
- // queries to begin with.
- // thought about maintaining a map of columns-to-aliases in the sql context but that would
- // be expensive and most of the time, useless. so instead we parse the SQL looking for the
- // alias. somewhat expensive too but nothing's free.
+ var orderBy = ordering.IsCustomField
+ ? ApplyCustomOrdering(ref sql, ordering)
+ : ApplySystemOrdering(ref sql, ordering);
- // note: ContentTypeAlias is not properly managed because it's not part of the query to begin with!
+ // beware! NPoco paging code parses the query to isolate the ORDER BY fragment,
+ // using a regex that wants "([\w\.\[\]\(\)\s""`,]+)" - meaning that anything
+ // else in orderBy is going to break NPoco / not be detected
- // get alias, if aliased
- var matches = SqlContext.SqlSyntax.AliasRegex.Matches(sql.SQL);
- var match = matches.Cast().FirstOrDefault(m => m.Groups[1].Value.InvariantEquals(dbfield));
- if (match != null) dbfield = match.Groups[2].Value;
+ // beware! NPoco paging code (in PagingHelper) collapses everything [foo].[bar]
+ // to [bar] only, so we MUST use aliases, cannot use [table].[field]
- return dbfield;
+ // beware! pre-2012 SqlServer is using a convoluted syntax for paging, which
+ // includes "SELECT ROW_NUMBER() OVER (ORDER BY ...) poco_rn FROM SELECT (...",
+ // so anything added here MUST also be part of the inner SELECT statement, ie
+ // the original statement, AND must be using the proper alias, as the inner SELECT
+ // will hide the original table.field names entirely
+
+ if (ordering.Direction == Direction.Ascending)
+ sql.OrderBy(orderBy);
+ else
+ sql.OrderByDescending(orderBy);
}
- private string GetOrderByNonSystemField(ref Sql sql, string orderBy)
+ protected virtual string ApplySystemOrdering(ref Sql sql, Ordering ordering)
+ {
+ // id is invariant
+ if (ordering.OrderBy.InvariantEquals("id"))
+ return GetAliasedField(SqlSyntax.GetFieldName(x => x.NodeId), sql);
+
+ // sort order is invariant
+ if (ordering.OrderBy.InvariantEquals("sortOrder"))
+ return GetAliasedField(SqlSyntax.GetFieldName(x => x.SortOrder), sql);
+
+ // path is invariant
+ if (ordering.OrderBy.InvariantEquals("path"))
+ return GetAliasedField(SqlSyntax.GetFieldName(x => x.Path), sql);
+
+ // note: 'owner' is the user who created the item as a whole,
+ // we don't have an 'owner' per culture (should we?)
+ if (ordering.OrderBy.InvariantEquals("owner"))
+ {
+ var joins = Sql()
+ .InnerJoin("ownerUser").On((node, user) => node.UserId == user.Id, aliasRight: "ownerUser");
+
+ // see notes in ApplyOrdering: the field MUST be selected + aliased
+ sql = Sql(InsertBefore(sql, "FROM", ", " + SqlSyntax.GetFieldName(x => x.UserName, "ownerUser") + " AS ordering "), sql.Arguments);
+
+ sql = InsertJoins(sql, joins);
+
+ return "ordering";
+ }
+
+ // note: each version culture variation has a date too,
+ // maybe we would want to use it instead?
+ if (ordering.OrderBy.InvariantEquals("versionDate") || ordering.OrderBy.InvariantEquals("updateDate"))
+ return GetAliasedField(SqlSyntax.GetFieldName(x => x.VersionDate), sql);
+
+ // create date is invariant (we don't keep each culture's creation date)
+ if (ordering.OrderBy.InvariantEquals("createDate"))
+ return GetAliasedField(SqlSyntax.GetFieldName(x => x.CreateDate), sql);
+
+ // name is variant
+ if (ordering.OrderBy.InvariantEquals("name"))
+ {
+ // no culture = can only work on the invariant name
+ // see notes in ApplyOrdering: the field MUST be aliased
+ if (ordering.Culture.IsNullOrWhiteSpace())
+ return GetAliasedField(SqlSyntax.GetFieldName(x => x.Text), sql);
+
+ // culture = must work on variant name ?? invariant name
+ // insert proper join and return coalesced ordering field
+
+ var joins = Sql()
+ .LeftJoin(nested =>
+ nested.InnerJoin("lang").On((ccv, lang) => ccv.LanguageId == lang.Id && lang.IsoCode == ordering.Culture, "ccv", "lang"), "ccv")
+ .On((version, ccv) => version.Id == ccv.VersionId, aliasRight: "ccv");
+
+ // see notes in ApplyOrdering: the field MUST be selected + aliased
+ sql = Sql(InsertBefore(sql, "FROM", ", " + SqlContext.Visit((ccv, node) => ccv.Name ?? node.Text, "ccv").Sql + " AS ordering "), sql.Arguments);
+
+ sql = InsertJoins(sql, joins);
+
+ return "ordering";
+ }
+
+ // previously, we'd accept anything and just sanitize it - not anymore
+ throw new NotSupportedException($"Ordering by {ordering.OrderBy} not supported.");
+ }
+
+ private string ApplyCustomOrdering(ref Sql sql, Ordering ordering)
{
// sorting by a custom field, so set-up sub-query for ORDER BY clause to pull through value
// from 'current' content version for the given order by field
var sortedInt = string.Format(SqlContext.SqlSyntax.ConvertIntegerToOrderableString, "intValue");
+ var sortedDecimal = string.Format(SqlContext.SqlSyntax.ConvertDecimalToOrderableString, "decimalValue");
var sortedDate = string.Format(SqlContext.SqlSyntax.ConvertDateToOrderableString, "dateValue");
var sortedString = "COALESCE(varcharValue,'')"; // assuming COALESCE is ok for all syntaxes
- var sortedDecimal = string.Format(SqlContext.SqlSyntax.ConvertDecimalToOrderableString, "decimalValue");
// needs to be an outer join since there's no guarantee that any of the nodes have values for this property
var innerSql = Sql().Select($@"CASE
@@ -329,49 +380,60 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
END AS customPropVal,
cver.nodeId AS customPropNodeId")
.From("cver")
- .InnerJoin("opdata").On((left, right) => left.Id == right.Id, "cver", "opdata")
- .InnerJoin("optype").On((left, right) => left.PropertyTypeId == right.Id, "opdata", "optype")
+ .InnerJoin("opdata")
+ .On((version, pdata) => version.Id == pdata.VersionId, "cver", "opdata")
+ .InnerJoin("optype").On((pdata, ptype) => pdata.PropertyTypeId == ptype.Id, "opdata", "optype")
+ .LeftJoin().On((pdata, lang) => pdata.LanguageId == lang.Id, "opdata")
.Where(x => x.Current, "cver") // always query on current (edit) values
- .Where(x => x.Alias == "", "optype");
+ .Where(x => x.Alias == ordering.OrderBy, "optype")
+ .Where((opdata, lang) => opdata.LanguageId == null || lang.IsoCode == ordering.Culture, "opdata");
- // @0 is for x.Current ie 'true' = 1
- // @1 is for x.Alias
- var innerSqlString = innerSql.SQL.Replace("@0", "1").Replace("@1", "@" + sql.Arguments.Length);
+ // merge arguments
+ var argsList = sql.Arguments.ToList();
+ var innerSqlString = ParameterHelper.ProcessParams(innerSql.SQL, innerSql.Arguments, argsList);
+
+ // create the outer join complete sql fragment
var outerJoinTempTable = $@"LEFT OUTER JOIN ({innerSqlString}) AS customPropData
ON customPropData.customPropNodeId = {Constants.DatabaseSchema.Tables.Node}.id "; // trailing space is important!
- // insert this just above the last WHERE
- var pos = sql.SQL.InvariantIndexOf("WHERE");
- if (pos < 0) throw new Exception("Oops, WHERE not found.");
- var newSql = sql.SQL.Insert(pos, outerJoinTempTable);
+ // insert this just above the first WHERE
+ var newSql = InsertBefore(sql.SQL, "WHERE", outerJoinTempTable);
- var newArgs = sql.Arguments.ToList();
- newArgs.Add(orderBy);
+ // see notes in ApplyOrdering: the field MUST be selected + aliased
+ newSql = InsertBefore(newSql, "FROM", ", customPropData.customPropVal AS ordering "); // trailing space is important!
- // insert the SQL selected field, too, else ordering cannot work
- if (sql.SQL.StartsWith("SELECT ") == false) throw new Exception("Oops: SELECT not found.");
- newSql = newSql.Insert("SELECT ".Length, "customPropData.customPropVal, ");
-
- sql = new Sql(sql.SqlContext, newSql, newArgs.ToArray());
+ // create the new sql
+ sql = Sql(newSql, argsList.ToArray());
// and order by the custom field
- return "customPropData.customPropVal";
+ // this original code means that an ascending sort would first expose all NULL values, ie items without a value
+ return "ordering";
+
+ // note: adding an extra sorting criteria on
+ // "(CASE WHEN customPropData.customPropVal IS NULL THEN 1 ELSE 0 END")
+ // would ensure that items without a value always come last, both in ASC and DESC-ending sorts
}
+ public abstract IEnumerable GetPage(IQuery query,
+ long pageIndex, int pageSize, out long totalRecords,
+ IQuery filter,
+ Ordering ordering);
+
+ // here, filter can be null and ordering cannot
protected IEnumerable GetPage(IQuery query,
long pageIndex, int pageSize, out long totalRecords,
Func, IEnumerable> mapDtos,
- string orderBy, Direction orderDirection, bool orderBySystemField,
- Sql filterSql = null) // fixme filter is different on v7?
+ Sql filter,
+ Ordering ordering)
{
- if (orderBy == null) throw new ArgumentNullException(nameof(orderBy));
+ if (ordering == null) throw new ArgumentNullException(nameof(ordering));
// start with base query, and apply the supplied IQuery
- if (query == null) query = AmbientScope.SqlContext.Query();
+ if (query == null) query = Query();
var sql = new SqlTranslator(GetBaseQuery(QueryType.Many), query).Translate();
// sort and filter
- sql = PrepareSqlForPage(sql, filterSql, orderBy, orderDirection, orderBySystemField);
+ sql = PreparePageSql(sql, filter, ordering);
// get a page of DTOs and the total count
var pagedResult = Database.Page(pageIndex + 1, pageSize, sql);
@@ -498,36 +560,48 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
return result;
}
- protected virtual string GetDatabaseFieldNameForOrderBy(string orderBy)
- {
- // translate the supplied "order by" field, which were originally defined for in-memory
- // object sorting of ContentItemBasic instance, to the actual database field names.
+ protected string InsertBefore(Sql s, string atToken, string insert)
+ => InsertBefore(s.SQL, atToken, insert);
- switch (orderBy.ToUpperInvariant())
- {
- case "VERSIONDATE":
- case "UPDATEDATE":
- return GetDatabaseFieldNameForOrderBy(Constants.DatabaseSchema.Tables.ContentVersion, "versionDate");
- case "CREATEDATE":
- return GetDatabaseFieldNameForOrderBy("umbracoNode", "createDate");
- case "NAME":
- return GetDatabaseFieldNameForOrderBy("umbracoNode", "text");
- case "PUBLISHED":
- return GetDatabaseFieldNameForOrderBy(Constants.DatabaseSchema.Tables.Document, "published");
- case "OWNER":
- //TODO: This isn't going to work very nicely because it's going to order by ID, not by letter
- return GetDatabaseFieldNameForOrderBy("umbracoNode", "nodeUser");
- case "PATH":
- return GetDatabaseFieldNameForOrderBy("umbracoNode", "path");
- case "SORTORDER":
- return GetDatabaseFieldNameForOrderBy("umbracoNode", "sortOrder");
- default:
- //ensure invalid SQL cannot be submitted
- return Regex.Replace(orderBy, @"[^\w\.,`\[\]@-]", "");
- }
+ protected string InsertBefore(string s, string atToken, string insert)
+ {
+ var pos = s.InvariantIndexOf(atToken);
+ if (pos < 0) throw new Exception($"Could not find token \"{atToken}\".");
+ return s.Insert(pos, insert);
}
- protected string GetDatabaseFieldNameForOrderBy(string tableName, string fieldName)
+ protected Sql InsertJoins(Sql sql, Sql joins)
+ {
+ var joinsSql = joins.SQL;
+ var args = sql.Arguments;
+
+ // merge args if any
+ if (joins.Arguments.Length > 0)
+ {
+ var argsList = args.ToList();
+ joinsSql = ParameterHelper.ProcessParams(joinsSql, joins.Arguments, argsList);
+ args = argsList.ToArray();
+ }
+
+ return Sql(InsertBefore(sql.SQL, "WHERE", joinsSql), args);
+ }
+
+ private string GetAliasedField(string field, Sql sql)
+ {
+ // get alias, if aliased
+ //
+ // regex looks for pattern "([\w+].[\w+]) AS ([\w+])" ie "(field) AS (alias)"
+ // and, if found & a group's field matches the field name, returns the alias
+ //
+ // so... if query contains "[umbracoNode].[nodeId] AS [umbracoNode__nodeId]"
+ // then GetAliased for "[umbracoNode].[nodeId]" returns "[umbracoNode__nodeId]"
+
+ var matches = SqlContext.SqlSyntax.AliasRegex.Matches(sql.SQL);
+ var match = matches.Cast().FirstOrDefault(m => m.Groups[1].Value.InvariantEquals(field));
+ return match == null ? field : match.Groups[2].Value;
+ }
+
+ protected string GetQuotedFieldName(string tableName, string fieldName)
{
return SqlContext.SqlSyntax.GetQuotedTableName(tableName) + "." + SqlContext.SqlSyntax.GetQuotedColumnName(fieldName);
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs
index 28c0b0dec6..fb1a33eb62 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs
@@ -8,13 +8,12 @@ using Umbraco.Core.Exceptions;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Membership;
-using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Umbraco.Core.Persistence.Dtos;
using Umbraco.Core.Persistence.Factories;
using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Persistence.SqlSyntax;
using Umbraco.Core.Scoping;
-using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics;
+using Umbraco.Core.Services;
namespace Umbraco.Core.Persistence.Repositories.Implement
{
@@ -504,8 +503,17 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
// names also impact 'edited'
foreach (var (culture, name) in content.CultureNames)
if (name != content.GetPublishName(culture))
+ {
+ edited = true;
(editedCultures ?? (editedCultures = new HashSet(StringComparer.OrdinalIgnoreCase))).Add(culture);
+ // fixme - change tracking
+ // at the moment, we don't do any dirty tracking on property values, so we don't know whether the
+ // culture has just been edited or not, so we don't update its update date - that date only changes
+ // when the name is set, and it all works because the controller does it - but, if someone uses a
+ // service to change a property value and save (without setting name), the update date does not change.
+ }
+
// replace the content version variations (rather than updating)
// only need to delete for the version that existed, the new version (if any) has no property data yet
var deleteContentVariations = Sql().Delete().Where(x => x.VersionId == versionToDelete);
@@ -673,13 +681,10 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
PermissionRepository.Save(permission);
}
- ///
- /// Gets paged content results.
- ///
+ ///
public override IEnumerable GetPage(IQuery query,
- long pageIndex, int pageSize, out long totalRecords,
- string orderBy, Direction orderDirection, bool orderBySystemField,
- IQuery filter = null)
+ long pageIndex, int pageSize, out long totalRecords,
+ IQuery filter, Ordering ordering)
{
Sql filterSql = null;
@@ -692,8 +697,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
return GetPage(query, pageIndex, pageSize, out totalRecords,
x => MapDtosToContent(x),
- orderBy, orderDirection, orderBySystemField,
- filterSql);
+ filterSql,
+ ordering);
}
public bool IsPathPublished(IContent content)
@@ -814,25 +819,59 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
#endregion
- protected override string GetDatabaseFieldNameForOrderBy(string orderBy)
+ protected override string ApplySystemOrdering(ref Sql sql, Ordering ordering)
{
- // NOTE see sortby.prevalues.controller.js for possible values
- // that need to be handled here or in VersionableRepositoryBase
-
- //Some custom ones
- switch (orderBy.ToUpperInvariant())
+ // note: 'updater' is the user who created the latest draft version,
+ // we don't have an 'updater' per culture (should we?)
+ if (ordering.OrderBy.InvariantEquals("updater"))
{
- case "UPDATER":
- // fixme orders by id not letter = bad
- return GetDatabaseFieldNameForOrderBy(Constants.DatabaseSchema.Tables.ContentVersion, "userId");
- case "PUBLISHED":
- // fixme kill
- return GetDatabaseFieldNameForOrderBy(Constants.DatabaseSchema.Tables.Document, "published");
- case "CONTENTTYPEALIAS":
- throw new NotSupportedException("Don't know how to support ContentTypeAlias.");
+ var joins = Sql()
+ .InnerJoin("updaterUser").On((version, user) => version.UserId == user.Id, aliasRight: "updaterUser");
+
+ // see notes in ApplyOrdering: the field MUST be selected + aliased
+ sql = Sql(InsertBefore(sql, "FROM", SqlSyntax.GetFieldName(x => x.UserName, "updaterUser") + " AS ordering"), sql.Arguments);
+
+ sql = InsertJoins(sql, joins);
+
+ return "ordering";
}
- return base.GetDatabaseFieldNameForOrderBy(orderBy);
+ if (ordering.OrderBy.InvariantEquals("published"))
+ {
+ // no culture = can only work on the global 'published' flag
+ if (ordering.Culture.IsNullOrWhiteSpace())
+ {
+ // see notes in ApplyOrdering: the field MUST be selected + aliased, and we cannot have
+ // the whole CASE fragment in ORDER BY due to it not being detected by NPoco
+ sql = Sql(InsertBefore(sql, "FROM", ", (CASE WHEN pcv.id IS NULL THEN 0 ELSE 1 END) AS ordering "), sql.Arguments);
+ return "ordering";
+ }
+
+ // invariant: left join will yield NULL and we must use pcv to determine published
+ // variant: left join may yield NULL or something, and that determines published
+
+ var joins = Sql()
+ .InnerJoin("ctype").On((content, contentType) => content.ContentTypeId == contentType.NodeId, aliasRight: "ctype")
+ .LeftJoin(nested =>
+ nested.InnerJoin("lang").On((ccv, lang) => ccv.LanguageId == lang.Id && lang.IsoCode == ordering.Culture, "ccv", "lang"), "ccv")
+ .On((pcv, ccv) => pcv.Id == ccv.VersionId, "pcv", "ccv"); // join on *published* content version
+
+ sql = InsertJoins(sql, joins);
+
+ // see notes in ApplyOrdering: the field MUST be selected + aliased, and we cannot have
+ // the whole CASE fragment in ORDER BY due to it not being detected by NPoco
+ var sqlText = InsertBefore(sql.SQL, "FROM",
+
+ // when invariant, ie 'variations' does not have the culture flag (value 1), use the global 'published' flag on pcv.id,
+ // otherwise check if there's a version culture variation for the lang, via ccv.id
+ ", (CASE WHEN (ctype.variations & 1) = 0 THEN (CASE WHEN pcv.id IS NULL THEN 0 ELSE 1 END) ELSE (CASE WHEN ccv.id IS NULL THEN 0 ELSE 1 END) END) AS ordering "); // trailing space is important!
+
+ sql = Sql(sqlText, sql.Arguments);
+
+ return "ordering";
+ }
+
+ return base.ApplySystemOrdering(ref sql, ordering);
}
private IEnumerable MapDtosToContent(List dtos, bool withCache = false)
@@ -1001,7 +1040,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
{
Culture = LanguageRepository.GetIsoCodeById(dto.LanguageId),
Name = dto.Name,
- Date = dto.Date
+ Date = dto.UpdateDate
});
}
@@ -1046,7 +1085,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."),
Culture = culture,
Name = name,
- Date = content.GetCultureDate(culture) ?? DateTime.MinValue // we *know* there is a value
+ UpdateDate = content.GetUpdateDate(culture) ?? DateTime.MinValue // we *know* there is a value
};
// if not publishing, we're just updating the 'current' (non-published) version,
@@ -1061,22 +1100,28 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."),
Culture = culture,
Name = name,
- Date = content.GetPublishDate(culture) ?? DateTime.MinValue // we *know* there is a value
+ UpdateDate = content.GetPublishDate(culture) ?? DateTime.MinValue // we *know* there is a value
};
}
private IEnumerable GetDocumentVariationDtos(IContent content, bool publishing, HashSet editedCultures)
{
- foreach (var (culture, name) in content.CultureNames)
+ var allCultures = content.AvailableCultures.Union(content.PublishedCultures); // union = distinct
+ foreach (var culture in allCultures)
yield return new DocumentCultureVariationDto
{
NodeId = content.Id,
LanguageId = LanguageRepository.GetIdByIsoCode(culture) ?? throw new InvalidOperationException("Not a valid culture."),
Culture = culture,
- // if not published, always edited
- // no need to check for availability: it *is* available since it is in content.CultureNames
- Edited = !content.IsCulturePublished(culture) || (editedCultures != null && editedCultures.Contains(culture))
+ Name = content.GetCultureName(culture) ?? content.GetPublishName(culture),
+
+ // note: can't use IsCultureEdited at that point - hasn't been updated yet - see PersistUpdatedItem
+
+ Available = content.IsCultureAvailable(culture),
+ Published = content.IsCulturePublished(culture),
+ Edited = content.IsCultureAvailable(culture) &&
+ (!content.IsCulturePublished(culture) || (editedCultures != null && editedCultures.Contains(culture)))
};
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs
index 340eecb538..fb8c2732e6 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/EntityRepository.cs
@@ -9,6 +9,7 @@ using Umbraco.Core.Persistence.Dtos;
using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Scoping;
using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics;
+using Umbraco.Core.Persistence.SqlSyntax;
namespace Umbraco.Core.Persistence.Repositories.Implement
{
@@ -34,6 +35,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
protected IUmbracoDatabase Database => _scopeAccessor.AmbientScope.Database;
protected Sql Sql() => _scopeAccessor.AmbientScope.SqlContext.Sql();
+ protected ISqlSyntaxProvider SqlSyntax => _scopeAccessor.AmbientScope.SqlContext.SqlSyntax;
#region Repository
@@ -57,83 +59,12 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
//fixme - we should be able to do sql = sql.OrderBy(x => Alias(x.NodeId, "NodeId")); but we can't because the OrderBy extension don't support Alias currently
sql = sql.OrderBy("NodeId");
- //IEnumerable result;
- //
- //if (isMedia)
- //{
- // //Treat media differently for now, as an Entity it will be returned with ALL of it's properties in the AdditionalData bag!
- // var pagedResult = UnitOfWork.Database.Page(pageIndex + 1, pageSize, pagedSql);
-
- // var ids = pagedResult.Items.Select(x => (int)x.id).InGroupsOf(2000);
- // var entities = pagedResult.Items.Select(BuildEntityFromDynamic).Cast().ToList();
-
- // //Now we need to merge in the property data since we need paging and we can't do this the way that the big media query was working before
- // foreach (var idGroup in ids)
- // {
- // var propSql = GetPropertySql(Constants.ObjectTypes.Media)
- // .WhereIn(x => x.NodeId, idGroup)
- // .OrderBy(x => x.NodeId);
-
- // //This does NOT fetch all data into memory in a list, this will read
- // // over the records as a data reader, this is much better for performance and memory,
- // // but it means that during the reading of this data set, nothing else can be read
- // // from SQL server otherwise we'll get an exception.
- // var allPropertyData = UnitOfWork.Database.Query(propSql);
-
- // //keep track of the current property data item being enumerated
- // var propertyDataSetEnumerator = allPropertyData.GetEnumerator();
- // var hasCurrent = false; // initially there is no enumerator.Current
-
- // try
- // {
- // //This must be sorted by node id (which is done by SQL) because this is how we are sorting the query to lookup property types above,
- // // which allows us to more efficiently iterate over the large data set of property values.
- // foreach (var entity in entities)
- // {
- // // assemble the dtos for this def
- // // use the available enumerator.Current if any else move to next
- // while (hasCurrent || propertyDataSetEnumerator.MoveNext())
- // {
- // if (propertyDataSetEnumerator.Current.nodeId == entity.Id)
- // {
- // hasCurrent = false; // enumerator.Current is not available
-
- // //the property data goes into the additional data
- // entity.AdditionalData[propertyDataSetEnumerator.Current.propertyTypeAlias] = new UmbracoEntity.EntityProperty
- // {
- // PropertyEditorAlias = propertyDataSetEnumerator.Current.propertyEditorAlias,
- // Value = StringExtensions.IsNullOrWhiteSpace(propertyDataSetEnumerator.Current.textValue)
- // ? propertyDataSetEnumerator.Current.varcharValue
- // : StringExtensions.ConvertToJsonIfPossible(propertyDataSetEnumerator.Current.textValue)
- // };
- // }
- // else
- // {
- // hasCurrent = true; // enumerator.Current is available for another def
- // break; // no more propertyDataDto for this def
- // }
- // }
- // }
- // }
- // finally
- // {
- // propertyDataSetEnumerator.Dispose();
- // }
- // }
-
- // result = entities;
- //}
- //else
- //{
- // var pagedResult = UnitOfWork.Database.Page(pageIndex + 1, pageSize, pagedSql);
- // result = pagedResult.Items.Select(BuildEntityFromDynamic).Cast().ToList();
- //}
-
var page = Database.Page(pageIndex + 1, pageSize, sql);
var dtos = page.Items;
var entities = dtos.Select(x => BuildEntity(isContent, isMedia, x)).ToArray();
-
- //TODO: For isContent will we need to build up the variation info?
+
+ if (isContent)
+ BuildVariants(entities.Cast());
if (isMedia)
BuildProperties(entities, dtos);
@@ -154,11 +85,9 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
//isContent is going to return a 1:M result now with the variants so we need to do different things
if (isContent)
{
- var dtos = Database.FetchOneToMany(
- ddto => ddto.VariationInfo,
- ddto => ddto.VersionId,
- sql);
- return dtos.Count == 0 ? null : BuildDocumentEntity(dtos[0]);
+ var cdtos = Database.Fetch(sql);
+
+ return cdtos.Count == 0 ? null : BuildVariants(BuildDocumentEntity(cdtos[0]));
}
var dto = Database.FirstOrDefault(sql);
@@ -216,13 +145,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
//isContent is going to return a 1:M result now with the variants so we need to do different things
if (isContent)
{
- var cdtos = Database.FetchOneToMany(
- dto => dto.VariationInfo,
- dto => dto.VersionId,
- sql);
+ var cdtos = Database.Fetch(sql);
+
return cdtos.Count == 0
? Enumerable.Empty()
- : cdtos.Select(BuildDocumentEntity).ToArray();
+ : BuildVariants(cdtos.Select(BuildDocumentEntity)).ToList();
}
var dtos = Database.Fetch(sql);
@@ -323,7 +250,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
private void BuildProperties(EntitySlim[] entities, List dtos)
{
- var versionIds = dtos.Select(x => x.VersionId).Distinct().ToArray();
+ var versionIds = dtos.Select(x => x.VersionId).Distinct().ToList();
var pdtos = Database.FetchByGroups(versionIds, 2000, GetPropertyData);
var xentity = entities.ToDictionary(x => x.Id, x => x); // nodeId -> entity
@@ -346,10 +273,70 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
entity.AdditionalData[pdto.PropertyTypeDto.Alias] = new EntitySlim.PropertySlim(pdto.PropertyTypeDto.DataTypeDto.EditorAlias, value);
}
+ private DocumentEntitySlim BuildVariants(DocumentEntitySlim entity)
+ => BuildVariants(new[] { entity }).First();
+
+ private IEnumerable BuildVariants(IEnumerable entities)
+ {
+ List v = null;
+ var entitiesList = entities.ToList();
+ foreach (var e in entitiesList)
+ {
+ if (e.Variations.VariesByCulture())
+ (v ?? (v = new List())).Add(e);
+ }
+
+ if (v == null) return entitiesList;
+
+ // fetch all variant info dtos
+ var dtos = Database.FetchByGroups(v.Select(x => x.Id), 2000, GetVariantInfos);
+
+ // group by node id (each group contains all languages)
+ var xdtos = dtos.GroupBy(x => x.NodeId).ToDictionary(x => x.Key, x => x);
+
+ foreach (var e in v)
+ {
+ // since we're only iterating on entities that vary, we must have something
+ var edtos = xdtos[e.Id];
+
+ e.CultureNames = edtos.Where(x => x.CultureAvailable).ToDictionary(x => x.IsoCode, x => x.Name);
+ e.PublishedCultures = edtos.Where(x => x.CulturePublished).Select(x => x.IsoCode);
+ e.EditedCultures = edtos.Where(x => x.CultureAvailable && x.CultureEdited).Select(x => x.IsoCode);
+ }
+
+ return entitiesList;
+ }
+
#endregion
#region Sql
+ protected Sql GetVariantInfos(IEnumerable ids)
+ {
+ return Sql()
+ .Select(x => x.NodeId)
+ .AndSelect(x => x.IsoCode)
+ .AndSelect("doc", x => Alias(x.Published, "DocumentPublished"), x => Alias(x.Edited, "DocumentEdited"))
+ .AndSelect("dcv",
+ x => Alias(x.Available, "CultureAvailable"), x => Alias(x.Published, "CulturePublished"), x => Alias(x.Edited, "CultureEdited"),
+ x => Alias(x.Name, "Name"))
+
+ // from node x language
+ .From()
+ .CrossJoin()
+
+ // join to document - always exists - indicates global document published/edited status
+ .InnerJoin("doc")
+ .On((node, doc) => node.NodeId == doc.NodeId, aliasRight: "doc")
+
+ // left-join do document variation - matches cultures that are *available* + indicates when *edited*
+ .LeftJoin("dcv")
+ .On((node, dcv, lang) => node.NodeId == dcv.NodeId && lang.Id == dcv.LanguageId, aliasRight: "dcv")
+
+ // for selected nodes
+ .WhereIn(x => x.NodeId, ids);
+ }
+
// gets the full sql for a given object type and a given unique id
protected Sql GetFullSqlForEntityType(bool isContent, bool isMedia, Guid objectType, Guid uniqueId)
{
@@ -371,24 +358,6 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
return AddGroupBy(isContent, isMedia, sql);
}
- // fixme kill this nonsense
- //// gets the SELECT + FROM + WHERE sql
- //// to get all property data for all items of the specified object type
- //private Sql GetPropertySql(Guid objectType)
- //{
- // return Sql()
- // .Select(x => x.VersionId, x => x.TextValue, x => x.VarcharValue)
- // .AndSelect(x => x.NodeId)
- // .AndSelect(x => x.PropertyEditorAlias)
- // .AndSelect(x => Alias(x.Alias, "propertyTypeAlias"))
- // .From()
- // .InnerJoin().On((left, right) => left.VersionId == right.Id)
- // .InnerJoin().On((left, right) => left.NodeId == right.NodeId)
- // .InnerJoin().On(dto => dto.PropertyTypeId, dto => dto.Id)
- // .InnerJoin().On(dto => dto.DataTypeId, dto => dto.DataTypeId)
- // .Where(x => x.NodeObjectType == objectType);
- //}
-
private Sql GetPropertyData(int versionId)
{
return Sql()
@@ -408,89 +377,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
.InnerJoin().On((left, right) => left.DataTypeId == right.NodeId)
.WhereIn(x => x.VersionId, versionIds)
.OrderBy(x => x.VersionId);
- }
-
- // fixme - wtf is this?
- //private Sql GetFullSqlForMedia(Sql entitySql, Action