diff --git a/.editorconfig b/.editorconfig
index 5a35b71ce6..ca85827de8 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,40 +1,43 @@
-# editorconfig.org
-
-# top-most EditorConfig file
-root = true
-
-# Default settings:
-# A newline ending every file
-# Use 4 spaces as indentation
-[*]
-insert_final_newline = true
-end_of_line = crlf
-indent_style = space
-indent_size = 4
-
-# Trim trailing whitespace, limited support.
-# https://github.com/editorconfig/editorconfig/wiki/Property-research:-Trim-trailing-spaces
-trim_trailing_whitespace = true
-
-[*.{cs,vb}]
-dotnet_style_predefined_type_for_locals_parameters_members = true:error
-
-dotnet_naming_rule.private_members_with_underscore.symbols = private_fields
-dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore
-dotnet_naming_rule.private_members_with_underscore.severity = suggestion
-
-dotnet_naming_symbols.private_fields.applicable_kinds = field
-dotnet_naming_symbols.private_fields.applicable_accessibilities = private
-
-dotnet_naming_style.prefix_underscore.capitalization = camel_case
-dotnet_naming_style.prefix_underscore.required_prefix = _
-
-# https://github.com/MicrosoftDocs/visualstudio-docs/blob/master/docs/ide/editorconfig-code-style-settings-reference.md
-[*.cs]
-csharp_style_var_for_built_in_types = true:suggestion
-csharp_style_var_when_type_is_apparent = true:suggestion
-csharp_style_var_elsewhere = true:suggestion
+# editorconfig.org
+
+# top-most EditorConfig file
+root = true
+
+# Default settings:
+# A newline ending every file
+# Use 4 spaces as indentation
+[*]
+insert_final_newline = true
+end_of_line = crlf
+indent_style = space
+indent_size = 4
+
+# Trim trailing whitespace, limited support.
+# https://github.com/editorconfig/editorconfig/wiki/Property-research:-Trim-trailing-spaces
+trim_trailing_whitespace = true
+
+[*.{cs,vb}]
+dotnet_style_predefined_type_for_locals_parameters_members = true:error
+
+dotnet_naming_rule.private_members_with_underscore.symbols = private_fields
+dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore
+dotnet_naming_rule.private_members_with_underscore.severity = suggestion
+
+dotnet_naming_symbols.private_fields.applicable_kinds = field
+dotnet_naming_symbols.private_fields.applicable_accessibilities = private
+
+dotnet_naming_style.prefix_underscore.capitalization = camel_case
+dotnet_naming_style.prefix_underscore.required_prefix = _
+
+# https://github.com/MicrosoftDocs/visualstudio-docs/blob/master/docs/ide/editorconfig-code-style-settings-reference.md
+[*.cs]
+csharp_style_var_for_built_in_types = true:suggestion
+csharp_style_var_when_type_is_apparent = true:suggestion
+csharp_style_var_elsewhere = true:suggestion
csharp_prefer_braces = false : none
-
-[*.{js,less}]
+
+[*.js]
+trim_trailing_whitespace = true
+
+[*.less]
trim_trailing_whitespace = false
diff --git a/.github/BUILD.md b/.github/BUILD.md
index ad33872423..5f962a8911 100644
--- a/.github/BUILD.md
+++ b/.github/BUILD.md
@@ -39,7 +39,7 @@ To build Umbraco, fire up PowerShell and move to Umbraco's repository root (the
build/build.ps1
-If you only see a build.bat-file, you're probably on the wrong branch. If you switch to the correct branch (dev-v8) the file will appear and you can build it.
+If you only see a build.bat-file, you're probably on the wrong branch. If you switch to the correct branch (v8/contrib) the file will appear and you can build it.
You might run into [Powershell quirks](#powershell-quirks).
diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md
index 1526c54656..e97b03e7e3 100644
--- a/.github/CODE_OF_CONDUCT.md
+++ b/.github/CODE_OF_CONDUCT.md
@@ -1,32 +1,92 @@
-# Code Of Conduct
-
-Our informal code of conduct concentrates on the values we, as Umbraco HQ, have set for ourselves and for our community. We expect you to be a friend.
-Instead of listing out all the exact "do's" and "don't's" we want to challenge you to think about our values and apply them:
+# Umbraco Code of Conduct
-If there's a need to talk to Umbraco HQ about anything, please make sure to send a mail to [Sebastiaan Janssen - sj@umbraco.dk](mailto:sj@umbraco.dk).
+## Preamble
-## Be a Friend
+We are the friendly CMS. And our friendliness stems from our values. That's why we have set for ourselves, Umbraco HQ, and the community, five values to guide us in everything we do:
-We welcome and thank you for registering at Our Umbraco. Find below the values that govern Umbraco and which you accept by using Our Umbraco.
+* Trust - We believe in and empower people
+* Respect - We treat others as we would like to be treated
+* Open - We share our thoughts and knowledge
+* Hungry - We want to do things better, best is next
+* Friendly - We want to build long-lasting relationships
-## Trust
+With these values in mind, we want to offer the Umbraco community a code of conduct that specifies a baseline standard of behavior so that people with different social values and communication styles can work together.
-Assume positive intent and try to understand before being understood.
+This code of conduct is based on the widely used Contributor Covenant, as described in [https://www.contributor-covenant.org/](https://www.contributor-covenant.org/)
-## Respect
+## Our Pledge
-Treat others as you would like to be treated.
+We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
-This also goes for treating the HQ with respect. For example: don’t promote products on [our.umbraco.com](https://our.umbraco.com) that directly compete with our commercial offerings which enables us to work for a sustainable Umbraco.
+We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
-## Open
+## Our Standards
+Examples of behavior that contributes to a positive environment for our community include:
-Be honest and straightforward. Tell it as it is. Share thoughts and knowledge and engage in collaboration.
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the overall community
-## Hungry
+Examples of unacceptable behavior include:
-Don't rest on your laurels and never accept the status quo. Contribute and give back to fellow Umbracians.
+* The use of sexualized language or imagery, and sexual attention or advances of any kind
+* Trolling, insulting or derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email address, without their explicit permission
+* Other conduct which could reasonably be considered inappropriate in a professional setting
-## Friendly
+## Enforcement Responsibilities
-Don’t judge upon mistakes made but rather upon the speed and quality with which mistakes are corrected. Friendly posts and contributions generate smiles and build long lasting relationships.
\ No newline at end of file
+Community leaders (e.g. Meetup & festival organizers, moderators, maintainers, ...) are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
+
+Specific enforcement steps are listed in the [Code of Conduct Enforcement Guidelines](https://github.com/umbraco/Umbraco-CMS/blob/v8/contrib/.github/CODE_OF_CONDUCT_ENFORCEMENT.md) document which is an appendix of this document, updated and maintained by the Code of Conduct Team.
+
+## Scope
+This Code of Conduct applies within all community spaces and events supported by Umbraco HQ or using the Umbraco name. It also applies when an individual is officially representing the community in public spaces.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior, may be reported at [conduct@umbraco.com](mailto:conduct@umbraco.com). All complaints will be reviewed and investigated promptly and fairly.
+
+Or alternatively, you can reach out directly to any of the team members behind the address above:
+
+* Sebastiaan Janssen (He, Him - Languages spoken: English, Dutch, Danish(Read)) [sebastiaan@umbraco.com](mailto:sebastiaan@umbraco.com)
+* Ilham Boulghallat (She, Her - Languages spoken: English, French, Arabic) [ilham@umbraco.com](mailto:ilham@umbraco.com)
+* Arnold Visser (He, Him - Languages spoken: English, Dutch) [arnold@umbraco.com](mailto:arnold@umbraco.com)
+
+The review process is done with full respect for the privacy and security of the reporter of any incident.
+
+People with a conflict of interest should exclude themselves or if necessary be excluded by the other team members.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
+
+**1. Correction**
+Community Impact: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
+
+Consequence: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
+
+**2. Warning**
+Community Impact: A violation through a single incident or series of actions.
+
+Consequence: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
+
+**3. Temporary Ban**
+Community Impact: A serious violation of community standards, including sustained inappropriate behavior.
+
+Consequence: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
+
+**4. Permanent Ban**
+Community Impact: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
+
+Consequence: A permanent ban from any sort of public interaction within the community.
+
+## Attribution
+This Code of Conduct is adapted from the Contributor Covenant, version 2.0, available at [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html).
+
+This Code of Conduct will be maintained and reviewed by the team listed above.
\ No newline at end of file
diff --git a/.github/CODE_OF_CONDUCT_ENFORCEMENT.md b/.github/CODE_OF_CONDUCT_ENFORCEMENT.md
new file mode 100644
index 0000000000..2bb45644c2
--- /dev/null
+++ b/.github/CODE_OF_CONDUCT_ENFORCEMENT.md
@@ -0,0 +1,57 @@
+# Umbraco Code of Conduct Enforcement guidelines - Consequence Ladder
+
+These are the steps followed by the [Umbraco Code of Conduct Team](https://github.com/umbraco/Umbraco-CMS/blob/v8/contrib/.github/CODE_OF_CONDUCT.md) when we respond to an issue or incident brought to our attention by a community member.
+
+This is an appendix to the Code of Conduct and is updated and maintained by the Code of Conduct Team.
+
+To make sure that all reports will be reviewed and investigated promptly and fairly, as highlighted in the Umbraco Code of Conduct, we are following [Mozilla’s Consequence Ladder approach](https://github.com/mozilla/inclusion/blob/master/code-of-conduct-enforcement/consequence-ladder.md).
+
+This approach helps the Team enforce the Code of Conduct in a structured manner and can be used as a way of communicating escalation. Each time the Team takes an action (warning, ban) the individual is made aware of future consequences. The Team can either follow the order of the levels in the ladder or decide to jump levels. When needed, the team can go directly to a permanent ban.
+
+**Level 0: No Action**
+Recommendations do not indicate a violation of the Code of Conduct.
+
+**Level 1: Simple Warning Issued**
+A private, written warning from the Code of Conduct Team, with clarity of violation, consequences of continued behavior.
+
+**Level 2: Warning**
+A private, written warning from the Code of Conduct Team, with clarity of violation, consequences of continued behavior. Additionally:
+
+* Communication of next-level consequences if behaviors are repeated (according to this ladder).
+
+**Level 3: Warning + Mandatory Cooling Off Period (Access Retained)**
+A private warning from the Code of Conduct Team, with clarity of violation, consequences of continued behavior. Additionally:
+
+* Request to avoid interaction on community messaging platforms (public forums, Our, commenting on issues).
+ * This includes avoiding any interactions in any Umbraco channels, spaces/offices, as well as external channels like social media (e.g. Twitter, Facebook, LinkedIn). For example, 'following/liking/retweeting' would be considered a violation of these terms, and consequence would escalate according to this ladder.
+* Require they do not interact with others in the report, or those who they suspect are involved in the report.
+* Suggestions for 'out of office' type of message on platforms, to reduce curiosity, or suspicion among those not involved.
+
+**Level 4: Temporary Ban (Access Revoked)**
+Private communication of ban from the Code of Conduct Team, with clarity of violation, consequences of continued behavior. Additionally:
+
+* 3-6 months imposed break.
+* All accounts deactivated, or blocked during this time (Our, HQ Slack if applicable).
+* Require to avoid interaction on community messaging platforms (public forums, Our, commenting on issues).
+ * This includes avoiding any interactions in any Umbraco channels, spaces/offices, as well as external channels like social media (e.g. Twitter, Facebook, LinkedIn). For example, 'following/liking/retweeting' would be considered a violation of these terms, and consequence would escalate according to this ladder.
+* All community leadership roles (e.g. Community Teams, Meetup/festival organizer, Commit right on Github..) suspended. (onboarding/reapplication required outside of this process)
+* No attendance at Umbraco events during the ban period.
+* Not allowed to enter Umbraco HQ offices during the ban period.
+* Permission to use the MVP title, if applicable, is revoked during this ban period.
+* The community leaders running events and other initiatives are informed of the ban.
+
+**Level 5: Permanent Ban**
+Private communication of ban from the Code of Conduct Team, with clarity of violation, consequences of continued behavior. Additionally:
+
+* All accounts deactivated permanently.
+* No attendance at Umbraco events going forward.
+* Not allowed to enter Umbraco HQ offices permanently.
+* All community leadership roles (e.g. Community Teams, Meetup/festival organizer, Commit right on Github..) permanently suspended.
+* Permission to use the MVP title, if applicable, revoked.
+* The community leaders running events and other initiatives are informed of the ban.
+
+
+Sources:
+* [Mozilla Code of Conduct - Enforcement Consequence Ladder](https://github.com/mozilla/inclusion/blob/master/code-of-conduct-enforcement/consequence-ladder.md)
+* [Drupal Conflict Resolution Policy and Process](https://www.drupal.org/conflict-resolution)
+* [Django Code of Conduct - Enforcement Manual](https://www.djangoproject.com/conduct/enforcement-manual/)
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 5537a46ef8..f4e237a1f2 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -102,10 +102,11 @@ That said, the Umbraco development team likes to follow the hints that ReSharper
The Core Contributors team consists of one member of Umbraco HQ, [Sebastiaan](https://github.com/nul800sebastiaan), who gets assistance from the following community members who have comitted to volunteering their free time:
-- [Anders Bjerner](https://github.com/abjerner)
-- [Emma Burstow](https://github.com/emmaburstow)
-- [Poornima Nayar](https://github.com/poornimanayar)
-- [Kenn Jacobsen](https://twitter.com/KennJacobsen_DK)
+- [Nathan Woulfe](https://github.com/nathanwoulfe)
+- [Joe Glombek](https://github.com/glombek)
+- [Laura Weatherhead](https://github.com/lssweatherhead)
+- [Michael Latouche](https://github.com/mikecp)
+- [Owain Williams](https://github.com/OwainWilliams)
These wonderful people aim to provide you with a first reply to your PR, review and test out your changes and on occasions, they might ask more questions. If they are happy with your work, they'll let Umbraco HQ know by approving the PR. Hq will have final sign-off and will check the work again before it is merged.
@@ -123,12 +124,12 @@ You can get in touch with [the core contributors team](#the-core-contributors-te
In order to build the Umbraco source code locally, first make sure you have the following installed.
- * [Visual Studio 2017 v15.9.7+](https://visualstudio.microsoft.com/vs/)
+ * [Visual Studio 2019 v16.3+ (with .NET Core 3.0)](https://visualstudio.microsoft.com/vs/)
* [Node.js v10+](https://nodejs.org/en/download/)
* npm v6.4.1+ (installed with Node.js)
* [Git command line](https://git-scm.com/download/)
-The easiest way to get started is to open `src\umbraco.sln` in Visual Studio 2017 (version 15.9.7 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.
+The easiest way to get started is to open `src\umbraco.sln` in Visual Studio 2019 (version 16.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.
Alternatively, you can run `build.ps1` from the Powershell command line, 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.
diff --git a/.github/ISSUE_TEMPLATE/3_BugNetCore.md b/.github/ISSUE_TEMPLATE/3_BugNetCore.md
new file mode 100644
index 0000000000..989904d4d8
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/3_BugNetCore.md
@@ -0,0 +1,65 @@
+---
+name: 🌟 .Net Core Bug Report
+about: For bugs specifically for the upcoming .NET Core release of Umbraco, don't use this if you're working with Umbraco version 7 or 8
+labels: project/net-core
+---
+
+ℹ️ If this bug **also** appears on the current version 8 of Umbraco then please [report it as a regular bug](https://github.com/umbraco/Umbraco-CMS/issues/new?template=1_Bug.md), fixes in version 8 will be merged to the .NET Core version.
+
+A brief description of the issue goes here.
+
+
+
+
+Reproduction
+------------
+
+If you're filing a bug, please describe how to reproduce it. Include as much
+relevant information as possible, such as:
+
+### Bug summary
+
+
+
+### Specifics
+
+
+
+### Steps to reproduce
+
+
+
+### Expected result
+
+
+
+### Actual result
+
+
diff --git a/.github/ISSUE_TEMPLATE/3_Support_question.md b/.github/ISSUE_TEMPLATE/3_Support_question.md
deleted file mode 100644
index 829df982f9..0000000000
--- a/.github/ISSUE_TEMPLATE/3_Support_question.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-name: ⁉️ Support Question
-about: Having trouble with Umbraco? -> https://our.umbraco.com
----
-
-This issue tracker is NOT meant for support questions. If you have a question,
-please join us on the forum at https://our.umbraco.com.
-
-Thanks!
diff --git a/.github/ISSUE_TEMPLATE/4_Documentation_issue.md b/.github/ISSUE_TEMPLATE/4_Documentation_issue.md
deleted file mode 100644
index 8893647aa8..0000000000
--- a/.github/ISSUE_TEMPLATE/4_Documentation_issue.md
+++ /dev/null
@@ -1,9 +0,0 @@
----
-name: 📖 Documentation Issue
-about: See https://github.com/umbraco/UmbracoDocs/issues for documentation issues
----
-
-The Umbraco documentation has its own dedicated repository. Please open your
-documentation-related issue at https://github.com/umbraco/UmbracoDocs/issues
-
-Thanks!
diff --git a/.github/ISSUE_TEMPLATE/5_Security_issue.md b/.github/ISSUE_TEMPLATE/5_Security_issue.md
deleted file mode 100644
index 84c5f5989c..0000000000
--- a/.github/ISSUE_TEMPLATE/5_Security_issue.md
+++ /dev/null
@@ -1,31 +0,0 @@
----
-name: 🔐 Security Issue
-about: Discovered a Security Issue in Umbraco?
----
-
-⚠️ PLEASE DON'T DISCLOSE SECURITY-RELATED ISSUES PUBLICLY, SEE BELOW.
-
-If you have found a security issue in Umbraco, please send the details to
-security@umbraco.com and don't disclose it publicly until we can provide a fix for
-it. If you wish, we'll credit you for finding verified issues, when we release
-the patched version.
-
-❗ Please read more about how to report security issues on https://umbraco.com/security
-
-A note on "Self XSS"
---------------------
-
-Umbraco is a CMS, that allows users to edit content on a website. As such,
-all _authenticated users_ can:
-
- - Edit content, and (depending on the field types) insert HTML and CSS in that
- content, with a variety of allowed attributes.
- - Depending on the user level: Edit template files, and insert C#, HTML, CSS and
- javascript in so on.
- - Upload files to the site, which will become publicly available.
-
-We see these functionalities as _features_, and not as security issues. Please
-report the mentioned items only if they can be performed by non-authorized
-users, or other exploitable vulnerabilities.
-
-Thanks!
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000000..37d1be9158
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,11 @@
+blank_issues_enabled: true
+contact_links:
+ - name: ⁉️ Support Question
+ url: https://our.umbraco.com
+ about: This issue tracker is NOT meant for support questions. If you have a question, please join us on the forum.
+ - name: 📖 Documentation Issue
+ url: https://github.com/umbraco/UmbracoDocs/issues
+ about: Documentation issues should be reported on the Umbraco documentation repository.
+ - name: 🔐 Security Issue
+ url: https://umbraco.com/about-us/trust-center/security-and-umbraco/how-to-report-a-vulnerability-in-umbraco/
+ about: Discovered a Security Issue in Umbraco?
\ No newline at end of file
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 0000000000..4dfa5a83b9
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,61 @@
+name: "Code scanning - action"
+
+on:
+ push:
+ branches: [v8/contrib,v8/dev,v8/bug,v8/feature]
+ pull_request:
+ # The branches below must be a subset of the branches above
+ schedule:
+ - cron: '0 7 * * 2'
+
+jobs:
+ CodeQL-Build:
+
+ runs-on: windows-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v2
+ with:
+ # We must fetch at least the immediate parents so that if this is
+ # a pull request then we can checkout the head.
+ fetch-depth: 2
+
+ # If this run was triggered by a pull request event, then checkout
+ # the head of the pull request instead of the merge commit.
+ - run: git checkout HEAD^2
+ if: ${{ github.event_name == 'pull_request' }}
+
+ # Initializes the CodeQL tools for scanning.
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v1
+ # Override language selection by uncommenting this and choosing your languages
+ # with:
+ # languages: go, javascript, csharp, python, cpp, java
+
+ # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
+ # If this step fails, then you should remove it and run the build manually (see below)
+
+ # ℹ️ Command-line programs to run using the OS shell.
+ # 📚 https://git.io/JvXDl
+
+ # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
+ # and modify them (or add more) to build your code if your project
+ # uses a compiled language
+
+ - name: configure Pagefile
+ uses: al-cheb/configure-pagefile-action@v1.2
+ with:
+ minimum-size: 8GB
+ maximum-size: 32GB
+
+ - run: |
+ echo "Run Umbraco-CMS build"
+ pwsh -command .\build\build.ps1
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v1
+ with:
+ config-file: ./.github/codeql-config.yml
+
+
diff --git a/.github/workflows/codeql-config.yml b/.github/workflows/codeql-config.yml
new file mode 100644
index 0000000000..59b55e48ec
--- /dev/null
+++ b/.github/workflows/codeql-config.yml
@@ -0,0 +1,13 @@
+name: "CodeQL config"
+on:
+ push:
+ branches: [v8/contrib,v8/dev]
+paths-ignore:
+ - node_modules
+ - Umbraco.TestData
+ - Umbraco.Tests
+ - Umbraco.Tests.AcceptanceTest
+ - Umbraco.Tests.Benchmarks
+ - bin
+paths:
+ - src
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 927c2ef570..ebbfe93576 100644
--- a/.gitignore
+++ b/.gitignore
@@ -167,6 +167,7 @@ build/temp/
# eof
/src/Umbraco.Web.UI.Client/TESTS-*.xml
/src/ApiDocs/api/*
+/src/Umbraco.Web.UI.Client/package-lock.json
# Acceptance tests
cypress.env.json
@@ -174,3 +175,6 @@ cypress.env.json
/src/Umbraco.Tests.AcceptanceTest/package-lock.json
/src/Umbraco.Tests.AcceptanceTest/cypress/videos/
/src/Umbraco.Tests.AcceptanceTest/cypress/screenshots/
+src/Umbraco.Web.UI/Umbraco/telemetrics-id.umb
+
+/src/Umbraco.Web.UI/config/umbracoSettings.config
diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec
index 72619db02e..48f5f32222 100644
--- a/build/NuSpecs/UmbracoCms.Web.nuspec
+++ b/build/NuSpecs/UmbracoCms.Web.nuspec
@@ -42,6 +42,8 @@
+
+
diff --git a/build/NuSpecs/tools/ReadmeUpgrade.txt b/build/NuSpecs/tools/ReadmeUpgrade.txt
index 91d49d896c..fd1b2174e2 100644
--- a/build/NuSpecs/tools/ReadmeUpgrade.txt
+++ b/build/NuSpecs/tools/ReadmeUpgrade.txt
@@ -11,7 +11,7 @@ Y88 88Y 888 888 888 888 d88P 888 888 888 Y88b. Y88..88P
Don't forget to build!
-We've done our best to transform your configuration files but in case something is not quite right: we recommmend you look in source control for the previous version so you can find the original files before they were transformed.
+We've done our best to transform your configuration files but in case something is not quite right: we recommend you look in source control for the previous version so you can find the original files before they were transformed.
This NuGet package includes build targets that extend the creation of a deploy package, which is generated by
Publishing from Visual Studio. The targets will only work once Publishing is configured, so if you don't use
diff --git a/build/NuSpecs/tools/Web.config.install.xdt b/build/NuSpecs/tools/Web.config.install.xdt
index 2b79f95c70..e215bdbf29 100644
--- a/build/NuSpecs/tools/Web.config.install.xdt
+++ b/build/NuSpecs/tools/Web.config.install.xdt
@@ -1,139 +1,136 @@
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
+
-
-
- >
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -177,20 +174,21 @@
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/build/build-bootstrap.ps1 b/build/build-bootstrap.ps1
index 82c789ff22..9a31a57acf 100644
--- a/build/build-bootstrap.ps1
+++ b/build/build-bootstrap.ps1
@@ -34,6 +34,7 @@
if (-not (test-path $nuget))
{
Write-Host "Download NuGet..."
+ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Invoke-WebRequest $source -OutFile $nuget
if (-not $?) { throw "Failed to download NuGet." }
}
diff --git a/build/build.ps1 b/build/build.ps1
index c2c5bdd232..15d455b976 100644
--- a/build/build.ps1
+++ b/build/build.ps1
@@ -125,7 +125,23 @@
$error.Clear()
Write-Output "### gulp build for version $($this.Version.Release)" >> $log 2>&1
- npx gulp build --buildversion=$this.Version.Release >> $log 2>&1
+ npm run build --buildversion=$this.Version.Release >> $log 2>&1
+
+ # We can ignore this warning, we need to update to node 12 at some point - https://github.com/jsdom/jsdom/issues/2939
+ $indexes = [System.Collections.ArrayList]::new()
+ $index = 0;
+ $error | ForEach-Object {
+ # Find which of the errors is the ExperimentalWarning
+ if($_.ToString().Contains("ExperimentalWarning: The fs.promises API is experimental")) {
+ [void]$indexes.Add($index)
+ }
+ $index++
+ }
+ $indexes | ForEach-Object {
+ # Loop through the list of indexes and remove the errors that we expect and feel confident we can ignore
+ $error.Remove($error[$_])
+ }
+
if (-not $?) { throw "Failed to build" } # that one is expected to work
} finally {
Pop-Location
diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs
index 5587979a6c..3ecfd20f03 100644
--- a/src/SolutionInfo.cs
+++ b/src/SolutionInfo.cs
@@ -2,7 +2,7 @@
using System.Resources;
[assembly: AssemblyCompany("Umbraco")]
-[assembly: AssemblyCopyright("Copyright © Umbraco 2020")]
+[assembly: AssemblyCopyright("Copyright © Umbraco 2021")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
@@ -18,5 +18,5 @@ using System.Resources;
[assembly: AssemblyVersion("8.0.0")]
// these are FYI and changed automatically
-[assembly: AssemblyFileVersion("8.7.0")]
-[assembly: AssemblyInformationalVersion("8.7.0")]
+[assembly: AssemblyFileVersion("8.11.1")]
+[assembly: AssemblyInformationalVersion("8.11.1")]
diff --git a/src/Umbraco.Core/Collections/EventClearingObservableCollection.cs b/src/Umbraco.Core/Collections/EventClearingObservableCollection.cs
new file mode 100644
index 0000000000..af25cc1b4a
--- /dev/null
+++ b/src/Umbraco.Core/Collections/EventClearingObservableCollection.cs
@@ -0,0 +1,41 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+
+namespace Umbraco.Core.Collections
+{
+ ///
+ /// Allows clearing all event handlers
+ ///
+ ///
+ public class EventClearingObservableCollection : ObservableCollection, INotifyCollectionChanged
+ {
+ public EventClearingObservableCollection()
+ {
+ }
+
+ public EventClearingObservableCollection(List list) : base(list)
+ {
+ }
+
+ public EventClearingObservableCollection(IEnumerable collection) : base(collection)
+ {
+ }
+
+ // need to explicitly implement with event accessor syntax in order to override in order to to clear
+ // c# events are weird, they do not behave the same way as other c# things that are 'virtual',
+ // a good article is here: https://medium.com/@unicorn_dev/virtual-events-in-c-something-went-wrong-c6f6f5fbe252
+ // and https://stackoverflow.com/questions/2268065/c-sharp-language-design-explicit-interface-implementation-of-an-event
+ private NotifyCollectionChangedEventHandler _changed;
+ event NotifyCollectionChangedEventHandler INotifyCollectionChanged.CollectionChanged
+ {
+ add { _changed += value; }
+ remove { _changed -= value; }
+ }
+
+ ///
+ /// Clears all event handlers for the event
+ ///
+ public void ClearCollectionChangedEvents() => _changed = null;
+ }
+}
diff --git a/src/Umbraco.Core/Collections/ObservableDictionary.cs b/src/Umbraco.Core/Collections/ObservableDictionary.cs
index c6aedab377..fd9e469f07 100644
--- a/src/Umbraco.Core/Collections/ObservableDictionary.cs
+++ b/src/Umbraco.Core/Collections/ObservableDictionary.cs
@@ -1,11 +1,14 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
+using System.Collections.Specialized;
+using System.ComponentModel;
using System.Linq;
using System.Runtime.Serialization;
namespace Umbraco.Core.Collections
{
+
///
/// An ObservableDictionary
///
@@ -15,7 +18,7 @@ namespace Umbraco.Core.Collections
///
/// The type of elements contained in the BindableCollection
/// The type of the indexing key
- public class ObservableDictionary : ObservableCollection, IReadOnlyDictionary, IDictionary
+ public class ObservableDictionary : ObservableCollection, IReadOnlyDictionary, IDictionary, INotifyCollectionChanged
{
protected Dictionary Indecies { get; }
protected Func KeySelector { get; }
@@ -74,6 +77,22 @@ namespace Umbraco.Core.Collections
#endregion
+ // need to explicitly implement with event accessor syntax in order to override in order to to clear
+ // c# events are weird, they do not behave the same way as other c# things that are 'virtual',
+ // a good article is here: https://medium.com/@unicorn_dev/virtual-events-in-c-something-went-wrong-c6f6f5fbe252
+ // and https://stackoverflow.com/questions/2268065/c-sharp-language-design-explicit-interface-implementation-of-an-event
+ private NotifyCollectionChangedEventHandler _changed;
+ event NotifyCollectionChangedEventHandler INotifyCollectionChanged.CollectionChanged
+ {
+ add { _changed += value; }
+ remove { _changed -= value; }
+ }
+
+ ///
+ /// Clears all event handlers
+ ///
+ public void ClearCollectionChangedEvents() => _changed = null;
+
public bool ContainsKey(TKey key)
{
return Indecies.ContainsKey(key);
diff --git a/src/Umbraco.Core/Compose/AuditEventsComponent.cs b/src/Umbraco.Core/Compose/AuditEventsComponent.cs
index 033b46c13f..62610ca9c7 100644
--- a/src/Umbraco.Core/Compose/AuditEventsComponent.cs
+++ b/src/Umbraco.Core/Compose/AuditEventsComponent.cs
@@ -42,7 +42,19 @@ namespace Umbraco.Core.Compose
}
public void Terminate()
- { }
+ {
+ UserService.SavedUserGroup -= OnSavedUserGroupWithUsers;
+
+ UserService.SavedUser -= OnSavedUser;
+ UserService.DeletedUser -= OnDeletedUser;
+ UserService.UserGroupPermissionsAssigned -= UserGroupPermissionAssigned;
+
+ MemberService.Saved -= OnSavedMember;
+ MemberService.Deleted -= OnDeletedMember;
+ MemberService.AssignedRoles -= OnAssignedRoles;
+ MemberService.RemovedRoles -= OnRemovedRoles;
+ MemberService.Exported -= OnMemberExported;
+ }
internal static IUser UnknownUser => new User { Id = Constants.Security.UnknownUserId, Name = Constants.Security.UnknownUserName, Email = "" };
diff --git a/src/Umbraco.Core/Compose/RelateOnCopyComponent.cs b/src/Umbraco.Core/Compose/RelateOnCopyComponent.cs
index b56ff8b87e..a9f9ed1ee0 100644
--- a/src/Umbraco.Core/Compose/RelateOnCopyComponent.cs
+++ b/src/Umbraco.Core/Compose/RelateOnCopyComponent.cs
@@ -14,7 +14,9 @@ namespace Umbraco.Core.Compose
}
public void Terminate()
- { }
+ {
+ ContentService.Copied -= ContentServiceCopied;
+ }
private static void ContentServiceCopied(IContentService sender, Events.CopyEventArgs e)
{
diff --git a/src/Umbraco.Core/Compose/RelateOnTrashComponent.cs b/src/Umbraco.Core/Compose/RelateOnTrashComponent.cs
index 4e01c50fc6..a216a584ba 100644
--- a/src/Umbraco.Core/Compose/RelateOnTrashComponent.cs
+++ b/src/Umbraco.Core/Compose/RelateOnTrashComponent.cs
@@ -18,7 +18,12 @@ namespace Umbraco.Core.Compose
}
public void Terminate()
- { }
+ {
+ ContentService.Moved -= ContentService_Moved;
+ ContentService.Trashed -= ContentService_Trashed;
+ MediaService.Moved -= MediaService_Moved;
+ MediaService.Trashed -= MediaService_Trashed;
+ }
private static void ContentService_Moved(IContentService sender, MoveEventArgs e)
{
diff --git a/src/Umbraco.Core/Composing/Current.cs b/src/Umbraco.Core/Composing/Current.cs
index a06f09baf6..a4ccd59f9d 100644
--- a/src/Umbraco.Core/Composing/Current.cs
+++ b/src/Umbraco.Core/Composing/Current.cs
@@ -30,6 +30,7 @@ namespace Umbraco.Core.Composing
public static class Current
{
private static IFactory _factory;
+ private static IRuntimeState _state;
// TODO: get rid of these oddities
// we don't want Umbraco tests to die because the container has not been properly initialized,
@@ -125,7 +126,17 @@ namespace Umbraco.Core.Composing
?? new ProfilingLogger(Logger, Profiler);
public static IRuntimeState RuntimeState
- => Factory.GetInstance();
+ {
+ get
+ {
+ return _state ?? Factory.GetInstance();
+ }
+ internal set
+ {
+ // this is only used when the boot entirely fails, we need to manually set this so we can report
+ _state = value;
+ }
+ }
public static TypeLoader TypeLoader
=> Factory.GetInstance();
diff --git a/src/Umbraco.Core/Composing/TypeFinder.cs b/src/Umbraco.Core/Composing/TypeFinder.cs
index 5ad1e43580..394d9480ae 100644
--- a/src/Umbraco.Core/Composing/TypeFinder.cs
+++ b/src/Umbraco.Core/Composing/TypeFinder.cs
@@ -35,7 +35,7 @@ namespace Umbraco.Core.Composing
var s = ConfigurationManager.AppSettings[Constants.AppSettings.AssembliesAcceptingLoadExceptions];
return _assembliesAcceptingLoadExceptions = string.IsNullOrWhiteSpace(s)
? Array.Empty()
- : s.Split(',').Select(x => x.Trim()).ToArray();
+ : s.Split(Constants.CharArrays.Comma).Select(x => x.Trim()).ToArray();
}
}
diff --git a/src/Umbraco.Core/CompositionExtensions.cs b/src/Umbraco.Core/CompositionExtensions.cs
index ced9a9386a..2fcf446d6b 100644
--- a/src/Umbraco.Core/CompositionExtensions.cs
+++ b/src/Umbraco.Core/CompositionExtensions.cs
@@ -64,7 +64,7 @@ namespace Umbraco.Core
=> composition.WithCollectionBuilder();
///
- /// Gets the url segment providers collection builder.
+ /// Gets the URL segment providers collection builder.
///
/// The composition.
public static UrlSegmentProviderCollectionBuilder UrlSegmentProviders(this Composition composition)
diff --git a/src/Umbraco.Core/Configuration/GlobalSettings.cs b/src/Umbraco.Core/Configuration/GlobalSettings.cs
index a888e3c42b..1d1ccaf7b4 100644
--- a/src/Umbraco.Core/Configuration/GlobalSettings.cs
+++ b/src/Umbraco.Core/Configuration/GlobalSettings.cs
@@ -68,9 +68,9 @@ namespace Umbraco.Core.Configuration
internal static bool? HasSmtpServer { get; set; }
///
- /// Gets the reserved urls from web.config.
+ /// Gets the reserved URLs from web.config.
///
- /// The reserved urls.
+ /// The reserved URLs.
public string ReservedUrls
{
get
@@ -145,6 +145,20 @@ namespace Umbraco.Core.Configuration
}
}
+ ///
+ /// Gets the path to folder containing the icons used in the umbraco backoffice (/umbraco/assets/icons by default).
+ ///
+ /// The icons path.
+ public string IconsPath
+ {
+ get
+ {
+ return ConfigurationManager.AppSettings.ContainsKey(Constants.AppSettings.IconsPath)
+ ? IOHelper.ResolveUrl(Constants.AppSettings.IconsPath)
+ : $"{Path}/assets/icons";
+ }
+ }
+
///
/// Gets or sets the configuration status. This will return the version number of the currently installed umbraco instance.
///
@@ -340,10 +354,10 @@ namespace Umbraco.Core.Configuration
}
///
- /// Gets a value indicating whether umbraco should hide top level nodes from generated urls.
+ /// Gets a value indicating whether umbraco should hide top level nodes from generated URLs.
///
///
- /// true if umbraco hides top level nodes from urls; otherwise, false.
+ /// true if umbraco hides top level nodes from URLs; otherwise, false.
///
public bool HideTopLevelNodeFromPath
{
@@ -377,5 +391,28 @@ namespace Umbraco.Core.Configuration
}
}
}
+
+
+ ///
+ /// An int value representing the time in milliseconds to lock the database for a write operation
+ ///
+ ///
+ /// The default value is 1800 milliseconds
+ ///
+ /// The timeout in milliseconds.
+ public int SqlWriteLockTimeOut
+ {
+ get
+ {
+ try
+ {
+ return int.Parse(ConfigurationManager.AppSettings[Constants.AppSettings.SqlWriteLockTimeOut]);
+ }
+ catch
+ {
+ return 1800;
+ }
+ }
+ }
}
}
diff --git a/src/Umbraco.Core/Configuration/GlobalSettingsExtensions.cs b/src/Umbraco.Core/Configuration/GlobalSettingsExtensions.cs
index bc76caacee..e8fea7f27d 100644
--- a/src/Umbraco.Core/Configuration/GlobalSettingsExtensions.cs
+++ b/src/Umbraco.Core/Configuration/GlobalSettingsExtensions.cs
@@ -44,7 +44,7 @@ namespace Umbraco.Core.Configuration
var path = globalSettings.Path;
if (path.StartsWith(SystemDirectories.Root)) // beware of TrimStart, see U4-2518
path = path.Substring(SystemDirectories.Root.Length);
- return path.TrimStart('~').TrimStart('/').Replace('/', '-').Trim().ToLower();
+ return path.TrimStart(Constants.CharArrays.Tilde).TrimStart(Constants.CharArrays.ForwardSlash).Replace('/', '-').Trim().ToLower();
}
}
diff --git a/src/Umbraco.Core/Configuration/IGlobalSettings.cs b/src/Umbraco.Core/Configuration/IGlobalSettings.cs
index b96434c30c..483829f85f 100644
--- a/src/Umbraco.Core/Configuration/IGlobalSettings.cs
+++ b/src/Umbraco.Core/Configuration/IGlobalSettings.cs
@@ -1,14 +1,14 @@
namespace Umbraco.Core.Configuration
{
///
- /// Contains general settings information for the entire Umbraco instance based on information from web.config appsettings
+ /// Contains general settings information for the entire Umbraco instance based on information from web.config appsettings
///
public interface IGlobalSettings
{
///
- /// Gets the reserved urls from web.config.
+ /// Gets the reserved URLs from web.config.
///
- /// The reserved urls.
+ /// The reserved URLs.
string ReservedUrls { get; }
///
@@ -21,7 +21,12 @@
/// Gets the path to umbraco's root directory (/umbraco by default).
///
string Path { get; }
-
+
+ ///
+ /// Gets the path to umbraco's icons directory (/umbraco/assets/icons by default).
+ ///
+ string IconsPath { get; }
+
///
/// Gets or sets the configuration status. This will return the version number of the currently installed umbraco instance.
///
@@ -40,10 +45,10 @@
string DefaultUILanguage { get; }
///
- /// Gets a value indicating whether umbraco should hide top level nodes from generated urls.
+ /// Gets a value indicating whether umbraco should hide top level nodes from generated URLs.
///
///
- /// true if umbraco hides top level nodes from urls; otherwise, false.
+ /// true if umbraco hides top level nodes from URLs; otherwise, false.
///
bool HideTopLevelNodeFromPath { get; }
@@ -67,5 +72,10 @@
/// Gets the location of temporary files.
///
string LocalTempPath { get; }
+
+ ///
+ /// Gets the write lock timeout.
+ ///
+ int SqlWriteLockTimeOut { get; }
}
}
diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/BackOfficeElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/BackOfficeElement.cs
index 79bff51d05..a84ae3d31c 100644
--- a/src/Umbraco.Core/Configuration/UmbracoSettings/BackOfficeElement.cs
+++ b/src/Umbraco.Core/Configuration/UmbracoSettings/BackOfficeElement.cs
@@ -8,5 +8,10 @@ namespace Umbraco.Core.Configuration.UmbracoSettings
internal TourConfigElement Tours => (TourConfigElement)this["tours"];
ITourSection IBackOfficeSection.Tours => Tours;
+
+ [ConfigurationProperty("id", DefaultValue = "")]
+ internal string Id => (string)base["id"];
+
+ string IBackOfficeSection.Id => (string)base["id"];
}
}
diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs
index 77ad7df0dc..3ebb632882 100644
--- a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs
+++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentElement.cs
@@ -40,6 +40,9 @@ namespace Umbraco.Core.Configuration.UmbracoSettings
[ConfigurationProperty("loginBackgroundImage")]
internal InnerTextConfigurationElement LoginBackgroundImage => GetOptionalTextElement("loginBackgroundImage", string.Empty);
+ [ConfigurationProperty("loginLogoImage")]
+ internal InnerTextConfigurationElement LoginLogoImage => GetOptionalTextElement("loginLogoImage", "assets/img/application/umbraco_logo_white.svg");
+
string IContentSection.NotificationEmailAddress => Notifications.NotificationEmailAddress;
bool IContentSection.DisableHtmlEmail => Notifications.DisableHtmlEmail;
@@ -61,5 +64,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings
bool IContentSection.ShowDeprecatedPropertyEditors => ShowDeprecatedPropertyEditors;
string IContentSection.LoginBackgroundImage => LoginBackgroundImage;
+
+ string IContentSection.LoginLogoImage => LoginLogoImage;
}
}
diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentSectionExtensions.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentSectionExtensions.cs
index 82cc5928cf..b3980d236a 100644
--- a/src/Umbraco.Core/Configuration/UmbracoSettings/ContentSectionExtensions.cs
+++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ContentSectionExtensions.cs
@@ -15,7 +15,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings
{
if (contentConfig == null) throw new ArgumentNullException(nameof(contentConfig));
if (extension == null) return false;
- extension = extension.TrimStart('.');
+ extension = extension.TrimStart(Constants.CharArrays.Period);
return contentConfig.ImageFileTypes.InvariantContains(extension);
}
diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IBackOfficeSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IBackOfficeSection.cs
index 36dd6a22ed..32e8e4e3ec 100644
--- a/src/Umbraco.Core/Configuration/UmbracoSettings/IBackOfficeSection.cs
+++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IBackOfficeSection.cs
@@ -1,7 +1,10 @@
-namespace Umbraco.Core.Configuration.UmbracoSettings
+using System;
+
+namespace Umbraco.Core.Configuration.UmbracoSettings
{
public interface IBackOfficeSection
{
ITourSection Tours { get; }
+ string Id { get; }
}
-}
\ No newline at end of file
+}
diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs
index 228b0923dc..0f52adf02e 100644
--- a/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs
+++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IContentSection.cs
@@ -32,5 +32,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings
bool ShowDeprecatedPropertyEditors { get; }
string LoginBackgroundImage { get; }
+
+ string LoginLogoImage { get; }
}
}
diff --git a/src/Umbraco.Core/Constants-AppSettings.cs b/src/Umbraco.Core/Constants-AppSettings.cs
index 704617d90c..0182034011 100644
--- a/src/Umbraco.Core/Constants-AppSettings.cs
+++ b/src/Umbraco.Core/Constants-AppSettings.cs
@@ -43,6 +43,11 @@ namespace Umbraco.Core
///
public const string Path = "Umbraco.Core.Path";
+ ///
+ /// Gets the path to umbraco's icons directory (/umbraco/assets/icons by default).
+ ///
+ public const string IconsPath = "Umbraco.Icons.Path";
+
///
/// Gets the path to the css directory (/css by default).
///
@@ -59,7 +64,7 @@ namespace Umbraco.Core
public const string MediaPath = "umbracoMediaPath";
///
- /// The reserved urls from web.config.
+ /// The reserved URLs from web.config.
///
public const string ReservedUrls = "Umbraco.Core.ReservedUrls";
@@ -96,7 +101,7 @@ namespace Umbraco.Core
public const string DefaultUILanguage = "Umbraco.Core.DefaultUILanguage";
///
- /// A true/false value indicating whether umbraco should hide top level nodes from generated urls.
+ /// A true/false value indicating whether umbraco should hide top level nodes from generated URLs.
///
public const string HideTopLevelNodeFromPath = "Umbraco.Core.HideTopLevelNodeFromPath";
@@ -135,6 +140,14 @@ namespace Umbraco.Core
///
public const string DatabaseFactoryServerVersion = "Umbraco.Core.Debug.DatabaseFactoryServerVersion";
}
+
+ ///
+ /// An int value representing the time in milliseconds to lock the database for a write operation
+ ///
+ ///
+ /// The default value is 1800 milliseconds
+ ///
+ public const string SqlWriteLockTimeOut = "Umbraco.Core.SqlWriteLockTimeOut";
}
}
}
diff --git a/src/Umbraco.Core/Constants-CharArrays.cs b/src/Umbraco.Core/Constants-CharArrays.cs
new file mode 100644
index 0000000000..2f8292b4a4
--- /dev/null
+++ b/src/Umbraco.Core/Constants-CharArrays.cs
@@ -0,0 +1,138 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Umbraco.Core
+{
+ public static partial class Constants
+ {
+ ///
+ /// Char Arrays to avoid allocations
+ ///
+ public static class CharArrays
+ {
+ ///
+ /// Char array containing only /
+ ///
+ public static readonly char[] ForwardSlash = new char[] { '/' };
+
+ ///
+ /// Char array containing only \
+ ///
+ public static readonly char[] Backslash = new char[] { '\\' };
+
+ ///
+ /// Char array containing only '
+ ///
+ public static readonly char[] SingleQuote = new char[] { '\'' };
+
+ ///
+ /// Char array containing only "
+ ///
+ public static readonly char[] DoubleQuote = new char[] { '\"' };
+
+
+ ///
+ /// Char array containing ' "
+ ///
+ public static readonly char[] DoubleQuoteSingleQuote = new char[] { '\"', '\'' };
+
+ ///
+ /// Char array containing only _
+ ///
+ public static readonly char[] Underscore = new char[] { '_' };
+
+ ///
+ /// Char array containing \n \r
+ ///
+ public static readonly char[] LineFeedCarriageReturn = new char[] { '\n', '\r' };
+
+
+ ///
+ /// Char array containing \n
+ ///
+ public static readonly char[] LineFeed = new char[] { '\n' };
+
+ ///
+ /// Char array containing only ,
+ ///
+ public static readonly char[] Comma = new char[] { ',' };
+
+ ///
+ /// Char array containing only &
+ ///
+ public static readonly char[] Ampersand = new char[] { '&' };
+
+ ///
+ /// Char array containing only \0
+ ///
+ public static readonly char[] NullTerminator = new char[] { '\0' };
+
+ ///
+ /// Char array containing only .
+ ///
+ public static readonly char[] Period = new char[] { '.' };
+
+ ///
+ /// Char array containing only ~
+ ///
+ public static readonly char[] Tilde = new char[] { '~' };
+ ///
+ /// Char array containing ~ /
+ ///
+ public static readonly char[] TildeForwardSlash = new char[] { '~', '/' };
+
+ ///
+ /// Char array containing only ?
+ ///
+ public static readonly char[] QuestionMark = new char[] { '?' };
+
+ ///
+ /// Char array containing ? &
+ ///
+ public static readonly char[] QuestionMarkAmpersand = new char[] { '?', '&' };
+
+ ///
+ /// Char array containing XML 1.1 whitespace chars
+ ///
+ public static readonly char[] XmlWhitespaceChars = new char[] { ' ', '\t', '\r', '\n' };
+
+ ///
+ /// Char array containing only the Space char
+ ///
+ public static readonly char[] Space = new char[] { ' ' };
+
+ ///
+ /// Char array containing only ;
+ ///
+ public static readonly char[] Semicolon = new char[] { ';' };
+
+ ///
+ /// Char array containing a comma and a space
+ ///
+ public static readonly char[] CommaSpace = new char[] { ',', ' ' };
+
+ ///
+ /// Char array containing _ -
+ ///
+ public static readonly char[] UnderscoreDash = new char[] { '_', '-' };
+
+ ///
+ /// Char array containing =
+ ///
+ public static readonly char[] EqualsChar = new char[] { '=' };
+
+ ///
+ /// Char array containing >
+ ///
+ public static readonly char[] GreaterThan = new char[] { '>' };
+
+ ///
+ /// Char array containing |
+ ///
+ public static readonly char[] VerticalTab = new char[] { '|' };
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Constants-Icons.cs b/src/Umbraco.Core/Constants-Icons.cs
index 05213ed1c4..d5cc37c9a5 100644
--- a/src/Umbraco.Core/Constants-Icons.cs
+++ b/src/Umbraco.Core/Constants-Icons.cs
@@ -24,6 +24,26 @@
///
public const string DataType = "icon-autofill";
+ ///
+ /// System dictionary icon
+ ///
+ public const string Dictionary = "icon-book-alt";
+
+ ///
+ /// System generic folder icon
+ ///
+ public const string Folder = "icon-folder";
+
+ ///
+ /// System language icon
+ ///
+ public const string Language = "icon-globe";
+
+ ///
+ /// System logviewer icon
+ ///
+ public const string LogViewer = "icon-box-alt";
+
///
/// System list view icon
///
@@ -69,6 +89,11 @@
///
public const string MemberType = "icon-users";
+ ///
+ /// System packages icon
+ ///
+ public const string Packages = "icon-box";
+
///
/// System property editor icon
///
diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs
index eb2b3525a7..90f5fbd0d0 100644
--- a/src/Umbraco.Core/Constants-PropertyEditors.cs
+++ b/src/Umbraco.Core/Constants-PropertyEditors.cs
@@ -36,6 +36,11 @@ namespace Umbraco.Core
///
public static class Aliases
{
+ ///
+ /// Block List.
+ ///
+ public const string BlockList = "Umbraco.BlockList";
+
///
/// CheckBox List.
///
@@ -187,7 +192,7 @@ namespace Umbraco.Core
public const string NestedContent = "Umbraco.NestedContent";
///
- /// Alias for the multi url picker editor.
+ /// Alias for the multi URL picker editor.
///
public const string MultiUrlPicker = "Umbraco.MultiUrlPicker";
}
diff --git a/src/Umbraco.Core/Constants-Security.cs b/src/Umbraco.Core/Constants-Security.cs
index 43c989805b..f900288ef5 100644
--- a/src/Umbraco.Core/Constants-Security.cs
+++ b/src/Umbraco.Core/Constants-Security.cs
@@ -56,6 +56,8 @@ namespace Umbraco.Core
public const string AllowedApplicationsClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/allowedapp";
public const string SessionIdClaimType = "http://umbraco.org/2015/02/identity/claims/backoffice/sessionid";
+ public const string BackOfficeExternalLoginOptionsProperty = "UmbracoBackOfficeExternalLoginOptions";
+
}
}
}
diff --git a/src/Umbraco.Core/Constants-SqlTemplates.cs b/src/Umbraco.Core/Constants-SqlTemplates.cs
index 984bc495b0..8529e6bfbc 100644
--- a/src/Umbraco.Core/Constants-SqlTemplates.cs
+++ b/src/Umbraco.Core/Constants-SqlTemplates.cs
@@ -14,7 +14,30 @@
public const string GetParentNode = "Umbraco.Core.VersionableRepository.GetParentNode";
public const string GetReservedId = "Umbraco.Core.VersionableRepository.GetReservedId";
}
+ public static class RelationRepository
+ {
+ public const string DeleteByParentAll = "Umbraco.Core.RelationRepository.DeleteByParent";
+ public const string DeleteByParentIn = "Umbraco.Core.RelationRepository.DeleteByParentIn";
+ }
+ public static class DataTypeRepository
+ {
+ public const string EnsureUniqueNodeName = "Umbraco.Core.DataTypeDefinitionRepository.EnsureUniqueNodeName";
+ }
+
+ internal static class NuCacheDatabaseDataSource
+ {
+ public const string WhereNodeId = "Umbraco.Web.PublishedCache.NuCache.DataSource.WhereNodeId";
+ public const string WhereNodeIdX = "Umbraco.Web.PublishedCache.NuCache.DataSource.WhereNodeIdX";
+ public const string SourcesSelectUmbracoNodeJoin = "Umbraco.Web.PublishedCache.NuCache.DataSource.SourcesSelectUmbracoNodeJoin";
+ public const string ContentSourcesSelect = "Umbraco.Web.PublishedCache.NuCache.DataSource.ContentSourcesSelect";
+ public const string ContentSourcesCount = "Umbraco.Web.PublishedCache.NuCache.DataSource.ContentSourcesCount";
+ public const string MediaSourcesSelect = "Umbraco.Web.PublishedCache.NuCache.DataSource.MediaSourcesSelect";
+ public const string MediaSourcesCount = "Umbraco.Web.PublishedCache.NuCache.DataSource.MediaSourcesCount";
+ public const string ObjectTypeNotTrashedFilter = "Umbraco.Web.PublishedCache.NuCache.DataSource.ObjectTypeNotTrashedFilter";
+ public const string OrderByLevelIdSortOrder = "Umbraco.Web.PublishedCache.NuCache.DataSource.OrderByLevelIdSortOrder";
+
+ }
}
}
}
diff --git a/src/Umbraco.Core/Constants-SvgSanitizer.cs b/src/Umbraco.Core/Constants-SvgSanitizer.cs
new file mode 100644
index 0000000000..c92b9f56c7
--- /dev/null
+++ b/src/Umbraco.Core/Constants-SvgSanitizer.cs
@@ -0,0 +1,23 @@
+using System.Collections.Generic;
+
+namespace Umbraco.Core
+{
+ public static partial class Constants
+ {
+ ///
+ /// Defines the alias identifiers for Umbraco's core application sections.
+ ///
+ public static class SvgSanitizer
+ {
+ ///
+ /// Allowlist for SVG attributes.
+ ///
+ public static readonly IList Attributes = new [] { "accent-height", "accumulate", "additive", "alignment-baseline", "allowReorder", "alphabetic", "amplitude", "arabic-form", "ascent", "attributeName", "attributeType", "autoReverse", "azimuth", "baseFrequency", "baseline-shift", "baseProfile", "bbox", "begin", "bias", "by", "calcMode", "cap-height", "class", "clip", "clipPathUnits", "clip-path", "clip-rule", "color", "color-interpolation", "color-interpolation-filters", "color-profile", "color-rendering", "contentScriptType", "contentStyleType", "cursor", "cx", "cy", "d", "decelerate", "descent", "diffuseConstant", "direction", "display", "divisor", "dominant-baseline", "dur", "dx", "dy", "edgeMode", "elevation", "enable-background", "end", "exponent", "externalResourcesRequired", "Section", "fill", "fill-opacity", "fill-rule", "filter", "filterRes", "filterUnits", "flood-color", "flood-opacity", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight", "format", "from", "fr", "fx", "fy", "g1", "g2", "glyph-name", "glyph-orientation-horizontal", "glyph-orientation-vertical", "glyphRef", "gradientTransform", "gradientUnits", "hanging", "height", "href", "hreflang", "horiz-adv-x", "horiz-origin-x", "ISection", "id", "ideographic", "image-rendering", "in", "in2", "intercept", "k", "k1", "k2", "k3", "k4", "kernelMatrix", "kernelUnitLength", "kerning", "keyPoints", "keySplines", "keyTimes", "lang", "lengthAdjust", "letter-spacing", "lighting-color", "limitingConeAngle", "local", "MSection", "marker-end", "marker-mid", "marker-start", "markerHeight", "markerUnits", "markerWidth", "mask", "maskContentUnits", "maskUnits", "mathematical", "max", "media", "method", "min", "mode", "NSection", "name", "numOctaves", "offset", "opacity", "operator", "order", "orient", "orientation", "origin", "overflow", "overline-position", "overline-thickness", "panose-1", "paint-order", "path", "pathLength", "patternContentUnits", "patternTransform", "patternUnits", "ping", "pointer-events", "points", "pointsAtX", "pointsAtY", "pointsAtZ", "preserveAlpha", "preserveAspectRatio", "primitiveUnits", "r", "radius", "referrerPolicy", "refX", "refY", "rel", "rendering-intent", "repeatCount", "repeatDur", "requiredExtensions", "requiredFeatures", "restart", "result", "rotate", "rx", "ry", "scale", "seed", "shape-rendering", "slope", "spacing", "specularConstant", "specularExponent", "speed", "spreadMethod", "startOffset", "stdDeviation", "stemh", "stemv", "stitchTiles", "stop-color", "stop-opacity", "strikethrough-position", "strikethrough-thickness", "string", "stroke", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke-width", "style", "surfaceScale", "systemLanguage", "tabindex", "tableValues", "target", "targetX", "targetY", "text-anchor", "text-decoration", "text-rendering", "textLength", "to", "transform", "type", "u1", "u2", "underline-position", "underline-thickness", "unicode", "unicode-bidi", "unicode-range", "units-per-em", "v-alphabetic", "v-hanging", "v-ideographic", "v-mathematical", "values", "vector-effect", "version", "vert-adv-y", "vert-origin-x", "vert-origin-y", "viewBox", "viewTarget", "visibility", "width", "widths", "word-spacing", "writing-mode", "x", "x-height", "x1", "x2", "xChannelSelector", "xlink:actuate", "xlink:arcrole", "xlink:href", "xlink:role", "xlink:show", "xlink:title", "xlink:type", "xml:base", "xml:lang", "xml:space", "y", "y1", "y2", "yChannelSelector", "z", "zoomAndPan" };
+
+ ///
+ /// Allowlist for SVG tabs.
+ ///
+ public static readonly IList Tags = new [] { "a", "altGlyph", "altGlyphDef", "altGlyphItem", "animate", "animateColor", "animateMotion", "animateTransform", "circle", "clipPath", "color-profile", "cursor", "defs", "desc", "discard", "ellipse", "feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDistantLight", "feDropShadow", "feFlood", "feFuncA", "feFuncB", "feFuncG", "feFuncR", "feGaussianBlur", "feImage", "feMerge", "feMergeNode", "feMorphology", "feOffset", "fePointLight", "feSpecularLighting", "feSpotLight", "feTile", "feTurbulence", "filter", "font", "font-face", "font-face-format", "font-face-name", "font-face-src", "font-face-uri", "foreignObject", "g", "glyph", "glyphRef", "hatch", "hatchpath", "hkern", "image", "line", "linearGradient", "marker", "mask", "mesh", "meshgradient", "meshpatch", "meshrow", "metadata", "missing-glyph", "mpath", "path", "pattern", "polygon", "polyline", "radialGradient", "rect", "set", "solidcolor", "stop", "svg", "switch", "symbol", "text", "textPath", "title", "tref", "tspan", "unknown", "use", "view", "vkern" };
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs
index 64216ba571..80906a0663 100644
--- a/src/Umbraco.Core/Constants-Web.cs
+++ b/src/Umbraco.Core/Constants-Web.cs
@@ -17,6 +17,11 @@
/// The preview cookie name
///
public const string PreviewCookieName = "UMB_PREVIEW";
+ ///
+
+ /// Client-side cookie that determines whether the user has accepted to be in Preview Mode when visiting the website.
+ ///
+ public const string AcceptPreviewCookieName = "UMB-WEBSITE-PREVIEW-ACCEPT";
public const string InstallerCookieName = "umb_installId";
}
diff --git a/src/Umbraco.Core/Contants-UdiEntityType.cs b/src/Umbraco.Core/Contants-UdiEntityType.cs
index 75a137bd2e..1ed862f328 100644
--- a/src/Umbraco.Core/Contants-UdiEntityType.cs
+++ b/src/Umbraco.Core/Contants-UdiEntityType.cs
@@ -25,6 +25,7 @@ namespace Umbraco.Core
{ Unknown, UdiType.Unknown },
{ AnyGuid, UdiType.GuidUdi },
+ { Element, UdiType.GuidUdi },
{ Document, UdiType.GuidUdi },
{ DocumentBlueprint, UdiType.GuidUdi },
{ Media, UdiType.GuidUdi },
@@ -64,6 +65,8 @@ namespace Umbraco.Core
public const string AnyGuid = "any-guid"; // that one is for tests
+ public const string Element = "element";
+
public const string Document = "document";
public const string DocumentBlueprint = "document-blueprint";
diff --git a/src/Umbraco.Core/ContentExtensions.cs b/src/Umbraco.Core/ContentExtensions.cs
index 7bce23e98e..eb6339741a 100644
--- a/src/Umbraco.Core/ContentExtensions.cs
+++ b/src/Umbraco.Core/ContentExtensions.cs
@@ -119,6 +119,15 @@ namespace Umbraco.Core
return false;
}
+ ///
+ /// Returns all properties based on the editorAlias
+ ///
+ ///
+ ///
+ ///
+ public static IEnumerable GetPropertiesByEditor(this IContentBase content, string editorAlias)
+ => content.Properties.Where(x => x.PropertyType.PropertyEditorAlias == editorAlias);
+
///
/// Returns properties that do not belong to a group
///
diff --git a/src/Umbraco.Core/DictionaryExtensions.cs b/src/Umbraco.Core/DictionaryExtensions.cs
index d9e998dbd1..88a042dbd5 100644
--- a/src/Umbraco.Core/DictionaryExtensions.cs
+++ b/src/Umbraco.Core/DictionaryExtensions.cs
@@ -253,7 +253,7 @@ namespace Umbraco.Core
{
builder.Append(String.Format("{0}={1}&", HttpUtility.UrlEncode(i.Key), i.Value == null ? string.Empty : HttpUtility.UrlEncode(i.Value.ToString())));
}
- return builder.ToString().TrimEnd('&');
+ return builder.ToString().TrimEnd(Constants.CharArrays.Ampersand);
}
/// The get entry ignore case.
diff --git a/src/Umbraco.Core/Exceptions/UnattendedInstallException.cs b/src/Umbraco.Core/Exceptions/UnattendedInstallException.cs
new file mode 100644
index 0000000000..6f672d17cd
--- /dev/null
+++ b/src/Umbraco.Core/Exceptions/UnattendedInstallException.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Runtime.Serialization;
+
+namespace Umbraco.Core.Exceptions
+{
+ ///
+ /// An exception that is thrown if an unattended installation occurs.
+ ///
+ [Serializable]
+ public class UnattendedInstallException : Exception
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public UnattendedInstallException()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class with a specified error message.
+ ///
+ /// The message that describes the error.
+ public UnattendedInstallException(string message) : base(message)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class with a specified error message
+ /// and a reference to the inner exception which is the cause of this exception.
+ ///
+ /// The message that describes the error.
+ /// The inner exception, or null.
+ public UnattendedInstallException(string message, Exception innerException) : base(message, innerException)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The that holds the serialized object data about the exception being thrown.
+ /// The that contains contextual information about the source or destination.
+ protected UnattendedInstallException(SerializationInfo info, StreamingContext context) : base(info, context)
+ {
+ }
+ }
+}
diff --git a/src/Umbraco.Core/GuidUdi.cs b/src/Umbraco.Core/GuidUdi.cs
index d78a33a435..3743f6f108 100644
--- a/src/Umbraco.Core/GuidUdi.cs
+++ b/src/Umbraco.Core/GuidUdi.cs
@@ -33,8 +33,8 @@ namespace Umbraco.Core
: base(uriValue)
{
Guid guid;
- if (Guid.TryParse(uriValue.AbsolutePath.TrimStart('/'), out guid) == false)
- throw new FormatException("Url \"" + uriValue + "\" is not a guid entity id.");
+ if (Guid.TryParse(uriValue.AbsolutePath.TrimStart(Constants.CharArrays.ForwardSlash), out guid) == false)
+ throw new FormatException("URI \"" + uriValue + "\" is not a GUID entity ID.");
Guid = guid;
}
@@ -48,7 +48,7 @@ namespace Umbraco.Core
{
var udi = Udi.Parse(s);
if (udi is GuidUdi == false)
- throw new FormatException("String \"" + s + "\" is not a guid entity id.");
+ throw new FormatException("String \"" + s + "\" is not a GUID entity id.");
return (GuidUdi) udi;
}
diff --git a/src/Umbraco.Core/HttpContextExtensions.cs b/src/Umbraco.Core/HttpContextExtensions.cs
index 22eb4d1917..e9ac1aa861 100644
--- a/src/Umbraco.Core/HttpContextExtensions.cs
+++ b/src/Umbraco.Core/HttpContextExtensions.cs
@@ -50,7 +50,7 @@ namespace Umbraco.Core
if (string.IsNullOrEmpty(ipAddress))
return request.UserHostAddress;
- var addresses = ipAddress.Split(',');
+ var addresses = ipAddress.Split(Constants.CharArrays.Comma);
if (addresses.Length != 0)
return addresses[0];
diff --git a/src/Umbraco.Core/IO/IFileSystem.cs b/src/Umbraco.Core/IO/IFileSystem.cs
index 0405c5925a..9de1ea40f2 100644
--- a/src/Umbraco.Core/IO/IFileSystem.cs
+++ b/src/Umbraco.Core/IO/IFileSystem.cs
@@ -102,7 +102,7 @@ namespace Umbraco.Core.IO
///
/// Returns the application relative path to the file.
///
- /// The full path or url.
+ /// The full path or URL.
///
/// The representing the relative path.
///
@@ -118,11 +118,11 @@ namespace Umbraco.Core.IO
string GetFullPath(string path);
///
- /// Returns the application relative url to the file.
+ /// Returns the application relative URL to the file.
///
- /// The path to return the url for.
+ /// The path to return the URL for.
///
- /// representing the relative url.
+ /// representing the relative URL.
///
string GetUrl(string path);
diff --git a/src/Umbraco.Core/IO/IOHelper.cs b/src/Umbraco.Core/IO/IOHelper.cs
index 53aa5a8179..8661f73fb1 100644
--- a/src/Umbraco.Core/IO/IOHelper.cs
+++ b/src/Umbraco.Core/IO/IOHelper.cs
@@ -40,7 +40,7 @@ namespace Umbraco.Core.IO
retval = virtualPath.Replace("~", SystemDirectories.Root);
if (virtualPath.StartsWith("/") && virtualPath.StartsWith(SystemDirectories.Root) == false)
- retval = SystemDirectories.Root + "/" + virtualPath.TrimStart('/');
+ retval = SystemDirectories.Root + "/" + virtualPath.TrimStart(Constants.CharArrays.ForwardSlash);
return retval;
}
@@ -98,11 +98,11 @@ namespace Umbraco.Core.IO
if (String.IsNullOrEmpty(path) == false && (path.StartsWith("~") || path.StartsWith(SystemDirectories.Root)))
return HostingEnvironment.MapPath(path);
else
- return HostingEnvironment.MapPath("~/" + path.TrimStart('/'));
+ return HostingEnvironment.MapPath("~/" + path.TrimStart(Constants.CharArrays.ForwardSlash));
}
var root = GetRootDirectorySafe();
- var newPath = path.TrimStart('~', '/').Replace('/', IOHelper.DirSepChar);
+ var newPath = path.TrimStart(Constants.CharArrays.TildeForwardSlash).Replace('/', IOHelper.DirSepChar);
var retval = root + IOHelper.DirSepChar.ToString(CultureInfo.InvariantCulture) + newPath;
return retval;
@@ -121,7 +121,7 @@ namespace Umbraco.Core.IO
if (string.IsNullOrEmpty(retval))
retval = standardPath;
- return retval.TrimEnd('/');
+ return retval.TrimEnd(Constants.CharArrays.ForwardSlash);
}
internal static string ReturnPath(string settingsKey, string standardPath)
@@ -188,7 +188,7 @@ namespace Umbraco.Core.IO
internal static bool VerifyFileExtension(string filePath, IEnumerable validFileExtensions)
{
var ext = Path.GetExtension(filePath);
- return ext != null && validFileExtensions.Contains(ext.TrimStart('.'));
+ return ext != null && validFileExtensions.Contains(ext.TrimStart(Constants.CharArrays.Period));
}
public static bool PathStartsWith(string path, string root, char separator)
diff --git a/src/Umbraco.Core/IO/PhysicalFileSystem.cs b/src/Umbraco.Core/IO/PhysicalFileSystem.cs
index 96aaf7ca27..a833ba43af 100644
--- a/src/Umbraco.Core/IO/PhysicalFileSystem.cs
+++ b/src/Umbraco.Core/IO/PhysicalFileSystem.cs
@@ -19,7 +19,7 @@ namespace Umbraco.Core.IO
// (is used in GetRelativePath)
private readonly string _rootPathFwd;
- // the relative url, using url separator chars, NOT ending with a separator
+ // the relative URL, using URL separator chars, NOT ending with a separator
// eg "" or "/Views" or "/Media" or "//Media" in case of a virtual path
private readonly string _rootUrl;
@@ -33,7 +33,7 @@ namespace Umbraco.Core.IO
_rootPath = EnsureDirectorySeparatorChar(IOHelper.MapPath(virtualRoot)).TrimEnd(Path.DirectorySeparatorChar);
_rootPathFwd = EnsureUrlSeparatorChar(_rootPath);
- _rootUrl = EnsureUrlSeparatorChar(IOHelper.ResolveUrl(virtualRoot)).TrimEnd('/');
+ _rootUrl = EnsureUrlSeparatorChar(IOHelper.ResolveUrl(virtualRoot)).TrimEnd(Constants.CharArrays.ForwardSlash);
}
public PhysicalFileSystem(string rootPath, string rootUrl)
@@ -54,7 +54,7 @@ namespace Umbraco.Core.IO
_rootPath = EnsureDirectorySeparatorChar(rootPath).TrimEnd(Path.DirectorySeparatorChar);
_rootPathFwd = EnsureUrlSeparatorChar(_rootPath);
- _rootUrl = EnsureUrlSeparatorChar(rootUrl).TrimEnd('/');
+ _rootUrl = EnsureUrlSeparatorChar(rootUrl).TrimEnd(Constants.CharArrays.ForwardSlash);
}
///
@@ -243,9 +243,9 @@ namespace Umbraco.Core.IO
}
///
- /// Gets the filesystem-relative path of a full path or of an url.
+ /// Gets the filesystem-relative path of a full path or of an URL.
///
- /// The full path or url.
+ /// The full path or URL.
/// The path, relative to this filesystem's root.
///
/// The relative path is relative to this filesystem's root, not starting with any
@@ -253,18 +253,18 @@ namespace Umbraco.Core.IO
///
public string GetRelativePath(string fullPathOrUrl)
{
- // test url
- var path = fullPathOrUrl.Replace('\\', '/'); // ensure url separator char
+ // test URL
+ var path = fullPathOrUrl.Replace('\\', '/'); // ensure URL separator char
- // if it starts with the root url, strip it and trim the starting slash to make it relative
+ // if it starts with the root URL, strip it and trim the starting slash to make it relative
// eg "/Media/1234/img.jpg" => "1234/img.jpg"
if (IOHelper.PathStartsWith(path, _rootUrl, '/'))
- return path.Substring(_rootUrl.Length).TrimStart('/');
+ return path.Substring(_rootUrl.Length).TrimStart(Constants.CharArrays.ForwardSlash);
// if it starts with the root path, strip it and trim the starting slash to make it relative
// eg "c:/websites/test/root/Media/1234/img.jpg" => "1234/img.jpg"
if (IOHelper.PathStartsWith(path, _rootPathFwd, '/'))
- return path.Substring(_rootPathFwd.Length).TrimStart('/');
+ return path.Substring(_rootPathFwd.Length).TrimStart(Constants.CharArrays.ForwardSlash);
// unchanged - what else?
return path;
@@ -319,14 +319,14 @@ namespace Umbraco.Core.IO
}
///
- /// Gets the url.
+ /// Gets the URL.
///
/// The filesystem-relative path.
- /// The url.
+ /// The URL.
/// All separators are forward-slashes.
public string GetUrl(string path)
{
- path = EnsureUrlSeparatorChar(path).Trim('/');
+ path = EnsureUrlSeparatorChar(path).Trim(Constants.CharArrays.ForwardSlash);
return _rootUrl + "/" + path;
}
diff --git a/src/Umbraco.Core/IO/ShadowFileSystem.cs b/src/Umbraco.Core/IO/ShadowFileSystem.cs
index 84ff1b428b..0e9390d13f 100644
--- a/src/Umbraco.Core/IO/ShadowFileSystem.cs
+++ b/src/Umbraco.Core/IO/ShadowFileSystem.cs
@@ -182,7 +182,7 @@ namespace Umbraco.Core.IO
if (Nodes.TryGetValue(normPath, out sf) && sf.IsExist && (sf.IsDir || overrideIfExists == false))
throw new InvalidOperationException(string.Format("A file at path '{0}' already exists", path));
- var parts = normPath.Split('/');
+ var parts = normPath.Split(Constants.CharArrays.ForwardSlash);
for (var i = 0; i < parts.Length - 1; i++)
{
var dirPath = string.Join("/", parts.Take(i + 1));
@@ -297,7 +297,7 @@ namespace Umbraco.Core.IO
if (Nodes.TryGetValue(normPath, out sf) && sf.IsExist && (sf.IsDir || overrideIfExists == false))
throw new InvalidOperationException(string.Format("A file at path '{0}' already exists", path));
- var parts = normPath.Split('/');
+ var parts = normPath.Split(Constants.CharArrays.ForwardSlash);
for (var i = 0; i < parts.Length - 1; i++)
{
var dirPath = string.Join("/", parts.Take(i + 1));
diff --git a/src/Umbraco.Core/IO/SystemDirectories.cs b/src/Umbraco.Core/IO/SystemDirectories.cs
index b4688d2e9f..485fd7f965 100644
--- a/src/Umbraco.Core/IO/SystemDirectories.cs
+++ b/src/Umbraco.Core/IO/SystemDirectories.cs
@@ -11,6 +11,8 @@ namespace Umbraco.Core.IO
public static string Data => "~/App_Data";
+ public static string LogFiles => Data + "/Logs";
+
public static string TempData => Data + "/TEMP";
public static string TempFileUploads => TempData + "/FileUploads";
diff --git a/src/Umbraco.Core/IO/SystemFiles.cs b/src/Umbraco.Core/IO/SystemFiles.cs
index 12e3f57d99..337c97d081 100644
--- a/src/Umbraco.Core/IO/SystemFiles.cs
+++ b/src/Umbraco.Core/IO/SystemFiles.cs
@@ -7,6 +7,8 @@ namespace Umbraco.Core.IO
{
public static string TinyMceConfig => SystemDirectories.Config + "/tinyMceConfig.config";
+ public static string UmbracoSettings => SystemDirectories.Config + "/UmbracoSettings.config";
+
// TODO: Kill this off we don't have umbraco.config XML cache we now have NuCache
public static string GetContentCacheXml(IGlobalSettings globalSettings)
{
diff --git a/src/Umbraco.Core/IRuntimeState.cs b/src/Umbraco.Core/IRuntimeState.cs
index 30c768ab01..9f188b5308 100644
--- a/src/Umbraco.Core/IRuntimeState.cs
+++ b/src/Umbraco.Core/IRuntimeState.cs
@@ -41,7 +41,7 @@ namespace Umbraco.Core
ServerRole ServerRole { get; }
///
- /// Gets the Umbraco application url.
+ /// Gets the Umbraco application URL.
///
/// This is eg "http://www.example.com".
Uri ApplicationUrl { get; }
diff --git a/src/Umbraco.Core/Logging/LoggingTaskExtension.cs b/src/Umbraco.Core/Logging/LoggingTaskExtension.cs
index 1f742133c3..2e3aa0a883 100644
--- a/src/Umbraco.Core/Logging/LoggingTaskExtension.cs
+++ b/src/Umbraco.Core/Logging/LoggingTaskExtension.cs
@@ -1,4 +1,5 @@
using System;
+using System.Threading;
using System.Threading.Tasks;
namespace Umbraco.Core.Logging
@@ -14,7 +15,12 @@ namespace Umbraco.Core.Logging
///
public static Task LogErrors(this Task task, Action logMethod)
{
- return task.ContinueWith(t => LogErrorsInner(t, logMethod), TaskContinuationOptions.OnlyOnFaulted);
+ return task.ContinueWith(
+ t => LogErrorsInner(t, logMethod),
+ CancellationToken.None,
+ TaskContinuationOptions.OnlyOnFaulted,
+ // Must explicitly specify this, see https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html
+ TaskScheduler.Default);
}
///
@@ -26,7 +32,10 @@ namespace Umbraco.Core.Logging
///
public static Task LogErrorsWaitable(this Task task, Action logMethod)
{
- return task.ContinueWith(t => LogErrorsInner(t, logMethod));
+ return task.ContinueWith(
+ t => LogErrorsInner(t, logMethod),
+ // Must explicitly specify this, see https://blog.stephencleary.com/2013/10/continuewith-is-dangerous-too.html
+ TaskScheduler.Default);
}
private static void LogErrorsInner(Task task, Action logAction)
diff --git a/src/Umbraco.Core/Logging/Serilog/LoggerConfigExtensions.cs b/src/Umbraco.Core/Logging/Serilog/LoggerConfigExtensions.cs
index 951d6f8787..6f8c32cf0c 100644
--- a/src/Umbraco.Core/Logging/Serilog/LoggerConfigExtensions.cs
+++ b/src/Umbraco.Core/Logging/Serilog/LoggerConfigExtensions.cs
@@ -7,6 +7,7 @@ using Serilog.Core;
using Serilog.Events;
using Serilog.Formatting;
using Serilog.Formatting.Compact;
+using Umbraco.Core.IO;
using Umbraco.Core.Logging.Serilog.Enrichers;
namespace Umbraco.Core.Logging.Serilog
@@ -26,7 +27,7 @@ namespace Umbraco.Core.Logging.Serilog
//Set this environment variable - so that it can be used in external config file
//add key="serilog:write-to:RollingFile.pathFormat" value="%BASEDIR%\logs\log.txt" />
- Environment.SetEnvironmentVariable("BASEDIR", AppDomain.CurrentDomain.BaseDirectory, EnvironmentVariableTarget.Process);
+ Environment.SetEnvironmentVariable("BASEDIR", IOHelper.MapPath("/").TrimEnd("\\"), EnvironmentVariableTarget.Process);
Environment.SetEnvironmentVariable("MACHINENAME", Environment.MachineName, EnvironmentVariableTarget.Process);
logConfig.MinimumLevel.Verbose() //Set to highest level of logging (as any sinks may want to restrict it to Errors only)
@@ -54,7 +55,7 @@ namespace Umbraco.Core.Logging.Serilog
{
//Main .txt logfile - in similar format to older Log4Net output
//Ends with ..txt as Date is inserted before file extension substring
- logConfig.WriteTo.File($@"{AppDomain.CurrentDomain.BaseDirectory}\App_Data\Logs\UmbracoTraceLog.{Environment.MachineName}..txt",
+ logConfig.WriteTo.File(IOHelper.MapPath(SystemDirectories.LogFiles + $"/UmbracoTraceLog.{Environment.MachineName}..txt"),
shared: true,
rollingInterval: RollingInterval.Day,
restrictedToMinimumLevel: minimumLevel,
@@ -110,7 +111,8 @@ namespace Umbraco.Core.Logging.Serilog
{
//.clef format (Compact log event format, that can be imported into local SEQ & will make searching/filtering logs easier)
//Ends with ..txt as Date is inserted before file extension substring
- logConfig.WriteTo.File(new CompactJsonFormatter(), $@"{AppDomain.CurrentDomain.BaseDirectory}\App_Data\Logs\UmbracoTraceLog.{Environment.MachineName}..json",
+ logConfig.WriteTo.File(new CompactJsonFormatter(),
+ IOHelper.MapPath(SystemDirectories.LogFiles + $"/UmbracoTraceLog.{Environment.MachineName}..json"),
shared: true,
rollingInterval: RollingInterval.Day, //Create a new JSON file every day
retainedFileCountLimit: retainedFileCount, //Setting to null means we keep all files - default is 31 days
@@ -127,7 +129,7 @@ namespace Umbraco.Core.Logging.Serilog
public static LoggerConfiguration ReadFromConfigFile(this LoggerConfiguration logConfig)
{
//Read from main serilog.config file
- logConfig.ReadFrom.AppSettings(filePath: AppDomain.CurrentDomain.BaseDirectory + @"\config\serilog.config");
+ logConfig.ReadFrom.AppSettings(filePath: IOHelper.MapPath(SystemDirectories.Config + "/serilog.config"));
return logConfig;
}
@@ -141,7 +143,7 @@ namespace Umbraco.Core.Logging.Serilog
{
//A nested logger - where any user configured sinks via config can not effect the main 'umbraco' logger above
logConfig.WriteTo.Logger(cfg =>
- cfg.ReadFrom.AppSettings(filePath: AppDomain.CurrentDomain.BaseDirectory + @"\config\serilog.user.config"));
+ cfg.ReadFrom.AppSettings(filePath: IOHelper.MapPath(SystemDirectories.Config + "/serilog.user.config")));
return logConfig;
}
diff --git a/src/Umbraco.Core/Logging/Viewer/JsonLogViewer.cs b/src/Umbraco.Core/Logging/Viewer/JsonLogViewer.cs
index bbe2f3704d..533c9847ce 100644
--- a/src/Umbraco.Core/Logging/Viewer/JsonLogViewer.cs
+++ b/src/Umbraco.Core/Logging/Viewer/JsonLogViewer.cs
@@ -5,6 +5,7 @@ using System.Linq;
using Newtonsoft.Json;
using Serilog.Events;
using Serilog.Formatting.Compact.Reader;
+using Umbraco.Core.IO;
namespace Umbraco.Core.Logging.Viewer
{
@@ -16,7 +17,7 @@ namespace Umbraco.Core.Logging.Viewer
public JsonLogViewer(ILogger logger, string logsPath = "", string searchPath = "") : base(searchPath)
{
if (string.IsNullOrEmpty(logsPath))
- logsPath = $@"{AppDomain.CurrentDomain.BaseDirectory}\App_Data\Logs\";
+ logsPath = IOHelper.MapPath(SystemDirectories.LogFiles);
_logsPath = logsPath;
_logger = logger;
@@ -62,7 +63,7 @@ namespace Umbraco.Core.Logging.Viewer
var logs = new List();
//Log Directory
- var logDirectory = $@"{AppDomain.CurrentDomain.BaseDirectory}\App_Data\Logs\";
+ var logDirectory = _logsPath;
var count = 0;
diff --git a/src/Umbraco.Core/Media/IEmbedProvider.cs b/src/Umbraco.Core/Media/IEmbedProvider.cs
index 99b162e0b7..39da6fae0d 100644
--- a/src/Umbraco.Core/Media/IEmbedProvider.cs
+++ b/src/Umbraco.Core/Media/IEmbedProvider.cs
@@ -15,7 +15,7 @@ namespace Umbraco.Core.Media
string[] UrlSchemeRegex { get; }
///
- /// A collection of querystring request parameters to append to the API Url
+ /// A collection of querystring request parameters to append to the API URL
///
/// ?key=value&key2=value2
Dictionary RequestParams { get; }
diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs b/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs
index d5d8bbab6f..fb9b8af46d 100644
--- a/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs
+++ b/src/Umbraco.Core/Migrations/Install/DatabaseBuilder.cs
@@ -112,6 +112,14 @@ namespace Umbraco.Core.Migrations.Install
}
}
+ internal bool IsUmbracoInstalled()
+ {
+ using (var scope = _scopeProvider.CreateScope(autoComplete: true))
+ {
+ return scope.Database.IsUmbracoInstalled(_logger);
+ }
+ }
+
#endregion
#region Configure Connection String
@@ -346,7 +354,7 @@ namespace Umbraco.Core.Migrations.Install
var sqlCeDatabaseExists = false;
if (dbIsSqlCe)
{
- var parts = databaseSettings.ConnectionString.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
+ var parts = databaseSettings.ConnectionString.Split(Constants.CharArrays.Semicolon, StringSplitOptions.RemoveEmptyEntries);
var dataSourcePart = parts.FirstOrDefault(x => x.InvariantStartsWith("Data Source="));
if (dataSourcePart != null)
{
@@ -391,15 +399,15 @@ namespace Umbraco.Core.Migrations.Install
private DatabaseSchemaResult ValidateSchema(IScope scope)
{
if (_databaseFactory.Initialized == false)
- return new DatabaseSchemaResult(_databaseFactory.SqlContext.SqlSyntax);
+ return new DatabaseSchemaResult();
if (_databaseSchemaValidationResult != null)
return _databaseSchemaValidationResult;
- var database = scope.Database;
- var dbSchema = new DatabaseSchemaCreator(database, _logger);
- _databaseSchemaValidationResult = dbSchema.ValidateSchema();
+ _databaseSchemaValidationResult = scope.Database.ValidateSchema(_logger);
+
scope.Complete();
+
return _databaseSchemaValidationResult;
}
@@ -438,11 +446,9 @@ namespace Umbraco.Core.Migrations.Install
var schemaResult = ValidateSchema();
var hasInstalledVersion = schemaResult.DetermineHasInstalledVersion();
- //var installedSchemaVersion = schemaResult.DetermineInstalledVersion();
- //var hasInstalledVersion = !installedSchemaVersion.Equals(new Version(0, 0, 0));
- //If Configuration Status is empty and the determined version is "empty" its a new install - otherwise upgrade the existing
- if (string.IsNullOrEmpty(_globalSettings.ConfigurationStatus) && !hasInstalledVersion)
+ //If the determined version is "empty" its a new install - otherwise upgrade the existing
+ if (!hasInstalledVersion)
{
if (_runtime.Level == RuntimeLevel.Run)
throw new Exception("Umbraco is already configured!");
diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs
index eab7afe308..e9580da74a 100644
--- a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs
+++ b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs
@@ -21,8 +21,13 @@ namespace Umbraco.Core.Migrations.Install
public DatabaseSchemaCreator(IUmbracoDatabase database, ILogger logger)
{
- _database = database;
- _logger = logger;
+ _database = database ?? throw new ArgumentNullException(nameof(database));
+ _logger = logger ?? throw new ArgumentNullException(nameof(logger));
+
+ if (_database?.SqlContext?.SqlSyntax == null)
+ {
+ throw new InvalidOperationException("No SqlContext has been assigned to the database");
+ }
}
private ISqlSyntaxProvider SqlSyntax => _database.SqlContext.SqlSyntax;
@@ -143,7 +148,7 @@ namespace Umbraco.Core.Migrations.Install
internal DatabaseSchemaResult ValidateSchema(IEnumerable orderedTables)
{
- var result = new DatabaseSchemaResult(SqlSyntax);
+ var result = new DatabaseSchemaResult();
result.IndexDefinitions.AddRange(SqlSyntax.GetDefinedIndexes(_database)
.Select(x => new DbIndexDefinition(x)));
@@ -445,14 +450,14 @@ namespace Umbraco.Core.Migrations.Install
}
//Execute the Create Table sql
- var created = _database.Execute(new Sql(createSql));
- _logger.Info("Create Table {TableName} ({Created}): \n {Sql}", tableName, created, createSql);
+ _database.Execute(new Sql(createSql));
+ _logger.Info("Create Table {TableName}: \n {Sql}", tableName, createSql);
//If any statements exists for the primary key execute them here
if (string.IsNullOrEmpty(createPrimaryKeySql) == false)
{
- var createdPk = _database.Execute(new Sql(createPrimaryKeySql));
- _logger.Info("Create Primary Key ({CreatedPk}):\n {Sql}", createdPk, createPrimaryKeySql);
+ _database.Execute(new Sql(createPrimaryKeySql));
+ _logger.Info("Create Primary Key:\n {Sql}", createPrimaryKeySql);
}
if (SqlSyntax.SupportsIdentityInsert() && tableDefinition.Columns.Any(x => x.IsIdentity))
@@ -469,24 +474,24 @@ namespace Umbraco.Core.Migrations.Install
//Loop through index statements and execute sql
foreach (var sql in indexSql)
{
- var createdIndex = _database.Execute(new Sql(sql));
- _logger.Info("Create Index ({CreatedIndex}):\n {Sql}", createdIndex, sql);
+ _database.Execute(new Sql(sql));
+ _logger.Info("Create Index:\n {Sql}", sql);
}
//Loop through foreignkey statements and execute sql
foreach (var sql in foreignSql)
{
- var createdFk = _database.Execute(new Sql(sql));
- _logger.Info("Create Foreign Key ({CreatedFk}):\n {Sql}", createdFk, sql);
+ _database.Execute(new Sql(sql));
+ _logger.Info("Create Foreign Key:\n {Sql}", sql);
}
if (overwrite)
{
- _logger.Info("Table {TableName} was recreated", tableName);
+ _logger.Info("Table {TableName} was recreated", tableName);
}
else
{
- _logger.Info("New table {TableName} was created", tableName);
+ _logger.Info("New table {TableName} was created", tableName);
}
}
diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaResult.cs b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaResult.cs
index f21216fde3..c0d8eff311 100644
--- a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaResult.cs
+++ b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaResult.cs
@@ -2,8 +2,8 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
+using Umbraco.Core.Persistence;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
-using Umbraco.Core.Persistence.SqlSyntax;
namespace Umbraco.Core.Migrations.Install
{
@@ -12,7 +12,7 @@ namespace Umbraco.Core.Migrations.Install
///
internal class DatabaseSchemaResult
{
- public DatabaseSchemaResult(ISqlSyntaxProvider sqlSyntax)
+ public DatabaseSchemaResult()
{
Errors = new List>();
TableDefinitions = new List();
diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs
index f0cfc08281..2a24c800b5 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs
@@ -7,6 +7,9 @@ using Umbraco.Core.Migrations.Upgrade.V_8_0_0;
using Umbraco.Core.Migrations.Upgrade.V_8_0_1;
using Umbraco.Core.Migrations.Upgrade.V_8_1_0;
using Umbraco.Core.Migrations.Upgrade.V_8_6_0;
+using Umbraco.Core.Migrations.Upgrade.V_8_9_0;
+using Umbraco.Core.Migrations.Upgrade.V_8_10_0;
+using Umbraco.Core.Migrations.Upgrade.V_8_12_0;
namespace Umbraco.Core.Migrations.Upgrade
{
@@ -175,25 +178,35 @@ namespace Umbraco.Core.Migrations.Upgrade
To("{78BAF571-90D0-4D28-8175-EF96316DA789}");
// release-8.0.0
- // to 8.0.1...
+ // to 8.0.1
To("{80C0A0CB-0DD5-4573-B000-C4B7C313C70D}");
// release-8.0.1
- // to 8.1.0...
+ // to 8.1.0
To("{B69B6E8C-A769-4044-A27E-4A4E18D1645A}");
To("{0372A42B-DECF-498D-B4D1-6379E907EB94}");
To("{5B1E0D93-F5A3-449B-84BA-65366B84E2D4}");
- // to 8.6.0...
+ // to 8.6.0
To("{4759A294-9860-46BC-99F9-B4C975CAE580}");
To("{0BC866BC-0665-487A-9913-0290BD0169AD}");
To("{3D67D2C8-5E65-47D0-A9E1-DC2EE0779D6B}");
To("{EE288A91-531B-4995-8179-1D62D9AA3E2E}");
To("{2AB29964-02A1-474D-BD6B-72148D2A53A2}");
-
+ // to 8.7.0
To("{a78e3369-8ea3-40ec-ad3f-5f76929d2b20}");
+ // to 8.9.0
+ To("{B5838FF5-1D22-4F6C-BCEB-F83ACB14B575}");
+
+ // to 8.10.0
+ To("{D6A8D863-38EC-44FB-91EC-ACD6A668BD18}");
+
+ // to 8.12.0...
+ To("{8DDDCD0B-D7D5-4C97-BD6A-6B38CA65752F}");
+ To("{4695D0C9-0729-4976-985B-048D503665D8}");
+
//FINAL
}
}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigrationBase.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigrationBase.cs
index 605f8a9eed..423ba71401 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigrationBase.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigrationBase.cs
@@ -30,7 +30,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
protected int[] ConvertStringValues(string val)
{
- var splitVals = val.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
+ var splitVals = val.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries);
var intVals = splitVals
.Select(x => int.TryParse(x, out var i) ? i : int.MinValue)
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_10_0/AddPropertyTypeLabelOnTopColumn.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_10_0/AddPropertyTypeLabelOnTopColumn.cs
new file mode 100644
index 0000000000..206ea2be02
--- /dev/null
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_10_0/AddPropertyTypeLabelOnTopColumn.cs
@@ -0,0 +1,20 @@
+using System.Linq;
+using Umbraco.Core.Persistence.Dtos;
+
+namespace Umbraco.Core.Migrations.Upgrade.V_8_10_0
+{
+
+ public class AddPropertyTypeLabelOnTopColumn : MigrationBase
+ {
+ public AddPropertyTypeLabelOnTopColumn(IMigrationContext context)
+ : base(context)
+ { }
+
+ public override void Migrate()
+ {
+ var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList();
+
+ AddColumnIfNotExists(columns, "labelOnTop");
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_12_0/AddCmsContentNuByteColumn.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_12_0/AddCmsContentNuByteColumn.cs
new file mode 100644
index 0000000000..7c793688ec
--- /dev/null
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_12_0/AddCmsContentNuByteColumn.cs
@@ -0,0 +1,21 @@
+using System.Linq;
+using Umbraco.Core.Persistence.Dtos;
+
+namespace Umbraco.Core.Migrations.Upgrade.V_8_12_0
+{
+ public class AddCmsContentNuByteColumn : MigrationBase
+ {
+ public AddCmsContentNuByteColumn(IMigrationContext context)
+ : base(context)
+ {
+
+ }
+
+ public override void Migrate()
+ {
+ var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToList();
+
+ AddColumnIfNotExists(columns, "dataRaw");
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_12_0/UpgradedIncludeIndexes.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_12_0/UpgradedIncludeIndexes.cs
new file mode 100644
index 0000000000..d88abdef75
--- /dev/null
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_12_0/UpgradedIncludeIndexes.cs
@@ -0,0 +1,65 @@
+using System.Linq;
+using Umbraco.Core.Migrations.Expressions.Execute.Expressions;
+using Umbraco.Core.Persistence.DatabaseModelDefinitions;
+using Umbraco.Core.Persistence.Dtos;
+
+namespace Umbraco.Core.Migrations.Upgrade.V_8_12_0
+{
+ public class UpgradedIncludeIndexes : MigrationBase
+ {
+ public UpgradedIncludeIndexes(IMigrationContext context)
+ : base(context)
+ {
+
+ }
+
+ public override void Migrate()
+ {
+ // Need to drop the FK for the redirect table before modifying the unique id index
+ Delete.ForeignKey()
+ .FromTable(Constants.DatabaseSchema.Tables.RedirectUrl)
+ .ForeignColumn("contentKey")
+ .ToTable(NodeDto.TableName)
+ .PrimaryColumn("uniqueID")
+ .Do();
+ var nodeDtoIndexes = new[] { $"IX_{NodeDto.TableName}_UniqueId", $"IX_{NodeDto.TableName}_ObjectType", $"IX_{NodeDto.TableName}_Level" };
+ DeleteIndexes(nodeDtoIndexes); // delete existing ones
+ CreateIndexes(nodeDtoIndexes); // update/add
+ // Now re-create the FK for the redirect table
+ Create.ForeignKey()
+ .FromTable(Constants.DatabaseSchema.Tables.RedirectUrl)
+ .ForeignColumn("contentKey")
+ .ToTable(NodeDto.TableName)
+ .PrimaryColumn("uniqueID")
+ .Do();
+
+
+ var contentVersionIndexes = new[] { $"IX_{ContentVersionDto.TableName}_NodeId", $"IX_{ContentVersionDto.TableName}_Current" };
+ DeleteIndexes(contentVersionIndexes); // delete existing ones
+ CreateIndexes(contentVersionIndexes); // update/add
+ }
+
+ private void DeleteIndexes(params string[] toDelete)
+ {
+ var tableDef = DefinitionFactory.GetTableDefinition(typeof(T), Context.SqlContext.SqlSyntax);
+
+ foreach (var i in toDelete)
+ if (IndexExists(i))
+ Delete.Index(i).OnTable(tableDef.Name).Do();
+
+ }
+
+ private void CreateIndexes(params string[] toCreate)
+ {
+ var tableDef = DefinitionFactory.GetTableDefinition(typeof(T), Context.SqlContext.SqlSyntax);
+
+ foreach (var c in toCreate)
+ {
+ // get the definition by name
+ var index = tableDef.Indexes.First(x => x.Name == c);
+ new ExecuteSqlStatementExpression(Context) { SqlStatement = Context.SqlContext.SqlSyntax.Format(index) }.Execute();
+ }
+
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_9_0/ExternalLoginTableUserData.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_9_0/ExternalLoginTableUserData.cs
new file mode 100644
index 0000000000..45bc1c620b
--- /dev/null
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_9_0/ExternalLoginTableUserData.cs
@@ -0,0 +1,20 @@
+using Umbraco.Core.Persistence.Dtos;
+
+namespace Umbraco.Core.Migrations.Upgrade.V_8_9_0
+{
+ public class ExternalLoginTableUserData : MigrationBase
+ {
+ public ExternalLoginTableUserData(IMigrationContext context)
+ : base(context)
+ {
+ }
+
+ ///
+ /// Adds new column to the External Login table
+ ///
+ public override void Migrate()
+ {
+ AddColumn(Constants.DatabaseSchema.Tables.ExternalLogin, "userData");
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Models/Blocks/BlockEditorData.cs b/src/Umbraco.Core/Models/Blocks/BlockEditorData.cs
new file mode 100644
index 0000000000..d8186f0dfa
--- /dev/null
+++ b/src/Umbraco.Core/Models/Blocks/BlockEditorData.cs
@@ -0,0 +1,45 @@
+using Newtonsoft.Json.Linq;
+using System;
+using System.Collections.Generic;
+
+namespace Umbraco.Core.Models.Blocks
+{
+
+ ///
+ /// Convertable block data from json
+ ///
+ public class BlockEditorData
+ {
+ private readonly string _propertyEditorAlias;
+
+ public static BlockEditorData Empty { get; } = new BlockEditorData();
+
+ private BlockEditorData()
+ {
+ BlockValue = new BlockValue();
+ }
+
+ public BlockEditorData(string propertyEditorAlias,
+ IEnumerable references,
+ BlockValue blockValue)
+ {
+ if (string.IsNullOrWhiteSpace(propertyEditorAlias))
+ throw new ArgumentException($"'{nameof(propertyEditorAlias)}' cannot be null or whitespace", nameof(propertyEditorAlias));
+ _propertyEditorAlias = propertyEditorAlias;
+ BlockValue = blockValue ?? throw new ArgumentNullException(nameof(blockValue));
+ References = references != null ? new List(references) : throw new ArgumentNullException(nameof(references));
+ }
+
+ ///
+ /// Returns the layout for this specific property editor
+ ///
+ public JToken Layout => BlockValue.Layout.TryGetValue(_propertyEditorAlias, out var layout) ? layout : null;
+
+ ///
+ /// Returns the reference to the original BlockValue
+ ///
+ public BlockValue BlockValue { get; }
+
+ public List References { get; } = new List();
+ }
+}
diff --git a/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs b/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs
new file mode 100644
index 0000000000..802e8c2ee3
--- /dev/null
+++ b/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs
@@ -0,0 +1,68 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using System.Linq;
+using System.Collections.Generic;
+
+namespace Umbraco.Core.Models.Blocks
+{
+
+ ///
+ /// Converts the block json data into objects
+ ///
+ public abstract class BlockEditorDataConverter
+ {
+ private readonly string _propertyEditorAlias;
+
+ protected BlockEditorDataConverter(string propertyEditorAlias)
+ {
+ _propertyEditorAlias = propertyEditorAlias;
+ }
+
+ public BlockEditorData ConvertFrom(JToken json)
+ {
+ var value = json.ToObject();
+ return Convert(value);
+ }
+
+ public bool TryDeserialize(string json, out BlockEditorData blockEditorData)
+ {
+ try
+ {
+ var value = JsonConvert.DeserializeObject(json);
+ blockEditorData = Convert(value);
+ return true;
+ }
+ catch (System.Exception)
+ {
+ blockEditorData = null;
+ return false;
+ }
+ }
+
+ public BlockEditorData Deserialize(string json)
+ {
+ var value = JsonConvert.DeserializeObject(json);
+ return Convert(value);
+ }
+
+ private BlockEditorData Convert(BlockValue value)
+ {
+ if (value.Layout == null)
+ return BlockEditorData.Empty;
+
+ var references = value.Layout.TryGetValue(_propertyEditorAlias, out var layout)
+ ? GetBlockReferences(layout)
+ : Enumerable.Empty();
+
+ return new BlockEditorData(_propertyEditorAlias, references, value);
+ }
+
+ ///
+ /// Return the collection of from the block editor's Layout (which could be an array or an object depending on the editor)
+ ///
+ ///
+ ///
+ protected abstract IEnumerable GetBlockReferences(JToken jsonLayout);
+
+ }
+}
diff --git a/src/Umbraco.Core/Models/Blocks/BlockItemData.cs b/src/Umbraco.Core/Models/Blocks/BlockItemData.cs
new file mode 100644
index 0000000000..02432766b0
--- /dev/null
+++ b/src/Umbraco.Core/Models/Blocks/BlockItemData.cs
@@ -0,0 +1,62 @@
+using Newtonsoft.Json;
+using System;
+using System.Collections.Generic;
+using Umbraco.Core.Serialization;
+
+namespace Umbraco.Core.Models.Blocks
+{
+ ///
+ /// Represents a single block's data in raw form
+ ///
+ public class BlockItemData
+ {
+ [JsonProperty("contentTypeKey")]
+ public Guid ContentTypeKey { get; set; }
+
+ ///
+ /// not serialized, manually set and used during internally
+ ///
+ [JsonIgnore]
+ public string ContentTypeAlias { get; set; }
+
+ [JsonProperty("udi")]
+ [JsonConverter(typeof(UdiJsonConverter))]
+ public Udi Udi { get; set; }
+
+ [JsonIgnore]
+ public Guid Key => Udi != null ? ((GuidUdi)Udi).Guid : throw new InvalidOperationException("No Udi assigned");
+
+ ///
+ /// The remaining properties will be serialized to a dictionary
+ ///
+ ///
+ /// The JsonExtensionDataAttribute is used to put the non-typed properties into a bucket
+ /// http://www.newtonsoft.com/json/help/html/DeserializeExtensionData.htm
+ /// NestedContent serializes to string, int, whatever eg
+ /// "stringValue":"Some String","numericValue":125,"otherNumeric":null
+ ///
+ [JsonExtensionData]
+ public Dictionary RawPropertyValues { get; set; } = new Dictionary();
+
+ ///
+ /// Used during deserialization to convert the raw property data into data with a property type context
+ ///
+ [JsonIgnore]
+ public IDictionary PropertyValues { get; set; } = new Dictionary();
+
+ ///
+ /// Used during deserialization to populate the property value/property type of a block item content property
+ ///
+ public class BlockPropertyValue
+ {
+ public BlockPropertyValue(object value, PropertyType propertyType)
+ {
+ Value = value;
+ PropertyType = propertyType ?? throw new ArgumentNullException(nameof(propertyType));
+ }
+
+ public object Value { get; }
+ public PropertyType PropertyType { get; }
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Models/Blocks/BlockListEditorDataConverter.cs b/src/Umbraco.Core/Models/Blocks/BlockListEditorDataConverter.cs
new file mode 100644
index 0000000000..23f69922d9
--- /dev/null
+++ b/src/Umbraco.Core/Models/Blocks/BlockListEditorDataConverter.cs
@@ -0,0 +1,22 @@
+using Newtonsoft.Json.Linq;
+using System.Linq;
+using System.Collections.Generic;
+
+namespace Umbraco.Core.Models.Blocks
+{
+ ///
+ /// Data converter for the block list property editor
+ ///
+ public class BlockListEditorDataConverter : BlockEditorDataConverter
+ {
+ public BlockListEditorDataConverter() : base(Constants.PropertyEditors.Aliases.BlockList)
+ {
+ }
+
+ protected override IEnumerable GetBlockReferences(JToken jsonLayout)
+ {
+ var blockListLayout = jsonLayout.ToObject>();
+ return blockListLayout.Select(x => new ContentAndSettingsReference(x.ContentUdi, x.SettingsUdi)).ToList();
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Models/Blocks/BlockListItem.cs b/src/Umbraco.Core/Models/Blocks/BlockListItem.cs
new file mode 100644
index 0000000000..620c3d9fe0
--- /dev/null
+++ b/src/Umbraco.Core/Models/Blocks/BlockListItem.cs
@@ -0,0 +1,130 @@
+using System;
+using System.Runtime.Serialization;
+using Umbraco.Core.Models.PublishedContent;
+
+namespace Umbraco.Core.Models.Blocks
+{
+ ///
+ /// Represents a layout item for the Block List editor.
+ ///
+ ///
+ [DataContract(Name = "block", Namespace = "")]
+ public class BlockListItem : IBlockReference
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The content UDI.
+ /// The content.
+ /// The settings UDI.
+ /// The settings.
+ /// contentUdi
+ /// or
+ /// content
+ public BlockListItem(Udi contentUdi, IPublishedElement content, Udi settingsUdi, IPublishedElement settings)
+ {
+ ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi));
+ Content = content ?? throw new ArgumentNullException(nameof(content));
+ SettingsUdi = settingsUdi;
+ Settings = settings;
+ }
+
+ ///
+ /// Gets the content UDI.
+ ///
+ ///
+ /// The content UDI.
+ ///
+ [DataMember(Name = "contentUdi")]
+ public Udi ContentUdi { get; }
+
+ ///
+ /// Gets the content.
+ ///
+ ///
+ /// The content.
+ ///
+ [DataMember(Name = "content")]
+ public IPublishedElement Content { get; }
+
+ ///
+ /// Gets the settings UDI.
+ ///
+ ///
+ /// The settings UDI.
+ ///
+ [DataMember(Name = "settingsUdi")]
+ public Udi SettingsUdi { get; }
+
+ ///
+ /// Gets the settings.
+ ///
+ ///
+ /// The settings.
+ ///
+ [DataMember(Name = "settings")]
+ public IPublishedElement Settings { get; }
+ }
+
+ ///
+ /// Represents a layout item with a generic content type for the Block List editor.
+ ///
+ /// The type of the content.
+ ///
+ public class BlockListItem : BlockListItem
+ where T : IPublishedElement
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The content UDI.
+ /// The content.
+ /// The settings UDI.
+ /// The settings.
+ public BlockListItem(Udi contentUdi, T content, Udi settingsUdi, IPublishedElement settings)
+ : base(contentUdi, content, settingsUdi, settings)
+ {
+ Content = content;
+ }
+
+ ///
+ /// Gets the content.
+ ///
+ ///
+ /// The content.
+ ///
+ public new T Content { get; }
+ }
+
+ ///
+ /// Represents a layout item with generic content and settings types for the Block List editor.
+ ///
+ /// The type of the content.
+ /// The type of the settings.
+ ///
+ public class BlockListItem : BlockListItem
+ where TContent : IPublishedElement
+ where TSettings : IPublishedElement
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The content udi.
+ /// The content.
+ /// The settings udi.
+ /// The settings.
+ public BlockListItem(Udi contentUdi, TContent content, Udi settingsUdi, TSettings settings)
+ : base(contentUdi, content, settingsUdi, settings)
+ {
+ Settings = settings;
+ }
+
+ ///
+ /// Gets the settings.
+ ///
+ ///
+ /// The settings.
+ ///
+ public new TSettings Settings { get; }
+ }
+}
diff --git a/src/Umbraco.Core/Models/Blocks/BlockListLayoutItem.cs b/src/Umbraco.Core/Models/Blocks/BlockListLayoutItem.cs
new file mode 100644
index 0000000000..5de44e16c1
--- /dev/null
+++ b/src/Umbraco.Core/Models/Blocks/BlockListLayoutItem.cs
@@ -0,0 +1,19 @@
+using Newtonsoft.Json;
+using Umbraco.Core.Serialization;
+
+namespace Umbraco.Core.Models.Blocks
+{
+ ///
+ /// Used for deserializing the block list layout
+ ///
+ public class BlockListLayoutItem
+ {
+ [JsonProperty("contentUdi", Required = Required.Always)]
+ [JsonConverter(typeof(UdiJsonConverter))]
+ public Udi ContentUdi { get; set; }
+
+ [JsonProperty("settingsUdi", NullValueHandling = NullValueHandling.Ignore)]
+ [JsonConverter(typeof(UdiJsonConverter))]
+ public Udi SettingsUdi { get; set; }
+ }
+}
diff --git a/src/Umbraco.Core/Models/Blocks/BlockListModel.cs b/src/Umbraco.Core/Models/Blocks/BlockListModel.cs
new file mode 100644
index 0000000000..9a3a26ab30
--- /dev/null
+++ b/src/Umbraco.Core/Models/Blocks/BlockListModel.cs
@@ -0,0 +1,63 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Runtime.Serialization;
+
+namespace Umbraco.Core.Models.Blocks
+{
+ ///
+ /// The strongly typed model for the Block List editor.
+ ///
+ ///
+ [DataContract(Name = "blockList", Namespace = "")]
+ public class BlockListModel : ReadOnlyCollection
+ {
+ ///
+ /// Gets the empty .
+ ///
+ ///
+ /// The empty .
+ ///
+ public static BlockListModel Empty { get; } = new BlockListModel();
+
+ ///
+ /// Prevents a default instance of the class from being created.
+ ///
+ private BlockListModel()
+ : this(new List())
+ { }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The list to wrap.
+ public BlockListModel(IList list)
+ : base(list)
+ { }
+
+ ///
+ /// Gets the with the specified content key.
+ ///
+ ///
+ /// The .
+ ///
+ /// The content key.
+ ///
+ /// The with the specified content key.
+ ///
+ public BlockListItem this[Guid contentKey] => this.FirstOrDefault(x => x.Content.Key == contentKey);
+
+ ///
+ /// Gets the with the specified content UDI.
+ ///
+ ///
+ /// The .
+ ///
+ /// The content UDI.
+ ///
+ /// The with the specified content UDI.
+ ///
+ public BlockListItem this[Udi contentUdi] => contentUdi is GuidUdi guidUdi ? this.FirstOrDefault(x => x.Content.Key == guidUdi.Guid) : null;
+ }
+}
diff --git a/src/Umbraco.Core/Models/Blocks/BlockValue.cs b/src/Umbraco.Core/Models/Blocks/BlockValue.cs
new file mode 100644
index 0000000000..4700ddfd3b
--- /dev/null
+++ b/src/Umbraco.Core/Models/Blocks/BlockValue.cs
@@ -0,0 +1,18 @@
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+using System.Collections.Generic;
+
+namespace Umbraco.Core.Models.Blocks
+{
+ public class BlockValue
+ {
+ [JsonProperty("layout")]
+ public IDictionary Layout { get; set; }
+
+ [JsonProperty("contentData")]
+ public List ContentData { get; set; } = new List();
+
+ [JsonProperty("settingsData")]
+ public List SettingsData { get; set; } = new List();
+ }
+}
diff --git a/src/Umbraco.Core/Models/Blocks/ContentAndSettingsReference.cs b/src/Umbraco.Core/Models/Blocks/ContentAndSettingsReference.cs
new file mode 100644
index 0000000000..f7222fe140
--- /dev/null
+++ b/src/Umbraco.Core/Models/Blocks/ContentAndSettingsReference.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Collections.Generic;
+
+namespace Umbraco.Core.Models.Blocks
+{
+ public struct ContentAndSettingsReference : IEquatable
+ {
+ public ContentAndSettingsReference(Udi contentUdi, Udi settingsUdi)
+ {
+ ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi));
+ SettingsUdi = settingsUdi;
+ }
+
+ public Udi ContentUdi { get; }
+
+ public Udi SettingsUdi { get; }
+
+ public override bool Equals(object obj) => obj is ContentAndSettingsReference reference && Equals(reference);
+
+ public bool Equals(ContentAndSettingsReference other) => other != null
+ && EqualityComparer.Default.Equals(ContentUdi, other.ContentUdi)
+ && EqualityComparer.Default.Equals(SettingsUdi, other.SettingsUdi);
+
+ public override int GetHashCode() => (ContentUdi, SettingsUdi).GetHashCode();
+
+ public static bool operator ==(ContentAndSettingsReference left, ContentAndSettingsReference right)
+ {
+ return left.Equals(right);
+ }
+
+ public static bool operator !=(ContentAndSettingsReference left, ContentAndSettingsReference right)
+ {
+ return !(left == right);
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Models/Blocks/IBlockReference.cs b/src/Umbraco.Core/Models/Blocks/IBlockReference.cs
new file mode 100644
index 0000000000..7f5c835b3c
--- /dev/null
+++ b/src/Umbraco.Core/Models/Blocks/IBlockReference.cs
@@ -0,0 +1,37 @@
+namespace Umbraco.Core.Models.Blocks
+{
+ ///
+ /// Represents a data item reference for a Block Editor implementation.
+ ///
+ ///
+ /// See: https://github.com/umbraco/rfcs/blob/907f3758cf59a7b6781296a60d57d537b3b60b8c/cms/0011-block-data-structure.md#strongly-typed
+ ///
+ public interface IBlockReference
+ {
+ ///
+ /// Gets the content UDI.
+ ///
+ ///
+ /// The content UDI.
+ ///
+ Udi ContentUdi { get; }
+ }
+
+ ///
+ /// Represents a data item reference with settings for a Block editor implementation.
+ ///
+ /// The type of the settings.
+ ///
+ /// See: https://github.com/umbraco/rfcs/blob/907f3758cf59a7b6781296a60d57d537b3b60b8c/cms/0011-block-data-structure.md#strongly-typed
+ ///
+ public interface IBlockReference : IBlockReference
+ {
+ ///
+ /// Gets the settings.
+ ///
+ ///
+ /// The settings.
+ ///
+ TSettings Settings { get; }
+ }
+}
diff --git a/src/Umbraco.Core/Models/Content.cs b/src/Umbraco.Core/Models/Content.cs
index c5930bf998..04f49e704e 100644
--- a/src/Umbraco.Core/Models/Content.cs
+++ b/src/Umbraco.Core/Models/Content.cs
@@ -98,10 +98,15 @@ namespace Umbraco.Core.Models
set
{
if (_schedule != null)
- _schedule.CollectionChanged -= ScheduleCollectionChanged;
+ {
+ _schedule.ClearCollectionChangedEvents();
+ }
+
SetPropertyValueAndDetectChanges(value, ref _schedule, nameof(ContentSchedule));
if (_schedule != null)
+ {
_schedule.CollectionChanged += ScheduleCollectionChanged;
+ }
}
}
@@ -223,10 +228,16 @@ namespace Umbraco.Core.Models
}
set
{
- if (_publishInfos != null) _publishInfos.CollectionChanged -= PublishNamesCollectionChanged;
+ if (_publishInfos != null)
+ {
+ _publishInfos.ClearCollectionChangedEvents();
+ }
+
_publishInfos = value;
if (_publishInfos != null)
+ {
_publishInfos.CollectionChanged += PublishNamesCollectionChanged;
+ }
}
}
@@ -321,7 +332,7 @@ namespace Umbraco.Core.Models
else
Properties.EnsurePropertyTypes(contentType.CompositionPropertyTypes);
- Properties.CollectionChanged -= PropertiesChanged; // be sure not to double add
+ Properties.ClearCollectionChangedEvents(); // be sure not to double add
Properties.CollectionChanged += PropertiesChanged;
}
@@ -438,7 +449,7 @@ namespace Umbraco.Core.Models
//if culture infos exist then deal with event bindings
if (clonedContent._publishInfos != null)
{
- clonedContent._publishInfos.CollectionChanged -= PublishNamesCollectionChanged; //clear this event handler if any
+ clonedContent._publishInfos.ClearCollectionChangedEvents(); //clear this event handler if any
clonedContent._publishInfos = (ContentCultureInfosCollection)_publishInfos.DeepClone(); //manually deep clone
clonedContent._publishInfos.CollectionChanged += clonedContent.PublishNamesCollectionChanged; //re-assign correct event handler
}
@@ -446,7 +457,7 @@ namespace Umbraco.Core.Models
//if properties exist then deal with event bindings
if (clonedContent._schedule != null)
{
- clonedContent._schedule.CollectionChanged -= ScheduleCollectionChanged; //clear this event handler if any
+ clonedContent._schedule.ClearCollectionChangedEvents(); //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 d02ea82012..cbdee479fe 100644
--- a/src/Umbraco.Core/Models/ContentBase.cs
+++ b/src/Umbraco.Core/Models/ContentBase.cs
@@ -138,7 +138,11 @@ namespace Umbraco.Core.Models
get => _properties;
set
{
- if (_properties != null) _properties.CollectionChanged -= PropertiesChanged;
+ if (_properties != null)
+ {
+ _properties.ClearCollectionChangedEvents();
+ }
+
_properties = value;
_properties.CollectionChanged += PropertiesChanged;
}
@@ -173,10 +177,15 @@ namespace Umbraco.Core.Models
}
set
{
- if (_cultureInfos != null) _cultureInfos.CollectionChanged -= CultureInfosCollectionChanged;
+ if (_cultureInfos != null)
+ {
+ _cultureInfos.ClearCollectionChangedEvents();
+ }
_cultureInfos = value;
if (_cultureInfos != null)
+ {
_cultureInfos.CollectionChanged += CultureInfosCollectionChanged;
+ }
}
}
@@ -479,7 +488,7 @@ namespace Umbraco.Core.Models
//if culture infos exist then deal with event bindings
if (clonedContent._cultureInfos != null)
{
- clonedContent._cultureInfos.CollectionChanged -= CultureInfosCollectionChanged; //clear this event handler if any
+ clonedContent._cultureInfos.ClearCollectionChangedEvents(); //clear this event handler if any
clonedContent._cultureInfos = (ContentCultureInfosCollection)_cultureInfos.DeepClone(); //manually deep clone
clonedContent._cultureInfos.CollectionChanged += clonedContent.CultureInfosCollectionChanged; //re-assign correct event handler
}
@@ -487,7 +496,7 @@ namespace Umbraco.Core.Models
//if properties exist then deal with event bindings
if (clonedContent._properties != null)
{
- clonedContent._properties.CollectionChanged -= PropertiesChanged; //clear this event handler if any
+ clonedContent._properties.ClearCollectionChangedEvents(); //clear this event handler if any
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/ContentCultureInfosCollection.cs b/src/Umbraco.Core/Models/ContentCultureInfosCollection.cs
index 9bc78fc56d..07ee661497 100644
--- a/src/Umbraco.Core/Models/ContentCultureInfosCollection.cs
+++ b/src/Umbraco.Core/Models/ContentCultureInfosCollection.cs
@@ -15,7 +15,7 @@ namespace Umbraco.Core.Models
public ContentCultureInfosCollection()
: base(x => x.Culture, StringComparer.InvariantCultureIgnoreCase)
{ }
-
+
///
/// Adds or updates a instance.
///
diff --git a/src/Umbraco.Core/Models/ContentScheduleCollection.cs b/src/Umbraco.Core/Models/ContentScheduleCollection.cs
index 6c7dd79312..0ebcc0fe4b 100644
--- a/src/Umbraco.Core/Models/ContentScheduleCollection.cs
+++ b/src/Umbraco.Core/Models/ContentScheduleCollection.cs
@@ -14,6 +14,11 @@ namespace Umbraco.Core.Models
public event NotifyCollectionChangedEventHandler CollectionChanged;
+ ///
+ /// Clears all event handlers
+ ///
+ public void ClearCollectionChangedEvents() => CollectionChanged = null;
+
private void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
{
CollectionChanged?.Invoke(this, args);
diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs
index 04bcb7424a..4865db428a 100644
--- a/src/Umbraco.Core/Models/ContentTypeBase.cs
+++ b/src/Umbraco.Core/Models/ContentTypeBase.cs
@@ -262,7 +262,10 @@ namespace Umbraco.Core.Models
set
{
if (_noGroupPropertyTypes != null)
- _noGroupPropertyTypes.CollectionChanged -= PropertyTypesChanged;
+ {
+ _noGroupPropertyTypes.ClearCollectionChangedEvents();
+ }
+
_noGroupPropertyTypes = new PropertyTypeCollection(SupportsPublishing, value);
_noGroupPropertyTypes.CollectionChanged += PropertyTypesChanged;
PropertyTypesChanged(_noGroupPropertyTypes, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
@@ -480,14 +483,14 @@ namespace Umbraco.Core.Models
// its ignored from the auto-clone process because its return values are unions, not raw and
// we end up with duplicates, see: http://issues.umbraco.org/issue/U4-4842
- clonedEntity._noGroupPropertyTypes.CollectionChanged -= PropertyTypesChanged; //clear this event handler if any
+ clonedEntity._noGroupPropertyTypes.ClearCollectionChangedEvents(); //clear this event handler if any
clonedEntity._noGroupPropertyTypes = (PropertyTypeCollection) _noGroupPropertyTypes.DeepClone(); //manually deep clone
clonedEntity._noGroupPropertyTypes.CollectionChanged += clonedEntity.PropertyTypesChanged; //re-assign correct event handler
}
if (clonedEntity._propertyGroups != null)
{
- clonedEntity._propertyGroups.CollectionChanged -= PropertyGroupsChanged; //clear this event handler if any
+ clonedEntity._propertyGroups.ClearCollectionChangedEvents(); //clear this event handler if any
clonedEntity._propertyGroups = (PropertyGroupCollection) _propertyGroups.DeepClone(); //manually deep clone
clonedEntity._propertyGroups.CollectionChanged += clonedEntity.PropertyGroupsChanged; //re-assign correct event handler
}
diff --git a/src/Umbraco.Core/Models/Entities/IMediaEntitySlim.cs b/src/Umbraco.Core/Models/Entities/IMediaEntitySlim.cs
index 9440000146..f7daf79ec9 100644
--- a/src/Umbraco.Core/Models/Entities/IMediaEntitySlim.cs
+++ b/src/Umbraco.Core/Models/Entities/IMediaEntitySlim.cs
@@ -7,7 +7,7 @@
{
///
- /// The media file's path/url
+ /// The media file's path/URL
///
string MediaPath { get; }
}
diff --git a/src/Umbraco.Core/Models/IContentBase.cs b/src/Umbraco.Core/Models/IContentBase.cs
index 0f660181fb..f4fae44ffc 100644
--- a/src/Umbraco.Core/Models/IContentBase.cs
+++ b/src/Umbraco.Core/Models/IContentBase.cs
@@ -4,6 +4,7 @@ using Umbraco.Core.Models.Entities;
namespace Umbraco.Core.Models
{
+
///
/// Provides a base class for content items.
///
diff --git a/src/Umbraco.Core/Models/IReadOnlyContentBase.cs b/src/Umbraco.Core/Models/IReadOnlyContentBase.cs
new file mode 100644
index 0000000000..3ed39b63ab
--- /dev/null
+++ b/src/Umbraco.Core/Models/IReadOnlyContentBase.cs
@@ -0,0 +1,72 @@
+using System;
+
+namespace Umbraco.Core.Models
+{
+ public interface IReadOnlyContentBase
+ {
+ ///
+ /// Gets the integer identifier of the entity.
+ ///
+ int Id { get; }
+
+ ///
+ /// Gets the Guid unique identifier of the entity.
+ ///
+ Guid Key { get; }
+
+ ///
+ /// Gets the creation date.
+ ///
+ DateTime CreateDate { get; }
+
+ ///
+ /// Gets the last update date.
+ ///
+ DateTime UpdateDate { get; }
+
+ ///
+ /// Gets the name of the entity.
+ ///
+ string Name { get; }
+
+ ///
+ /// Gets the identifier of the user who created this entity.
+ ///
+ int CreatorId { get; }
+
+ ///
+ /// Gets the identifier of the parent entity.
+ ///
+ int ParentId { get; }
+
+ ///
+ /// Gets the level of the entity.
+ ///
+ int Level { get; }
+
+ ///
+ /// Gets the path to the entity.
+ ///
+ string Path { get; }
+
+ ///
+ /// Gets the sort order of the entity.
+ ///
+ int SortOrder { get; }
+
+ ///
+ /// Gets the content type id
+ ///
+ int ContentTypeId { get; }
+
+ ///
+ /// Gets the identifier of the writer.
+ ///
+ int WriterId { get; }
+
+ ///
+ /// Gets the version identifier.
+ ///
+ int VersionId { get; }
+ }
+}
diff --git a/src/Umbraco.Core/Models/IRedirectUrl.cs b/src/Umbraco.Core/Models/IRedirectUrl.cs
index e066881645..527dad57da 100644
--- a/src/Umbraco.Core/Models/IRedirectUrl.cs
+++ b/src/Umbraco.Core/Models/IRedirectUrl.cs
@@ -5,7 +5,7 @@ using Umbraco.Core.Models.Entities;
namespace Umbraco.Core.Models
{
///
- /// Represents a redirect url.
+ /// Represents a redirect URL.
///
public interface IRedirectUrl : IEntity, IRememberBeingDirty
{
@@ -22,7 +22,7 @@ namespace Umbraco.Core.Models
Guid ContentKey { get; set; }
///
- /// Gets or sets the redirect url creation date.
+ /// Gets or sets the redirect URL creation date.
///
[DataMember]
DateTime CreateDateUtc { get; set; }
@@ -34,7 +34,7 @@ namespace Umbraco.Core.Models
string Culture { get; set; }
///
- /// Gets or sets the redirect url route.
+ /// Gets or sets the redirect URL route.
///
/// Is a proper Umbraco route eg /path/to/foo or 123/path/tofoo.
[DataMember]
diff --git a/src/Umbraco.Core/Models/IconModel.cs b/src/Umbraco.Core/Models/IconModel.cs
new file mode 100644
index 0000000000..0de44301bb
--- /dev/null
+++ b/src/Umbraco.Core/Models/IconModel.cs
@@ -0,0 +1,8 @@
+namespace Umbraco.Core.Models
+{
+ public class IconModel
+ {
+ public string Name { get; set; }
+ public string SvgString { get; set; }
+ }
+}
diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs
index 8dc056d555..d2899caccc 100644
--- a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs
+++ b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
+using Umbraco.Core.Collections;
using Umbraco.Core.Composing;
using Umbraco.Core.Models.Entities;
using Umbraco.Core.Models.Membership;
@@ -62,7 +63,7 @@ namespace Umbraco.Core.Models.Identity
_culture = Current.Configs.Global().DefaultUILanguage; // TODO: inject
// must initialize before setting groups
- _roles = new ObservableCollection>();
+ _roles = new EventClearingObservableCollection>();
_roles.CollectionChanged += _roles_CollectionChanged;
// use the property setters - they do more than just setting a field
@@ -223,7 +224,7 @@ namespace Umbraco.Core.Models.Identity
_groups = value;
//now clear all roles and re-add them
- _roles.CollectionChanged -= _roles_CollectionChanged;
+ _roles.ClearCollectionChangedEvents();
_roles.Clear();
foreach (var identityUserRole in _groups.Select(x => new IdentityUserRole
{
@@ -275,16 +276,23 @@ namespace Umbraco.Core.Models.Identity
{
get
{
- if (_getLogins != null && _getLogins.IsValueCreated == false)
+ // return if it exists
+ if (_logins != null) return _logins;
+
+ _logins = new ObservableCollection();
+
+ // if the callback is there and hasn't been created yet then execute it and populate the logins
+ if (_getLogins != null && !_getLogins.IsValueCreated)
{
- _logins = new ObservableCollection();
foreach (var l in _getLogins.Value)
{
_logins.Add(l);
}
- //now assign events
- _logins.CollectionChanged += Logins_CollectionChanged;
}
+
+ //now assign events
+ _logins.CollectionChanged += Logins_CollectionChanged;
+
return _logins;
}
}
@@ -299,7 +307,7 @@ namespace Umbraco.Core.Models.Identity
_beingDirty.OnPropertyChanged(nameof(Roles));
}
- private readonly ObservableCollection> _roles;
+ private readonly EventClearingObservableCollection> _roles;
///
/// helper method to easily add a role without having to deal with IdentityUserRole{T}
diff --git a/src/Umbraco.Core/Models/Identity/ExternalLogin.cs b/src/Umbraco.Core/Models/Identity/ExternalLogin.cs
new file mode 100644
index 0000000000..6e4abf2906
--- /dev/null
+++ b/src/Umbraco.Core/Models/Identity/ExternalLogin.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace Umbraco.Core.Models.Identity
+{
+ ///
+ public class ExternalLogin : IExternalLogin
+ {
+ public ExternalLogin(string loginProvider, string providerKey, string userData = null)
+ {
+ LoginProvider = loginProvider ?? throw new ArgumentNullException(nameof(loginProvider));
+ ProviderKey = providerKey ?? throw new ArgumentNullException(nameof(providerKey));
+ UserData = userData;
+ }
+
+ ///
+ public string LoginProvider { get; }
+
+ ///
+ public string ProviderKey { get; }
+
+ ///
+ public string UserData { get; }
+ }
+}
diff --git a/src/Umbraco.Core/Models/Identity/IExternalLogin.cs b/src/Umbraco.Core/Models/Identity/IExternalLogin.cs
new file mode 100644
index 0000000000..68f66a5cee
--- /dev/null
+++ b/src/Umbraco.Core/Models/Identity/IExternalLogin.cs
@@ -0,0 +1,12 @@
+namespace Umbraco.Core.Models.Identity
+{
+ ///
+ /// Used to persist external login data for a user
+ ///
+ public interface IExternalLogin
+ {
+ string LoginProvider { get; }
+ string ProviderKey { get; }
+ string UserData { get; }
+ }
+}
diff --git a/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs b/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs
index 276f601771..feb8af24f3 100644
--- a/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs
+++ b/src/Umbraco.Core/Models/Identity/IIdentityUserLogin.cs
@@ -2,23 +2,30 @@
namespace Umbraco.Core.Models.Identity
{
+ // TODO: Merge these in v8! This is here purely for backward compat
+
+ public interface IIdentityUserLoginExtended : IIdentityUserLogin
+ {
+ ///
+ /// Used to store any arbitrary data for the user and external provider - like user tokens returned from the provider
+ ///
+ string UserData { get; set; }
+ }
+
public interface IIdentityUserLogin : IEntity, IRememberBeingDirty
{
///
/// The login provider for the login (i.e. Facebook, Google)
- ///
///
string LoginProvider { get; set; }
///
/// Key representing the login for the provider
- ///
///
string ProviderKey { get; set; }
///
/// User Id for the user who owns this login
- ///
///
int UserId { get; set; }
}
diff --git a/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs b/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs
index 5876f420b4..66911b08ac 100644
--- a/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs
+++ b/src/Umbraco.Core/Models/Identity/IdentityUserLogin.cs
@@ -3,11 +3,11 @@ using Umbraco.Core.Models.Entities;
namespace Umbraco.Core.Models.Identity
{
+
///
/// Entity type for a user's login (i.e. Facebook, Google)
- ///
///
- public class IdentityUserLogin : EntityBase, IIdentityUserLogin
+ public class IdentityUserLogin : EntityBase, IIdentityUserLoginExtended
{
public IdentityUserLogin(string loginProvider, string providerKey, int userId)
{
@@ -25,22 +25,16 @@ namespace Umbraco.Core.Models.Identity
CreateDate = createDate;
}
- ///
- /// The login provider for the login (i.e. Facebook, Google)
- ///
- ///
+ ///
public string LoginProvider { get; set; }
- ///
- /// Key representing the login for the provider
- ///
- ///
+ ///
public string ProviderKey { get; set; }
- ///
- /// User Id for the user who owns this login
- ///
- ///
+ ///
public int UserId { get; set; }
+
+ ///
+ public string UserData { get; set; }
}
}
diff --git a/src/Umbraco.Core/Models/Media.cs b/src/Umbraco.Core/Models/Media.cs
index 002611c09c..87ec9196d6 100644
--- a/src/Umbraco.Core/Models/Media.cs
+++ b/src/Umbraco.Core/Models/Media.cs
@@ -77,7 +77,7 @@ namespace Umbraco.Core.Models
else
Properties.EnsurePropertyTypes(contentType.CompositionPropertyTypes);
- Properties.CollectionChanged -= PropertiesChanged; // be sure not to double add
+ Properties.ClearCollectionChangedEvents(); // be sure not to double add
Properties.CollectionChanged += PropertiesChanged;
}
}
diff --git a/src/Umbraco.Core/Models/MediaExtensions.cs b/src/Umbraco.Core/Models/MediaExtensions.cs
index 96f183b6e6..c985ace8b0 100644
--- a/src/Umbraco.Core/Models/MediaExtensions.cs
+++ b/src/Umbraco.Core/Models/MediaExtensions.cs
@@ -9,7 +9,7 @@ namespace Umbraco.Core.Models
public static class MediaExtensions
{
///
- /// Gets the url of a media item.
+ /// Gets the URL of a media item.
///
public static string GetUrl(this IMedia media, string propertyAlias, ILogger logger)
{
@@ -29,7 +29,7 @@ namespace Umbraco.Core.Models
}
///
- /// Gets the urls of a media item.
+ /// Gets the URLs of a media item.
///
public static string[] GetUrls(this IMedia media, IContentSection contentSection, ILogger logger)
{
diff --git a/src/Umbraco.Core/Models/PathValidationExtensions.cs b/src/Umbraco.Core/Models/PathValidationExtensions.cs
index 2db954b316..4ade88f621 100644
--- a/src/Umbraco.Core/Models/PathValidationExtensions.cs
+++ b/src/Umbraco.Core/Models/PathValidationExtensions.cs
@@ -25,7 +25,7 @@ namespace Umbraco.Core.Models
if (entity.Path.IsNullOrWhiteSpace())
throw new InvalidDataException($"The content item {entity.NodeId} has an empty path: {entity.Path} with parentID: {entity.ParentId}");
- var pathParts = entity.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
+ var pathParts = entity.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries);
if (pathParts.Length < 2)
{
//a path cannot be less than 2 parts, at a minimum it must be root (-1) and it's own id
@@ -53,7 +53,7 @@ namespace Umbraco.Core.Models
if (entity.Path.IsNullOrWhiteSpace())
return false;
- var pathParts = entity.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
+ var pathParts = entity.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries);
if (pathParts.Length < 2)
{
//a path cannot be less than 2 parts, at a minimum it must be root (-1) and it's own id
diff --git a/src/Umbraco.Core/Models/PropertyCollection.cs b/src/Umbraco.Core/Models/PropertyCollection.cs
index c587a45424..a73df6a095 100644
--- a/src/Umbraco.Core/Models/PropertyCollection.cs
+++ b/src/Umbraco.Core/Models/PropertyCollection.cs
@@ -7,6 +7,7 @@ using System.Runtime.Serialization;
namespace Umbraco.Core.Models
{
+
///
/// Represents a collection of property values.
///
@@ -168,6 +169,8 @@ namespace Umbraco.Core.Models
///
public event NotifyCollectionChangedEventHandler CollectionChanged;
+ public void ClearCollectionChangedEvents() => CollectionChanged = null;
+
protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
{
CollectionChanged?.Invoke(this, args);
diff --git a/src/Umbraco.Core/Models/PropertyGroup.cs b/src/Umbraco.Core/Models/PropertyGroup.cs
index 2e6da5d837..022fb6ed03 100644
--- a/src/Umbraco.Core/Models/PropertyGroup.cs
+++ b/src/Umbraco.Core/Models/PropertyGroup.cs
@@ -66,7 +66,10 @@ namespace Umbraco.Core.Models
set
{
if (_propertyTypes != null)
- _propertyTypes.CollectionChanged -= PropertyTypesChanged;
+ {
+ _propertyTypes.ClearCollectionChangedEvents();
+ }
+
_propertyTypes = value;
// since we're adding this collection to this group,
@@ -100,7 +103,7 @@ namespace Umbraco.Core.Models
if (clonedEntity._propertyTypes != null)
{
- clonedEntity._propertyTypes.CollectionChanged -= PropertyTypesChanged; //clear this event handler if any
+ clonedEntity._propertyTypes.ClearCollectionChangedEvents(); //clear this event handler if any
clonedEntity._propertyTypes = (PropertyTypeCollection) _propertyTypes.DeepClone(); //manually deep clone
clonedEntity._propertyTypes.CollectionChanged += clonedEntity.PropertyTypesChanged; //re-assign correct event handler
}
diff --git a/src/Umbraco.Core/Models/PropertyGroupCollection.cs b/src/Umbraco.Core/Models/PropertyGroupCollection.cs
index 5422dfb792..973147b3fb 100644
--- a/src/Umbraco.Core/Models/PropertyGroupCollection.cs
+++ b/src/Umbraco.Core/Models/PropertyGroupCollection.cs
@@ -160,6 +160,11 @@ namespace Umbraco.Core.Models
public event NotifyCollectionChangedEventHandler CollectionChanged;
+ ///
+ /// Clears all event handlers
+ ///
+ public void ClearCollectionChangedEvents() => CollectionChanged = null;
+
protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
{
CollectionChanged?.Invoke(this, args);
diff --git a/src/Umbraco.Core/Models/PropertyType.cs b/src/Umbraco.Core/Models/PropertyType.cs
index 75c2a7cc00..abdbbfd8db 100644
--- a/src/Umbraco.Core/Models/PropertyType.cs
+++ b/src/Umbraco.Core/Models/PropertyType.cs
@@ -31,6 +31,7 @@ namespace Umbraco.Core.Models
private string _validationRegExp;
private string _validationRegExpMessage;
private ContentVariation _variations;
+ private bool _labelOnTop;
///
/// Initializes a new instance of the class.
@@ -205,6 +206,16 @@ namespace Umbraco.Core.Models
set => SetPropertyValueAndDetectChanges(value, ref _mandatoryMessage, nameof(MandatoryMessage));
}
+ ///
+ /// Gets or sets a value indicating whether the label of this property type should be displayed on top.
+ ///
+ [DataMember]
+ public bool LabelOnTop
+ {
+ get => _labelOnTop;
+ set => SetPropertyValueAndDetectChanges(value, ref _labelOnTop, nameof(LabelOnTop));
+ }
+
///
/// Gets of sets the sort order of the property type.
///
@@ -438,7 +449,7 @@ namespace Umbraco.Core.Models
base.PerformDeepClone(clone);
var clonedEntity = (PropertyType)clone;
-
+
//need to manually assign the Lazy value as it will not be automatically mapped
if (PropertyGroupId != null)
{
diff --git a/src/Umbraco.Core/Models/PropertyTypeCollection.cs b/src/Umbraco.Core/Models/PropertyTypeCollection.cs
index 6e41f0d12b..cc9d00fa5d 100644
--- a/src/Umbraco.Core/Models/PropertyTypeCollection.cs
+++ b/src/Umbraco.Core/Models/PropertyTypeCollection.cs
@@ -162,7 +162,11 @@ namespace Umbraco.Core.Models
}
public event NotifyCollectionChangedEventHandler CollectionChanged;
-
+
+ ///
+ /// Clears all event handlers
+ ///
+ public void ClearCollectionChangedEvents() => CollectionChanged = null;
protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
{
CollectionChanged?.Invoke(this, args);
diff --git a/src/Umbraco.Core/Models/PublicAccessEntry.cs b/src/Umbraco.Core/Models/PublicAccessEntry.cs
index cfb30de147..cc86b797cb 100644
--- a/src/Umbraco.Core/Models/PublicAccessEntry.cs
+++ b/src/Umbraco.Core/Models/PublicAccessEntry.cs
@@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Runtime.Serialization;
+using Umbraco.Core.Collections;
using Umbraco.Core.Models.Entities;
namespace Umbraco.Core.Models
@@ -12,7 +13,7 @@ namespace Umbraco.Core.Models
[DataContract(IsReference = true)]
public class PublicAccessEntry : EntityBase
{
- private readonly ObservableCollection _ruleCollection;
+ private readonly EventClearingObservableCollection _ruleCollection;
private int _protectedNodeId;
private int _noAccessNodeId;
private int _loginNodeId;
@@ -28,7 +29,7 @@ namespace Umbraco.Core.Models
NoAccessNodeId = noAccessNode.Id;
_protectedNodeId = protectedNode.Id;
- _ruleCollection = new ObservableCollection(ruleCollection);
+ _ruleCollection = new EventClearingObservableCollection(ruleCollection);
_ruleCollection.CollectionChanged += _ruleCollection_CollectionChanged;
foreach (var rule in _ruleCollection)
@@ -44,7 +45,7 @@ namespace Umbraco.Core.Models
NoAccessNodeId = noAccessNodeId;
_protectedNodeId = protectedNodeId;
- _ruleCollection = new ObservableCollection(ruleCollection);
+ _ruleCollection = new EventClearingObservableCollection(ruleCollection);
_ruleCollection.CollectionChanged += _ruleCollection_CollectionChanged;
foreach (var rule in _ruleCollection)
@@ -148,7 +149,7 @@ namespace Umbraco.Core.Models
if (cloneEntity._ruleCollection != null)
{
- cloneEntity._ruleCollection.CollectionChanged -= _ruleCollection_CollectionChanged; //clear this event handler if any
+ cloneEntity._ruleCollection.ClearCollectionChangedEvents(); //clear this event handler if any
cloneEntity._ruleCollection.CollectionChanged += cloneEntity._ruleCollection_CollectionChanged; //re-assign correct event handler
}
}
diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs
index 4e154bf514..23861dfa9d 100644
--- a/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContent.cs
@@ -31,7 +31,7 @@ namespace Umbraco.Core.Models.PublishedContent
string Name { get; }
///
- /// Gets the url segment of the content item for the current culture.
+ /// Gets the URL segment of the content item for the current culture.
///
string UrlSegment { get; }
@@ -93,7 +93,7 @@ namespace Umbraco.Core.Models.PublishedContent
DateTime UpdateDate { get; }
///
- /// Gets the url of the content item for the current culture.
+ /// Gets the URL of the content item for the current culture.
///
///
/// The value of this property is contextual. It depends on the 'current' request uri,
diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentType.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentType.cs
index ab6920377c..cfc789324a 100644
--- a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentType.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentType.cs
@@ -1,7 +1,21 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
namespace Umbraco.Core.Models.PublishedContent
{
+ ///
+ /// Represents an type.
+ ///
+ /// Instances implementing the interface should be
+ /// immutable, ie if the content type changes, then a new instance needs to be created.
+ public interface IPublishedContentType2 : IPublishedContentType
+ {
+ ///
+ /// Gets the unique key for the content type.
+ ///
+ Guid Key { get; }
+ }
+
///
/// Represents an type.
///
diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs
index 89009ac7b8..1a29f970d9 100644
--- a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs
@@ -1,5 +1,6 @@
namespace Umbraco. Core.Models.PublishedContent
{
+
///
/// Creates published content types.
///
diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedModelFactory.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedModelFactory.cs
index ae4caf352e..60fa0fe603 100644
--- a/src/Umbraco.Core/Models/PublishedContent/IPublishedModelFactory.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedModelFactory.cs
@@ -3,6 +3,7 @@ using System.Collections;
namespace Umbraco.Core.Models.PublishedContent
{
+
///
/// Provides the published model creation service.
///
diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtensionsForModels.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtensionsForModels.cs
index bfc65b70d6..033396e4a1 100644
--- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtensionsForModels.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtensionsForModels.cs
@@ -3,6 +3,7 @@ using Umbraco.Core.Composing;
namespace Umbraco.Core.Models.PublishedContent
{
+
///
/// Provides strongly typed published content models services.
///
diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs
index 458b63ade3..7aa9b0dfd9 100644
--- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs
@@ -9,7 +9,7 @@ namespace Umbraco.Core.Models.PublishedContent
///
/// Instances of the class are immutable, ie
/// if the content type changes, then a new class needs to be created.
- public class PublishedContentType : IPublishedContentType
+ public class PublishedContentType : IPublishedContentType2
{
private readonly IPublishedPropertyType[] _propertyTypes;
@@ -20,7 +20,7 @@ namespace Umbraco.Core.Models.PublishedContent
/// Initializes a new instance of the class with a content type.
///
public PublishedContentType(IContentTypeComposition contentType, IPublishedContentTypeFactory factory)
- : this(contentType.Id, contentType.Alias, contentType.GetItemType(), contentType.CompositionAliases(), contentType.Variations, contentType.IsElement)
+ : this(contentType.Key, contentType.Id, contentType.Alias, contentType.GetItemType(), contentType.CompositionAliases(), contentType.Variations, contentType.IsElement)
{
var propertyTypes = contentType.CompositionPropertyTypes
.Select(x => factory.CreatePropertyType(this, x))
@@ -40,8 +40,20 @@ namespace Umbraco.Core.Models.PublishedContent
///
/// Values are assumed to be consistent and are not checked.
///
+ public PublishedContentType(Guid key, int id, string alias, PublishedItemType itemType, IEnumerable compositionAliases, IEnumerable propertyTypes, ContentVariation variations, bool isElement = false)
+ : this(key, id, alias, itemType, compositionAliases, variations, isElement)
+ {
+ var propertyTypesA = propertyTypes.ToArray();
+ foreach (var propertyType in propertyTypesA)
+ propertyType.ContentType = this;
+ _propertyTypes = propertyTypesA;
+
+ InitializeIndexes();
+ }
+
+ [Obsolete("Use the overload specifying a key instead")]
public PublishedContentType(int id, string alias, PublishedItemType itemType, IEnumerable compositionAliases, IEnumerable propertyTypes, ContentVariation variations, bool isElement = false)
- : this (id, alias, itemType, compositionAliases, variations, isElement)
+ : this (Guid.Empty, id, alias, itemType, compositionAliases, variations, isElement)
{
var propertyTypesA = propertyTypes.ToArray();
foreach (var propertyType in propertyTypesA)
@@ -57,16 +69,26 @@ namespace Umbraco.Core.Models.PublishedContent
///
/// Values are assumed to be consistent and are not checked.
///
+ public PublishedContentType(Guid key, int id, string alias, PublishedItemType itemType, IEnumerable compositionAliases, Func> propertyTypes, ContentVariation variations, bool isElement = false)
+ : this(key, id, alias, itemType, compositionAliases, variations, isElement)
+ {
+ _propertyTypes = propertyTypes(this).ToArray();
+
+ InitializeIndexes();
+ }
+
+ [Obsolete("Use the overload specifying a key instead")]
public PublishedContentType(int id, string alias, PublishedItemType itemType, IEnumerable compositionAliases, Func> propertyTypes, ContentVariation variations, bool isElement = false)
- : this(id, alias, itemType, compositionAliases, variations, isElement)
+ : this(Guid.Empty, id, alias, itemType, compositionAliases, variations, isElement)
{
_propertyTypes = propertyTypes(this).ToArray();
InitializeIndexes();
}
- private PublishedContentType(int id, string alias, PublishedItemType itemType, IEnumerable compositionAliases, ContentVariation variations, bool isElement)
+ private PublishedContentType(Guid key, int id, string alias, PublishedItemType itemType, IEnumerable compositionAliases, ContentVariation variations, bool isElement)
{
+ Key = key;
Id = id;
Alias = alias;
ItemType = itemType;
@@ -116,6 +138,9 @@ namespace Umbraco.Core.Models.PublishedContent
#region Content type
+ ///
+ public Guid Key { get; }
+
///
public int Id { get; }
diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeExtensions.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeExtensions.cs
new file mode 100644
index 0000000000..feab33c1d6
--- /dev/null
+++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeExtensions.cs
@@ -0,0 +1,24 @@
+using System;
+
+namespace Umbraco.Core.Models.PublishedContent
+{
+ public static class PublishedContentTypeExtensions
+ {
+ ///
+ /// Get the GUID key from an
+ ///
+ ///
+ ///
+ ///
+ public static bool TryGetKey(this IPublishedContentType publishedContentType, out Guid key)
+ {
+ if (publishedContentType is IPublishedContentType2 contentTypeWithKey)
+ {
+ key = contentTypeWithKey.Key;
+ return true;
+ }
+ key = Guid.Empty;
+ return false;
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs
index 34094508c3..c1548d3c3d 100644
--- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs
@@ -35,18 +35,18 @@ namespace Umbraco.Core.Models.PublishedContent
/// This method is for tests and is not intended to be used directly from application code.
///
/// Values are assumed to be consisted and are not checked.
- internal IPublishedContentType CreateContentType(int id, string alias, Func> propertyTypes, ContentVariation variations = ContentVariation.Nothing, bool isElement = false)
+ internal IPublishedContentType CreateContentType(Guid key, int id, string alias, Func> propertyTypes, ContentVariation variations = ContentVariation.Nothing, bool isElement = false)
{
- return new PublishedContentType(id, alias, PublishedItemType.Content, Enumerable.Empty(), propertyTypes, variations, isElement);
+ return new PublishedContentType(key, id, alias, PublishedItemType.Content, Enumerable.Empty(), propertyTypes, variations, isElement);
}
///
/// This method is for tests and is not intended to be used directly from application code.
///
/// Values are assumed to be consisted and are not checked.
- internal IPublishedContentType CreateContentType(int id, string alias, IEnumerable compositionAliases, Func> propertyTypes, ContentVariation variations = ContentVariation.Nothing, bool isElement = false)
+ internal IPublishedContentType CreateContentType(Guid key, int id, string alias, IEnumerable compositionAliases, Func> propertyTypes, ContentVariation variations = ContentVariation.Nothing, bool isElement = false)
{
- return new PublishedContentType(id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, variations, isElement);
+ return new PublishedContentType(key, id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, variations, isElement);
}
///
diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedCultureInfos.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedCultureInfos.cs
index 908b97fc36..58241ade24 100644
--- a/src/Umbraco.Core/Models/PublishedContent/PublishedCultureInfos.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/PublishedCultureInfos.cs
@@ -32,7 +32,7 @@ namespace Umbraco.Core.Models.PublishedContent
internal string Name { get; }
///
- /// Gets the url segment of the item.
+ /// Gets the URL segment of the item.
///
internal string UrlSegment { get; }
diff --git a/src/Umbraco.Core/Models/PublishedContent/UrlMode.cs b/src/Umbraco.Core/Models/PublishedContent/UrlMode.cs
index 4cd6a680f4..d11459bb9e 100644
--- a/src/Umbraco.Core/Models/PublishedContent/UrlMode.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/UrlMode.cs
@@ -1,27 +1,27 @@
namespace Umbraco.Core.Models.PublishedContent
{
///
- /// Specifies the type of urls that the url provider should produce, Auto is the default.
+ /// Specifies the type of URLs that the URL provider should produce, Auto is the default.
///
public enum UrlMode
{
///
- /// Indicates that the url provider should do what it has been configured to do.
+ /// Indicates that the URL provider should do what it has been configured to do.
///
Default = 0,
///
- /// Indicates that the url provider should produce relative urls exclusively.
+ /// Indicates that the URL provider should produce relative URLs exclusively.
///
Relative,
///
- /// Indicates that the url provider should produce absolute urls exclusively.
+ /// Indicates that the URL provider should produce absolute URLs exclusively.
///
Absolute,
///
- /// Indicates that the url provider should determine automatically whether to return relative or absolute urls.
+ /// Indicates that the URL provider should determine automatically whether to return relative or absolute URLs.
///
Auto
}
diff --git a/src/Umbraco.Core/Models/Range.cs b/src/Umbraco.Core/Models/Range.cs
index 83afa11658..108d564665 100644
--- a/src/Umbraco.Core/Models/Range.cs
+++ b/src/Umbraco.Core/Models/Range.cs
@@ -1,52 +1,130 @@
using System;
+using System.Globalization;
+
namespace Umbraco.Core.Models
{
- // The Range class. Adapted from http://stackoverflow.com/questions/5343006/is-there-a-c-sharp-type-for-representing-an-integer-range
- /// Generic parameter.
- public class Range where T : IComparable
+ ///
+ /// Represents a range with a minimum and maximum value.
+ ///
+ /// The type of the minimum and maximum values.
+ ///
+ public class Range : IEquatable>
+ where T : IComparable
{
- /// Minimum value of the range.
+ ///
+ /// Gets or sets the minimum value.
+ ///
+ ///
+ /// The minimum value.
+ ///
public T Minimum { get; set; }
- /// Maximum value of the range.
+ ///
+ /// Gets or sets the maximum value.
+ ///
+ ///
+ /// The maximum value.
+ ///
public T Maximum { get; set; }
- /// Presents the Range in readable format.
- /// String representation of the Range
- public override string ToString()
- {
- return string.Format("{0},{1}", this.Minimum, this.Maximum);
- }
+ ///
+ /// Returns a that represents this instance.
+ ///
+ ///
+ /// A that represents this instance.
+ ///
+ public override string ToString() => this.ToString("{0},{1}", CultureInfo.InvariantCulture);
- /// Determines if the range is valid.
- /// True if range is valid, else false
- public bool IsValid()
- {
- return this.Minimum.CompareTo(this.Maximum) <= 0;
- }
+ ///
+ /// Returns a that represents this instance.
+ ///
+ /// A composite format string for a single value (minimum and maximum are equal). Use {0} for the minimum and {1} for the maximum value.
+ /// A composite format string for the range values. Use {0} for the minimum and {1} for the maximum value.
+ /// An object that supplies culture-specific formatting information.
+ ///
+ /// A that represents this instance.
+ ///
+ public string ToString(string format, string formatRange, IFormatProvider provider = null) => this.ToString(this.Minimum.CompareTo(this.Maximum) == 0 ? format : formatRange, provider);
- /// Determines if the provided value is inside the range.
- /// The value to test
- /// True if the value is inside Range, else false
- public bool ContainsValue(T value)
- {
- return (this.Minimum.CompareTo(value) <= 0) && (value.CompareTo(this.Maximum) <= 0);
- }
+ ///
+ /// Returns a that represents this instance.
+ ///
+ /// A composite format string for the range values. Use {0} for the minimum and {1} for the maximum value.
+ /// An object that supplies culture-specific formatting information.
+ ///
+ /// A that represents this instance.
+ ///
+ public string ToString(string format, IFormatProvider provider = null) => string.Format(provider, format, this.Minimum, this.Maximum);
- /// Determines if this Range is inside the bounds of another range.
- /// The parent range to test on
- /// True if range is inclusive, else false
- public bool IsInsideRange(Range range)
- {
- return this.IsValid() && range.IsValid() && range.ContainsValue(this.Minimum) && range.ContainsValue(this.Maximum);
- }
+ ///
+ /// Determines whether this range is valid (the minimum value is lower than or equal to the maximum value).
+ ///
+ ///
+ /// true if this range is valid; otherwise, false.
+ ///
+ public bool IsValid() => this.Minimum.CompareTo(this.Maximum) <= 0;
- /// Determines if another range is inside the bounds of this range.
- /// The child range to test
- /// True if range is inside, else false
- public bool ContainsRange(Range range)
- {
- return this.IsValid() && range.IsValid() && this.ContainsValue(range.Minimum) && this.ContainsValue(range.Maximum);
- }
+ ///
+ /// Determines whether this range contains the specified value.
+ ///
+ /// The value.
+ ///
+ /// true if this range contains the specified value; otherwise, false.
+ ///
+ public bool ContainsValue(T value) => this.Minimum.CompareTo(value) <= 0 && value.CompareTo(this.Maximum) <= 0;
+
+ ///
+ /// Determines whether this range is inside the specified range.
+ ///
+ /// The range.
+ ///
+ /// true if this range is inside the specified range; otherwise, false.
+ ///
+ public bool IsInsideRange(Range range) => this.IsValid() && range.IsValid() && range.ContainsValue(this.Minimum) && range.ContainsValue(this.Maximum);
+
+ ///
+ /// Determines whether this range contains the specified range.
+ ///
+ /// The range.
+ ///
+ /// true if this range contains the specified range; otherwise, false.
+ ///
+ public bool ContainsRange(Range range) => this.IsValid() && range.IsValid() && this.ContainsValue(range.Minimum) && this.ContainsValue(range.Maximum);
+
+ ///
+ /// Determines whether the specified , is equal to this instance.
+ ///
+ /// The to compare with this instance.
+ ///
+ /// true if the specified is equal to this instance; otherwise, false.
+ ///
+ public override bool Equals(object obj) => obj is Range other && this.Equals(other);
+
+ ///
+ /// Indicates whether the current object is equal to another object of the same type.
+ ///
+ /// An object to compare with this object.
+ ///
+ /// if the current object is equal to the parameter; otherwise, .
+ ///
+ public bool Equals(Range other) => other != null && this.Equals(other.Minimum, other.Maximum);
+
+ ///
+ /// Determines whether the specified and values are equal to this instance values.
+ ///
+ /// The minimum value.
+ /// The maximum value.
+ ///
+ /// true if the specified and values are equal to this instance values; otherwise, false.
+ ///
+ public bool Equals(T minimum, T maximum) => this.Minimum.CompareTo(minimum) == 0 && this.Maximum.CompareTo(maximum) == 0;
+
+ ///
+ /// Returns a hash code for this instance.
+ ///
+ ///
+ /// A hash code for this instance, suitable for use in hashing algorithms and data structures like a hash table.
+ ///
+ public override int GetHashCode() => (this.Minimum, this.Maximum).GetHashCode();
}
}
diff --git a/src/Umbraco.Core/Models/ReadOnlyContentBaseAdapter.cs b/src/Umbraco.Core/Models/ReadOnlyContentBaseAdapter.cs
new file mode 100644
index 0000000000..f707d2ab1c
--- /dev/null
+++ b/src/Umbraco.Core/Models/ReadOnlyContentBaseAdapter.cs
@@ -0,0 +1,42 @@
+using System;
+
+namespace Umbraco.Core.Models
+{
+ internal struct ReadOnlyContentBaseAdapter : IReadOnlyContentBase
+ {
+ private readonly IContentBase _content;
+
+ private ReadOnlyContentBaseAdapter(IContentBase content)
+ {
+ _content = content ?? throw new ArgumentNullException(nameof(content));
+ }
+
+ public static ReadOnlyContentBaseAdapter Create(IContentBase content) => new ReadOnlyContentBaseAdapter(content);
+
+ public int Id => _content.Id;
+
+ public Guid Key => _content.Key;
+
+ public DateTime CreateDate => _content.CreateDate;
+
+ public DateTime UpdateDate => _content.UpdateDate;
+
+ public string Name => _content.Name;
+
+ public int CreatorId => _content.CreatorId;
+
+ public int ParentId => _content.ParentId;
+
+ public int Level => _content.Level;
+
+ public string Path => _content.Path;
+
+ public int SortOrder => _content.SortOrder;
+
+ public int ContentTypeId => _content.ContentTypeId;
+
+ public int WriterId => _content.WriterId;
+
+ public int VersionId => _content.VersionId;
+ }
+}
diff --git a/src/Umbraco.Core/Models/ServerRegistration.cs b/src/Umbraco.Core/Models/ServerRegistration.cs
index 7dae5d5393..a862b11c23 100644
--- a/src/Umbraco.Core/Models/ServerRegistration.cs
+++ b/src/Umbraco.Core/Models/ServerRegistration.cs
@@ -24,7 +24,7 @@ namespace Umbraco.Core.Models
/// Initializes a new instance of the class.
///
/// The unique id of the server registration.
- /// The server url.
+ /// The server URL.
/// The unique server identity.
/// The date and time the registration was created.
/// The date and time the registration was last accessed.
@@ -45,7 +45,7 @@ namespace Umbraco.Core.Models
///
/// Initializes a new instance of the class.
///
- /// The server url.
+ /// The server URL.
/// The unique server identity.
/// The date and time the registration was created.
public ServerRegistration(string serverAddress, string serverIdentity, DateTime registered)
@@ -58,7 +58,7 @@ namespace Umbraco.Core.Models
}
///
- /// Gets or sets the server url.
+ /// Gets or sets the server URL.
///
public string ServerAddress
{
diff --git a/src/Umbraco.Core/Models/SimpleContentType.cs b/src/Umbraco.Core/Models/SimpleContentType.cs
index 5c81017ec8..45b9d0ecfc 100644
--- a/src/Umbraco.Core/Models/SimpleContentType.cs
+++ b/src/Umbraco.Core/Models/SimpleContentType.cs
@@ -37,6 +37,7 @@ namespace Umbraco.Core.Models
if (contentType == null) throw new ArgumentNullException(nameof(contentType));
Id = contentType.Id;
+ Key = contentType.Key;
Alias = contentType.Alias;
Variations = contentType.Variations;
Icon = contentType.Icon;
@@ -51,6 +52,8 @@ namespace Umbraco.Core.Models
public int Id { get; }
+ public Guid Key { get; }
+
///
public ITemplate DefaultTemplate { get; }
@@ -109,13 +112,14 @@ namespace Umbraco.Core.Models
string ITreeEntity.Name { get => this.Name; set => throw new NotImplementedException(); }
int IEntity.Id { get => this.Id; set => throw new NotImplementedException(); }
bool IEntity.HasIdentity => this.Id != default;
+ Guid IEntity.Key { get => this.Key; set => throw new NotImplementedException(); }
+
int ITreeEntity.CreatorId { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
int ITreeEntity.ParentId { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
int ITreeEntity.Level { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
string ITreeEntity.Path { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
int ITreeEntity.SortOrder { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
- bool ITreeEntity.Trashed => throw new NotImplementedException();
- Guid IEntity.Key { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
+ bool ITreeEntity.Trashed => throw new NotImplementedException();
DateTime IEntity.CreateDate { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
DateTime IEntity.UpdateDate { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
DateTime? IEntity.DeleteDate { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
diff --git a/src/Umbraco.Core/Packaging/PackageDataInstallation.cs b/src/Umbraco.Core/Packaging/PackageDataInstallation.cs
index 6a5acb0dc7..4d1f12baaa 100644
--- a/src/Umbraco.Core/Packaging/PackageDataInstallation.cs
+++ b/src/Umbraco.Core/Packaging/PackageDataInstallation.cs
@@ -522,7 +522,7 @@ namespace Umbraco.Core.Packaging
&& ((string)infoElement.Element("Master")).IsNullOrWhiteSpace())
{
var alias = documentType.Element("Info").Element("Alias").Value;
- var folders = foldersAttribute.Value.Split('/');
+ var folders = foldersAttribute.Value.Split(Constants.CharArrays.ForwardSlash);
var rootFolder = HttpUtility.UrlDecode(folders[0]);
//level 1 = root level folders, there can only be one with the same name
var current = _contentTypeService.GetContainers(rootFolder, 1).FirstOrDefault();
@@ -604,6 +604,8 @@ namespace Umbraco.Core.Packaging
var defaultTemplateElement = infoElement.Element("DefaultTemplate");
contentType.Name = infoElement.Element("Name").Value;
+ if (infoElement.Element("Key") != null)
+ contentType.Key = new Guid(infoElement.Element("Key").Value);
contentType.Icon = infoElement.Element("Icon").Value;
contentType.Thumbnail = infoElement.Element("Thumbnail").Value;
contentType.Description = infoElement.Element("Description").Value;
@@ -798,8 +800,13 @@ namespace Umbraco.Core.Packaging
SortOrder = sortOrder,
Variations = property.Element("Variations") != null
? (ContentVariation)Enum.Parse(typeof(ContentVariation), property.Element("Variations").Value)
- : ContentVariation.Nothing
+ : ContentVariation.Nothing,
+ LabelOnTop = property.Element("LabelOnTop") != null
+ ? property.Element("LabelOnTop").Value.ToLowerInvariant().Equals("true")
+ : false
};
+ if (property.Element("Key") != null)
+ propertyType.Key = new Guid(property.Element("Key").Value);
var tab = (string)property.Element("Tab");
if (string.IsNullOrEmpty(tab))
@@ -935,7 +942,7 @@ namespace Umbraco.Core.Packaging
if (foldersAttribute != null)
{
var name = datatypeElement.Attribute("Name").Value;
- var folders = foldersAttribute.Value.Split('/');
+ var folders = foldersAttribute.Value.Split(Constants.CharArrays.ForwardSlash);
var rootFolder = HttpUtility.UrlDecode(folders[0]);
//there will only be a single result by name for level 1 (root) containers
var current = _dataTypeService.GetContainers(rootFolder, 1).FirstOrDefault();
diff --git a/src/Umbraco.Core/Packaging/PackageDefinitionXmlParser.cs b/src/Umbraco.Core/Packaging/PackageDefinitionXmlParser.cs
index 50cb692530..1bba373218 100644
--- a/src/Umbraco.Core/Packaging/PackageDefinitionXmlParser.cs
+++ b/src/Umbraco.Core/Packaging/PackageDefinitionXmlParser.cs
@@ -44,13 +44,13 @@ namespace Umbraco.Core.Packaging
Actions = xml.Element("actions")?.ToString(SaveOptions.None) ?? "", //take the entire outer xml value
ContentNodeId = xml.Element("content")?.AttributeValue("nodeId") ?? string.Empty,
ContentLoadChildNodes = xml.Element("content")?.AttributeValue("loadChildNodes") ?? false,
- Macros = xml.Element("macros")?.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(),
- Templates = xml.Element("templates")?.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(),
- Stylesheets = xml.Element("stylesheets")?.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(),
- DocumentTypes = xml.Element("documentTypes")?.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(),
- Languages = xml.Element("languages")?.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(),
- DictionaryItems = xml.Element("dictionaryitems")?.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(),
- DataTypes = xml.Element("datatypes")?.Value.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(),
+ Macros = xml.Element("macros")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(),
+ Templates = xml.Element("templates")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(),
+ Stylesheets = xml.Element("stylesheets")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(),
+ DocumentTypes = xml.Element("documentTypes")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(),
+ Languages = xml.Element("languages")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(),
+ DictionaryItems = xml.Element("dictionaryitems")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(),
+ DataTypes = xml.Element("datatypes")?.Value.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries).ToList() ?? new List(),
Files = xml.Element("files")?.Elements("file").Select(x => x.Value).ToList() ?? new List()
};
diff --git a/src/Umbraco.Core/Persistence/DatabaseAnnotations/IndexAttribute.cs b/src/Umbraco.Core/Persistence/DatabaseAnnotations/IndexAttribute.cs
index 138dceff09..5aaabbfa6f 100644
--- a/src/Umbraco.Core/Persistence/DatabaseAnnotations/IndexAttribute.cs
+++ b/src/Umbraco.Core/Persistence/DatabaseAnnotations/IndexAttribute.cs
@@ -31,5 +31,10 @@ namespace Umbraco.Core.Persistence.DatabaseAnnotations
/// Gets or sets the column name(s) for the current index
///
public string ForColumns { get; set; }
+
+ ///
+ /// Gets or sets the column name(s) for the columns to include in the index
+ ///
+ public string IncludeColumns { get; set; }
}
}
diff --git a/src/Umbraco.Core/Persistence/DatabaseAnnotations/SpecialDbTypes.cs b/src/Umbraco.Core/Persistence/DatabaseAnnotations/SpecialDbTypes.cs
index 4b606bc0f5..6d211ebbd9 100644
--- a/src/Umbraco.Core/Persistence/DatabaseAnnotations/SpecialDbTypes.cs
+++ b/src/Umbraco.Core/Persistence/DatabaseAnnotations/SpecialDbTypes.cs
@@ -7,6 +7,7 @@
public enum SpecialDbTypes
{
NTEXT,
- NCHAR
+ NCHAR,
+ NVARCHARMAX
}
}
diff --git a/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs b/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs
index 5925e58afc..e6f06e4200 100644
--- a/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs
+++ b/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/DefinitionFactory.cs
@@ -160,12 +160,20 @@ namespace Umbraco.Core.Persistence.DatabaseModelDefinitions
if (string.IsNullOrEmpty(attribute.ForColumns) == false)
{
- var columns = attribute.ForColumns.Split(',').Select(p => p.Trim());
+ var columns = attribute.ForColumns.Split(Constants.CharArrays.Comma).Select(p => p.Trim());
foreach (var column in columns)
{
definition.Columns.Add(new IndexColumnDefinition {Name = column, Direction = Direction.Ascending});
}
}
+ if (string.IsNullOrEmpty(attribute.IncludeColumns) == false)
+ {
+ var columns = attribute.IncludeColumns.Split(',').Select(p => p.Trim());
+ foreach (var column in columns)
+ {
+ definition.IncludeColumns.Add(new IndexColumnDefinition { Name = column, Direction = Direction.Ascending });
+ }
+ }
return definition;
}
}
diff --git a/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/IndexDefinition.cs b/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/IndexDefinition.cs
index 582f9a40f7..20f75d38c8 100644
--- a/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/IndexDefinition.cs
+++ b/src/Umbraco.Core/Persistence/DatabaseModelDefinitions/IndexDefinition.cs
@@ -6,17 +6,13 @@ namespace Umbraco.Core.Persistence.DatabaseModelDefinitions
{
public class IndexDefinition
{
- public IndexDefinition()
- {
- Columns = new List();
- }
-
public virtual string Name { get; set; }
public virtual string SchemaName { get; set; }
public virtual string TableName { get; set; }
public virtual string ColumnName { get; set; }
- public virtual ICollection Columns { get; set; }
+ public virtual ICollection Columns { get; set; } = new List();
+ public virtual ICollection IncludeColumns { get; set; } = new List();
public IndexTypes IndexType { get; set; }
}
}
diff --git a/src/Umbraco.Core/Persistence/Dtos/ContentNuDto.cs b/src/Umbraco.Core/Persistence/Dtos/ContentNuDto.cs
index c6269d5317..a2f36584e0 100644
--- a/src/Umbraco.Core/Persistence/Dtos/ContentNuDto.cs
+++ b/src/Umbraco.Core/Persistence/Dtos/ContentNuDto.cs
@@ -25,9 +25,16 @@ namespace Umbraco.Core.Persistence.Dtos
///
[Column("data")]
[SpecialDbType(SpecialDbTypes.NTEXT)]
+ [NullSetting(NullSetting = NullSettings.Null)]
public string Data { get; set; }
[Column("rv")]
public long Rv { get; set; }
+
+ [Column("dataRaw")]
+ [NullSetting(NullSetting = NullSettings.Null)]
+ public byte[] RawData { get; set; }
+
+
}
}
diff --git a/src/Umbraco.Core/Persistence/Dtos/ContentVersionDto.cs b/src/Umbraco.Core/Persistence/Dtos/ContentVersionDto.cs
index 4b203c128f..f9bf283be9 100644
--- a/src/Umbraco.Core/Persistence/Dtos/ContentVersionDto.cs
+++ b/src/Umbraco.Core/Persistence/Dtos/ContentVersionDto.cs
@@ -19,7 +19,7 @@ namespace Umbraco.Core.Persistence.Dtos
[Column("nodeId")]
[ForeignKey(typeof(ContentDto))]
- [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_NodeId", ForColumns = "nodeId,current")]
+ [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_NodeId", ForColumns = "nodeId,current", IncludeColumns = "id,versionDate,text,userId")]
public int NodeId { get; set; }
[Column("versionDate")] // TODO: db rename to 'updateDate'
@@ -32,6 +32,7 @@ namespace Umbraco.Core.Persistence.Dtos
public int? UserId { get => _userId == 0 ? null : _userId; set => _userId = value; } //return null if zero
[Column("current")]
+ [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Current", IncludeColumns = "nodeId")]
public bool Current { get; set; }
// about current:
diff --git a/src/Umbraco.Core/Persistence/Dtos/ExternalLoginDto.cs b/src/Umbraco.Core/Persistence/Dtos/ExternalLoginDto.cs
index 1b774854a6..0a56552000 100644
--- a/src/Umbraco.Core/Persistence/Dtos/ExternalLoginDto.cs
+++ b/src/Umbraco.Core/Persistence/Dtos/ExternalLoginDto.cs
@@ -14,9 +14,13 @@ namespace Umbraco.Core.Persistence.Dtos
[PrimaryKeyColumn(Name = "PK_umbracoExternalLogin")]
public int Id { get; set; }
+ // TODO: This is completely missing a FK!!?
+
[Column("userId")]
public int UserId { get; set; }
+ // TODO: There should be an index on both LoginProvider and ProviderKey
+
[Column("loginProvider")]
[Length(4000)]
[NullSetting(NullSetting = NullSettings.NotNull)]
@@ -30,5 +34,13 @@ namespace Umbraco.Core.Persistence.Dtos
[Column("createDate")]
[Constraint(Default = SystemMethods.CurrentDateTime)]
public DateTime CreateDate { get; set; }
+
+ ///
+ /// Used to store any arbitrary data for the user and external provider - like user tokens returned from the provider
+ ///
+ [Column("userData")]
+ [NullSetting(NullSetting = NullSettings.Null)]
+ [SpecialDbType(SpecialDbTypes.NTEXT)]
+ public string UserData { get; set; }
}
}
diff --git a/src/Umbraco.Core/Persistence/Dtos/NodeDto.cs b/src/Umbraco.Core/Persistence/Dtos/NodeDto.cs
index 5800efb97a..207195e594 100644
--- a/src/Umbraco.Core/Persistence/Dtos/NodeDto.cs
+++ b/src/Umbraco.Core/Persistence/Dtos/NodeDto.cs
@@ -20,7 +20,7 @@ namespace Umbraco.Core.Persistence.Dtos
[Column("uniqueId")]
[NullSetting(NullSetting = NullSettings.NotNull)]
- [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_UniqueId")]
+ [Index(IndexTypes.UniqueNonClustered, Name = "IX_" + TableName + "_UniqueId", IncludeColumns = "parentId,level,path,sortOrder,trashed,nodeUser,text,createDate")]
[Constraint(Default = SystemMethods.NewGuid)]
public Guid UniqueId { get; set; }
@@ -29,7 +29,9 @@ namespace Umbraco.Core.Persistence.Dtos
[Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ParentId")]
public int ParentId { get; set; }
+ // NOTE: This index is primarily for the nucache data lookup, see https://github.com/umbraco/Umbraco-CMS/pull/8365#issuecomment-673404177
[Column("level")]
+ [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_Level", ForColumns = "level,parentId,sortOrder,nodeObjectType,trashed", IncludeColumns = "nodeUser,path,uniqueId,createDate")]
public short Level { get; set; }
[Column("path")]
@@ -55,8 +57,8 @@ namespace Umbraco.Core.Persistence.Dtos
public string Text { get; set; }
[Column("nodeObjectType")] // TODO: db rename to 'objectType'
- [NullSetting(NullSetting = NullSettings.Null)]
- [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ObjectType")]
+ [NullSetting(NullSetting = NullSettings.Null)]
+ [Index(IndexTypes.NonClustered, Name = "IX_" + TableName + "_ObjectType", ForColumns = "nodeObjectType,trashed", IncludeColumns = "uniqueId,parentId,level,path,sortOrder,nodeUser,text,createDate")]
public Guid? NodeObjectType { get; set; }
[Column("createDate")]
diff --git a/src/Umbraco.Core/Persistence/Dtos/PropertyTypeDto.cs b/src/Umbraco.Core/Persistence/Dtos/PropertyTypeDto.cs
index 3e8d6e7496..572201c94a 100644
--- a/src/Umbraco.Core/Persistence/Dtos/PropertyTypeDto.cs
+++ b/src/Umbraco.Core/Persistence/Dtos/PropertyTypeDto.cs
@@ -62,6 +62,10 @@ namespace Umbraco.Core.Persistence.Dtos
[Length(2000)]
public string Description { get; set; }
+ [Column("labelOnTop")]
+ [Constraint(Default = "0")]
+ public bool LabelOnTop { get; set; }
+
[Column("variations")]
[Constraint(Default = "1" /*ContentVariation.InvariantNeutral*/)]
public byte Variations { get; set; }
diff --git a/src/Umbraco.Core/Persistence/Dtos/PropertyTypeReadOnlyDto.cs b/src/Umbraco.Core/Persistence/Dtos/PropertyTypeReadOnlyDto.cs
index 4c352a0134..d2001c00d5 100644
--- a/src/Umbraco.Core/Persistence/Dtos/PropertyTypeReadOnlyDto.cs
+++ b/src/Umbraco.Core/Persistence/Dtos/PropertyTypeReadOnlyDto.cs
@@ -44,6 +44,9 @@ namespace Umbraco.Core.Persistence.Dtos
[Column("Description")]
public string Description { get; set; }
+ [Column("labelOnTop")]
+ public bool LabelOnTop { get; set; }
+
/* cmsMemberType */
[Column("memberCanEdit")]
public bool CanEdit { get; set; }
diff --git a/src/Umbraco.Core/Persistence/Factories/ExternalLoginFactory.cs b/src/Umbraco.Core/Persistence/Factories/ExternalLoginFactory.cs
index 4309fe511f..6c1af68acd 100644
--- a/src/Umbraco.Core/Persistence/Factories/ExternalLoginFactory.cs
+++ b/src/Umbraco.Core/Persistence/Factories/ExternalLoginFactory.cs
@@ -1,13 +1,17 @@
-using Umbraco.Core.Models.Identity;
+using System;
+using Umbraco.Core.Models.Identity;
using Umbraco.Core.Persistence.Dtos;
namespace Umbraco.Core.Persistence.Factories
{
internal static class ExternalLoginFactory
{
- public static IIdentityUserLogin BuildEntity(ExternalLoginDto dto)
+ public static IIdentityUserLoginExtended BuildEntity(ExternalLoginDto dto)
{
- var entity = new IdentityUserLogin(dto.Id, dto.LoginProvider, dto.ProviderKey, dto.UserId, dto.CreateDate);
+ var entity = new IdentityUserLogin(dto.Id, dto.LoginProvider, dto.ProviderKey, dto.UserId, dto.CreateDate)
+ {
+ UserData = dto.UserData
+ };
// reset dirty initial properties (U4-1946)
entity.ResetDirtyProperties(false);
@@ -16,13 +20,30 @@ namespace Umbraco.Core.Persistence.Factories
public static ExternalLoginDto BuildDto(IIdentityUserLogin entity)
{
+ var asExtended = entity as IIdentityUserLoginExtended;
var dto = new ExternalLoginDto
{
Id = entity.Id,
CreateDate = entity.CreateDate,
LoginProvider = entity.LoginProvider,
ProviderKey = entity.ProviderKey,
- UserId = entity.UserId
+ UserId = entity.UserId,
+ UserData = asExtended?.UserData
+ };
+
+ return dto;
+ }
+
+ public static ExternalLoginDto BuildDto(int userId, IExternalLogin entity, int? id = null)
+ {
+ var dto = new ExternalLoginDto
+ {
+ Id = id ?? default,
+ UserId = userId,
+ LoginProvider = entity.LoginProvider,
+ ProviderKey = entity.ProviderKey,
+ UserData = entity.UserData,
+ CreateDate = DateTime.Now
};
return dto;
diff --git a/src/Umbraco.Core/Persistence/Factories/PropertyGroupFactory.cs b/src/Umbraco.Core/Persistence/Factories/PropertyGroupFactory.cs
index dc1629e8f7..f139619302 100644
--- a/src/Umbraco.Core/Persistence/Factories/PropertyGroupFactory.cs
+++ b/src/Umbraco.Core/Persistence/Factories/PropertyGroupFactory.cs
@@ -132,7 +132,8 @@ namespace Umbraco.Core.Persistence.Factories
ValidationRegExp = propertyType.ValidationRegExp,
ValidationRegExpMessage = propertyType.ValidationRegExpMessage,
UniqueId = propertyType.Key,
- Variations = (byte)propertyType.Variations
+ Variations = (byte)propertyType.Variations,
+ LabelOnTop = propertyType.LabelOnTop
};
if (tabId != default)
diff --git a/src/Umbraco.Core/Persistence/FaultHandling/Strategies/SqlAzureTransientErrorDetectionStrategy.cs b/src/Umbraco.Core/Persistence/FaultHandling/Strategies/SqlAzureTransientErrorDetectionStrategy.cs
index 849fd35fad..f763594616 100644
--- a/src/Umbraco.Core/Persistence/FaultHandling/Strategies/SqlAzureTransientErrorDetectionStrategy.cs
+++ b/src/Umbraco.Core/Persistence/FaultHandling/Strategies/SqlAzureTransientErrorDetectionStrategy.cs
@@ -4,6 +4,10 @@ using System.Data.SqlClient;
namespace Umbraco.Core.Persistence.FaultHandling.Strategies
{
+ // See https://docs.microsoft.com/en-us/azure/azure-sql/database/troubleshoot-common-connectivity-issues
+ // Also we could just use the nuget package instead https://www.nuget.org/packages/EnterpriseLibrary.TransientFaultHandling/ ?
+ // but i guess that's not netcore so we'll just leave it.
+
///
/// Provides the transient error detection logic for transient faults that are specific to SQL Azure.
///
@@ -71,7 +75,7 @@ namespace Umbraco.Core.Persistence.FaultHandling.Strategies
/// Determines whether the specified exception represents a transient failure that can be compensated by a retry.
///
/// The exception object to be verified.
- /// True if the specified exception is considered as transient, otherwise false.
+ /// true if the specified exception is considered as transient; otherwise, false.
public bool IsTransient(Exception ex)
{
if (ex != null)
@@ -97,40 +101,50 @@ namespace Umbraco.Core.Persistence.FaultHandling.Strategies
return true;
- // SQL Error Code: 40197
- // The service has encountered an error processing your request. Please try again.
- case 40197:
+ // SQL Error Code: 10928
+ // Resource ID: %d. The %s limit for the database is %d and has been reached.
+ case 10928:
+ // SQL Error Code: 10929
+ // Resource ID: %d. The %s minimum guarantee is %d, maximum limit is %d and the current usage for the database is %d.
+ // However, the server is currently too busy to support requests greater than %d for this database.
+ case 10929:
// SQL Error Code: 10053
// A transport-level error has occurred when receiving results from the server.
// An established connection was aborted by the software in your host machine.
case 10053:
// SQL Error Code: 10054
- // A transport-level error has occurred when sending the request to the server.
+ // A transport-level error has occurred when sending the request to the server.
// (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.)
case 10054:
// SQL Error Code: 10060
- // A network-related or instance-specific error occurred while establishing a connection to SQL Server.
- // The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server
- // is configured to allow remote connections. (provider: TCP Provider, error: 0 - A connection attempt failed
- // because the connected party did not properly respond after a period of time, or established connection failed
+ // A network-related or instance-specific error occurred while establishing a connection to SQL Server.
+ // The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server
+ // is configured to allow remote connections. (provider: TCP Provider, error: 0 - A connection attempt failed
+ // because the connected party did not properly respond after a period of time, or established connection failed
// because connected host has failed to respond.)"}
case 10060:
+ // SQL Error Code: 40197
+ // The service has encountered an error processing your request. Please try again.
+ case 40197:
+ // SQL Error Code: 40540
+ // The service has encountered an error processing your request. Please try again.
+ case 40540:
// SQL Error Code: 40613
- // Database XXXX on server YYYY is not currently available. Please retry the connection later. If the problem persists, contact customer
+ // Database XXXX on server YYYY is not currently available. Please retry the connection later. If the problem persists, contact customer
// support, and provide them the session tracing ID of ZZZZZ.
case 40613:
// SQL Error Code: 40143
// The service has encountered an error processing your request. Please try again.
case 40143:
// SQL Error Code: 233
- // The client was unable to establish a connection because of an error during connection initialization process before login.
- // Possible causes include the following: the client tried to connect to an unsupported version of SQL Server; the server was too busy
- // to accept new connections; or there was a resource limitation (insufficient memory or maximum allowed connections) on the server.
+ // The client was unable to establish a connection because of an error during connection initialization process before login.
+ // Possible causes include the following: the client tried to connect to an unsupported version of SQL Server; the server was too busy
+ // to accept new connections; or there was a resource limitation (insufficient memory or maximum allowed connections) on the server.
// (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.)
case 233:
// SQL Error Code: 64
- // A connection was successfully established with the server, but then an error occurred during the login process.
- // (provider: TCP Provider, error: 0 - The specified network name is no longer available.)
+ // A connection was successfully established with the server, but then an error occurred during the login process.
+ // (provider: TCP Provider, error: 0 - The specified network name is no longer available.)
case 64:
// DBNETLIB Error Code: 20
// The instance of SQL Server you attempted to connect to does not support encryption.
diff --git a/src/Umbraco.Core/Persistence/Mappers/PropertyTypeMapper.cs b/src/Umbraco.Core/Persistence/Mappers/PropertyTypeMapper.cs
index 6f22b61f9a..d44618cc96 100644
--- a/src/Umbraco.Core/Persistence/Mappers/PropertyTypeMapper.cs
+++ b/src/Umbraco.Core/Persistence/Mappers/PropertyTypeMapper.cs
@@ -29,6 +29,7 @@ namespace Umbraco.Core.Persistence.Mappers
DefineMap(nameof(PropertyType.SortOrder), nameof(PropertyTypeDto.SortOrder));
DefineMap(nameof(PropertyType.ValidationRegExp), nameof(PropertyTypeDto.ValidationRegExp));
DefineMap(nameof(PropertyType.ValidationRegExpMessage), nameof(PropertyTypeDto.ValidationRegExpMessage));
+ DefineMap(nameof(PropertyType.LabelOnTop), nameof(PropertyTypeDto.LabelOnTop));
DefineMap(nameof(PropertyType.PropertyEditorAlias), nameof(DataTypeDto.EditorAlias));
DefineMap(nameof(PropertyType.ValueStorageType), nameof(DataTypeDto.DbType));
}
diff --git a/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs b/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs
index 10db1ca18e..bff682d095 100644
--- a/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs
+++ b/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions-Bulk.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlServerCe;
+using System.Data.SqlTypes;
using System.Linq;
using NPoco;
using Umbraco.Core.Persistence.SqlSyntax;
@@ -210,7 +211,15 @@ namespace Umbraco.Core.Persistence
if (IncludeColumn(pocoData, columns[i]))
{
var val = columns[i].Value.GetValue(record);
- updatableRecord.SetValue(i, val);
+ if (val is byte[])
+ {
+ var bytes = val as byte[];
+ updatableRecord.SetSqlBinary(i, new SqlBinary(bytes));
+ }
+ else
+ {
+ updatableRecord.SetValue(i, val);
+ }
}
}
resultSet.Insert(updatableRecord);
diff --git a/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions.cs b/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions.cs
index 152dcbe6d3..c2100d97ad 100644
--- a/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions.cs
+++ b/src/Umbraco.Core/Persistence/NPocoDatabaseExtensions.cs
@@ -27,12 +27,13 @@ namespace Umbraco.Core.Persistence
/// The number of rows to load per page
///
///
+ /// Specify a custom Sql command to get the total count, if null is specified than the auto-generated sql count will be used
///
///
/// NPoco's normal Page returns a List{T} but sometimes we don't want all that in memory and instead want to
/// iterate over each row with a reader using Query vs Fetch.
///
- internal static IEnumerable QueryPaged(this IDatabase database, long pageSize, Sql sql)
+ internal static IEnumerable QueryPaged(this IDatabase database, long pageSize, Sql sql, Sql sqlCount)
{
var sqlString = sql.SQL;
var sqlArgs = sql.Arguments;
@@ -42,12 +43,12 @@ namespace Umbraco.Core.Persistence
do
{
// Get the paged queries
- database.BuildPageQueries(pageIndex * pageSize, pageSize, sqlString, ref sqlArgs, out var sqlCount, out var sqlPage);
+ database.BuildPageQueries(pageIndex * pageSize, pageSize, sqlString, ref sqlArgs, out var generatedSqlCount, out var sqlPage);
// get the item count once
if (itemCount == null)
{
- itemCount = database.ExecuteScalar(sqlCount, sqlArgs);
+ itemCount = database.ExecuteScalar(sqlCount?.SQL ?? generatedSqlCount, sqlCount?.Arguments ?? sqlArgs);
}
pageIndex++;
@@ -60,6 +61,22 @@ namespace Umbraco.Core.Persistence
} while ((pageIndex * pageSize) < itemCount);
}
+ ///
+ /// Iterates over the result of a paged data set with a db reader
+ ///
+ ///
+ ///
+ ///
+ /// The number of rows to load per page
+ ///
+ ///
+ ///
+ ///
+ /// NPoco's normal Page returns a List{T} but sometimes we don't want all that in memory and instead want to
+ /// iterate over each row with a reader using Query vs Fetch.
+ ///
+ internal static IEnumerable QueryPaged(this IDatabase database, long pageSize, Sql sql) => database.QueryPaged(pageSize, sql, null);
+
// NOTE
//
// proper way to do it with TSQL and SQLCE
diff --git a/src/Umbraco.Core/Persistence/PocoDataDataReader.cs b/src/Umbraco.Core/Persistence/PocoDataDataReader.cs
index 1d7d301b87..460a4d3d90 100644
--- a/src/Umbraco.Core/Persistence/PocoDataDataReader.cs
+++ b/src/Umbraco.Core/Persistence/PocoDataDataReader.cs
@@ -79,6 +79,9 @@ namespace Umbraco.Core.Persistence
case SpecialDbTypes.NCHAR:
sqlDbType = SqlDbType.NChar;
break;
+ case SpecialDbTypes.NVARCHARMAX:
+ sqlDbType = SqlDbType.NVarChar;
+ break;
default:
throw new ArgumentOutOfRangeException();
}
diff --git a/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs b/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs
index d04930fa92..388286a79b 100644
--- a/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs
+++ b/src/Umbraco.Core/Persistence/Querying/ExpressionVisitorBase.cs
@@ -328,8 +328,11 @@ namespace Umbraco.Core.Persistence.Querying
{
case ExpressionType.MemberAccess:
// false property , i.e. x => !Trashed
- SqlParameters.Add(true);
- return Visited ? string.Empty : $"NOT ({o} = @{SqlParameters.Count - 1})";
+ // BUT we don't want to do a NOT SQL statement since this generally results in indexes not being used
+ // so we want to do an == false
+ SqlParameters.Add(false);
+ return Visited ? string.Empty : $"{o} = @{SqlParameters.Count - 1}";
+ //return Visited ? string.Empty : $"NOT ({o} = @{SqlParameters.Count - 1})";
default:
// could be anything else, such as: x => !x.Path.StartsWith("-20")
return Visited ? string.Empty : string.Concat("NOT (", o, ")");
@@ -744,7 +747,9 @@ namespace Umbraco.Core.Persistence.Querying
var c = exp[0];
return (c == '"' || c == '`' || c == '\'') && exp[exp.Length - 1] == c
- ? exp.Substring(1, exp.Length - 2)
+ ? exp.Length == 1
+ ? string.Empty
+ : exp.Substring(1, exp.Length - 2)
: exp;
}
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs
index fc5382499f..0971b2047a 100644
--- a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs
@@ -12,6 +12,11 @@ namespace Umbraco.Core.Persistence.Repositories
///
void ClearSchedule(DateTime date);
+ void ClearSchedule(DateTime date, ContentScheduleAction action);
+
+ bool HasContentForExpiration(DateTime date);
+ bool HasContentForRelease(DateTime date);
+
///
/// Gets objects having an expiration date before (lower than, or equal to) a specified date.
///
diff --git a/src/Umbraco.Core/Persistence/Repositories/IExternalLoginRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IExternalLoginRepository.cs
index 6d145e9961..a5a93449d3 100644
--- a/src/Umbraco.Core/Persistence/Repositories/IExternalLoginRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/IExternalLoginRepository.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using Microsoft.AspNet.Identity;
using Umbraco.Core.Models.Identity;
@@ -6,7 +7,9 @@ namespace Umbraco.Core.Persistence.Repositories
{
public interface IExternalLoginRepository : IReadWriteQueryRepository
{
+ [Obsolete("Use the overload specifying IIdentityUserLoginExtended instead")]
void SaveUserLogins(int memberId, IEnumerable logins);
+ void Save(int userId, IEnumerable logins);
void DeleteUserLogins(int memberId);
}
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/IMemberGroupRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IMemberGroupRepository.cs
index 9c75c051bd..d63b1bb9db 100644
--- a/src/Umbraco.Core/Persistence/Repositories/IMemberGroupRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/IMemberGroupRepository.cs
@@ -1,10 +1,18 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using Umbraco.Core.Models;
namespace Umbraco.Core.Persistence.Repositories
{
public interface IMemberGroupRepository : IReadWriteQueryRepository
{
+ ///
+ /// Gets a member group by it's uniqueId
+ ///
+ ///
+ ///
+ IMemberGroup Get(Guid uniqueId);
+
///
/// Gets a member group by it's name
///
diff --git a/src/Umbraco.Core/Persistence/Repositories/IMemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IMemberRepository.cs
index 245c024356..c737c2bf66 100644
--- a/src/Umbraco.Core/Persistence/Repositories/IMemberRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/IMemberRepository.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence.Querying;
@@ -6,6 +7,8 @@ namespace Umbraco.Core.Persistence.Repositories
{
public interface IMemberRepository : IContentRepository
{
+ IMember GetByUsername(string username);
+
///
/// Finds members in a given role
///
@@ -35,5 +38,17 @@ namespace Umbraco.Core.Persistence.Repositories
///
///
int GetCountByQuery(IQuery query);
+
+ ///
+ /// Sets a members last login date based on their username
+ ///
+ ///
+ ///
+ ///
+ /// This is a specialized method because whenever a member logs in, the membership provider requires us to set the 'online' which requires
+ /// updating their login date. This operation must be fast and cannot use database locks which is fine if we are only executing a single query
+ /// for this data since there won't be any other data contention issues.
+ ///
+ void SetLastLogin(string username, DateTime date);
}
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/IRedirectUrlRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IRedirectUrlRepository.cs
index d05f4e007c..36f885520d 100644
--- a/src/Umbraco.Core/Persistence/Repositories/IRedirectUrlRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/IRedirectUrlRepository.cs
@@ -10,72 +10,80 @@ namespace Umbraco.Core.Persistence.Repositories
public interface IRedirectUrlRepository : IReadWriteQueryRepository
{
///
- /// Gets a redirect url.
+ /// Gets a redirect URL.
///
- /// The Umbraco redirect url route.
+ /// The Umbraco redirect URL route.
/// The content unique key.
/// The culture.
///
IRedirectUrl Get(string url, Guid contentKey, string culture);
///
- /// Deletes a redirect url.
+ /// Deletes a redirect URL.
///
- /// The redirect url identifier.
+ /// The redirect URL identifier.
void Delete(Guid id);
///
- /// Deletes all redirect urls.
+ /// Deletes all redirect URLs.
///
void DeleteAll();
///
- /// Deletes all redirect urls for a given content.
+ /// Deletes all redirect URLs for a given content.
///
/// The content unique key.
void DeleteContentUrls(Guid contentKey);
///
- /// Gets the most recent redirect url corresponding to an Umbraco redirect url route.
+ /// Gets the most recent redirect URL corresponding to an Umbraco redirect URL route.
///
- /// The Umbraco redirect url route.
- /// The most recent redirect url corresponding to the route.
+ /// The Umbraco redirect URL route.
+ /// The most recent redirect URL corresponding to the route.
IRedirectUrl GetMostRecentUrl(string url);
///
- /// Gets all redirect urls for a content item.
+ /// Gets the most recent redirect URL corresponding to an Umbraco redirect URL route.
+ ///
+ /// The Umbraco redirect URL route.
+ /// The culture the domain is associated with
+ /// The most recent redirect URL corresponding to the route.
+ IRedirectUrl GetMostRecentUrl(string url, string culture);
+
+ ///
+ /// Gets all redirect URLs for a content item.
///
/// The content unique key.
- /// All redirect urls for the content item.
+ /// All redirect URLs for the content item.
IEnumerable GetContentUrls(Guid contentKey);
///
- /// Gets all redirect urls.
+ /// Gets all redirect URLs.
///
/// The page index.
/// The page size.
- /// The total count of redirect urls.
- /// The redirect urls.
+ /// The total count of redirect URLs.
+ /// The redirect URLs.
IEnumerable GetAllUrls(long pageIndex, int pageSize, out long total);
///
- /// Gets all redirect urls below a given content item.
+ /// Gets all redirect URLs below a given content item.
///
/// The content unique identifier.
/// The page index.
/// The page size.
- /// The total count of redirect urls.
- /// The redirect urls.
+ /// The total count of redirect URLs.
+ /// The redirect URLs.
IEnumerable GetAllUrls(int rootContentId, long pageIndex, int pageSize, out long total);
///
- /// Searches for all redirect urls that contain a given search term in their URL property.
+ /// Searches for all redirect URLs that contain a given search term in their URL property.
///
/// The term to search for.
/// The page index.
/// The page size.
- /// The total count of redirect urls.
- /// The redirect urls.
+ /// The total count of redirect URLs.
+ /// The redirect URLs.
IEnumerable SearchUrls(string searchTerm, long pageIndex, int pageSize, out long total);
}
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs
index d56724db9f..1aabb51d6c 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentRepositoryBase.cs
@@ -514,7 +514,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
currentParentIds.Add(node.NodeId);
// paths parts without the roots
- var pathParts = node.Path.Split(',').Where(x => !rootIds.Contains(int.Parse(x))).ToArray();
+ var pathParts = node.Path.Split(Constants.CharArrays.Comma).Where(x => !rootIds.Contains(int.Parse(x))).ToArray();
if (!prevParentIds.Contains(node.ParentId))
{
@@ -897,7 +897,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.EnsureUniqueNodeName, tsql => tsql
.Select(x => Alias(x.NodeId, "id"), x => Alias(x.Text, "name"))
.From()
- .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && x.ParentId == SqlTemplate.Arg("parentId")));
+ .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && x.ParentId == SqlTemplate.Arg("parentId"))
+ );
var sql = template.Sql(NodeObjectTypeId, parentId);
var names = Database.Fetch(sql);
@@ -907,28 +908,43 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
protected virtual int GetNewChildSortOrder(int parentId, int first)
{
- var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetSortOrder, tsql =>
- tsql.Select($"COALESCE(MAX(sortOrder),{first - 1})").From().Where(x => x.ParentId == SqlTemplate.Arg("parentId") && x.NodeObjectType == NodeObjectTypeId)
+ var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetSortOrder, tsql => tsql
+ .Select("MAX(sortOrder)")
+ .From()
+ .Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType") && x.ParentId == SqlTemplate.Arg("parentId"))
);
- return Database.ExecuteScalar(template.Sql(new { parentId })) + 1;
+ var sql = template.Sql(NodeObjectTypeId, parentId);
+ var sortOrder = Database.ExecuteScalar(sql);
+
+ return (sortOrder + 1) ?? first;
}
protected virtual NodeDto GetParentNodeDto(int parentId)
{
- var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetParentNode, tsql =>
- tsql.Select().From().Where(x => x.NodeId == SqlTemplate.Arg("parentId"))
+ var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetParentNode, tsql => tsql
+ .Select()
+ .From()
+ .Where(x => x.NodeId == SqlTemplate.Arg("parentId"))
);
- return Database.Fetch(template.Sql(parentId)).First();
+ var sql = template.Sql(parentId);
+ var nodeDto = Database.First(sql);
+
+ return nodeDto;
}
protected virtual int GetReservedId(Guid uniqueId)
{
- var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetReservedId, tsql =>
- tsql.Select(x => x.NodeId).From().Where(x => x.UniqueId == SqlTemplate.Arg("uniqueId") && x.NodeObjectType == Constants.ObjectTypes.IdReservation)
+ var template = SqlContext.Templates.Get(Constants.SqlTemplates.VersionableRepository.GetReservedId, tsql => tsql
+ .Select(x => x.NodeId)
+ .From()
+ .Where(x => x.UniqueId == SqlTemplate.Arg("uniqueId") && x.NodeObjectType == Constants.ObjectTypes.IdReservation)
);
- var id = Database.ExecuteScalar(template.Sql(new { uniqueId = uniqueId }));
+
+ var sql = template.Sql(new { uniqueId });
+ var id = Database.ExecuteScalar(sql);
+
return id ?? 0;
}
@@ -987,6 +1003,81 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
}
+ ///
+ /// Inserts property values for the content entity
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// Used when creating a new entity
+ ///
+ protected void InsertPropertyValues(TEntity entity, int publishedVersionId, out bool edited, out HashSet editedCultures)
+ {
+ // persist the property data
+ var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, publishedVersionId, entity.Properties, LanguageRepository, out edited, out editedCultures);
+ foreach (var propertyDataDto in propertyDataDtos)
+ {
+ Database.Insert(propertyDataDto);
+ }
+ // TODO: we can speed this up: Use BulkInsert and then do one SELECT to re-retrieve the property data inserted with assigned IDs.
+ // This is a perfect thing to benchmark with Benchmark.NET to compare perf between Nuget releases.
+ }
+
+ ///
+ /// Used to atomically replace the property values for the entity version specified
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+
+ protected void ReplacePropertyValues(TEntity entity, int versionId, int publishedVersionId, out bool edited, out HashSet editedCultures)
+ {
+ // Replace the property data.
+ // Lookup the data to update with a UPDLOCK (using ForUpdate()) this is because we need to be atomic
+ // and handle DB concurrency. Doing a clear and then re-insert is prone to concurrency issues.
+
+ var propDataSql = SqlContext.Sql().Select("*").From().Where(x => x.VersionId == versionId).ForUpdate();
+ var existingPropData = Database.Fetch(propDataSql);
+ var propertyTypeToPropertyData = new Dictionary<(int propertyTypeId, int versionId, int? languageId, string segment), PropertyDataDto>();
+ var existingPropDataIds = new List();
+ foreach (var p in existingPropData)
+ {
+ existingPropDataIds.Add(p.Id);
+ propertyTypeToPropertyData[(p.PropertyTypeId, p.VersionId, p.LanguageId, p.Segment)] = p;
+ }
+ var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, publishedVersionId, entity.Properties, LanguageRepository, out edited, out editedCultures);
+
+ foreach (var propertyDataDto in propertyDataDtos)
+ {
+
+ // Check if this already exists and update, else insert a new one
+ if (propertyTypeToPropertyData.TryGetValue((propertyDataDto.PropertyTypeId, propertyDataDto.VersionId, propertyDataDto.LanguageId, propertyDataDto.Segment), out var propData))
+ {
+ propertyDataDto.Id = propData.Id;
+ Database.Update(propertyDataDto);
+ }
+ else
+ {
+ // TODO: we can speed this up: Use BulkInsert and then do one SELECT to re-retrieve the property data inserted with assigned IDs.
+ // This is a perfect thing to benchmark with Benchmark.NET to compare perf between Nuget releases.
+ Database.Insert(propertyDataDto);
+ }
+
+ // track which ones have been processed
+ existingPropDataIds.Remove(propertyDataDto.Id);
+ }
+ // For any remaining that haven't been processed they need to be deleted
+ if (existingPropDataIds.Count > 0)
+ {
+ Database.Execute(SqlContext.Sql().Delete().WhereIn(x => x.Id, existingPropDataIds));
+ }
+
+ }
+
private class NodeIdKey
{
[Column("id")]
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs
index 7781e2e38a..90774e4c0b 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs
@@ -303,7 +303,8 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
SortOrder = dto.SortOrder,
ValidationRegExp = dto.ValidationRegExp,
ValidationRegExpMessage = dto.ValidationRegExpMessage,
- Variations = (ContentVariation)dto.Variations
+ Variations = (ContentVariation)dto.Variations,
+ LabelOnTop = dto.LabelOnTop
};
}
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs
index 359b967dab..483f2393af 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepository.cs
@@ -134,7 +134,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
if (objectTypes.Any())
{
- sql = sql.Where("umbracoNode.nodeObjectType IN (@objectTypes)", objectTypes);
+ sql = sql.Where("umbracoNode.nodeObjectType IN (@objectTypes)", new { objectTypes = objectTypes });
}
return Database.Fetch(sql);
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs
index 357798a8a9..ed0c288691 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs
@@ -448,8 +448,14 @@ AND umbracoNode.id <> @id",
// The composed property is only considered segment variant when the base content type is also segment variant.
// Example: Culture variant content type with a Culture+Segment variant property type will become ContentVariation.Culture
var target = newContentTypeVariation & composedPropertyType.Variations;
+ // Determine the previous variation
+ // We have to compare with the old content type variation because the composed property might already have changed
+ // Example: A property with variations in an element type with variations is used in a document without
+ // when you enable variations the property has already enabled variations from the element type,
+ // but it's still a change from nothing because the document did not have variations, but it does now.
+ var from = oldContentTypeVariation & composedPropertyType.Variations;
- propertyTypeVariationChanges[composedPropertyType.Id] = (composedPropertyType.Variations, target);
+ propertyTypeVariationChanges[composedPropertyType.Id] = (from, target);
}
}
@@ -1184,7 +1190,7 @@ AND umbracoNode.id <> @id",
{
// first clear dependencies
Database.Delete("WHERE propertyTypeId = @Id", new { Id = propertyTypeId });
- Database.Delete("WHERE propertytypeid = @Id", new { Id = propertyTypeId });
+ Database.Delete("WHERE propertyTypeId = @Id", new { Id = propertyTypeId });
// then delete the property type
Database.Delete("WHERE contentTypeId = @Id AND id = @PropertyTypeId",
@@ -1312,7 +1318,7 @@ WHERE cmsContentType." + aliasColumn + @" LIKE @pattern",
///
public bool HasContainerInPath(string contentPath)
{
- var ids = contentPath.Split(',').Select(int.Parse).ToArray();
+ var ids = contentPath.Split(Constants.CharArrays.Comma).Select(int.Parse).ToArray();
return HasContainerInPath(ids);
}
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DataTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DataTypeRepository.cs
index 9ccf6e9623..cba030e17a 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/DataTypeRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DataTypeRepository.cs
@@ -303,7 +303,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
private string EnsureUniqueNodeName(string nodeName, int id = 0)
{
- var template = SqlContext.Templates.Get("Umbraco.Core.DataTypeDefinitionRepository.EnsureUniqueNodeName", tsql => tsql
+ var template = SqlContext.Templates.Get(Constants.SqlTemplates.DataTypeRepository.EnsureUniqueNodeName, tsql => tsql
.Select(x => Alias(x.NodeId, "id"), x => Alias(x.Text, "name"))
.From()
.Where(x => x.NodeObjectType == SqlTemplate.Arg("nodeObjectType")));
diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs
index db1e2b350d..09d41a49a0 100644
--- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs
@@ -610,17 +610,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
Database.Insert(documentVersionDto);
}
- // replace the property data (rather than updating)
+ // replace the property data (rather than updating)
// only need to delete for the version that existed, the new version (if any) has no property data yet
- var versionToDelete = publishing ? entity.PublishedVersionId : entity.VersionId;
- var deletePropertyDataSql = Sql().Delete().Where(x => x.VersionId == versionToDelete);
- Database.Execute(deletePropertyDataSql);
-
- // insert property data
- var propertyDataDtos = PropertyFactory.BuildDtos(entity.ContentType.Variations, entity.VersionId, publishing ? entity.PublishedVersionId : 0,
- entity.Properties, LanguageRepository, out var edited, out var editedCultures);
- foreach (var propertyDataDto in propertyDataDtos)
- Database.Insert(propertyDataDto);
+ var versionToDelete = publishing ? entity.PublishedVersionId : entity.VersionId;
+ // insert property data
+ ReplacePropertyValues(entity, versionToDelete, publishing ? entity.PublishedVersionId : 0, out var edited, out var editedCultures);
// if !publishing, we may have a new name != current publish name,
// also impacts 'edited'
@@ -900,7 +894,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
if (content.ParentId == -1)
return content.Published;
- var ids = content.Path.Split(',').Skip(1).Select(int.Parse);
+ var ids = content.Path.Split(Constants.CharArrays.Comma).Skip(1).Select(int.Parse);
var sql = SqlContext.Sql()
.SelectCount(x => x.NodeId)
@@ -1017,6 +1011,37 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
Database.Execute(sql);
}
+ ///
+ public void ClearSchedule(DateTime date, ContentScheduleAction action)
+ {
+ var a = action.ToString();
+ var sql = Sql().Delete().Where(x => x.Date <= date && x.Action == a);
+ Database.Execute(sql);
+ }
+
+ private Sql GetSqlForHasScheduling(ContentScheduleAction action, DateTime date)
+ {
+ var template = SqlContext.Templates.Get("Umbraco.Core.DocumentRepository.GetSqlForHasScheduling", tsql => tsql
+ .SelectCount()
+ .From()
+ .Where(x => x.Action == SqlTemplate.Arg("action") && x.Date <= SqlTemplate.Arg("date")));
+
+ var sql = template.Sql(action.ToString(), date);
+ return sql;
+ }
+
+ public bool HasContentForExpiration(DateTime date)
+ {
+ var sql = GetSqlForHasScheduling(ContentScheduleAction.Expire, date);
+ return Database.ExecuteScalar