diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 6e421da5d7..96014f65b7 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -1,4 +1,4 @@
-_Looking for Umbraco version 8? [Click here](https://github.com/umbraco/Umbraco-CMS/blob/temp8/docs/CONTRIBUTING.md) to go to the v8 branch_
+_Looking for Umbraco version 8? [Click here](https://github.com/umbraco/Umbraco-CMS/blob/temp8/.github/V8_GETTING_STARTED.md) to go to the v8 branch_
# Contributing to Umbraco CMS
👍🎉 First off, thanks for taking the time to contribute! 🎉👍
@@ -16,9 +16,9 @@ This document gives you a quick overview on how to get started, we will link to
## Guidelines for contributions we welcome
-Not all changes are wanted so on occassion we might close a PR without merging it. We will give you feedback why we can't accept your changes and we'll be nice about it, thanking you for spending your valueable time.
+Not all changes are wanted, so on occassion we might close a PR without merging it. We will give you feedback why we can't accept your changes and we'll be nice about it, thanking you for spending your valueable time.
-We have [documented what we consider small and large changes](CONTRIBUTION_GUIDELINES.md), make sure to talk to us before making large changes.
+We have [documented what we consider small and large changes](CONTRIBUTION_GUIDELINES.md). Make sure to talk to us before making large changes.
Remember, if an issue is in the `Up for grabs` list or you've asked for some feedback before you sent us a PR, your PR will not be closed as unwanted.
@@ -36,7 +36,7 @@ Great question! The short version goes like this:
* **Build** - build your fork of Umbraco locally as described in [building Umbraco from source code](BUILD.md)
* **Change** - make your changes, experiment, have fun, explore and learn, and don't be afraid. We welcome all contributions and will [happily give feedback](#questions)
- * **Commit** - done? Yay! 🎉 It is recommended to create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-U4-12345`. This means it's a temporary branch for the particular issue you're working on, in this case `U4-12345`
+ * **Commit** - done? Yay! 🎉 It is recommended to create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-12345`. This means it's a temporary branch for the particular issue you're working on, in this case `12345`
* **Push** - great, now you can push the changes up to your fork on GitHub
* **Create pull request** - exciting! You're ready to show us your changes (or not quite ready, you just need some feedback to progress). GitHub has picked up on the new branch you've pushed and will offer to create a Pull Request. Click that green button and away you go.
@@ -72,7 +72,6 @@ The pull request team consists of a member of Umbraco HQ, [Sebastiaan](https://g
- [Anders Bjerner](https://github.com/abjerner)
- [Dave Woestenborghs](https://github.com/dawoe)
- [Emma Burstow](https://github.com/emmaburstow)
-- [Kyle Weems](https://github.com/cssquirrel)
- [Poornima Nayar](https://github.com/poornimanayar)
These wonderful volunteers will provide you with a first reply to your PR, review and test out your changes and might ask more questions. After that they'll let Umbraco HQ know if everything seems okay.
diff --git a/.github/CONTRIBUTING_DETAILED.md b/.github/CONTRIBUTING_DETAILED.md
index a07539da6a..8c2bfffd87 100644
--- a/.github/CONTRIBUTING_DETAILED.md
+++ b/.github/CONTRIBUTING_DETAILED.md
@@ -19,13 +19,13 @@ When contributing code to Umbraco there's plenty of things you'll want to know,
* [What branch should I target for my contributions?](#what-branch-should-i-target-for-my-contributions)
* [Building Umbraco from source code](#building-umbraco-from-source-code)
* [Keeping your Umbraco fork in sync with the main repository](#keeping-your-umbraco-fork-in-sync-with-the-main-repository)
-
+
## How Can I Contribute?
### Reporting Bugs
This section guides you through submitting a bug report for Umbraco CMS. Following these guidelines helps maintainers and the community understand your report 📝, reproduce the behavior 💻 💻, and find related reports 🔎.
-Before creating bug reports, please check [this list](#before-submitting-a-bug-report) as you might find out that you don't need to create one. When you are creating a bug report, please [include as many details as possible](#how-do-i-submit-a-good-bug-report). Fill out [the required template](http://issues.umbraco.org/issues#newissue=61-30118), the information it asks for helps us resolve issues faster.
+Before creating bug reports, please check [this list](#before-submitting-a-bug-report) as you might find out that you don't need to create one. When you are creating a bug report, please [include as many details as possible](#how-do-i-submit-a-good-bug-report). Fill out [the required template](https://github.com/umbraco/Umbraco-CMS/issues/new/choose), the information it asks for helps us resolve issues faster.
> **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one.
@@ -52,7 +52,7 @@ Provide more context by answering these questions:
Include details about your configuration and environment:
- * **Which version of Umbraco are you using?**
+ * **Which version of Umbraco are you using?**
* **What is the environment you're using Umbraco in?** Is this a problem on your local machine or on a server. Tell us about your configuration: Windows version, IIS/IISExpress, database type, etc.
* **Which packages do you have installed?**
@@ -80,7 +80,7 @@ The most successful pull requests usually look a like this:
* Unit tests, while optional are awesome, thank you!
* New code is commented with documentation from which [the reference documentation](https://our.umbraco.com/documentation/Reference/) is generated
-Again, these are guidelines, not strict requirements.
+Again, these are guidelines, not strict requirements.
## Making changes after the PR was opened
@@ -90,7 +90,7 @@ If you make the corrections we ask for in the same branch and push them to your
To be honest, we don't like rules very much. We trust you have the best of intentions and we encourage you to create working code. If it doesn't look perfect then we'll happily help clean it up.
-That said, the Umbraco development team likes to follow the hints that ReSharper gives us (no problem if you don't have this installed) and we've added a `.editorconfig` file so that Visual Studio knows what to do with whitespace, line endings, etc.
+That said, the Umbraco development team likes to follow the hints that ReSharper gives us (no problem if you don't have this installed) and we've added a `.editorconfig` file so that Visual Studio knows what to do with whitespace, line endings, etc.
## What should I know before I get started?
@@ -125,6 +125,12 @@ We like to use [Gitflow as much as possible](https://jeffkreeftmeijer.com/git-fl
### Building Umbraco from source code
+In order to build the Umbraco source code locally, first make sure you have the following installed.
+
+ * Visual Studio 2017 v15.3+
+ * Node v10+ (Installed via `build.bat` script. If you already have it installed, make sure you're running at least v10)
+ * npm v6.4.1+ (Installed via `build.bat` script. If you already have it installed, make sure you're running at least v6.4.1)
+
The easiest way to get started is to run `build.bat` which will build both the backoffice (also known as "Belle") and the Umbraco core. You can then easily start debugging from Visual Studio, or if you need to debug Belle you can run `gulp dev` in `src\Umbraco.Web.UI.Client`. See [this page](BUILD.md) for more details.
Alternatively, you can open `src\umbraco.sln` in Visual Studio 2017 (version 15.3 or higher, [the community edition is free](https://www.visualstudio.com/thank-you-downloading-visual-studio/?sku=Community&rel=15) for you to use to contribute to Open Source projects). In Visual Studio, find the Task Runner Explorer (in the View menu under Other Windows) and run the build task under the gulpfile.
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 6275d161dc..8cb9017518 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,11 +1,12 @@
### Prerequisites
-- [ ] I have [created an issue](https://github.com/umbraco/Umbraco-CMS/issues) for the proposed changes in this PR, the link is:
- [ ] I have added steps to test this contribution in the description below
+If there's an existing issue for this PR then this fixes:
+
### Description
-
-
+
+
diff --git a/.github/README.md b/.github/README.md
index cf29f4e527..5a1340006e 100644
--- a/.github/README.md
+++ b/.github/README.md
@@ -15,7 +15,7 @@ Once a track is done, we start releasing previews where we ask people to test th
Umbraco CMS
===========
-The friendliest, most flexible and fastest growing ASP.NET CMS used by more than 443,000 websites worldwide: [https://umbraco.com](https://umbraco.com)
+The friendliest, most flexible and fastest growing ASP.NET CMS, and used by more than 443,000 websites worldwide: [https://umbraco.com](https://umbraco.com)
[](https://vimeo.com/172382998/)
@@ -28,34 +28,34 @@ Umbraco is a free open source Content Management System built on the ASP.NET pla
## Umbraco - The Friendly CMS
-For the first time on the Microsoft platform, there is a free user and developer friendly CMS that makes it quick and easy to create websites - or a breeze to build complex web applications. Umbraco has award-winning integration capabilities and supports ASP.NET MVC or Web Forms, including User and Custom Controls, out of the box.
+For the first time on the Microsoft platform, there is a free user- and developer-friendly CMS that makes it quick and easy to create websites - and a breeze to build complex web applications. Umbraco has award-winning integration capabilities and supports ASP.NET MVC or Web Forms, including User and Custom Controls, right out of the box.
-Umbraco is not only loved by developers, but is a content editors dream. Enjoy intuitive editing tools, media management, responsive views and approval workflows to send your content live.
+Umbraco is not only loved by developers, but is a content editor's dream. Enjoy intuitive editing tools, media management, responsive views, and approval workflows to send your content live.
-Used by more than 443,000 active websites including Carlsberg, Segway, Amazon and Heinz and **The Official ASP.NET and IIS.NET website from Microsoft** ([https://asp.net](https://asp.net) / [https://iis.net](https://iis.net)), you can be sure that the technology is proven, stable and scales. Backed by the team at Umbraco HQ, and supported by a dedicated community of over 220,000 craftspeople globally, you can trust that Umbraco is a safe choice and is here to stay.
+Used by more than 443,000 active websites including Carlsberg, Segway, Amazon and Heinz and **The Official ASP.NET and IIS.NET website from Microsoft** ([https://asp.net](https://asp.net) / [https://iis.net](https://iis.net)), you can be sure that the technology is proven, stable and scalable. Backed by the team at Umbraco HQ, and supported by a dedicated community of over 220,000 craftspeople globally, you can trust that Umbraco is a safe choice and is here to stay.
To view more examples, please visit [https://umbraco.com/case-studies-testimonials/](https://umbraco.com/case-studies-testimonials/)
## Why Open Source?
-As an Open Source platform, Umbraco is more than just a CMS. We are transparent with our roadmap for future versions, our incremental sprint planning notes are publicly accessible and community contributions and packages are available for all to use.
+As an Open Source platform, Umbraco is more than just a CMS. We are transparent with our roadmap for future versions, our incremental sprint planning notes are publicly accessible, and community contributions and packages are available for all to use.
## Trying out Umbraco CMS
-[Umbraco Cloud](https://umbraco.com/cloud) is the easiest and fastest way to use Umbraco yet with full support for all your custom .NET code and intergrations. You're up and running in less than a minute and your life will be made easier with automated upgrades and a built-in deployment engine. We offer a free 14 day trial, no credit card needed.
+[Umbraco Cloud](https://umbraco.com/cloud) is the easiest and fastest way to use Umbraco yet, with full support for all your custom .NET code and integrations. You're up and running in less than a minute, and your life will be made easier with automated upgrades and a built-in deployment engine. We offer a free 14-day trial, no credit card needed.
-If you want to DIY you can [download Umbraco](https://our.umbraco.com/download) either as a ZIP file or via NuGet. It's the same version of Umbraco CMS that powers Umbraco Cloud, but you'll need to find a place to host yourself and handling deployments and upgrades is all down to you.
+If you want to DIY, you can [download Umbraco](https://our.umbraco.com/download) either as a ZIP file or via NuGet. It's the same version of Umbraco CMS that powers Umbraco Cloud, but you'll need to find a place to host it yourself, and handling deployments and upgrades will be all up to you.
## Community
-Our friendly community is available 24/7 at the community hub we call ["Our Umbraco"](https://our.umbraco.com). Our Umbraco feature forums for questions and answers, documentation, downloadable plugins for Umbraco and a rich collection of community resources.
+Our friendly community is available 24/7 at the community hub we call ["Our Umbraco"](https://our.umbraco.com). Our Umbraco features forums for questions and answers, documentation, downloadable plugins for Umbraco, and a rich collection of community resources.
## Contribute to Umbraco
-Umbraco is contribution focused and community driven. If you want to contribute back to Umbraco please check out our [guide to contributing](CONTRIBUTING.md).
+Umbraco is contribution-focused and community-driven. If you want to contribute back to Umbraco, please check out our [guide to contributing](CONTRIBUTING.md).
## Found a bug?
Another way you can contribute to Umbraco is by providing issue reports. For information on how to submit an issue report refer to our [online guide for reporting issues](CONTRIBUTING_DETAILED.md#reporting-bugs).
You can comment and report issues on the [github issue tracker](https://github.com/umbraco/Umbraco-CMS/issues).
-Since [September 2018](https://umbraco.com/blog/a-second-take-on-umbraco-issue-tracker-hello-github-issues/) the old issue tracker is in read only mode, but can still be found at [http://issues.umbraco.org](http://issues.umbraco.org).
+Since [September 2018](https://umbraco.com/blog/a-second-take-on-umbraco-issue-tracker-hello-github-issues/), the old issue tracker is in read-only mode, but can still be found at [http://issues.umbraco.org](http://issues.umbraco.org).
diff --git a/.github/V8_GETTING_STARTED.md b/.github/V8_GETTING_STARTED.md
index 62b376b0e7..1cc33bb126 100644
--- a/.github/V8_GETTING_STARTED.md
+++ b/.github/V8_GETTING_STARTED.md
@@ -33,5 +33,5 @@ We recommend running the site with the Visual Studio since you'll be able to rem
We are keeping track of [known issues and limitations here](http://issues.umbraco.org/issue/U4-11279). These line items will eventually be turned into actual tasks to be worked on. Feel free to help us keep this list updated if you find issues and even help fix some of these items. If there is a particular item you'd like to help fix please mention this on the task and we'll create a sub task for the item to continue discussion there.
-There's [a list of tasks for v8 that haven't been completed](https://issues.umbraco.org/issues?q=&project=U4&tagValue=&release=8.0.0&issueType=&resolvedState=open&search=search). If you are interested in helping out with any of these please mention this on the task. This list will be constantly updated as we begin to document and design some of the other tasks that still need to get done.
+There's [a list of tasks for v8 that haven't been completed](https://github.com/umbraco/Umbraco-CMS/labels/release%2F8.0.0). If you are interested in helping out with any of these please mention this on the task. This list will be constantly updated as we begin to document and design some of the other tasks that still need to get done.
diff --git a/build/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec
index daa0018668..dd565aa1d4 100644
--- a/build/NuSpecs/UmbracoCms.Core.nuspec
+++ b/build/NuSpecs/UmbracoCms.Core.nuspec
@@ -36,7 +36,7 @@
-
+
diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec
index e9bd8ca6ea..adf090c69b 100644
--- a/build/NuSpecs/UmbracoCms.Web.nuspec
+++ b/build/NuSpecs/UmbracoCms.Web.nuspec
@@ -25,7 +25,7 @@
-
+
diff --git a/build/NuSpecs/tools/Dashboard.config.install.xdt b/build/NuSpecs/tools/Dashboard.config.install.xdt
index 036beeba29..a81af8c365 100644
--- a/build/NuSpecs/tools/Dashboard.config.install.xdt
+++ b/build/NuSpecs/tools/Dashboard.config.install.xdt
@@ -3,7 +3,7 @@
-
+
views/dashboard/settings/settingsdashboardintro.html
@@ -14,7 +14,7 @@
forms
-
+
views/dashboard/forms/formsdashboardintro.html
@@ -28,7 +28,7 @@
-
+
views/dashboard/developer/developerdashboardvideos.html
@@ -47,7 +47,7 @@
-
+
views/dashboard/media/mediafolderbrowser.html
@@ -56,7 +56,7 @@
-
+
views/dashboard/members/membersdashboardvideos.html
@@ -92,4 +92,4 @@
-
\ No newline at end of file
+
diff --git a/build/build.ps1 b/build/build.ps1
index 8548cbb1ac..1066c62876 100644
--- a/build/build.ps1
+++ b/build/build.ps1
@@ -43,14 +43,6 @@
$release = "" + $semver.Major + "." + $semver.Minor + "." + $semver.Patch
- Write-Host "Update UmbracoVersion.cs"
- $this.ReplaceFileText("$($this.SolutionRoot)\src\Umbraco.Core\Configuration\UmbracoVersion.cs", `
- "(\d+)\.(\d+)\.(\d+)(.(\d+))?", `
- "$release")
- $this.ReplaceFileText("$($this.SolutionRoot)\src\Umbraco.Core\Configuration\UmbracoVersion.cs", `
- "CurrentComment => `"(.+)`"", `
- "CurrentComment => `"$($semver.PreRelease)`"")
-
Write-Host "Update IIS Express port in csproj"
$updater = New-Object "Umbraco.Build.ExpressPortUpdater"
$csproj = "$($this.SolutionRoot)\src\Umbraco.Web.UI\Umbraco.Web.UI.csproj"
@@ -69,7 +61,7 @@
$global:node_nodepath = $this.ClearEnvVar("NODEPATH")
$global:node_npmcache = $this.ClearEnvVar("NPM_CONFIG_CACHE")
$global:node_npmprefix = $this.ClearEnvVar("NPM_CONFIG_PREFIX")
-
+
# https://github.com/gruntjs/grunt-contrib-connect/issues/235
$this.SetEnvVar("NODE_NO_HTTP2", "1")
})
@@ -81,7 +73,7 @@
$this.SetEnvVar("NODEPATH", $node_nodepath)
$this.SetEnvVar("NPM_CONFIG_CACHE", $node_npmcache)
$this.SetEnvVar("NPM_CONFIG_PREFIX", $node_npmprefix)
-
+
$ignore = $this.ClearEnvVar("NODE_NO_HTTP2")
})
@@ -434,7 +426,7 @@
Write-Host "Prepare Azure Gallery"
$this.CopyFile("$($this.SolutionRoot)\build\Azure\azuregalleryrelease.ps1", $this.BuildOutput)
})
-
+
$ubuild.DefineMethod("Build",
{
$error.Clear()
diff --git a/src/ApiDocs/umbracotemplate/partials/footer.tmpl.partial b/src/ApiDocs/umbracotemplate/partials/footer.tmpl.partial
index 69f51a101f..7aac413bfd 100644
--- a/src/ApiDocs/umbracotemplate/partials/footer.tmpl.partial
+++ b/src/ApiDocs/umbracotemplate/partials/footer.tmpl.partial
@@ -7,7 +7,7 @@
Back to top
- Copyright © 2016 Umbraco
Generated by DocFX
+ Copyright © 2016-present Umbraco
Generated by DocFX
diff --git a/src/Umbraco.Core/Collections/ConcurrentHashSet.cs b/src/Umbraco.Core/Collections/ConcurrentHashSet.cs
index 4cad6e9f15..54367ed588 100644
--- a/src/Umbraco.Core/Collections/ConcurrentHashSet.cs
+++ b/src/Umbraco.Core/Collections/ConcurrentHashSet.cs
@@ -70,10 +70,7 @@ namespace Umbraco.Core.Collections
/// The number of elements contained in the .
///
/// 2
- public int Count
- {
- get { return GetThreadSafeClone().Count; }
- }
+ public int Count => GetThreadSafeClone().Count;
///
/// Gets a value indicating whether the is read-only.
@@ -81,10 +78,7 @@ namespace Umbraco.Core.Collections
///
/// true if the is read-only; otherwise, false.
///
- public bool IsReadOnly
- {
- get { return false; }
- }
+ public bool IsReadOnly => false;
///
/// Adds an item to the .
diff --git a/src/Umbraco.Core/Composing/CompositionRoots/ConfigurationCompositionRoot.cs b/src/Umbraco.Core/Composing/CompositionRoots/ConfigurationCompositionRoot.cs
index 82912163b6..80de6d1c35 100644
--- a/src/Umbraco.Core/Composing/CompositionRoots/ConfigurationCompositionRoot.cs
+++ b/src/Umbraco.Core/Composing/CompositionRoots/ConfigurationCompositionRoot.cs
@@ -16,6 +16,7 @@ namespace Umbraco.Core.Composing.CompositionRoots
container.Register(factory => factory.GetInstance().Templates);
container.Register(factory => factory.GetInstance().RequestHandler);
container.Register(factory => UmbracoConfig.For.GlobalSettings());
+ container.Register(factory => UmbracoConfig.For.DashboardSettings());
// fixme - other sections we need to add?
}
diff --git a/src/Umbraco.Core/Composing/TypeFinder.cs b/src/Umbraco.Core/Composing/TypeFinder.cs
index a42b84e0c5..9604f599cf 100644
--- a/src/Umbraco.Core/Composing/TypeFinder.cs
+++ b/src/Umbraco.Core/Composing/TypeFinder.cs
@@ -210,53 +210,55 @@ namespace Umbraco.Core.Composing
/// NOTE the comma vs period... comma delimits the name in an Assembly FullName property so if it ends with comma then its an exact name match
/// NOTE this means that "foo." will NOT exclude "foo.dll" but only "foo.*.dll"
///
- internal static readonly string[] KnownAssemblyExclusionFilter = new[]
- {
- "mscorlib,",
- "System.",
- "Antlr3.",
- "Autofac.",
- "Autofac,",
- "Castle.",
- "ClientDependency.",
- "DataAnnotationsExtensions.",
- "DataAnnotationsExtensions,",
- "Dynamic,",
- "HtmlDiff,",
- "Iesi.Collections,",
- "Microsoft.",
- "Newtonsoft.",
- "NHibernate.",
- "NHibernate,",
- "NuGet.",
- "RouteDebugger,",
- "SqlCE4Umbraco,",
- "umbraco.datalayer,",
- "umbraco.interfaces,",
- //"umbraco.providers,",
- //"Umbraco.Web.UI,",
- "umbraco.webservices",
- "Lucene.",
- "Examine,",
- "Examine.",
- "ServiceStack.",
- "MySql.",
- "HtmlAgilityPack.",
- "TidyNet.",
- "ICSharpCode.",
- "CookComputing.",
- "AutoMapper,",
- "AutoMapper.",
- "AzureDirectory,",
- "itextsharp,",
- "UrlRewritingNet.",
- "HtmlAgilityPack,",
- "MiniProfiler,",
- "Moq,",
- "nunit.framework,",
- "TidyNet,",
- "WebDriver,"
- };
+ internal static readonly string[] KnownAssemblyExclusionFilter = {
+ "Antlr3.",
+ "AutoMapper,",
+ "AutoMapper.",
+ "Autofac,", // DI
+ "Autofac.",
+ "AzureDirectory,",
+ "Castle.", // DI, tests
+ "ClientDependency.",
+ "CookComputing.",
+ "CSharpTest.", // BTree for NuCache
+ "DataAnnotationsExtensions,",
+ "DataAnnotationsExtensions.",
+ "Dynamic,",
+ "Examine,",
+ "Examine.",
+ "HtmlAgilityPack,",
+ "HtmlAgilityPack.",
+ "HtmlDiff,",
+ "ICSharpCode.",
+ "Iesi.Collections,", // used by NHibernate
+ "LightInject.", // DI
+ "LightInject,",
+ "Lucene.",
+ "Markdown,",
+ "Microsoft.",
+ "MiniProfiler,",
+ "Moq,",
+ "MySql.",
+ "NHibernate,",
+ "NHibernate.",
+ "Newtonsoft.",
+ "NPoco,",
+ "NuGet.",
+ "RouteDebugger,",
+ "Semver.",
+ "Serilog.",
+ "Serilog,",
+ "ServiceStack.",
+ "SqlCE4Umbraco,",
+ "Superpower,", // used by Serilog
+ "System.",
+ "TidyNet,",
+ "TidyNet.",
+ "WebDriver,",
+ "itextsharp,",
+ "mscorlib,",
+ "nunit.framework,",
+ };
///
/// Finds any classes derived from the type T that contain the attribute TAttribute
diff --git a/src/Umbraco.Core/Composing/TypeLoader.cs b/src/Umbraco.Core/Composing/TypeLoader.cs
index 304638e017..f79c288e91 100644
--- a/src/Umbraco.Core/Composing/TypeLoader.cs
+++ b/src/Umbraco.Core/Composing/TypeLoader.cs
@@ -520,6 +520,8 @@ namespace Umbraco.Core.Composing
// if not caching, or not IDiscoverable, directly get types
if (cache == false || typeof(IDiscoverable).IsAssignableFrom(typeof(T)) == false)
{
+ _logger.Logger.Debug("Running a full, non-cached, scan for type {TypeName} (slow).", typeof(T).FullName);
+
return GetTypesInternal(
typeof (T), null,
() => TypeFinder.FindClassesOfType(specificAssemblies ?? AssembliesToScan),
@@ -559,6 +561,8 @@ namespace Umbraco.Core.Composing
// if not caching, or not IDiscoverable, directly get types
if (cache == false || typeof(IDiscoverable).IsAssignableFrom(typeof(T)) == false)
{
+ _logger.Logger.Debug("Running a full, non-cached, scan for type {TypeName} / attribute {AttributeName} (slow).", typeof(T).FullName, typeof(TAttribute).FullName);
+
return GetTypesInternal(
typeof (T), typeof (TAttribute),
() => TypeFinder.FindClassesOfTypeWithAttribute(specificAssemblies ?? AssembliesToScan),
@@ -595,6 +599,11 @@ namespace Umbraco.Core.Composing
// do not cache anything from specific assemblies
cache &= specificAssemblies == null;
+ if (cache == false)
+ {
+ _logger.Logger.Debug("Running a full, non-cached, scan for types / attribute {AttributeName} (slow).", typeof(TAttribute).FullName);
+ }
+
return GetTypesInternal(
typeof (object), typeof (TAttribute),
() => TypeFinder.FindClassesWithAttribute(specificAssemblies ?? AssembliesToScan),
diff --git a/src/Umbraco.Core/Configuration/Dashboard/AccessElement.cs b/src/Umbraco.Core/Configuration/Dashboard/AccessElement.cs
index 1642f23fc5..01538c8e0b 100644
--- a/src/Umbraco.Core/Configuration/Dashboard/AccessElement.cs
+++ b/src/Umbraco.Core/Configuration/Dashboard/AccessElement.cs
@@ -7,26 +7,22 @@ namespace Umbraco.Core.Configuration.Dashboard
internal class AccessElement : RawXmlConfigurationElement, IAccess
{
public AccessElement()
- {
-
- }
+ { }
public AccessElement(XElement rawXml)
- :base(rawXml)
- {
- }
+ : base(rawXml)
+ { }
- public IEnumerable Rules
+ public IEnumerable Rules
{
get
{
- var result = new List();
- if (RawXml != null)
- {
- result.AddRange(RawXml.Elements("deny").Select(x => new AccessItem {Action = AccessType.Deny, Value = x.Value }));
- result.AddRange(RawXml.Elements("grant").Select(x => new AccessItem { Action = AccessType.Grant, Value = x.Value }));
- result.AddRange(RawXml.Elements("grantBySection").Select(x => new AccessItem { Action = AccessType.GrantBySection, Value = x.Value }));
- }
+ var result = new List();
+ if (RawXml == null) return result;
+
+ result.AddRange(RawXml.Elements("deny").Select(x => new AccessRule {Type = AccessRuleType.Deny, Value = x.Value }));
+ result.AddRange(RawXml.Elements("grant").Select(x => new AccessRule { Type = AccessRuleType.Grant, Value = x.Value }));
+ result.AddRange(RawXml.Elements("grantBySection").Select(x => new AccessRule { Type = AccessRuleType.GrantBySection, Value = x.Value }));
return result;
}
}
diff --git a/src/Umbraco.Core/Configuration/Dashboard/AccessItem.cs b/src/Umbraco.Core/Configuration/Dashboard/AccessItem.cs
deleted file mode 100644
index 37cf491536..0000000000
--- a/src/Umbraco.Core/Configuration/Dashboard/AccessItem.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-namespace Umbraco.Core.Configuration.Dashboard
-{
- internal class AccessItem : IAccessItem
- {
- ///
- /// This can be grant, deny or grantBySection
- ///
- public AccessType Action { get; set; }
-
- ///
- /// The value of the action
- ///
- public string Value { get; set; }
- }
-}
diff --git a/src/Umbraco.Core/Configuration/Dashboard/AccessRule.cs b/src/Umbraco.Core/Configuration/Dashboard/AccessRule.cs
new file mode 100644
index 0000000000..fe6840ff64
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/Dashboard/AccessRule.cs
@@ -0,0 +1,14 @@
+namespace Umbraco.Core.Configuration.Dashboard
+{
+ ///
+ /// Implements .
+ ///
+ internal class AccessRule : IAccessRule
+ {
+ ///
+ public AccessRuleType Type { get; set; }
+
+ ///
+ public string Value { get; set; }
+ }
+}
diff --git a/src/Umbraco.Core/Configuration/Dashboard/AccessRuleType.cs b/src/Umbraco.Core/Configuration/Dashboard/AccessRuleType.cs
new file mode 100644
index 0000000000..cb9ce983fe
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/Dashboard/AccessRuleType.cs
@@ -0,0 +1,28 @@
+namespace Umbraco.Core.Configuration.Dashboard
+{
+ ///
+ /// Defines dashboard access rules type.
+ ///
+ public enum AccessRuleType
+ {
+ ///
+ /// Unknown (default value).
+ ///
+ Unknown = 0,
+
+ ///
+ /// Grant access to the dashboard if user belongs to the specified user group.
+ ///
+ Grant,
+
+ ///
+ /// Deny access to the dashboard if user belongs to the specified user group.
+ ///
+ Deny,
+
+ ///
+ /// Grant access to the dashboard if user has access to the specified section.
+ ///
+ GrantBySection
+ }
+}
diff --git a/src/Umbraco.Core/Configuration/Dashboard/AccessType.cs b/src/Umbraco.Core/Configuration/Dashboard/AccessType.cs
deleted file mode 100644
index d72cac15d0..0000000000
--- a/src/Umbraco.Core/Configuration/Dashboard/AccessType.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace Umbraco.Core.Configuration.Dashboard
-{
- public enum AccessType
- {
- Grant,
- Deny,
- GrantBySection
- }
-}
diff --git a/src/Umbraco.Core/Configuration/Dashboard/ControlElement.cs b/src/Umbraco.Core/Configuration/Dashboard/ControlElement.cs
index 0434eea47e..20dac7460e 100644
--- a/src/Umbraco.Core/Configuration/Dashboard/ControlElement.cs
+++ b/src/Umbraco.Core/Configuration/Dashboard/ControlElement.cs
@@ -1,5 +1,4 @@
-using System;
-using System.Configuration;
+using System.Configuration;
using System.Linq;
using System.Xml.Linq;
@@ -8,33 +7,12 @@ namespace Umbraco.Core.Configuration.Dashboard
internal class ControlElement : RawXmlConfigurationElement, IDashboardControl
{
- public bool ShowOnce
- {
- get
- {
- return RawXml.Attribute("showOnce") == null
- ? false
- : bool.Parse(RawXml.Attribute("showOnce").Value);
- }
- }
-
- public bool AddPanel
- {
- get
- {
- return RawXml.Attribute("addPanel") == null
- ? true
- : bool.Parse(RawXml.Attribute("addPanel").Value);
- }
- }
-
public string PanelCaption
{
get
{
- return RawXml.Attribute("panelCaption") == null
- ? ""
- : RawXml.Attribute("panelCaption").Value;
+ var panelCaption = RawXml.Attribute("panelCaption");
+ return panelCaption == null ? "" : panelCaption.Value;
}
}
@@ -43,11 +21,7 @@ namespace Umbraco.Core.Configuration.Dashboard
get
{
var access = RawXml.Element("access");
- if (access == null)
- {
- return new AccessElement();
- }
- return new AccessElement(access);
+ return access == null ? new AccessElement() : new AccessElement(access);
}
}
@@ -65,10 +39,6 @@ namespace Umbraco.Core.Configuration.Dashboard
}
}
-
- IAccess IDashboardControl.AccessRights
- {
- get { return Access; }
- }
+ IAccess IDashboardControl.AccessRights => Access;
}
}
diff --git a/src/Umbraco.Core/Configuration/Dashboard/IAccess.cs b/src/Umbraco.Core/Configuration/Dashboard/IAccess.cs
index b7d8540a79..8ac1b18cca 100644
--- a/src/Umbraco.Core/Configuration/Dashboard/IAccess.cs
+++ b/src/Umbraco.Core/Configuration/Dashboard/IAccess.cs
@@ -4,6 +4,6 @@ namespace Umbraco.Core.Configuration.Dashboard
{
public interface IAccess
{
- IEnumerable Rules { get; }
+ IEnumerable Rules { get; }
}
}
diff --git a/src/Umbraco.Core/Configuration/Dashboard/IAccessItem.cs b/src/Umbraco.Core/Configuration/Dashboard/IAccessItem.cs
deleted file mode 100644
index 8b18d50bb3..0000000000
--- a/src/Umbraco.Core/Configuration/Dashboard/IAccessItem.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-namespace Umbraco.Core.Configuration.Dashboard
-{
- public interface IAccessItem
- {
- ///
- /// This can be grant, deny or grantBySection
- ///
- AccessType Action { get; set; }
-
- ///
- /// The value of the action
- ///
- string Value { get; set; }
- }
-}
diff --git a/src/Umbraco.Core/Configuration/Dashboard/IAccessRule.cs b/src/Umbraco.Core/Configuration/Dashboard/IAccessRule.cs
new file mode 100644
index 0000000000..8b51b1b73a
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/Dashboard/IAccessRule.cs
@@ -0,0 +1,18 @@
+namespace Umbraco.Core.Configuration.Dashboard
+{
+ ///
+ /// Represents an access rule.
+ ///
+ public interface IAccessRule
+ {
+ ///
+ /// Gets or sets the rule type.
+ ///
+ AccessRuleType Type { get; set; }
+
+ ///
+ /// Gets or sets the value for the rule.
+ ///
+ string Value { get; set; }
+ }
+}
diff --git a/src/Umbraco.Core/Configuration/Dashboard/IDashboardControl.cs b/src/Umbraco.Core/Configuration/Dashboard/IDashboardControl.cs
index 7dab542258..cdf05af1ec 100644
--- a/src/Umbraco.Core/Configuration/Dashboard/IDashboardControl.cs
+++ b/src/Umbraco.Core/Configuration/Dashboard/IDashboardControl.cs
@@ -2,10 +2,6 @@
{
public interface IDashboardControl
{
- bool ShowOnce { get; }
-
- bool AddPanel { get; }
-
string PanelCaption { get; }
string ControlPath { get; }
diff --git a/src/Umbraco.Core/Configuration/IGlobalSettings.cs b/src/Umbraco.Core/Configuration/IGlobalSettings.cs
index cf9478d30a..a043f608f4 100644
--- a/src/Umbraco.Core/Configuration/IGlobalSettings.cs
+++ b/src/Umbraco.Core/Configuration/IGlobalSettings.cs
@@ -1,4 +1,6 @@
-namespace Umbraco.Core.Configuration
+using System;
+
+namespace Umbraco.Core.Configuration
{
///
/// Contains general settings information for the entire Umbraco instance based on information from web.config appsettings
@@ -24,6 +26,8 @@
///
/// Defaults to ~/App_Data/umbraco.config
///
+ //fixme - remove
+ [Obsolete("This should not be used, need to remove the content xml cache")]
string ContentXmlFile { get; }
///
diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs
index 46ad221837..73df566a0f 100644
--- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs
+++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs
@@ -10,37 +10,61 @@ namespace Umbraco.Core.Configuration
///
public static class UmbracoVersion
{
- // BEWARE!
- // This class is parsed and updated by the build scripts.
- // Do NOT modify it unless you understand what you are doing.
+ static UmbracoVersion()
+ {
+ var umbracoCoreAssembly = typeof(UmbracoVersion).Assembly;
+
+ // gets the value indicated by the AssemblyVersion attribute
+ AssemblyVersion = umbracoCoreAssembly.GetName().Version;
+
+ // gets the value indicated by the AssemblyFileVersion attribute
+ AssemblyFileVersion = System.Version.Parse(umbracoCoreAssembly.GetCustomAttribute().Version);
+
+ // gets the value indicated by the AssemblyInformationalVersion attribute
+ // this is the true semantic version of the Umbraco Cms
+ SemanticVersion = SemVersion.Parse(umbracoCoreAssembly.GetCustomAttribute().InformationalVersion);
+
+ // gets the non-semantic version
+ Current = SemanticVersion.GetVersion(3);
+ }
///
- /// Gets the version of the executing code.
+ /// Gets the non-semantic version of the Umbraco code.
///
- public static Version Current { get; } = new Version("8.0.0");
+ // TODO rename to Version
+ public static Version Current { get; }
///
- /// Gets the version comment of the executing code (eg "beta").
+ /// Gets the semantic version comments of the Umbraco code.
///
- public static string CurrentComment => "alpha.52";
+ public static string Comment => SemanticVersion.Prerelease;
///
- /// Gets the assembly version of Umbraco.Code.dll.
+ /// Gets the assembly version of the Umbraco code.
///
- /// Get it by looking at a class in that dll, due to medium trust issues,
- /// see http://haacked.com/archive/2010/11/04/assembly-location-and-medium-trust.aspx,
- /// however fixme we don't support medium trust anymore?
- public static string AssemblyVersion => new AssemblyName(typeof(UmbracoVersion).Assembly.FullName).Version.ToString();
+ ///
+ /// The assembly version is the value of the .
+ /// Is is the one that the CLR checks for compatibility. Therefore, it changes only on
+ /// hard-breaking changes (for instance, on new major versions).
+ ///
+ public static Version AssemblyVersion {get; }
///
- /// Gets the semantic version of the executing code.
+ /// Gets the assembly file version of the Umbraco code.
///
- public static SemVersion SemanticVersion { get; } = new SemVersion(
- Current.Major,
- Current.Minor,
- Current.Build,
- CurrentComment.IsNullOrWhiteSpace() ? null : CurrentComment,
- Current.Revision > 0 ? Current.Revision.ToInvariantString() : null);
+ ///
+ /// The assembly version is the value of the .
+ ///
+ public static Version AssemblyFileVersion { get; }
+
+ ///
+ /// Gets the semantic version of the Umbraco code.
+ ///
+ ///
+ /// The semantic version is the value of the .
+ /// It is the full version of Umbraco, including comments.
+ ///
+ public static SemVersion SemanticVersion { get; }
///
/// Gets the "local" version of the site.
@@ -51,7 +75,7 @@ namespace Umbraco.Core.Configuration
/// and changes during an upgrade. The executing code version changes when new code is
/// deployed. The site/files version changes during an upgrade.
///
- public static SemVersion Local
+ public static SemVersion LocalVersion
{
get
{
diff --git a/src/Umbraco.Core/Constants-Indexes.cs b/src/Umbraco.Core/Constants-Indexes.cs
new file mode 100644
index 0000000000..c73a170b62
--- /dev/null
+++ b/src/Umbraco.Core/Constants-Indexes.cs
@@ -0,0 +1,19 @@
+using System;
+using System.ComponentModel;
+
+namespace Umbraco.Core
+{
+ public static partial class Constants
+ {
+ public static class UmbracoIndexes
+ {
+ public const string InternalIndexName = InternalIndexPath + "Index";
+ public const string ExternalIndexName = ExternalIndexPath + "Index";
+ public const string MembersIndexName = MembersIndexPath + "Index";
+
+ public const string InternalIndexPath = "Internal";
+ public const string ExternalIndexPath = "External";
+ public const string MembersIndexPath = "Members";
+ }
+ }
+}
diff --git a/src/Umbraco.Core/ContentExtensions.cs b/src/Umbraco.Core/ContentExtensions.cs
index 910717304c..b15e371e87 100644
--- a/src/Umbraco.Core/ContentExtensions.cs
+++ b/src/Umbraco.Core/ContentExtensions.cs
@@ -23,6 +23,49 @@ namespace Umbraco.Core
#region IContent
+ ///
+ /// Gets the current status of the Content
+ ///
+ public static ContentStatus GetStatus(this IContent content, string culture = null)
+ {
+ if (content.Trashed)
+ return ContentStatus.Trashed;
+
+ if (!content.ContentType.VariesByCulture())
+ culture = string.Empty;
+ else if (culture.IsNullOrWhiteSpace())
+ throw new ArgumentNullException($"{nameof(culture)} cannot be null or empty");
+
+ var expires = content.ContentSchedule.GetSchedule(culture, ContentScheduleAction.Expire);
+ if (expires != null && expires.Any(x => x.Date > DateTime.MinValue && DateTime.Now > x.Date))
+ return ContentStatus.Expired;
+
+ var release = content.ContentSchedule.GetSchedule(culture, ContentScheduleAction.Release);
+ if (release != null && release.Any(x => x.Date > DateTime.MinValue && x.Date > DateTime.Now))
+ return ContentStatus.AwaitingRelease;
+
+ if (content.Published)
+ return ContentStatus.Published;
+
+ return ContentStatus.Unpublished;
+ }
+
+ ///
+ /// Gets the cultures that have been flagged for unpublishing.
+ ///
+ /// Gets cultures for which content.UnpublishCulture() has been invoked.
+ internal static IReadOnlyList GetCulturesUnpublishing(this IContent content)
+ {
+ if (!content.Published || !content.ContentType.VariesByCulture() || !content.IsPropertyDirty("PublishCultureInfos"))
+ return Array.Empty();
+
+ var culturesChanging = content.CultureInfos.Where(x => x.Value.IsDirty()).Select(x => x.Key);
+ return culturesChanging
+ .Where(x => !content.IsCulturePublished(x) && // is not published anymore
+ content.WasCulturePublished(x)) // but was published before
+ .ToList();
+ }
+
///
/// Returns true if this entity was just published as part of a recent save operation (i.e. it wasn't previously published)
///
diff --git a/src/Umbraco.Core/EnumerableExtensions.cs b/src/Umbraco.Core/EnumerableExtensions.cs
index c455fadad7..4fa013c095 100644
--- a/src/Umbraco.Core/EnumerableExtensions.cs
+++ b/src/Umbraco.Core/EnumerableExtensions.cs
@@ -1,11 +1,6 @@
using System;
-using System.Collections;
using System.Collections.Generic;
-using System.ComponentModel;
-using System.Diagnostics.CodeAnalysis;
using System.Linq;
-using System.Text;
-using Umbraco.Core.Logging;
namespace Umbraco.Core
{
@@ -14,6 +9,18 @@ namespace Umbraco.Core
///
public static class EnumerableExtensions
{
+ ///
+ /// Wraps this object instance into an IEnumerable{T} consisting of a single item.
+ ///
+ /// Type of the object.
+ /// The instance that will be wrapped.
+ /// An IEnumerable{T} consisting of a single item.
+ public static IEnumerable Yield(this T item)
+ {
+ // see EnumeratorBenchmarks - this is faster, and allocates less, than returning an array
+ yield return item;
+ }
+
public static IEnumerable> InGroupsOf(this IEnumerable source, int groupSize)
{
if (source == null)
diff --git a/src/Umbraco.Core/IO/FileSystems.cs b/src/Umbraco.Core/IO/FileSystems.cs
index 5d7088b0e1..62ce25dff0 100644
--- a/src/Umbraco.Core/IO/FileSystems.cs
+++ b/src/Umbraco.Core/IO/FileSystems.cs
@@ -35,11 +35,7 @@ namespace Umbraco.Core.IO
private object _wkfsObject;
private MediaFileSystem _mediaFileSystem;
-
- //fixme - is this needed to be a managed file system? seems irrelevant since it won't ever be moved and is only used in one place in code
- private IFileSystem _javascriptLibraryFileSystem;
-
#region Constructor
// DI wants a public ctor
@@ -129,16 +125,6 @@ namespace Umbraco.Core.IO
}
}
- //fixme - is this needed to be a managed file system? seems irrelevant since it won't ever be moved and is only used in one place in code
- internal IFileSystem JavaScriptLibraryFileSystem
- {
- get
- {
- if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems();
- return _javascriptLibraryFileSystem;
- }
- }
-
private void EnsureWellKnownFileSystems()
{
LazyInitializer.EnsureInitialized(ref _wkfsObject, ref _wkfsInitialized, ref _wkfsLock, CreateWellKnownFileSystems);
@@ -154,7 +140,6 @@ namespace Umbraco.Core.IO
var scriptsFileSystem = new PhysicalFileSystem(SystemDirectories.Scripts);
var masterPagesFileSystem = new PhysicalFileSystem(SystemDirectories.Masterpages);
var mvcViewsFileSystem = new PhysicalFileSystem(SystemDirectories.MvcViews);
- var javaScriptLibraryFileSystem = new PhysicalFileSystem(SystemDirectories.JavaScriptLibrary);
_macroPartialFileSystem = new ShadowWrapper(macroPartialFileSystem, "Views/MacroPartials", () => IsScoped());
_partialViewsFileSystem = new ShadowWrapper(partialViewsFileSystem, "Views/Partials", () => IsScoped());
@@ -162,7 +147,6 @@ namespace Umbraco.Core.IO
_scriptsFileSystem = new ShadowWrapper(scriptsFileSystem, "scripts", () => IsScoped());
_masterPagesFileSystem = new ShadowWrapper(masterPagesFileSystem, "masterpages", () => IsScoped());
_mvcViewsFileSystem = new ShadowWrapper(mvcViewsFileSystem, "Views", () => IsScoped());
- _javascriptLibraryFileSystem = new ShadowWrapper(javaScriptLibraryFileSystem, "Lib", () => IsScoped());
// filesystems obtained from GetFileSystemProvider are already wrapped and do not need to be wrapped again
_mediaFileSystem = GetFileSystemProvider();
diff --git a/src/Umbraco.Core/IO/SystemDirectories.cs b/src/Umbraco.Core/IO/SystemDirectories.cs
index 46c5bf851c..183d48e3d9 100644
--- a/src/Umbraco.Core/IO/SystemDirectories.cs
+++ b/src/Umbraco.Core/IO/SystemDirectories.cs
@@ -8,24 +8,20 @@ namespace Umbraco.Core.IO
{
public static string Bin => "~/bin";
- public static string Config => IOHelper.ReturnPath("umbracoConfigDirectory", "~/config");
+ public static string Config => "~/config";
- public static string Css => IOHelper.ReturnPath("umbracoCssDirectory", "~/css");
+ public static string Data => "~/App_Data";
- public static string Data => IOHelper.ReturnPath("umbracoStorageDirectory", "~/App_Data");
+ public static string Install => "~/install";
- public static string Install => IOHelper.ReturnPath("umbracoInstallPath", "~/install");
+ //fixme: remove this
+ [Obsolete("Master pages are obsolete and code should be removed")]
+ public static string Masterpages => "~/masterpages";
- //TODO: Consider removing this
- public static string Masterpages => IOHelper.ReturnPath("umbracoMasterPagesPath", "~/masterpages");
-
- //NOTE: this is not configurable and shouldn't need to be
public static string AppCode => "~/App_Code";
- //NOTE: this is not configurable and shouldn't need to be
public static string AppPlugins => "~/App_Plugins";
- //NOTE: this is not configurable and shouldn't need to be
public static string MvcViews => "~/Views";
public static string PartialViews => MvcViews + "/Partials/";
@@ -36,21 +32,20 @@ namespace Umbraco.Core.IO
public static string Scripts => IOHelper.ReturnPath("umbracoScriptsPath", "~/scripts");
- public static string StyleSheets => IOHelper.ReturnPath("umbracoStylesheetsPath", "~/css");
+ public static string Css => IOHelper.ReturnPath("umbracoCssPath", "~/css");
public static string Umbraco => IOHelper.ReturnPath("umbracoPath", "~/umbraco");
- //TODO: Consider removing this
- public static string UserControls => IOHelper.ReturnPath("umbracoUsercontrolsPath", "~/usercontrols");
+ //fixme: remove this
+ [Obsolete("Usercontrols are obsolete and code should be removed")]
+ public static string UserControls => "~/usercontrols";
+ [Obsolete("Only used by legacy load balancing which is obsolete and should be removed")]
public static string WebServices => IOHelper.ReturnPath("umbracoWebservicesPath", Umbraco.EnsureEndsWith("/") + "webservices");
- //by default the packages folder should exist in the data folder
- public static string Packages => IOHelper.ReturnPath("umbracoPackagesPath", Data + IOHelper.DirSepChar + "packages");
+ public static string Packages => Data + IOHelper.DirSepChar + "packages";
- public static string Preview => IOHelper.ReturnPath("umbracoPreviewPath", Data + IOHelper.DirSepChar + "preview");
-
- public static string JavaScriptLibrary => IOHelper.ReturnPath("umbracoJavaScriptLibraryPath", Umbraco + IOHelper.DirSepChar + "lib");
+ public static string Preview => Data + IOHelper.DirSepChar + "preview";
private static string _root;
diff --git a/src/Umbraco.Core/Manifest/ContentAppDefinitionConverter.cs b/src/Umbraco.Core/Manifest/ContentAppDefinitionConverter.cs
deleted file mode 100644
index 87f104d90e..0000000000
--- a/src/Umbraco.Core/Manifest/ContentAppDefinitionConverter.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-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/DashboardAccessRuleConverter.cs b/src/Umbraco.Core/Manifest/DashboardAccessRuleConverter.cs
new file mode 100644
index 0000000000..c627728a32
--- /dev/null
+++ b/src/Umbraco.Core/Manifest/DashboardAccessRuleConverter.cs
@@ -0,0 +1,45 @@
+using System;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using Umbraco.Core.Configuration.Dashboard;
+using Umbraco.Core.Serialization;
+
+namespace Umbraco.Core.Manifest
+{
+ ///
+ /// Implements a json read converter for .
+ ///
+ internal class DashboardAccessRuleConverter : JsonReadConverter
+ {
+ ///
+ protected override IAccessRule Create(Type objectType, string path, JObject jObject)
+ {
+ return new AccessRule();
+ }
+
+ ///
+ protected override void Deserialize(JObject jobject, IAccessRule target, JsonSerializer serializer)
+ {
+ // see Create above, target is either DataEditor (parameter) or ConfiguredDataEditor (property)
+
+ if (!(target is AccessRule accessRule))
+ throw new Exception("panic.");
+
+ GetRule(accessRule, jobject, "grant", AccessRuleType.Grant);
+ GetRule(accessRule, jobject, "deny", AccessRuleType.Deny);
+ GetRule(accessRule, jobject, "grantBySection", AccessRuleType.GrantBySection);
+
+ if (accessRule.Type == AccessRuleType.Unknown) throw new InvalidOperationException("Rule is not defined.");
+ }
+
+ private void GetRule(AccessRule rule, JObject jobject, string name, AccessRuleType type)
+ {
+ var token = jobject[name];
+ if (token == null) return;
+ if (rule.Type != AccessRuleType.Unknown) throw new InvalidOperationException("Multiple definition of a rule.");
+ if (token.Type != JTokenType.String) throw new InvalidOperationException("Rule value is not a string.");
+ rule.Type = type;
+ rule.Value = token.Value();
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Manifest/ManifestContentAppDefinition.cs b/src/Umbraco.Core/Manifest/ManifestContentAppDefinition.cs
index d5f6c2b8c4..0667f11aab 100644
--- a/src/Umbraco.Core/Manifest/ManifestContentAppDefinition.cs
+++ b/src/Umbraco.Core/Manifest/ManifestContentAppDefinition.cs
@@ -31,11 +31,9 @@ namespace Umbraco.Core.Manifest
/// Represents a content app definition, parsed from a manifest.
///
[DataContract(Name = "appdef", Namespace = "")]
- public class ManifestContentAppDefinition : IContentAppDefinition
+ public class ManifestContentAppDefinition
{
private string _view;
- private ContentApp _app;
- private ShowRule[] _showRules;
///
/// Gets or sets the name of the content app.
@@ -83,132 +81,5 @@ namespace Umbraco.Core.Manifest
[DataMember(Name = "show")]
public string[] Show { get; set; } = Array.Empty();
- ///
- public ContentApp GetContentAppFor(object o, IEnumerable userGroups)
- {
- 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());
- var userGroupsList = userGroups.ToList();
-
- var okRole = false;
- var hasRole = false;
- var okType = false;
- var hasType = false;
-
- foreach (var rule in rules)
- {
- if (rule.PartA.InvariantEquals("role"))
- {
- // if roles have been ok-ed already, skip the rule
- if (okRole)
- continue;
-
- // remember we have role rules
- hasRole = true;
-
- foreach (var group in userGroupsList)
- {
- // if the entry does not apply, skip
- if (!rule.Matches("role", group.Alias))
- continue;
-
- // if the entry applies,
- // if it's an exclude entry, exit, do not display the content app
- if (!rule.Show)
- return null;
-
- // else ok to display, remember roles are ok, break from userGroupsList
- okRole = rule.Show;
- break;
- }
- }
- else // it is a type rule
- {
- // if type has been ok-ed already, skip the rule
- if (okType)
- continue;
-
- // remember we have type rules
- hasType = true;
-
- // 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 ok to display, remember type rules are ok
- okType = true;
- }
- }
-
- // if roles rules are specified but not ok,
- // or if type roles are specified but not ok,
- // cannot display the content app
- if ((hasRole && !okRole) || (hasType && !okType))
- return null;
-
- // else
- // 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/ManifestContentAppFactory.cs b/src/Umbraco.Core/Manifest/ManifestContentAppFactory.cs
new file mode 100644
index 0000000000..1c50a4b895
--- /dev/null
+++ b/src/Umbraco.Core/Manifest/ManifestContentAppFactory.cs
@@ -0,0 +1,171 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+using Umbraco.Core.Models;
+using Umbraco.Core.Models.ContentEditing;
+using Umbraco.Core.Models.Membership;
+
+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
+ // '+role/admin' // show for admin users. Role based permissions will override others.
+ // ]
+ // },
+ // ...
+ // ]
+
+ ///
+ /// Represents a content app factory, for content apps parsed from the manifest.
+ ///
+ public class ManifestContentAppFactory : IContentAppFactory
+ {
+ private readonly ManifestContentAppDefinition _definition;
+
+ public ManifestContentAppFactory(ManifestContentAppDefinition definition)
+ {
+ _definition = definition;
+ }
+
+ private ContentApp _app;
+ private ShowRule[] _showRules;
+
+ ///
+ public ContentApp GetContentAppFor(object o,IEnumerable userGroups)
+ {
+ 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(_definition.Show).ToArray());
+ var userGroupsList = userGroups.ToList();
+
+ var okRole = false;
+ var hasRole = false;
+ var okType = false;
+ var hasType = false;
+
+ foreach (var rule in rules)
+ {
+ if (rule.PartA.InvariantEquals("role"))
+ {
+ // if roles have been ok-ed already, skip the rule
+ if (okRole)
+ continue;
+
+ // remember we have role rules
+ hasRole = true;
+
+ foreach (var group in userGroupsList)
+ {
+ // if the entry does not apply, skip
+ if (!rule.Matches("role", group.Alias))
+ continue;
+
+ // if the entry applies,
+ // if it's an exclude entry, exit, do not display the content app
+ if (!rule.Show)
+ return null;
+
+ // else ok to display, remember roles are ok, break from userGroupsList
+ okRole = rule.Show;
+ break;
+ }
+ }
+ else // it is a type rule
+ {
+ // if type has been ok-ed already, skip the rule
+ if (okType)
+ continue;
+
+ // remember we have type rules
+ hasType = true;
+
+ // 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 ok to display, remember type rules are ok
+ okType = true;
+ }
+ }
+
+ // if roles rules are specified but not ok,
+ // or if type roles are specified but not ok,
+ // cannot display the content app
+ if ((hasRole && !okRole) || (hasType && !okType))
+ return null;
+
+ // else
+ // content app can be displayed
+ return _app ?? (_app = new ContentApp
+ {
+ Alias = _definition.Alias,
+ Name = _definition.Name,
+ Icon = _definition.Icon,
+ View = _definition.View,
+ Weight = _definition.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/ManifestDashboardDefinition.cs b/src/Umbraco.Core/Manifest/ManifestDashboardDefinition.cs
new file mode 100644
index 0000000000..83f047b264
--- /dev/null
+++ b/src/Umbraco.Core/Manifest/ManifestDashboardDefinition.cs
@@ -0,0 +1,36 @@
+using System;
+using System.ComponentModel;
+using Newtonsoft.Json;
+using Umbraco.Core.Configuration.Dashboard;
+using Umbraco.Core.IO;
+
+namespace Umbraco.Core.Manifest
+{
+ public class ManifestDashboardDefinition
+ {
+ private string _view;
+
+ [JsonProperty("name", Required = Required.Always)]
+ public string Name { get; set; }
+
+ [JsonProperty("alias", Required = Required.Always)]
+ public string Alias { get; set; }
+
+ [JsonProperty("weight", DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate)]
+ [DefaultValue(100)]
+ public int Weight { get; set; }
+
+ [JsonProperty("view", Required = Required.Always)]
+ public string View
+ {
+ get => _view;
+ set => _view = IOHelper.ResolveVirtualUrl(value);
+ }
+
+ [JsonProperty("sections")]
+ public string[] Sections { get; set; } = Array.Empty();
+
+ [JsonProperty("access")]
+ public IAccessRule[] AccessRules { get; set; } = Array.Empty();
+ }
+}
diff --git a/src/Umbraco.Core/Manifest/ManifestParser.cs b/src/Umbraco.Core/Manifest/ManifestParser.cs
index 125dee5c05..59753df66a 100644
--- a/src/Umbraco.Core/Manifest/ManifestParser.cs
+++ b/src/Umbraco.Core/Manifest/ManifestParser.cs
@@ -1,177 +1,179 @@
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Linq;
-using System.Text;
-using Newtonsoft.Json;
-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
-{
- ///
- /// Parses the Main.js file and replaces all tokens accordingly.
- ///
- public class ManifestParser
- {
- private static readonly string Utf8Preamble = Encoding.UTF8.GetString(Encoding.UTF8.GetPreamble());
-
- private readonly IRuntimeCacheProvider _cache;
- private readonly ILogger _logger;
- private readonly ManifestValueValidatorCollection _validators;
-
- private string _path;
-
- ///
- /// Initializes a new instance of the class.
- ///
- public ManifestParser(IRuntimeCacheProvider cache, ManifestValueValidatorCollection validators, ILogger logger)
- : this(cache, validators, "~/App_Plugins", logger)
- { }
-
- ///
- /// Initializes a new instance of the class.
- ///
- private ManifestParser(IRuntimeCacheProvider cache, ManifestValueValidatorCollection validators, string path, ILogger logger)
- {
- _cache = cache ?? throw new ArgumentNullException(nameof(cache));
- _validators = validators ?? throw new ArgumentNullException(nameof(validators));
- if (string.IsNullOrWhiteSpace(path)) throw new ArgumentNullOrEmptyException(nameof(path));
- Path = path;
- _logger = logger ?? throw new ArgumentNullException(nameof(logger));
- }
-
- public string Path
- {
- get => _path;
- set => _path = value.StartsWith("~/") ? IOHelper.MapPath(value) : value;
- }
-
- ///
- /// Gets all manifests, merged into a single manifest object.
- ///
- ///
- public PackageManifest Manifest
- => _cache.GetCacheItem("Umbraco.Core.Manifest.ManifestParser::Manifests", () =>
- {
- var manifests = GetManifests();
- return MergeManifests(manifests);
- }, new TimeSpan(0, 4, 0));
-
- ///
- /// Gets all manifests.
- ///
- private IEnumerable GetManifests()
- {
- var manifests = new List();
-
- foreach (var path in GetManifestFiles())
- {
- try
- {
- var text = File.ReadAllText(path);
- text = TrimPreamble(text);
- if (string.IsNullOrWhiteSpace(text))
- continue;
- var manifest = ParseManifest(text);
- manifests.Add(manifest);
- }
- catch (Exception e)
- {
- _logger.Error(e, "Failed to parse manifest at '{Path}', ignoring.", path);
- }
- }
-
- return manifests;
- }
-
- ///
- /// Merges all manifests into one.
- ///
- private static PackageManifest MergeManifests(IEnumerable manifests)
- {
- var scripts = new HashSet();
- var stylesheets = new HashSet();
- var propertyEditors = new List();
- var parameterEditors = new List();
- var gridEditors = new List();
- var contentApps = new List();
-
- foreach (var manifest in manifests)
- {
- if (manifest.Scripts != null) foreach (var script in manifest.Scripts) scripts.Add(script);
- if (manifest.Stylesheets != null) foreach (var stylesheet in manifest.Stylesheets) stylesheets.Add(stylesheet);
- 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
- {
- Scripts = scripts.ToArray(),
- Stylesheets = stylesheets.ToArray(),
- PropertyEditors = propertyEditors.ToArray(),
- ParameterEditors = parameterEditors.ToArray(),
- GridEditors = gridEditors.ToArray(),
- ContentApps = contentApps.ToArray()
- };
- }
-
- // gets all manifest files (recursively)
- private IEnumerable GetManifestFiles()
- {
- if (Directory.Exists(_path) == false)
- return new string[0];
- return Directory.GetFiles(_path, "package.manifest", SearchOption.AllDirectories);
- }
-
-
- private static string TrimPreamble(string text)
- {
- // strangely StartsWith(preamble) would always return true
- if (text.Substring(0, 1) == Utf8Preamble)
- text = text.Remove(0, Utf8Preamble.Length);
-
- return text;
- }
-
- ///
- /// Parses a manifest.
- ///
- internal PackageManifest ParseManifest(string text)
- {
- if (string.IsNullOrWhiteSpace(text))
- throw new ArgumentNullOrEmptyException(nameof(text));
-
- var manifest = JsonConvert.DeserializeObject(text,
- new DataEditorConverter(_logger),
- new ValueValidatorConverter(_validators),
- new ContentAppDefinitionConverter());
-
- // scripts and stylesheets are raw string, must process here
- for (var i = 0; i < manifest.Scripts.Length; i++)
- manifest.Scripts[i] = IOHelper.ResolveVirtualUrl(manifest.Scripts[i]);
- for (var i = 0; i < manifest.Stylesheets.Length; i++)
- manifest.Stylesheets[i] = IOHelper.ResolveVirtualUrl(manifest.Stylesheets[i]);
-
- // add property editors that are also parameter editors, to the parameter editors list
- // (the manifest format is kinda legacy)
- var ppEditors = manifest.PropertyEditors.Where(x => (x.Type & EditorType.MacroParameter) > 0).ToList();
- if (ppEditors.Count > 0)
- manifest.ParameterEditors = manifest.ParameterEditors.Union(ppEditors).ToArray();
-
- return manifest;
- }
-
- // purely for tests
- internal IEnumerable ParseGridEditors(string text)
- {
- return JsonConvert.DeserializeObject>(text);
- }
- }
-}
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using Newtonsoft.Json;
+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
+{
+ ///
+ /// Parses the Main.js file and replaces all tokens accordingly.
+ ///
+ public class ManifestParser
+ {
+ private static readonly string Utf8Preamble = Encoding.UTF8.GetString(Encoding.UTF8.GetPreamble());
+
+ private readonly IRuntimeCacheProvider _cache;
+ private readonly ILogger _logger;
+ private readonly ManifestValueValidatorCollection _validators;
+
+ private string _path;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ManifestParser(IRuntimeCacheProvider cache, ManifestValueValidatorCollection validators, ILogger logger)
+ : this(cache, validators, "~/App_Plugins", logger)
+ { }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ private ManifestParser(IRuntimeCacheProvider cache, ManifestValueValidatorCollection validators, string path, ILogger logger)
+ {
+ _cache = cache ?? throw new ArgumentNullException(nameof(cache));
+ _validators = validators ?? throw new ArgumentNullException(nameof(validators));
+ if (string.IsNullOrWhiteSpace(path)) throw new ArgumentNullOrEmptyException(nameof(path));
+ Path = path;
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ public string Path
+ {
+ get => _path;
+ set => _path = value.StartsWith("~/") ? IOHelper.MapPath(value) : value;
+ }
+
+ ///
+ /// Gets all manifests, merged into a single manifest object.
+ ///
+ ///
+ public PackageManifest Manifest
+ => _cache.GetCacheItem("Umbraco.Core.Manifest.ManifestParser::Manifests", () =>
+ {
+ var manifests = GetManifests();
+ return MergeManifests(manifests);
+ }, new TimeSpan(0, 4, 0));
+
+ ///
+ /// Gets all manifests.
+ ///
+ private IEnumerable GetManifests()
+ {
+ var manifests = new List();
+
+ foreach (var path in GetManifestFiles())
+ {
+ try
+ {
+ var text = File.ReadAllText(path);
+ text = TrimPreamble(text);
+ if (string.IsNullOrWhiteSpace(text))
+ continue;
+ var manifest = ParseManifest(text);
+ manifests.Add(manifest);
+ }
+ catch (Exception e)
+ {
+ _logger.Error(e, "Failed to parse manifest at '{Path}', ignoring.", path);
+ }
+ }
+
+ return manifests;
+ }
+
+ ///
+ /// Merges all manifests into one.
+ ///
+ private static PackageManifest MergeManifests(IEnumerable manifests)
+ {
+ var scripts = new HashSet();
+ var stylesheets = new HashSet();
+ var propertyEditors = new List();
+ var parameterEditors = new List();
+ var gridEditors = new List();
+ var contentApps = new List();
+ var dashboards = new List();
+
+ foreach (var manifest in manifests)
+ {
+ if (manifest.Scripts != null) foreach (var script in manifest.Scripts) scripts.Add(script);
+ if (manifest.Stylesheets != null) foreach (var stylesheet in manifest.Stylesheets) stylesheets.Add(stylesheet);
+ 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);
+ if (manifest.Dashboards != null) dashboards.AddRange(manifest.Dashboards);
+ }
+
+ return new PackageManifest
+ {
+ Scripts = scripts.ToArray(),
+ Stylesheets = stylesheets.ToArray(),
+ PropertyEditors = propertyEditors.ToArray(),
+ ParameterEditors = parameterEditors.ToArray(),
+ GridEditors = gridEditors.ToArray(),
+ ContentApps = contentApps.ToArray(),
+ Dashboards = dashboards.ToArray()
+ };
+ }
+
+ // gets all manifest files (recursively)
+ private IEnumerable GetManifestFiles()
+ {
+ if (Directory.Exists(_path) == false)
+ return new string[0];
+ return Directory.GetFiles(_path, "package.manifest", SearchOption.AllDirectories);
+ }
+
+ private static string TrimPreamble(string text)
+ {
+ // strangely StartsWith(preamble) would always return true
+ if (text.Substring(0, 1) == Utf8Preamble)
+ text = text.Remove(0, Utf8Preamble.Length);
+
+ return text;
+ }
+
+ ///
+ /// Parses a manifest.
+ ///
+ internal PackageManifest ParseManifest(string text)
+ {
+ if (string.IsNullOrWhiteSpace(text))
+ throw new ArgumentNullOrEmptyException(nameof(text));
+
+ var manifest = JsonConvert.DeserializeObject(text,
+ new DataEditorConverter(_logger),
+ new ValueValidatorConverter(_validators),
+ new DashboardAccessRuleConverter());
+
+ // scripts and stylesheets are raw string, must process here
+ for (var i = 0; i < manifest.Scripts.Length; i++)
+ manifest.Scripts[i] = IOHelper.ResolveVirtualUrl(manifest.Scripts[i]);
+ for (var i = 0; i < manifest.Stylesheets.Length; i++)
+ manifest.Stylesheets[i] = IOHelper.ResolveVirtualUrl(manifest.Stylesheets[i]);
+
+ // add property editors that are also parameter editors, to the parameter editors list
+ // (the manifest format is kinda legacy)
+ var ppEditors = manifest.PropertyEditors.Where(x => (x.Type & EditorType.MacroParameter) > 0).ToList();
+ if (ppEditors.Count > 0)
+ manifest.ParameterEditors = manifest.ParameterEditors.Union(ppEditors).ToArray();
+
+ return manifest;
+ }
+
+ // purely for tests
+ internal IEnumerable ParseGridEditors(string text)
+ {
+ return JsonConvert.DeserializeObject>(text);
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Manifest/PackageManifest.cs b/src/Umbraco.Core/Manifest/PackageManifest.cs
index 32dae46a9a..cd806ac847 100644
--- a/src/Umbraco.Core/Manifest/PackageManifest.cs
+++ b/src/Umbraco.Core/Manifest/PackageManifest.cs
@@ -1,31 +1,34 @@
-using System;
-using Newtonsoft.Json;
-using Umbraco.Core.Models.ContentEditing;
-using Umbraco.Core.PropertyEditors;
-
-namespace Umbraco.Core.Manifest
-{
- ///
- /// Represents the content of a package manifest.
- ///
- public class PackageManifest
- {
- [JsonProperty("javascript")]
- public string[] Scripts { get; set; } = Array.Empty();
-
- [JsonProperty("css")]
- public string[] Stylesheets { get; set; }= Array.Empty();
-
- [JsonProperty("propertyEditors")]
- public IDataEditor[] PropertyEditors { get; set; } = Array.Empty();
-
- [JsonProperty("parameterEditors")]
- public IDataEditor[] ParameterEditors { get; set; } = Array.Empty();
-
- [JsonProperty("gridEditors")]
- public GridEditor[] GridEditors { get; set; } = Array.Empty();
-
- [JsonProperty("contentApps")]
- public IContentAppDefinition[] ContentApps { get; set; } = Array.Empty();
- }
-}
+using System;
+using Newtonsoft.Json;
+using Umbraco.Core.Models.ContentEditing;
+using Umbraco.Core.PropertyEditors;
+
+namespace Umbraco.Core.Manifest
+{
+ ///
+ /// Represents the content of a package manifest.
+ ///
+ public class PackageManifest
+ {
+ [JsonProperty("javascript")]
+ public string[] Scripts { get; set; } = Array.Empty();
+
+ [JsonProperty("css")]
+ public string[] Stylesheets { get; set; }= Array.Empty();
+
+ [JsonProperty("propertyEditors")]
+ public IDataEditor[] PropertyEditors { get; set; } = Array.Empty();
+
+ [JsonProperty("parameterEditors")]
+ public IDataEditor[] ParameterEditors { get; set; } = Array.Empty();
+
+ [JsonProperty("gridEditors")]
+ public GridEditor[] GridEditors { get; set; } = Array.Empty();
+
+ [JsonProperty("contentApps")]
+ public ManifestContentAppDefinition[] ContentApps { get; set; } = Array.Empty();
+
+ [JsonProperty("dashboards")]
+ public ManifestDashboardDefinition[] Dashboards { get; set; } = Array.Empty();
+ }
+}
diff --git a/src/Umbraco.Core/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs b/src/Umbraco.Core/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs
index 8d95d9c14c..eee9826a85 100644
--- a/src/Umbraco.Core/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs
+++ b/src/Umbraco.Core/Migrations/Expressions/Alter/Table/AlterTableBuilder.cs
@@ -4,6 +4,7 @@ using Umbraco.Core.Migrations.Expressions.Alter.Expressions;
using Umbraco.Core.Migrations.Expressions.Common.Expressions;
using Umbraco.Core.Migrations.Expressions.Create.Expressions;
using Umbraco.Core.Persistence;
+using Umbraco.Core.Persistence.DatabaseAnnotations;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
namespace Umbraco.Core.Migrations.Expressions.Alter.Table
@@ -154,8 +155,8 @@ namespace Umbraco.Core.Migrations.Expressions.Alter.Table
var index = new CreateIndexExpression(_context, new IndexDefinition
{
Name = indexName,
- TableName = Expression.TableName,
- IsUnique = true
+ TableName = Expression.TableName,
+ IndexType = IndexTypes.UniqueNonClustered
});
index.Index.Columns.Add(new IndexColumnDefinition
diff --git a/src/Umbraco.Core/Migrations/Expressions/Create/Column/CreateColumnBuilder.cs b/src/Umbraco.Core/Migrations/Expressions/Create/Column/CreateColumnBuilder.cs
index 28de5aef14..656aedcea0 100644
--- a/src/Umbraco.Core/Migrations/Expressions/Create/Column/CreateColumnBuilder.cs
+++ b/src/Umbraco.Core/Migrations/Expressions/Create/Column/CreateColumnBuilder.cs
@@ -1,6 +1,7 @@
using System.Data;
using NPoco;
using Umbraco.Core.Migrations.Expressions.Common.Expressions;
+using Umbraco.Core.Persistence.DatabaseAnnotations;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
namespace Umbraco.Core.Migrations.Expressions.Create.Column
@@ -112,8 +113,8 @@ namespace Umbraco.Core.Migrations.Expressions.Create.Column
var index = new CreateIndexExpression(_context, new IndexDefinition
{
Name = indexName,
- TableName = Expression.TableName,
- IsUnique = true
+ TableName = Expression.TableName,
+ IndexType = IndexTypes.UniqueNonClustered
});
index.Index.Columns.Add(new IndexColumnDefinition
diff --git a/src/Umbraco.Core/Migrations/Expressions/Create/Index/CreateIndexBuilder.cs b/src/Umbraco.Core/Migrations/Expressions/Create/Index/CreateIndexBuilder.cs
index ad8ac7f22d..1f2cb93f95 100644
--- a/src/Umbraco.Core/Migrations/Expressions/Create/Index/CreateIndexBuilder.cs
+++ b/src/Umbraco.Core/Migrations/Expressions/Create/Index/CreateIndexBuilder.cs
@@ -56,41 +56,29 @@ namespace Umbraco.Core.Migrations.Expressions.Create.Index
///
ICreateIndexOnColumnBuilder ICreateIndexColumnOptionsBuilder.Unique()
- {
- Expression.Index.IsUnique = true;
- //if it is Unique then it must be unique nonclustered and set the other flags
- Expression.Index.IndexType = IndexTypes.UniqueNonClustered;
- Expression.Index.IsClustered = false;
+ {
+ Expression.Index.IndexType = IndexTypes.UniqueNonClustered;
return this;
}
///
public ICreateIndexOnColumnBuilder NonClustered()
{
- Expression.Index.IndexType = IndexTypes.NonClustered;
- Expression.Index.IsClustered = false;
- Expression.Index.IndexType = IndexTypes.NonClustered;
- Expression.Index.IsUnique = false;
+ Expression.Index.IndexType = IndexTypes.NonClustered;
return this;
}
///
public ICreateIndexOnColumnBuilder Clustered()
- {
- Expression.Index.IndexType = IndexTypes.Clustered;
- Expression.Index.IsClustered = true;
- //if it is clustered then we have to change the index type set the other flags
- Expression.Index.IndexType = IndexTypes.Clustered;
- Expression.Index.IsClustered = true;
- Expression.Index.IsUnique = false;
- return this;
+ {
+ Expression.Index.IndexType = IndexTypes.Clustered;
+ return this;
}
///
ICreateIndexOnColumnBuilder ICreateIndexOptionsBuilder.Unique()
{
- Expression.Index.IndexType = IndexTypes.UniqueNonClustered;
- Expression.Index.IsUnique = true;
+ Expression.Index.IndexType = IndexTypes.UniqueNonClustered;
return this;
}
}
diff --git a/src/Umbraco.Core/Migrations/Expressions/Create/Table/CreateTableBuilder.cs b/src/Umbraco.Core/Migrations/Expressions/Create/Table/CreateTableBuilder.cs
index 10836fd228..f765a58169 100644
--- a/src/Umbraco.Core/Migrations/Expressions/Create/Table/CreateTableBuilder.cs
+++ b/src/Umbraco.Core/Migrations/Expressions/Create/Table/CreateTableBuilder.cs
@@ -3,6 +3,7 @@ using NPoco;
using Umbraco.Core.Migrations.Expressions.Common.Expressions;
using Umbraco.Core.Migrations.Expressions.Create.Expressions;
using Umbraco.Core.Persistence;
+using Umbraco.Core.Persistence.DatabaseAnnotations;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
namespace Umbraco.Core.Migrations.Expressions.Create.Table
@@ -176,8 +177,8 @@ namespace Umbraco.Core.Migrations.Expressions.Create.Table
{
Name = indexName,
SchemaName = Expression.SchemaName,
- TableName = Expression.TableName,
- IsUnique = true
+ TableName = Expression.TableName,
+ IndexType = IndexTypes.UniqueNonClustered
});
index.Index.Columns.Add(new IndexColumnDefinition
diff --git a/src/Umbraco.Core/Migrations/IMigration.cs b/src/Umbraco.Core/Migrations/IMigration.cs
index 53b7874b3a..c929234f77 100644
--- a/src/Umbraco.Core/Migrations/IMigration.cs
+++ b/src/Umbraco.Core/Migrations/IMigration.cs
@@ -7,6 +7,9 @@ namespace Umbraco.Core.Migrations
///
public interface IMigration : IDiscoverable
{
+ ///
+ /// Executes the migration.
+ ///
void Migrate();
}
}
diff --git a/src/Umbraco.Core/Migrations/IMigrationContext.cs b/src/Umbraco.Core/Migrations/IMigrationContext.cs
index 4db1b07b63..80ba78b6de 100644
--- a/src/Umbraco.Core/Migrations/IMigrationContext.cs
+++ b/src/Umbraco.Core/Migrations/IMigrationContext.cs
@@ -24,8 +24,13 @@ namespace Umbraco.Core.Migrations
ISqlContext SqlContext { get; }
///
- /// Gets the expression index.
+ /// Gets or sets the expression index.
///
int Index { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether an expression is being built.
+ ///
+ bool BuildingExpression { get; set; }
}
}
diff --git a/src/Umbraco.Core/Migrations/IncompleteMigrationExpressionException.cs b/src/Umbraco.Core/Migrations/IncompleteMigrationExpressionException.cs
new file mode 100644
index 0000000000..91d1838d6f
--- /dev/null
+++ b/src/Umbraco.Core/Migrations/IncompleteMigrationExpressionException.cs
@@ -0,0 +1,28 @@
+using System;
+
+namespace Umbraco.Core.Migrations
+{
+ ///
+ /// Represents errors that occurs when a migration exception is not executed.
+ ///
+ ///
+ /// Migration expression such as Alter.Table(...).Do() *must* end with Do() else they are
+ /// not executed. When a non-executed expression is detected, an IncompleteMigrationExpressionException
+ /// is thrown.
+ ///
+ public class IncompleteMigrationExpressionException : Exception
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public IncompleteMigrationExpressionException()
+ { }
+
+ ///
+ /// Initializes a new instance of the class with a message.
+ ///
+ public IncompleteMigrationExpressionException(string message)
+ : base(message)
+ { }
+ }
+}
diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs
index 30f7ba8834..64be8161f2 100644
--- a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs
+++ b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs
@@ -81,7 +81,8 @@ namespace Umbraco.Core.Migrations.Install
typeof (ConsentDto),
typeof (AuditEntryDto),
typeof (ContentVersionCultureVariationDto),
- typeof (DocumentCultureVariationDto)
+ typeof (DocumentCultureVariationDto),
+ typeof (ContentScheduleDto)
};
///
diff --git a/src/Umbraco.Core/Migrations/MigrationBase.cs b/src/Umbraco.Core/Migrations/MigrationBase.cs
index 9fbee0ed92..58edcae80a 100644
--- a/src/Umbraco.Core/Migrations/MigrationBase.cs
+++ b/src/Umbraco.Core/Migrations/MigrationBase.cs
@@ -62,42 +62,65 @@ namespace Umbraco.Core.Migrations
///
protected Sql Sql(string sql, params object[] args) => Context.SqlContext.Sql(sql, args);
- ///
+ ///
+ /// Executes the migration.
+ ///
public abstract void Migrate();
+ ///
+ void IMigration.Migrate()
+ {
+ Migrate();
+
+ // ensure there is no building expression
+ // ie we did not forget to .Do() an expression
+ if (Context.BuildingExpression)
+ throw new IncompleteMigrationExpressionException("The migration has run, but leaves an expression that has not run.");
+ }
+
+ // ensures we are not already building,
+ // ie we did not forget to .Do() an expression
+ private T BeginBuild(T builder)
+ {
+ if (Context.BuildingExpression)
+ throw new IncompleteMigrationExpressionException("Cannot create a new expression: the previous expression has not run.");
+ Context.BuildingExpression = true;
+ return builder;
+ }
+
///
/// Builds an Alter expression.
///
- public IAlterBuilder Alter => new AlterBuilder(Context);
+ public IAlterBuilder Alter => BeginBuild(new AlterBuilder(Context));
///
/// Builds a Create expression.
///
- public ICreateBuilder Create => new CreateBuilder(Context);
+ public ICreateBuilder Create => BeginBuild(new CreateBuilder(Context));
///
/// Builds a Delete expression.
///
- public IDeleteBuilder Delete => new DeleteBuilder(Context);
+ public IDeleteBuilder Delete => BeginBuild(new DeleteBuilder(Context));
///
/// Builds an Execute expression.
///
- public IExecuteBuilder Execute => new ExecuteBuilder(Context);
+ public IExecuteBuilder Execute => BeginBuild(new ExecuteBuilder(Context));
///
/// Builds an Insert expression.
///
- public IInsertBuilder Insert => new InsertBuilder(Context);
+ public IInsertBuilder Insert => BeginBuild(new InsertBuilder(Context));
///
/// Builds a Rename expression.
///
- public IRenameBuilder Rename => new RenameBuilder(Context);
+ public IRenameBuilder Rename => BeginBuild(new RenameBuilder(Context));
///
/// Builds an Update expression.
///
- public IUpdateBuilder Update => new UpdateBuilder(Context);
+ public IUpdateBuilder Update => BeginBuild(new UpdateBuilder(Context));
}
}
diff --git a/src/Umbraco.Core/Migrations/MigrationContext.cs b/src/Umbraco.Core/Migrations/MigrationContext.cs
index d0802c813d..da454fab03 100644
--- a/src/Umbraco.Core/Migrations/MigrationContext.cs
+++ b/src/Umbraco.Core/Migrations/MigrationContext.cs
@@ -4,20 +4,33 @@ using Umbraco.Core.Persistence;
namespace Umbraco.Core.Migrations
{
+ ///
+ /// Represents a migration context.
+ ///
internal class MigrationContext : IMigrationContext
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
public MigrationContext(IUmbracoDatabase database, ILogger logger)
{
Database = database ?? throw new ArgumentNullException(nameof(database));
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
+ ///
public ILogger Logger { get; }
+ ///
public IUmbracoDatabase Database { get; }
+ ///
public ISqlContext SqlContext => Database.SqlContext;
+ ///
public int Index { get; set; }
+
+ ///
+ public bool BuildingExpression { get; set; }
}
}
diff --git a/src/Umbraco.Core/Migrations/MigrationExpressionBase.cs b/src/Umbraco.Core/Migrations/MigrationExpressionBase.cs
index 4c5a1f1aa7..6ac92a07aa 100644
--- a/src/Umbraco.Core/Migrations/MigrationExpressionBase.cs
+++ b/src/Umbraco.Core/Migrations/MigrationExpressionBase.cs
@@ -50,6 +50,7 @@ namespace Umbraco.Core.Migrations
if (_executed)
throw new InvalidOperationException("This expression has already been executed.");
_executed = true;
+ Context.BuildingExpression = false;
var sql = GetSql();
diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs
index fb0f18700c..ec49544976 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs
@@ -119,7 +119,7 @@ namespace Umbraco.Core.Migrations.Upgrade
Chain("{517CE9EA-36D7-472A-BF4B-A0D6FB1B8F89}"); // from 7.12.0
Chain("{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}"); // from 7.12.0
//Chain("{2C87AA47-D1BC-4ECB-8A73-2D8D1046C27F}"); // stephan added that one = merge conflict, remove
-
+
Chain("{8B14CEBD-EE47-4AAD-A841-93551D917F11}"); // add andy's after others, with a new target state
From("{CF51B39B-9B9A-4740-BB7C-EAF606A7BFBF}") // and provide a path out of andy's
.CopyChain("{39E5B1F7-A50B-437E-B768-1723AEC45B65}", "{BBD99901-1545-40E4-8A5A-D7A675C7D2F2}", "{8B14CEBD-EE47-4AAD-A841-93551D917F11}"); // to next
@@ -141,6 +141,9 @@ namespace Umbraco.Core.Migrations.Upgrade
Chain("{8804D8E8-FE62-4E3A-B8A2-C047C2118C38}");
Chain("{23275462-446E-44C7-8C2C-3B8C1127B07D}");
Chain("{6B251841-3069-4AD5-8AE9-861F9523E8DA}");
+ Chain("{EE429F1B-9B26-43CA-89F8-A86017C809A3}");
+ Chain("{08919C4B-B431-449C-90EC-2B8445B5C6B1}");
+ Chain("{7EB0254C-CB8B-4C75-B15B-D48C55B449EB}");
//FINAL
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/SetDefaultTagsStorageType.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/SetDefaultTagsStorageType.cs
index d8f2d37067..c8d65961f4 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/SetDefaultTagsStorageType.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_7_12_0/SetDefaultTagsStorageType.cs
@@ -1,47 +1,51 @@
-using System;
-using System.Linq;
-using Newtonsoft.Json;
+using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
-using Umbraco.Core.Logging;
+using Umbraco.Core.Models;
+using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.Dtos;
-using Umbraco.Core.Persistence.SqlSyntax;
+using Umbraco.Core.PropertyEditors;
namespace Umbraco.Core.Migrations.Upgrade.V_7_12_0
{
///
- /// Set the default storageType for the tags datatype to "CSV" to ensure backwards compatibilty since the default is going to be JSON in new versions
- ///
+ /// Set the default storageType for the tags datatype to "CSV" to ensure backwards compatibility since the default is going to be JSON in new versions.
+ ///
public class SetDefaultTagsStorageType : MigrationBase
{
- public SetDefaultTagsStorageType(IMigrationContext context) : base(context)
- {
- }
+ public SetDefaultTagsStorageType(IMigrationContext context)
+ : base(context)
+ { }
+
+ // dummy editor for deserialization
+ private class TagConfigurationEditor : ConfigurationEditor
+ { }
public override void Migrate()
{
- if (Context?.Database == null) return;
+ // get all Umbraco.Tags datatypes
+ var dataTypeDtos = Database.Fetch(Context.SqlContext.Sql()
+ .Select()
+ .From()
+ .Where(x => x.EditorAlias == Constants.PropertyEditors.Aliases.Tags));
- // We need to get all datatypes with an alias of "umbraco.tags" so we can loop over them and set the missing values if needed
- var datatypes = Context.Database.Fetch();
- var tagsDataTypes = datatypes.Where(x => string.Equals(x.EditorAlias, Constants.PropertyEditors.Aliases.Tags, StringComparison.InvariantCultureIgnoreCase));
+ // get a dummy editor for deserialization
+ var editor = new TagConfigurationEditor();
- foreach (var datatype in tagsDataTypes)
+ foreach (var dataTypeDto in dataTypeDtos)
{
- var dataTypePreValues = JsonConvert.DeserializeObject(datatype.Configuration);
+ // need to check storageType on raw dictionary, as TagConfiguration would have a default value
+ var dictionary = JsonConvert.DeserializeObject(dataTypeDto.Configuration);
- // We need to check if the node has a "storageType" set
- if (!dataTypePreValues.ContainsKey("storageType"))
+ // if missing, use TagConfiguration to properly update the configuration
+ // due to ... reasons ... the key can start with a lower or upper 'S'
+ if (!dictionary.ContainsKey("storageType") && !dictionary.ContainsKey("StorageType"))
{
- dataTypePreValues["storageType"] = "Csv";
+ var configuration = (TagConfiguration)editor.FromDatabase(dataTypeDto.Configuration);
+ configuration.StorageType = TagsStorageType.Csv;
+ dataTypeDto.Configuration = ConfigurationEditor.ToDatabase(configuration);
+ Database.Update(dataTypeDto);
}
-
- Update.Table(Constants.DatabaseSchema.Tables.DataType)
- .Set(new { config = JsonConvert.SerializeObject(dataTypePreValues) })
- .Where(new { nodeId = datatype.NodeId })
- .Do();
}
}
-
-
}
}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs
index e5cda85168..ed2990aff7 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropDownPropertyEditorsMigration.cs
@@ -1,92 +1,105 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using Newtonsoft.Json;
+using Umbraco.Core.Cache;
using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.Dtos;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
+using Umbraco.Core.Sync;
namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
{
public class DropDownPropertyEditorsMigration : MigrationBase
{
- public DropDownPropertyEditorsMigration(IMigrationContext context) : base(context)
+ private readonly CacheRefresherCollection _cacheRefreshers;
+ private readonly IServerMessenger _serverMessenger;
+
+ public DropDownPropertyEditorsMigration(IMigrationContext context, CacheRefresherCollection cacheRefreshers, IServerMessenger serverMessenger)
+ : base(context)
{
+ _cacheRefreshers = cacheRefreshers;
+ _serverMessenger = serverMessenger;
}
+ // dummy editor for deserialization
+ private class ValueListConfigurationEditor : ConfigurationEditor
+ { }
+
public override void Migrate()
{
//need to convert the old drop down data types to use the new one
- var oldDropDowns = Database.Fetch(Sql()
+ var dataTypes = Database.Fetch(Sql()
.Select()
.From()
.Where(x => x.EditorAlias.Contains(".DropDown")));
- foreach (var dd in oldDropDowns)
+
+ foreach (var dataType in dataTypes)
{
- //nothing to change if there is no config
- if (dd.Configuration.IsNullOrWhiteSpace()) continue;
-
ValueListConfiguration config;
- try
- {
- config = JsonConvert.DeserializeObject(dd.Configuration);
- }
- catch (Exception ex)
- {
- Logger.Error(
- ex, "Invalid drop down configuration detected: \"{Configuration}\", cannot convert editor, values will be cleared",
- dd.Configuration);
- dd.Configuration = null;
- Database.Update(dd);
- continue;
- }
- var propDataSql = Sql().Select().From()
- .InnerJoin().On(x => x.Id, x => x.PropertyTypeId)
- .InnerJoin().On(x => x.NodeId, x => x.DataTypeId)
- .Where(x => x.DataTypeId == dd.NodeId);
-
- var propDatas = Database.Query(propDataSql);
- var toUpdate = new List();
- foreach (var propData in propDatas)
+ if (!dataType.Configuration.IsNullOrWhiteSpace())
{
- if (UpdatePropertyDataDto(propData, config))
+ // parse configuration, and update everything accordingly
+ try
{
- //update later, we are iterating all values right now so no SQL can be run inside of this iteration (i.e. Query)
- toUpdate.Add(propData);
+ config = (ValueListConfiguration) new ValueListConfigurationEditor().FromDatabase(dataType.Configuration);
}
- }
+ catch (Exception ex)
+ {
+ Logger.Error(
+ ex, "Invalid drop down configuration detected: \"{Configuration}\", cannot convert editor, values will be cleared",
+ dataType.Configuration);
- //run the property data updates
- foreach (var propData in toUpdate)
+ // reset
+ config = new ValueListConfiguration();
+ }
+
+ // get property data dtos
+ var propertyDataDtos = Database.Fetch(Sql()
+ .Select()
+ .From()
+ .InnerJoin().On((pt, pd) => pt.Id == pd.PropertyTypeId)
+ .InnerJoin().On((dt, pt) => dt.NodeId == pt.DataTypeId)
+ .Where(x => x.DataTypeId == dataType.NodeId));
+
+ // update dtos
+ var updatedDtos = propertyDataDtos.Where(x => UpdatePropertyDataDto(x, config));
+
+ // persist changes
+ foreach (var propertyDataDto in updatedDtos)
+ Database.Update(propertyDataDto);
+ }
+ else
{
- Database.Update(propData);
+ // default configuration
+ config = new ValueListConfiguration();
}
var requiresCacheRebuild = false;
- switch (dd.EditorAlias)
+ switch (dataType.EditorAlias)
{
case string ea when ea.InvariantEquals("Umbraco.DropDown"):
- UpdateDataType(dd, config, false);
+ UpdateDataType(dataType, config, false);
break;
case string ea when ea.InvariantEquals("Umbraco.DropdownlistPublishingKeys"):
- UpdateDataType(dd, config, false);
+ UpdateDataType(dataType, config, false);
requiresCacheRebuild = true;
break;
case string ea when ea.InvariantEquals("Umbraco.DropDownMultiple"):
- UpdateDataType(dd, config, true);
+ UpdateDataType(dataType, config, true);
break;
case string ea when ea.InvariantEquals("Umbraco.DropdownlistMultiplePublishKeys"):
- UpdateDataType(dd, config, true);
+ UpdateDataType(dataType, config, true);
requiresCacheRebuild = true;
break;
}
if (requiresCacheRebuild)
{
- //TODO: How to force rebuild the cache?
+ var dataTypeCacheRefresher = _cacheRefreshers[Guid.Parse("35B16C25-A17E-45D7-BC8F-EDAB1DCC28D2")];
+ _serverMessenger.PerformRefreshAll(dataTypeCacheRefresher);
}
}
}
@@ -94,13 +107,15 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
private void UpdateDataType(DataTypeDto dataType, ValueListConfiguration config, bool isMultiple)
{
dataType.EditorAlias = Constants.PropertyEditors.Aliases.DropDownListFlexible;
- var flexConfig = new
- {
- multiple = isMultiple,
- items = config.Items
- };
dataType.DbType = ValueStorageType.Nvarchar.ToString();
- dataType.Configuration = JsonConvert.SerializeObject(flexConfig);
+
+ var flexConfig = new DropDownFlexibleConfiguration
+ {
+ Items = config.Items,
+ Multiple = isMultiple
+ };
+ dataType.Configuration = ConfigurationEditor.ToDatabase(flexConfig);
+
Database.Update(dataType);
}
@@ -137,7 +152,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
Logger.Warn(
"Could not find associated data type configuration for stored Id {DataTypeId}", id);
canConvert = false;
- }
+ }
}
if (canConvert)
{
@@ -146,7 +161,6 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
propData.IntegerValue = null;
return true;
}
-
}
return false;
@@ -167,6 +181,5 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
return null;
}
-
}
}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropTaskTables.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropTaskTables.cs
index f706671022..e8fd4f409e 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropTaskTables.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropTaskTables.cs
@@ -1,5 +1,6 @@
namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
{
+
public class DropTaskTables : MigrationBase
{
public DropTaskTables(IMigrationContext context)
@@ -8,8 +9,10 @@
public override void Migrate()
{
- Delete.Table("cmsTaskType");
- Delete.Table("cmsTask");
+ if (TableExists("cmsTaskType"))
+ Delete.Table("cmsTaskType");
+ if (TableExists("cmsTask"))
+ Delete.Table("cmsTask");
}
}
}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropTemplateDesignColumn.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropTemplateDesignColumn.cs
new file mode 100644
index 0000000000..f1b25403d4
--- /dev/null
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DropTemplateDesignColumn.cs
@@ -0,0 +1,15 @@
+namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
+{
+ public class DropTemplateDesignColumn : MigrationBase
+ {
+ public DropTemplateDesignColumn(IMigrationContext context)
+ : base(context)
+ { }
+
+ public override void Migrate()
+ {
+ if(ColumnExists("cmsTemplate", "design"))
+ Delete.Column("design").FromTable("cmsTemplate").Do();
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs
new file mode 100644
index 0000000000..cd4de179bd
--- /dev/null
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TablesForScheduledPublishing.cs
@@ -0,0 +1,46 @@
+using NPoco;
+using System;
+using Umbraco.Core.Models;
+using Umbraco.Core.Persistence.Dtos;
+
+namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
+{
+ public class TablesForScheduledPublishing : MigrationBase
+ {
+ public TablesForScheduledPublishing(IMigrationContext context)
+ : base(context)
+ { }
+
+ public override void Migrate()
+ {
+ //Get anything currently scheduled
+ var scheduleSql = new Sql()
+ .Select("nodeId", "releaseDate", "expireDate")
+ .From("umbracoDocument")
+ .Where("releaseDate IS NOT NULL OR expireDate IS NOT NULL");
+ var schedules = Database.Dictionary (scheduleSql);
+
+ //drop old cols
+ Delete.Column("releaseDate").FromTable("umbracoDocument").Do();
+ Delete.Column("expireDate").FromTable("umbracoDocument").Do();
+ //add new table
+ Create.Table().Do();
+
+ //migrate the schedule
+ foreach(var s in schedules)
+ {
+ var date = s.Value.releaseDate;
+ var action = ContentScheduleAction.Release.ToString();
+ if (!date.HasValue)
+ {
+ date = s.Value.expireDate;
+ action = ContentScheduleAction.Expire.ToString();
+ }
+
+ Insert.IntoTable(ContentScheduleDto.TableName)
+ .Row(new { nodeId = s.Key, date = date.Value, action = action })
+ .Do();
+ }
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TagsMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TagsMigration.cs
index 89cb7e74ac..5dc5e0b6fe 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TagsMigration.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TagsMigration.cs
@@ -18,7 +18,9 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
// kill unused parentId column
Delete.ForeignKey("FK_cmsTags_cmsTags").OnTable(Constants.DatabaseSchema.Tables.Tag).Do();
- Delete.Column("ParentId").FromTable(Constants.DatabaseSchema.Tables.Tag);
+ Delete.Column("ParentId").FromTable(Constants.DatabaseSchema.Tables.Tag).Do();
}
}
+
+ // fixes TagsMigration that... originally failed to properly drop the ParentId column
}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TagsMigrationFix.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TagsMigrationFix.cs
new file mode 100644
index 0000000000..4ee95c9f58
--- /dev/null
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/TagsMigrationFix.cs
@@ -0,0 +1,16 @@
+namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
+{
+ public class TagsMigrationFix : MigrationBase
+ {
+ public TagsMigrationFix(IMigrationContext context)
+ : base(context)
+ { }
+
+ public override void Migrate()
+ {
+ // kill unused parentId column, if it still exists
+ if (ColumnExists(Constants.DatabaseSchema.Tables.Tag, "ParentId"))
+ Delete.Column("ParentId").FromTable(Constants.DatabaseSchema.Tables.Tag).Do();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs
index b048998691..3f6e387dec 100644
--- a/src/Umbraco.Core/Models/Content.cs
+++ b/src/Umbraco.Core/Models/Content.cs
@@ -17,10 +17,9 @@ namespace Umbraco.Core.Models
{
private IContentType _contentType;
private ITemplate _template;
+ private ContentScheduleCollection _schedule;
private bool _published;
private PublishedState _publishedState;
- private DateTime? _releaseDate;
- private DateTime? _expireDate;
private ContentCultureInfosCollection _publishInfos;
private ContentCultureInfosCollection _publishInfosOrig;
private HashSet _editedCultures;
@@ -86,11 +85,43 @@ namespace Umbraco.Core.Models
{
public readonly PropertyInfo TemplateSelector = ExpressionHelper.GetPropertyInfo(x => x.Template);
public readonly PropertyInfo PublishedSelector = ExpressionHelper.GetPropertyInfo(x => x.Published);
- public readonly PropertyInfo ReleaseDateSelector = ExpressionHelper.GetPropertyInfo(x => x.ReleaseDate);
- public readonly PropertyInfo ExpireDateSelector = ExpressionHelper.GetPropertyInfo(x => x.ExpireDate);
+ public readonly PropertyInfo ContentScheduleSelector = ExpressionHelper.GetPropertyInfo(x => x.ContentSchedule);
public readonly PropertyInfo PublishCultureInfosSelector = ExpressionHelper.GetPropertyInfo>(x => x.PublishCultureInfos);
}
+ ///
+ [DoNotClone]
+ public ContentScheduleCollection ContentSchedule
+ {
+ get
+ {
+ if (_schedule == null)
+ {
+ _schedule = new ContentScheduleCollection();
+ _schedule.CollectionChanged += ScheduleCollectionChanged;
+ }
+ return _schedule;
+ }
+ set
+ {
+ if(_schedule != null)
+ _schedule.CollectionChanged -= ScheduleCollectionChanged;
+ SetPropertyValueAndDetectChanges(value, ref _schedule, Ps.Value.ContentScheduleSelector);
+ if (_schedule != null)
+ _schedule.CollectionChanged += ScheduleCollectionChanged;
+ }
+ }
+
+ ///
+ /// Collection changed event handler to ensure the schedule field is set to dirty when the schedule changes
+ ///
+ ///
+ ///
+ private void ScheduleCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+ {
+ OnPropertyChanged(Ps.Value.ContentScheduleSelector);
+ }
+
///
/// Gets or sets the template used by the Content.
/// This is used to override the default one from the ContentType.
@@ -100,35 +131,12 @@ namespace Umbraco.Core.Models
/// the Default template from the ContentType will be returned.
///
[DataMember]
- public virtual ITemplate Template
+ public ITemplate Template
{
get => _template ?? _contentType.DefaultTemplate;
set => SetPropertyValueAndDetectChanges(value, ref _template, Ps.Value.TemplateSelector);
}
- ///
- /// Gets the current status of the Content
- ///
- [IgnoreDataMember]
- public ContentStatus Status
- {
- get
- {
- if(Trashed)
- return ContentStatus.Trashed;
-
- if(ExpireDate.HasValue && ExpireDate.Value > DateTime.MinValue && DateTime.Now > ExpireDate.Value)
- return ContentStatus.Expired;
-
- if(ReleaseDate.HasValue && ReleaseDate.Value > DateTime.MinValue && ReleaseDate.Value > DateTime.Now)
- return ContentStatus.AwaitingRelease;
-
- if(Published)
- return ContentStatus.Published;
-
- return ContentStatus.Unpublished;
- }
- }
///
/// Gets or sets a value indicating whether this content item is published or not.
@@ -169,26 +177,6 @@ namespace Umbraco.Core.Models
[IgnoreDataMember]
public bool Edited { get; internal set; }
- ///
- /// The date this Content should be released and thus be published
- ///
- [DataMember]
- public DateTime? ReleaseDate
- {
- get => _releaseDate;
- set => SetPropertyValueAndDetectChanges(value, ref _releaseDate, Ps.Value.ReleaseDateSelector);
- }
-
- ///
- /// The date this Content should expire and thus be unpublished
- ///
- [DataMember]
- public DateTime? ExpireDate
- {
- get => _expireDate;
- set => SetPropertyValueAndDetectChanges(value, ref _expireDate, Ps.Value.ExpireDateSelector);
- }
-
///
/// Gets the ContentType used by this content object
///
@@ -229,7 +217,7 @@ namespace Umbraco.Core.Models
public bool WasCulturePublished(string culture)
// just check _publishInfosOrig - a copy of _publishInfos
// a non-available culture could not become published anyways
- => _publishInfosOrig != null && _publishInfosOrig.ContainsKey(culture);
+ => _publishInfosOrig != null && _publishInfosOrig.ContainsKey(culture);
// adjust dates to sync between version, cultures etc
// used by the repo when persisting
@@ -310,6 +298,9 @@ namespace Umbraco.Core.Models
if (_publishInfos == null) return;
_publishInfos.Remove(culture);
if (_publishInfos.Count == 0) _publishInfos = null;
+
+ // set the culture to be dirty - it's been modified
+ TouchCultureInfo(culture);
}
// sets a publish edited
@@ -351,7 +342,7 @@ namespace Umbraco.Core.Models
public bool Blueprint { get; internal set; }
///
- public virtual bool PublishCulture(string culture = "*")
+ public bool PublishCulture(string culture = "*")
{
culture = culture.NullOrWhiteSpaceAsNull();
@@ -404,7 +395,7 @@ namespace Umbraco.Core.Models
}
///
- public virtual void UnpublishCulture(string culture = "*")
+ public void UnpublishCulture(string culture = "*")
{
culture = culture.NullOrWhiteSpaceAsNull();
@@ -520,7 +511,14 @@ namespace Umbraco.Core.Models
clonedContent._publishInfos = (ContentCultureInfosCollection) _publishInfos.DeepClone(); //manually deep clone
clonedContent._publishInfos.CollectionChanged += clonedContent.PublishNamesCollectionChanged; //re-assign correct event handler
}
-
+
+ //if properties exist then deal with event bindings
+ if (clonedContent._schedule != null)
+ {
+ clonedContent._schedule.CollectionChanged -= ScheduleCollectionChanged; //clear this event handler if any
+ clonedContent._schedule = (ContentScheduleCollection)_schedule.DeepClone(); //manually deep clone
+ clonedContent._schedule.CollectionChanged += clonedContent.ScheduleCollectionChanged; //re-assign correct event handler
+ }
}
}
}
diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs
index 7e70238d2f..b0c786d4b0 100644
--- a/src/Umbraco.Core/Models/ContentBase.cs
+++ b/src/Umbraco.Core/Models/ContentBase.cs
@@ -159,7 +159,7 @@ namespace Umbraco.Core.Models
///
[DataMember]
public virtual IReadOnlyDictionary CultureInfos => _cultureInfos ?? NoInfos;
-
+
///
public string GetCultureName(string culture)
{
@@ -222,6 +222,12 @@ namespace Umbraco.Core.Models
_cultureInfos = null;
}
+ protected void TouchCultureInfo(string culture)
+ {
+ if (_cultureInfos == null || !_cultureInfos.TryGetValue(culture, out var infos)) return;
+ _cultureInfos.AddOrUpdate(culture, infos.Name, DateTime.Now);
+ }
+
// internal for repository
internal void SetCultureInfo(string culture, string name, DateTime date)
{
@@ -235,7 +241,7 @@ namespace Umbraco.Core.Models
{
_cultureInfos = new ContentCultureInfosCollection();
_cultureInfos.CollectionChanged += CultureInfosCollectionChanged;
- }
+ }
_cultureInfos.AddOrUpdate(culture, name, date);
}
@@ -368,7 +374,7 @@ namespace Umbraco.Core.Models
#endregion
#region Validation
-
+
///
public virtual Property[] ValidateProperties(string culture = "*")
{
@@ -496,7 +502,6 @@ namespace Umbraco.Core.Models
clonedContent._properties = (PropertyCollection) _properties.DeepClone(); //manually deep clone
clonedContent._properties.CollectionChanged += clonedContent.PropertiesChanged; //re-assign correct event handler
}
-
}
}
}
diff --git a/src/Umbraco.Core/Models/ContentEditing/IContentAppDefinition.cs b/src/Umbraco.Core/Models/ContentEditing/IContentAppFactory.cs
similarity index 89%
rename from src/Umbraco.Core/Models/ContentEditing/IContentAppDefinition.cs
rename to src/Umbraco.Core/Models/ContentEditing/IContentAppFactory.cs
index 2d30fc6ba9..6b8d90d418 100644
--- a/src/Umbraco.Core/Models/ContentEditing/IContentAppDefinition.cs
+++ b/src/Umbraco.Core/Models/ContentEditing/IContentAppFactory.cs
@@ -4,9 +4,9 @@ using Umbraco.Core.Models.Membership;
namespace Umbraco.Core.Models.ContentEditing
{
///
- /// Represents a content app definition.
+ /// Represents a content app factory.
///
- public interface IContentAppDefinition
+ public interface IContentAppFactory
{
///
/// Gets the content app for an object.
diff --git a/src/Umbraco.Core/Models/ContentSchedule.cs b/src/Umbraco.Core/Models/ContentSchedule.cs
new file mode 100644
index 0000000000..cac4a0fd1c
--- /dev/null
+++ b/src/Umbraco.Core/Models/ContentSchedule.cs
@@ -0,0 +1,77 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace Umbraco.Core.Models
+{
+ ///
+ /// Represents a scheduled action for a document.
+ ///
+ [Serializable]
+ [DataContract(IsReference = true)]
+ public class ContentSchedule : IDeepCloneable
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ContentSchedule(string culture, DateTime date, ContentScheduleAction action)
+ {
+ Id = Guid.Empty; // will be assigned by document repository
+ Culture = culture;
+ Date = date;
+ Action = action;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ContentSchedule(Guid id, string culture, DateTime date, ContentScheduleAction action)
+ {
+ Id = id;
+ Culture = culture;
+ Date = date;
+ Action = action;
+ }
+
+ ///
+ /// Gets the unique identifier of the document targeted by the scheduled action.
+ ///
+ [DataMember]
+ public Guid Id { get; internal set; }
+
+ ///
+ /// Gets the culture of the scheduled action.
+ ///
+ ///
+ /// string.Empty represents the invariant culture.
+ ///
+ [DataMember]
+ public string Culture { get; }
+
+ ///
+ /// Gets the date of the scheduled action.
+ ///
+ [DataMember]
+ public DateTime Date { get; }
+
+ ///
+ /// Gets the action to take.
+ ///
+ [DataMember]
+ public ContentScheduleAction Action { get; }
+
+ public override bool Equals(object obj)
+ => obj is ContentSchedule other && Equals(other);
+
+ public bool Equals(ContentSchedule other)
+ {
+ // don't compare Ids, two ContentSchedule are equal if they are for the same change
+ // for the same culture, on the same date - and the collection deals w/duplicates
+ return Culture.InvariantEquals(other.Culture) && Date == other.Date && Action == other.Action;
+ }
+
+ public object DeepClone()
+ {
+ return new ContentSchedule(Id, Culture, Date, Action);
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Models/ContentScheduleAction.cs b/src/Umbraco.Core/Models/ContentScheduleAction.cs
new file mode 100644
index 0000000000..0816f17731
--- /dev/null
+++ b/src/Umbraco.Core/Models/ContentScheduleAction.cs
@@ -0,0 +1,18 @@
+namespace Umbraco.Core.Models
+{
+ ///
+ /// Defines scheduled actions for documents.
+ ///
+ public enum ContentScheduleAction
+ {
+ ///
+ /// Release the document.
+ ///
+ Release,
+
+ ///
+ /// Expire the document.
+ ///
+ Expire
+ }
+}
diff --git a/src/Umbraco.Core/Models/ContentScheduleCollection.cs b/src/Umbraco.Core/Models/ContentScheduleCollection.cs
new file mode 100644
index 0000000000..4c06f8927d
--- /dev/null
+++ b/src/Umbraco.Core/Models/ContentScheduleCollection.cs
@@ -0,0 +1,223 @@
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+
+namespace Umbraco.Core.Models
+{
+ public class ContentScheduleCollection : INotifyCollectionChanged, IDeepCloneable, IEquatable
+ {
+ //underlying storage for the collection backed by a sorted list so that the schedule is always in order of date and that duplicate dates per culture are not allowed
+ private readonly Dictionary> _schedule
+ = new Dictionary>(StringComparer.InvariantCultureIgnoreCase);
+
+ public event NotifyCollectionChangedEventHandler CollectionChanged;
+
+ private void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
+ {
+ CollectionChanged?.Invoke(this, args);
+ }
+
+ ///
+ /// Add an existing schedule
+ ///
+ ///
+ public void Add(ContentSchedule schedule)
+ {
+ if (!_schedule.TryGetValue(schedule.Culture, out var changes))
+ {
+ changes = new SortedList();
+ _schedule[schedule.Culture] = changes;
+ }
+
+ //TODO: Below will throw if there are duplicate dates added, validate/return bool?
+ changes.Add(schedule.Date, schedule);
+
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, schedule));
+ }
+
+ ///
+ /// Adds a new schedule for invariant content
+ ///
+ ///
+ ///
+ public bool Add(DateTime? releaseDate, DateTime? expireDate)
+ {
+ return Add(string.Empty, releaseDate, expireDate);
+ }
+
+ ///
+ /// Adds a new schedule for a culture
+ ///
+ ///
+ ///
+ ///
+ /// true if successfully added, false if validation fails
+ public bool Add(string culture, DateTime? releaseDate, DateTime? expireDate)
+ {
+ if (culture == null) throw new ArgumentNullException(nameof(culture));
+ if (releaseDate.HasValue && expireDate.HasValue && releaseDate >= expireDate)
+ return false;
+
+ if (!releaseDate.HasValue && !expireDate.HasValue) return false;
+
+ //TODO: Do we allow passing in a release or expiry date that is before now?
+
+ if (!_schedule.TryGetValue(culture, out var changes))
+ {
+ changes = new SortedList();
+ _schedule[culture] = changes;
+ }
+
+ //TODO: Below will throw if there are duplicate dates added, should validate/return bool?
+ // but the bool won't indicate which date was in error, maybe have 2 diff methods to schedule start/end?
+
+ if (releaseDate.HasValue)
+ {
+ var entry = new ContentSchedule(culture, releaseDate.Value, ContentScheduleAction.Release);
+ changes.Add(releaseDate.Value, entry);
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, entry));
+ }
+
+ if (expireDate.HasValue)
+ {
+ var entry = new ContentSchedule(culture, expireDate.Value, ContentScheduleAction.Expire);
+ changes.Add(expireDate.Value, entry);
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, entry));
+ }
+
+ return true;
+ }
+
+ ///
+ /// Remove a scheduled change
+ ///
+ ///
+ public void Remove(ContentSchedule change)
+ {
+ if (_schedule.TryGetValue(change.Culture, out var s))
+ {
+ var removed = s.Remove(change.Date);
+ if (removed)
+ {
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, change));
+ if (s.Count == 0)
+ _schedule.Remove(change.Culture);
+ }
+
+ }
+ }
+
+ ///
+ /// Clear all of the scheduled change type for invariant content
+ ///
+ ///
+ /// If specified, will clear all entries with dates less than or equal to the value
+ public void Clear(ContentScheduleAction action, DateTime? changeDate = null)
+ {
+ Clear(string.Empty, action, changeDate);
+ }
+
+ ///
+ /// Clear all of the scheduled change type for the culture
+ ///
+ ///
+ ///
+ /// If specified, will clear all entries with dates less than or equal to the value
+ public void Clear(string culture, ContentScheduleAction action, DateTime? date = null)
+ {
+ if (!_schedule.TryGetValue(culture, out var schedules))
+ return;
+
+ var removes = schedules.Where(x => x.Value.Action == action && (!date.HasValue || x.Value.Date <= date.Value)).ToList();
+
+ foreach (var remove in removes)
+ {
+ var removed = schedules.Remove(remove.Value.Date);
+ if (!removed)
+ continue;
+
+ OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, remove.Value));
+ }
+
+ if (schedules.Count == 0)
+ _schedule.Remove(culture);
+ }
+
+ ///
+ /// Returns all pending schedules based on the date and type provided
+ ///
+ ///
+ ///
+ ///
+ public IReadOnlyList GetPending(ContentScheduleAction action, DateTime date)
+ {
+ return _schedule.Values.SelectMany(x => x.Values).Where(x => x.Date <= date).ToList();
+ }
+
+ ///
+ /// Gets the schedule for invariant content
+ ///
+ ///
+ public IEnumerable GetSchedule(ContentScheduleAction? action = null)
+ {
+ return GetSchedule(string.Empty, action);
+ }
+
+ ///
+ /// Gets the schedule for a culture
+ ///
+ ///
+ ///
+ ///
+ public IEnumerable GetSchedule(string culture, ContentScheduleAction? action = null)
+ {
+ if (_schedule.TryGetValue(culture, out var changes))
+ return action == null ? changes.Values : changes.Values.Where(x => x.Action == action.Value);
+ return Enumerable.Empty();
+ }
+
+ ///
+ /// Returns all schedules registered
+ ///
+ ///
+ public IReadOnlyList FullSchedule => _schedule.SelectMany(x => x.Value.Values).ToList();
+
+ public object DeepClone()
+ {
+ var clone = new ContentScheduleCollection();
+ foreach(var cultureSched in _schedule)
+ {
+ var list = new SortedList();
+ foreach (var schedEntry in cultureSched.Value)
+ list.Add(schedEntry.Key, (ContentSchedule)schedEntry.Value.DeepClone());
+ clone._schedule[cultureSched.Key] = list;
+ }
+ return clone;
+ }
+
+ public override bool Equals(object obj)
+ => obj is ContentScheduleCollection other && Equals(other);
+
+ public bool Equals(ContentScheduleCollection other)
+ {
+ if (other == null) return false;
+
+ var thisSched = _schedule;
+ var thatSched = other._schedule;
+
+ if (thisSched.Count != thatSched.Count)
+ return false;
+
+ foreach (var (culture, thisList) in thisSched)
+ {
+ // if culture is missing, or actions differ, false
+ if (!thatSched.TryGetValue(culture, out var thatList) || !thatList.SequenceEqual(thisList))
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Models/ContentStatus.cs b/src/Umbraco.Core/Models/ContentStatus.cs
index 4caf214399..1d35844874 100644
--- a/src/Umbraco.Core/Models/ContentStatus.cs
+++ b/src/Umbraco.Core/Models/ContentStatus.cs
@@ -4,20 +4,42 @@ using System.Runtime.Serialization;
namespace Umbraco.Core.Models
{
///
- /// Enum for the various statuses a Content object can have
+ /// Describes the states of a document, with regard to (schedule) publishing.
///
[Serializable]
[DataContract]
public enum ContentStatus
{
+ // typical flow:
+ // Unpublished (add release date)-> AwaitingRelease (release)-> Published (expire)-> Expired
+
+ ///
+ /// The document is not trashed, and not published.
+ ///
[EnumMember]
Unpublished,
+
+ ///
+ /// The document is published.
+ ///
[EnumMember]
Published,
+
+ ///
+ /// The document is not trashed, not published, after being unpublished by a scheduled action.
+ ///
[EnumMember]
Expired,
+
+ ///
+ /// The document is trashed.
+ ///
[EnumMember]
Trashed,
+
+ ///
+ /// The document is not trashed, not published, and pending publication by a scheduled action.
+ ///
[EnumMember]
AwaitingRelease
}
diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs
index caa63d7526..88b1179f6d 100644
--- a/src/Umbraco.Core/Models/ContentTypeBase.cs
+++ b/src/Umbraco.Core/Models/ContentTypeBase.cs
@@ -92,8 +92,8 @@ namespace Umbraco.Core.Models
public readonly PropertyInfo AllowedAsRootSelector = ExpressionHelper.GetPropertyInfo(x => x.AllowedAsRoot);
public readonly PropertyInfo IsContainerSelector = ExpressionHelper.GetPropertyInfo(x => x.IsContainer);
public readonly PropertyInfo AllowedContentTypesSelector = ExpressionHelper.GetPropertyInfo>(x => x.AllowedContentTypes);
- public readonly PropertyInfo PropertyGroupCollectionSelector = ExpressionHelper.GetPropertyInfo(x => x.PropertyGroups);
- public readonly PropertyInfo PropertyTypeCollectionSelector = ExpressionHelper.GetPropertyInfo>(x => x.PropertyTypes);
+ public readonly PropertyInfo PropertyGroupsSelector = ExpressionHelper.GetPropertyInfo(x => x.PropertyGroups);
+ public readonly PropertyInfo PropertyTypesSelector = ExpressionHelper.GetPropertyInfo>(x => x.PropertyTypes);
public readonly PropertyInfo HasPropertyTypeBeenRemovedSelector = ExpressionHelper.GetPropertyInfo(x => x.HasPropertyTypeBeenRemoved);
public readonly PropertyInfo VaryBy = ExpressionHelper.GetPropertyInfo(x => x.Variations);
@@ -106,12 +106,12 @@ namespace Umbraco.Core.Models
protected void PropertyGroupsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
- OnPropertyChanged(Ps.Value.PropertyGroupCollectionSelector);
+ OnPropertyChanged(Ps.Value.PropertyGroupsSelector);
}
protected void PropertyTypesChanged(object sender, NotifyCollectionChangedEventArgs e)
{
- OnPropertyChanged(Ps.Value.PropertyTypeCollectionSelector);
+ OnPropertyChanged(Ps.Value.PropertyTypesSelector);
}
///
@@ -263,6 +263,8 @@ namespace Umbraco.Core.Models
get => _noGroupPropertyTypes;
set
{
+ if (_noGroupPropertyTypes != null)
+ _noGroupPropertyTypes.CollectionChanged -= PropertyTypesChanged;
_noGroupPropertyTypes = new PropertyTypeCollection(IsPublishing, value);
_noGroupPropertyTypes.CollectionChanged += PropertyTypesChanged;
PropertyTypesChanged(_noGroupPropertyTypes, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
@@ -376,7 +378,7 @@ namespace Umbraco.Core.Models
if (!HasPropertyTypeBeenRemoved)
{
HasPropertyTypeBeenRemoved = true;
- OnPropertyChanged(Ps.Value.PropertyTypeCollectionSelector);
+ OnPropertyChanged(Ps.Value.PropertyTypesSelector);
}
break;
}
@@ -388,7 +390,7 @@ namespace Umbraco.Core.Models
if (!HasPropertyTypeBeenRemoved)
{
HasPropertyTypeBeenRemoved = true;
- OnPropertyChanged(Ps.Value.PropertyTypeCollectionSelector);
+ OnPropertyChanged(Ps.Value.PropertyTypesSelector);
}
}
}
@@ -412,7 +414,7 @@ namespace Umbraco.Core.Models
// actually remove the group
PropertyGroups.RemoveItem(propertyGroupName);
- OnPropertyChanged(Ps.Value.PropertyGroupCollectionSelector);
+ OnPropertyChanged(Ps.Value.PropertyGroupsSelector);
}
///
diff --git a/src/Umbraco.Core/Models/GridValue.cs b/src/Umbraco.Core/Models/GridValue.cs
index 717fcd2f88..237385f3f4 100644
--- a/src/Umbraco.Core/Models/GridValue.cs
+++ b/src/Umbraco.Core/Models/GridValue.cs
@@ -5,6 +5,8 @@ using Newtonsoft.Json.Linq;
namespace Umbraco.Core.Models
{
+ //TODO: Make a property value converter for this!
+
///
/// A model representing the value saved for the grid
///
@@ -19,7 +21,7 @@ namespace Umbraco.Core.Models
public class GridSection
{
[JsonProperty("grid")]
- public string Grid { get; set; }
+ public string Grid { get; set; } //fixme: what is this?
[JsonProperty("rows")]
public IEnumerable Rows { get; set; }
@@ -46,7 +48,7 @@ namespace Umbraco.Core.Models
public class GridArea
{
[JsonProperty("grid")]
- public string Grid { get; set; }
+ public string Grid { get; set; } //fixme: what is this?
[JsonProperty("controls")]
public IEnumerable Controls { get; set; }
diff --git a/src/Umbraco.Core/Models/IContent.cs b/src/Umbraco.Core/Models/IContent.cs
index 0c0d9449e0..a414a03d2f 100644
--- a/src/Umbraco.Core/Models/IContent.cs
+++ b/src/Umbraco.Core/Models/IContent.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
namespace Umbraco.Core.Models
{
+
///
/// Represents a document.
///
@@ -11,6 +12,11 @@ namespace Umbraco.Core.Models
///
public interface IContent : IContentBase
{
+ ///
+ /// Gets or sets the content schedule
+ ///
+ ContentScheduleCollection ContentSchedule { get; set; }
+
///
/// Gets or sets the template used to render the content.
///
@@ -60,26 +66,11 @@ namespace Umbraco.Core.Models
///
DateTime? PublishDate { get; }
- ///
- /// Gets or sets the date and time the content item should be published.
- ///
- DateTime? ReleaseDate { get; set; }
-
- ///
- /// Gets or sets the date and time the content should be unpublished.
- ///
- DateTime? ExpireDate { get; set; }
-
///
/// Gets the content type of this content.
///
IContentType ContentType { get; }
- ///
- /// Gets the current status of the content.
- ///
- ContentStatus Status { get; }
-
///
/// Gets a value indicating whether a culture is published.
///
@@ -89,6 +80,7 @@ namespace Umbraco.Core.Models
/// whenever values for this culture are unpublished.
/// A culture becomes published as soon as PublishCulture has been invoked,
/// even though the document might now have been saved yet (and can have no identity).
+ /// Does not support the '*' wildcard (returns false).
///
bool IsCulturePublished(string culture);
@@ -112,6 +104,7 @@ namespace Umbraco.Core.Models
/// A culture is edited when it is available, and not published or published but
/// with changes.
/// A culture can be edited even though the document might now have been saved yet (and can have no identity).
+ /// Does not support the '*' wildcard (returns false).
///
bool IsCultureEdited(string culture);
@@ -172,7 +165,7 @@ namespace Umbraco.Core.Models
///
/// A value indicating whether the culture can be published.
///
- /// Fails if properties don't pass variant validtion rules.
+ /// Fails if properties don't pass variant validation rules.
/// Publishing must be finalized via the content service SavePublishing method.
///
bool PublishCulture(string culture = "*");
diff --git a/src/Umbraco.Core/Models/IContentBase.cs b/src/Umbraco.Core/Models/IContentBase.cs
index cef8086207..fb3714cfc0 100644
--- a/src/Umbraco.Core/Models/IContentBase.cs
+++ b/src/Umbraco.Core/Models/IContentBase.cs
@@ -76,6 +76,7 @@ namespace Umbraco.Core.Models
/// Returns false for the invariant culture, in order to be consistent
/// with , even though the invariant culture is
/// always available.
+ /// Does not support the '*' wildcard (returns false).
///
bool IsCultureAvailable(string culture);
diff --git a/src/Umbraco.Core/Models/Language.cs b/src/Umbraco.Core/Models/Language.cs
index 940648c4b9..e190c8ad3b 100644
--- a/src/Umbraco.Core/Models/Language.cs
+++ b/src/Umbraco.Core/Models/Language.cs
@@ -1,7 +1,10 @@
using System;
using System.Globalization;
+using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
+using System.Threading;
+using Umbraco.Core.Configuration;
using Umbraco.Core.Models.Entities;
namespace Umbraco.Core.Models
@@ -48,7 +51,57 @@ namespace Umbraco.Core.Models
[DataMember]
public string CultureName
{
- get => _cultureName ?? CultureInfo.GetCultureInfo(IsoCode).DisplayName;
+ // CultureInfo.DisplayName is the name in the installed .NET language
+ // .NativeName is the name in culture info's language
+ // .EnglishName is the name in English
+ //
+ // there is no easy way to get the name in a specified culture (which would need to be installed on the server)
+ // this works:
+ // var rm = new ResourceManager("mscorlib", typeof(int).Assembly);
+ // var name = rm.GetString("Globalization.ci_" + culture.Name, displayCulture);
+ // but can we rely on it?
+ //
+ // and... DisplayName is captured and cached in culture infos returned by GetCultureInfo(), using
+ // the value for the current thread culture at the moment it is first retrieved - whereas creating
+ // a new CultureInfo() creates a new instance, which _then_ can get DisplayName again in a different
+ // culture
+ //
+ // I assume that, on a site, all language names should be in the SAME language, in DB,
+ // and that would be the umbracoDefaultUILanguage (app setting) - BUT if by accident
+ // ANY culture has been retrieved with another current thread culture - it's now corrupt
+ //
+ // so, the logic below ensures that the name always end up being the correct name
+ // see also LanguageController.GetAllCultures which is doing the same
+ //
+ // all this, including the ugly settings injection, because se store language names in db,
+ // otherwise it would be ok to simply return new CultureInfo(IsoCode).DisplayName to get the name
+ // in whatever culture is current - we should not do it, see task #3623
+ //
+ // but then, some tests that compare audit strings (for culture names) would need to be fixed
+
+ get
+ {
+ if (_cultureName != null) return _cultureName;
+
+ // capture
+ var threadUiCulture = Thread.CurrentThread.CurrentUICulture;
+
+ try
+ {
+ var globalSettings = (IGlobalSettings) Composing.Current.Container.GetInstance(typeof(IGlobalSettings));
+ var defaultUiCulture = CultureInfo.GetCultureInfo(globalSettings.DefaultUILanguage);
+ Thread.CurrentThread.CurrentUICulture = defaultUiCulture;
+
+ // get name - new-ing an instance to get proper display name
+ return new CultureInfo(IsoCode).DisplayName;
+ }
+ finally
+ {
+ // restore
+ Thread.CurrentThread.CurrentUICulture = threadUiCulture;
+ }
+ }
+
set => SetPropertyValueAndDetectChanges(value, ref _cultureName, Ps.Value.CultureNameSelector);
}
diff --git a/src/Umbraco.Core/Models/PagedResult.cs b/src/Umbraco.Core/Models/PagedResult.cs
index 653712d9f8..ef4d4efdfd 100644
--- a/src/Umbraco.Core/Models/PagedResult.cs
+++ b/src/Umbraco.Core/Models/PagedResult.cs
@@ -19,7 +19,7 @@ namespace Umbraco.Core.Models
if (pageSize > 0)
{
- TotalPages = (long)Math.Ceiling(totalItems / (Decimal)pageSize);
+ TotalPages = (long)Math.Ceiling(totalItems / (decimal)pageSize);
}
else
{
diff --git a/src/Umbraco.Core/Models/PropertyGroup.cs b/src/Umbraco.Core/Models/PropertyGroup.cs
index 1d0b949932..595e8d1d6a 100644
--- a/src/Umbraco.Core/Models/PropertyGroup.cs
+++ b/src/Umbraco.Core/Models/PropertyGroup.cs
@@ -35,12 +35,12 @@ namespace Umbraco.Core.Models
{
public readonly PropertyInfo NameSelector = ExpressionHelper.GetPropertyInfo(x => x.Name);
public readonly PropertyInfo SortOrderSelector = ExpressionHelper.GetPropertyInfo(x => x.SortOrder);
- public readonly PropertyInfo PropertyTypeCollectionSelector = ExpressionHelper.GetPropertyInfo(x => x.PropertyTypes);
+ public readonly PropertyInfo PropertyTypes = ExpressionHelper.GetPropertyInfo(x => x.PropertyTypes);
}
private void PropertyTypesChanged(object sender, NotifyCollectionChangedEventArgs e)
{
- OnPropertyChanged(Ps.Value.PropertyTypeCollectionSelector);
+ OnPropertyChanged(Ps.Value.PropertyTypes);
}
///
@@ -76,6 +76,8 @@ namespace Umbraco.Core.Models
get => _propertyTypes;
set
{
+ if (_propertyTypes != null)
+ _propertyTypes.CollectionChanged -= PropertyTypesChanged;
_propertyTypes = value;
// since we're adding this collection to this group,
@@ -83,6 +85,7 @@ namespace Umbraco.Core.Models
foreach (var propertyType in _propertyTypes)
propertyType.PropertyGroupId = new Lazy(() => Id);
+ OnPropertyChanged(Ps.Value.PropertyTypes);
_propertyTypes.CollectionChanged += PropertyTypesChanged;
}
}
diff --git a/src/Umbraco.Core/Models/PublicAccessEntry.cs b/src/Umbraco.Core/Models/PublicAccessEntry.cs
index e93dc56e35..df2d9f9ddc 100644
--- a/src/Umbraco.Core/Models/PublicAccessEntry.cs
+++ b/src/Umbraco.Core/Models/PublicAccessEntry.cs
@@ -21,6 +21,10 @@ namespace Umbraco.Core.Models
public PublicAccessEntry(IContent protectedNode, IContent loginNode, IContent noAccessNode, IEnumerable ruleCollection)
{
+ if (protectedNode == null) throw new ArgumentNullException(nameof(protectedNode));
+ if (loginNode == null) throw new ArgumentNullException(nameof(loginNode));
+ if (noAccessNode == null) throw new ArgumentNullException(nameof(noAccessNode));
+
LoginNodeId = loginNode.Id;
NoAccessNodeId = noAccessNode.Id;
_protectedNodeId = protectedNode.Id;
diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs
index 82e4935616..6733eea500 100644
--- a/src/Umbraco.Core/Models/UserExtensions.cs
+++ b/src/Umbraco.Core/Models/UserExtensions.cs
@@ -3,12 +3,14 @@ using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
+using System.Security.Cryptography;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration;
using Umbraco.Core.Composing;
using Umbraco.Core.Models.Entities;
using Umbraco.Core.Models.Membership;
using Umbraco.Core.Services;
+using Umbraco.Core.Security;
namespace Umbraco.Core.Models
{
@@ -54,8 +56,11 @@ namespace Umbraco.Core.Models
///
internal static string[] GetUserAvatarUrls(this IUser user, ICacheProvider staticCache)
{
- //check if the user has explicitly removed all avatars including a gravatar, this will be possible and the value will be "none"
- if (user.Avatar == "none")
+ // If FIPS is required, never check the Gravatar service as it only supports MD5 hashing.
+ // Unfortunately, if the FIPS setting is enabled on Windows, using MD5 will throw an exception
+ // and the website will not run.
+ // Also, check if the user has explicitly removed all avatars including a gravatar, this will be possible and the value will be "none"
+ if (user.Avatar == "none" || CryptoConfig.AllowOnlyFipsAlgorithms)
{
return new string[0];
}
@@ -146,68 +151,46 @@ namespace Umbraco.Core.Models
internal static bool HasContentRootAccess(this IUser user, IEntityService entityService)
{
- return HasPathAccess(Constants.System.Root.ToInvariantString(), user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent);
+ return ContentPermissionsHelper.HasPathAccess(Constants.System.Root.ToInvariantString(), user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent);
}
internal static bool HasContentBinAccess(this IUser user, IEntityService entityService)
{
- return HasPathAccess(Constants.System.RecycleBinContent.ToInvariantString(), user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent);
+ return ContentPermissionsHelper.HasPathAccess(Constants.System.RecycleBinContent.ToInvariantString(), user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent);
}
internal static bool HasMediaRootAccess(this IUser user, IEntityService entityService)
{
- return HasPathAccess(Constants.System.Root.ToInvariantString(), user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia);
+ return ContentPermissionsHelper.HasPathAccess(Constants.System.Root.ToInvariantString(), user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia);
}
internal static bool HasMediaBinAccess(this IUser user, IEntityService entityService)
{
- return HasPathAccess(Constants.System.RecycleBinMedia.ToInvariantString(), user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia);
+ return ContentPermissionsHelper.HasPathAccess(Constants.System.RecycleBinMedia.ToInvariantString(), user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia);
}
internal static bool HasPathAccess(this IUser user, IContent content, IEntityService entityService)
{
- return HasPathAccess(content.Path, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent);
+ if (content == null) throw new ArgumentNullException(nameof(content));
+ return ContentPermissionsHelper.HasPathAccess(content.Path, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent);
}
internal static bool HasPathAccess(this IUser user, IMedia media, IEntityService entityService)
{
- return HasPathAccess(media.Path, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia);
+ if (media == null) throw new ArgumentNullException(nameof(media));
+ return ContentPermissionsHelper.HasPathAccess(media.Path, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia);
}
- internal static bool HasPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService, int recycleBinId)
+ internal static bool HasContentPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService)
{
- switch (recycleBinId)
- {
- case Constants.System.RecycleBinMedia:
- return HasPathAccess(entity.Path, user.CalculateMediaStartNodeIds(entityService), recycleBinId);
- case Constants.System.RecycleBinContent:
- return HasPathAccess(entity.Path, user.CalculateContentStartNodeIds(entityService), recycleBinId);
- default:
- throw new NotSupportedException("Path access is only determined on content or media");
- }
+ if (entity == null) throw new ArgumentNullException(nameof(entity));
+ return ContentPermissionsHelper.HasPathAccess(entity.Path, user.CalculateContentStartNodeIds(entityService), Constants.System.RecycleBinContent);
}
- internal static bool HasPathAccess(string path, int[] startNodeIds, int recycleBinId)
+ internal static bool HasMediaPathAccess(this IUser user, IUmbracoEntity entity, IEntityService entityService)
{
- if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(path));
-
- // check for no access
- if (startNodeIds.Length == 0)
- return false;
-
- // check for root access
- if (startNodeIds.Contains(Constants.System.Root))
- return true;
-
- var formattedPath = string.Concat(",", path, ",");
-
- // only users with root access have access to the recycle bin,
- // if the above check didn't pass then access is denied
- if (formattedPath.Contains(string.Concat(",", recycleBinId, ",")))
- return false;
-
- // check for a start node in the path
- return startNodeIds.Any(x => formattedPath.Contains(string.Concat(",", x, ",")));
+ if (entity == null) throw new ArgumentNullException(nameof(entity));
+ return ContentPermissionsHelper.HasPathAccess(entity.Path, user.CalculateMediaStartNodeIds(entityService), Constants.System.RecycleBinMedia);
}
internal static bool IsInBranchOfStartNode(this IUser user, IUmbracoEntity entity, IEntityService entityService, int recycleBinId, out bool hasPathAccess)
@@ -215,58 +198,14 @@ namespace Umbraco.Core.Models
switch (recycleBinId)
{
case Constants.System.RecycleBinMedia:
- return IsInBranchOfStartNode(entity.Path, user.CalculateMediaStartNodeIds(entityService), user.GetMediaStartNodePaths(entityService), out hasPathAccess);
+ return ContentPermissionsHelper.IsInBranchOfStartNode(entity.Path, user.CalculateMediaStartNodeIds(entityService), user.GetMediaStartNodePaths(entityService), out hasPathAccess);
case Constants.System.RecycleBinContent:
- return IsInBranchOfStartNode(entity.Path, user.CalculateContentStartNodeIds(entityService), user.GetContentStartNodePaths(entityService), out hasPathAccess);
+ return ContentPermissionsHelper.IsInBranchOfStartNode(entity.Path, user.CalculateContentStartNodeIds(entityService), user.GetContentStartNodePaths(entityService), out hasPathAccess);
default:
throw new NotSupportedException("Path access is only determined on content or media");
}
}
- internal static bool IsInBranchOfStartNode(string path, int[] startNodeIds, string[] startNodePaths, out bool hasPathAccess)
- {
- if (string.IsNullOrWhiteSpace(path)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(path));
-
- hasPathAccess = false;
-
- // check for no access
- if (startNodeIds.Length == 0)
- return false;
-
- // check for root access
- if (startNodeIds.Contains(Constants.System.Root))
- {
- hasPathAccess = true;
- return true;
- }
-
- //is it self?
- var self = startNodePaths.Any(x => x == path);
- if (self)
- {
- hasPathAccess = true;
- return true;
- }
-
- //is it ancestor?
- var ancestor = startNodePaths.Any(x => x.StartsWith(path));
- if (ancestor)
- {
- //hasPathAccess = false;
- return true;
- }
-
- //is it descendant?
- var descendant = startNodePaths.Any(x => path.StartsWith(x));
- if (descendant)
- {
- hasPathAccess = true;
- return true;
- }
-
- return false;
- }
-
///
/// Determines whether this user has access to view sensitive data
///
diff --git a/src/Umbraco.Core/ObjectExtensions.cs b/src/Umbraco.Core/ObjectExtensions.cs
index 44e5968a9f..318a826b25 100644
--- a/src/Umbraco.Core/ObjectExtensions.cs
+++ b/src/Umbraco.Core/ObjectExtensions.cs
@@ -724,8 +724,8 @@ namespace Umbraco.Core
{
return typeConverter;
}
-
- TypeConverter converter = TypeDescriptor.GetConverter(target);
+
+ var converter = TypeDescriptor.GetConverter(target);
if (converter.CanConvertFrom(source))
{
return DestinationTypeConverterCache[key] = converter;
diff --git a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs
index 523d09c1f7..9eb4c3f90f 100644
--- a/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs
+++ b/src/Umbraco.Core/Persistence/Constants-DatabaseSchema.cs
@@ -31,6 +31,7 @@ namespace Umbraco.Core
public const string DocumentCultureVariation = TableNamePrefix + "DocumentCultureVariation";
public const string DocumentVersion = TableNamePrefix + "DocumentVersion";
public const string MediaVersion = TableNamePrefix + "MediaVersion";
+ public const string ContentSchedule = TableNamePrefix + "ContentSchedule";
public const string PropertyType = /*TableNamePrefix*/ "cms" + "PropertyType";
public const string PropertyTypeGroup = /*TableNamePrefix*/ "cms" + "PropertyTypeGroup";
diff --git a/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs b/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs
index 97f995e99d..5a0e44a281 100644
--- a/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs
+++ b/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs
@@ -159,9 +159,7 @@ namespace Umbraco.Core.Persistence.DatabaseModelDefinitions
Name = indexName,
IndexType = attribute.IndexType,
ColumnName = columnName,
- TableName = tableName,
- IsClustered = attribute.IndexType == IndexTypes.Clustered,
- IsUnique = attribute.IndexType == IndexTypes.UniqueNonClustered
+ TableName = tableName,
};
if (string.IsNullOrEmpty(attribute.ForColumns) == false)
diff --git a/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/IndexDefinition.cs b/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/IndexDefinition.cs
index d4f2a27ae6..582f9a40f7 100644
--- a/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/IndexDefinition.cs
+++ b/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/IndexDefinition.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using Umbraco.Core.Persistence.DatabaseAnnotations;
namespace Umbraco.Core.Persistence.DatabaseModelDefinitions
@@ -14,8 +15,7 @@ namespace Umbraco.Core.Persistence.DatabaseModelDefinitions
public virtual string SchemaName { get; set; }
public virtual string TableName { get; set; }
public virtual string ColumnName { get; set; }
- public virtual bool IsUnique { get; set; }
- public bool IsClustered { get; set; }
+
public virtual ICollection Columns { get; set; }
public IndexTypes IndexType { get; set; }
}
diff --git a/src/Umbraco.Core/Persistence/Dtos/ContentScheduleDto.cs b/src/Umbraco.Core/Persistence/Dtos/ContentScheduleDto.cs
new file mode 100644
index 0000000000..492a3d7cbd
--- /dev/null
+++ b/src/Umbraco.Core/Persistence/Dtos/ContentScheduleDto.cs
@@ -0,0 +1,33 @@
+using System;
+using NPoco;
+using Umbraco.Core.Persistence.DatabaseAnnotations;
+
+namespace Umbraco.Core.Persistence.Dtos
+{
+ [TableName(TableName)]
+ [PrimaryKey("id", AutoIncrement = false)]
+ [ExplicitColumns]
+ internal class ContentScheduleDto
+ {
+ public const string TableName = Constants.DatabaseSchema.Tables.ContentSchedule;
+
+ [Column("id")]
+ [PrimaryKeyColumn(AutoIncrement = false)]
+ public Guid Id { get; set; }
+
+ [Column("nodeId")]
+ [ForeignKey(typeof(ContentDto))]
+ public int NodeId { get; set; }
+
+ [Column("languageId")]
+ [ForeignKey(typeof(LanguageDto))]
+ [NullSetting(NullSetting = NullSettings.Null)] // can be invariant
+ public int? LanguageId { get; set; }
+
+ [Column("date")]
+ public DateTime Date { get; set; }
+
+ [Column("action")]
+ public string Action { get; set; }
+ }
+}
diff --git a/src/Umbraco.Core/Persistence/Dtos/DocumentDto.cs b/src/Umbraco.Core/Persistence/Dtos/DocumentDto.cs
index fd3df69b8a..abe13a0e23 100644
--- a/src/Umbraco.Core/Persistence/Dtos/DocumentDto.cs
+++ b/src/Umbraco.Core/Persistence/Dtos/DocumentDto.cs
@@ -1,9 +1,9 @@
-using System;
-using NPoco;
+using NPoco;
using Umbraco.Core.Persistence.DatabaseAnnotations;
namespace Umbraco.Core.Persistence.Dtos
{
+
[TableName(TableName)]
[PrimaryKey("nodeId", AutoIncrement = false)]
[ExplicitColumns]
@@ -23,14 +23,6 @@ namespace Umbraco.Core.Persistence.Dtos
[Column("edited")]
public bool Edited { get; set; }
- [Column("releaseDate")]
- [NullSetting(NullSetting = NullSettings.Null)]
- public DateTime? ReleaseDate { get; set; }
-
- [Column("expireDate")]
- [NullSetting(NullSetting = NullSettings.Null)]
- public DateTime? ExpiresDate { get; set; }
-
//[Column("publishDate")]
//[NullSetting(NullSetting = NullSettings.Null)] // is contentVersionDto.VersionDate for the published version
//public DateTime? PublishDate { get; set; }
diff --git a/src/Umbraco.Core/Persistence/Dtos/TemplateDto.cs b/src/Umbraco.Core/Persistence/Dtos/TemplateDto.cs
index 2f0d149ee7..a73425db8d 100644
--- a/src/Umbraco.Core/Persistence/Dtos/TemplateDto.cs
+++ b/src/Umbraco.Core/Persistence/Dtos/TemplateDto.cs
@@ -22,10 +22,6 @@ namespace Umbraco.Core.Persistence.Dtos
[NullSetting(NullSetting = NullSettings.Null)]
public string Alias { get; set; }
- [Column("design")]
- [SpecialDbType(SpecialDbTypes.NTEXT)]
- public string Design { get; set; }
-
[ResultColumn]
[Reference(ReferenceType.OneToOne, ColumnName = "NodeId")]
public NodeDto NodeDto { get; set; }
diff --git a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs
index ec364c7c6a..c8467f47e2 100644
--- a/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs
+++ b/src/Umbraco.Core/Persistence/Factories/ContentBaseFactory.cs
@@ -1,7 +1,10 @@
using System;
+using System.Collections.Generic;
+using System.Linq;
using System.Text.RegularExpressions;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence.Dtos;
+using Umbraco.Core.Persistence.Repositories;
namespace Umbraco.Core.Persistence.Factories
{
@@ -45,8 +48,6 @@ namespace Umbraco.Core.Persistence.Factories
content.Published = dto.Published;
content.Edited = dto.Edited;
- content.ExpireDate = dto.ExpiresDate;
- content.ReleaseDate = dto.ReleaseDate;
// fixme - shall we get published infos or not?
//if (dto.Published)
@@ -142,7 +143,7 @@ namespace Umbraco.Core.Persistence.Factories
content.CreateDate = nodeDto.CreateDate;
content.UpdateDate = contentVersionDto.VersionDate;
- content.ProviderUserKey = content.Key; // fixme explain
+ content.ProviderUserKey = content.Key; // The `ProviderUserKey` is a membership provider thing
// reset dirty initial properties (U4-1946)
content.ResetDirtyProperties(false);
@@ -155,7 +156,7 @@ namespace Umbraco.Core.Persistence.Factories
}
///
- /// Buils a dto from an IContent item.
+ /// Builds a dto from an IContent item.
///
public static DocumentDto BuildDto(IContent entity, Guid objectType)
{
@@ -165,9 +166,6 @@ namespace Umbraco.Core.Persistence.Factories
{
NodeId = entity.Id,
Published = entity.Published,
- ReleaseDate = entity.ReleaseDate,
- ExpiresDate = entity.ExpireDate,
-
ContentDto = contentDto,
DocumentVersionDto = BuildDocumentVersionDto(entity, contentDto)
};
@@ -175,6 +173,19 @@ namespace Umbraco.Core.Persistence.Factories
return dto;
}
+ public static IEnumerable<(ContentSchedule Model, ContentScheduleDto Dto)> BuildScheduleDto(IContent entity, ILanguageRepository languageRepository)
+ {
+ return entity.ContentSchedule.FullSchedule.Select(x =>
+ (x, new ContentScheduleDto
+ {
+ Action = x.Action.ToString(),
+ Date = x.Date,
+ NodeId = entity.Id,
+ LanguageId = languageRepository.GetIdByIsoCode(x.Culture, false),
+ Id = x.Id
+ }));
+ }
+
///
/// Buils a dto from an IMedia item.
///
@@ -302,6 +313,9 @@ namespace Umbraco.Core.Persistence.Factories
// more dark magic ;-(
internal static bool TryMatch(string text, out string path)
{
+ //fixme: In v8 we should allow exposing this via the property editor in a much nicer way so that the property editor
+ // can tell us directly what any URL is for a given property if it contains an asset
+
path = null;
if (string.IsNullOrWhiteSpace(text)) return false;
diff --git a/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs b/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs
index fa6513775f..01b0e59a1d 100644
--- a/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs
+++ b/src/Umbraco.Core/Persistence/Factories/DataTypeFactory.cs
@@ -53,7 +53,7 @@ namespace Umbraco.Core.Persistence.Factories
EditorAlias = entity.EditorAlias,
NodeId = entity.Id,
DbType = entity.DatabaseType.ToString(),
- Configuration = entity.Configuration == null ? null : JsonConvert.SerializeObject(entity.Configuration, ConfigurationEditor.ConfigurationJsonSettings),
+ Configuration = ConfigurationEditor.ToDatabase(entity.Configuration),
NodeDto = BuildNodeDto(entity)
};
diff --git a/src/Umbraco.Core/Persistence/Factories/TemplateFactory.cs b/src/Umbraco.Core/Persistence/Factories/TemplateFactory.cs
index b2b5be872e..7682ea47db 100644
--- a/src/Umbraco.Core/Persistence/Factories/TemplateFactory.cs
+++ b/src/Umbraco.Core/Persistence/Factories/TemplateFactory.cs
@@ -45,7 +45,6 @@ namespace Umbraco.Core.Persistence.Factories
var dto = new TemplateDto
{
Alias = entity.Alias,
- Design = entity.Content ?? string.Empty,
NodeDto = BuildNodeDto(entity, nodeObjectTypeId)
};
diff --git a/src/Umbraco.Core/Persistence/Mappers/ContentMapper.cs b/src/Umbraco.Core/Persistence/Mappers/ContentMapper.cs
index d2e0e7245c..2cc3a5b140 100644
--- a/src/Umbraco.Core/Persistence/Mappers/ContentMapper.cs
+++ b/src/Umbraco.Core/Persistence/Mappers/ContentMapper.cs
@@ -37,8 +37,6 @@ namespace Umbraco.Core.Persistence.Mappers
CacheMap(src => src.ContentTypeId, dto => dto.ContentTypeId);
CacheMap(src => src.UpdateDate, dto => dto.VersionDate);
- CacheMap(src => src.ExpireDate, dto => dto.ExpiresDate);
- CacheMap(src => src.ReleaseDate, dto => dto.ReleaseDate);
CacheMap(src => src.Published, dto => dto.Published);
//CacheMap(src => src.Name, dto => dto.Alias);
diff --git a/src/Umbraco.Core/Persistence/Mappers/TemplateMapper.cs b/src/Umbraco.Core/Persistence/Mappers/TemplateMapper.cs
index f402081e08..ca5faab134 100644
--- a/src/Umbraco.Core/Persistence/Mappers/TemplateMapper.cs
+++ b/src/Umbraco.Core/Persistence/Mappers/TemplateMapper.cs
@@ -24,7 +24,6 @@ namespace Umbraco.Core.Persistence.Mappers
CacheMap(src => src.MasterTemplateId, dto => dto.ParentId);
CacheMap(src => src.Key, dto => dto.UniqueId);
CacheMap(src => src.Alias, dto => dto.Alias);
- CacheMap(src => src.Content, dto => dto.Design);
}
}
}
diff --git a/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs b/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs
index 76116a8d03..d313d27bbc 100644
--- a/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs
+++ b/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs
@@ -32,7 +32,7 @@ namespace Umbraco.Core.Persistence.Querying
///
/// Gets or sets the SQL syntax provider for the current database.
///
- protected ISqlSyntaxProvider SqlSyntax { get; private set; }
+ protected ISqlSyntaxProvider SqlSyntax { get; }
///
/// Gets the list of SQL parameters.
@@ -56,6 +56,8 @@ namespace Umbraco.Core.Persistence.Querying
/// Also populates the SQL parameters.
public virtual string Visit(Expression expression)
{
+ if (expression == null) return string.Empty;
+
// if the expression is a CachedExpression,
// visit the inner expression if not already visited
var cachedExpression = expression as CachedExpression;
@@ -65,8 +67,6 @@ namespace Umbraco.Core.Persistence.Querying
expression = cachedExpression.InnerExpression;
}
- if (expression == null) return string.Empty;
-
string result;
switch (expression.NodeType)
@@ -135,35 +135,28 @@ namespace Umbraco.Core.Persistence.Querying
// if the expression is a CachedExpression,
// and is not already compiled, assign the result
- if (cachedExpression != null)
- {
- if (cachedExpression.Visited == false)
- cachedExpression.VisitResult = result;
- result = cachedExpression.VisitResult;
- }
-
- return result;
+ if (cachedExpression == null)
+ return result;
+ if (!cachedExpression.Visited)
+ cachedExpression.VisitResult = result;
+ return cachedExpression.VisitResult;
}
protected abstract string VisitMemberAccess(MemberExpression m);
protected virtual string VisitLambda(LambdaExpression lambda)
{
- if (lambda.Body.NodeType == ExpressionType.MemberAccess)
- {
- var m = lambda.Body as MemberExpression;
-
- if (m != null && m.Expression != null)
+ if (lambda.Body.NodeType == ExpressionType.MemberAccess &&
+ lambda.Body is MemberExpression memberExpression && memberExpression.Expression != null)
{
//This deals with members that are boolean (i.e. x => IsTrashed )
- var r = VisitMemberAccess(m);
+ var result = VisitMemberAccess(memberExpression);
SqlParameters.Add(true);
- return Visited ? string.Empty : string.Format("{0} = @{1}", r, SqlParameters.Count - 1);
+ return Visited ? string.Empty : $"{result} = @{SqlParameters.Count - 1}";
}
- }
return Visit(lambda.Body);
}
@@ -248,21 +241,10 @@ namespace Umbraco.Core.Persistence.Querying
{
case "MOD":
case "COALESCE":
- //don't execute if compiled
- if (Visited == false)
- {
- return string.Format("{0}({1},{2})", operand, left, right);
- }
- //already compiled, return
- return string.Empty;
+ return Visited ? string.Empty : $"{operand}({left},{right})";
+
default:
- //don't execute if compiled
- if (Visited == false)
- {
- return string.Concat("(", left, " ", operand, " ", right, ")");
- }
- //already compiled, return
- return string.Empty;
+ return Visited ? string.Empty : $"({left} {operand} {right})";
}
}
@@ -284,10 +266,10 @@ namespace Umbraco.Core.Persistence.Querying
return list;
}
- protected virtual string VisitNew(NewExpression nex)
+ protected virtual string VisitNew(NewExpression newExpression)
{
// TODO : check !
- var member = Expression.Convert(nex, typeof(object));
+ var member = Expression.Convert(newExpression, typeof(object));
var lambda = Expression.Lambda>(member);
try
{
@@ -295,20 +277,16 @@ namespace Umbraco.Core.Persistence.Querying
var o = getter();
SqlParameters.Add(o);
+
return Visited ? string.Empty : $"@{SqlParameters.Count - 1}";
}
catch (InvalidOperationException)
{
- if (Visited) return string.Empty;
+ if (Visited)
+ return string.Empty;
- var exprs = VisitExpressionList(nex.Arguments);
- var r = new StringBuilder();
- foreach (var e in exprs)
- {
- if (r.Length > 0) r.Append(",");
- r.Append(e);
- }
- return r.ToString();
+ var exprs = VisitExpressionList(newExpression.Arguments);
+ return string.Join(",", exprs);
}
}
@@ -323,6 +301,7 @@ namespace Umbraco.Core.Persistence.Querying
return "null";
SqlParameters.Add(c.Value);
+
return Visited ? string.Empty : $"@{SqlParameters.Count - 1}";
}
@@ -375,27 +354,11 @@ namespace Umbraco.Core.Persistence.Querying
protected virtual string VisitNewArray(NewArrayExpression na)
{
var exprs = VisitExpressionList(na.Expressions);
-
- //don't execute if compiled
- if (Visited == false)
- {
- var r = new StringBuilder();
- foreach (var e in exprs)
- {
- r.Append(r.Length > 0 ? "," + e : e);
- }
-
- return r.ToString();
- }
- //already compiled, return
- return string.Empty;
+ return Visited ? string.Empty : string.Join(",", exprs);
}
protected virtual List