diff --git a/.editorconfig b/.editorconfig
index 2a15465c59..5f3b4d684a 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -1,10 +1,17 @@
-root=true
+# editorconfig.org
+# top-most EditorConfig file
+root = true
+
+# Default settings:
+# A newline ending every file
+# Use 4 spaces as indentation
[*]
-end_of_line = crlf
+insert_final_newline = true
+end_of_line = lf
indent_style = space
indent_size = 4
-trim_trailing_whitespace = true
-[*.{cs,cshtml,csx,vb,vbx,vbhtml,fs,fsx,txt,ps1,sql}]
-indent_size = 4
+# Trim trailing whitespace, limited support.
+# https://github.com/editorconfig/editorconfig/wiki/Property-research:-Trim-trailing-spaces
+trim_trailing_whitespace = true
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000000..a664be3a85
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,49 @@
+*.doc diff=astextplain
+*.DOC diff=astextplain
+*.docx diff=astextplain
+*.DOCX diff=astextplain
+*.dot diff=astextplain
+*.DOT diff=astextplain
+*.pdf diff=astextplain
+*.PDF diff=astextplain
+*.rtf diff=astextplain
+*.RTF diff=astextplain
+
+*.jpg binary
+*.png binary
+*.gif binary
+
+*.cs text=auto diff=csharp
+*.vb text=auto
+*.c text=auto
+*.cpp text=auto
+*.cxx text=auto
+*.h text=auto
+*.hxx text=auto
+*.py text=auto
+*.rb text=auto
+*.java text=auto
+*.html text=auto
+*.htm text=auto
+*.css text=auto
+*.scss text=auto
+*.sass text=auto
+*.less text=auto
+*.js text=auto
+*.lisp text=auto
+*.clj text=auto
+*.sql text=auto
+*.php text=auto
+*.lua text=auto
+*.m text=auto
+*.asm text=auto
+*.erl text=auto
+*.fs text=auto
+*.fsx text=auto
+*.hs text=auto
+
+*.csproj text=auto merge=union
+*.vbproj text=auto merge=union
+*.fsproj text=auto merge=union
+*.dbproj text=auto merge=union
+*.sln text=auto eol=crlf merge=union
diff --git a/.gitignore b/.gitignore
index c5d51abd0d..e03ef6f408 100644
--- a/.gitignore
+++ b/.gitignore
@@ -136,6 +136,7 @@ src/Umbraco.Tests/media
tools/docfx/*
apidocs/_site/*
src/*/project.lock.json
+src/.idea/*
apidocs/api/*
build/docs.zip
@@ -149,4 +150,4 @@ src/PrecompiledWeb/*
build.out/
build.tmp/
build/hooks/
-build/temp/
\ No newline at end of file
+build/temp/
diff --git a/BUILD.md b/BUILD.md
index 9515e26654..30ea950778 100644
--- a/BUILD.md
+++ b/BUILD.md
@@ -14,6 +14,43 @@ By default, this builds the current version. It is possible to specify a differe
Valid version strings are defined in the `Set-UmbracoVersion` documentation below.
+## PowerShell Quirks
+
+There is a good chance that running `build.ps1` ends up in error, with messages such as
+
+>The file ...\build\build.ps1 is not digitally signed. You cannot run this script on the current system. For more information about running scripts and setting execution policy, see about_Execution_Policies.
+
+PowerShell has *Execution Policies* that may prevent the script from running. You can check the current policies with:
+
+ PS> Get-ExecutionPolicy -List
+
+ Scope ExecutionPolicy
+ ----- ---------------
+ MachinePolicy Undefined
+ UserPolicy Undefined
+ Process Undefined
+ CurrentUser Undefined
+ LocalMachine RemoteSigned
+
+Policies can be `Restricted`, `AllSigned`, `RemoteSigned`, `Unrestricted` and `Bypass`. Scopes can be `MachinePolicy`, `UserPolicy`, `Process`, `CurrentUser`, `LocalMachine`. You need the current policy to be `RemoteSigned`—as long as it is `Undefined`, the script cannot run. You can change the current user policy with:
+
+ PS> Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
+
+Alternatively, you can do it at machine level, from within an elevated PowerShell session:
+
+ PS> Set-ExecutionPolicy -Scope LocalMachine -ExecutionPolicy RemoteSigned
+
+And *then* the script should run. It *might* however still complain about executing scripts, with messages such as:
+
+>Security warning - Run only scripts that you trust. While scripts from the internet can be useful, this script can potentially harm your computer. If you trust this script, use the Unblock-File cmdlet to allow the script to run without this warning message. Do you want to run ...\build\build.ps1?
+[D] Do not run [R] Run once [S] Suspend [?] Help (default is "D"):
+
+This is usually caused by the scripts being *blocked*. And that usually happens when the source code has been downloaded as a Zip file. When Windows downloads Zip files, they are marked as *blocked* (technically, they have a Zone.Identifier alternate data stream, with a value of "3" to indicate that they were downloaded from the Internet). And when such a Zip file is un-zipped, each and every single file is also marked as blocked.
+
+The best solution is to unblock the Zip file before un-zipping: right-click the files, open *Properties*, and there should be a *Unblock* checkbox at the bottom of the dialog. If, however, the Zip file has already been un-zipped, it is possible to recursively unblock all files from PowerShell with:
+
+ PS> Get-ChildItem -Recurse *.* | Unblock-File
+
## Notes
Git might have issues dealing with long file paths during build. You may want/need to enable `core.longpaths` support (see [this page](https://github.com/msysgit/msysgit/wiki/Git-cannot-create-a-file-or-directory-with-a-long-path) for details).
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000000..3baa5dbe66
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,80 @@
+# Code of Conduct
+
+## 1. Purpose
+
+A primary goal of Umbraco CMS is to be inclusive to the largest number of contributors, with the most varied and diverse backgrounds possible. As such, we are committed to providing a friendly, safe and welcoming environment for all, regardless of gender, sexual orientation, ability, ethnicity, socioeconomic status, and religion (or lack thereof).
+
+This code of conduct outlines our expectations for all those who participate in our community, as well as the consequences for unacceptable behavior.
+
+We invite all those who participate in Umbraco CMS to help us create safe and positive experiences for everyone.
+
+## 2. Open Source Citizenship
+
+A supplemental goal of this Code of Conduct is to increase open source citizenship by encouraging participants to recognize and strengthen the relationships between our actions and their effects on our community.
+
+Communities mirror the societies in which they exist and positive action is essential to counteract the many forms of inequality and abuses of power that exist in society.
+
+If you see someone who is making an extra effort to ensure our community is welcoming, friendly, and encourages all participants to contribute to the fullest extent, we want to know.
+
+## 3. Expected Behavior
+
+The following behaviors are expected and requested of all community members:
+
+* Participate in an authentic and active way. In doing so, you contribute to the health and longevity of this community.
+* Exercise consideration and respect in your speech and actions.
+* Attempt collaboration before conflict.
+* Refrain from demeaning, discriminatory, or harassing behavior and speech.
+* Be mindful of your surroundings and of your fellow participants. Alert community leaders if you notice a dangerous situation, someone in distress, or violations of this Code of Conduct, even if they seem inconsequential.
+* Remember that community event venues may be shared with members of the public; please be respectful to all patrons of these locations.
+
+## 4. Unacceptable Behavior
+
+The following behaviors are considered harassment and are unacceptable within our community:
+
+* Violence, threats of violence or violent language directed against another person.
+* Sexist, racist, homophobic, transphobic, ableist or otherwise discriminatory jokes and language.
+* Posting or displaying sexually explicit or violent material.
+* Posting or threatening to post other people’s personally identifying information ("doxing").
+* Personal insults, particularly those related to gender, sexual orientation, race, religion, or disability.
+* Inappropriate photography or recording.
+* Inappropriate physical contact. You should have someone’s consent before touching them.
+* Unwelcome sexual attention. This includes, sexualized comments or jokes; inappropriate touching, groping, and unwelcomed sexual advances.
+* Deliberate intimidation, stalking or following (online or in person).
+* Advocating for, or encouraging, any of the above behavior.
+* Sustained disruption of community events, including talks and presentations.
+
+## 5. Consequences of Unacceptable Behavior
+
+Unacceptable behavior from any community member, including sponsors and those with decision-making authority, will not be tolerated.
+
+Anyone asked to stop unacceptable behavior is expected to comply immediately.
+
+If a community member engages in unacceptable behavior, the community organizers may take any action they deem appropriate, up to and including a temporary ban or permanent expulsion from the community without warning (and without refund in the case of a paid event).
+
+## 6. Reporting Guidelines
+
+If you are subject to or witness unacceptable behavior, or have any other concerns, please notify a community organizer as soon as possible. Please contact Sebastiaan Janssen - [sj@umbraco.dk](mailto:sj@umbraco.dk).
+
+Additionally, community organizers are available to help community members engage with local law enforcement or to otherwise help those experiencing unacceptable behavior feel safe. In the context of in-person events, organizers will also provide escorts as desired by the person experiencing distress.
+
+## 7. Addressing Grievances
+
+If you feel you have been falsely or unfairly accused of violating this Code of Conduct, you should notify Umbraco with a concise description of your grievance. Your grievance will be handled in accordance with our existing governing policies.
+
+## 8. Scope
+
+We expect all community participants (contributors, paid or otherwise; sponsors; and other guests) to abide by this Code of Conduct in all community venues–online and in-person–as well as in all one-on-one communications pertaining to community business.
+
+This code of conduct and its related procedures also applies to unacceptable behavior occurring outside the scope of community activities when such behavior has the potential to adversely affect the safety and well-being of community members.
+
+## 9. Contact info
+
+Sebastiaan Janssen - [sj@umbraco.dk](mailto:sj@umbraco.dk)
+
+## 10. License and attribution
+
+This Code of Conduct is distributed under a [Creative Commons Attribution-ShareAlike license](http://creativecommons.org/licenses/by-sa/3.0/).
+
+Portions of text derived from the [Django Code of Conduct](https://www.djangoproject.com/conduct/) and the [Geek Feminism Anti-Harassment Policy](http://geekfeminism.wikia.com/wiki/Conference_anti-harassment/Policy).
+
+Retrieved on November 22, 2016 from [http://citizencodeofconduct.org/](http://citizencodeofconduct.org/)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000000..0e87f9824d
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,207 @@
+# Contributing to Umbraco CMS
+
+👍🎉 First off, thanks for taking the time to contribute! 🎉👍
+
+The following is a set of guidelines for contributing to Umbraco CMS.
+
+These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.
+
+Remember, we're a friendly bunch and are happy with whatever contribution you might provide. Below are guidelines for success that we've gathered over the years. If you choose to ignore them then we still love you 💖.
+
+#### Table Of Contents
+
+[Code of Conduct](#code-of-conduct)
+
+[How Can I Contribute?](#how-can-i-contribute)
+ * [Reporting Bugs](#reporting-bugs)
+ * [Suggesting Enhancements](#suggesting-enhancements)
+ * [Your First Code Contribution](#your-first-code-contribution)
+ * [Pull Requests](#pull-requests)
+
+[Styleguides](#styleguides)
+
+[What should I know before I get started?](#what-should-i-know-before-i-get-started)
+ * [Working with the source code](#working-with-the-source-code)
+ * [What branch should I target for my contributions?](#what-branch-should-i-target-for-my-contributions)
+ * [Building Umbraco from source code](#building-umbraco-from-source-code)
+ * [Keeping your Umbraco fork in sync with the main repository](#keeping-your-umbraco-fork-in-sync-with-the-main-repository)
+
+[How do I even begin?](#how-do-i-even-begin)
+
+[Problems?](#problems)
+
+[Credits](#credits)
+
+## Code of Conduct
+
+This project and everyone participating in it is governed by the [our Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to [Sebastiaan Janssen - sj@umbraco.dk](mailto:sj@umbraco.dk).
+
+## How Can I Contribute?
+
+### Reporting Bugs
+This section guides you through submitting a bug report for Umbraco CMS. Following these guidelines helps maintainers and the community understand your report 📝, reproduce the behavior 💻 💻, and find related reports 🔎.
+
+Before creating bug reports, please check [this list](#before-submitting-a-bug-report) as you might find out that you don't need to create one. When you are creating a bug report, please [include as many details as possible](#how-do-i-submit-a-good-bug-report). Fill out [the required template](ISSUE_TEMPLATE.md), the information it asks for helps us resolve issues faster.
+
+> **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one.
+
+##### Before Submitting A Bug Report
+
+ * Most importantly, check **if you can reproduce the problem** in the [latest version of Umbraco](https://our.umbraco.org/download/). We might have already fixed your particular problem.
+ * It also helps tremendously to check if the issue you're experiencing is present in **a clean install** of the Umbraco version you're currently using. Custom code can have side-effects that don't occur in a clean install.
+ * **Use the Google**. Whatever you're experiencing, Google it plus "Umbraco" - usually you can get some pretty good hints from the search results, including open issues and further troubleshooting hints.
+ * If you do find and existing issue has **and the issue is still open**, add a comment to the existing issue if you have additional information. If you have the same problem and no new info to add, just "star" the issue.
+
+Explain the problem and include additional details to help maintainers reproduce the problem. The following is a long description which we've boiled down into a few very simple question in the issue tracker when you create a new issue. We're listing the following hints to indicate that the most successful reports usually have a lot of this ground covered:
+
+ * **Use a clear and descriptive title** for the issue to identify the problem.
+ * **Describe the exact steps which reproduce the problem** in as many details as possible. For example, start by explaining which steps you took in the backoffice to get to a certain undesireable result, e.g. you created a document type, inherting 3 levels deep, added a certain datatype, tried to save it and you got an error.
+ * **Provide specific examples to demonstrate the steps**. If you wrote some code, try to provide a code sample as specific as possible to be able to reproduce the behavior.
+ * **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior.
+ * **Explain which behavior you expected to see instead and why.**
+
+Provide more context by answering these questions:
+
+ * **Can you reproduce the problem** when `debug="false"` in your `web.config` file?
+ * **Did the problem start happening recently** (e.g. after updating to a new version of Umbraco) or was this always a problem?
+ * **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens.
+
+Include details about your configuration and environment:
+
+ * **Which version of Umbraco are you using?**
+ * **What is the environment you're using Umbraco in?** Is this a problem on your local machine or on a server. Tell us about your configuration: Windows version, IIS/IISExpress, database type, etc.
+ * **Which packages do you have installed?**
+
+### Suggesting Enhancements
+
+This section guides you through submitting an enhancement suggestion for Atom, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion 📝 and find related suggestions 🔎.
+
+Most of the suggestions in the [reporting bugs](#reporting-bugs) section also count for suggesting enhancements.
+
+Some additional hints that may be helpful:
+
+ * **Include screenshots and animated GIFs** which help you demonstrate the steps or point out the part of Umbraco which the suggestion is related to.
+ * **Explain why this enhancement would be useful to most Umbraco users** and isn't something that can or should be implemented as a [community package](https://our.umbraco.org/projects/).
+
+### Your First Code Contribution
+
+Unsure where to begin contributing to Umbraco? You can start by looking through [these `Up for grabs` and issues](http://issues.umbraco.org/issues/U4?q=%28project%3A+%7BU4%7D+Difficulty%3A+%7BVery+Easy%7D+%23Easy+%23Unresolved+Priority%3A+Normal+%23Major+%23Show-stopper+State%3A+-%7BIn+Progress%7D+sort+by%3A+votes+Affected+versions%3A+-6.*+Affected+versions%3A+-4.*%29+OR+%28tag%3A+%7BUp+For+Grabs%7D+%23Unresolved+%29).
+
+The issue list is sorted by total number of upvotes. While not perfect, number of upvotes is a reasonable proxy for impact a given change will have.
+
+### Pull Requests
+
+The most successful pull requests usually look a like this:
+
+ * Fill in the required template
+ * Include screenshots and animated GIFs in your pull request whenever possible.
+ * Unit tests, while optional are awesome, thank you!
+ * New code is commented with documentation from which [the reference documentation](https://our.umbraco.org/documentation/Reference/) is generated
+
+Again, these are guidelines, not strict requirements.
+
+## Styleguides
+
+To be honest, we don't like rules very much. We trust you have the best of intentions and we encourage you to create working code. If it doesn't look perfect then we'll happily help clean it up.
+
+That said, the Umbraco development team likes to follow the hints that ReSharper gives us (no problem if you don't have this installed) and we've added a `.editorconfig` file so that Visual Studio knows what to do with whitespace, line endings, etc.
+
+## What should I know before I get started?
+
+### Working with the source code
+
+Some parts of our source code is over 10 years old now. And when we say "old", we mean "mature" of course!
+
+There's two big areas that you should know about:
+
+ 1. The Umbraco backoffice is a extensible AngularJS app and requires you to run a `gulp dev` command while you're working with it, so changes are copied over to the appropriate directories and you can refresh your browser to view the results of your changes.
+ You may need to run the following commands to set up gulp properly:
+ ```
+ npm cache clean
+ npm install -g bower
+ npm install -g gulp
+ npm install -g gulp-cli
+ npm install
+ gulp build
+ ```
+ 2. "The rest" is a C# based codebase, with some traces of our WebForms past but mostly ASP.NET MVC based these days. You can make changes, build them in Visual Studio, and hit `F5` to see the result.
+
+To find the general areas of something you're looking to fix or improve, have a look at the following two parts of the API documentation.
+
+ * [The AngularJS based backoffice files](https://our.umbraco.org/apidocs/ui/#/api) (to be found in `src\Umbraco.Web.UI.Client\src`)
+ * [The rest](https://our.umbraco.org/apidocs/csharp/)
+
+### What branch should I target for my contributions?
+
+We like to use [Gitflow as much as possible](https://jeffkreeftmeijer.com/git-flow/), don't worry if you are not familiar with it. The most important thing you need to know is that when you fork the Umbraco repository, the default branch is set to something, usually `dev-v7`. Whatever the default is, that's where we'd like you to target your contributions.
+
+
+
+### Building Umbraco from source code
+
+The easiest way to get started is to run `build.bat` which will build both the backoffice (also known as "Belle") and the Umbraco core. You can then easily start debugging from Visual Studio, or if you need to debug Belle you can run `gulp dev` in `src\Umbraco.Web.UI.Client`. See [this page](BUILD.md) for more details.
+
+Alternatively, you can open `src\umbraco.sln` in Visual Studio 2017 ([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.
+
+
+
+After this build completes, you should be able to hit `F5` in Visual Studio to build and run the project. A IISExpress webserver will start and the Umbraco installer will pop up in your browser, follow the directions there to get a working Umbraco install up and running.
+
+### Keeping your Umbraco fork in sync with the main repository
+
+We recommend you sync with our repository before you submit your pull request. That way, you can fix any potential merge conflicts and make our lives a little bit easier.
+
+Also, if you've submitted a pull request three weeks ago and want to work on something new, you'll want to get the latest code to build against of course.
+
+To sync your fork with this original one, you'll have to add the upstream url, you only have to do this once:
+
+```
+git remote add upstream https://github.com/umbraco/Umbraco-CMS.git
+```
+
+Then when you want to get the changes from the main repository:
+
+```
+git fetch upstream
+git rebase upstream/dev-v7
+```
+
+In this command we're syncing with the `dev-v7` branch, but you can of course choose another one if needed.
+
+(More info on how this works: [http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated](http://robots.thoughtbot.com/post/5133345960/keeping-a-git-fork-updated))
+
+## How do I even begin?
+
+Great question! The short version goes like this:
+
+ * **Fork** - create a fork of [`Umbraco-CMS` on GitHub](https://github.com/umbraco/Umbraco-CMS)
+
+ 
+
+ * **Clone** - when GitHub has created your fork, you can clone it in your favorite Git tool
+
+ 
+
+ * **Build** - build your fork of Umbraco locally as described in [building Umbraco from source code](#building-umbraco-from-source-code)
+ * **Change** - make your changes, experiment, have fun, explore and learn, and don't be afraid. We welcome all contributions and will happily give feedback
+ * **Commit** - done? Yay! 🎉 It is recommended to create a new branch now and name it after the issue you're fixing, we usually follow the format: `temp-U4-12345`. This means it's a temporary branch for the particular issue you're working on, in this case `U4-12345`
+ * **Push** - great, now you can push the changes up to your fork on GitHub
+ * **Create pull request** - exciting! You're ready to show us your changes (or not quite ready, you just need some feedback to progress). GitHub has picked up on the new branch you've pushed and will offer to create a Pull Request. Click that green button and away you go.
+
+ 
+
+The Umbraco development team can now start reviewing your proposed changes and give you feedback on them. If it's not perfect, we'll either fix up what we need or we can request you to make some additional changes.
+
+If you make the corrections we ask for in the same branch and push them to your fork again, the pull request automatically updates with the additional commit(s) so we can review it again. If all is well, we'll merge the code and your commits are forever part of Umbraco!
+
+Not all changes are wanted so on occassion we might close a PR without merging it. We will give you feedback why we can't accept your changes at this and we'll be nice about it, thanking you for spending your valueable time.
+
+Remember, if an issue is in the `Up for grabs` list or you've asked for some feedback before you send us a PR, your PR will not be closed as unwanted.
+
+## Problems?
+
+Did something not work as expected? Try leaving a note in the ["Contributing to Umbraco"](https://our.umbraco.org/forum/contributing-to-umbraco-cms/) forum, the team monitors that one closely!
+
+## Credits
+
+This contribution guide borrows heavily from the excellent work on [the Atom contribution guide](https://github.com/atom/atom/blob/master/CONTRIBUTING.md). A big [#h5yr](http://h5yr.com/) to them!
diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000000..db1e5c88bd
--- /dev/null
+++ b/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,11 @@
+### Prerequisites
+
+- [ ] I have written a descriptive pull-request title
+- [ ] I have linked this PR to an issue on the tracker at http://issues.umbraco.org
+
+### Description
+
+
+
+
+
diff --git a/README.md b/README.md
index 69aba2f06a..045c91fae8 100644
--- a/README.md
+++ b/README.md
@@ -1,49 +1,44 @@
Umbraco CMS
===========
-The friendliest, most flexible and fastest growing ASP.NET CMS used by more than 350,000 websites worldwide: [https://umbraco.com](https://umbraco.com)
+The friendliest, most flexible and fastest growing ASP.NET CMS used by more than 443,000 websites worldwide: [https://umbraco.com](https://umbraco.com)
[](https://vimeo.com/172382998/)
-## Umbraco CMS ##
+## Umbraco CMS
Umbraco is a free open source Content Management System built on the ASP.NET platform. Our mission is to help you deliver delightful digital experiences by making Umbraco friendly, simpler and social.
-
-## Building Umbraco from source ##
-
-The easiest way to get started is to run `build/build.bat` which will build both the backoffice (also known as "Belle") and the Umbraco core. You can then easily start debugging from Visual Studio, or if you need to debug Belle you can run `gulp dev` in `src\Umbraco.Web.UI.Client`.
-
-Note that you can always [download a nightly build](http://nightly.umbraco.org/?container=umbraco-750) so you don't have to build the code yourself.
-
-## Watch an introduction video ##
+## Watch an introduction video
[](https://umbraco.tv/videos/umbraco-v7/content-editor/basics/introduction/cms-explanation/)
-## Umbraco - The Friendly CMS ##
+## Umbraco - The Friendly CMS
For the first time on the Microsoft platform, there is a free user and developer friendly CMS that makes it quick and easy to create websites - or a breeze to build complex web applications. Umbraco has award-winning integration capabilities and supports ASP.NET MVC or Web Forms, including User and Custom Controls, out of the box.
Umbraco is not only loved by developers, but is a content editors dream. Enjoy intuitive editing tools, media management, responsive views and approval workflows to send your content live.
-Used by more than 350,000 active websites including Carlsberg, Segway, Amazon and Heinz and **The Official ASP.NET and IIS.NET website from Microsoft** ([https://asp.net](https://asp.net) / [https://iis.net](https://iis.net)), you can be sure that the technology is proven, stable and scales. Backed by the team at Umbraco HQ, and supported by a dedicated community of over 200,000 craftspeople globally, you can trust that Umbraco is a safe choice and is here to stay.
+Used by more than 443,000 active websites including Carlsberg, Segway, Amazon and Heinz and **The Official ASP.NET and IIS.NET website from Microsoft** ([https://asp.net](https://asp.net) / [https://iis.net](https://iis.net)), you can be sure that the technology is proven, stable and scales. Backed by the team at Umbraco HQ, and supported by a dedicated community of over 220,000 craftspeople globally, you can trust that Umbraco is a safe choice and is here to stay.
To view more examples, please visit [https://umbraco.com/why-umbraco/#caseStudies](https://umbraco.com/why-umbraco/#caseStudies)
-## Why Open Source? ##
+## Why Open Source?
As an Open Source platform, Umbraco is more than just a CMS. We are transparent with our roadmap for future versions, our incremental sprint planning notes are publicly accessible and community contributions and packages are available for all to use.
-## Downloading ##
+## Trying out Umbraco CMS
-The downloadable Umbraco releases live at [https://our.umbraco.org/download](https://our.umbraco.org/download).
+[Umbraco Cloud](https://umbraco.com) is the easiest and fastest way to use Umbraco yet with full support for all your custom .NET code and intergrations. You're up and running in less than a minute and your life will be made easier with automated upgrades and a built-in deployment engine. We offer a free 14 day trial, no credit card needed.
-## Forums ##
+If you want to DIY you can [download Umbraco](https://our.umbraco.org/download) either as a ZIP file or via NuGet. It's the same version of Umbraco CMS that powers Umbraco Xloud, but you'll need to find a place to host yourself and handling deployments and upgrades is all down to you.
-Peer-to-peer support is available 24/7 at the community forum on [https://our.umbraco.org](https://our.umbraco.org).
+## Community
-## Contribute to Umbraco ##
+Our friendly community is available 24/7 at the community hub we call ["Our Umbraco"](https://our.umbraco.org). Our Umbraco feature forums for questions and answers, documentation, downloadable plugins for Umbraco and a rich collection of community resources.
-Umbraco is contribution focused and community driven. If you want to contribute back to Umbraco please check out our [guide to contributing](https://our.umbraco.org/contribute).
+## Contribute to Umbraco
-## Found a bug? ##
+Umbraco is contribution focused and community driven. If you want to contribute back to Umbraco please check out our [guide to contributing](CONTRIBUTING.md).
+
+## Found a bug?
Another way you can contribute to Umbraco is by providing issue reports. For information on how to submit an issue report refer to our [online guide for reporting issues](https://our.umbraco.org/contribute/report-an-issue-or-request-a-feature).
diff --git a/build/Azure/azuregalleryrelease.ps1 b/build/Azure/azuregalleryrelease.ps1
new file mode 100644
index 0000000000..502ca3010e
--- /dev/null
+++ b/build/Azure/azuregalleryrelease.ps1
@@ -0,0 +1,59 @@
+Param(
+ [string]$GitHubPersonalAccessToken,
+ [string]$Directory
+)
+$workingDirectory = $Directory
+CD $workingDirectory
+
+# Clone repo
+$fullGitUrl = "https://$env:GIT_URL/$env:GIT_REPOSITORYNAME.git"
+git clone $fullGitUrl 2>&1 | % { $_.ToString() }
+
+# Remove everything so that unzipping the release later will update everything
+# Don't remove the readme file nor the git directory
+Write-Host "Cleaning up git directory before adding new version"
+Remove-Item -Recurse $workingDirectory\$env:GIT_REPOSITORYNAME\* -Exclude README.md,.git
+
+# Find release zip
+$zipsDir = "$workingDirectory\$env:BUILD_DEFINITIONNAME\zips"
+$pattern = "UmbracoCms.([0-9]{1,2}.[0-9]{1,3}.[0-9]{1,3}).zip"
+Write-Host "Searching for Umbraco release files in $workingDirectory\$zipsDir for a file with pattern $pattern"
+$file = (Get-ChildItem $zipsDir | Where-Object { $_.Name -match "$pattern" })
+
+if($file)
+{
+ # Get release name
+ $version = [regex]::Match($file.Name, $pattern).captures.groups[1].value
+ $releaseName = "Umbraco $version"
+ Write-Host "Found $releaseName"
+
+ # Unzip into repository to update release
+ Add-Type -AssemblyName System.IO.Compression.FileSystem
+ Write-Host "Unzipping $($file.FullName) to $workingDirectory\$env:GIT_REPOSITORYNAME"
+ [System.IO.Compression.ZipFile]::ExtractToDirectory("$($file.FullName)", "$workingDirectory\$env:GIT_REPOSITORYNAME")
+
+ # Telling git who we are
+ git config --global user.email "coffee@umbraco.com" 2>&1 | % { $_.ToString() }
+ git config --global user.name "Umbraco HQ" 2>&1 | % { $_.ToString() }
+
+ # Commit
+ CD $env:GIT_REPOSITORYNAME
+ Write-Host "Committing Umbraco $version Release from Build Output"
+
+ git add . 2>&1 | % { $_.ToString() }
+ git commit -m " Release $releaseName from Build Output" 2>&1 | % { $_.ToString() }
+
+ # Tag the release
+ git tag -a "v$version" -m "v$version"
+
+ # Push release to master
+ $fullGitAuthUrl = "https://$($env:GIT_USERNAME):$GitHubPersonalAccessToken@$env:GIT_URL/$env:GIT_REPOSITORYNAME.git"
+ git push $fullGitAuthUrl 2>&1 | % { $_.ToString() }
+
+ #Push tag to master
+ git push $fullGitAuthUrl --tags 2>&1 | % { $_.ToString() }
+}
+else
+{
+ Write-Error "Umbraco release file not found, searched in $workingDirectory\$zipsDir for a file with pattern $pattern - cancelling"
+}
diff --git a/build/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec
index cbab1536b0..7775aaabbe 100644
--- a/build/NuSpecs/UmbracoCms.Core.nuspec
+++ b/build/NuSpecs/UmbracoCms.Core.nuspec
@@ -24,10 +24,8 @@
-
-
-
+
@@ -36,8 +34,8 @@
-
-
+
+
diff --git a/build/NuSpecs/UmbracoCms.nuspec b/build/NuSpecs/UmbracoCms.nuspec
index 1b2ad2d6bf..a8fb9cf0c3 100644
--- a/build/NuSpecs/UmbracoCms.nuspec
+++ b/build/NuSpecs/UmbracoCms.nuspec
@@ -31,6 +31,7 @@
+
diff --git a/build/NuSpecs/tools/ReadmeUpgrade.txt b/build/NuSpecs/tools/ReadmeUpgrade.txt
index e85b22a902..2b6da733a1 100644
--- a/build/NuSpecs/tools/ReadmeUpgrade.txt
+++ b/build/NuSpecs/tools/ReadmeUpgrade.txt
@@ -8,7 +8,7 @@
----------------------------------------------------
-*** IMPORTANT NOTICE FOR 7.7 UPGRADES ***
+*** IMPORTANT NOTICE FOR UPGRADES FROM VERSIONS BELOW 7.7.0 ***
Be sure to read the version specific upgrade information before proceeding:
https://our.umbraco.org/documentation/Getting-Started/Setup/Upgrading/version-specific#version-7-7-0
diff --git a/build/NuSpecs/tools/install.ps1 b/build/NuSpecs/tools/install.ps1
index 0e62fb0749..592dc951e0 100644
--- a/build/NuSpecs/tools/install.ps1
+++ b/build/NuSpecs/tools/install.ps1
@@ -98,10 +98,51 @@ if ($project) {
$umbracoUIXMLDestination = Join-Path $projectPath "Umbraco\Config\Create\UI.xml"
Copy-Item $umbracoUIXMLSource $umbracoUIXMLDestination -Force
} else {
+ # This part only runs for upgrades
+
$upgradeViewSource = Join-Path $umbracoFolderSource "Views\install\*"
$upgradeView = Join-Path $umbracoFolder "Views\install\"
Write-Host "Copying2 ${upgradeViewSource} to ${upgradeView}"
Copy-Item $upgradeViewSource $upgradeView -Force
+
+ Try
+ {
+ # Disable tours for upgrades, presumably Umbraco experience is already available
+ $umbracoSettingsConfigPath = Join-Path $configFolder "umbracoSettings.config"
+ $content = (Get-Content $umbracoSettingsConfigPath).Replace('','')
+ # Saves with UTF-8 encoding without BOM which makes sure Umbraco can still read it
+ # Reference: https://stackoverflow.com/a/32951824/5018
+ [IO.File]::WriteAllLines($umbracoSettingsConfigPath, $content)
+ }
+ Catch
+ {
+ # Not a big problem if this fails, let it go
+ }
+
+ Try
+ {
+ $uiXmlConfigPath = Join-Path $umbracoFolder -ChildPath "Config" | Join-Path -ChildPath "create" | Join-Path -ChildPath "UI.xml"
+ $uiXmlFile = Join-Path $umbracoFolder -ChildPath "Config" | Join-Path -ChildPath "create" | Join-Path -ChildPath "UI.xml"
+
+ $uiXml = New-Object System.Xml.XmlDocument
+ $uiXml.PreserveWhitespace = $true
+
+ $uiXml.Load($uiXmlFile)
+ $createExists = $uiXml.SelectNodes("//nodeType[@alias='macros']/tasks/create")
+
+ if($createExists.Count -eq 0)
+ {
+ $macrosTasksNode = $uiXml.SelectNodes("//nodeType[@alias='macros']/tasks")
+
+ #Creating:
+ $createNode = $uiXml.CreateElement("create")
+ $createNode.SetAttribute("assembly", "umbraco")
+ $createNode.SetAttribute("type", "macroTasks")
+ $macrosTasksNode.AppendChild($createNode)
+ $uiXml.Save($uiXmlFile)
+ }
+ }
+ Catch { }
}
$installFolder = Join-Path $projectPath "Install"
diff --git a/build/NuSpecs/tools/trees.config.install.xdt b/build/NuSpecs/tools/trees.config.install.xdt
index 65aa9c2b53..5a549e3fd8 100644
--- a/build/NuSpecs/tools/trees.config.install.xdt
+++ b/build/NuSpecs/tools/trees.config.install.xdt
@@ -43,7 +43,7 @@
xdt:Locator="Match(application,alias)"
xdt:Transform="InsertIfMissing" />
-
-
-
-
diff --git a/build/build.ps1 b/build/build.ps1
index ae4fcb679b..20eeb1a8ea 100644
--- a/build/build.ps1
+++ b/build/build.ps1
@@ -328,15 +328,7 @@
$this.CopyFiles("$src\Umbraco.Web.UI\umbraco\js", "*", "$tmp\WebApp\umbraco\js")
$this.CopyFiles("$src\Umbraco.Web.UI\umbraco\lib", "*", "$tmp\WebApp\umbraco\lib")
$this.CopyFiles("$src\Umbraco.Web.UI\umbraco\views", "*", "$tmp\WebApp\umbraco\views")
- $this.CopyFiles("$src\Umbraco.Web.UI\umbraco\preview", "*", "$tmp\WebApp\umbraco\preview")
-
- # prepare WebPI
- Write-Host "Prepare WebPI"
- $this.RemoveDirectory("$tmp\WebPi")
- mkdir "$tmp\WebPi" > $null
- mkdir "$tmp\WebPi\umbraco" > $null
- $this.CopyFiles("$tmp\WebApp", "*", "$tmp\WebPi\umbraco")
- $this.CopyFiles("$src\WebPi", "*", "$tmp\WebPi")
+ $this.CopyFiles("$src\Umbraco.Web.UI\umbraco\preview", "*", "$tmp\WebApp\umbraco\preview")
})
$ubuild.DefineMethod("PackageZip",
@@ -359,19 +351,7 @@
"$tmp\WebApp\*" `
"-x!dotless.Core.*" "-x!Content_Types.xml" "-x!*.pdb" "-x!Umbraco.Compat7.*" `
> $null
- if (-not $?) { throw "Failed to zip UmbracoCms." }
-
- Write-Host "Zip WebPI"
- &$this.BuildEnv.Zip a -r "$out\UmbracoCms.WebPI.$($this.Version.Semver).zip" "-x!*.pdb" `
- "$tmp\WebPi\*" `
- "-x!dotless.Core.*" "-x!Umbraco.Compat7.*" `
- > $null
- if (-not $?) { throw "Failed to zip UmbracoCms.WebPI." }
-
- # hash the webpi file
- Write-Host "Hash WebPI"
- $hash = $this.GetFileHash("$out\UmbracoCms.WebPI.$($this.Version.Semver).zip")
- Write-Output $hash | out-file "$out\webpihash.txt" -encoding ascii
+ if (-not $?) { throw "Failed to zip UmbracoCms." }
})
$ubuild.DefineMethod("PrepareBuild",
@@ -450,6 +430,12 @@
if ($this.OnError()) { return }
})
+ $ubuild.DefineMethod("PrepareAzureGallery",
+ {
+ Write-Host "Prepare Azure Gallery"
+ $this.CopyFile("$($this.SolutionRoot)\build\Azure\azuregalleryrelease.ps1", $this.BuildOutput)
+ })
+
$ubuild.DefineMethod("Build",
{
$this.PrepareBuild()
@@ -475,6 +461,8 @@
if ($this.OnError()) { return }
$this.PackageNuGet()
if ($this.OnError()) { return }
+ $this.PrepareAzureGallery()
+ if ($this.OnError()) { return }
})
# ################################################################
diff --git a/src/NuGet.Config b/src/NuGet.Config
index 2cb8d8dfbd..722f368692 100644
--- a/src/NuGet.Config
+++ b/src/NuGet.Config
@@ -2,7 +2,7 @@
-
+
\ No newline at end of file
diff --git a/src/Umbraco.Core/Auditing/AuditEventsComponent.cs b/src/Umbraco.Core/Auditing/AuditEventsComponent.cs
new file mode 100644
index 0000000000..57457f9241
--- /dev/null
+++ b/src/Umbraco.Core/Auditing/AuditEventsComponent.cs
@@ -0,0 +1,353 @@
+using System;
+using System.Linq;
+using System.Text;
+using System.Threading;
+using System.Web;
+using Umbraco.Core.Components;
+using Umbraco.Core.Events;
+using Umbraco.Core.Models;
+using Umbraco.Core.Models.Membership;
+using Umbraco.Core.Security;
+using Umbraco.Core.Services;
+using Umbraco.Core.Services.Implement;
+
+namespace Umbraco.Core.Auditing
+{
+ public sealed class AuditEventsComponent : UmbracoComponentBase, IUmbracoCoreComponent
+ {
+ private IAuditService _auditService;
+ private IUserService _userService;
+ private IEntityService _entityService;
+
+ private IUser CurrentPerformingUser
+ {
+ get
+ {
+ var identity = Thread.CurrentPrincipal?.GetUmbracoIdentity();
+ return identity == null
+ ? new User { Id = 0, Name = "SYSTEM", Email = "" }
+ : _userService.GetUserById(Convert.ToInt32(identity.Id));
+ }
+ }
+
+ private IUser GetPerformingUser(int userId)
+ {
+ var found = userId >= 0 ? _userService.GetUserById(userId) : null;
+ return found ?? new User {Id = 0, Name = "SYSTEM", Email = ""};
+ }
+
+ private string PerformingIp
+ {
+ get
+ {
+ var httpContext = HttpContext.Current == null ? (HttpContextBase) null : new HttpContextWrapper(HttpContext.Current);
+ var ip = httpContext.GetCurrentRequestIpAddress();
+ if (ip.ToLowerInvariant().StartsWith("unknown")) ip = "";
+ return ip;
+ }
+ }
+
+ public void Initialize(IAuditService auditService, IUserService userService, IEntityService entityService)
+ {
+ _auditService = auditService;
+ _userService = userService;
+ _entityService = entityService;
+
+ //BackOfficeUserManager.AccountLocked += ;
+ //BackOfficeUserManager.AccountUnlocked += ;
+ BackOfficeUserManager.ForgotPasswordRequested += OnForgotPasswordRequest;
+ BackOfficeUserManager.ForgotPasswordChangedSuccess += OnForgotPasswordChange;
+ BackOfficeUserManager.LoginFailed += OnLoginFailed;
+ //BackOfficeUserManager.LoginRequiresVerification += ;
+ BackOfficeUserManager.LoginSuccess += OnLoginSuccess;
+ BackOfficeUserManager.LogoutSuccess += OnLogoutSuccess;
+ BackOfficeUserManager.PasswordChanged += OnPasswordChanged;
+ BackOfficeUserManager.PasswordReset += OnPasswordReset;
+ //BackOfficeUserManager.ResetAccessFailedCount += ;
+
+ 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;
+ }
+
+ private string FormatEmail(IMember member)
+ {
+ return member == null ? string.Empty : member.Email.IsNullOrWhiteSpace() ? "" : $"<{member.Email}>";
+ }
+
+ private string FormatEmail(IUser user)
+ {
+ return user == null ? string.Empty : user.Email.IsNullOrWhiteSpace() ? "" : $"<{user.Email}>";
+ }
+
+ private void OnRemovedRoles(IMemberService sender, RolesEventArgs args)
+ {
+ var performingUser = CurrentPerformingUser;
+ var roles = string.Join(", ", args.Roles);
+ var members = sender.GetAllMembers(args.MemberIds).ToDictionary(x => x.Id, x => x);
+ foreach (var id in args.MemberIds)
+ {
+ members.TryGetValue(id, out var member);
+ _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp,
+ DateTime.UtcNow,
+ -1, $"Member {id} \"{member?.Name ?? "(unknown)"}\" {FormatEmail(member)}",
+ "umbraco/member/roles/removed", $"roles modified, removed {roles}");
+ }
+ }
+
+ private void OnAssignedRoles(IMemberService sender, RolesEventArgs args)
+ {
+ var performingUser = CurrentPerformingUser;
+ var roles = string.Join(", ", args.Roles);
+ var members = sender.GetAllMembers(args.MemberIds).ToDictionary(x => x.Id, x => x);
+ foreach (var id in args.MemberIds)
+ {
+ members.TryGetValue(id, out var member);
+ _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp,
+ DateTime.UtcNow,
+ -1, $"Member {id} \"{member?.Name ?? "(unknown)"}\" {FormatEmail(member)}",
+ "umbraco/member/roles/assigned", $"roles modified, assigned {roles}");
+ }
+ }
+
+ private void OnMemberExported(IMemberService sender, ExportedMemberEventArgs exportedMemberEventArgs)
+ {
+ var performingUser = CurrentPerformingUser;
+ var member = exportedMemberEventArgs.Member;
+
+ _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp,
+ DateTime.UtcNow,
+ -1, $"Member {member.Id} \"{member.Name}\" {FormatEmail(member)}",
+ "umbraco/member/exported", "exported member data");
+ }
+
+ private void OnSavedUserGroupWithUsers(IUserService sender, SaveEventArgs saveEventArgs)
+ {
+ var performingUser = CurrentPerformingUser;
+ foreach (var groupWithUser in saveEventArgs.SavedEntities)
+ {
+ var group = groupWithUser.UserGroup;
+
+ var dp = string.Join(", ", ((UserGroup)group).GetWereDirtyProperties());
+ var sections = ((UserGroup)group).WasPropertyDirty("AllowedSections")
+ ? string.Join(", ", group.AllowedSections)
+ : null;
+ var perms = ((UserGroup)group).WasPropertyDirty("Permissions")
+ ? string.Join(", ", group.Permissions)
+ : null;
+
+ var sb = new StringBuilder();
+ sb.Append($"updating {(string.IsNullOrWhiteSpace(dp) ? "(nothing)" : dp)};");
+ if (sections != null)
+ sb.Append($", assigned sections: {sections}");
+ if (perms != null)
+ {
+ if (sections != null)
+ sb.Append(", ");
+ sb.Append($"default perms: {perms}");
+ }
+
+ _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp,
+ DateTime.UtcNow,
+ -1, $"User Group {group.Id} \"{group.Name}\" ({group.Alias})",
+ "umbraco/user-group/save", $"{sb}");
+
+ // now audit the users that have changed
+
+ foreach (var user in groupWithUser.RemovedUsers)
+ {
+ _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp,
+ DateTime.UtcNow,
+ user.Id, $"User \"{user.Name}\" {FormatEmail(user)}",
+ "umbraco/user-group/save", $"Removed user \"{user.Name}\" {FormatEmail(user)} from group {group.Id} \"{group.Name}\" ({group.Alias})");
+ }
+
+ foreach (var user in groupWithUser.AddedUsers)
+ {
+ _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp,
+ DateTime.UtcNow,
+ user.Id, $"User \"{user.Name}\" {FormatEmail(user)}",
+ "umbraco/user-group/save", $"Added user \"{user.Name}\" {FormatEmail(user)} to group {group.Id} \"{group.Name}\" ({group.Alias})");
+ }
+ }
+ }
+
+ private void UserGroupPermissionAssigned(IUserService sender, SaveEventArgs saveEventArgs)
+ {
+ var performingUser = CurrentPerformingUser;
+ var perms = saveEventArgs.SavedEntities;
+ foreach (var perm in perms)
+ {
+ var group = sender.GetUserGroupById(perm.UserGroupId);
+ var assigned = string.Join(", ", perm.AssignedPermissions);
+ var entity = _entityService.Get(perm.EntityId);
+
+ _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp,
+ DateTime.UtcNow,
+ -1, $"User Group {group.Id} \"{group.Name}\" ({group.Alias})",
+ "umbraco/user-group/permissions-change", $"assigning {(string.IsNullOrWhiteSpace(assigned) ? "(nothing)" : assigned)} on id:{perm.EntityId} \"{entity.Name}\"");
+ }
+ }
+
+ private void OnSavedMember(IMemberService sender, SaveEventArgs saveEventArgs)
+ {
+ var performingUser = CurrentPerformingUser;
+ var members = saveEventArgs.SavedEntities;
+ foreach (var member in members)
+ {
+ var dp = string.Join(", ", ((Member) member).GetWereDirtyProperties());
+
+ _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp,
+ DateTime.UtcNow,
+ -1, $"Member {member.Id} \"{member.Name}\" {FormatEmail(member)}",
+ "umbraco/member/save", $"updating {(string.IsNullOrWhiteSpace(dp) ? "(nothing)" : dp)}");
+ }
+ }
+
+ private void OnDeletedMember(IMemberService sender, DeleteEventArgs deleteEventArgs)
+ {
+ var performingUser = CurrentPerformingUser;
+ var members = deleteEventArgs.DeletedEntities;
+ foreach (var member in members)
+ {
+ _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp,
+ DateTime.UtcNow,
+ -1, $"Member {member.Id} \"{member.Name}\" {FormatEmail(member)}",
+ "umbraco/member/delete", $"delete member id:{member.Id} \"{member.Name}\" {FormatEmail(member)}");
+ }
+ }
+
+ private void OnSavedUser(IUserService sender, SaveEventArgs saveEventArgs)
+ {
+ var performingUser = CurrentPerformingUser;
+ var affectedUsers = saveEventArgs.SavedEntities;
+ foreach (var affectedUser in affectedUsers)
+ {
+ var groups = affectedUser.WasPropertyDirty("Groups")
+ ? string.Join(", ", affectedUser.Groups.Select(x => x.Alias))
+ : null;
+
+ var dp = string.Join(", ", ((User)affectedUser).GetWereDirtyProperties());
+
+ _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp,
+ DateTime.UtcNow,
+ affectedUser.Id, $"User \"{affectedUser.Name}\" {FormatEmail(affectedUser)}",
+ "umbraco/user/save", $"updating {(string.IsNullOrWhiteSpace(dp) ? "(nothing)" : dp)}{(groups == null ? "" : "; groups assigned: " + groups)}");
+ }
+ }
+
+ private void OnDeletedUser(IUserService sender, DeleteEventArgs deleteEventArgs)
+ {
+ var performingUser = CurrentPerformingUser;
+ var affectedUsers = deleteEventArgs.DeletedEntities;
+ foreach (var affectedUser in affectedUsers)
+ _auditService.Write(performingUser.Id, $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}", PerformingIp,
+ DateTime.UtcNow,
+ affectedUser.Id, $"User \"{affectedUser.Name}\" {FormatEmail(affectedUser)}",
+ "umbraco/user/delete", "delete user");
+ }
+
+ private void OnLoginSuccess(object sender, EventArgs args)
+ {
+ if (args is IdentityAuditEventArgs identityArgs)
+ {
+ var performingUser = GetPerformingUser(identityArgs.PerformingUser);
+ WriteAudit(performingUser, identityArgs.AffectedUser, identityArgs.IpAddress, "umbraco/user/sign-in/login", "login success");
+ }
+ }
+
+ private void OnLogoutSuccess(object sender, EventArgs args)
+ {
+ if (args is IdentityAuditEventArgs identityArgs)
+ {
+ var performingUser = GetPerformingUser(identityArgs.PerformingUser);
+ WriteAudit(performingUser, identityArgs.AffectedUser, identityArgs.IpAddress, "umbraco/user/sign-in/logout", "logout success");
+ }
+ }
+
+ private void OnPasswordReset(object sender, EventArgs args)
+ {
+ if (args is IdentityAuditEventArgs identityArgs && identityArgs.PerformingUser >= 0)
+ {
+ WriteAudit(identityArgs.PerformingUser, identityArgs.AffectedUser, identityArgs.IpAddress, "umbraco/user/password/reset", "password reset");
+ }
+ }
+
+ private void OnPasswordChanged(object sender, EventArgs args)
+ {
+ if (args is IdentityAuditEventArgs identityArgs && identityArgs.PerformingUser >= 0)
+ {
+ WriteAudit(identityArgs.PerformingUser, identityArgs.AffectedUser, identityArgs.IpAddress, "umbraco/user/password/change", "password change");
+ }
+ }
+
+ private void OnLoginFailed(object sender, EventArgs args)
+ {
+ if (args is IdentityAuditEventArgs identityArgs && identityArgs.PerformingUser >= 0)
+ {
+ WriteAudit(identityArgs.PerformingUser, 0, identityArgs.IpAddress, "umbraco/user/sign-in/failed", "login failed", affectedDetails: "");
+ }
+ }
+
+ private void OnForgotPasswordChange(object sender, EventArgs args)
+ {
+ if (args is IdentityAuditEventArgs identityArgs && identityArgs.PerformingUser >= 0)
+ {
+ WriteAudit(identityArgs.PerformingUser, identityArgs.AffectedUser, identityArgs.IpAddress, "umbraco/user/password/forgot/change", "password forgot/change");
+ }
+ }
+
+ private void OnForgotPasswordRequest(object sender, EventArgs args)
+ {
+ if (args is IdentityAuditEventArgs identityArgs && identityArgs.PerformingUser >= 0)
+ {
+ WriteAudit(identityArgs.PerformingUser, identityArgs.AffectedUser, identityArgs.IpAddress, "umbraco/user/password/forgot/request", "password forgot/request");
+ }
+ }
+
+ private void WriteAudit(int performingId, int affectedId, string ipAddress, string eventType, string eventDetails, string affectedDetails = null)
+ {
+ var performingUser = _userService.GetUserById(performingId);
+
+ var performingDetails = performingUser == null
+ ? $"User UNKNOWN:{performingId}"
+ : $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}";
+
+ WriteAudit(performingId, performingDetails, affectedId, ipAddress, eventType, eventDetails, affectedDetails);
+ }
+
+ private void WriteAudit(IUser performingUser, int affectedId, string ipAddress, string eventType, string eventDetails)
+ {
+ var performingDetails = performingUser == null
+ ? $"User UNKNOWN"
+ : $"User \"{performingUser.Name}\" {FormatEmail(performingUser)}";
+
+ WriteAudit(performingUser?.Id ?? 0, performingDetails, affectedId, ipAddress, eventType, eventDetails);
+ }
+
+ private void WriteAudit(int performingId, string performingDetails, int affectedId, string ipAddress, string eventType, string eventDetails, string affectedDetails = null)
+ {
+ if (affectedDetails == null)
+ {
+ var affectedUser = _userService.GetUserById(affectedId);
+ affectedDetails = affectedUser == null
+ ? $"User UNKNOWN:{affectedId}"
+ : $"User \"{affectedUser.Name}\" {FormatEmail(affectedUser)}";
+ }
+
+ _auditService.Write(performingId, performingDetails,
+ ipAddress,
+ DateTime.UtcNow,
+ affectedId, affectedDetails,
+ eventType, eventDetails);
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Auditing/IdentityAuditEventArgs.cs b/src/Umbraco.Core/Auditing/IdentityAuditEventArgs.cs
index 14445d461f..c58bb409b0 100644
--- a/src/Umbraco.Core/Auditing/IdentityAuditEventArgs.cs
+++ b/src/Umbraco.Core/Auditing/IdentityAuditEventArgs.cs
@@ -1,4 +1,5 @@
using System;
+using System.ComponentModel;
using System.Threading;
using System.Web;
using Umbraco.Core.Security;
@@ -45,12 +46,8 @@ namespace Umbraco.Core.Auditing
///
public string Username { get; private set; }
- ///
- /// Sets the properties on the event being raised, all parameters are optional except for the action being performed
- ///
- /// An action based on the AuditEvent enum
- /// The client's IP address. This is usually automatically set but could be overridden if necessary
- /// The Id of the user performing the action (if different from the user affected by the action)
+ [Obsolete("Use the method that has the affectedUser parameter instead")]
+ [EditorBrowsable(EditorBrowsableState.Never)]
public IdentityAuditEventArgs(AuditEvent action, string ipAddress, int performingUser = -1)
{
DateTimeUtc = DateTime.UtcNow;
@@ -63,6 +60,35 @@ namespace Umbraco.Core.Auditing
: performingUser;
}
+ ///
+ /// Default constructor
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public IdentityAuditEventArgs(AuditEvent action, string ipAddress, string comment = null, int performingUser = -1, int affectedUser = -1)
+ {
+ DateTimeUtc = DateTime.UtcNow;
+ Action = action;
+
+ IpAddress = ipAddress;
+ Comment = comment;
+ AffectedUser = affectedUser;
+
+ PerformingUser = performingUser == -1
+ ? GetCurrentRequestBackofficeUserId()
+ : performingUser;
+ }
+
+ ///
+ /// Creates an instance without a performing or affected user (the id will be set to -1)
+ ///
+ ///
+ ///
+ ///
+ ///
public IdentityAuditEventArgs(AuditEvent action, string ipAddress, string username, string comment)
{
DateTimeUtc = DateTime.UtcNow;
@@ -71,6 +97,22 @@ namespace Umbraco.Core.Auditing
IpAddress = ipAddress;
Username = username;
Comment = comment;
+
+ PerformingUser = -1;
+ }
+
+ public IdentityAuditEventArgs(AuditEvent action, string ipAddress, string username, string comment, int performingUser)
+ {
+ DateTimeUtc = DateTime.UtcNow;
+ Action = action;
+
+ IpAddress = ipAddress;
+ Username = username;
+ Comment = comment;
+
+ PerformingUser = performingUser == -1
+ ? GetCurrentRequestBackofficeUserId()
+ : performingUser;
}
///
diff --git a/src/Umbraco.Core/Cache/HttpRequestCacheProvider.cs b/src/Umbraco.Core/Cache/HttpRequestCacheProvider.cs
index 4a43dd154f..6f97651042 100644
--- a/src/Umbraco.Core/Cache/HttpRequestCacheProvider.cs
+++ b/src/Umbraco.Core/Cache/HttpRequestCacheProvider.cs
@@ -145,7 +145,7 @@ namespace Umbraco.Core.Cache
#region Insert
#endregion
- private class NoopLocker : DisposableObject
+ private class NoopLocker : DisposableObjectSlim
{
protected override void DisposeResources()
{ }
diff --git a/src/Umbraco.Core/Collections/CompositeTypeTypeKey.cs b/src/Umbraco.Core/Collections/CompositeTypeTypeKey.cs
new file mode 100644
index 0000000000..cc555afe55
--- /dev/null
+++ b/src/Umbraco.Core/Collections/CompositeTypeTypeKey.cs
@@ -0,0 +1,62 @@
+using System;
+
+namespace Umbraco.Core.Collections
+{
+ ///
+ /// Represents a composite key of (Type, Type) for fast dictionaries.
+ ///
+ internal struct CompositeTypeTypeKey : IEquatable
+ {
+ ///
+ /// Initializes a new instance of the struct.
+ ///
+ public CompositeTypeTypeKey(Type type1, Type type2)
+ : this()
+ {
+ Type1 = type1;
+ Type2 = type2;
+ }
+
+ ///
+ /// Gets the first type.
+ ///
+ public Type Type1 { get; }
+
+ ///
+ /// Gets the second type.
+ ///
+ public Type Type2 { get; }
+
+ ///
+ public bool Equals(CompositeTypeTypeKey other)
+ {
+ return Type1 == other.Type1 && Type2 == other.Type2;
+ }
+
+ ///
+ public override bool Equals(object obj)
+ {
+ var other = obj is CompositeTypeTypeKey key ? key : default;
+ return Type1 == other.Type1 && Type2 == other.Type2;
+ }
+
+ public static bool operator ==(CompositeTypeTypeKey key1, CompositeTypeTypeKey key2)
+ {
+ return key1.Type1 == key2.Type1 && key1.Type2 == key2.Type2;
+ }
+
+ public static bool operator !=(CompositeTypeTypeKey key1, CompositeTypeTypeKey key2)
+ {
+ return key1.Type1 != key2.Type1 || key1.Type2 != key2.Type2;
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ return (Type1.GetHashCode() * 397) ^ Type2.GetHashCode();
+ }
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Collections/DeepCloneableList.cs b/src/Umbraco.Core/Collections/DeepCloneableList.cs
index 4853f89560..48d35efde9 100644
--- a/src/Umbraco.Core/Collections/DeepCloneableList.cs
+++ b/src/Umbraco.Core/Collections/DeepCloneableList.cs
@@ -136,5 +136,11 @@ namespace Umbraco.Core.Collections
dc.ResetDirtyProperties(rememberDirty);
}
}
+
+ /// Always return an empty enumerable, the list has no properties that can be dirty.
+ public IEnumerable GetWereDirtyProperties()
+ {
+ return Enumerable.Empty();
+ }
}
}
diff --git a/src/Umbraco.Core/Collections/TypeList.cs b/src/Umbraco.Core/Collections/TypeList.cs
new file mode 100644
index 0000000000..37ca427ba1
--- /dev/null
+++ b/src/Umbraco.Core/Collections/TypeList.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+
+namespace Umbraco.Core.Collections
+{
+ ///
+ /// Represents a list of types.
+ ///
+ /// Types in the list are, or derive from, or implement, the base type.
+ /// The base type.
+ internal class TypeList
+ {
+ private readonly List _list = new List();
+
+ ///
+ /// Adds a type to the list.
+ ///
+ /// The type to add.
+ public void Add()
+ where T : TBase
+ {
+ _list.Add(typeof(T));
+ }
+
+ ///
+ /// Determines whether a type is in the list.
+ ///
+ public bool Contains(Type type)
+ {
+ return _list.Contains(type);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Composing/CompositionRoots/RepositoryCompositionRoot.cs b/src/Umbraco.Core/Composing/CompositionRoots/RepositoryCompositionRoot.cs
index c72ecb528f..56538a8058 100644
--- a/src/Umbraco.Core/Composing/CompositionRoots/RepositoryCompositionRoot.cs
+++ b/src/Umbraco.Core/Composing/CompositionRoots/RepositoryCompositionRoot.cs
@@ -37,6 +37,7 @@ namespace Umbraco.Core.Composing.CompositionRoots
// repositories
container.RegisterSingleton();
+ container.RegisterSingleton();
container.RegisterSingleton();
container.RegisterSingleton();
container.RegisterSingleton();
@@ -67,6 +68,7 @@ namespace Umbraco.Core.Composing.CompositionRoots
container.RegisterSingleton();
container.RegisterSingleton();
container.RegisterSingleton();
+ container.RegisterSingleton();
// repositories that depend on a filesystem
// these have an annotated ctor parameter to pick the right file system
diff --git a/src/Umbraco.Core/Composing/CompositionRoots/ServicesCompositionRoot.cs b/src/Umbraco.Core/Composing/CompositionRoots/ServicesCompositionRoot.cs
index e3d7bca07d..d126cd6aa3 100644
--- a/src/Umbraco.Core/Composing/CompositionRoots/ServicesCompositionRoot.cs
+++ b/src/Umbraco.Core/Composing/CompositionRoots/ServicesCompositionRoot.cs
@@ -51,6 +51,7 @@ namespace Umbraco.Core.Composing.CompositionRoots
container.RegisterSingleton();
container.RegisterSingleton();
container.RegisterSingleton();
+ container.RegisterSingleton();
container.Register(factory =>
{
var mainLangFolder = new DirectoryInfo(IOHelper.MapPath(SystemDirectories.Umbraco + "/config/lang/"));
diff --git a/src/Umbraco.Core/Composing/TypeFinder.cs b/src/Umbraco.Core/Composing/TypeFinder.cs
index ca7c642453..f244d1d1ce 100644
--- a/src/Umbraco.Core/Composing/TypeFinder.cs
+++ b/src/Umbraco.Core/Composing/TypeFinder.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Configuration;
using System.IO;
using System.Linq;
using System.Reflection;
@@ -21,6 +22,38 @@ namespace Umbraco.Core.Composing
{
private static volatile HashSet _localFilteredAssemblyCache;
private static readonly object LocalFilteredAssemblyCacheLocker = new object();
+ private static readonly List NotifiedLoadExceptionAssemblies = new List();
+ private static string[] _assembliesAcceptingLoadExceptions;
+
+ private static string[] AssembliesAcceptingLoadExceptions
+ {
+ get
+ {
+ if (_assembliesAcceptingLoadExceptions != null)
+ return _assembliesAcceptingLoadExceptions;
+
+ var s = ConfigurationManager.AppSettings["Umbraco.AssembliesAcceptingLoadExceptions"];
+ return _assembliesAcceptingLoadExceptions = string.IsNullOrWhiteSpace(s)
+ ? Array.Empty()
+ : s.Split(',').Select(x => x.Trim()).ToArray();
+ }
+ }
+
+ private static bool AcceptsLoadExceptions(Assembly a)
+ {
+ if (AssembliesAcceptingLoadExceptions.Length == 0)
+ return false;
+ if (AssembliesAcceptingLoadExceptions.Length == 1 && AssembliesAcceptingLoadExceptions[0] == "*")
+ return true;
+ var name = a.GetName().Name; // simple name of the assembly
+ return AssembliesAcceptingLoadExceptions.Any(pattern =>
+ {
+ if (pattern.Length > name.Length) return false; // pattern longer than name
+ if (pattern.Length == name.Length) return pattern.InvariantEquals(name); // same length, must be identical
+ if (pattern[pattern.Length] != '.') return false; // pattern is shorter than name, must end with dot
+ return name.StartsWith(pattern); // and name must start with pattern
+ });
+ }
///
/// lazily load a reference to all assemblies and only local assemblies.
@@ -45,7 +78,7 @@ namespace Umbraco.Core.Composing
HashSet assemblies = null;
try
{
- var isHosted = HttpContext.Current != null || HostingEnvironment.IsHosted;
+ var isHosted = IOHelper.IsHosted;
try
{
@@ -529,8 +562,21 @@ namespace Umbraco.Core.Composing
foreach (var loaderException in rex.LoaderExceptions.WhereNotNull())
AppendLoaderException(sb, loaderException);
- // rethrow with new message
- throw new ReflectionTypeLoadException(rex.Types, rex.LoaderExceptions, sb.ToString());
+ var ex = new ReflectionTypeLoadException(rex.Types, rex.LoaderExceptions, sb.ToString());
+
+ // rethrow with new message, unless accepted
+ if (AcceptsLoadExceptions(a) == false) throw ex;
+
+ // log a warning, and return what we can
+ lock (NotifiedLoadExceptionAssemblies)
+ {
+ if (NotifiedLoadExceptionAssemblies.Contains(a.FullName) == false)
+ {
+ NotifiedLoadExceptionAssemblies.Add(a.FullName);
+ Current.Logger.Warn(typeof (TypeFinder), ex, $"Could not load all types from {a.GetName().Name}.");
+ }
+ }
+ return rex.Types.WhereNotNull().ToArray();
}
}
diff --git a/src/Umbraco.Core/Composing/TypeLoader.cs b/src/Umbraco.Core/Composing/TypeLoader.cs
index 02c69ceec8..f3cc8e86ff 100644
--- a/src/Umbraco.Core/Composing/TypeLoader.cs
+++ b/src/Umbraco.Core/Composing/TypeLoader.cs
@@ -5,8 +5,10 @@ using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
+using System.Web;
using System.Web.Compilation;
using Umbraco.Core.Cache;
+using Umbraco.Core.Configuration;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using File = System.IO.File;
@@ -28,7 +30,6 @@ namespace Umbraco.Core.Composing
private readonly IRuntimeCacheProvider _runtimeCache;
private readonly ProfilingLogger _logger;
- private readonly string _tempFolder;
private readonly object _typesLock = new object();
private readonly Dictionary _types = new Dictionary();
@@ -37,6 +38,8 @@ namespace Umbraco.Core.Composing
private string _currentAssembliesHash;
private IEnumerable _assemblies;
private bool _reportedChange;
+ private static LocalTempStorage _localTempStorage = LocalTempStorage.Unknown;
+ private static string _fileBasePath;
///
/// Initializes a new instance of the class.
@@ -49,13 +52,6 @@ namespace Umbraco.Core.Composing
_runtimeCache = runtimeCache ?? throw new ArgumentNullException(nameof(runtimeCache));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
- // the temp folder where the cache file lives
- _tempFolder = IOHelper.MapPath("~/App_Data/TEMP/TypesCache");
- if (Directory.Exists(_tempFolder) == false)
- Directory.CreateDirectory(_tempFolder);
-
- var typesListFile = GeTypesListFilePath();
-
if (detectChanges)
{
//first check if the cached hash is string.Empty, if it is then we need
@@ -67,7 +63,9 @@ namespace Umbraco.Core.Composing
// if the hash has changed, clear out the persisted list no matter what, this will force
// rescanning of all types including lazy ones.
// http://issues.umbraco.org/issue/U4-4789
- File.Delete(typesListFile);
+ var typesListFilePath = GetTypesListFilePath();
+ if (File.Exists(typesListFilePath))
+ File.Delete(typesListFilePath);
WriteCacheTypesHash();
}
@@ -77,7 +75,9 @@ namespace Umbraco.Core.Composing
// if the hash has changed, clear out the persisted list no matter what, this will force
// rescanning of all types including lazy ones.
// http://issues.umbraco.org/issue/U4-4789
- File.Delete(typesListFile);
+ var typesListFilePath = GetTypesListFilePath();
+ if (File.Exists(typesListFilePath))
+ File.Delete(typesListFilePath);
// always set to true if we're not detecting (generally only for testing)
RequiresRescanning = true;
@@ -135,10 +135,10 @@ namespace Umbraco.Core.Composing
if (_cachedAssembliesHash != null)
return _cachedAssembliesHash;
- var filePath = GetTypesHashFilePath();
- if (File.Exists(filePath) == false) return string.Empty;
+ var typesHashFilePath = GetTypesHashFilePath();
+ if (!File.Exists(typesHashFilePath)) return string.Empty;
- var hash = File.ReadAllText(filePath, Encoding.UTF8);
+ var hash = File.ReadAllText(typesHashFilePath, Encoding.UTF8);
_cachedAssembliesHash = hash;
return _cachedAssembliesHash;
@@ -177,8 +177,8 @@ namespace Umbraco.Core.Composing
///
private void WriteCacheTypesHash()
{
- var filePath = GetTypesHashFilePath();
- File.WriteAllText(filePath, CurrentAssembliesHash, Encoding.UTF8);
+ var typesHashFilePath = GetTypesHashFilePath();
+ File.WriteAllText(typesHashFilePath, CurrentAssembliesHash, Encoding.UTF8);
}
///
@@ -295,8 +295,8 @@ namespace Umbraco.Core.Composing
{
try
{
- var filePath = GeTypesListFilePath();
- File.Delete(filePath);
+ var typesListFilePath = GetTypesListFilePath();
+ File.Delete(typesListFilePath);
}
catch
{
@@ -312,11 +312,11 @@ namespace Umbraco.Core.Composing
{
var cache = new Dictionary, IEnumerable>();
- var filePath = GeTypesListFilePath();
- if (File.Exists(filePath) == false)
+ var typesListFilePath = GetTypesListFilePath();
+ if (File.Exists(typesListFilePath) == false)
return cache;
- using (var stream = GetFileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, ListFileOpenReadTimeout))
+ using (var stream = GetFileStream(typesListFilePath, FileMode.Open, FileAccess.Read, FileShare.Read, ListFileOpenReadTimeout))
using (var reader = new StreamReader(stream))
{
while (true)
@@ -354,28 +354,88 @@ namespace Umbraco.Core.Composing
}
// internal for tests
- internal string GeTypesListFilePath()
+ internal static string GetTypesListFilePath() => GetFileBasePath() + ".list";
+
+ private static string GetTypesHashFilePath() => GetFileBasePath() + ".hash";
+
+ private static string GetFileBasePath()
{
- var filename = "umbraco-types." + NetworkHelper.FileSafeMachineName + ".list";
- return Path.Combine(_tempFolder, filename);
+ var localTempStorage = GlobalSettings.LocalTempStorageLocation;
+ if (_localTempStorage != localTempStorage)
+ {
+ string path;
+ switch (GlobalSettings.LocalTempStorageLocation)
+ {
+ case LocalTempStorage.AspNetTemp:
+ path = Path.Combine(HttpRuntime.CodegenDir, "UmbracoData", "umbraco-types");
+ break;
+ case LocalTempStorage.EnvironmentTemp:
+ // include the appdomain hash is just a safety check, for example if a website is moved from worker A to worker B and then back
+ // to worker A again, in theory the %temp% folder should already be empty but we really want to make sure that its not
+ // utilizing an old path - assuming we cannot have SHA1 collisions on AppDomainAppId
+ var appDomainHash = HttpRuntime.AppDomainAppId.ToSHA1();
+ var cachePath = Path.Combine(Environment.ExpandEnvironmentVariables("%temp%"), "UmbracoData", appDomainHash);
+ path = Path.Combine(cachePath, "umbraco-types");
+ break;
+ case LocalTempStorage.Default:
+ default:
+ var tempFolder = IOHelper.MapPath("~/App_Data/TEMP/TypesCache");
+ path = Path.Combine(tempFolder, "umbraco-types." + NetworkHelper.FileSafeMachineName);
+ break;
+ }
+
+ _fileBasePath = path;
+ _localTempStorage = localTempStorage;
+ }
+
+ // ensure that the folder exists
+ var directory = Path.GetDirectoryName(_fileBasePath);
+ if (directory == null)
+ throw new InvalidOperationException($"Could not determine folder for path \"{_fileBasePath}\".");
+ if (Directory.Exists(directory) == false)
+ Directory.CreateDirectory(directory);
+
+ return _fileBasePath;
}
- private string GetTypesHashFilePath()
+ private static string GetFilePath(string extension)
{
- var filename = "umbraco-types." + NetworkHelper.FileSafeMachineName + ".hash";
- return Path.Combine(_tempFolder, filename);
+ string path;
+ switch (GlobalSettings.LocalTempStorageLocation)
+ {
+ case LocalTempStorage.AspNetTemp:
+ path = Path.Combine(HttpRuntime.CodegenDir, "UmbracoData", "umbraco-types." + extension);
+ break;
+ case LocalTempStorage.EnvironmentTemp:
+ // include the appdomain hash is just a safety check, for example if a website is moved from worker A to worker B and then back
+ // to worker A again, in theory the %temp% folder should already be empty but we really want to make sure that its not
+ // utilizing an old path - assuming we cannot have SHA1 collisions on AppDomainAppId
+ var appDomainHash = HttpRuntime.AppDomainAppId.ToSHA1();
+ var cachePath = Path.Combine(Environment.ExpandEnvironmentVariables("%temp%"), "UmbracoData", appDomainHash);
+ path = Path.Combine(cachePath, "umbraco-types." + extension);
+ break;
+ case LocalTempStorage.Default:
+ default:
+ var tempFolder = IOHelper.MapPath("~/App_Data/TEMP/TypesCache");
+ path = Path.Combine(tempFolder, "umbraco-types." + NetworkHelper.FileSafeMachineName + "." + extension);
+ break;
+ }
+
+ // ensure that the folder exists
+ var directory = Path.GetDirectoryName(path);
+ if (directory == null)
+ throw new InvalidOperationException($"Could not determine folder for file \"{path}\".");
+ if (Directory.Exists(directory) == false)
+ Directory.CreateDirectory(directory);
+
+ return path;
}
// internal for tests
internal void WriteCache()
{
- // be absolutely sure
- if (Directory.Exists(_tempFolder) == false)
- Directory.CreateDirectory(_tempFolder);
-
- var filePath = GeTypesListFilePath();
-
- using (var stream = GetFileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, ListFileOpenWriteTimeout))
+ var typesListFilePath = GetTypesListFilePath();
+ using (var stream = GetFileStream(typesListFilePath, FileMode.Create, FileAccess.Write, FileShare.None, ListFileOpenWriteTimeout))
using (var writer = new StreamWriter(stream))
{
foreach (var typeList in _types.Values)
@@ -405,13 +465,13 @@ namespace Umbraco.Core.Composing
/// Generally only used for resetting cache, for example during the install process.
public void ClearTypesCache()
{
- var path = GeTypesListFilePath();
- if (File.Exists(path))
- File.Delete(path);
+ var typesListFilePath = GetTypesListFilePath();
+ if (File.Exists(typesListFilePath))
+ File.Delete(typesListFilePath);
- path = GetTypesHashFilePath();
- if (File.Exists(path))
- File.Delete(path);
+ var typesHashFilePath = GetTypesHashFilePath();
+ if (File.Exists(typesHashFilePath))
+ File.Delete(typesHashFilePath);
_runtimeCache.ClearCacheItem(CacheKey);
}
@@ -589,7 +649,8 @@ namespace Umbraco.Core.Composing
// else proceed,
typeList = new TypeList(baseType, attributeType);
- var scan = RequiresRescanning || File.Exists(GeTypesListFilePath()) == false;
+ var typesListFilePath = GetTypesListFilePath();
+ var scan = RequiresRescanning || File.Exists(typesListFilePath) == false;
if (scan)
{
diff --git a/src/Umbraco.Core/Configuration/ClientDependencyConfiguration.cs b/src/Umbraco.Core/Configuration/ClientDependencyConfiguration.cs
index c85df0be73..8693f2e6e8 100644
--- a/src/Umbraco.Core/Configuration/ClientDependencyConfiguration.cs
+++ b/src/Umbraco.Core/Configuration/ClientDependencyConfiguration.cs
@@ -1,10 +1,14 @@
using System;
using System.Collections.Generic;
using System.IO;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
using System.Web;
using System.Xml.Linq;
using ClientDependency.Core.CompositeFiles.Providers;
using ClientDependency.Core.Config;
+using Semver;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
@@ -24,10 +28,74 @@ namespace Umbraco.Core.Configuration
_logger = logger;
_fileName = IOHelper.MapPath(string.Format("{0}/ClientDependency.config", SystemDirectories.Config));
}
+
+ ///
+ /// Changes the version number in ClientDependency.config to a hashed value for the version and the DateTime.Day
+ ///
+ /// The version of Umbraco we're upgrading to
+ /// A date value to use in the hash to prevent this method from updating the version on each startup
+ /// Allows the developer to specify the date precision for the hash (i.e. "yyyyMMdd" would be a precision for the day)
+ /// Boolean to indicate succesful update of the ClientDependency.config file
+ public bool UpdateVersionNumber(SemVersion version, DateTime date, string dateFormat)
+ {
+ var byteContents = Encoding.Unicode.GetBytes(version + date.ToString(dateFormat));
+
+ //This is a way to convert a string to a long
+ //see https://www.codeproject.com/Articles/34309/Convert-String-to-bit-Integer
+ //We could much more easily use MD5 which would create us an INT but since that is not compliant with
+ //hashing standards we have to use SHA
+ int intHash;
+ using (var hash = SHA256.Create())
+ {
+ var bytes = hash.ComputeHash(byteContents);
+
+ var longResult = new[] { 0, 8, 16, 24 }
+ .Select(i => BitConverter.ToInt64(bytes, i))
+ .Aggregate((x, y) => x ^ y);
+
+ //CDF requires an INT, and although this isn't fail safe, it will work for our purposes. We are not hashing for crypto purposes
+ //so there could be some collisions with this conversion but it's not a problem for our purposes
+ //It's also important to note that the long.GetHashCode() implementation in .NET is this: return (int) this ^ (int) (this >> 32);
+ //which means that this value will not change per appdomain like some GetHashCode implementations.
+ intHash = longResult.GetHashCode();
+ }
+
+ try
+ {
+ var clientDependencyConfigXml = XDocument.Load(_fileName, LoadOptions.PreserveWhitespace);
+ if (clientDependencyConfigXml.Root != null)
+ {
+
+ var versionAttribute = clientDependencyConfigXml.Root.Attribute("version");
+
+ //Set the new version to the hashcode of now
+ var oldVersion = versionAttribute.Value;
+ var newVersion = Math.Abs(intHash).ToString();
+
+ //don't update if it's the same version
+ if (oldVersion == newVersion)
+ return false;
+
+ versionAttribute.SetValue(newVersion);
+ clientDependencyConfigXml.Save(_fileName, SaveOptions.DisableFormatting);
+
+ _logger.Info(string.Format("Updated version number from {0} to {1}", oldVersion, newVersion));
+ return true;
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.Error("Couldn't update ClientDependency version number", ex);
+ }
+
+ return false;
+ }
///
/// Changes the version number in ClientDependency.config to a random value to avoid stale caches
///
+ ///
+ [Obsolete("Use the UpdateVersionNumber method specifying the version, date and dateFormat instead")]
public bool IncreaseVersionNumber()
{
try
diff --git a/src/Umbraco.Core/Configuration/ContentXmlStorage.cs b/src/Umbraco.Core/Configuration/ContentXmlStorage.cs
deleted file mode 100644
index f81ca8f8cc..0000000000
--- a/src/Umbraco.Core/Configuration/ContentXmlStorage.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace Umbraco.Core.Configuration
-{
- internal enum ContentXmlStorage
- {
- Default,
- AspNetTemp,
- EnvironmentTemp
- }
-}
diff --git a/src/Umbraco.Core/Configuration/GlobalSettings.cs b/src/Umbraco.Core/Configuration/GlobalSettings.cs
index 3b7fc8daad..060ecf5d17 100644
--- a/src/Umbraco.Core/Configuration/GlobalSettings.cs
+++ b/src/Umbraco.Core/Configuration/GlobalSettings.cs
@@ -473,24 +473,24 @@ namespace Umbraco.Core.Configuration
internal static bool ContentCacheXmlStoredInCodeGen
{
- get { return ContentCacheXmlStorageLocation == ContentXmlStorage.AspNetTemp; }
+ get { return LocalTempStorageLocation == LocalTempStorage.AspNetTemp; }
}
- internal static ContentXmlStorage ContentCacheXmlStorageLocation
+ ///
+ /// This is the location type to store temporary files such as cache files or other localized files for a given machine
+ ///
+ ///
+ /// Currently used for the xml cache file and the plugin cache files
+ ///
+ internal static LocalTempStorage LocalTempStorageLocation
{
get
{
- if (ConfigurationManager.AppSettings.ContainsKey("umbracoContentXMLStorage"))
- {
- return Enum.Parse(ConfigurationManager.AppSettings["umbracoContentXMLStorage"]);
- }
- if (ConfigurationManager.AppSettings.ContainsKey("umbracoContentXMLUseLocalTemp"))
- {
- return bool.Parse(ConfigurationManager.AppSettings["umbracoContentXMLUseLocalTemp"])
- ? ContentXmlStorage.AspNetTemp
- : ContentXmlStorage.Default;
- }
- return ContentXmlStorage.Default;
+ var setting = ConfigurationManager.AppSettings["umbracoLocalTempStorage"];
+ if (!string.IsNullOrWhiteSpace(setting))
+ return Enum.Parse(setting);
+
+ return LocalTempStorage.Default;
}
}
diff --git a/src/Umbraco.Core/Configuration/LocalTempStorage.cs b/src/Umbraco.Core/Configuration/LocalTempStorage.cs
new file mode 100644
index 0000000000..1231ee7156
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/LocalTempStorage.cs
@@ -0,0 +1,10 @@
+namespace Umbraco.Core.Configuration
+{
+ internal enum LocalTempStorage
+ {
+ Unknown = 0,
+ Default,
+ AspNetTemp,
+ EnvironmentTemp
+ }
+}
diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/BackOfficeElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/BackOfficeElement.cs
new file mode 100644
index 0000000000..d1d2a26a96
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/UmbracoSettings/BackOfficeElement.cs
@@ -0,0 +1,18 @@
+using System.Configuration;
+
+namespace Umbraco.Core.Configuration.UmbracoSettings
+{
+ internal class BackOfficeElement : UmbracoConfigurationElement, IBackOfficeSection
+ {
+ [ConfigurationProperty("tours")]
+ internal TourConfigElement Tours
+ {
+ get { return (TourConfigElement)this["tours"]; }
+ }
+
+ ITourSection IBackOfficeSection.Tours
+ {
+ get { return Tours; }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IBackOfficeSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IBackOfficeSection.cs
new file mode 100644
index 0000000000..36dd6a22ed
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IBackOfficeSection.cs
@@ -0,0 +1,7 @@
+namespace Umbraco.Core.Configuration.UmbracoSettings
+{
+ public interface IBackOfficeSection
+ {
+ ITourSection Tours { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/ITourSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/ITourSection.cs
new file mode 100644
index 0000000000..938642521e
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/UmbracoSettings/ITourSection.cs
@@ -0,0 +1,7 @@
+namespace Umbraco.Core.Configuration.UmbracoSettings
+{
+ public interface ITourSection
+ {
+ bool EnableTours { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/IUmbracoSettingsSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/IUmbracoSettingsSection.cs
index 0801c9933f..085a826626 100644
--- a/src/Umbraco.Core/Configuration/UmbracoSettings/IUmbracoSettingsSection.cs
+++ b/src/Umbraco.Core/Configuration/UmbracoSettings/IUmbracoSettingsSection.cs
@@ -5,6 +5,8 @@ namespace Umbraco.Core.Configuration.UmbracoSettings
{
public interface IUmbracoSettingsSection : IUmbracoConfigurationSection
{
+ IBackOfficeSection BackOffice { get; }
+
IContentSection Content { get; }
ISecuritySection Security { get; }
diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/TourConfigElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/TourConfigElement.cs
new file mode 100644
index 0000000000..ebb649ca3b
--- /dev/null
+++ b/src/Umbraco.Core/Configuration/UmbracoSettings/TourConfigElement.cs
@@ -0,0 +1,17 @@
+using System.Configuration;
+
+namespace Umbraco.Core.Configuration.UmbracoSettings
+{
+ internal class TourConfigElement : UmbracoConfigurationElement, ITourSection
+ {
+ //disabled by default so that upgraders don't get it enabled by default
+ //TODO: we probably just want to disable the initial one from automatically loading ?
+ [ConfigurationProperty("enable", DefaultValue = false)]
+ public bool EnableTours
+ {
+ get { return (bool)this["enable"]; }
+ }
+
+ //TODO: We could have additional filters, etc... defined here
+ }
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoSettingsSection.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoSettingsSection.cs
index 49a791144b..0cf97b2560 100644
--- a/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoSettingsSection.cs
+++ b/src/Umbraco.Core/Configuration/UmbracoSettings/UmbracoSettingsSection.cs
@@ -8,6 +8,12 @@ namespace Umbraco.Core.Configuration.UmbracoSettings
public class UmbracoSettingsSection : ConfigurationSection, IUmbracoSettingsSection
{
+ [ConfigurationProperty("backOffice")]
+ internal BackOfficeElement BackOffice
+ {
+ get { return (BackOfficeElement)this["backOffice"]; }
+ }
+
[ConfigurationProperty("content")]
internal ContentElement Content
{
@@ -132,6 +138,11 @@ namespace Umbraco.Core.Configuration.UmbracoSettings
get { return Templates; }
}
+ IBackOfficeSection IUmbracoSettingsSection.BackOffice
+ {
+ get { return BackOffice; }
+ }
+
IDeveloperSection IUmbracoSettingsSection.Developer
{
get { return Developer; }
diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs
index c2ca568a54..a5fe8de270 100644
--- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs
+++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs
@@ -1,4 +1,5 @@
using System;
+using System.Configuration;
using System.Reflection;
using Semver;
@@ -34,11 +35,37 @@ namespace Umbraco.Core.Configuration
///
/// Gets the semantic version of the executing code.
///
- public static SemVersion SemanticVersion => new SemVersion(
- Current.Major,
- Current.Minor,
- Current.Build,
- CurrentComment.IsNullOrWhiteSpace() ? null : CurrentComment,
- Current.Revision > 0 ? Current.Revision.ToInvariantString() : null);
+ public static SemVersion SemanticVersion { get; } = new SemVersion(
+ Current.Major,
+ Current.Minor,
+ Current.Build,
+ CurrentComment.IsNullOrWhiteSpace() ? null : CurrentComment,
+ Current.Revision > 0 ? Current.Revision.ToInvariantString() : null);
+
+ ///
+ /// Gets the "local" version of the site.
+ ///
+ ///
+ /// Three things have a version, really: the executing code, the database model,
+ /// and the site/files. The database model version is entirely managed via migrations,
+ /// and changes during an upgrade. The executing code version changes when new code is
+ /// deployed. The site/files version changes during an upgrade.
+ ///
+ public static SemVersion Local
+ {
+ get
+ {
+ try
+ {
+ // fixme - this should live in its own independent file! NOT web.config!
+ var value = ConfigurationManager.AppSettings["umbracoConfigurationStatus"];
+ return SemVersion.TryParse(value, out var semver) ? semver : null;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+ }
}
}
diff --git a/src/Umbraco.Core/Constants-Applications.cs b/src/Umbraco.Core/Constants-Applications.cs
index 014c4af450..4c859469fd 100644
--- a/src/Umbraco.Core/Constants-Applications.cs
+++ b/src/Umbraco.Core/Constants-Applications.cs
@@ -73,6 +73,11 @@
///
public const string Media = "media";
+ ///
+ /// alias for the macro tree.
+ ///
+ public const string Macros = "macros";
+
///
/// alias for the datatype tree.
///
@@ -121,8 +126,6 @@
public const string Languages = "languages";
- public const string Macros = "macros";
-
///
/// alias for the user types tree.
///
diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs
index f6ce98901d..43bd381571 100644
--- a/src/Umbraco.Core/Constants-PropertyEditors.cs
+++ b/src/Umbraco.Core/Constants-PropertyEditors.cs
@@ -63,6 +63,11 @@ namespace Umbraco.Core
/// DropDown List Multiple, Publish Keys.
///
public const string DropdownlistMultiplePublishKeys = "Umbraco.DropdownlistMultiplePublishKeys";
+
+ ///
+ /// DropDown List.
+ ///
+ public const string DropDownListFlexible = "Umbraco.DropDown.Flexible";
///
/// Folder Browser.
diff --git a/src/Umbraco.Core/Constants-Security.cs b/src/Umbraco.Core/Constants-Security.cs
index 2e9de0d8a4..4c24febb22 100644
--- a/src/Umbraco.Core/Constants-Security.cs
+++ b/src/Umbraco.Core/Constants-Security.cs
@@ -13,6 +13,7 @@ namespace Umbraco.Core
public const int SuperId = -1;
public const string AdminGroupAlias = "admin";
+ public const string SensitiveDataGroupAlias = "sensitiveData";
public const string TranslatorGroupAlias = "translator";
public const string BackOfficeAuthenticationType = "UmbracoBackOffice";
diff --git a/src/Umbraco.Core/Constants-System.cs b/src/Umbraco.Core/Constants-System.cs
index 8ab2f74f4c..9fad62c347 100644
--- a/src/Umbraco.Core/Constants-System.cs
+++ b/src/Umbraco.Core/Constants-System.cs
@@ -12,16 +12,52 @@
///
public const int Root = -1;
+ ///
+ /// The string identifier for global system root node.
+ ///
+ /// Use this instead of re-creating the string everywhere.
+ public const string RootString = "-1";
+
///
/// The integer identifier for content's recycle bin.
///
public const int RecycleBinContent = -20;
+ ///
+ /// The string identifier for content's recycle bin.
+ ///
+ /// Use this instead of re-creating the string everywhere.
+ public const string RecycleBinContentString = "-20";
+
+ ///
+ /// The string path prefix of the content's recycle bin.
+ ///
+ ///
+ /// Everything that is in the content recycle bin, has a path that starts with the prefix.
+ /// Use this instead of re-creating the string everywhere.
+ ///
+ public const string RecycleBinContentPathPrefix = "-1,-20,";
+
///
/// The integer identifier for media's recycle bin.
///
public const int RecycleBinMedia = -21;
+ ///
+ /// The string identifier for media's recycle bin.
+ ///
+ /// Use this instead of re-creating the string everywhere.
+ public const string RecycleBinMediaString = "-21";
+
+ ///
+ /// The string path prefix of the media's recycle bin.
+ ///
+ ///
+ /// Everything that is in the media recycle bin, has a path that starts with the prefix.
+ /// Use this instead of re-creating the string everywhere.
+ ///
+ public const string RecycleBinMediaPathPrefix = "-1,-21,";
+
public const string UmbracoConnectionName = "umbracoDbDSN";
public const string UmbracoUpgradePlanName = "Umbraco.Core";
}
diff --git a/src/Umbraco.Core/DateTimeExtensions.cs b/src/Umbraco.Core/DateTimeExtensions.cs
index d82ec99c6a..b3babd2d07 100644
--- a/src/Umbraco.Core/DateTimeExtensions.cs
+++ b/src/Umbraco.Core/DateTimeExtensions.cs
@@ -21,9 +21,9 @@ namespace Umbraco.Core
public static DateTime TruncateTo(this DateTime dt, DateTruncate truncateTo)
{
if (truncateTo == DateTruncate.Year)
- return new DateTime(dt.Year, 0, 0);
+ return new DateTime(dt.Year, 1, 1);
if (truncateTo == DateTruncate.Month)
- return new DateTime(dt.Year, dt.Month, 0);
+ return new DateTime(dt.Year, dt.Month, 1);
if (truncateTo == DateTruncate.Day)
return new DateTime(dt.Year, dt.Month, dt.Day);
if (truncateTo == DateTruncate.Hour)
diff --git a/src/Umbraco.Core/DisposableObject.cs b/src/Umbraco.Core/DisposableObject.cs
index 956804e347..ecdc149f6e 100644
--- a/src/Umbraco.Core/DisposableObject.cs
+++ b/src/Umbraco.Core/DisposableObject.cs
@@ -6,6 +6,9 @@ namespace Umbraco.Core
/// Abstract implementation of IDisposable.
///
///
+ /// This is for objects that DO have unmanaged resources. Use
+ /// for objects that do NOT have unmanaged resources, and avoid creating a finalizer.
+ ///
/// Can also be used as a pattern for when inheriting is not possible.
///
/// See also: https://msdn.microsoft.com/en-us/library/b1yfkh5e%28v=vs.110%29.aspx
diff --git a/src/Umbraco.Core/DisposableObjectSlim.cs b/src/Umbraco.Core/DisposableObjectSlim.cs
new file mode 100644
index 0000000000..4992f8bc0f
--- /dev/null
+++ b/src/Umbraco.Core/DisposableObjectSlim.cs
@@ -0,0 +1,53 @@
+using System;
+
+namespace Umbraco.Core
+{
+ ///
+ /// Abstract implementation of managed IDisposable.
+ ///
+ ///
+ /// This is for objects that do NOT have unmanaged resources. Use
+ /// for objects that DO have unmanaged resources and need to deal with them when disposing.
+ ///
+ /// Can also be used as a pattern for when inheriting is not possible.
+ ///
+ /// See also: https://msdn.microsoft.com/en-us/library/b1yfkh5e%28v=vs.110%29.aspx
+ /// See also: https://lostechies.com/chrispatterson/2012/11/29/idisposable-done-right/
+ ///
+ /// Note: if an object's ctor throws, it will never be disposed, and so if that ctor
+ /// has allocated disposable objects, it should take care of disposing them.
+ ///
+ public abstract class DisposableObjectSlim : IDisposable
+ {
+ private readonly object _locko = new object();
+
+ // gets a value indicating whether this instance is disposed.
+ // for internal tests only (not thread safe)
+ public bool Disposed { get; private set; }
+
+ // implements IDisposable
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+
+ private void Dispose(bool disposing)
+ {
+ // can happen if the object construction failed
+ if (_locko == null)
+ return;
+
+ lock (_locko)
+ {
+ if (Disposed) return;
+ Disposed = true;
+ }
+
+ if (disposing)
+ DisposeResources();
+ }
+
+ protected virtual void DisposeResources() { }
+ }
+}
diff --git a/src/Umbraco.Core/DisposableTimer.cs b/src/Umbraco.Core/DisposableTimer.cs
index 819e86f8e1..6ded588be6 100644
--- a/src/Umbraco.Core/DisposableTimer.cs
+++ b/src/Umbraco.Core/DisposableTimer.cs
@@ -7,7 +7,7 @@ namespace Umbraco.Core
///
/// Starts the timer and invokes a callback upon disposal. Provides a simple way of timing an operation by wrapping it in a using (C#) statement.
///
- public class DisposableTimer : DisposableObject
+ public class DisposableTimer : DisposableObjectSlim
{
private readonly ILogger _logger;
private readonly LogType? _logType;
diff --git a/src/Umbraco.Core/Events/EventMessages.cs b/src/Umbraco.Core/Events/EventMessages.cs
index aac0d07e85..2df1911249 100644
--- a/src/Umbraco.Core/Events/EventMessages.cs
+++ b/src/Umbraco.Core/Events/EventMessages.cs
@@ -5,7 +5,7 @@ namespace Umbraco.Core.Events
///
/// Event messages collection
///
- public sealed class EventMessages : DisposableObject
+ public sealed class EventMessages : DisposableObjectSlim
{
private readonly List _msgs = new List();
diff --git a/src/Umbraco.Core/Events/ExportedMemberEventArgs.cs b/src/Umbraco.Core/Events/ExportedMemberEventArgs.cs
new file mode 100644
index 0000000000..9c91f3e5bd
--- /dev/null
+++ b/src/Umbraco.Core/Events/ExportedMemberEventArgs.cs
@@ -0,0 +1,18 @@
+using System;
+using Umbraco.Core.Models;
+using Umbraco.Core.Models.Membership;
+
+namespace Umbraco.Core.Events
+{
+ internal class ExportedMemberEventArgs : EventArgs
+ {
+ public IMember Member { get; }
+ public MemberExportModel Exported { get; }
+
+ public ExportedMemberEventArgs(IMember member, MemberExportModel exported)
+ {
+ Member = member;
+ Exported = exported;
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Events/ImportPackageEventArgs.cs b/src/Umbraco.Core/Events/ImportPackageEventArgs.cs
index 6ad7b52806..7536b43e93 100644
--- a/src/Umbraco.Core/Events/ImportPackageEventArgs.cs
+++ b/src/Umbraco.Core/Events/ImportPackageEventArgs.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel;
using Umbraco.Core.Models.Packaging;
namespace Umbraco.Core.Events
@@ -8,17 +9,26 @@ namespace Umbraco.Core.Events
{
private readonly MetaData _packageMetaData;
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [Obsolete("Use the overload specifying packageMetaData instead")]
public ImportPackageEventArgs(TEntity eventObject, bool canCancel)
: base(new[] { eventObject }, canCancel)
{
}
- public ImportPackageEventArgs(TEntity eventObject, MetaData packageMetaData)
- : base(new[] { eventObject })
+ public ImportPackageEventArgs(TEntity eventObject, MetaData packageMetaData, bool canCancel)
+ : base(new[] { eventObject }, canCancel)
{
+ if (packageMetaData == null) throw new ArgumentNullException("packageMetaData");
_packageMetaData = packageMetaData;
}
+ public ImportPackageEventArgs(TEntity eventObject, MetaData packageMetaData)
+ : this(eventObject, packageMetaData, true)
+ {
+
+ }
+
public MetaData PackageMetaData
{
get { return _packageMetaData; }
diff --git a/src/Umbraco.Core/Events/QueuingEventDispatcherBase.cs b/src/Umbraco.Core/Events/QueuingEventDispatcherBase.cs
index 2cc7046078..0283ac372e 100644
--- a/src/Umbraco.Core/Events/QueuingEventDispatcherBase.cs
+++ b/src/Umbraco.Core/Events/QueuingEventDispatcherBase.cs
@@ -78,255 +78,256 @@ namespace Umbraco.Core.Events
if (_events == null)
return Enumerable.Empty();
+ IReadOnlyList events;
switch (filter)
{
case EventDefinitionFilter.All:
- return FilterSupersededAndUpdateToLatestEntity(_events);
+ events = _events;
+ break;
case EventDefinitionFilter.FirstIn:
var l1 = new OrderedHashSet();
foreach (var e in _events)
l1.Add(e);
- return FilterSupersededAndUpdateToLatestEntity(l1);
+ events = l1;
+ break;
case EventDefinitionFilter.LastIn:
var l2 = new OrderedHashSet(keepOldest: false);
foreach (var e in _events)
l2.Add(e);
- return FilterSupersededAndUpdateToLatestEntity(l2);
+ events = l2;
+ break;
default:
- throw new ArgumentOutOfRangeException(nameof(filter), filter, null);
+ throw new ArgumentOutOfRangeException("filter", filter, null);
}
+
+ return FilterSupersededAndUpdateToLatestEntity(events);
}
- private class EventDefinitionTypeData
+ private class EventDefinitionInfos
{
public IEventDefinition EventDefinition { get; set; }
- public Type EventArgType { get; set; }
- public SupersedeEventAttribute[] SupersedeAttributes { get; set; }
+ public Type[] SupersedeTypes { get; set; }
}
- ///
- /// This will iterate over the events (latest first) and filter out any events or entities in event args that are included
- /// in more recent events that Supersede previous ones. For example, If an Entity has been Saved and then Deleted, we don't want
- /// to raise the Saved event (well actually we just don't want to include it in the args for that saved event)
- ///
- ///
- ///
- private static IEnumerable FilterSupersededAndUpdateToLatestEntity(IReadOnlyList events)
+ // fixme
+ // this is way too convoluted, the superceede attribute is used only on DeleteEventargs to specify
+ // that it superceeds save, publish, move and copy - BUT - publish event args is also used for
+ // unpublishing and should NOT be superceeded - so really it should not be managed at event args
+ // level but at event level
+ //
+ // what we want is:
+ // if an entity is deleted, then all Saved, Moved, Copied, Published events prior to this should
+ // not trigger for the entity - and even though, does it make any sense? making a copy of an entity
+ // should ... trigger?
+ //
+ // not going to refactor it all - we probably want to *always* trigger event but tell people that
+ // due to scopes, they should not expected eg a saved entity to still be around - however, now,
+ // going to write a ugly condition to deal with U4-10764
+
+ // iterates over the events (latest first) and filter out any events or entities in event args that are included
+ // in more recent events that Supersede previous ones. For example, If an Entity has been Saved and then Deleted, we don't want
+ // to raise the Saved event (well actually we just don't want to include it in the args for that saved event)
+ internal static IEnumerable FilterSupersededAndUpdateToLatestEntity(IReadOnlyList events)
{
- //used to keep the 'latest' entity and associated event definition data
- var allEntities = new List>();
-
- //tracks all CancellableObjectEventArgs instances in the events which is the only type of args we can work with
- var cancelableArgs = new List();
+ // keeps the 'latest' entity and associated event data
+ var entities = new List>();
+ // collects the event definitions
+ // collects the arguments in result, that require their entities to be updated
var result = new List();
+ var resultArgs = new List();
- //This will eagerly load all of the event arg types and their attributes so we don't have to continuously look this data up
- var allArgTypesWithAttributes = events.Select(x => x.Args.GetType())
+ // eagerly fetch superceeded arg types for each arg type
+ var argTypeSuperceeding = events.Select(x => x.Args.GetType())
.Distinct()
- .ToDictionary(x => x, x => x.GetCustomAttributes(false).ToArray());
+ .ToDictionary(x => x, x => x.GetCustomAttributes(false).Select(y => y.SupersededEventArgsType).ToArray());
- //Iterate all events and collect the actual entities in them and relates them to their corresponding EventDefinitionTypeData
- //we'll process the list in reverse because events are added in the order they are raised and we want to filter out
- //any entities from event args that are not longer relevant
- //(i.e. if an item is Deleted after it's Saved, we won't include the item in the Saved args)
+ // iterate over all events and filter
+ //
+ // process the list in reverse, because events are added in the order they are raised and we want to keep
+ // the latest (most recent) entities and filter out what is not relevant anymore (too old), eg if an entity
+ // is Deleted after being Saved, we want to filter out the Saved event
for (var index = events.Count - 1; index >= 0; index--)
{
- var eventDefinition = events[index];
+ var def = events[index];
- var argType = eventDefinition.Args.GetType();
- var attributes = allArgTypesWithAttributes[eventDefinition.Args.GetType()];
-
- var meta = new EventDefinitionTypeData
+ var infos = new EventDefinitionInfos
{
- EventDefinition = eventDefinition,
- EventArgType = argType,
- SupersedeAttributes = attributes
+ EventDefinition = def,
+ SupersedeTypes = argTypeSuperceeding[def.Args.GetType()]
};
- var args = eventDefinition.Args as CancellableObjectEventArgs;
- if (args != null)
+ var args = def.Args as CancellableObjectEventArgs;
+ if (args == null)
{
- var list = TypeHelper.CreateGenericEnumerableFromObject(args.EventObject);
-
- if (list == null)
+ // not a cancellable event arg, include event definition in result
+ result.Add(def);
+ }
+ else
+ {
+ // event object can either be a single object or an enumerable of objects
+ // try to get as an enumerable, get null if it's not
+ var eventObjects = TypeHelper.CreateGenericEnumerableFromObject(args.EventObject);
+ if (eventObjects == null)
{
- //extract the event object
- var obj = args.EventObject as IEntity;
- if (obj != null)
+ // single object, cast as an IEntity
+ // if cannot cast, cannot filter, nothing - just include event definition in result
+ var eventEntity = args.EventObject as IEntity;
+ if (eventEntity == null)
{
- //Now check if this entity already exists in other event args that supersede this current event arg type
- if (IsFiltered(obj, meta, allEntities) == false)
- {
- //if it's not filtered we can adde these args to the response
- cancelableArgs.Add(args);
- result.Add(eventDefinition);
- //track the entity
- allEntities.Add(Tuple.Create(obj, meta));
- }
+ result.Add(def);
+ continue;
}
- else
+
+ // look for this entity in superceding event args
+ // found = must be removed (ie not added), else track
+ if (IsSuperceeded(eventEntity, infos, entities) == false)
{
- //Can't retrieve the entity so cant' filter or inspect, just add to the output
- result.Add(eventDefinition);
+ // track
+ entities.Add(Tuple.Create(eventEntity, infos));
+
+ // track result arguments
+ // include event definition in result
+ resultArgs.Add(args);
+ result.Add(def);
}
}
else
{
+ // enumerable of objects
var toRemove = new List();
- foreach (var entity in list)
+ foreach (var eventObject in eventObjects)
{
- //extract the event object
- var obj = entity as IEntity;
- if (obj != null)
- {
- //Now check if this entity already exists in other event args that supersede this current event arg type
- if (IsFiltered(obj, meta, allEntities))
- {
- //track it to be removed
- toRemove.Add(obj);
- }
- else
- {
- //track the entity, it's not filtered
- allEntities.Add(Tuple.Create(obj, meta));
- }
- }
+ // extract the event object, cast as an IEntity
+ // if cannot cast, cannot filter, nothing to do - just leave it in the list & continue
+ var eventEntity = eventObject as IEntity;
+ if (eventEntity == null)
+ continue;
+
+ // look for this entity in superceding event args
+ // found = must be removed, else track
+ if (IsSuperceeded(eventEntity, infos, entities))
+ toRemove.Add(eventEntity);
else
- {
- //we don't need to do anything here, we can't cast to IEntity so we cannot filter, so it will just remain in the list
- }
+ entities.Add(Tuple.Create(eventEntity, infos));
}
- //remove anything that has been filtered
+ // remove superceded entities
foreach (var entity in toRemove)
- {
- list.Remove(entity);
- }
+ eventObjects.Remove(entity);
- //track the event and include in the response if there's still entities remaining in the list
- if (list.Count > 0)
+ // if there are still entities in the list, keep the event definition
+ if (eventObjects.Count > 0)
{
if (toRemove.Count > 0)
{
- //re-assign if the items have changed
- args.EventObject = list;
+ // re-assign if changed
+ args.EventObject = eventObjects;
}
- cancelableArgs.Add(args);
- result.Add(eventDefinition);
+
+ // track result arguments
+ // include event definition in result
+ resultArgs.Add(args);
+ result.Add(def);
}
}
}
- else
- {
- //it's not a cancelable event arg so we just include it in the result
- result.Add(eventDefinition);
- }
}
- //Now we'll deal with ensuring that only the latest(non stale) entities are used throughout all event args
- UpdateToLatestEntities(allEntities, cancelableArgs);
+ // go over all args in result, and update them with the latest instanceof each entity
+ UpdateToLatestEntities(entities, resultArgs);
- //we need to reverse the result since we've been adding by latest added events first!
+ // reverse, since we processed the list in reverse
result.Reverse();
return result;
}
- private static void UpdateToLatestEntities(IEnumerable> allEntities, IEnumerable cancelableArgs)
+ // edits event args to use the latest instance of each entity
+ private static void UpdateToLatestEntities(IEnumerable> entities, IEnumerable args)
{
- //Now we'll deal with ensuring that only the latest(non stale) entities are used throughout all event args
-
+ // get the latest entities
+ // ordered hash set + keepOldest will keep the latest inserted entity (in case of duplicates)
var latestEntities = new OrderedHashSet(keepOldest: true);
- foreach (var entity in allEntities.OrderByDescending(entity => entity.Item1.UpdateDate))
- {
+ foreach (var entity in entities.OrderByDescending(entity => entity.Item1.UpdateDate))
latestEntities.Add(entity.Item1);
- }
- foreach (var args in cancelableArgs)
+ foreach (var arg in args)
{
- var list = TypeHelper.CreateGenericEnumerableFromObject(args.EventObject);
- if (list == null)
+ // event object can either be a single object or an enumerable of objects
+ // try to get as an enumerable, get null if it's not
+ var eventObjects = TypeHelper.CreateGenericEnumerableFromObject(arg.EventObject);
+ if (eventObjects == null)
{
- //try to find the args entity in the latest entity - based on the equality operators, this will
- //match by Id since that is the default equality checker for IEntity. If one is found, than it is
- //the most recent entity instance so update the args with that instance so we don't emit a stale instance.
- var foundEntity = latestEntities.FirstOrDefault(x => Equals(x, args.EventObject));
+ // single object
+ // look for a more recent entity for that object, and replace if any
+ // works by "equalling" entities ie the more recent one "equals" this one (though different object)
+ var foundEntity = latestEntities.FirstOrDefault(x => Equals(x, arg.EventObject));
if (foundEntity != null)
- {
- args.EventObject = foundEntity;
- }
+ arg.EventObject = foundEntity;
}
else
{
+ // enumerable of objects
+ // same as above but for each object
var updated = false;
-
- for (int i = 0; i < list.Count; i++)
+ for (var i = 0; i < eventObjects.Count; i++)
{
- //try to find the args entity in the latest entity - based on the equality operators, this will
- //match by Id since that is the default equality checker for IEntity. If one is found, than it is
- //the most recent entity instance so update the args with that instance so we don't emit a stale instance.
- var foundEntity = latestEntities.FirstOrDefault(x => Equals(x, list[i]));
- if (foundEntity != null)
- {
- list[i] = foundEntity;
- updated = true;
- }
+ var foundEntity = latestEntities.FirstOrDefault(x => Equals(x, eventObjects[i]));
+ if (foundEntity == null) continue;
+ eventObjects[i] = foundEntity;
+ updated = true;
}
if (updated)
- {
- args.EventObject = list;
- }
+ arg.EventObject = eventObjects;
}
}
}
- ///
- /// This will check against all of the processed entity/events (allEntities) to see if this entity already exists in
- /// event args that supersede the event args being passed in and if so returns true.
- ///
- ///
- ///
- ///
- ///
- private static bool IsFiltered(
- IEntity entity,
- EventDefinitionTypeData eventDef,
- List> allEntities)
+ // determines if a given entity, appearing in a given event definition, should be filtered out,
+ // considering the entities that have already been visited - an entity is filtered out if it
+ // appears in another even definition, which superceedes this event definition.
+ private static bool IsSuperceeded(IEntity entity, EventDefinitionInfos infos, List> entities)
{
- var argType = eventDef.EventDefinition.Args.GetType();
+ //var argType = meta.EventArgsType;
+ var argType = infos.EventDefinition.Args.GetType();
- //check if the entity is found in any processed event data that could possible supersede this one
- var foundByEntity = allEntities
- .Where(x => x.Item2.SupersedeAttributes.Length > 0
- //if it's the same arg type than it cannot supersede
- && x.Item2.EventArgType != argType
- && Equals(x.Item1, entity))
+ // look for other instances of the same entity, coming from an event args that supercedes other event args,
+ // ie is marked with the attribute, and is not this event args (cannot supersede itself)
+ var superceeding = entities
+ .Where(x => x.Item2.SupersedeTypes.Length > 0 // has the attribute
+ && x.Item2.EventDefinition.Args.GetType() != argType // is not the same
+ && Equals(x.Item1, entity)) // same entity
.ToArray();
- //no args have been processed with this entity so it should not be filtered
- if (foundByEntity.Length == 0)
+ // first time we see this entity = not filtered
+ if (superceeding.Length == 0)
return false;
+ // fixme see notes above
+ // delete event args does NOT superceedes 'unpublished' event
+ if (argType.IsGenericType && argType.GetGenericTypeDefinition() == typeof(PublishEventArgs<>) && infos.EventDefinition.EventName == "UnPublished")
+ return false;
+
+ // found occurences, need to determine if this event args is superceded
if (argType.IsGenericType)
{
- var supercededBy = foundByEntity
- .FirstOrDefault(x =>
- x.Item2.SupersedeAttributes.Any(y =>
- //if the attribute type is a generic type def then compare with the generic type def of the event arg
- (y.SupersededEventArgsType.IsGenericTypeDefinition && y.SupersededEventArgsType == argType.GetGenericTypeDefinition())
- //if the attribute type is not a generic type def then compare with the normal type of the event arg
- || (y.SupersededEventArgsType.IsGenericTypeDefinition == false && y.SupersededEventArgsType == argType)));
+ // generic, must compare type arguments
+ var supercededBy = superceeding.FirstOrDefault(x =>
+ x.Item2.SupersedeTypes.Any(y =>
+ // superceeding a generic type which has the same generic type definition
+ // fixme no matter the generic type parameters? could be different?
+ y.IsGenericTypeDefinition && y == argType.GetGenericTypeDefinition()
+ // or superceeding a non-generic type which is ... fixme how is this ever possible? argType *is* generic?
+ || y.IsGenericTypeDefinition == false && y == argType));
return supercededBy != null;
}
else
{
- var supercededBy = foundByEntity
- .FirstOrDefault(x =>
- x.Item2.SupersedeAttributes.Any(y =>
- //since the event arg type is not a generic type, then we just compare type 1:1
- y.SupersededEventArgsType == argType));
+ // non-generic, can compare types 1:1
+ var supercededBy = superceeding.FirstOrDefault(x =>
+ x.Item2.SupersedeTypes.Any(y => y == argType));
return supercededBy != null;
}
}
diff --git a/src/Umbraco.Core/Events/RolesEventArgs.cs b/src/Umbraco.Core/Events/RolesEventArgs.cs
new file mode 100644
index 0000000000..3104412f99
--- /dev/null
+++ b/src/Umbraco.Core/Events/RolesEventArgs.cs
@@ -0,0 +1,16 @@
+using System;
+
+namespace Umbraco.Core.Events
+{
+ public class RolesEventArgs : EventArgs
+ {
+ public RolesEventArgs(int[] memberIds, string[] roles)
+ {
+ MemberIds = memberIds;
+ Roles = roles;
+ }
+
+ public int[] MemberIds { get; set; }
+ public string[] Roles { get; set; }
+ }
+}
diff --git a/src/Umbraco.Core/Events/UserGroupWithUsers.cs b/src/Umbraco.Core/Events/UserGroupWithUsers.cs
new file mode 100644
index 0000000000..b69650d33f
--- /dev/null
+++ b/src/Umbraco.Core/Events/UserGroupWithUsers.cs
@@ -0,0 +1,19 @@
+using System;
+using Umbraco.Core.Models.Membership;
+
+namespace Umbraco.Core.Events
+{
+ internal class UserGroupWithUsers
+ {
+ public UserGroupWithUsers(IUserGroup userGroup, IUser[] addedUsers, IUser[] removedUsers)
+ {
+ UserGroup = userGroup;
+ AddedUsers = addedUsers;
+ RemovedUsers = removedUsers;
+ }
+
+ public IUserGroup UserGroup { get; }
+ public IUser[] AddedUsers { get; }
+ public IUser[] RemovedUsers { get; }
+ }
+}
diff --git a/src/Umbraco.Core/HashGenerator.cs b/src/Umbraco.Core/HashGenerator.cs
index 0f03e22a9b..f8e45c362d 100644
--- a/src/Umbraco.Core/HashGenerator.cs
+++ b/src/Umbraco.Core/HashGenerator.cs
@@ -14,7 +14,7 @@ namespace Umbraco.Core
/// This will use the crypto libs to generate the hash and will try to ensure that
/// strings, etc... are not re-allocated so it's not consuming much memory.
///
- internal class HashGenerator : DisposableObject
+ internal class HashGenerator : DisposableObjectSlim
{
public HashGenerator()
{
diff --git a/src/Umbraco.Core/HttpContextExtensions.cs b/src/Umbraco.Core/HttpContextExtensions.cs
index df134ad7ca..e370b055a4 100644
--- a/src/Umbraco.Core/HttpContextExtensions.cs
+++ b/src/Umbraco.Core/HttpContextExtensions.cs
@@ -40,13 +40,13 @@ namespace Umbraco.Core
var ipAddress = httpContext.Request.ServerVariables["HTTP_X_FORWARDED_FOR"];
if (string.IsNullOrEmpty(ipAddress))
- return httpContext.Request.ServerVariables["REMOTE_ADDR"];
+ return httpContext.Request.UserHostAddress;
var addresses = ipAddress.Split(',');
if (addresses.Length != 0)
return addresses[0];
- return httpContext.Request.ServerVariables["REMOTE_ADDR"];
+ return httpContext.Request.UserHostAddress;
}
catch (System.Exception ex)
{
diff --git a/src/Umbraco.Core/IO/IOHelper.cs b/src/Umbraco.Core/IO/IOHelper.cs
index 94d2ccfe7c..5a2928f795 100644
--- a/src/Umbraco.Core/IO/IOHelper.cs
+++ b/src/Umbraco.Core/IO/IOHelper.cs
@@ -13,11 +13,22 @@ namespace Umbraco.Core.IO
{
public static class IOHelper
{
+ ///
+ /// Gets or sets a value forcing Umbraco to consider it is non-hosted.
+ ///
+ /// This should always be false, unless unit testing.
+ public static bool ForceNotHosted { get; set; }
+
private static string _rootDir = "";
// static compiled regex for faster performance
//private static readonly Regex ResolveUrlPattern = new Regex("(=[\"\']?)(\\W?\\~(?:.(?![\"\']?\\s+(?:\\S+)=|[>\"\']))+.)[\"\']?", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
+ ///
+ /// Gets a value indicating whether Umbraco is hosted.
+ ///
+ public static bool IsHosted => !ForceNotHosted && (HttpContext.Current != null || HostingEnvironment.IsHosted);
+
public static char DirSepChar => Path.DirectorySeparatorChar;
internal static void UnZip(string zipFilePath, string unPackDirectory, bool deleteZipFile)
@@ -80,6 +91,7 @@ namespace Umbraco.Core.IO
public static string MapPath(string path, bool useHttpContext)
{
if (path == null) throw new ArgumentNullException("path");
+ useHttpContext = useHttpContext && IsHosted;
// Check if the path is already mapped
if ((path.Length >= 2 && path[1] == Path.VolumeSeparatorChar)
diff --git a/src/Umbraco.Core/IO/PhysicalFileSystem.cs b/src/Umbraco.Core/IO/PhysicalFileSystem.cs
index 3b9cac42a7..9a2c6eb1de 100644
--- a/src/Umbraco.Core/IO/PhysicalFileSystem.cs
+++ b/src/Umbraco.Core/IO/PhysicalFileSystem.cs
@@ -4,6 +4,7 @@ using System.IO;
using System.Linq;
using Umbraco.Core.Composing;
using Umbraco.Core.Exceptions;
+using System.Threading;
using Umbraco.Core.Logging;
namespace Umbraco.Core.IO
@@ -104,7 +105,7 @@ namespace Umbraco.Core.IO
try
{
- Directory.Delete(fullPath, recursive);
+ WithRetry(() => Directory.Delete(fullPath, recursive));
}
catch (DirectoryNotFoundException ex)
{
@@ -221,7 +222,7 @@ namespace Umbraco.Core.IO
try
{
- File.Delete(fullPath);
+ WithRetry(() => File.Delete(fullPath));
}
catch (FileNotFoundException ex)
{
@@ -371,7 +372,7 @@ namespace Umbraco.Core.IO
{
if (overrideIfExists == false)
throw new InvalidOperationException($"A file at path '{path}' already exists");
- File.Delete(fullPath);
+ WithRetry(() => File.Delete(fullPath));
}
var directory = Path.GetDirectoryName(fullPath);
@@ -379,9 +380,9 @@ namespace Umbraco.Core.IO
Directory.CreateDirectory(directory); // ensure it exists
if (copy)
- File.Copy(physicalPath, fullPath);
+ WithRetry(() => File.Copy(physicalPath, fullPath));
else
- File.Move(physicalPath, fullPath);
+ WithRetry(() => File.Move(physicalPath, fullPath));
}
#region Helper Methods
@@ -410,6 +411,35 @@ namespace Umbraco.Core.IO
return path;
}
+ protected void WithRetry(Action action)
+ {
+ // 10 times 100ms is 1s
+ const int count = 10;
+ const int pausems = 100;
+
+ for (var i = 0;; i++)
+ {
+ try
+ {
+ action();
+ break; // done
+ }
+ catch (IOException e)
+ {
+ // if it's not *exactly* IOException then it could be
+ // some inherited exception such as FileNotFoundException,
+ // and then we don't want to retry
+ if (e.GetType() != typeof(IOException)) throw;
+
+ // if we have tried enough, throw, else swallow
+ // the exception and retry after a pause
+ if (i == count) throw;
+ }
+
+ Thread.Sleep(pausems);
+ }
+ }
+
#endregion
}
}
diff --git a/src/Umbraco.Core/IO/SystemFiles.cs b/src/Umbraco.Core/IO/SystemFiles.cs
index fa22a9a447..20b7bf6a3e 100644
--- a/src/Umbraco.Core/IO/SystemFiles.cs
+++ b/src/Umbraco.Core/IO/SystemFiles.cs
@@ -22,19 +22,19 @@ namespace Umbraco.Core.IO
{
get
{
- switch (GlobalSettings.ContentCacheXmlStorageLocation)
+ switch (GlobalSettings.LocalTempStorageLocation)
{
- case ContentXmlStorage.AspNetTemp:
+ case LocalTempStorage.AspNetTemp:
return Path.Combine(HttpRuntime.CodegenDir, @"UmbracoData\umbraco.config");
- case ContentXmlStorage.EnvironmentTemp:
+ case LocalTempStorage.EnvironmentTemp:
var appDomainHash = HttpRuntime.AppDomainAppId.ToSHA1();
- var cachePath = Path.Combine(Environment.ExpandEnvironmentVariables("%temp%"), "UmbracoXml",
+ var cachePath = Path.Combine(Environment.ExpandEnvironmentVariables("%temp%"), "UmbracoData",
//include the appdomain hash is just a safety check, for example if a website is moved from worker A to worker B and then back
// to worker A again, in theory the %temp% folder should already be empty but we really want to make sure that its not
// utilizing an old path
appDomainHash);
return Path.Combine(cachePath, "umbraco.config");
- case ContentXmlStorage.Default:
+ case LocalTempStorage.Default:
return IOHelper.ReturnPath("umbracoContentXML", "~/App_Data/umbraco.config");
default:
throw new ArgumentOutOfRangeException();
diff --git a/src/Umbraco.Core/Logging/RollingFileCleanupAppender.cs b/src/Umbraco.Core/Logging/RollingFileCleanupAppender.cs
new file mode 100644
index 0000000000..6be2552296
--- /dev/null
+++ b/src/Umbraco.Core/Logging/RollingFileCleanupAppender.cs
@@ -0,0 +1,95 @@
+using System;
+using System.IO;
+using log4net.Appender;
+using log4net.Util;
+
+namespace Umbraco.Core.Logging
+{
+ ///
+ /// This class will do the exact same thing as the RollingFileAppender that comes from log4net
+ /// With the extension, that it is able to do automatic cleanup of the logfiles in the directory where logging happens
+ ///
+ /// By specifying the properties MaxLogFileDays and BaseFilePattern, the files will automaticly get deleted when
+ /// the logger is configured(typically when the app starts). To utilize this appender swap out the type of the rollingFile appender
+ /// that ships with Umbraco, to be Umbraco.Core.Logging.RollingFileCleanupAppender, and add the maxLogFileDays and baseFilePattern elements
+ /// to the configuration i.e.:
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ public class RollingFileCleanupAppender : RollingFileAppender
+ {
+ public int MaxLogFileDays { get; set; }
+ public string BaseFilePattern { get; set; }
+
+ ///
+ /// This override will delete logs older than the specified amount of days
+ ///
+ ///
+ ///
+ protected override void OpenFile(string fileName, bool append)
+ {
+ bool cleanup = true;
+ // Validate settings and input
+ if (MaxLogFileDays <= 0)
+ {
+ LogLog.Warn(typeof(RollingFileCleanupAppender), "Parameter 'MaxLogFileDays' needs to be a positive integer, aborting cleanup");
+ cleanup = false;
+ }
+
+ if (string.IsNullOrWhiteSpace(BaseFilePattern))
+ {
+ LogLog.Warn(typeof(RollingFileCleanupAppender), "Parameter 'BaseFilePattern' is empty, aborting cleanup");
+ cleanup = false;
+ }
+ // grab the directory we are logging to, as this is were we will search for older logfiles
+ var logFolder = Path.GetDirectoryName(fileName);
+ if (Directory.Exists(logFolder) == false)
+ {
+ LogLog.Warn(typeof(RollingFileCleanupAppender), string.Format("Directory '{0}' for logfiles does not exist, aborting cleanup", logFolder));
+ cleanup = false;
+ }
+ // If everything is validated, we can do the actual cleanup
+ if (cleanup)
+ {
+ Cleanup(logFolder);
+ }
+
+ base.OpenFile(fileName, append);
+ }
+
+ private void Cleanup(string directoryPath)
+ {
+ // only take files that matches the pattern we are using i.e. UmbracoTraceLog.*.txt.*
+ string[] logFiles = Directory.GetFiles(directoryPath, BaseFilePattern);
+ LogLog.Debug(typeof(RollingFileCleanupAppender), string.Format("Found {0} files that matches the baseFilePattern: '{1}'", logFiles.Length, BaseFilePattern));
+
+ foreach (var logFile in logFiles)
+ {
+ DateTime lastAccessTime = System.IO.File.GetLastWriteTimeUtc(logFile);
+ // take the value from the config file
+ if (lastAccessTime < DateTime.Now.AddDays(-MaxLogFileDays))
+ {
+ LogLog.Debug(typeof(RollingFileCleanupAppender), string.Format("Deleting file {0} as its lastAccessTime is older than {1} days speficied by MaxLogFileDays", logFile, MaxLogFileDays));
+ base.DeleteFile(logFile);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Manifest/ManifestWatcher.cs b/src/Umbraco.Core/Manifest/ManifestWatcher.cs
index 014721eb88..3bc70e2d78 100644
--- a/src/Umbraco.Core/Manifest/ManifestWatcher.cs
+++ b/src/Umbraco.Core/Manifest/ManifestWatcher.cs
@@ -7,7 +7,7 @@ using Umbraco.Core.Logging;
namespace Umbraco.Core.Manifest
{
- internal class ManifestWatcher : DisposableObject
+ internal class ManifestWatcher : DisposableObjectSlim
{
private static readonly object Locker = new object();
private static volatile bool _isRestarting;
diff --git a/src/Umbraco.Core/Migrations/Expressions/Insert/Expressions/InsertDataExpression.cs b/src/Umbraco.Core/Migrations/Expressions/Insert/Expressions/InsertDataExpression.cs
index 7fbd7765e3..7efe457402 100644
--- a/src/Umbraco.Core/Migrations/Expressions/Insert/Expressions/InsertDataExpression.cs
+++ b/src/Umbraco.Core/Migrations/Expressions/Insert/Expressions/InsertDataExpression.cs
@@ -32,19 +32,25 @@ namespace Umbraco.Core.Migrations.Expressions.Insert.Expressions
{
foreach (var item in Rows)
{
- var cols = "";
- var vals = "";
+ var cols = new StringBuilder();
+ var vals = new StringBuilder();
+ var first = true;
foreach (var keyVal in item)
{
- cols += SqlSyntax.GetQuotedColumnName(keyVal.Key) + ",";
- vals += GetQuotedValue(keyVal.Value) + ",";
+ if (first)
+ {
+ first = false;
+ }
+ else
+ {
+ cols.Append(",");
+ vals.Append(",");
+ }
+ cols.Append(SqlSyntax.GetQuotedColumnName(keyVal.Key));
+ vals.Append(GetQuotedValue(keyVal.Value));
}
- cols = cols.TrimEnd(',');
- vals = vals.TrimEnd(',');
- var sql = string.Format(SqlSyntax.InsertData,
- SqlSyntax.GetQuotedTableName(TableName),
- cols, vals);
+ var sql = string.Format(SqlSyntax.InsertData, SqlSyntax.GetQuotedTableName(TableName), cols, vals);
stmts.Append(sql);
AppendStatementSeparator(stmts);
diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs
index 1371486ce5..f12533a5de 100644
--- a/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs
+++ b/src/Umbraco.Core/Migrations/Install/DatabaseDataCreator.cs
@@ -153,11 +153,13 @@ namespace Umbraco.Core.Migrations.Install
_database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 2, StartMediaId = -1, StartContentId = -1, Alias = "writer", Name = "Writers", DefaultPermissions = "CAH:F", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-edit" });
_database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 3, StartMediaId = -1, StartContentId = -1, Alias = "editor", Name = "Editors", DefaultPermissions = "CADMOSKTPUZ:5Fï", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-tools" });
_database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 4, StartMediaId = -1, StartContentId = -1, Alias = Constants.Security.TranslatorGroupAlias, Name = "Translators", DefaultPermissions = "AF", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-globe" });
+ _database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", false, new UserGroupDto { Id = 5, StartMediaId = -1, StartContentId = -1, Alias = Constants.Security.SensitiveDataGroupAlias, Name = "Sensitive data", DefaultPermissions = "", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-lock" });
}
private void CreateUser2UserGroupData()
{
- _database.Insert(new User2UserGroupDto { UserGroupId = 1, UserId = Constants.Security.SuperId });
+ _database.Insert(new User2UserGroupDto { UserGroupId = 1, UserId = Constants.Security.SuperId }); // add super to admins
+ _database.Insert(new User2UserGroupDto { UserGroupId = 5, UserId = Constants.Security.SuperId }); // add super to sensitive data
}
private void CreateUserGroup2AppData()
diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs
index dfd173681b..cc9c0f3893 100644
--- a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs
+++ b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaCreator.cs
@@ -28,71 +28,60 @@ namespace Umbraco.Core.Migrations.Install
private ISqlSyntaxProvider SqlSyntax => _database.SqlContext.SqlSyntax;
// all tables, in order
- public static readonly Dictionary OrderedTables = new Dictionary
+ public static readonly List OrderedTables = new List
{
- {0, typeof (NodeDto)},
- {1, typeof (ContentTypeDto)},
- {2, typeof (TemplateDto)},
- {3, typeof (ContentDto)},
- {4, typeof (ContentVersionDto)},
- {5, typeof (DocumentDto)},
- {6, typeof (ContentTypeTemplateDto)},
- {7, typeof (DataTypeDto)},
- //removed: {8, typeof (DataTypePreValueDto)},
- {9, typeof (DictionaryDto)},
-
- {10, typeof (LanguageDto)},
- {11, typeof (LanguageTextDto)},
- {12, typeof (DomainDto)},
- {13, typeof (LogDto)},
- {14, typeof (MacroDto)},
- {15, typeof (MacroPropertyDto)},
- {16, typeof (MemberTypeDto)},
- {17, typeof (MemberDto)},
- {18, typeof (Member2MemberGroupDto)},
- {19, typeof (ContentXmlDto)},
-
- {20, typeof (PreviewXmlDto)},
- {21, typeof (PropertyTypeGroupDto)},
- {22, typeof (PropertyTypeDto)},
- {23, typeof (PropertyDataDto)},
- {24, typeof (RelationTypeDto)},
- {25, typeof (RelationDto)},
- //removed: {26...
- //removed: {27...
- {28, typeof (TagDto)},
- {29, typeof (TagRelationshipDto)},
-
- //removed: {30...
- //removed in 7.6: {31, typeof (UserTypeDto)},
- {32, typeof (UserDto)},
- {33, typeof (TaskTypeDto)},
- {34, typeof (TaskDto)},
- {35, typeof (ContentType2ContentTypeDto)},
- {36, typeof (ContentTypeAllowedContentTypeDto)},
- //removed in 7.6: {37, typeof (User2AppDto)},
- {38, typeof (User2NodeNotifyDto)},
- //removed in 7.6: {39, typeof (User2NodePermissionDto)},
-
- {40, typeof (ServerRegistrationDto)},
- {41, typeof (AccessDto)},
- {42, typeof (AccessRuleDto)},
- {43, typeof (CacheInstructionDto)},
- {44, typeof (ExternalLoginDto)},
- //removed: {45, typeof (MigrationDto)},
- //removed: {46, typeof (UmbracoDeployChecksumDto)},
- //removed: {47, typeof (UmbracoDeployDependencyDto)},
- {48, typeof (RedirectUrlDto) },
- {49, typeof (LockDto) },
-
- {50, typeof (UserGroupDto) },
- {51, typeof (User2UserGroupDto) },
- {52, typeof (UserGroup2NodePermissionDto) },
- {53, typeof (UserGroup2AppDto) },
- {54, typeof (UserStartNodeDto) },
- {55, typeof (ContentNuDto) },
- {56, typeof (DocumentVersionDto) },
- {57, typeof (KeyValueDto) }
+ typeof (NodeDto),
+ typeof (ContentTypeDto),
+ typeof (TemplateDto),
+ typeof (ContentDto),
+ typeof (ContentVersionDto),
+ typeof (MediaVersionDto),
+ typeof (DocumentDto),
+ typeof (ContentTypeTemplateDto),
+ typeof (DataTypeDto),
+ typeof (DictionaryDto),
+ typeof (LanguageDto),
+ typeof (LanguageTextDto),
+ typeof (DomainDto),
+ typeof (LogDto),
+ typeof (MacroDto),
+ typeof (MacroPropertyDto),
+ typeof (MemberTypeDto),
+ typeof (MemberDto),
+ typeof (Member2MemberGroupDto),
+ typeof (ContentXmlDto),
+ typeof (PreviewXmlDto),
+ typeof (PropertyTypeGroupDto),
+ typeof (PropertyTypeDto),
+ typeof (PropertyDataDto),
+ typeof (RelationTypeDto),
+ typeof (RelationDto),
+ typeof (TagDto),
+ typeof (TagRelationshipDto),
+ typeof (UserDto),
+ typeof (TaskTypeDto),
+ typeof (TaskDto),
+ typeof (ContentType2ContentTypeDto),
+ typeof (ContentTypeAllowedContentTypeDto),
+ typeof (User2NodeNotifyDto),
+ typeof (ServerRegistrationDto),
+ typeof (AccessDto),
+ typeof (AccessRuleDto),
+ typeof (CacheInstructionDto),
+ typeof (ExternalLoginDto),
+ typeof (RedirectUrlDto),
+ typeof (LockDto),
+ typeof (UserGroupDto),
+ typeof (User2UserGroupDto),
+ typeof (UserGroup2NodePermissionDto),
+ typeof (UserGroup2AppDto),
+ typeof (UserStartNodeDto),
+ typeof (ContentNuDto),
+ typeof (DocumentVersionDto),
+ typeof (KeyValueDto),
+ typeof (UserLoginDto),
+ typeof (ConsentDto),
+ typeof (AuditEntryDto)
};
///
@@ -102,11 +91,10 @@ namespace Umbraco.Core.Migrations.Install
{
_logger.Info("Start UninstallDatabaseSchema");
- foreach (var item in OrderedTables.OrderByDescending(x => x.Key))
+ foreach (var table in OrderedTables.AsEnumerable().Reverse())
{
- var tableNameAttribute = item.Value.FirstAttribute();
-
- var tableName = tableNameAttribute == null ? item.Value.Name : tableNameAttribute.Value;
+ var tableNameAttribute = table.FirstAttribute();
+ var tableName = tableNameAttribute == null ? table.Name : tableNameAttribute.Value;
_logger.Info("Uninstall" + tableName);
@@ -135,8 +123,8 @@ namespace Umbraco.Core.Migrations.Install
if (e.Cancel == false)
{
var dataCreation = new DatabaseDataCreator(_database, _logger);
- foreach (var item in OrderedTables.OrderBy(x => x.Key))
- CreateTable(false, item.Value, dataCreation);
+ foreach (var table in OrderedTables)
+ CreateTable(false, table, dataCreation);
}
FireAfterCreation(e);
@@ -160,8 +148,7 @@ namespace Umbraco.Core.Migrations.Install
}).ToArray();
result.TableDefinitions.AddRange(OrderedTables
- .OrderBy(x => x.Key)
- .Select(x => DefinitionFactory.GetTableDefinition(x.Value, SqlSyntax)));
+ .Select(x => DefinitionFactory.GetTableDefinition(x, SqlSyntax)));
ValidateDbTables(result);
ValidateDbColumns(result);
diff --git a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaResult.cs b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaResult.cs
index 41184c4471..0ec27cf0b1 100644
--- a/src/Umbraco.Core/Migrations/Install/DatabaseSchemaResult.cs
+++ b/src/Umbraco.Core/Migrations/Install/DatabaseSchemaResult.cs
@@ -136,6 +136,18 @@ namespace Umbraco.Core.Migrations.Install
return new Version(7, 6, 0);
}
+ //if the error is for cmsMedia it must be the previous version to 7.8 since that is when it is added
+ if (Errors.Any(x => x.Item1.Equals("Table") && (x.Item2.InvariantEquals("umbracoMedia"))))
+ {
+ return new Version(7, 7, 0);
+ }
+
+ //if the error is for isSensitive column it must be the previous version to 7.9 since that is when it is added
+ if (Errors.Any(x => x.Item1.Equals("Column") && (x.Item2.InvariantEquals("cmsMemberType,isSensitive"))))
+ {
+ return new Version(7, 8, 0);
+ }
+
return UmbracoVersion.Current;
}
diff --git a/src/Umbraco.Core/Migrations/MigrationBase_Extra.cs b/src/Umbraco.Core/Migrations/MigrationBase_Extra.cs
index 9f4c45f0ad..b1b405bcf4 100644
--- a/src/Umbraco.Core/Migrations/MigrationBase_Extra.cs
+++ b/src/Umbraco.Core/Migrations/MigrationBase_Extra.cs
@@ -12,19 +12,41 @@ namespace Umbraco.Core.Migrations
{
// provides extra methods for migrations
- //fixme - why do we have tableName and provide a table type which we can just extract the table name from?
+ protected void AddColumn(string columnName)
+ {
+ var table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax);
+ AddColumn(table, table.Name, columnName);
+ }
+
protected void AddColumn(string tableName, string columnName)
+ {
+ var table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax);
+ AddColumn(table, tableName, columnName);
+ }
+
+ private void AddColumn(TableDefinition table, string tableName, string columnName)
{
if (ColumnExists(tableName, columnName)) return;
- var table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax);
var column = table.Columns.First(x => x.Name == columnName);
var createSql = SqlSyntax.Format(column);
- Execute.Sql(string.Format(SqlSyntax.AddColumn, SqlSyntax.GetQuotedTableName(table.Name), createSql)).Do();
+ Execute.Sql(string.Format(SqlSyntax.AddColumn, SqlSyntax.GetQuotedTableName(tableName), createSql)).Do();
+ }
+
+ protected void AddColumn(string columnName, out IEnumerable sqls)
+ {
+ var table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax);
+ AddColumn(table, table.Name, columnName, out sqls);
}
protected void AddColumn(string tableName, string columnName, out IEnumerable sqls)
+ {
+ var table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax);
+ AddColumn(table, tableName, columnName, out sqls);
+ }
+
+ private void AddColumn(TableDefinition table, string tableName, string columnName, out IEnumerable sqls)
{
if (ColumnExists(tableName, columnName))
{
@@ -32,7 +54,6 @@ namespace Umbraco.Core.Migrations
return;
}
- var table = DefinitionFactory.GetTableDefinition(typeof(T), SqlSyntax);
var column = table.Columns.First(x => x.Name == columnName);
var createSql = SqlSyntax.Format(column, SqlSyntax.GetQuotedTableName(tableName), out sqls);
Execute.Sql(string.Format(SqlSyntax.AddColumn, SqlSyntax.GetQuotedTableName(tableName), createSql)).Do();
diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs
index 03e52e6e97..fb0b3fc670 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs
@@ -7,6 +7,8 @@ using Umbraco.Core.Migrations.Upgrade.V_7_5_0;
using Umbraco.Core.Migrations.Upgrade.V_7_5_5;
using Umbraco.Core.Migrations.Upgrade.V_7_6_0;
using Umbraco.Core.Migrations.Upgrade.V_7_7_0;
+using Umbraco.Core.Migrations.Upgrade.V_7_8_0;
+using Umbraco.Core.Migrations.Upgrade.V_7_9_0;
using Umbraco.Core.Migrations.Upgrade.V_8_0_0;
namespace Umbraco.Core.Migrations.Upgrade
@@ -36,23 +38,25 @@ namespace Umbraco.Core.Migrations.Upgrade
if (!SemVersion.TryParse(ConfigurationManager.AppSettings["umbracoConfigurationStatus"], out var currentVersion))
throw new InvalidOperationException("Could not get current version from web.config umbracoConfigurationStatus appSetting.");
- // must be at least 7.8.0 - fixme adjust when releasing
- if (currentVersion < new SemVersion(7, 8))
+ // must be at least 7.?.? - fixme adjust when releasing
+ if (currentVersion < new SemVersion(7, 999))
throw new InvalidOperationException($"Version {currentVersion} cannot be upgraded to {UmbracoVersion.SemanticVersion}.");
// cannot go back in time
if (currentVersion > UmbracoVersion.SemanticVersion)
- throw new InvalidOperationException($"Version {currentVersion} cannot be upgraded to {UmbracoVersion.SemanticVersion}.");
+ throw new InvalidOperationException($"Version {currentVersion} cannot be downgraded to {UmbracoVersion.SemanticVersion}.");
switch (currentVersion.Major)
{
case 7:
+ // upgrading from version 7
return "{orig-" + currentVersion + "}";
case 8: // fixme remove when releasing
- // this is very temp and for my own website - zpqrtbnk
+ // upgrading from version 8
+ // should never happen, this is very temp and for my own website - zpqrtbnk
return "{04F54303-3055-4700-8F76-35A37F232FF5}"; // right before the variants migration
default:
- throw new InvalidOperationException($"Version {currentVersion} should have an upgrade state in the key-value table.");
+ throw new InvalidOperationException($"Version {currentVersion} is not supported by the migration plan.");
}
}
@@ -60,12 +64,6 @@ namespace Umbraco.Core.Migrations.Upgrade
private void DefinePlan()
{
- // INSTALL
- //
- // when installing, the source state is empty, and the target state should be the final state.
-
- Add(string.Empty, "{7F0BF916-F64E-4B25-864A-170D6E6B68E5}");
-
// UPGRADE FROM 7
//
// when 8.0.0 is released, on the first upgrade, the state is automatically
@@ -74,46 +72,38 @@ namespace Umbraco.Core.Migrations.Upgrade
// then, as more v7 and v8 versions are released, new chains needs to be defined to
// support the upgrades (new v7 may backport some migrations and require their own
// upgrade paths, etc).
+ // fixme adjust when releasing
- From("{init-7.8.0}")
- .Chain("{7C447271-CA3F-4A6A-A913-5D77015655CB}") // add more lock objects
- .Chain("{CBFF58A2-7B50-4F75-8E98-249920DB0F37}")
- .Chain("{3D18920C-E84D-405C-A06A-B7CEE52FE5DD}")
- .Chain("{FB0A5429-587E-4BD0-8A67-20F0E7E62FF7}")
- .Chain("{F0C42457-6A3B-4912-A7EA-F27ED85A2092}")
- .Chain("{8640C9E4-A1C0-4C59-99BB-609B4E604981}")
- .Chain("{DD1B99AF-8106-4E00-BAC7-A43003EA07F8}")
- .Chain("{9DF05B77-11D1-475C-A00A-B656AF7E0908}")
- .Chain("{9E98CF10-3AE9-437B-AF54-8697D251A541}")
- .Chain("{7F0BF916-F64E-4B25-864A-170D6E6B68E5}");
+ From("{init-7.8.0}");
+ Chain("{7C447271-CA3F-4A6A-A913-5D77015655CB}"); // add more lock objects
+ Chain("{CBFF58A2-7B50-4F75-8E98-249920DB0F37}");
+ Chain("{3D18920C-E84D-405C-A06A-B7CEE52FE5DD}");
+ Chain("{FB0A5429-587E-4BD0-8A67-20F0E7E62FF7}");
+ Chain("{F0C42457-6A3B-4912-A7EA-F27ED85A2092}");
+ Chain("{8640C9E4-A1C0-4C59-99BB-609B4E604981}");
+ Chain("{DD1B99AF-8106-4E00-BAC7-A43003EA07F8}");
+ Chain("{9DF05B77-11D1-475C-A00A-B656AF7E0908}");
+ Chain("{CA7DB949-3EF4-403D-8464-F9BA36A52E87}");
// 7.8.1 = same as 7.8.0
- From("{init-7.8.1}")
- .Chain("{init-7.8.0}");
+ From("{init-7.8.1}");
+ Chain("{init-7.8.0}");
// 7.9.0 = requires its own chain
- From("{init-7.9.0}")
- // chain...
- .Chain("{82C4BA1D-7720-46B1-BBD7-07F3F73800E6}");
+ From("{init-7.9.0}");
+ // chain...
+ Chain("{82C4BA1D-7720-46B1-BBD7-07F3F73800E6}");
+
// UPGRADE 8
//
// starting from the original 8.0.0 final state, chain migrations to upgrade version 8,
// defining new final states as more migrations are added to the chain.
-
- //From("")
- // .Chain("")
- // .Chain("");
-
- // WIP 8
//
// before v8 is released, some sites may exist, and these "pre-8" versions require their
- // own upgrade plan. in other words, this is the plan for sites that were on v8 before
+ // own upgrade plan. in other words, this is also the plan for sites that were on v8 before
// v8 was released
- // fixme - this is essentially for ZpqrtBnk website
- // need to determine which version it is and where it should resume running migrations
-
// 8.0.0
From("{init-origin}");
Chain("{98347B5E-65BF-4DD7-BB43-A09CB7AF4FCA}");
@@ -130,8 +120,6 @@ namespace Umbraco.Core.Migrations.Upgrade
Chain("{44484C32-EEB3-4A12-B1CB-11E02CE22AB2}");
// 7.6.0
- //Chain("{858B4039-070C-4928-BBEC-DDE8303352DA}");
- //Chain("{64F587C1-0B28-4D78-B4CC-26B7D87F69C1}");
Chain("{3586E4E9-2922-49EB-8E2A-A530CE6DBDE0}");
Chain("{D4A5674F-654D-4CC7-85E5-CFDBC533A318}");
Chain("{7F828EDD-6622-4A8D-AD80-EEAF46C11680}");
@@ -158,6 +146,28 @@ namespace Umbraco.Core.Migrations.Upgrade
Chain("{CC1B1201-1328-443C-954A-E0BBB8CCC1B5}");
Chain("{CA7DB949-3EF4-403D-8464-F9BA36A52E87}");
Chain("{7F0BF916-F64E-4B25-864A-170D6E6B68E5}");
+
+ // at this point of the chain, people started to work on v8, so whenever we
+ // merge stuff from v7, we have to chain the migrations here so they also
+ // run for v8.
+
+ // mergin from 7.8.0
+ Chain("{FDCB727A-EFB6-49F3-89E4-A346503AB849}");
+ Chain("{2A796A08-4FE4-4783-A1A5-B8A6C8AA4A92}");
+ Chain("{1A46A98B-2AAB-4C8E-870F-A2D55A97FD1F}");
+ Chain("{0AE053F6-2683-4234-87B2-E963F8CE9498}");
+ Chain("{D454541C-15C5-41CF-8109-937F26A78E71}");
+
+ // merging from 7.9.0
+ Chain("{89A728D1-FF4C-4155-A269-62CC09AD2131}");
+ Chain("{FD8631BC-0388-425C-A451-5F58574F6F05}");
+ Chain("{2821F53E-C58B-4812-B184-9CD240F990D7}");
+ Chain("{8918450B-3DA0-4BB7-886A-6FA8B7E4186E}");
+ Chain("FIXGUID NEW FINAL");
+
+ // FINAL STATE - MUST MATCH LAST ONE ABOVE !
+
+ Add(string.Empty, "{8918450B-3DA0-4BB7-886A-6FA8B7E4186E}");
}
}
}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddIndexToPropertyTypeAliasColumn.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddIndexToPropertyTypeAliasColumn.cs
new file mode 100644
index 0000000000..ddb084a609
--- /dev/null
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddIndexToPropertyTypeAliasColumn.cs
@@ -0,0 +1,27 @@
+using System.Linq;
+using Umbraco.Core.Persistence.SqlSyntax;
+
+namespace Umbraco.Core.Migrations.Upgrade.V_7_8_0
+{
+ internal class AddIndexToPropertyTypeAliasColumn : MigrationBase
+ {
+ public AddIndexToPropertyTypeAliasColumn(IMigrationContext context)
+ : base(context)
+ { }
+
+ public override void Migrate()
+ {
+ var dbIndexes = SqlSyntax.GetDefinedIndexesDefinitions(Context.Database);
+
+ //make sure it doesn't already exist
+ if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_cmsPropertyTypeAlias")) == false)
+ {
+ //we can apply the index
+ Create.Index("IX_cmsPropertyTypeAlias").OnTable(Constants.DatabaseSchema.Tables.PropertyType)
+ .OnColumn("Alias")
+ .Ascending().WithOptions().NonClustered()
+ .Do();
+ }
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddInstructionCountColumn.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddInstructionCountColumn.cs
new file mode 100644
index 0000000000..0ce2c91f2e
--- /dev/null
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddInstructionCountColumn.cs
@@ -0,0 +1,20 @@
+using System.Linq;
+using Umbraco.Core.Persistence.Dtos;
+
+namespace Umbraco.Core.Migrations.Upgrade.V_7_8_0
+{
+ internal class AddInstructionCountColumn : MigrationBase
+ {
+ public AddInstructionCountColumn(IMigrationContext context)
+ : base(context)
+ { }
+
+ public override void Migrate()
+ {
+ var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray();
+
+ if (columns.Any(x => x.TableName.InvariantEquals(Constants.DatabaseSchema.Tables.CacheInstruction) && x.ColumnName.InvariantEquals("instructionCount")) == false)
+ AddColumn("instructionCount");
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddMediaVersionTable.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddMediaVersionTable.cs
new file mode 100644
index 0000000000..b4c0062770
--- /dev/null
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddMediaVersionTable.cs
@@ -0,0 +1,65 @@
+using System.Collections.Generic;
+using System.Linq;
+using Umbraco.Core.Persistence;
+using Umbraco.Core.Persistence.Dtos;
+using Umbraco.Core.Persistence.Factories;
+using static Umbraco.Core.Persistence.NPocoSqlExtensions.Statics;
+
+namespace Umbraco.Core.Migrations.Upgrade.V_7_8_0
+{
+ internal class AddMediaVersionTable : MigrationBase
+ {
+ public AddMediaVersionTable(IMigrationContext context)
+ : base(context)
+ { }
+
+ public override void Migrate()
+ {
+ var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray();
+
+ if (tables.InvariantContains(Constants.DatabaseSchema.Tables.MediaVersion)) return;
+
+ Create.Table().Do();
+ MigrateMediaPaths();
+ }
+
+ private void MigrateMediaPaths()
+ {
+ // this may not be the most efficient way to do it, compared to how it's done in v7, but this
+ // migration should only run for v8 sites that are being developed, before v8 is released, so
+ // no big sites and performances don't matter here - keep it simple
+
+ var sql = Sql()
+ .Select(x => x.VarcharValue, x => x.TextValue)
+ .AndSelect(x => Alias(x.Id, "versionId"))
+ .From()
+ .InnerJoin().On((left, right) => left.PropertyTypeId == right.Id)
+ .InnerJoin().On((left, right) => left.VersionId == right.Id)
+ .InnerJoin().On((left, right) => left.NodeId == right.NodeId)
+ .Where(x => x.Alias == "umbracoFile")
+ .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media);
+
+ var paths = new List();
+
+ //using QUERY = a db cursor, we won't load this all into memory first, just row by row
+ foreach (var row in Database.Query(sql))
+ {
+ // if there's values then ensure there's a media path match and extract it
+ string mediaPath = null;
+ if (
+ (row.varcharValue != null && ContentBaseFactory.TryMatch((string) row.varcharValue, out mediaPath))
+ || (row.textValue != null && ContentBaseFactory.TryMatch((string) row.textValue, out mediaPath)))
+ {
+ paths.Add(new MediaVersionDto
+ {
+ Id = (int) row.versionId,
+ Path = mediaPath
+ });
+ }
+ }
+
+ // bulk insert
+ Database.BulkInsertRecords(paths);
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddTourDataUserColumn.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddTourDataUserColumn.cs
new file mode 100644
index 0000000000..cd2678205f
--- /dev/null
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddTourDataUserColumn.cs
@@ -0,0 +1,21 @@
+using System.Linq;
+using Umbraco.Core.Persistence.DatabaseAnnotations;
+using Umbraco.Core.Persistence.Dtos;
+
+namespace Umbraco.Core.Migrations.Upgrade.V_7_8_0
+{
+ internal class AddTourDataUserColumn : MigrationBase
+ {
+ public AddTourDataUserColumn(IMigrationContext context)
+ : base(context)
+ { }
+
+ public override void Migrate()
+ {
+ var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray();
+
+ if (columns.Any(x => x.TableName.InvariantEquals(Constants.DatabaseSchema.Tables.User) && x.ColumnName.InvariantEquals("tourData")) == false)
+ AddColumn("tourData");
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddUserLoginTable.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddUserLoginTable.cs
new file mode 100644
index 0000000000..7a55362072
--- /dev/null
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_7_8_0/AddUserLoginTable.cs
@@ -0,0 +1,22 @@
+using System.Linq;
+using Umbraco.Core.Persistence.Dtos;
+
+namespace Umbraco.Core.Migrations.Upgrade.V_7_8_0
+{
+ internal class AddUserLoginTable : MigrationBase
+ {
+ public AddUserLoginTable(IMigrationContext context)
+ : base(context)
+ { }
+
+ public override void Migrate()
+ {
+ var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray();
+
+ if (tables.InvariantContains(Constants.DatabaseSchema.Tables.UserLogin) == false)
+ {
+ Create.Table().Do();
+ }
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_9_0/AddIsSensitiveMemberTypeColumn.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_9_0/AddIsSensitiveMemberTypeColumn.cs
new file mode 100644
index 0000000000..4e1a7d1470
--- /dev/null
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_7_9_0/AddIsSensitiveMemberTypeColumn.cs
@@ -0,0 +1,20 @@
+using System.Linq;
+using Umbraco.Core.Persistence.Dtos;
+
+namespace Umbraco.Core.Migrations.Upgrade.V_7_9_0
+{
+ internal class AddIsSensitiveMemberTypeColumn : MigrationBase
+ {
+ public AddIsSensitiveMemberTypeColumn(IMigrationContext context)
+ : base(context)
+ { }
+
+ public override void Migrate()
+ {
+ var columns = SqlSyntax.GetColumnsInSchema(Context.Database).ToArray();
+
+ if (columns.Any(x => x.TableName.InvariantEquals(Constants.DatabaseSchema.Tables.MemberType) && x.ColumnName.InvariantEquals("isSensitive")) == false)
+ AddColumn("isSensitive");
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_9_0/AddUmbracoAuditTable.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_9_0/AddUmbracoAuditTable.cs
new file mode 100644
index 0000000000..e7880dfc73
--- /dev/null
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_7_9_0/AddUmbracoAuditTable.cs
@@ -0,0 +1,22 @@
+using System.Linq;
+using Umbraco.Core.Persistence.Dtos;
+
+namespace Umbraco.Core.Migrations.Upgrade.V_7_9_0
+{
+ internal class AddUmbracoAuditTable : MigrationBase
+ {
+ public AddUmbracoAuditTable(IMigrationContext context)
+ : base(context)
+ { }
+
+ public override void Migrate()
+ {
+ var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray();
+
+ if (tables.InvariantContains(Constants.DatabaseSchema.Tables.AuditEntry))
+ return;
+
+ Create.Table().Do();
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_9_0/AddUmbracoConsentTable.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_9_0/AddUmbracoConsentTable.cs
new file mode 100644
index 0000000000..e3656f69ac
--- /dev/null
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_7_9_0/AddUmbracoConsentTable.cs
@@ -0,0 +1,22 @@
+using System.Linq;
+using Umbraco.Core.Persistence.Dtos;
+
+namespace Umbraco.Core.Migrations.Upgrade.V_7_9_0
+{
+ internal class AddUmbracoConsentTable : MigrationBase
+ {
+ public AddUmbracoConsentTable(IMigrationContext context)
+ : base(context)
+ { }
+
+ public override void Migrate()
+ {
+ var tables = SqlSyntax.GetTablesInSchema(Context.Database).ToArray();
+
+ if (tables.InvariantContains(Constants.DatabaseSchema.Tables.Consent))
+ return;
+
+ Create.Table().Do();
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_7_9_0/CreateSensitiveDataUserGroup.cs b/src/Umbraco.Core/Migrations/Upgrade/V_7_9_0/CreateSensitiveDataUserGroup.cs
new file mode 100644
index 0000000000..a3749f7be5
--- /dev/null
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_7_9_0/CreateSensitiveDataUserGroup.cs
@@ -0,0 +1,27 @@
+using System;
+using Umbraco.Core.Persistence;
+using Umbraco.Core.Persistence.Dtos;
+
+namespace Umbraco.Core.Migrations.Upgrade.V_7_9_0
+{
+ internal class CreateSensitiveDataUserGroup : MigrationBase
+ {
+ public CreateSensitiveDataUserGroup(IMigrationContext context)
+ : base(context)
+ { }
+
+ public override void Migrate()
+ {
+ var sql = Sql()
+ .SelectCount()
+ .From()
+ .Where(x => x.Alias == Constants.Security.SensitiveDataGroupAlias);
+
+ var exists = Database.ExecuteScalar(sql) > 0;
+ if (exists) return;
+
+ var groupId = Database.Insert(Constants.DatabaseSchema.Tables.UserGroup, "id", new UserGroupDto { StartMediaId = -1, StartContentId = -1, Alias = Constants.Security.SensitiveDataGroupAlias, Name = "Sensitive data", DefaultPermissions = "", CreateDate = DateTime.Now, UpdateDate = DateTime.Now, Icon = "icon-lock" });
+ Database.Insert(new User2UserGroupDto { UserGroupId = Convert.ToInt32(groupId), UserId = Constants.Security.SuperId }); // add super to sensitive data
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs
index 65db0cdd2d..eb39f37112 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/DataTypeMigration.cs
@@ -34,7 +34,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
// re-create *all* keys and indexes
foreach (var x in DatabaseSchemaCreator.OrderedTables)
- Create.KeysAndIndexes(x.Value).Do();
+ Create.KeysAndIndexes(x).Do();
// renames
Execute.Sql(Sql()
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs
index cd4e702ea9..ee439088be 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/PropertyEditorsMigration.cs
@@ -17,19 +17,22 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
RenameDataType(Constants.PropertyEditors.Aliases.MemberPicker + "2", Constants.PropertyEditors.Aliases.MemberPicker);
RenameDataType(Constants.PropertyEditors.Aliases.MultiNodeTreePicker + "2", Constants.PropertyEditors.Aliases.MultiNodeTreePicker);
RenameDataType(Constants.PropertyEditors.Aliases.RelatedLinks + "2", Constants.PropertyEditors.Aliases.RelatedLinks);
- RenameDataType("Umbraco.TextboxMultiple", Constants.PropertyEditors.Aliases.TextArea);
- RenameDataType("Umbraco.Textbox", Constants.PropertyEditors.Aliases.TextBox);
+ RenameDataType("Umbraco.TextboxMultiple", Constants.PropertyEditors.Aliases.TextArea, false);
+ RenameDataType("Umbraco.Textbox", Constants.PropertyEditors.Aliases.TextBox, false);
}
- private void RenameDataType(string fromAlias, string toAlias)
+ private void RenameDataType(string fromAlias, string toAlias, bool checkCollision = true)
{
- var oldCount = Database.ExecuteScalar(Sql()
- .SelectCount()
- .From()
- .Where(x => x.EditorAlias == toAlias));
+ if (checkCollision)
+ {
+ var oldCount = Database.ExecuteScalar(Sql()
+ .SelectCount()
+ .From()
+ .Where(x => x.EditorAlias == toAlias));
- if (oldCount > 0)
- throw new InvalidOperationException($"Cannot rename datatype alias \"{fromAlias}\" to \"{toAlias}\" because the target alias is already used.");
+ if (oldCount > 0)
+ throw new InvalidOperationException($"Cannot rename datatype alias \"{fromAlias}\" to \"{toAlias}\" because the target alias is already used.");
+ }
Database.Execute(Sql()
.Update(u => u.Set(x => x.EditorAlias, toAlias))
diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs
index 78d6bf1085..174404b1b9 100644
--- a/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs
+++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_0_0/VariantsMigration.cs
@@ -52,7 +52,7 @@ HAVING COUNT(v2.id) <> 1").Any())
// re-create *all* keys and indexes
foreach (var x in DatabaseSchemaCreator.OrderedTables)
- Create.KeysAndIndexes(x.Value).Do();
+ Create.KeysAndIndexes(x).Do();
}
private void MigratePropertyData()
diff --git a/src/Umbraco.Core/Models/AuditEntry.cs b/src/Umbraco.Core/Models/AuditEntry.cs
new file mode 100644
index 0000000000..2076e5328c
--- /dev/null
+++ b/src/Umbraco.Core/Models/AuditEntry.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Reflection;
+using System.Runtime.Serialization;
+using Umbraco.Core.Models.Entities;
+
+namespace Umbraco.Core.Models
+{
+ ///
+ /// Represents an audited event.
+ ///
+ [Serializable]
+ [DataContract(IsReference = true)]
+ internal class AuditEntry : EntityBase, IAuditEntry
+ {
+ private static PropertySelectors _selectors;
+
+ private int _performingUserId;
+ private string _performingDetails;
+ private string _performingIp;
+ private int _affectedUserId;
+ private string _affectedDetails;
+ private string _eventType;
+ private string _eventDetails;
+
+ private static PropertySelectors Selectors => _selectors ?? (_selectors = new PropertySelectors());
+
+ private class PropertySelectors
+ {
+ public readonly PropertyInfo PerformingUserId = ExpressionHelper.GetPropertyInfo(x => x.PerformingUserId);
+ public readonly PropertyInfo PerformingDetails = ExpressionHelper.GetPropertyInfo(x => x.PerformingDetails);
+ public readonly PropertyInfo PerformingIp = ExpressionHelper.GetPropertyInfo(x => x.PerformingIp);
+ public readonly PropertyInfo AffectedUserId = ExpressionHelper.GetPropertyInfo(x => x.AffectedUserId);
+ public readonly PropertyInfo AffectedDetails = ExpressionHelper.GetPropertyInfo(x => x.AffectedDetails);
+ public readonly PropertyInfo EventType = ExpressionHelper.GetPropertyInfo(x => x.EventType);
+ public readonly PropertyInfo EventDetails = ExpressionHelper.GetPropertyInfo(x => x.EventDetails);
+ }
+
+ ///
+ public int PerformingUserId
+ {
+ get => _performingUserId;
+ set => SetPropertyValueAndDetectChanges(value, ref _performingUserId, Selectors.PerformingUserId);
+ }
+
+ ///
+ public string PerformingDetails
+ {
+ get => _performingDetails;
+ set => SetPropertyValueAndDetectChanges(value, ref _performingDetails, Selectors.PerformingDetails);
+ }
+
+ ///
+ public string PerformingIp
+ {
+ get => _performingIp;
+ set => SetPropertyValueAndDetectChanges(value, ref _performingIp, Selectors.PerformingIp);
+ }
+
+ ///
+ public DateTime EventDateUtc
+ {
+ get => CreateDate;
+ set => CreateDate = value;
+ }
+
+ ///
+ public int AffectedUserId
+ {
+ get => _affectedUserId;
+ set => SetPropertyValueAndDetectChanges(value, ref _affectedUserId, Selectors.AffectedUserId);
+ }
+
+ ///
+ public string AffectedDetails
+ {
+ get => _affectedDetails;
+ set => SetPropertyValueAndDetectChanges(value, ref _affectedDetails, Selectors.AffectedDetails);
+ }
+
+ ///
+ public string EventType
+ {
+ get => _eventType;
+ set => SetPropertyValueAndDetectChanges(value, ref _eventType, Selectors.EventType);
+ }
+
+ ///
+ public string EventDetails
+ {
+ get => _eventDetails;
+ set => SetPropertyValueAndDetectChanges(value, ref _eventDetails, Selectors.EventDetails);
+ }
+ }
+}
diff --git a/src/Umbraco.Core/Models/AuditItem.cs b/src/Umbraco.Core/Models/AuditItem.cs
index 519f288ef0..6bfe32bd77 100644
--- a/src/Umbraco.Core/Models/AuditItem.cs
+++ b/src/Umbraco.Core/Models/AuditItem.cs
@@ -2,18 +2,29 @@
namespace Umbraco.Core.Models
{
- public sealed class AuditItem : EntityBase
+ public sealed class AuditItem : EntityBase, IAuditItem
{
+ ///
+ /// Constructor for creating an item to be created
+ ///
+ ///
+ ///
+ ///
+ ///
public AuditItem(int objectId, string comment, AuditType type, int userId)
{
+ DisableChangeTracking();
+
Id = objectId;
Comment = comment;
AuditType = type;
UserId = userId;
+
+ EnableChangeTracking();
}
- public string Comment { get; private set; }
- public AuditType AuditType { get; private set; }
- public int UserId { get; private set; }
+ public string Comment { get; }
+ public AuditType AuditType { get; }
+ public int UserId { get; }
}
}
diff --git a/src/Umbraco.Core/Models/Consent.cs b/src/Umbraco.Core/Models/Consent.cs
new file mode 100644
index 0000000000..87dd9767a0
--- /dev/null
+++ b/src/Umbraco.Core/Models/Consent.cs
@@ -0,0 +1,102 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Runtime.Serialization;
+using Umbraco.Core.Models.Entities;
+
+namespace Umbraco.Core.Models
+{
+ ///
+ /// Represents a consent.
+ ///
+ [Serializable]
+ [DataContract(IsReference = true)]
+ internal class Consent : EntityBase, IConsent
+ {
+ private static PropertySelectors _selector;
+
+ private bool _current;
+ private string _source;
+ private string _context;
+ private string _action;
+ private ConsentState _state;
+ private string _comment;
+
+ // ReSharper disable once ClassNeverInstantiated.Local
+ private class PropertySelectors
+ {
+ public readonly PropertyInfo Current = ExpressionHelper.GetPropertyInfo(x => x.Current);
+ public readonly PropertyInfo Source = ExpressionHelper.GetPropertyInfo(x => x.Source);
+ public readonly PropertyInfo Context = ExpressionHelper.GetPropertyInfo(x => x.Context);
+ public readonly PropertyInfo Action = ExpressionHelper.GetPropertyInfo(x => x.Action);
+ public readonly PropertyInfo State = ExpressionHelper.GetPropertyInfo(x => x.State);
+ public readonly PropertyInfo Comment = ExpressionHelper.GetPropertyInfo(x => x.Comment);
+ }
+
+ private static PropertySelectors Selectors => _selector ?? (_selector = new PropertySelectors());
+
+ ///
+ public bool Current
+ {
+ get => _current;
+ set => SetPropertyValueAndDetectChanges(value, ref _current, Selectors.Current);
+ }
+
+ ///
+ public string Source
+ {
+ get => _source;
+ set
+ {
+ if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException(nameof(value));
+ SetPropertyValueAndDetectChanges(value, ref _source, Selectors.Source);
+ }
+ }
+
+ ///
+ public string Context
+ {
+ get => _context;
+ set
+ {
+ if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException(nameof(value));
+ SetPropertyValueAndDetectChanges(value, ref _context, Selectors.Context);
+ }
+ }
+
+ ///
+ public string Action
+ {
+ get => _action;
+ set
+ {
+ if (string.IsNullOrWhiteSpace(value)) throw new ArgumentException(nameof(value));
+ SetPropertyValueAndDetectChanges(value, ref _action, Selectors.Action);
+ }
+ }
+
+ ///
+ public ConsentState State
+ {
+ get => _state;
+ // note: we probably should validate the state here, but since the
+ // enum is [Flags] with many combinations, this could be expensive
+ set => SetPropertyValueAndDetectChanges(value, ref _state, Selectors.State);
+ }
+
+ ///
+ public string Comment
+ {
+ get => _comment;
+ set => SetPropertyValueAndDetectChanges(value, ref _comment, Selectors.Comment);
+ }
+
+ ///
+ public IEnumerable History => HistoryInternal;
+
+ ///
+ /// Gets the previous states of this consent.
+ ///
+ internal List HistoryInternal { get; set; }
+ }
+}
diff --git a/src/Umbraco.Core/Models/ConsentExtensions.cs b/src/Umbraco.Core/Models/ConsentExtensions.cs
new file mode 100644
index 0000000000..fabeaf5809
--- /dev/null
+++ b/src/Umbraco.Core/Models/ConsentExtensions.cs
@@ -0,0 +1,18 @@
+namespace Umbraco.Core.Models
+{
+ ///
+ /// Provides extension methods for the interface.
+ ///
+ public static class ConsentExtensions
+ {
+ ///
+ /// Determines whether the consent is granted.
+ ///
+ public static bool IsGranted(this IConsent consent) => (consent.State & ConsentState.Granted) > 0;
+
+ ///
+ /// Determines whether the consent is revoked.
+ ///
+ public static bool IsRevoked(this IConsent consent) => (consent.State & ConsentState.Revoked) > 0;
+ }
+}
diff --git a/src/Umbraco.Core/Models/ConsentState.cs b/src/Umbraco.Core/Models/ConsentState.cs
new file mode 100644
index 0000000000..ed370823f3
--- /dev/null
+++ b/src/Umbraco.Core/Models/ConsentState.cs
@@ -0,0 +1,38 @@
+using System;
+
+namespace Umbraco.Core.Models
+{
+ ///
+ /// Represents the state of a consent.
+ ///
+ [Flags]
+ public enum ConsentState // : int
+ {
+ // note - this is a [Flags] enumeration
+ // on can create detailed flags such as:
+ //GrantedOptIn = Granted | 0x0001
+ //GrandedByForce = Granted | 0x0002
+ //
+ // 16 situations for each Pending/Granted/Revoked should be ok
+
+ ///
+ /// There is no consent.
+ ///
+ None = 0,
+
+ ///
+ /// Consent is pending and has not been granted yet.
+ ///
+ Pending = 0x10000,
+
+ ///
+ /// Consent has been granted.
+ ///
+ Granted = 0x20000,
+
+ ///
+ /// Consent has been revoked.
+ ///
+ Revoked = 0x40000
+ }
+}
diff --git a/src/Umbraco.Core/Models/ContentBase.cs b/src/Umbraco.Core/Models/ContentBase.cs
index 3bf614a3ef..a1df720a83 100644
--- a/src/Umbraco.Core/Models/ContentBase.cs
+++ b/src/Umbraco.Core/Models/ContentBase.cs
@@ -223,9 +223,8 @@ namespace Umbraco.Core.Models
#region Dirty
- ///
- /// Resets dirty properties.
- ///
+ ///
+ /// Overriden to include user properties.
public override void ResetDirtyProperties(bool rememberDirty)
{
base.ResetDirtyProperties(rememberDirty);
@@ -235,17 +234,15 @@ namespace Umbraco.Core.Models
prop.ResetDirtyProperties(rememberDirty);
}
- ///
- /// Gets a value indicating whether the current entity is dirty.
- ///
+ ///
+ /// Overriden to include user properties.
public override bool IsDirty()
{
return IsEntityDirty() || this.IsAnyUserPropertyDirty();
}
- ///
- /// Gets a value indicating whether the current entity was dirty.
- ///
+ ///
+ /// Overriden to include user properties.
public override bool WasDirty()
{
return WasEntityDirty() || this.WasAnyUserPropertyDirty();
@@ -267,9 +264,8 @@ namespace Umbraco.Core.Models
return base.WasDirty();
}
- ///
- /// Gets a value indicating whether a user property is dirty.
- ///
+ ///
+ /// Overriden to include user properties.
public override bool IsPropertyDirty(string propertyName)
{
if (base.IsPropertyDirty(propertyName))
@@ -278,9 +274,8 @@ namespace Umbraco.Core.Models
return Properties.Contains(propertyName) && Properties[propertyName].IsDirty();
}
- ///
- /// Gets a value indicating whether a user property was dirty.
- ///
+ ///
+ /// Overriden to include user properties.
public override bool WasPropertyDirty(string propertyName)
{
if (base.WasPropertyDirty(propertyName))
@@ -289,6 +284,24 @@ namespace Umbraco.Core.Models
return Properties.Contains(propertyName) && Properties[propertyName].WasDirty();
}
+ ///
+ /// Overriden to include user properties.
+ public override IEnumerable GetDirtyProperties()
+ {
+ var instanceProperties = base.GetDirtyProperties();
+ var propertyTypes = Properties.Where(x => x.IsDirty()).Select(x => x.Alias);
+ return instanceProperties.Concat(propertyTypes);
+ }
+
+ ///
+ /// Overriden to include user properties.
+ public override IEnumerable GetWereDirtyProperties()
+ {
+ var instanceProperties = base.GetWereDirtyProperties();
+ var propertyTypes = Properties.Where(x => x.WasDirty()).Select(x => x.Alias);
+ return instanceProperties.Concat(propertyTypes);
+ }
+
#endregion
}
}
diff --git a/src/Umbraco.Core/Models/ContentType.cs b/src/Umbraco.Core/Models/ContentType.cs
index d0f3edea8b..a7418a1441 100644
--- a/src/Umbraco.Core/Models/ContentType.cs
+++ b/src/Umbraco.Core/Models/ContentType.cs
@@ -97,11 +97,13 @@ namespace Umbraco.Core.Models
[DataMember]
public IEnumerable AllowedTemplates
{
- get { return _allowedTemplates; }
+ get => _allowedTemplates;
set
{
- SetPropertyValueAndDetectChanges(value, ref _allowedTemplates, Ps.Value.AllowedTemplatesSelector,
- Ps.Value.TemplateComparer);
+ SetPropertyValueAndDetectChanges(value, ref _allowedTemplates, Ps.Value.AllowedTemplatesSelector, Ps.Value.TemplateComparer);
+
+ if (_allowedTemplates.Any(x => x.Id == _defaultTemplate) == false)
+ DefaultTemplateId = 0;
}
}
diff --git a/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs b/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs
index 4e2279caaf..711b7c9b9f 100644
--- a/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs
+++ b/src/Umbraco.Core/Models/Entities/BeingDirtyBase.cs
@@ -87,6 +87,15 @@ namespace Umbraco.Core.Models.Entities
_currentChanges = null;
}
+ ///
+ public virtual IEnumerable GetWereDirtyProperties()
+ {
+ // ReSharper disable once MergeConditionalExpression
+ return _savedChanges == null
+ ? Enumerable.Empty()
+ : _savedChanges.Where(x => x.Value).Select(x => x.Key);
+ }
+
#endregion
#region Change Tracking
diff --git a/src/Umbraco.Core/Models/Entities/EntitySlim.cs b/src/Umbraco.Core/Models/Entities/EntitySlim.cs
index fbef1fafbd..163879bbe0 100644
--- a/src/Umbraco.Core/Models/Entities/EntitySlim.cs
+++ b/src/Umbraco.Core/Models/Entities/EntitySlim.cs
@@ -215,6 +215,11 @@ namespace Umbraco.Core.Models.Entities
throw new WontImplementException();
}
+ public IEnumerable GetWereDirtyProperties()
+ {
+ throw new WontImplementException();
+ }
+
#endregion
}
}
diff --git a/src/Umbraco.Core/Models/Entities/IRememberBeingDirty.cs b/src/Umbraco.Core/Models/Entities/IRememberBeingDirty.cs
index 75faba729b..e679b98b93 100644
--- a/src/Umbraco.Core/Models/Entities/IRememberBeingDirty.cs
+++ b/src/Umbraco.Core/Models/Entities/IRememberBeingDirty.cs
@@ -1,4 +1,6 @@
-namespace Umbraco.Core.Models.Entities
+using System.Collections.Generic;
+
+namespace Umbraco.Core.Models.Entities
{
///
/// Defines an entity that tracks property changes and can be dirty, and remembers
@@ -29,5 +31,10 @@
/// A value indicating whether to remember dirty properties.
/// When is true, dirty properties are saved so they can be checked with WasDirty.
void ResetDirtyProperties(bool rememberDirty);
+
+ ///
+ /// Gets properties that were dirty.
+ ///
+ IEnumerable GetWereDirtyProperties();
}
}
diff --git a/src/Umbraco.Core/Models/IAuditEntry.cs b/src/Umbraco.Core/Models/IAuditEntry.cs
new file mode 100644
index 0000000000..c097f84752
--- /dev/null
+++ b/src/Umbraco.Core/Models/IAuditEntry.cs
@@ -0,0 +1,60 @@
+using System;
+using Umbraco.Core.Models.Entities;
+
+namespace Umbraco.Core.Models
+{
+ ///
+ /// Represents an audited event.
+ ///
+ ///
+ /// The free-form details properties can be used to capture relevant infos (for example,
+ /// a user email and identifier) at the time of the audited event, even though they may change
+ /// later on - but we want to keep a track of their value at that time.
+ /// Depending on audit loggers, these properties can be purely free-form text, or
+ /// contain json serialized objects.
+ ///
+ public interface IAuditEntry : IEntity, IRememberBeingDirty
+ {
+ ///
+ /// Gets or sets the identifier of the user triggering the audited event.
+ ///
+ int PerformingUserId { get; set; }
+
+ ///
+ /// Gets or sets free-form details about the user triggering the audited event.
+ ///
+ string PerformingDetails { get; set; }
+
+ ///
+ /// Gets or sets the IP address or the request triggering the audited event.
+ ///
+ string PerformingIp { get; set; }
+
+ ///
+ /// Gets or sets the date and time of the audited event.
+ ///
+ DateTime EventDateUtc { get; set; }
+
+ ///
+ /// Gets or sets the identifier of the user affected by the audited event.
+ ///
+ /// Not used when no single user is affected by the event.
+ int AffectedUserId { get; set; }
+
+ ///
+ /// Gets or sets free-form details about the entity affected by the audited event.
+ ///
+ /// The entity affected by the event can be another user, a member...
+ string AffectedDetails { get; set; }
+
+ ///
+ /// Gets or sets the type of the audited event.
+ ///
+ string EventType { get; set; }
+
+ ///
+ /// Gets or sets free-form details about the audited event.
+ ///
+ string EventDetails { get; set; }
+ }
+}
diff --git a/src/Umbraco.Core/Models/IAuditItem.cs b/src/Umbraco.Core/Models/IAuditItem.cs
new file mode 100644
index 0000000000..9416e2a055
--- /dev/null
+++ b/src/Umbraco.Core/Models/IAuditItem.cs
@@ -0,0 +1,12 @@
+using System;
+using Umbraco.Core.Models.Entities;
+
+namespace Umbraco.Core.Models
+{
+ public interface IAuditItem : IEntity
+ {
+ string Comment { get; }
+ AuditType AuditType { get; }
+ int UserId { get; }
+ }
+}
diff --git a/src/Umbraco.Core/Models/IConsent.cs b/src/Umbraco.Core/Models/IConsent.cs
new file mode 100644
index 0000000000..7e0156fd6e
--- /dev/null
+++ b/src/Umbraco.Core/Models/IConsent.cs
@@ -0,0 +1,55 @@
+using System.Collections.Generic;
+using Umbraco.Core.Models.Entities;
+
+namespace Umbraco.Core.Models
+{
+ ///
+ /// Represents a consent state.
+ ///
+ ///
+ /// A consent is fully identified by a source (whoever is consenting), a context (for
+ /// example, an application), and an action (whatever is consented).
+ /// A consent state registers the state of the consent (granted, revoked...).
+ ///
+ public interface IConsent : IEntity, IRememberBeingDirty
+ {
+ ///
+ /// Determines whether the consent entity represents the current state.
+ ///
+ bool Current { get; }
+
+ ///
+ /// Gets the unique identifier of whoever is consenting.
+ ///
+ string Source { get; }
+
+ ///
+ /// Gets the unique identifier of the context of the consent.
+ ///
+ ///
+ /// Represents the domain, application, scope... of the action.
+ /// When the action is a Udi, this should be the Udi type.
+ ///
+ string Context { get; }
+
+ ///
+ /// Gets the unique identifier of the consented action.
+ ///
+ string Action { get; }
+
+ ///
+ /// Gets the state of the consent.
+ ///
+ ConsentState State { get; }
+
+ ///
+ /// Gets some additional free text.
+ ///
+ string Comment { get; }
+
+ ///
+ /// Gets the previous states of this consent.
+ ///
+ IEnumerable History { get; }
+ }
+}
diff --git a/src/Umbraco.Core/Models/IMemberType.cs b/src/Umbraco.Core/Models/IMemberType.cs
index a1ce10dac8..9596d88cca 100644
--- a/src/Umbraco.Core/Models/IMemberType.cs
+++ b/src/Umbraco.Core/Models/IMemberType.cs
@@ -19,6 +19,13 @@
///
bool MemberCanViewProperty(string propertyTypeAlias);
+ ///
+ /// Gets a boolean indicating whether a Property is marked as storing sensitive values on the Members profile.
+ ///
+ /// PropertyType Alias of the Property to check
+ ///
+ bool IsSensitiveProperty(string propertyTypeAlias);
+
///
/// Sets a boolean indicating whether a Property is editable by the Member.
///
@@ -32,5 +39,12 @@
/// PropertyType Alias of the Property to set
/// Boolean value, true or false
void SetMemberCanViewProperty(string propertyTypeAlias, bool value);
+
+ ///
+ /// Sets a boolean indicating whether a Property is a sensitive value on the Members profile.
+ ///
+ /// PropertyType Alias of the Property to set
+ /// Boolean value, true or false
+ void SetIsSensitiveProperty(string propertyTypeAlias, bool value);
}
}
diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs
index 3de0d11de1..bd4905729d 100644
--- a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs
+++ b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs
@@ -60,6 +60,14 @@ namespace Umbraco.Core.Models.Identity
private BackOfficeIdentityUser()
{
+ _startMediaIds = new int[] { };
+ _startContentIds = new int[] { };
+ _groups = new IReadOnlyUserGroup[] { };
+ _allowedSections = new string[] { };
+ _culture = Configuration.GlobalSettings.DefaultUILanguage;
+ _groups = new IReadOnlyUserGroup[0];
+ _roles = new ObservableCollection>();
+ _roles.CollectionChanged += _roles_CollectionChanged;
}
///
@@ -382,6 +390,10 @@ namespace Umbraco.Core.Models.Identity
_beingDirty.ResetDirtyProperties(rememberDirty);
}
+ ///
+ public IEnumerable GetWereDirtyProperties()
+ => _beingDirty.GetWereDirtyProperties();
+
///
/// Disables change tracking.
///
diff --git a/src/Umbraco.Core/Models/Identity/IdentityProfile.cs b/src/Umbraco.Core/Models/Identity/IdentityProfile.cs
index 46baad29b7..eef2a17aa5 100644
--- a/src/Umbraco.Core/Models/Identity/IdentityProfile.cs
+++ b/src/Umbraco.Core/Models/Identity/IdentityProfile.cs
@@ -46,20 +46,6 @@ namespace Umbraco.Core.Models.Identity
dest.ResetDirtyProperties(true);
dest.EnableChangeTracking();
});
-
- CreateMap()
- .ConstructUsing(source => new UserData(Guid.NewGuid().ToString("N"))) //this is the 'session id'
- .ForMember(dest => dest.Id, opt => opt.MapFrom(src => src.Id))
- .ForMember(dest => dest.AllowedApplications, opt => opt.MapFrom(src => src.AllowedSections))
- .ForMember(dest => dest.Roles, opt => opt.MapFrom(src => new[] { src.Roles.Select(x => x.RoleId).ToArray()}))
- .ForMember(dest => dest.RealName, opt => opt.MapFrom(src => src.Name))
- //When mapping to UserData which is used in the authcookie we want ALL start nodes including ones defined on the groups
- .ForMember(dest => dest.StartContentNodes, opt => opt.MapFrom(src => src.CalculatedContentStartNodeIds))
- //When mapping to UserData which is used in the authcookie we want ALL start nodes including ones defined on the groups
- .ForMember(dest => dest.StartMediaNodes, opt => opt.MapFrom(src => src.CalculatedMediaStartNodeIds))
- .ForMember(dest => dest.Username, opt => opt.MapFrom(src => src.UserName))
- .ForMember(dest => dest.Culture, opt => opt.MapFrom(src => src.Culture))
- .ForMember(dest => dest.SessionId, opt => opt.MapFrom(src => src.SecurityStamp.IsNullOrWhiteSpace() ? Guid.NewGuid().ToString("N") : src.SecurityStamp));
}
private static string GetPasswordHash(string storedPass)
diff --git a/src/Umbraco.Core/Models/Member.cs b/src/Umbraco.Core/Models/Member.cs
index f09094f466..a45b1e356b 100644
--- a/src/Umbraco.Core/Models/Member.cs
+++ b/src/Umbraco.Core/Models/Member.cs
@@ -168,7 +168,19 @@ namespace Umbraco.Core.Models
public string RawPasswordValue
{
get { return _rawPasswordValue; }
- set { SetPropertyValueAndDetectChanges(value, ref _rawPasswordValue, Ps.Value.PasswordSelector); }
+ set
+ {
+ if (value == null)
+ {
+ //special case, this is used to ensure that the password is not updated when persisting, in this case
+ //we don't want to track changes either
+ _rawPasswordValue = null;
+ }
+ else
+ {
+ SetPropertyValueAndDetectChanges(value, ref _rawPasswordValue, Ps.Value.PasswordSelector);
+ }
+ }
}
///
diff --git a/src/Umbraco.Core/Models/MemberType.cs b/src/Umbraco.Core/Models/MemberType.cs
index df087920f8..76450e9115 100644
--- a/src/Umbraco.Core/Models/MemberType.cs
+++ b/src/Umbraco.Core/Models/MemberType.cs
@@ -79,7 +79,7 @@ namespace Umbraco.Core.Models
}
///
- /// Gets or Sets a Dictionary of Tuples (MemberCanEdit, VisibleOnProfile) by the PropertyTypes' alias.
+ /// Gets or Sets a Dictionary of Tuples (MemberCanEdit, VisibleOnProfile, IsSensitive) by the PropertyTypes' alias.
///
[DataMember]
internal IDictionary MemberTypePropertyTypes { get; private set; }
@@ -91,7 +91,7 @@ namespace Umbraco.Core.Models
///
public bool MemberCanEditProperty(string propertyTypeAlias)
{
- return MemberTypePropertyTypes.ContainsKey(propertyTypeAlias) && MemberTypePropertyTypes[propertyTypeAlias].IsEditable;
+ return MemberTypePropertyTypes.TryGetValue(propertyTypeAlias, out var propertyProfile) && propertyProfile.IsEditable;
}
///
@@ -101,7 +101,16 @@ namespace Umbraco.Core.Models
///
public bool MemberCanViewProperty(string propertyTypeAlias)
{
- return MemberTypePropertyTypes.ContainsKey(propertyTypeAlias) && MemberTypePropertyTypes[propertyTypeAlias].IsVisible;
+ return MemberTypePropertyTypes.TryGetValue(propertyTypeAlias, out var propertyProfile) && propertyProfile.IsVisible;
+ }
+ ///
+ /// Gets a boolean indicating whether a Property is marked as storing sensitive values on the Members profile.
+ ///
+ /// PropertyType Alias of the Property to check
+ ///
+ public bool IsSensitiveProperty(string propertyTypeAlias)
+ {
+ return MemberTypePropertyTypes.TryGetValue(propertyTypeAlias, out var propertyProfile) && propertyProfile.IsSensitive;
}
///
@@ -111,13 +120,13 @@ namespace Umbraco.Core.Models
/// Boolean value, true or false
public void SetMemberCanEditProperty(string propertyTypeAlias, bool value)
{
- if (MemberTypePropertyTypes.ContainsKey(propertyTypeAlias))
+ if (MemberTypePropertyTypes.TryGetValue(propertyTypeAlias, out var propertyProfile))
{
- MemberTypePropertyTypes[propertyTypeAlias].IsEditable = value;
+ propertyProfile.IsEditable = value;
}
else
{
- var tuple = new MemberTypePropertyProfileAccess(false, value);
+ var tuple = new MemberTypePropertyProfileAccess(false, value, false);
MemberTypePropertyTypes.Add(propertyTypeAlias, tuple);
}
}
@@ -129,13 +138,31 @@ namespace Umbraco.Core.Models
/// Boolean value, true or false
public void SetMemberCanViewProperty(string propertyTypeAlias, bool value)
{
- if (MemberTypePropertyTypes.ContainsKey(propertyTypeAlias))
+ if (MemberTypePropertyTypes.TryGetValue(propertyTypeAlias, out var propertyProfile))
{
- MemberTypePropertyTypes[propertyTypeAlias].IsVisible = value;
+ propertyProfile.IsVisible = value;
}
else
{
- var tuple = new MemberTypePropertyProfileAccess(value, false);
+ var tuple = new MemberTypePropertyProfileAccess(value, false, false);
+ MemberTypePropertyTypes.Add(propertyTypeAlias, tuple);
+ }
+ }
+
+ ///
+ /// Sets a boolean indicating whether a Property is a sensitive value on the Members profile.
+ ///
+ /// PropertyType Alias of the Property to set
+ /// Boolean value, true or false
+ public void SetIsSensitiveProperty(string propertyTypeAlias, bool value)
+ {
+ if (MemberTypePropertyTypes.TryGetValue(propertyTypeAlias, out var propertyProfile))
+ {
+ propertyProfile.IsSensitive = value;
+ }
+ else
+ {
+ var tuple = new MemberTypePropertyProfileAccess(false, false, true);
MemberTypePropertyTypes.Add(propertyTypeAlias, tuple);
}
}
diff --git a/src/Umbraco.Core/Models/MemberTypePropertyProfileAccess.cs b/src/Umbraco.Core/Models/MemberTypePropertyProfileAccess.cs
index 0f6b2a3dce..386fdf560b 100644
--- a/src/Umbraco.Core/Models/MemberTypePropertyProfileAccess.cs
+++ b/src/Umbraco.Core/Models/MemberTypePropertyProfileAccess.cs
@@ -5,13 +5,15 @@
///
internal class MemberTypePropertyProfileAccess
{
- public MemberTypePropertyProfileAccess(bool isVisible, bool isEditable)
+ public MemberTypePropertyProfileAccess(bool isVisible, bool isEditable, bool isSenstive)
{
IsVisible = isVisible;
IsEditable = isEditable;
+ IsSensitive = isSenstive;
}
public bool IsVisible { get; set; }
public bool IsEditable { get; set; }
+ public bool IsSensitive { get; set; }
}
}
diff --git a/src/Umbraco.Core/Models/Membership/EntityPermissionCollection.cs b/src/Umbraco.Core/Models/Membership/EntityPermissionCollection.cs
index a2f70ef5ef..12e874d5d7 100644
--- a/src/Umbraco.Core/Models/Membership/EntityPermissionCollection.cs
+++ b/src/Umbraco.Core/Models/Membership/EntityPermissionCollection.cs
@@ -17,7 +17,30 @@ namespace Umbraco.Core.Models.Membership
}
///
- /// Returns the aggregate permissions in the permission set
+ /// Returns the aggregate permissions in the permission set for a single node
+ ///
+ ///
+ ///
+ /// This value is only calculated once per node
+ ///
+ public IEnumerable GetAllPermissions(int entityId)
+ {
+ if (_aggregateNodePermissions == null)
+ _aggregateNodePermissions = new Dictionary();
+
+ string[] entityPermissions;
+ if (_aggregateNodePermissions.TryGetValue(entityId, out entityPermissions) == false)
+ {
+ entityPermissions = this.Where(x => x.EntityId == entityId).SelectMany(x => x.AssignedPermissions).Distinct().ToArray();
+ _aggregateNodePermissions[entityId] = entityPermissions;
+ }
+ return entityPermissions;
+ }
+
+ private Dictionary _aggregateNodePermissions;
+
+ ///
+ /// Returns the aggregate permissions in the permission set for all nodes
///
///
///
diff --git a/src/Umbraco.Core/Models/Membership/IUser.cs b/src/Umbraco.Core/Models/Membership/IUser.cs
index dec0095243..8219af17b9 100644
--- a/src/Umbraco.Core/Models/Membership/IUser.cs
+++ b/src/Umbraco.Core/Models/Membership/IUser.cs
@@ -58,6 +58,11 @@ namespace Umbraco.Core.Models.Membership
///
/// Will hold the media file system relative path of the users custom avatar if they uploaded one
///
- string Avatar { get; set; }
+ string Avatar { get; set; }
+
+ ///
+ /// A Json blob stored for recording tour data for a user
+ ///
+ string TourData { get; set; }
}
-}
+}
\ No newline at end of file
diff --git a/src/Umbraco.Core/Models/Membership/IUserGroup.cs b/src/Umbraco.Core/Models/Membership/IUserGroup.cs
index de5842df61..508eb015ed 100644
--- a/src/Umbraco.Core/Models/Membership/IUserGroup.cs
+++ b/src/Umbraco.Core/Models/Membership/IUserGroup.cs
@@ -3,7 +3,7 @@ using Umbraco.Core.Models.Entities;
namespace Umbraco.Core.Models.Membership
{
- public interface IUserGroup : IEntity
+ public interface IUserGroup : IEntity, IRememberBeingDirty
{
string Alias { get; set; }
diff --git a/src/Umbraco.Core/Models/Membership/MemberExportModel.cs b/src/Umbraco.Core/Models/Membership/MemberExportModel.cs
new file mode 100644
index 0000000000..7153d380b4
--- /dev/null
+++ b/src/Umbraco.Core/Models/Membership/MemberExportModel.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+
+namespace Umbraco.Core.Models.Membership
+{
+ internal class MemberExportModel
+ {
+ public int Id { get; set; }
+ public Guid Key { get; set; }
+ public string Name { get; set; }
+ public string Username { get; set; }
+ public string Email { get; set; }
+ public List Groups { get; set; }
+ public string ContentTypeAlias { get; set; }
+ public DateTime CreateDate { get; set; }
+ public DateTime UpdateDate { get; set; }
+ public List Properties { get; set; }
+ }
+}
diff --git a/src/Umbraco.Core/Models/Membership/MemberExportProperty.cs b/src/Umbraco.Core/Models/Membership/MemberExportProperty.cs
new file mode 100644
index 0000000000..546d9255ea
--- /dev/null
+++ b/src/Umbraco.Core/Models/Membership/MemberExportProperty.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace Umbraco.Core.Models.Membership
+{
+ internal class MemberExportProperty
+ {
+ public int Id { get; set; }
+ public string Alias { get; set; }
+ public string Name { get; set; }
+ public object Value { get; set; }
+ public DateTime? CreateDate { get; set; }
+ public DateTime? UpdateDate { get; set; }
+ }
+}
diff --git a/src/Umbraco.Core/Models/Membership/User.cs b/src/Umbraco.Core/Models/Membership/User.cs
index de410ffb9a..2dd750a353 100644
--- a/src/Umbraco.Core/Models/Membership/User.cs
+++ b/src/Umbraco.Core/Models/Membership/User.cs
@@ -100,6 +100,7 @@ namespace Umbraco.Core.Models.Membership
private string _name;
private string _securityStamp;
private string _avatar;
+ private string _tourData;
private int _sessionTimeout;
private int[] _startContentIds;
private int[] _startMediaIds;
@@ -133,6 +134,7 @@ namespace Umbraco.Core.Models.Membership
public readonly PropertyInfo SecurityStampSelector = ExpressionHelper.GetPropertyInfo(x => x.SecurityStamp);
public readonly PropertyInfo AvatarSelector = ExpressionHelper.GetPropertyInfo(x => x.Avatar);
+ public readonly PropertyInfo TourDataSelector = ExpressionHelper.GetPropertyInfo(x => x.TourData);
public readonly PropertyInfo SessionTimeoutSelector = ExpressionHelper.GetPropertyInfo(x => x.SessionTimeout);
public readonly PropertyInfo StartContentIdSelector = ExpressionHelper.GetPropertyInfo(x => x.StartContentIds);
public readonly PropertyInfo StartMediaIdSelector = ExpressionHelper.GetPropertyInfo(x => x.StartMediaIds);
@@ -467,6 +469,16 @@ namespace Umbraco.Core.Models.Membership
set { SetPropertyValueAndDetectChanges(value, ref _avatar, Ps.Value.AvatarSelector); }
}
+ ///
+ /// A Json blob stored for recording tour data for a user
+ ///
+ [DataMember]
+ public string TourData
+ {
+ get { return _tourData; }
+ set { SetPropertyValueAndDetectChanges(value, ref _tourData, Ps.Value.TourDataSelector); }
+ }
+
///
/// Gets or sets the session timeout.
///
@@ -671,5 +683,6 @@ namespace Umbraco.Core.Models.Membership
return _user.GetHashCode();
}
}
+
}
}
diff --git a/src/Umbraco.Core/Models/Membership/UserGroup.cs b/src/Umbraco.Core/Models/Membership/UserGroup.cs
index ccd60f5861..db21c78438 100644
--- a/src/Umbraco.Core/Models/Membership/UserGroup.cs
+++ b/src/Umbraco.Core/Models/Membership/UserGroup.cs
@@ -12,7 +12,7 @@ namespace Umbraco.Core.Models.Membership
///
[Serializable]
[DataContract(IsReference = true)]
- internal class UserGroup : EntityBase, IUserGroup, IReadOnlyUserGroup
+ public class UserGroup : EntityBase, IUserGroup, IReadOnlyUserGroup
{
private int? _startContentId;
private int? _startMediaId;
diff --git a/src/Umbraco.Core/Models/PropertyCollection.cs b/src/Umbraco.Core/Models/PropertyCollection.cs
index 9c51ddbe79..3d02c13e6c 100644
--- a/src/Umbraco.Core/Models/PropertyCollection.cs
+++ b/src/Umbraco.Core/Models/PropertyCollection.cs
@@ -158,7 +158,7 @@ namespace Umbraco.Core.Models
public bool TryGetValue(string propertyTypeAlias, out Property property)
{
- property = this[propertyTypeAlias];
+ property = this.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias));
return property != null;
}
diff --git a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs
index c58f69d223..31b5f2e513 100644
--- a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs
+++ b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs
@@ -188,11 +188,11 @@ namespace Umbraco.Core.Models
Language,
///
- /// Document
+ /// Document Blueprint
///
[UmbracoObjectType(Constants.ObjectTypes.Strings.DocumentBlueprint, typeof(IContent))]
[FriendlyName("DocumentBlueprint")]
- [UmbracoUdiType(Constants.UdiEntityType.DocumentBluePrint)]
+ [UmbracoUdiType(Constants.UdiEntityType.DocumentBlueprint)]
DocumentBlueprint,
///
diff --git a/src/Umbraco.Core/Models/UserExtensions.cs b/src/Umbraco.Core/Models/UserExtensions.cs
index 2ec9f43a53..f66f0b4ef7 100644
--- a/src/Umbraco.Core/Models/UserExtensions.cs
+++ b/src/Umbraco.Core/Models/UserExtensions.cs
@@ -48,12 +48,11 @@ namespace Umbraco.Core.Models
/// Tries to lookup the user's gravatar to see if the endpoint can be reached, if so it returns the valid URL
///
///
- ///
///
///
/// A list of 5 different sized avatar URLs
///
- internal static string[] GetCurrentUserAvatarUrls(this IUser user, IUserService userService, ICacheProvider staticCache)
+ internal static string[] GetUserAvatarUrls(this IUser user, ICacheProvider staticCache)
{
//check if the user has explicitly removed all avatars including a gravatar, this will be possible and the value will be "none"
if (user.Avatar == "none")
@@ -276,6 +275,16 @@ namespace Umbraco.Core.Models
return false;
}
+ ///
+ /// Determines whether this user has access to view sensitive data
+ ///
+ ///
+ public static bool HasAccessToSensitiveData(this IUser user)
+ {
+ if (user == null) throw new ArgumentNullException("user");
+ return user.Groups != null && user.Groups.Any(x => x.Alias == Constants.Security.SensitiveDataGroupAlias);
+ }
+
// calc. start nodes, combining groups' and user's, and excluding what's in the bin
public static int[] CalculateContentStartNodeIds(this IUser user, IEntityService entityService)
{
diff --git a/src/Umbraco.Core/ObjectExtensions.cs b/src/Umbraco.Core/ObjectExtensions.cs
index 1c928730e2..4f48322a8f 100644
--- a/src/Umbraco.Core/ObjectExtensions.cs
+++ b/src/Umbraco.Core/ObjectExtensions.cs
@@ -6,10 +6,10 @@ using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
+using System.Runtime.CompilerServices;
using System.Xml;
using Newtonsoft.Json;
-using Newtonsoft.Json.Serialization;
-using Umbraco.Core.Composing;
+using Umbraco.Core.Collections;
namespace Umbraco.Core
{
@@ -18,8 +18,15 @@ namespace Umbraco.Core
///
public static class ObjectExtensions
{
- private static readonly ConcurrentDictionary> ToObjectTypes
- = new ConcurrentDictionary>();
+ private static readonly ConcurrentDictionary> ToObjectTypes = new ConcurrentDictionary>();
+ private static readonly ConcurrentDictionary NullableGenericCache = new ConcurrentDictionary();
+ private static readonly ConcurrentDictionary InputTypeConverterCache = new ConcurrentDictionary();
+ private static readonly ConcurrentDictionary DestinationTypeConverterCache = new ConcurrentDictionary();
+ private static readonly ConcurrentDictionary AssignableTypeCache = new ConcurrentDictionary();
+ private static readonly ConcurrentDictionary BoolConvertCache = new ConcurrentDictionary();
+
+ private static readonly char[] NumberDecimalSeparatorsToNormalize = { '.', ',' };
+ private static readonly CustomBooleanTypeConverter CustomBooleanTypeConverter = new CustomBooleanTypeConverter();
//private static readonly ConcurrentDictionary> ObjectFactoryCache = new ConcurrentDictionary>();
@@ -40,8 +47,8 @@ namespace Umbraco.Core
///
public static void DisposeIfDisposable(this object input)
{
- var disposable = input as IDisposable;
- if (disposable != null) disposable.Dispose();
+ if (input is IDisposable disposable)
+ disposable.Dispose();
}
///
@@ -53,347 +60,335 @@ namespace Umbraco.Core
///
internal static T SafeCast(this object input)
{
- if (ReferenceEquals(null, input) || ReferenceEquals(default(T), input)) return default(T);
- if (input is T) return (T)input;
- return default(T);
+ if (ReferenceEquals(null, input) || ReferenceEquals(default(T), input)) return default;
+ if (input is T variable) return variable;
+ return default;
}
///
- /// Tries to convert the input object to the output type using TypeConverters
+ /// Attempts to convert the input object to the output type.
///
- ///
- ///
- ///
+ /// This code is an optimized version of the original Umbraco method
+ /// The type to convert to
+ /// The input.
+ /// The
public static Attempt TryConvertTo(this object input)
{
var result = TryConvertTo(input, typeof(T));
- if (result.Success == false)
+
+ if (result.Success)
+ return Attempt.Succeed((T)result.Result);
+
+ // just try to cast
+ try
{
- //just try a straight up conversion
- try
- {
- var converted = (T) input;
- return Attempt.Succeed(converted);
- }
- catch (Exception e)
- {
- return Attempt.Fail(e);
- }
+ return Attempt.Succeed((T)input);
+ }
+ catch (Exception e)
+ {
+ return Attempt.Fail(e);
}
- return result.Success == false ? Attempt.Fail() : Attempt.Succeed((T)result.Result);
}
///
- /// Tries to convert the input object to the output type using TypeConverters. If the destination
- /// type is a superclass of the input type, if will use .
+ /// Attempts to convert the input object to the output type.
///
+ /// This code is an optimized version of the original Umbraco method
/// The input.
- /// Type of the destination.
- ///
- public static Attempt