diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md deleted file mode 100644 index 1acc6b602b..0000000000 --- a/.github/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,93 +0,0 @@ -# Umbraco Code of Conduct - -## Preamble - -We are the friendly CMS. And our friendliness stems from our values. That's why we have set for ourselves, Umbraco HQ, and the community, five values to guide us in everything we do: - -* Trust - We believe in and empower people -* Respect - We treat others as we would like to be treated -* Open - We share our thoughts and knowledge -* Hungry - We want to do things better, best is next -* Friendly - We want to build long-lasting relationships - -With these values in mind, we want to offer the Umbraco community a code of conduct that specifies a baseline standard of behavior so that people with different social values and communication styles can work together. - -This code of conduct is based on the widely used Contributor Covenant, as described in [https://www.contributor-covenant.org/](https://www.contributor-covenant.org/) - -## Our Pledge - -We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. - -We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. - -## Our Standards -Examples of behavior that contributes to a positive environment for our community include: - -* Demonstrating empathy and kindness toward other people -* Being respectful of differing opinions, viewpoints, and experiences -* Giving and gracefully accepting constructive feedback -* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience -* Focusing on what is best not just for us as individuals, but for the overall community - -Examples of unacceptable behavior include: - -* The use of sexualized language or imagery, and sexual attention or advances of any kind -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or email address, without their explicit permission -* Other conduct which could reasonably be considered inappropriate in a professional setting - -## Enforcement Responsibilities - -Community leaders (e.g. Meetup & festival organizers, moderators, maintainers, ...) are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. - -Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. - -Specific enforcement steps are listed in the [Code of Conduct Enforcement Guidelines](https://github.com/umbraco/Umbraco-CMS/blob/v8/contrib/.github/CODE_OF_CONDUCT_ENFORCEMENT.md) document which is an appendix of this document, updated and maintained by the Code of Conduct Team. - -## Scope -This Code of Conduct applies within all community spaces and events supported by Umbraco HQ or using the Umbraco name. It also applies when an individual is officially representing the community in public spaces. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior, may be reported at [conduct@umbraco.com](mailto:conduct@umbraco.com). All complaints will be reviewed and investigated promptly and fairly. - -Or alternatively, you can reach out directly to any of the team members behind the address above: - -* Sebastiaan Janssen (He, Him - Languages spoken: English, Dutch, Danish(Read)) [sebastiaan@umbraco.com](mailto:sebastiaan@umbraco.com) -* Ilham Boulghallat (She, Her - Languages spoken: English, French, Arabic) [ilham@umbraco.com](mailto:ilham@umbraco.com) -* Arnold Visser (He, Him - Languages spoken: English, Dutch) [arnold@umbraco.com](mailto:arnold@umbraco.com) -* Emma Burstow (She, Her - Languages spoken: English) [ema@umbraco.com](mailto:ema@umbraco.com) - -The review process is done with full respect for the privacy and security of the reporter of any incident. - -People with a conflict of interest should exclude themselves or if necessary be excluded by the other team members. - -## Enforcement Guidelines - -Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: - -**1. Correction** -Community Impact: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. - -Consequence: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. - -**2. Warning** -Community Impact: A violation through a single incident or series of actions. - -Consequence: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. - -**3. Temporary Ban** -Community Impact: A serious violation of community standards, including sustained inappropriate behavior. - -Consequence: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. - -**4. Permanent Ban** -Community Impact: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. - -Consequence: A permanent ban from any sort of public interaction within the community. - -## Attribution -This Code of Conduct is adapted from the Contributor Covenant, version 2.0, available at [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html](https://www.contributor-covenant.org/version/2/0/code_of_conduct.html). - -This Code of Conduct will be maintained and reviewed by the team listed above. diff --git a/.github/CODE_OF_CONDUCT_ENFORCEMENT.md b/.github/CODE_OF_CONDUCT_ENFORCEMENT.md deleted file mode 100644 index 2bb45644c2..0000000000 --- a/.github/CODE_OF_CONDUCT_ENFORCEMENT.md +++ /dev/null @@ -1,57 +0,0 @@ -# Umbraco Code of Conduct Enforcement guidelines - Consequence Ladder - -These are the steps followed by the [Umbraco Code of Conduct Team](https://github.com/umbraco/Umbraco-CMS/blob/v8/contrib/.github/CODE_OF_CONDUCT.md) when we respond to an issue or incident brought to our attention by a community member. - -This is an appendix to the Code of Conduct and is updated and maintained by the Code of Conduct Team. - -To make sure that all reports will be reviewed and investigated promptly and fairly, as highlighted in the Umbraco Code of Conduct, we are following [Mozilla’s Consequence Ladder approach](https://github.com/mozilla/inclusion/blob/master/code-of-conduct-enforcement/consequence-ladder.md). - -This approach helps the Team enforce the Code of Conduct in a structured manner and can be used as a way of communicating escalation. Each time the Team takes an action (warning, ban) the individual is made aware of future consequences. The Team can either follow the order of the levels in the ladder or decide to jump levels. When needed, the team can go directly to a permanent ban. - -**Level 0: No Action** -Recommendations do not indicate a violation of the Code of Conduct. - -**Level 1: Simple Warning Issued** -A private, written warning from the Code of Conduct Team, with clarity of violation, consequences of continued behavior. - -**Level 2: Warning** -A private, written warning from the Code of Conduct Team, with clarity of violation, consequences of continued behavior. Additionally: - -* Communication of next-level consequences if behaviors are repeated (according to this ladder). - -**Level 3: Warning + Mandatory Cooling Off Period (Access Retained)** -A private warning from the Code of Conduct Team, with clarity of violation, consequences of continued behavior. Additionally: - -* Request to avoid interaction on community messaging platforms (public forums, Our, commenting on issues). - * This includes avoiding any interactions in any Umbraco channels, spaces/offices, as well as external channels like social media (e.g. Twitter, Facebook, LinkedIn). For example, 'following/liking/retweeting' would be considered a violation of these terms, and consequence would escalate according to this ladder. -* Require they do not interact with others in the report, or those who they suspect are involved in the report. -* Suggestions for 'out of office' type of message on platforms, to reduce curiosity, or suspicion among those not involved. - -**Level 4: Temporary Ban (Access Revoked)** -Private communication of ban from the Code of Conduct Team, with clarity of violation, consequences of continued behavior. Additionally: - -* 3-6 months imposed break. -* All accounts deactivated, or blocked during this time (Our, HQ Slack if applicable). -* Require to avoid interaction on community messaging platforms (public forums, Our, commenting on issues). - * This includes avoiding any interactions in any Umbraco channels, spaces/offices, as well as external channels like social media (e.g. Twitter, Facebook, LinkedIn). For example, 'following/liking/retweeting' would be considered a violation of these terms, and consequence would escalate according to this ladder. -* All community leadership roles (e.g. Community Teams, Meetup/festival organizer, Commit right on Github..) suspended. (onboarding/reapplication required outside of this process) -* No attendance at Umbraco events during the ban period. -* Not allowed to enter Umbraco HQ offices during the ban period. -* Permission to use the MVP title, if applicable, is revoked during this ban period. -* The community leaders running events and other initiatives are informed of the ban. - -**Level 5: Permanent Ban** -Private communication of ban from the Code of Conduct Team, with clarity of violation, consequences of continued behavior. Additionally: - -* All accounts deactivated permanently. -* No attendance at Umbraco events going forward. -* Not allowed to enter Umbraco HQ offices permanently. -* All community leadership roles (e.g. Community Teams, Meetup/festival organizer, Commit right on Github..) permanently suspended. -* Permission to use the MVP title, if applicable, revoked. -* The community leaders running events and other initiatives are informed of the ban. - - -Sources: -* [Mozilla Code of Conduct - Enforcement Consequence Ladder](https://github.com/mozilla/inclusion/blob/master/code-of-conduct-enforcement/consequence-ladder.md) -* [Drupal Conflict Resolution Policy and Process](https://www.drupal.org/conflict-resolution) -* [Django Code of Conduct - Enforcement Manual](https://www.djangoproject.com/conduct/enforcement-manual/) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 4cb593a39b..e8b378fb15 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -10,7 +10,7 @@ Remember, we're a friendly bunch and are happy with whatever contribution you mi **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). +This project and everyone participating in it, is governed by the [our Code of Conduct](https://github.com/umbraco/.github/blob/main/.github/CODE_OF_CONDUCT.md). **Table of contents** diff --git a/.github/README.md b/.github/README.md index d4565a8cb5..5fa412cae4 100644 --- a/.github/README.md +++ b/.github/README.md @@ -1,4 +1,4 @@ -# [Umbraco CMS](https://umbraco.com) · [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](../LICENSE.md) [![Build status](https://umbraco.visualstudio.com/Umbraco%20Cms/_apis/build/status/Cms%208%20Continuous?branchName=v8/contrib)](https://umbraco.visualstudio.com/Umbraco%20Cms/_build?definitionId=75) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) [![Twitter](https://img.shields.io/twitter/follow/umbraco.svg?style=social&label=Follow)](https://twitter.com/intent/follow?screen_name=umbraco) +# [Umbraco CMS](https://umbraco.com) · [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](../LICENSE.md) [![Build status](https://umbraco.visualstudio.com/Umbraco%20Cms/_apis/build/status/Cms%208%20Continuous?branchName=v8/contrib)](https://umbraco.visualstudio.com/Umbraco%20Cms/_build?definitionId=75) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) [![Twitter](https://img.shields.io/twitter/follow/umbraco.svg?style=social&label=Follow)](https://twitter.com/intent/follow?screen_name=umbraco) [![Discord](https://img.shields.io/discord/869656431308189746)](https://discord.gg/umbraco) Umbraco is the friendliest, most flexible and fastest growing ASP.NET CMS, and used by more than 500,000 websites worldwide. Our mission is to help you deliver delightful digital experiences by making Umbraco friendly, simpler and social. @@ -15,7 +15,7 @@ See the official [Umbraco website](https://umbraco.com) for an introduction, cor - [Community](#join-the-umbraco-community) - [Contributing](#contributing) -Please also see our [Code of Conduct](CODE_OF_CONDUCT.md). +Please also see our [Code of Conduct](https://github.com/umbraco/.github/blob/main/.github/CODE_OF_CONDUCT.md). ## Getting Started diff --git a/.github/workflows/pr-first-response.yml b/.github/workflows/pr-first-response.yml new file mode 100644 index 0000000000..f54c8b91ba --- /dev/null +++ b/.github/workflows/pr-first-response.yml @@ -0,0 +1,26 @@ +name: pr-first-response + +on: + pull_request: + types: [opened] + +jobs: + send-response: + runs-on: ubuntu-latest + steps: + - name: Fetch random comment 🗣️ + uses: JamesIves/fetch-api-data-action@v2.1.0 + with: + ENDPOINT: https://collaboratorsv2.euwest01.umbraco.io/umbraco/api/comments/PostComment + CONFIGURATION: '{ "method": "POST", "headers": {"Authorization": "Bearer ${{ secrets.OUR_BOT_API_TOKEN }}", "Content-Type": "application/json" }, "body": { "repo": "${{ github.repository }}", "number": "${{ github.event.number }}", "actor": "${{ github.actor }}", "commentType": "opened-pr-first-comment"} }' + - name: Add PR comment + if: "${{ env.fetch-api-data != '' }}" + uses: actions/github-script@v5 + with: + script: | + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: `${{ env.fetch-api-data }}` + }) \ No newline at end of file diff --git a/.gitignore b/.gitignore index 59b050f634..5775b2c708 100644 --- a/.gitignore +++ b/.gitignore @@ -85,6 +85,12 @@ src/Umbraco.Web.UI/wwwroot/[Uu]mbraco/lib/* src/Umbraco.Web.UI/wwwroot/[Uu]mbraco/views/* src/Umbraco.Web.UI/wwwroot/Media/* src/Umbraco.Web.UI/Smidge/ +src/Umbraco.Web.UI/App_Code/ +src/Umbraco.Web.UI/App_Plugins/ +src/Umbraco.Web.UI/Views/ +!src/Umbraco.Web.UI/Views/Partials/blocklist/ +!src/Umbraco.Web.UI/Views/Partials/grid/ +!src/Umbraco.Web.UI/Views/_ViewImports.cshtml # Tests cypress.env.json diff --git a/build/NuSpecs/buildTransitive/Umbraco.Cms.StaticAssets.props b/build/NuSpecs/buildTransitive/Umbraco.Cms.StaticAssets.props index 371b0aa5ab..ea0b013665 100644 --- a/build/NuSpecs/buildTransitive/Umbraco.Cms.StaticAssets.props +++ b/build/NuSpecs/buildTransitive/Umbraco.Cms.StaticAssets.props @@ -1,7 +1,8 @@ - - - - $(DefaultItemExcludes);wwwroot\is-cache\**;wwwroot\ms-cache\** - - + + + $(DefaultItemExcludes);App_Plugins/** + $(DefaultItemExcludes);umbraco/Data/** + $(DefaultItemExcludes);umbraco/Logs/** + $(DefaultItemExcludes);wwwroot/media/** + diff --git a/build/NuSpecs/buildTransitive/Umbraco.Cms.StaticAssets.targets b/build/NuSpecs/buildTransitive/Umbraco.Cms.StaticAssets.targets index 7f73c14650..2ef0a44a9c 100644 --- a/build/NuSpecs/buildTransitive/Umbraco.Cms.StaticAssets.targets +++ b/build/NuSpecs/buildTransitive/Umbraco.Cms.StaticAssets.targets @@ -1,4 +1,4 @@ - + $(MSBuildThisFileDirectory)..\content\umbraco\**\*.* @@ -6,16 +6,6 @@ umbraco - - $(DefaultItemExcludes);App_Plugins\**; - - $(DefaultItemExcludes);umbraco\Data\**; - $(DefaultItemExcludes);umbraco\Logs\**; - $(DefaultItemExcludes);umbraco\mediacache\**; - - $(DefaultItemExcludes);wwwroot\media\**; - - @@ -39,6 +29,7 @@ @@ -65,6 +56,7 @@ @@ -92,7 +84,7 @@ - + diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml index ae92f37709..feecac1c10 100644 --- a/build/azure-pipelines.yml +++ b/build/azure-pipelines.yml @@ -540,42 +540,27 @@ stages: $ubuild = build/build.ps1 -get -continue - $version = $ubuild.GetUmbracoVersion() $isRelease = [regex]::matches($env:BUILD_SOURCEBRANCH,"v\d+\/\d+.\d+.*") - if ($isRelease.Count -gt 0){ + if ($isRelease.Count -gt 0) { $continuous = $version.Semver - - } - else - { + } else { $date = (Get-Date).ToString("yyyyMMdd") $continuous = "$($version.release)-preview$date.$(Build.BuildId)" $ubuild.SetUmbracoVersion($continuous) - #Update the version in templates also - - $templatePath = - 'build/templates/UmbracoProject/.template.config/template.json' - - $a = Get-Content $templatePath -raw | ConvertFrom-Json - - $a.symbols.version.defaultValue = $continuous - - $a | ConvertTo-Json -depth 32| set-content $templatePath - - - $templatePath = - 'build/templates/UmbracoPackage/.template.config/template.json' - - $a = Get-Content $templatePath -raw | ConvertFrom-Json - - $a.symbols.version.defaultValue = $continuous - - $a | ConvertTo-Json -depth 32| set-content $templatePath - } + # Update the default Umbraco version in templates + $templatePaths = Get-ChildItem 'templates/**/.template.config/template.json' + foreach ($templatePath in $templatePaths) { + $a = Get-Content $templatePath -Raw | ConvertFrom-Json + if ($a.symbols -and $a.symbols.UmbracoVersion) { + $a.symbols.UmbracoVersion.defaultValue = $continuous + $a | ConvertTo-Json -Depth 32 | Set-Content $templatePath + } + } + } Write-Host "##vso[build.updatebuildnumber]$continuous.$(Build.BuildId)" diff --git a/build/build.ps1 b/build/build.ps1 index 8b4764c0df..c2be223a2c 100644 --- a/build/build.ps1 +++ b/build/build.ps1 @@ -11,7 +11,7 @@ [Alias("loc")] [switch] $local = $false, - # enable docfx + # enable docfx [Parameter(Mandatory=$false)] [Alias("doc")] [switch] $docfx = $false, @@ -40,7 +40,7 @@ @{ Continue = $continue }) if ($ubuild.OnError()) { return } - Write-Host "Umbraco Cms Build" + Write-Host "Umbraco CMS Build" Write-Host "Umbraco.Build v$($ubuild.BuildVersion)" # ################################################################ @@ -84,7 +84,7 @@ $this.SetEnvVar("NPM_CONFIG_CACHE", $node_npmcache) $this.SetEnvVar("NPM_CONFIG_PREFIX", $node_npmprefix) - $ignore = $this.ClearEnvVar("NODE_NO_HTTP2") + $this.ClearEnvVar("NODE_NO_HTTP2") }) $ubuild.DefineMethod("CompileBelle", @@ -171,11 +171,6 @@ $src = "$($this.SolutionRoot)\src" $log = "$($this.BuildTemp)\build.umbraco.log" - if ($this.BuildEnv.VisualStudio -eq $null) - { - throw "Build environment does not provide VisualStudio." - } - Write-Host "Compile Umbraco" Write-Host "Logging to $log" @@ -191,14 +186,14 @@ # remove extra files $webAppBin = "$($this.BuildTemp)\WebApp\bin" - $excludeDirs = @("$($webAppBin)\refs","$($webAppBin)\runtimes","$($webAppBin)\Umbraco","$($webAppBin)\wwwroot") + $excludeDirs = @("$($webAppBin)\refs","$($webAppBin)\runtimes","$($webAppBin)\umbraco","$($webAppBin)\wwwroot") $excludeFiles = @("$($webAppBin)\appsettings.*","$($webAppBin)\*.deps.json","$($webAppBin)\*.exe","$($webAppBin)\*.config","$($webAppBin)\*.runtimeconfig.json") $this.RemoveDirectory($excludeDirs) $this.RemoveFile($excludeFiles) # copy rest of the files into WebApp - $this.CopyFiles("$($this.SolutionRoot)\src\Umbraco.Web.UI\Umbraco", "*", "$($this.BuildTemp)\WebApp\umbraco") - $excludeUmbracoDirs = @("$($this.BuildTemp)\WebApp\umbraco\lib") + $this.CopyFiles("$($this.SolutionRoot)\src\Umbraco.Web.UI\umbraco", "*", "$($this.BuildTemp)\WebApp\umbraco") + $excludeUmbracoDirs = @("$($this.BuildTemp)\WebApp\umbraco\lib","$($this.BuildTemp)\WebApp\umbraco\Data","$($this.BuildTemp)\WebApp\umbraco\Logs") $this.RemoveDirectory($excludeUmbracoDirs) $this.CopyFiles("$($this.SolutionRoot)\src\Umbraco.Web.UI\Views", "*", "$($this.BuildTemp)\WebApp\Views") Copy-Item "$($this.SolutionRoot)\src\Umbraco.Web.UI\appsettings.json" "$($this.BuildTemp)\WebApp" @@ -251,27 +246,21 @@ $buildConfiguration = "Release" $log = "$($this.BuildTemp)\msbuild.tests.log" - if ($this.BuildEnv.VisualStudio -eq $null) - { - throw "Build environment does not provide VisualStudio." - } - Write-Host "Compile Tests" Write-Host "Logging to $log" # beware of the weird double \\ at the end of paths # see http://edgylogic.com/blog/powershell-and-external-commands-done-right/ - &$this.BuildEnv.VisualStudio.MsBuild "$($this.SolutionRoot)\tests\Umbraco.Tests\Umbraco.Tests.csproj" ` - /p:WarningLevel=0 ` - /p:Configuration=$buildConfiguration ` - /p:Platform=AnyCPU ` - /p:UseWPP_CopyWebApplication=True ` - /p:PipelineDependsOnBuild=False ` - /p:OutDir="$($this.BuildTemp)\tests\\" ` - /p:Verbosity=minimal ` - /t:Build ` - /tv:"$($this.BuildEnv.VisualStudio.ToolsVersion)" ` - /p:UmbracoBuild=True ` + &dotnet msbuild "$($this.SolutionRoot)\tests\Umbraco.Tests\Umbraco.Tests.csproj" ` + -target:Build ` + -property:WarningLevel=0 ` + -property:Configuration=$buildConfiguration ` + -property:Platform=AnyCPU ` + -property:UseWPP_CopyWebApplication=True ` + -property:PipelineDependsOnBuild=False ` + -property:OutDir="$($this.BuildTemp)\tests\\" ` + -property:Verbosity=minimal ` + -property:UmbracoBuild=True ` > $log if (-not $?) { throw "Failed to compile tests." } @@ -285,10 +274,6 @@ $src = "$($this.SolutionRoot)\src" $tmp = "$($this.BuildTemp)" - $out = "$($this.BuildOutput)" - $templates = "$($this.SolutionRoot)\build\templates" - - $buildConfiguration = "Release" # cleanup build Write-Host "Clean build" @@ -302,7 +287,6 @@ # create directories Write-Host "Create directories" mkdir "$tmp\WebApp\App_Data" > $null - mkdir "$tmp\Templates" > $null #mkdir "$tmp\WebApp\Media" > $null #mkdir "$tmp\WebApp\Views" > $null @@ -324,17 +308,6 @@ $this.CopyFiles("$src\Umbraco.Web.UI\wwwroot\umbraco\js", "*", "$tmp\WebApp\wwwroot\umbraco\js") $this.CopyFiles("$src\Umbraco.Web.UI\wwwroot\umbraco\lib", "*", "$tmp\WebApp\wwwroot\umbraco\lib") $this.CopyFiles("$src\Umbraco.Web.UI\wwwroot\umbraco\views", "*", "$tmp\WebApp\wwwroot\umbraco\views") - - # Prepare templates - Write-Host "Copy template files" - $this.CopyFiles("$templates", "*", "$tmp\Templates") - - Write-Host "Copy files for dotnet templates" - $this.CopyFiles("$src\Umbraco.Web.UI", "Program.cs", "$tmp\Templates\UmbracoProject") - $this.CopyFiles("$src\Umbraco.Web.UI", "Startup.cs", "$tmp\Templates\UmbracoProject") - $this.CopyFiles("$src\Umbraco.Web.UI\Views", "*", "$tmp\Templates\UmbracoProject\Views") - - $this.RemoveDirectory("$tmp\Templates\UmbracoProject\bin") }) @@ -368,7 +341,7 @@ { Write-Host "Restore NuGet" Write-Host "Logging to $($this.BuildTemp)\nuget.restore.log" - $params = "-Source", $nugetsourceUmbraco + $params = "-Source", $nugetsourceUmbraco &$this.BuildEnv.NuGet restore "$($this.SolutionRoot)\umbraco.sln" > "$($this.BuildTemp)\nuget.restore.log" @params if (-not $?) { throw "Failed to restore NuGet packages." } }) @@ -376,7 +349,7 @@ $ubuild.DefineMethod("PackageNuGet", { $nuspecs = "$($this.SolutionRoot)\build\NuSpecs" - $templates = "$($this.BuildTemp)\Templates" + $templates = "$($this.SolutionRoot)\templates" Write-Host "Create NuGet packages" diff --git a/build/templates/Umbraco.Templates.nuspec b/build/templates/Umbraco.Templates.nuspec deleted file mode 100644 index 21201d8d55..0000000000 --- a/build/templates/Umbraco.Templates.nuspec +++ /dev/null @@ -1,21 +0,0 @@ - - - - Umbraco.Templates - 1.0.0 - Umbraco HQ - Umbraco HQ - MIT - https://umbraco.com/ - https://umbraco.com/dist/nuget/logo-small.png - false - Umbraco Cms templates for .NET Core Template Engine available through the dotnet CLI's new command - en-US - umbraco - - - - - - - diff --git a/build/templates/UmbracoPackage/.template.config/dotnetcli.host.json b/build/templates/UmbracoPackage/.template.config/dotnetcli.host.json deleted file mode 100644 index 141f7bf97c..0000000000 --- a/build/templates/UmbracoPackage/.template.config/dotnetcli.host.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/dotnetcli.host", - "symbolInfo": { - - } -} diff --git a/build/templates/UmbracoPackage/.template.config/ide.host.json b/build/templates/UmbracoPackage/.template.config/ide.host.json deleted file mode 100644 index 8d3bae3e3c..0000000000 --- a/build/templates/UmbracoPackage/.template.config/ide.host.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/vs-2017.3.host", - "order" : 0, - "icon": "icon.png", - "description": { - "id": "UmbracoPackage", - "text": "Umbraco Package - An empty Umbraco CMS package (Plugin)" - }, - "symbolInfo": [ - - ] - -} diff --git a/build/templates/UmbracoPackage/.template.config/template.json b/build/templates/UmbracoPackage/.template.config/template.json deleted file mode 100644 index 21232aec64..0000000000 --- a/build/templates/UmbracoPackage/.template.config/template.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/template", - "author": "Umbraco HQ", - "description": "An empty Umbraco Package/Plugin ready to get started", - "classifications": [ "Web", "CMS", "Umbraco", "Package", "Plugin"], - "groupIdentity": "Umbraco.Templates.UmbracoPackage", - "identity": "Umbraco.Templates.UmbracoPackage.CSharp", - "name": "Umbraco Package", - "shortName": "umbracopackage", - "defaultName": "UmbracoPackage1", - "preferNameDirectory": true, - "tags": { - "language": "C#", - "type": "project" - }, - "primaryOutputs": [ - { - "path": "UmbracoPackage.csproj" - } - ], - "sourceName": "UmbracoPackage", - "preferNameDirectory": true, - "symbols": { - "version": { - "type": "parameter", - "datatype": "string", - "defaultValue": "10.0.0-rc", - "description": "The version of Umbraco to load using NuGet", - "replaces": "UMBRACO_VERSION_FROM_TEMPLATE" - }, - "namespaceReplacer": { - "type": "generated", - "generator": "regex", - "dataType": "string", - "replaces": "UmbracoPackage", - "parameters": { - "source": "name", - "steps": [ - { - "regex": "\\s", - "replacement": "_" - }, - { - "regex": "-", - "replacement": "_" - }, - { - "regex": "^[^a-zA-Z_]+", - "replacement": "_" - } - ] - } - }, - "msbuildReplacer": { - "type": "generated", - "generator": "regex", - "dataType": "string", - "replaces": "UmbracoPackageMsBuild", - "parameters": { - "source": "name", - "steps": [ - { - "regex": "\\s", - "replacement": "" - }, - { - "regex": "\\.", - "replacement": "" - }, - { - "regex": "-", - "replacement": "" - }, - { - "regex": "^[^a-zA-Z_]+", - "replacement": "" - } - ] - } - }, - "Framework": { - "type": "parameter", - "description": "The target framework for the project.", - "datatype": "choice", - "choices": [ - { - "choice": "net6.0", - "description": "Target net6.0" - } - ], - "replaces": "net6.0", - "defaultValue": "net6.0" - } - } -} diff --git a/build/templates/UmbracoPackage/App_Plugins/UmbracoPackage/package.manifest b/build/templates/UmbracoPackage/App_Plugins/UmbracoPackage/package.manifest deleted file mode 100644 index 8593c62d96..0000000000 --- a/build/templates/UmbracoPackage/App_Plugins/UmbracoPackage/package.manifest +++ /dev/null @@ -1,2 +0,0 @@ -{ -} \ No newline at end of file diff --git a/build/templates/UmbracoProject/.template.config/dotnetcli.host.json b/build/templates/UmbracoProject/.template.config/dotnetcli.host.json deleted file mode 100644 index 825f0c9ee4..0000000000 --- a/build/templates/UmbracoProject/.template.config/dotnetcli.host.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/dotnetcli.host", - "symbolInfo": { - "PackageTestSiteName": { - "longName": "PackageTestSiteName", - "shortName": "p" - }, - "SkipRestore": { - "longName": "no-restore", - "shortName": "" - }, - "FriendlyName": { - "longName": "friendly-name", - "shortName": "" - }, - "Email": { - "longName": "email", - "shortName": "" - }, - "Password": { - "longName": "password", - "shortName": "" - }, - "ConnectionString":{ - "longName": "connection-string", - "shortName": "" - }, - "NoNodesViewPath":{ - "longName": "no-nodes-view-path", - "shortName": "" - }, - "UseHttpsRedirect": { - "longName": "use-https-redirect", - "shortName": "" - } - }, - "usageExamples": [ - "dotnet new umbraco -n MyNewProject", - "dotnet new umbraco -n MyNewProject --no-restore", - "dotnet new umbraco -n MyNewProject --friendly-name \"Friendly User\" --email user@email.com --password password1234 --connection-string \"Server=ConnectionStringHere\"" - ] -} diff --git a/build/templates/UmbracoProject/.template.config/icon.png b/build/templates/UmbracoProject/.template.config/icon.png deleted file mode 100644 index 6e94105808..0000000000 Binary files a/build/templates/UmbracoProject/.template.config/icon.png and /dev/null differ diff --git a/build/templates/UmbracoProject/.template.config/ide.host.json b/build/templates/UmbracoProject/.template.config/ide.host.json deleted file mode 100644 index 9e2ee6e8cd..0000000000 --- a/build/templates/UmbracoProject/.template.config/ide.host.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/vs-2017.3.host", - "order" : 0, - "icon": "icon.png", - "description": { - "id": "UmbracoProject", - "text": "Umbraco Web Application - An empty Umbraco CMS web application" - }, - "symbolInfo": [ - { - "id": "SkipRestore", - "name": { - "text": "Skips the automatic NuGet restore of the project on create" - }, - "isVisible": "true" - }, - { - "id": "PackageTestSiteName", - "name": { - "text": "Optional: Specify the name of a package that this should be a test site for" - }, - "isVisible": "true" - }, - { - "id": "FriendlyName", - "name": { - "text": "Optional: The friendly name of the user for Umbraco login when using Unattended install" - }, - "isVisible": "true" - }, - { - "id": "Email", - "name": { - "text": "Optional: Email to use for Umbraco login when using Unattended install" - }, - "isVisible": "true" - }, - { - "id": "Password", - "name": { - "text": "Optional: Password to use for Umbraco login when using Unattended install" - }, - "isVisible": "true" - }, - { - "id": "ConnectionString", - "name": { - "text": "Optional: Database connection string when using Unattended install" - }, - "isVisible": "true" - }, - { - "id": "NoNodesViewPath", - "name": { - "text": "Optional: Path to a custom view presented with the Umbraco installation contains no published content" - }, - "isVisible": "true" - }, - { - "id": "UseHttpsRedirect", - "name": { - "text": "Optional: Adds code to Startup.cs to redirect HTTP to HTTPS and enables the UseHttps setting." - }, - "isVisible": "true" - } - ] -} diff --git a/build/templates/UmbracoProject/.template.config/template.json b/build/templates/UmbracoProject/.template.config/template.json deleted file mode 100644 index 4bc2892d47..0000000000 --- a/build/templates/UmbracoProject/.template.config/template.json +++ /dev/null @@ -1,294 +0,0 @@ -{ - "$schema": "http://json.schemastore.org/template", - "author": "Umbraco HQ", - "description": "An empty Umbraco Project ready to get started", - "classifications": [ "Web", "CMS", "Umbraco"], - "groupIdentity": "Umbraco.Templates.UmbracoProject", - "identity": "Umbraco.Templates.UmbracoProject.CSharp", - "name": "Umbraco Project", - "shortName": "umbraco", - "defaultName": "UmbracoProject1", - "preferNameDirectory": true, - "tags": { - "language": "C#", - "type": "project" - }, - "primaryOutputs": [ - { - "path": "UmbracoProject.csproj" - } - ], - "postActions": [ - { - "condition": "(!SkipRestore)", - "description": "Restore NuGet packages required by this project", - "manualInstructions": [{ - "text": "Run 'dotnet restore'" - }], - "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", - "continueOnError": true - } - ], - "sourceName": "UmbracoProject", - "symbols": { - "namespaceReplacer": { - "type": "generated", - "generator": "regex", - "dataType": "string", - "replaces": "Umbraco.Cms.Web.UI", - "parameters": { - "source": "name", - "steps": [ - { - "regex": "\\s", - "replacement": "_" - }, - { - "regex": "-", - "replacement": "_" - }, - { - "regex": "^[^a-zA-Z_]+", - "replacement": "_" - } - ] - } - }, - "version": { - "type": "parameter", - "datatype": "string", - "defaultValue": "10.0.0-rc", - "description": "The version of Umbraco to load using NuGet", - "replaces": "UMBRACO_VERSION_FROM_TEMPLATE" - }, - "PackageTestSiteName": { - "type": "parameter", - "datatype":"text", - "defaultValue": "", - "replaces":"PackageTestSiteName", - "description": "The name of the package this should be a test site for (Default: '')" - }, - "Framework": { - "type": "parameter", - "description": "The target framework for the project", - "datatype": "choice", - "choices": [ - { - "choice": "net6.0", - "description": "Target net6.0" - } - ], - "replaces": "net6.0", - "defaultValue": "net6.0" - }, - "SkipRestore": { - "type": "parameter", - "datatype": "bool", - "description": "If specified, skips the automatic restore of the project on create", - "defaultValue": "false" - }, - "HttpPort": { - "type": "generated", - "generator": "port", - "replaces": "HTTP_PORT_FROM_TEMPLATE", - "parameters": { - "high": 65535, - "low": 1024, - "fallback": 5000 - } - }, - "HttpsPort": { - "type": "generated", - "generator": "port", - "replaces": "HTTPS_PORT_FROM_TEMPLATE", - "parameters": { - "low": 44300, - "high": 44399, - "fallback": 5001 - } - }, - "FriendlyName":{ - "type": "parameter", - "datatype":"text", - "description": "The friendly name of the user for Umbraco login when using Unattended install (Without installer wizard UI)", - "defaultValue": "" - }, - "FriendlyNameReplaced":{ - "type": "generated", - "generator": "regex", - "dataType": "string", - "replaces": "FRIENDLY_NAME_FROM_TEMPLATE", - "parameters": { - "source": "FriendlyName", - "steps": [ - { - "regex": "\\\\", - "replacement": "\\\\" - }, - { - "regex": "\\\"", - "replacement": "\\\"" - }, - { - "regex": "\\\n", - "replacement": "\\\n" - }, - { - "regex": "\\\t", - "replacement": "\\\t" - } - ] - } - }, - "Email":{ - "type": "parameter", - "datatype":"text", - "description": "Email to use for Umbraco login when using Unattended install (Without installer wizard UI)", - "defaultValue": "" - }, - "EmailReplaced":{ - "type": "generated", - "generator": "regex", - "dataType": "string", - "replaces": "EMAIL_FROM_TEMPLATE", - "parameters": { - "source": "Email", - "steps": [ - { - "regex": "\\\\", - "replacement": "\\\\" - }, - { - "regex": "\\\"", - "replacement": "\\\"" - }, - { - "regex": "\\\n", - "replacement": "\\\n" - }, - { - "regex": "\\\t", - "replacement": "\\\t" - } - ] - } - }, - "Password":{ - "type": "parameter", - "datatype":"text", - "description": "Password to use for Umbraco login when using Unattended install (Without installer wizard UI)", - "defaultValue": "" - }, - "PasswordReplaced":{ - "type": "generated", - "generator": "regex", - "dataType": "string", - "replaces": "PASSWORD_FROM_TEMPLATE", - "parameters": { - "source": "Password", - "steps": [ - { - "regex": "\\\\", - "replacement": "\\\\" - }, - { - "regex": "\\\"", - "replacement": "\\\"" - }, - { - "regex": "\\\n", - "replacement": "\\\n" - }, - { - "regex": "\\\t", - "replacement": "\\\t" - } - ] - } - }, - "ConnectionString":{ - "type": "parameter", - "datatype":"text", - "description": "Database connection string when using Unattended install (Without installer wizard UI)", - "defaultValue": "" - }, - "ConnectionStringReplaced":{ - "type": "generated", - "generator": "regex", - "dataType": "string", - "replaces": "CONNECTION_FROM_TEMPLATE", - "parameters": { - "source": "ConnectionString", - "steps": [ - { - "regex": "\\\\", - "replacement": "\\\\" - }, - { - "regex": "\\\"", - "replacement": "\\\"" - }, - { - "regex": "\\\n", - "replacement": "\\\n" - }, - { - "regex": "\\\t", - "replacement": "\\\t" - } - ] - } - }, - "NoNodesViewPath":{ - "type": "parameter", - "datatype":"text", - "description": "Path to a custom view presented with the Umbraco installation contains no published content", - "defaultValue": "" - }, - "NoNodesViewPathReplaced":{ - "type": "generated", - "generator": "regex", - "dataType": "string", - "replaces": "NO_NODES_VIEW_PATH_FROM_TEMPLATE", - "parameters": { - "source": "NoNodesViewPath", - "steps": [ - { - "regex": "\\\\", - "replacement": "\\\\" - }, - { - "regex": "\\\"", - "replacement": "\\\"" - }, - { - "regex": "\\\n", - "replacement": "\\\n" - }, - { - "regex": "\\\t", - "replacement": "\\\t" - } - ] - } - }, - "HasConnectionString":{ - "type": "computed", - "value": "(ConnectionString != \"\")" - }, - "HasNoNodesViewPath":{ - "type": "computed", - "value": "(NoNodesViewPath != \"\")" - }, - "UsingUnattenedInstall":{ - "type": "computed", - "value": "(FriendlyName != \"\" && Email != \"\" && Password != \"\" && ConnectionString != \"\")" - }, - "UseHttpsRedirect":{ - "type": "parameter", - "datatype":"bool", - "defaultValue": "false", - "description": "Adds code to Startup.cs to redirect HTTP to HTTPS and enables the UseHttps setting (Default: false)" - } - } -} diff --git a/build/templates/UmbracoProject/UmbracoProject.csproj b/build/templates/UmbracoProject/UmbracoProject.csproj deleted file mode 100644 index 3cfe803a3f..0000000000 --- a/build/templates/UmbracoProject/UmbracoProject.csproj +++ /dev/null @@ -1,38 +0,0 @@ - - - - net6.0 - Umbraco.Cms.Web.UI - - - - - - - - - - - - - - - - - - - - - true - - - - - false - false - - - diff --git a/build/templates/UmbracoProject/Views/_ViewImports.cshtml b/build/templates/UmbracoProject/Views/_ViewImports.cshtml deleted file mode 100644 index cb9a0b658e..0000000000 --- a/build/templates/UmbracoProject/Views/_ViewImports.cshtml +++ /dev/null @@ -1,8 +0,0 @@ -@using Umbraco.Web.UI -@using Umbraco.Extensions -@using Umbraco.Web.PublishedModels -@using Umbraco.Cms.Core.Models.PublishedContent -@using Microsoft.AspNetCore.Html -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers -@addTagHelper *, Smidge -@inject Smidge.SmidgeHelper SmidgeHelper diff --git a/src/JsonSchema/JsonSchema.csproj b/src/JsonSchema/JsonSchema.csproj index e46fc3ee4b..3cd8def105 100644 --- a/src/JsonSchema/JsonSchema.csproj +++ b/src/JsonSchema/JsonSchema.csproj @@ -1,27 +1,24 @@ - - - Exe - net6.0 - true - + + Exe + net6.0 + true + false + - - - - - - + + + - + $(UserProfile)\.nuget\packages\ diff --git a/src/Umbraco.Core/Composing/TypeFinder.cs b/src/Umbraco.Core/Composing/TypeFinder.cs index e66b30cf07..dfeac6a731 100644 --- a/src/Umbraco.Core/Composing/TypeFinder.cs +++ b/src/Umbraco.Core/Composing/TypeFinder.cs @@ -198,7 +198,7 @@ namespace Umbraco.Cms.Core.Composing IEnumerable? assemblies = null, bool onlyConcreteClasses = true) { - var assemblyList = (assemblies ?? AssembliesToScan).ToList(); + var assemblyList = assemblies ?? AssembliesToScan; return GetClassesWithBaseType(assignTypeFrom, assemblyList, onlyConcreteClasses, //the additional filter will ensure that any found types also have the attribute applied. @@ -214,7 +214,7 @@ namespace Umbraco.Cms.Core.Composing /// public IEnumerable FindClassesOfType(Type assignTypeFrom, IEnumerable? assemblies = null, bool onlyConcreteClasses = true) { - var assemblyList = (assemblies ?? AssembliesToScan).ToList(); + var assemblyList = assemblies ?? AssembliesToScan; return GetClassesWithBaseType(assignTypeFrom, assemblyList, onlyConcreteClasses); } @@ -231,7 +231,7 @@ namespace Umbraco.Cms.Core.Composing IEnumerable? assemblies = null, bool onlyConcreteClasses = true) { - var assemblyList = (assemblies ?? AssembliesToScan).ToList(); + var assemblyList = assemblies ?? AssembliesToScan; return GetClassesWithAttribute(attributeType, assemblyList, onlyConcreteClasses); } diff --git a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs index b355d81444..38e00b876b 100644 --- a/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/GlobalSettings.cs @@ -19,7 +19,7 @@ namespace Umbraco.Cms.Core.Configuration.Models internal const bool StaticHideTopLevelNodeFromPath = true; internal const bool StaticUseHttps = false; internal const int StaticVersionCheckPeriod = 7; - internal const string StaticUmbracoPath = "~/umbraco"; + internal const string StaticUmbracoPath = Constants.System.DefaultUmbracoPath; internal const string StaticIconsPath = "~/umbraco/assets/icons"; internal const string StaticUmbracoCssPath = "~/css"; internal const string StaticUmbracoScriptsPath = "~/scripts"; diff --git a/src/Umbraco.Core/Configuration/Models/KeepAliveSettings.cs b/src/Umbraco.Core/Configuration/Models/KeepAliveSettings.cs index 305df2be8c..297e1dff87 100644 --- a/src/Umbraco.Core/Configuration/Models/KeepAliveSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/KeepAliveSettings.cs @@ -12,6 +12,7 @@ namespace Umbraco.Cms.Core.Configuration.Models public class KeepAliveSettings { internal const bool StaticDisableKeepAliveTask = false; + internal const string StaticKeepAlivePingUrl = "~/api/keepalive/ping"; /// /// Gets or sets a value indicating whether the keep alive task is disabled. @@ -20,8 +21,9 @@ namespace Umbraco.Cms.Core.Configuration.Models public bool DisableKeepAliveTask { get; set; } = StaticDisableKeepAliveTask; /// - /// Gets a value for the keep alive ping URL. + /// Gets or sets a value for the keep alive ping URL. /// - public string KeepAlivePingUrl => "~/api/keepalive/ping"; + [DefaultValue(StaticKeepAlivePingUrl)] + public string KeepAlivePingUrl { get; set; } = StaticKeepAlivePingUrl; } } diff --git a/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs b/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs index 7767d1dbdc..ee41fc32d3 100644 --- a/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/NuCacheSettings.cs @@ -13,6 +13,7 @@ namespace Umbraco.Cms.Core.Configuration.Models { internal const string StaticNuCacheSerializerType = "MessagePack"; internal const int StaticSqlPageSize = 1000; + internal const int StaticKitBatchSize = 1; /// /// Gets or sets a value defining the BTree block size. @@ -31,6 +32,12 @@ namespace Umbraco.Cms.Core.Configuration.Models [DefaultValue(StaticSqlPageSize)] public int SqlPageSize { get; set; } = StaticSqlPageSize; + /// + /// The size to use for nucache Kit batches. Higher value means more content loaded into memory at a time. + /// + [DefaultValue(StaticKitBatchSize)] + public int KitBatchSize { get; set; } = StaticKitBatchSize; + public bool UnPublishedContentCompression { get; set; } = false; } } diff --git a/src/Umbraco.Core/Constants-System.cs b/src/Umbraco.Core/Constants-System.cs index eeea929662..8b19781d39 100644 --- a/src/Umbraco.Core/Constants-System.cs +++ b/src/Umbraco.Core/Constants-System.cs @@ -62,7 +62,7 @@ public const string UmbracoDefaultDatabaseName = "Umbraco"; - public const string UmbracoConnectionName = "umbracoDbDSN"; + public const string UmbracoConnectionName = "umbracoDbDSN";public const string DefaultUmbracoPath = "~/umbraco"; } } } diff --git a/src/Umbraco.Core/Constants-Telemetry.cs b/src/Umbraco.Core/Constants-Telemetry.cs new file mode 100644 index 0000000000..6fc474d9ae --- /dev/null +++ b/src/Umbraco.Core/Constants-Telemetry.cs @@ -0,0 +1,32 @@ +namespace Umbraco.Cms.Core +{ + public static partial class Constants + { + public static class Telemetry + { + + public static string RootCount = "RootCount"; + public static string DomainCount = "DomainCount"; + public static string ExamineIndexCount = "ExamineIndexCount"; + public static string LanguageCount = "LanguageCount"; + public static string MacroCount = "MacroCount"; + public static string MediaCount = "MediaCount"; + public static string MemberCount = "MemberCount"; + public static string TemplateCount = "TemplateCount"; + public static string ContentCount = "ContentCount"; + public static string DocumentTypeCount = "DocumentTypeCount"; + public static string Properties = "Properties"; + public static string UserCount = "UserCount"; + public static string UserGroupCount = "UserGroupCount"; + public static string ServerOs = "ServerOs"; + public static string ServerFramework = "ServerFramework"; + public static string OsLanguage = "OsLanguage"; + public static string WebServer = "WebServer"; + public static string ModelsBuilderMode = "ModelBuilderMode"; + public static string CustomUmbracoPath = "CustomUmbracoPath"; + public static string AspEnvironment = "AspEnvironment"; + public static string IsDebug = "IsDebug"; + public static string DatabaseProvider = "DatabaseProvider"; + } + } +} diff --git a/src/Umbraco.Core/Dashboards/AnalyticsDashboard.cs b/src/Umbraco.Core/Dashboards/AnalyticsDashboard.cs new file mode 100644 index 0000000000..1be6e045d0 --- /dev/null +++ b/src/Umbraco.Core/Dashboards/AnalyticsDashboard.cs @@ -0,0 +1,15 @@ +using System; + +namespace Umbraco.Cms.Core.Dashboards +{ + public class AnalyticsDashboard : IDashboard + { + public string Alias => "settingsAnalytics"; + + public string[] Sections => new [] { "settings" }; + + public string View => "views/dashboard/settings/analytics.html"; + + public IAccessRule[] AccessRules => Array.Empty(); + } +} diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs index f106536169..b1913037a3 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.Collections.cs @@ -118,7 +118,8 @@ namespace Umbraco.Cms.Core.DependencyInjection .Append() .Append() .Append() - .Append(); + .Append() + .Append(); builder.SearchableTrees()?.Add(() => builder.TypeLoader.GetTypes()); builder.BackOfficeAssets(); } diff --git a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs index 3fefcbef97..85c333e8b5 100644 --- a/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs +++ b/src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs @@ -25,7 +25,7 @@ using Umbraco.Cms.Core.Install; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Mail; -using Umbraco.Cms.Core.Manifest; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Packaging; @@ -42,7 +42,6 @@ using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Core.Telemetry; using Umbraco.Cms.Core.Templates; using Umbraco.Cms.Core.Web; -using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Extensions; namespace Umbraco.Cms.Core.DependencyInjection @@ -200,7 +199,7 @@ namespace Umbraco.Cms.Core.DependencyInjection Services.AddSingleton(); Services.AddUnique(); - Services.AddUnique(); + Services.AddSingleton(); // will be injected in controllers when needed to invoke rest endpoints on Our Services.AddUnique(); diff --git a/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs b/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs index 27f092b6ac..48769bda2c 100644 --- a/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs +++ b/src/Umbraco.Core/Extensions/PublishedContentExtensions.cs @@ -1106,7 +1106,7 @@ namespace Umbraco.Extensions { return content.Parent != null ? content.Parent.Children(variationContextAccessor, culture) - : publishedSnapshot?.Content?.GetAtRoot().WhereIsInvariantOrHasCulture(variationContextAccessor, culture); + : publishedSnapshot?.Content?.GetAtRoot(culture).WhereIsInvariantOrHasCulture(variationContextAccessor, culture); } /// @@ -1122,7 +1122,7 @@ namespace Umbraco.Extensions { return content.Parent != null ? content.Parent.ChildrenOfType(variationContextAccessor, contentTypeAlias, culture) - : publishedSnapshot?.Content?.GetAtRoot().OfTypes(contentTypeAlias).WhereIsInvariantOrHasCulture(variationContextAccessor, culture); + : publishedSnapshot?.Content?.GetAtRoot(culture).OfTypes(contentTypeAlias).WhereIsInvariantOrHasCulture(variationContextAccessor, culture); } /// @@ -1139,7 +1139,7 @@ namespace Umbraco.Extensions { return content.Parent != null ? content.Parent.Children(variationContextAccessor, culture) - : publishedSnapshot?.Content?.GetAtRoot().OfType().WhereIsInvariantOrHasCulture(variationContextAccessor, culture); + : publishedSnapshot?.Content?.GetAtRoot(culture).OfType().WhereIsInvariantOrHasCulture(variationContextAccessor, culture); } #endregion diff --git a/src/Umbraco.Core/Media/EmbedProviders/LottieFiles.cs b/src/Umbraco.Core/Media/EmbedProviders/LottieFiles.cs new file mode 100644 index 0000000000..938d22d37d --- /dev/null +++ b/src/Umbraco.Core/Media/EmbedProviders/LottieFiles.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; +using Umbraco.Cms.Core.Serialization; + +namespace Umbraco.Cms.Core.Media.EmbedProviders +{ + /// + /// Embed Provider for lottiefiles.com the popular opensource JSON-based animation format platform. + /// + public class LottieFiles : OEmbedProviderBase + { + public LottieFiles(IJsonSerializer jsonSerializer) : base(jsonSerializer) + { + + } + + public override string ApiEndpoint => "https://embed.lottiefiles.com/oembed"; + + public override string[] UrlSchemeRegex => new string[] + { + @"lottiefiles\.com/*" + }; + public override Dictionary RequestParams => new Dictionary(); + + public override string GetMarkup(string url, int maxWidth = 0, int maxHeight = 0) + { + var requestUrl = base.GetEmbedProviderUrl(url, maxWidth, maxHeight); + OEmbedResponse oembed = base.GetJsonResponse(requestUrl); + var html = oembed.GetHtml(); + //LottieFiles doesn't seem to support maxwidth and maxheight via oembed + // this is therefore a hack... with regexes.. is that ok? HtmlAgility etc etc + // otherwise it always defaults to 300... + if (maxWidth > 0 && maxHeight > 0) + { + + html = Regex.Replace(html, "width=\"([0-9]{1,4})\"", "width=\"" + maxWidth + "\""); + html = Regex.Replace(html, "height=\"([0-9]{1,4})\"", "height=\"" + maxHeight + "\""); + + } + else + { + //if set to 0, let's default to 100% as an easter egg + html = Regex.Replace(html, "width=\"([0-9]{1,4})\"", "width=\"100%\""); + html = Regex.Replace(html, "height=\"([0-9]{1,4})\"", "height=\"100%\""); + } + return html; + } + + } +} diff --git a/src/Umbraco.Core/Models/TelemetryLevel.cs b/src/Umbraco.Core/Models/TelemetryLevel.cs new file mode 100644 index 0000000000..26a714b385 --- /dev/null +++ b/src/Umbraco.Core/Models/TelemetryLevel.cs @@ -0,0 +1,12 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Cms.Core.Models +{ + [DataContract] + public enum TelemetryLevel + { + Minimal, + Basic, + Detailed, + } +} diff --git a/src/Umbraco.Core/Models/TelemetryResource.cs b/src/Umbraco.Core/Models/TelemetryResource.cs new file mode 100644 index 0000000000..401e07848f --- /dev/null +++ b/src/Umbraco.Core/Models/TelemetryResource.cs @@ -0,0 +1,11 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Cms.Core.Models +{ + [DataContract] + public class TelemetryResource + { + [DataMember] + public TelemetryLevel TelemetryLevel { get; set; } + } +} diff --git a/src/Umbraco.Core/Models/UsageInformation.cs b/src/Umbraco.Core/Models/UsageInformation.cs new file mode 100644 index 0000000000..e2bedd6f0f --- /dev/null +++ b/src/Umbraco.Core/Models/UsageInformation.cs @@ -0,0 +1,20 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Cms.Core.Models +{ + [DataContract] + public class UsageInformation + { + [DataMember(Name = "name")] + public string Name { get; } + + [DataMember(Name = "data")] + public object Data { get; } + + public UsageInformation(string name, object data) + { + Name = name; + Data = data; + } + } +} diff --git a/src/Umbraco.Core/Models/UserTwoFactorProviderModel.cs b/src/Umbraco.Core/Models/UserTwoFactorProviderModel.cs new file mode 100644 index 0000000000..095d4f50a9 --- /dev/null +++ b/src/Umbraco.Core/Models/UserTwoFactorProviderModel.cs @@ -0,0 +1,20 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Cms.Core.Models +{ + [DataContract] + public class UserTwoFactorProviderModel + { + public UserTwoFactorProviderModel(string providerName, bool isEnabledOnUser) + { + ProviderName = providerName; + IsEnabledOnUser = isEnabledOnUser; + } + + [DataMember(Name = "providerName")] + public string ProviderName { get; } + + [DataMember(Name = "isEnabledOnUser")] + public bool IsEnabledOnUser { get; } + } +} diff --git a/src/Umbraco.Core/Notifications/SendingAllowedChildrenNotification.cs b/src/Umbraco.Core/Notifications/SendingAllowedChildrenNotification.cs new file mode 100644 index 0000000000..07ab3c3626 --- /dev/null +++ b/src/Umbraco.Core/Notifications/SendingAllowedChildrenNotification.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core.Models.ContentEditing; +using Umbraco.Cms.Core.Web; + +namespace Umbraco.Cms.Core.Notifications +{ + public class SendingAllowedChildrenNotification : INotification + { + public IUmbracoContext UmbracoContext { get; } + + public IEnumerable Children { get; set; } + + public SendingAllowedChildrenNotification(IEnumerable children, IUmbracoContext umbracoContext) + { + UmbracoContext = umbracoContext; + Children = children; + } + } +} diff --git a/src/Umbraco.Core/Notifications/UserTwoFactorRequestedNotification.cs b/src/Umbraco.Core/Notifications/UserTwoFactorRequestedNotification.cs new file mode 100644 index 0000000000..ccb07c593c --- /dev/null +++ b/src/Umbraco.Core/Notifications/UserTwoFactorRequestedNotification.cs @@ -0,0 +1,14 @@ +using System; + +namespace Umbraco.Cms.Core.Notifications +{ + public class UserTwoFactorRequestedNotification : INotification + { + public UserTwoFactorRequestedNotification(Guid userKey) + { + UserKey = userKey; + } + + public Guid UserKey { get; } + } +} diff --git a/src/Umbraco.Core/Persistence/Repositories/INodeCountRepository.cs b/src/Umbraco.Core/Persistence/Repositories/INodeCountRepository.cs new file mode 100644 index 0000000000..4ae191fa72 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/INodeCountRepository.cs @@ -0,0 +1,9 @@ +using System; + +namespace Umbraco.Cms.Core.Persistence.Repositories; + +public interface INodeCountRepository +{ + int GetNodeCount(Guid nodeType); + int GetMediaCount(); +} diff --git a/src/Umbraco.Core/Services/IExamineIndexCountService.cs b/src/Umbraco.Core/Services/IExamineIndexCountService.cs new file mode 100644 index 0000000000..05c5f7d554 --- /dev/null +++ b/src/Umbraco.Core/Services/IExamineIndexCountService.cs @@ -0,0 +1,7 @@ +namespace Umbraco.Cms.Core.Services +{ + public interface IExamineIndexCountService + { + public int GetCount(); + } +} diff --git a/src/Umbraco.Core/Services/IMetricsConsentService.cs b/src/Umbraco.Core/Services/IMetricsConsentService.cs new file mode 100644 index 0000000000..e55cfd71d0 --- /dev/null +++ b/src/Umbraco.Core/Services/IMetricsConsentService.cs @@ -0,0 +1,11 @@ +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Services +{ + public interface IMetricsConsentService + { + TelemetryLevel GetConsentLevel(); + + void SetConsentLevel(TelemetryLevel telemetryLevel); + } +} diff --git a/src/Umbraco.Core/Services/INodeCountService.cs b/src/Umbraco.Core/Services/INodeCountService.cs new file mode 100644 index 0000000000..50d91c1512 --- /dev/null +++ b/src/Umbraco.Core/Services/INodeCountService.cs @@ -0,0 +1,10 @@ +using System; + +namespace Umbraco.Cms.Core.Services +{ + public interface INodeCountService + { + int GetNodeCount(Guid nodeType); + int GetMediaCount(); + } +} diff --git a/src/Umbraco.Core/Services/ITwoFactorLoginService.cs b/src/Umbraco.Core/Services/ITwoFactorLoginService.cs index f30e371d6a..30b221742c 100644 --- a/src/Umbraco.Core/Services/ITwoFactorLoginService.cs +++ b/src/Umbraco.Core/Services/ITwoFactorLoginService.cs @@ -58,4 +58,12 @@ namespace Umbraco.Cms.Core.Services /// Task> GetEnabledTwoFactorProviderNamesAsync(Guid userOrMemberKey); } + + [Obsolete("This will be merged into ITwoFactorLoginService in Umbraco 11")] + public interface ITwoFactorLoginService2 : ITwoFactorLoginService + { + Task DisableWithCodeAsync(string providerName, Guid userOrMemberKey, string code); + + Task ValidateAndSaveAsync(string providerName, Guid userKey, string secret, string code); + } } diff --git a/src/Umbraco.Core/Services/IUsageInformationService.cs b/src/Umbraco.Core/Services/IUsageInformationService.cs new file mode 100644 index 0000000000..fbc988d6b4 --- /dev/null +++ b/src/Umbraco.Core/Services/IUsageInformationService.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Services +{ + public interface IUsageInformationService + { + IEnumerable GetDetailed(); + } +} diff --git a/src/Umbraco.Core/Services/MetricsConsentService.cs b/src/Umbraco.Core/Services/MetricsConsentService.cs new file mode 100644 index 0000000000..3e93a34d8a --- /dev/null +++ b/src/Umbraco.Core/Services/MetricsConsentService.cs @@ -0,0 +1,34 @@ +using System; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Core.Services +{ + public class MetricsConsentService : IMetricsConsentService + { + internal const string Key = "UmbracoAnalyticsLevel"; + + private readonly IKeyValueService _keyValueService; + + public MetricsConsentService(IKeyValueService keyValueService) + { + _keyValueService = keyValueService; + } + + public TelemetryLevel GetConsentLevel() + { + var analyticsLevelString = _keyValueService.GetValue(Key); + + if (analyticsLevelString is null || Enum.TryParse(analyticsLevelString, out TelemetryLevel analyticsLevel) is false) + { + return TelemetryLevel.Basic; + } + + return analyticsLevel; + } + + public void SetConsentLevel(TelemetryLevel telemetryLevel) + { + _keyValueService.SetValue(Key, telemetryLevel.ToString()); + } + } +} diff --git a/src/Umbraco.Core/Services/NodeCountService.cs b/src/Umbraco.Core/Services/NodeCountService.cs new file mode 100644 index 0000000000..7fe77a22a5 --- /dev/null +++ b/src/Umbraco.Core/Services/NodeCountService.cs @@ -0,0 +1,31 @@ +using System; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Infrastructure.Services.Implement +{ + public class NodeCountService : INodeCountService + { + private readonly INodeCountRepository _nodeCountRepository; + private readonly IScopeProvider _scopeProvider; + + public NodeCountService(INodeCountRepository nodeCountRepository, IScopeProvider scopeProvider) + { + _nodeCountRepository = nodeCountRepository; + _scopeProvider = scopeProvider; + } + + public int GetNodeCount(Guid nodeType) + { + using var scope = _scopeProvider.CreateScope(autoComplete: true); + return _nodeCountRepository.GetNodeCount(nodeType); + } + + public int GetMediaCount() + { + using var scope = _scopeProvider.CreateScope(autoComplete: true); + return _nodeCountRepository.GetMediaCount(); + } + } +} diff --git a/src/Umbraco.Core/Services/UserDataService.cs b/src/Umbraco.Core/Services/UserDataService.cs index 490b5af6a8..a3c6bd11b4 100644 --- a/src/Umbraco.Core/Services/UserDataService.cs +++ b/src/Umbraco.Core/Services/UserDataService.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; @@ -9,11 +10,13 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Core.Services { + [Obsolete("Use the IUserDataService interface instead")] public class UserDataService : IUserDataService { private readonly IUmbracoVersion _version; private readonly ILocalizationService _localizationService; + public UserDataService(IUmbracoVersion version, ILocalizationService localizationService) { _version = version; diff --git a/src/Umbraco.Core/Services/UserServiceExtensions.cs b/src/Umbraco.Core/Services/UserServiceExtensions.cs index 087e262101..dd0189fd40 100644 --- a/src/Umbraco.Core/Services/UserServiceExtensions.cs +++ b/src/Umbraco.Core/Services/UserServiceExtensions.cs @@ -84,5 +84,11 @@ namespace Umbraco.Extensions }); } + + public static IUser GetByKey(this IUserService userService, Guid key) + { + int id = BitConverter.ToInt32(key.ToByteArray(), 0); + return userService.GetUserById(id); + } } } diff --git a/src/Umbraco.Core/Telemetry/Models/TelemetryReportData.cs b/src/Umbraco.Core/Telemetry/Models/TelemetryReportData.cs index 71fee55e4a..ea6ff63f91 100644 --- a/src/Umbraco.Core/Telemetry/Models/TelemetryReportData.cs +++ b/src/Umbraco.Core/Telemetry/Models/TelemetryReportData.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Runtime.Serialization; +using Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Core.Telemetry.Models { @@ -30,5 +31,8 @@ namespace Umbraco.Cms.Core.Telemetry.Models /// [DataMember(Name = "packages")] public IEnumerable? Packages { get; set; } + + [DataMember(Name = "detailed")] + public IEnumerable? Detailed { get; set; } } } diff --git a/src/Umbraco.Core/Telemetry/TelemetryService.cs b/src/Umbraco.Core/Telemetry/TelemetryService.cs index ce3696fca4..3a8aa0fcaf 100644 --- a/src/Umbraco.Core/Telemetry/TelemetryService.cs +++ b/src/Umbraco.Core/Telemetry/TelemetryService.cs @@ -5,6 +5,8 @@ using System; using System.Collections.Generic; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Manifest; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Telemetry.Models; using Umbraco.Extensions; @@ -16,6 +18,8 @@ namespace Umbraco.Cms.Core.Telemetry private readonly IManifestParser _manifestParser; private readonly IUmbracoVersion _umbracoVersion; private readonly ISiteIdentifierService _siteIdentifierService; + private readonly IUsageInformationService _usageInformationService; + private readonly IMetricsConsentService _metricsConsentService; /// /// Initializes a new instance of the class. @@ -23,11 +27,15 @@ namespace Umbraco.Cms.Core.Telemetry public TelemetryService( IManifestParser manifestParser, IUmbracoVersion umbracoVersion, - ISiteIdentifierService siteIdentifierService) + ISiteIdentifierService siteIdentifierService, + IUsageInformationService usageInformationService, + IMetricsConsentService metricsConsentService) { _manifestParser = manifestParser; _umbracoVersion = umbracoVersion; _siteIdentifierService = siteIdentifierService; + _usageInformationService = usageInformationService; + _metricsConsentService = metricsConsentService; } /// @@ -42,14 +50,30 @@ namespace Umbraco.Cms.Core.Telemetry telemetryReportData = new TelemetryReportData { Id = telemetryId, - Version = _umbracoVersion.SemanticVersion.ToSemanticStringWithoutBuild(), + Version = GetVersion(), Packages = GetPackageTelemetry(), + Detailed = _usageInformationService.GetDetailed(), }; return true; } + private string GetVersion() + { + if (_metricsConsentService.GetConsentLevel() == TelemetryLevel.Minimal) + { + return null; + } + + return _umbracoVersion.SemanticVersion.ToSemanticStringWithoutBuild(); + } + private IEnumerable GetPackageTelemetry() { + if (_metricsConsentService.GetConsentLevel() == TelemetryLevel.Minimal) + { + return null; + } + List packages = new(); IEnumerable manifests = _manifestParser.GetManifests(); diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 0c8b8c3343..af212673a0 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -55,6 +55,7 @@ using Umbraco.Cms.Infrastructure.Runtime; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Cms.Infrastructure.Search; using Umbraco.Cms.Infrastructure.Serialization; +using Umbraco.Cms.Infrastructure.Services.Implement; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.DependencyInjection @@ -201,6 +202,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Services.AddSingleton(); + builder.Services.AddTransient(); builder.AddInstaller(); // Services required to run background jobs (with out the handler) diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs index 734fcb5661..7ae70440cc 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Repositories.cs @@ -65,6 +65,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Services.AddUnique(); builder.Services.AddUnique(); builder.Services.AddUnique(); + builder.Services.AddUnique(); return builder; } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs index 049e3ad8cb..c0bb0ca42e 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Services.cs @@ -16,6 +16,8 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Implement; using Umbraco.Cms.Infrastructure.Packaging; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Services.Implement; +using Umbraco.Cms.Infrastructure.Telemetry.Providers; using Umbraco.Cms.Infrastructure.Templates; using Umbraco.Extensions; @@ -47,6 +49,9 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection builder.Services.AddSingleton(); builder.Services.AddUnique(); builder.Services.AddUnique(); + builder.Services.AddTransient(); + builder.Services.AddUnique(); + builder.Services.AddTransient(); return builder; } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.TelemetryProviders.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.TelemetryProviders.cs new file mode 100644 index 0000000000..f0ab1ec344 --- /dev/null +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.TelemetryProviders.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Infrastructure.Telemetry.Interfaces; +using Umbraco.Cms.Infrastructure.Telemetry.Providers; + +namespace Umbraco.Cms.Infrastructure.DependencyInjection +{ + public static class UmbracoBuilder_TelemetryProviders + { + public static IUmbracoBuilder AddTelemetryProviders(this IUmbracoBuilder builder) + { + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + return builder; + } + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs index 7c9aabd195..c95bb87cc6 100644 --- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs +++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs @@ -859,7 +859,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install if (_database.Exists(1048)) { - _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1048, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.MemberPicker, DbType = "Ntext" }); + _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1048, EditorAlias = Cms.Core.Constants.PropertyEditors.Aliases.MediaPicker, DbType = "Ntext" }); } if (_database.Exists(1049)) diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NodeCountRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NodeCountRepository.cs new file mode 100644 index 0000000000..fe9bbaa9d7 --- /dev/null +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NodeCountRepository.cs @@ -0,0 +1,44 @@ +using System; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; + +public class NodeCountRepository : INodeCountRepository +{ + private readonly IScopeAccessor _scopeAccessor; + + public NodeCountRepository(IScopeAccessor scopeAccessor) => _scopeAccessor = scopeAccessor; + + /// + + public int GetNodeCount(Guid nodeType) + { + var query = _scopeAccessor.AmbientScope.Database.SqlContext.Sql() + .SelectCount() + .From() + .Where(x => x.NodeObjectType == nodeType && x.Trashed == false); + + return _scopeAccessor.AmbientScope.Database.ExecuteScalar(query); + + } + + public int GetMediaCount() + { + var query = _scopeAccessor.AmbientScope.Database.SqlContext.Sql() + .SelectCount() + .From() + .InnerJoin() + .On(left => left.NodeId, right => right.NodeId) + .InnerJoin() + .On(left => left.ContentTypeId, right => right.NodeId) + .Where(x => x.NodeObjectType == Constants.ObjectTypes.Media) + .Where(x => !x.Trashed) + .Where(x => x.Alias != Constants.Conventions.MediaTypes.Folder); + + return _scopeAccessor.AmbientScope.Database.ExecuteScalar(query); + } +} diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MultiUrlPickerValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MultiUrlPickerValueConverter.cs index 6ba63b5765..6ebcb39148 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MultiUrlPickerValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/MultiUrlPickerValueConverter.cs @@ -41,7 +41,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot; - public override bool? IsValue(object? value, PropertyValueLevel level) => value?.ToString() != "[]"; + public override bool? IsValue(object? value, PropertyValueLevel level) => value is not null && value.ToString() != "[]"; public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) => source?.ToString()!; @@ -51,7 +51,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters { var maxNumber = propertyType.DataType.ConfigurationAs()!.MaxNumber; - if (inter == null) + if (string.IsNullOrWhiteSpace(inter?.ToString())) { return maxNumber == 1 ? null : Enumerable.Empty(); } diff --git a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs index a9c324ebca..83fca576dd 100644 --- a/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/BackOfficeUserStore.cs @@ -35,6 +35,7 @@ namespace Umbraco.Cms.Core.Security private readonly GlobalSettings _globalSettings; private readonly IUmbracoMapper _mapper; private readonly AppCaches _appCaches; + private readonly ITwoFactorLoginService _twoFactorLoginService; /// /// Initializes a new instance of the class. @@ -48,7 +49,8 @@ namespace Umbraco.Cms.Core.Security IOptionsSnapshot globalSettings, IUmbracoMapper mapper, BackOfficeErrorDescriber describer, - AppCaches appCaches) + AppCaches appCaches, + ITwoFactorLoginService twoFactorLoginService) : base(describer) { _scopeProvider = scopeProvider; @@ -58,10 +60,71 @@ namespace Umbraco.Cms.Core.Security _globalSettings = globalSettings.Value; _mapper = mapper; _appCaches = appCaches; + _twoFactorLoginService = twoFactorLoginService; _userService = userService; _externalLoginService = externalLoginService; } + [Obsolete("Use non obsolete ctor")] + public BackOfficeUserStore( + IScopeProvider scopeProvider, + IUserService userService, + IEntityService entityService, + IExternalLoginWithKeyService externalLoginService, + IOptions globalSettings, + IUmbracoMapper mapper, + BackOfficeErrorDescriber describer, + AppCaches appCaches) + : this( + scopeProvider, + userService, + entityService, + externalLoginService, + StaticServiceProvider.Instance.GetRequiredService>(), + mapper, + describer, + appCaches, + StaticServiceProvider.Instance.GetRequiredService()) + { + + } + + [Obsolete("Use non obsolete ctor")] + public BackOfficeUserStore( + IScopeProvider scopeProvider, + IUserService userService, + IEntityService entityService, + IExternalLoginService externalLoginService, + IOptions globalSettings, + IUmbracoMapper mapper, + BackOfficeErrorDescriber describer, + AppCaches appCaches) + : this( + scopeProvider, + userService, + entityService, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService>(), + mapper, + describer, + appCaches, + StaticServiceProvider.Instance.GetRequiredService()) + { + + } + + /// + public override async Task GetTwoFactorEnabledAsync(BackOfficeIdentityUser user, + CancellationToken cancellationToken = default(CancellationToken)) + { + if (!int.TryParse(user.Id, NumberStyles.Integer, CultureInfo.InvariantCulture, out var intUserId)) + { + return await base.GetTwoFactorEnabledAsync(user, cancellationToken); + } + + return await _twoFactorLoginService.IsTwoFactorEnabledAsync(user.Key); + } + /// public override Task CreateAsync(BackOfficeIdentityUser user, CancellationToken cancellationToken = default) { diff --git a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs index e8d93d9221..c87249ba84 100644 --- a/src/Umbraco.Infrastructure/Security/MemberUserStore.cs +++ b/src/Umbraco.Infrastructure/Security/MemberUserStore.cs @@ -607,7 +607,10 @@ namespace Umbraco.Cms.Core.Security || (member.LastLoginDate != default && identityUser.LastLoginDateUtc.HasValue == false) || (identityUser.LastLoginDateUtc.HasValue && member.LastLoginDate.ToUniversalTime() != identityUser.LastLoginDateUtc.Value)) { - changeType = MemberDataChangeType.LoginOnly; + // If the LastLoginDate is default on the member we have to do a full save. + // This is because the umbraco property data for the member doesn't exist yet in this case + // meaning we can't just update that property data, but have to do a full save to create it + changeType = member.LastLoginDate == default ? MemberDataChangeType.FullSave : MemberDataChangeType.LoginOnly; // if the LastLoginDate is being set to MinValue, don't convert it ToLocalTime DateTime dt = identityUser.LastLoginDateUtc == DateTime.MinValue ? DateTime.MinValue : identityUser.LastLoginDateUtc?.ToLocalTime() ?? DateTime.MinValue; diff --git a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs index 7df608e521..4fcf444eea 100644 --- a/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs +++ b/src/Umbraco.Infrastructure/Security/UmbracoUserManager.cs @@ -49,10 +49,10 @@ namespace Umbraco.Cms.Core.Security public override bool SupportsQueryableUsers => false; // It would be nice to support this but we don't need to currently and that would require IQueryable support for our user service/repository /// - /// Developers will need to override this to support custom 2 factor auth + /// Both users and members supports 2FA /// /// - public override bool SupportsUserTwoFactor => false; + public override bool SupportsUserTwoFactor => true; /// public override bool SupportsUserPhoneNumber => false; // We haven't needed to support this yet, though might be necessary for 2FA diff --git a/src/Umbraco.Infrastructure/Services/Implement/ExamineIndexCountService.cs b/src/Umbraco.Infrastructure/Services/Implement/ExamineIndexCountService.cs new file mode 100644 index 0000000000..eed1a4f5c2 --- /dev/null +++ b/src/Umbraco.Infrastructure/Services/Implement/ExamineIndexCountService.cs @@ -0,0 +1,21 @@ +using System.Linq; +using Examine; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Infrastructure.Services.Implement +{ + public class ExamineIndexCountService : IExamineIndexCountService + { + private readonly IExamineManager _examineManager; + + public ExamineIndexCountService(IExamineManager examineManager) + { + _examineManager = examineManager; + } + + public int GetCount() + { + return _examineManager.Indexes.Count(); + } + } +} diff --git a/src/Umbraco.Infrastructure/Services/Implement/TwoFactorLoginService.cs b/src/Umbraco.Infrastructure/Services/Implement/TwoFactorLoginService.cs index da5190e27e..52bd370301 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/TwoFactorLoginService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/TwoFactorLoginService.cs @@ -3,22 +3,26 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Persistence.Repositories; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Web.Common.DependencyInjection; namespace Umbraco.Cms.Core.Services { /// - public class TwoFactorLoginService : ITwoFactorLoginService + public class TwoFactorLoginService : ITwoFactorLoginService2 { private readonly ITwoFactorLoginRepository _twoFactorLoginRepository; private readonly IScopeProvider _scopeProvider; private readonly IOptions _identityOptions; private readonly IOptions _backOfficeIdentityOptions; private readonly IDictionary _twoFactorSetupGenerators; + private readonly ILogger _logger; /// /// Initializes a new instance of the class. @@ -28,16 +32,34 @@ namespace Umbraco.Cms.Core.Services IScopeProvider scopeProvider, IEnumerable twoFactorSetupGenerators, IOptions identityOptions, - IOptions backOfficeIdentityOptions - ) + IOptions backOfficeIdentityOptions, + ILogger logger) { _twoFactorLoginRepository = twoFactorLoginRepository; _scopeProvider = scopeProvider; _identityOptions = identityOptions; _backOfficeIdentityOptions = backOfficeIdentityOptions; + _logger = logger; _twoFactorSetupGenerators = twoFactorSetupGenerators.ToDictionary(x =>x.ProviderName); } + [Obsolete("Use ctor with all params - This will be removed in v11")] + public TwoFactorLoginService( + ITwoFactorLoginRepository twoFactorLoginRepository, + IScopeProvider scopeProvider, + IEnumerable twoFactorSetupGenerators, + IOptions identityOptions, + IOptions backOfficeIdentityOptions) + : this(twoFactorLoginRepository, + scopeProvider, + twoFactorSetupGenerators, + identityOptions, + backOfficeIdentityOptions, + StaticServiceProvider.Instance.GetRequiredService>()) + { + + } + /// public async Task DeleteUserLoginsAsync(Guid userOrMemberKey) { @@ -51,6 +73,56 @@ namespace Umbraco.Cms.Core.Services return await GetEnabledProviderNamesAsync(userOrMemberKey); } + public async Task DisableWithCodeAsync(string providerName, Guid userOrMemberKey, string code) + { + var secret = await GetSecretForUserAndProviderAsync(userOrMemberKey, providerName); + + if (!_twoFactorSetupGenerators.TryGetValue(providerName, out ITwoFactorProvider generator)) + { + throw new InvalidOperationException($"No ITwoFactorSetupGenerator found for provider: {providerName}"); + } + + var isValid = generator.ValidateTwoFactorPIN(secret, code); + + if (!isValid) + { + return false; + } + + return await DisableAsync(userOrMemberKey, providerName); + } + + public async Task ValidateAndSaveAsync(string providerName, Guid userOrMemberKey, string secret, string code) + { + + try + { + var isValid = ValidateTwoFactorSetup(providerName, secret, code); + if (isValid == false) + { + return false; + } + + var twoFactorLogin = new TwoFactorLogin() + { + Confirmed = true, + Secret = secret, + UserOrMemberKey = userOrMemberKey, + ProviderName = providerName + }; + + await SaveAsync(twoFactorLogin); + + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Could not log in with the provided one-time-password"); + } + + return false; + } + private async Task> GetEnabledProviderNamesAsync(Guid userOrMemberKey) { using IScope scope = _scopeProvider.CreateScope(autoComplete: true); diff --git a/src/Umbraco.Infrastructure/Telemetry/Interfaces/IDetailedTelemetryProvider.cs b/src/Umbraco.Infrastructure/Telemetry/Interfaces/IDetailedTelemetryProvider.cs new file mode 100644 index 0000000000..0936dc14a2 --- /dev/null +++ b/src/Umbraco.Infrastructure/Telemetry/Interfaces/IDetailedTelemetryProvider.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core.Models; + +namespace Umbraco.Cms.Infrastructure.Telemetry.Interfaces +{ + internal interface IDetailedTelemetryProvider + { + IEnumerable GetInformation(); + } +} diff --git a/src/Umbraco.Infrastructure/Telemetry/Providers/ContentTelemetryProvider.cs b/src/Umbraco.Infrastructure/Telemetry/Providers/ContentTelemetryProvider.cs new file mode 100644 index 0000000000..23971aec99 --- /dev/null +++ b/src/Umbraco.Infrastructure/Telemetry/Providers/ContentTelemetryProvider.cs @@ -0,0 +1,23 @@ +using System.Collections.Generic; +using System.Linq; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Telemetry.Interfaces; + +namespace Umbraco.Cms.Infrastructure.Telemetry.Providers +{ + public class ContentTelemetryProvider : IDetailedTelemetryProvider + { + private readonly IContentService _contentService; + + public ContentTelemetryProvider(IContentService contentService) => _contentService = contentService; + + public IEnumerable GetInformation() + { + var rootNodes = _contentService.GetRootContent(); + int nodes = rootNodes.Count(); + yield return new UsageInformation(Constants.Telemetry.RootCount, nodes); + } + } +} diff --git a/src/Umbraco.Infrastructure/Telemetry/Providers/DomainTelemetryProvider.cs b/src/Umbraco.Infrastructure/Telemetry/Providers/DomainTelemetryProvider.cs new file mode 100644 index 0000000000..0fc845b490 --- /dev/null +++ b/src/Umbraco.Infrastructure/Telemetry/Providers/DomainTelemetryProvider.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.Linq; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Telemetry.Interfaces; + +namespace Umbraco.Cms.Infrastructure.Telemetry.Providers +{ + public class DomainTelemetryProvider : IDetailedTelemetryProvider + { + private readonly IDomainService _domainService; + + public DomainTelemetryProvider(IDomainService domainService) => _domainService = domainService; + + public IEnumerable GetInformation() + { + var domains = _domainService.GetAll(true).Count(); + yield return new UsageInformation(Constants.Telemetry.DomainCount, domains); + } + } +} diff --git a/src/Umbraco.Infrastructure/Telemetry/Providers/ExamineTelemetryProvider.cs b/src/Umbraco.Infrastructure/Telemetry/Providers/ExamineTelemetryProvider.cs new file mode 100644 index 0000000000..fd64b7dce1 --- /dev/null +++ b/src/Umbraco.Infrastructure/Telemetry/Providers/ExamineTelemetryProvider.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Telemetry.Interfaces; + +namespace Umbraco.Cms.Infrastructure.Telemetry.Providers +{ + public class ExamineTelemetryProvider : IDetailedTelemetryProvider + { + private readonly IExamineIndexCountService _examineIndexCountService; + + public ExamineTelemetryProvider(IExamineIndexCountService examineIndexCountService) => _examineIndexCountService = examineIndexCountService; + + public IEnumerable GetInformation() + { + var indexes = _examineIndexCountService.GetCount(); + yield return new UsageInformation(Constants.Telemetry.ExamineIndexCount, indexes); + } + } +} diff --git a/src/Umbraco.Infrastructure/Telemetry/Providers/LanguagesTelemetryProvider.cs b/src/Umbraco.Infrastructure/Telemetry/Providers/LanguagesTelemetryProvider.cs new file mode 100644 index 0000000000..b3b18e3488 --- /dev/null +++ b/src/Umbraco.Infrastructure/Telemetry/Providers/LanguagesTelemetryProvider.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Linq; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Telemetry.Interfaces; + +namespace Umbraco.Cms.Infrastructure.Telemetry.Providers +{ + public class LanguagesTelemetryProvider : IDetailedTelemetryProvider + { + private readonly ILocalizationService _localizationService; + + public LanguagesTelemetryProvider(ILocalizationService localizationService) + { + _localizationService = localizationService; + } + + public IEnumerable GetInformation() + { + int languages = _localizationService.GetAllLanguages().Count(); + yield return new UsageInformation(Constants.Telemetry.LanguageCount, languages); + } + } +} diff --git a/src/Umbraco.Infrastructure/Telemetry/Providers/MacroTelemetryProvider.cs b/src/Umbraco.Infrastructure/Telemetry/Providers/MacroTelemetryProvider.cs new file mode 100644 index 0000000000..ee96acd1e7 --- /dev/null +++ b/src/Umbraco.Infrastructure/Telemetry/Providers/MacroTelemetryProvider.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; +using System.Linq; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Telemetry.Interfaces; + +namespace Umbraco.Cms.Infrastructure.Telemetry.Providers +{ + public class MacroTelemetryProvider : IDetailedTelemetryProvider + { + private readonly IMacroService _macroService; + + public MacroTelemetryProvider(IMacroService macroService) + { + _macroService = macroService; + } + + public IEnumerable GetInformation() + { + var macros = _macroService.GetAll().Count(); + yield return new UsageInformation(Constants.Telemetry.MacroCount, macros); + } + } +} diff --git a/src/Umbraco.Infrastructure/Telemetry/Providers/MediaTelemetryProvider.cs b/src/Umbraco.Infrastructure/Telemetry/Providers/MediaTelemetryProvider.cs new file mode 100644 index 0000000000..9e690ce461 --- /dev/null +++ b/src/Umbraco.Infrastructure/Telemetry/Providers/MediaTelemetryProvider.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Telemetry.Interfaces; + +namespace Umbraco.Cms.Infrastructure.Telemetry.Providers +{ + public class MediaTelemetryProvider : IDetailedTelemetryProvider + { + private readonly INodeCountService _nodeCountService; + + public MediaTelemetryProvider(INodeCountService nodeCountService) => _nodeCountService = nodeCountService; + + public IEnumerable GetInformation() + { + yield return new UsageInformation(Constants.Telemetry.MediaCount, _nodeCountService.GetMediaCount()); + } + } +} diff --git a/src/Umbraco.Infrastructure/Telemetry/Providers/NodeCountTelemetryProvider.cs b/src/Umbraco.Infrastructure/Telemetry/Providers/NodeCountTelemetryProvider.cs new file mode 100644 index 0000000000..8e27c39eed --- /dev/null +++ b/src/Umbraco.Infrastructure/Telemetry/Providers/NodeCountTelemetryProvider.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Telemetry.Interfaces; + +namespace Umbraco.Cms.Infrastructure.Telemetry.Providers +{ + /// + public class NodeCountTelemetryProvider : IDetailedTelemetryProvider + { + private readonly INodeCountService _nodeCountService; + + public NodeCountTelemetryProvider(INodeCountService nodeCountService) => _nodeCountService = nodeCountService; + + public IEnumerable GetInformation() + { + yield return new UsageInformation(Constants.Telemetry.MemberCount, _nodeCountService.GetNodeCount(Constants.ObjectTypes.Member)); + yield return new UsageInformation(Constants.Telemetry.TemplateCount, _nodeCountService.GetNodeCount(Constants.ObjectTypes.Template)); + yield return new UsageInformation(Constants.Telemetry.ContentCount, _nodeCountService.GetNodeCount(Constants.ObjectTypes.Document)); + yield return new UsageInformation(Constants.Telemetry.DocumentTypeCount, _nodeCountService.GetNodeCount(Constants.ObjectTypes.DocumentType)); + } + } +} diff --git a/src/Umbraco.Infrastructure/Telemetry/Providers/PropertyEditorTelemetryProvider.cs b/src/Umbraco.Infrastructure/Telemetry/Providers/PropertyEditorTelemetryProvider.cs new file mode 100644 index 0000000000..b78ede7851 --- /dev/null +++ b/src/Umbraco.Infrastructure/Telemetry/Providers/PropertyEditorTelemetryProvider.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Linq; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Telemetry.Interfaces; + +namespace Umbraco.Cms.Infrastructure.Telemetry.Providers +{ + public class PropertyEditorTelemetryProvider : IDetailedTelemetryProvider + { + private readonly IContentTypeService _contentTypeService; + + public PropertyEditorTelemetryProvider(IContentTypeService contentTypeService) => _contentTypeService = contentTypeService; + + public IEnumerable GetInformation() + { + var contentTypes = _contentTypeService.GetAll(); + var propertyTypes = new HashSet(); + foreach (IContentType contentType in contentTypes) + { + propertyTypes.UnionWith(contentType.PropertyTypes.Select(x => x.PropertyEditorAlias)); + } + + yield return new UsageInformation(Constants.Telemetry.Properties, propertyTypes); + } + } +} diff --git a/src/Umbraco.Infrastructure/Telemetry/Providers/SystemInformationTelemetryProvider.cs b/src/Umbraco.Infrastructure/Telemetry/Providers/SystemInformationTelemetryProvider.cs new file mode 100644 index 0000000000..4d01a41cd9 --- /dev/null +++ b/src/Umbraco.Infrastructure/Telemetry/Providers/SystemInformationTelemetryProvider.cs @@ -0,0 +1,100 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Telemetry.Interfaces; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Telemetry.Providers; + +internal class SystemInformationTelemetryProvider : IDetailedTelemetryProvider, IUserDataService +{ + private readonly GlobalSettings _globalSettings; + private readonly IHostEnvironment _hostEnvironment; + private readonly HostingSettings _hostingSettings; + private readonly ILocalizationService _localizationService; + private readonly ModelsBuilderSettings _modelsBuilderSettings; + private readonly IUmbracoDatabaseFactory _umbracoDatabaseFactory; + private readonly IUmbracoVersion _version; + + public SystemInformationTelemetryProvider( + IUmbracoVersion version, + ILocalizationService localizationService, + IOptionsMonitor modelsBuilderSettings, + IOptionsMonitor hostingSettings, + IOptionsMonitor globalSettings, + IHostEnvironment hostEnvironment, + IUmbracoDatabaseFactory umbracoDatabaseFactory) + { + _version = version; + _localizationService = localizationService; + _hostEnvironment = hostEnvironment; + _umbracoDatabaseFactory = umbracoDatabaseFactory; + + _globalSettings = globalSettings.CurrentValue; + _hostingSettings = hostingSettings.CurrentValue; + _modelsBuilderSettings = modelsBuilderSettings.CurrentValue; + } + + private string CurrentWebServer => IsRunningInProcessIIS() ? "IIS" : "Kestrel"; + + private string ServerFramework => RuntimeInformation.FrameworkDescription; + + private string ModelsBuilderMode => _modelsBuilderSettings.ModelsMode.ToString(); + + private string CurrentCulture => Thread.CurrentThread.CurrentCulture.ToString(); + + private bool IsDebug => _hostingSettings.Debug; + + private bool UmbracoPathCustomized => _globalSettings.UmbracoPath != Constants.System.DefaultUmbracoPath; + + private string AspEnvironment => _hostEnvironment.EnvironmentName; + + private string ServerOs => RuntimeInformation.OSDescription; + + private string DatabaseProvider => _umbracoDatabaseFactory.CreateDatabase().DatabaseType.GetProviderName(); + + public IEnumerable GetInformation() => + new UsageInformation[] + { + new(Constants.Telemetry.ServerOs, ServerOs), new(Constants.Telemetry.ServerFramework, ServerFramework), + new(Constants.Telemetry.OsLanguage, CurrentCulture), + new(Constants.Telemetry.WebServer, CurrentWebServer), + new(Constants.Telemetry.ModelsBuilderMode, ModelsBuilderMode), + new(Constants.Telemetry.CustomUmbracoPath, UmbracoPathCustomized), + new(Constants.Telemetry.AspEnvironment, AspEnvironment), new(Constants.Telemetry.IsDebug, IsDebug), + new(Constants.Telemetry.DatabaseProvider, DatabaseProvider) + }; + + public IEnumerable GetUserData() => + new UserData[] + { + new("Server OS", ServerOs), new("Server Framework", ServerFramework), + new("Default Language", _localizationService.GetDefaultLanguageIsoCode()), + new("Umbraco Version", _version.SemanticVersion.ToSemanticStringWithoutBuild()), + new("Current Culture", CurrentCulture), + new("Current UI Culture", Thread.CurrentThread.CurrentUICulture.ToString()), + new("Current Webserver", CurrentWebServer), new("Models Builder Mode", ModelsBuilderMode), + new("Debug Mode", IsDebug.ToString()), new("Database Provider", DatabaseProvider) + }; + + private bool IsRunningInProcessIIS() + { + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + return false; + } + + var processName = Path.GetFileNameWithoutExtension(Process.GetCurrentProcess().ProcessName); + return processName.Contains("w3wp") || processName.Contains("iisexpress"); + } +} diff --git a/src/Umbraco.Infrastructure/Telemetry/Providers/UserTelemetryProvider.cs b/src/Umbraco.Infrastructure/Telemetry/Providers/UserTelemetryProvider.cs new file mode 100644 index 0000000000..66f697daef --- /dev/null +++ b/src/Umbraco.Infrastructure/Telemetry/Providers/UserTelemetryProvider.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using System.Linq; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Telemetry.Interfaces; + +namespace Umbraco.Cms.Infrastructure.Telemetry.Providers +{ + public class UserTelemetryProvider : IDetailedTelemetryProvider + { + private readonly IUserService _userService; + + public UserTelemetryProvider(IUserService userService) + { + _userService = userService; + } + + public IEnumerable GetInformation() + { + _userService.GetAll(1, 1, out long total); + int userGroups = _userService.GetAllUserGroups().Count(); + + yield return new UsageInformation(Constants.Telemetry.UserCount, total); + yield return new UsageInformation(Constants.Telemetry.UserGroupCount, userGroups); + } + } +} diff --git a/src/Umbraco.Infrastructure/Telemetry/Services/UsageInformationService.cs b/src/Umbraco.Infrastructure/Telemetry/Services/UsageInformationService.cs new file mode 100644 index 0000000000..486a071af7 --- /dev/null +++ b/src/Umbraco.Infrastructure/Telemetry/Services/UsageInformationService.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Infrastructure.Telemetry.Interfaces; + +namespace Umbraco.Cms.Core.Services +{ + internal class UsageInformationService : IUsageInformationService + { + private readonly IMetricsConsentService _metricsConsentService; + private readonly IEnumerable _providers; + + public UsageInformationService( + IMetricsConsentService metricsConsentService, + IEnumerable providers) + { + _metricsConsentService = metricsConsentService; + _providers = providers; + } + + public IEnumerable GetDetailed() + { + if (_metricsConsentService.GetConsentLevel() != TelemetryLevel.Detailed) + { + return null; + } + + var detailedUsageInformation = new List(); + foreach (IDetailedTelemetryProvider provider in _providers) + { + detailedUsageInformation.AddRange(provider.GetInformation()); + } + + return detailedUsageInformation; + } + } +} diff --git a/src/Umbraco.PublishedCache.NuCache/ContentStore.cs b/src/Umbraco.PublishedCache.NuCache/ContentStore.cs index f5492cf0ab..2411b72a75 100644 --- a/src/Umbraco.PublishedCache.NuCache/ContentStore.cs +++ b/src/Umbraco.PublishedCache.NuCache/ContentStore.cs @@ -344,8 +344,8 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache var node = link.Value; if (node == null) continue; var contentTypeId = node.ContentType?.Id; - if (contentTypeId is null || index.TryGetValue(contentTypeId.Value, out var contentType) == false) continue; - SetValueLocked(_contentNodes, node.Id, new ContentNode(node, _publishedModelFactory, contentType)); + if (contentTypeId is null || index.TryGetValue(contentTypeId, out var contentType) == false) continue; + SetValueLocked(_contentNodes, node.Id, new ContentNode(node, _publishedModelFactory, contentType)); } } @@ -450,10 +450,18 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache refreshedIdsA.Contains(x.ContentTypeId) && BuildKit(x, out _))) { - // replacing the node: must preserve the parents + // replacing the node: must preserve the relations var node = GetHead(_contentNodes, kit.Node.Id)?.Value; if (node != null) + { + // Preserve children kit.Node.FirstChildContentId = node.FirstChildContentId; + kit.Node.LastChildContentId = node.LastChildContentId; + + // Also preserve siblings + kit.Node.NextSiblingContentId = node.NextSiblingContentId; + kit.Node.PreviousSiblingContentId = node.PreviousSiblingContentId; + } SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); @@ -514,7 +522,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache _contentNodes.TryGetValue(id, out var link); if (link?.Value == null) continue; - var node = new ContentNode(link.Value, _publishedModelFactory, contentType); + var node = new ContentNode(link.Value, _publishedModelFactory, contentType); SetValueLocked(_contentNodes, id, node); if (_localDb != null) RegisterChange(id, node.ToKit()); } @@ -631,12 +639,12 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache // moving? var moving = existing != null && existing.ParentContentId != kit.Node.ParentContentId; - // manage children - if (existing != null) - { - kit.Node.FirstChildContentId = existing.FirstChildContentId; - kit.Node.LastChildContentId = existing.LastChildContentId; - } + // manage children + if (existing != null) + { + kit.Node.FirstChildContentId = existing.FirstChildContentId; + kit.Node.LastChildContentId = existing.LastChildContentId; + } // set SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); @@ -698,7 +706,36 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache /// /// Thrown if this method is not called within a write lock /// + [Obsolete("Use the overload that takes a 'kitGroupSize' parameter instead")] public bool SetAllFastSortedLocked(IEnumerable kits, bool fromDb) + { + return SetAllFastSortedLocked(kits, 1, fromDb); + } + + /// + /// Builds all kits on startup using a fast forward only cursor + /// + /// + /// All kits sorted by Level + Parent Id + Sort order + /// + /// + /// True if the data is coming from the database (not the local cache db) + /// + /// + /// + /// This requires that the collection is sorted by Level + ParentId + Sort Order. + /// This should be used only on a site startup as the first generations. + /// This CANNOT be used after startup since it bypasses all checks for Generations. + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// + /// Thrown if this method is not called within a write lock + /// + public bool SetAllFastSortedLocked(IEnumerable kits, int kitGroupSize, bool fromDb) { EnsureLocked(); @@ -716,54 +753,67 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache ContentNode? previousNode = null; ContentNode? parent = null; - foreach (var kit in kits) + // By using InGroupsOf() here we are forcing the database query result extraction to retrieve items in batches, + // reducing the possibility of a database timeout (ThreadAbortException) on large datasets. + // This in turn reduces the possibility that the NuCache file will remain locked, because an exception + // here results in the calling method to not release the lock. + + // However the larger the batck size, the more content loaded into memory. So by default, this is set to 1 and can be increased by setting + // the configuration setting Umbraco:CMS:NuCache:KitPageSize to a higher value. + + // If we are not loading from the database, then we can ignore this restriction. + + foreach (var kitGroup in kits.InGroupsOf(!fromDb || kitGroupSize < 1 ? 1 : kitGroupSize)) { - if (!BuildKit(kit, out var parentLink)) + foreach (var kit in kitGroup) { - ok = false; - continue; // skip that one + if (!BuildKit(kit, out var parentLink)) + { + ok = false; + continue; // skip that one + } + + var thisNode = kit.Node; + + if (parent == null) + { + // first parent + parent = parentLink.Value; + parent!.FirstChildContentId = thisNode.Id; // this node is the first node + } + else if (parent.Id != parentLink.Value!.Id) + { + // new parent + parent = parentLink.Value; + parent.FirstChildContentId = thisNode.Id; // this node is the first node + previousNode = null; // there is no previous sibling + } + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Set {thisNodeId} with parent {thisNodeParentContentId}", thisNode.Id, thisNode.ParentContentId); + } + + SetValueLocked(_contentNodes, thisNode.Id, thisNode); + + // if we are initializing from the database source ensure the local db is updated + if (fromDb && _localDb != null) RegisterChange(thisNode.Id, kit); + + // this node is always the last child + parent.LastChildContentId = thisNode.Id; + + // wire previous node as previous sibling + if (previousNode != null) + { + previousNode.NextSiblingContentId = thisNode.Id; + thisNode.PreviousSiblingContentId = previousNode.Id; + } + + // this node becomes the previous node + previousNode = thisNode; + + _contentKeyToIdMap[kit.Node.Uid] = kit.Node.Id; } - - var thisNode = kit.Node; - - if (parent == null) - { - // first parent - parent = parentLink.Value; - parent!.FirstChildContentId = thisNode.Id; // this node is the first node - } - else if (parent.Id != parentLink.Value!.Id) - { - // new parent - parent = parentLink.Value; - parent.FirstChildContentId = thisNode.Id; // this node is the first node - previousNode = null; // there is no previous sibling - } - - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Set {thisNodeId} with parent {thisNodeParentContentId}", thisNode.Id, thisNode.ParentContentId); - } - - SetValueLocked(_contentNodes, thisNode.Id, thisNode); - - // if we are initializing from the database source ensure the local db is updated - if (fromDb && _localDb != null) RegisterChange(thisNode.Id, kit); - - // this node is always the last child - parent.LastChildContentId = thisNode.Id; - - // wire previous node as previous sibling - if (previousNode != null) - { - previousNode.NextSiblingContentId = thisNode.Id; - thisNode.PreviousSiblingContentId = previousNode.Id; - } - - // this node becomes the previous node - previousNode = thisNode; - - _contentKeyToIdMap[kit.Node.Uid] = kit.Node.Id; } return ok; @@ -781,7 +831,27 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache /// /// Thrown if this method is not called within a write lock /// + [Obsolete("Use the overload that takes the 'kitGroupSize' and 'fromDb' parameters instead")] public bool SetAllLocked(IEnumerable kits) + { + return SetAllLocked(kits, 1, false); + } + + /// + /// Set all data for a collection of + /// + /// + /// + /// True if the data is coming from the database (not the local cache db) + /// + /// + /// This methods MUST be called from within a write lock, normally wrapped within GetScopedWriteLock + /// otherwise an exception will occur. + /// + /// + /// Thrown if this method is not called within a write lock + /// + public bool SetAllLocked(IEnumerable kits, int kitGroupSize, bool fromDb) { EnsureLocked(); @@ -794,25 +864,37 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache //ClearLocked(_contentTypesById); //ClearLocked(_contentTypesByAlias); - foreach (var kit in kits) + // By using InGroupsOf() here we are forcing the database query result extraction to retrieve items in batches, + // reducing the possibility of a database timeout (ThreadAbortException) on large datasets. + // This in turn reduces the possibility that the NuCache file will remain locked, because an exception + // here results in the calling method to not release the lock. + + // However the larger the batck size, the more content loaded into memory. So by default, this is set to 1 and can be increased by setting + // the configuration setting Umbraco:CMS:NuCache:KitPageSize to a higher value. + + // If we are not loading from the database, then we can ignore this restriction. + foreach (var kitGroup in kits.InGroupsOf(!fromDb || kitGroupSize < 1 ? 1 : kitGroupSize)) { - if (!BuildKit(kit, out var parent)) + foreach (var kit in kitGroup) { - ok = false; - continue; // skip that one + if (!BuildKit(kit, out var parent)) + { + ok = false; + continue; // skip that one + } + + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.LogDebug("Set {kitNodeId} with parent {kitNodeParentContentId}", kit.Node.Id, kit.Node.ParentContentId); + } + + SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); + + if (_localDb != null) RegisterChange(kit.Node.Id, kit); + AddTreeNodeLocked(kit.Node, parent); + + _contentKeyToIdMap[kit.Node.Uid] = kit.Node.Id; } - - if (_logger.IsEnabled(LogLevel.Debug)) - { - _logger.LogDebug("Set {kitNodeId} with parent {kitNodeParentContentId}", kit.Node.Id, kit.Node.ParentContentId); - } - - SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); - - if (_localDb != null) RegisterChange(kit.Node.Id, kit); - AddTreeNodeLocked(kit.Node, parent); - - _contentKeyToIdMap[kit.Node.Uid] = kit.Node.Id; } return ok; diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs index dee5231031..2221cdd7d2 100644 --- a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs @@ -337,8 +337,19 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache _localContentDb?.Clear(); // IMPORTANT GetAllContentSources sorts kits by level + parentId + sortOrder - var kits = _publishedContentService.GetAllContentSources(); - return onStartup ? _contentStore.SetAllFastSortedLocked(kits, true) : _contentStore.SetAllLocked(kits); + + try + { + var kits = _publishedContentService.GetAllContentSources(); + return onStartup ? _contentStore.SetAllFastSortedLocked(kits, _config.KitBatchSize, true) : _contentStore.SetAllLocked(kits, _config.KitBatchSize, true); + } + catch (ThreadAbortException tae) + { + // Caught a ThreadAbortException, most likely from a database timeout. + // If we don't catch it here, the whole local cache can remain locked causing widespread panic (see above comment). + _logger.LogWarning(tae, tae.Message); + } + return false; } } @@ -385,8 +396,20 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache _logger.LogDebug("Loading media from database..."); // IMPORTANT GetAllMediaSources sorts kits by level + parentId + sortOrder - var kits = _publishedContentService.GetAllMediaSources(); - return onStartup ? _mediaStore.SetAllFastSortedLocked(kits, true) : _mediaStore.SetAllLocked(kits); + + try + { + + var kits = _publishedContentService.GetAllMediaSources(); + return onStartup ? _mediaStore.SetAllFastSortedLocked(kits, _config.KitBatchSize, true) : _mediaStore.SetAllLocked(kits, _config.KitBatchSize, true); + } + catch (ThreadAbortException tae) + { + // Caught a ThreadAbortException, most likely from a database timeout. + // If we don't catch it here, the whole local cache can remain locked causing widespread panic (see above comment). + _logger.LogWarning(tae, tae.Message); + } + return false; } } @@ -434,7 +457,7 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache return false; } - return onStartup ? store.SetAllFastSortedLocked(kits, false) : store.SetAllLocked(kits); + return onStartup ? store.SetAllFastSortedLocked(kits, _config.KitBatchSize, false) : store.SetAllLocked(kits, _config.KitBatchSize, false); } private void LockAndLoadDomains() diff --git a/src/Umbraco.Web.BackOffice/Controllers/AnalyticsController.cs b/src/Umbraco.Web.BackOffice/Controllers/AnalyticsController.cs new file mode 100644 index 0000000000..e1aac7319b --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Controllers/AnalyticsController.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Web.BackOffice.Controllers +{ + public class AnalyticsController : UmbracoAuthorizedJsonController + { + private readonly IMetricsConsentService _metricsConsentService; + public AnalyticsController(IMetricsConsentService metricsConsentService) + { + _metricsConsentService = metricsConsentService; + } + + public TelemetryLevel GetConsentLevel() + { + return _metricsConsentService.GetConsentLevel(); + } + + [HttpPost] + public IActionResult SetConsentLevel([FromBody]TelemetryResource telemetryResource) + { + if (!ModelState.IsValid) + { + return BadRequest(); + } + + _metricsConsentService.SetConsentLevel(telemetryResource.TelemetryLevel); + return Ok(); + } + + public IEnumerable GetAllLevels() => new[] { TelemetryLevel.Minimal, TelemetryLevel.Basic, TelemetryLevel.Detailed }; + } +} diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs index 74c14ddd62..fcf76f4e9c 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs @@ -74,11 +74,12 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers private readonly IBackOfficeExternalLoginProviders _externalAuthenticationOptions; private readonly IBackOfficeTwoFactorOptions _backOfficeTwoFactorOptions; private readonly IHttpContextAccessor _httpContextAccessor; + private readonly ITwoFactorLoginService _twoFactorLoginService; private readonly WebRoutingSettings _webRoutingSettings; // TODO: We need to review all _userManager.Raise calls since many/most should be on the usermanager or signinmanager, very few should be here [ActivatorUtilitiesConstructor] - public AuthenticationController( + public AuthenticationController( IBackOfficeSecurityAccessor backofficeSecurityAccessor, IBackOfficeUserManager backOfficeUserManager, IBackOfficeSignInManager signInManager, @@ -97,7 +98,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers IBackOfficeExternalLoginProviders externalAuthenticationOptions, IBackOfficeTwoFactorOptions backOfficeTwoFactorOptions, IHttpContextAccessor httpContextAccessor, - IOptions webRoutingSettings) + IOptions webRoutingSettings, + ITwoFactorLoginService twoFactorLoginService) { _backofficeSecurityAccessor = backofficeSecurityAccessor; _userManager = backOfficeUserManager; @@ -118,6 +120,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers _backOfficeTwoFactorOptions = backOfficeTwoFactorOptions; _httpContextAccessor = httpContextAccessor; _webRoutingSettings = webRoutingSettings.Value; + _twoFactorLoginService = twoFactorLoginService; } /// @@ -433,7 +436,8 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return NotFound(); } - var userFactors = await _userManager.GetValidTwoFactorProvidersAsync(user); + var userFactors = await _twoFactorLoginService.GetEnabledTwoFactorProviderNamesAsync(user.Key); + return new ObjectResult(userFactors); } diff --git a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs index 1e2e986413..f84934975b 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/BackOfficeServerVariables.cs @@ -244,6 +244,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers "authenticationApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( controller => controller.PostLogin(new LoginModel())) }, + { + "twoFactorLoginApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( + controller => controller.SetupInfo(null)) + }, { "currentUserApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( controller => controller.PostChangePassword(new ChangingPasswordModel())) @@ -390,7 +394,11 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers { "trackedReferencesApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( controller => controller.GetPagedReferences(0, 1, 1, false)) - } + }, + { + "analyticsApiBaseUrl", _linkGenerator.GetUmbracoApiServiceBaseUrl( + controller => controller.GetConsentLevel()) + }, } }, { diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs index fa7ed04ab6..1dbe63fed5 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeController.cs @@ -22,6 +22,7 @@ using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Infrastructure.Packaging; +using Umbraco.Cms.Web.BackOffice.Filters; using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; @@ -454,6 +455,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// [Authorize(Policy = AuthorizationPolicies.TreeAccessDocumentsOrDocumentTypes)] + [OutgoingEditorModelEvent] public IEnumerable GetAllowedChildren(int contentId) { if (contentId == Constants.System.RecycleBinContent) diff --git a/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs b/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs index 1d31ffc78e..9f733dffe4 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/DictionaryController.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Net.Http; +using System.Net.Mime; +using System.Text; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; @@ -191,6 +193,39 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return _umbracoMapper.Map(dictionary); } + /// + /// Changes the structure for dictionary items + /// + /// + /// + public IActionResult PostMove(MoveOrCopy move) + { + var dictionaryItem = _localizationService.GetDictionaryItemById(move.Id); + if (dictionaryItem == null) + return ValidationProblem(_localizedTextService.Localize("dictionary", "itemDoesNotExists")); + + var parent = _localizationService.GetDictionaryItemById(move.ParentId); + if (parent == null) + { + if (move.ParentId == Constants.System.Root) + dictionaryItem.ParentId = null; + else + return ValidationProblem(_localizedTextService.Localize("dictionary", "parentDoesNotExists")); + } + else + { + dictionaryItem.ParentId = parent.Key; + if (dictionaryItem.Key == parent.ParentId) + return ValidationProblem(_localizedTextService.Localize("moveOrCopy", "notAllowedByPath")); + } + + _localizationService.Save(dictionaryItem); + + var model = _umbracoMapper.Map(dictionaryItem); + + return Content(model.Path, MediaTypeNames.Text.Plain, Encoding.UTF8); + } + /// /// Saves a dictionary item /// diff --git a/src/Umbraco.Web.BackOffice/Controllers/MediaTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/MediaTypeController.cs index a02a7ed3c4..c5c5a38b4b 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MediaTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MediaTypeController.cs @@ -12,6 +12,7 @@ using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; +using Umbraco.Cms.Web.BackOffice.Filters; using Umbraco.Cms.Web.Common.ActionsResults; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; @@ -339,6 +340,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers /// /// [Authorize(Policy = AuthorizationPolicies.TreeAccessMediaOrMediaTypes)] + [OutgoingEditorModelEvent] public IEnumerable GetAllowedChildren(int contentId) { if (contentId == Constants.System.RecycleBinContent) diff --git a/src/Umbraco.Web.BackOffice/Controllers/MemberTypeController.cs b/src/Umbraco.Web.BackOffice/Controllers/MemberTypeController.cs index 6ac985e2d5..a73fb442ca 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/MemberTypeController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/MemberTypeController.cs @@ -252,5 +252,19 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers return display; } + + /// + /// Copy the member type + /// + /// + /// + [Authorize(Policy = AuthorizationPolicies.TreeAccessMemberTypes)] + public IActionResult PostCopy(MoveOrCopy copy) + { + return PerformCopy( + copy, + i => _memberTypeService.Get(i), + (type, i) => _memberTypeService.Copy(type, i)); + } } } diff --git a/src/Umbraco.Web.BackOffice/Controllers/TwoFactorLoginController.cs b/src/Umbraco.Web.BackOffice/Controllers/TwoFactorLoginController.cs new file mode 100644 index 0000000000..41c25e3b47 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Controllers/TwoFactorLoginController.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Serialization; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Web.BackOffice.Security; +using Umbraco.Cms.Web.Common.Authorization; + +namespace Umbraco.Cms.Web.BackOffice.Controllers +{ + public class TwoFactorLoginController : UmbracoAuthorizedJsonController + { + private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor; + private readonly ILogger _logger; + private readonly ITwoFactorLoginService2 _twoFactorLoginService; + private readonly IBackOfficeSignInManager _backOfficeSignInManager; + private readonly IBackOfficeUserManager _backOfficeUserManager; + private readonly IOptionsSnapshot _twoFactorLoginViewOptions; + + public TwoFactorLoginController( + IBackOfficeSecurityAccessor backOfficeSecurityAccessor, + ILogger logger, + ITwoFactorLoginService twoFactorLoginService, + IBackOfficeSignInManager backOfficeSignInManager, + IBackOfficeUserManager backOfficeUserManager, + IOptionsSnapshot twoFactorLoginViewOptions) + { + _backOfficeSecurityAccessor = backOfficeSecurityAccessor; + _logger = logger; + + if (twoFactorLoginService is not ITwoFactorLoginService2 twoFactorLoginService2) + { + throw new ArgumentException("twoFactorLoginService needs to implement ITwoFactorLoginService2 until the interfaces are merged", nameof(twoFactorLoginService)); + } + _twoFactorLoginService = twoFactorLoginService2; + _backOfficeSignInManager = backOfficeSignInManager; + _backOfficeUserManager = backOfficeUserManager; + _twoFactorLoginViewOptions = twoFactorLoginViewOptions; + } + + /// + /// Used to retrieve the 2FA providers for code submission + /// + /// + [HttpGet] + [AllowAnonymous] + public async Task>> GetEnabled2FAProvidersForCurrentUser() + { + var user = await _backOfficeSignInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + _logger.LogWarning("No verified user found, returning 404"); + return NotFound(); + } + + var userFactors = await _backOfficeUserManager.GetValidTwoFactorProvidersAsync(user); + return new ObjectResult(userFactors); + } + + + [HttpGet] + public async Task>> Get2FAProvidersForUser(int userId) + { + var user = await _backOfficeUserManager.FindByIdAsync(userId.ToString()); + + var enabledProviderNameHashSet = new HashSet(await _twoFactorLoginService.GetEnabledTwoFactorProviderNamesAsync(user.Key)); + + var providerNames = await _backOfficeUserManager.GetValidTwoFactorProvidersAsync(user); + + return providerNames.Select(providerName => + new UserTwoFactorProviderModel(providerName, enabledProviderNameHashSet.Contains(providerName))).ToArray(); + } + + [HttpGet] + public async Task> SetupInfo(string providerName) + { + var user = _backOfficeSecurityAccessor?.BackOfficeSecurity.CurrentUser; + + var setupInfo = await _twoFactorLoginService.GetSetupInfoAsync(user.Key, providerName); + + return setupInfo; + } + + + [HttpPost] + public async Task> ValidateAndSave(string providerName, string secret, string code) + { + var user = _backOfficeSecurityAccessor?.BackOfficeSecurity.CurrentUser; + + return await _twoFactorLoginService.ValidateAndSaveAsync(providerName, user.Key, secret, code); + } + + [HttpPost] + [Authorize(Policy = AuthorizationPolicies.SectionAccessUsers)] + public async Task> Disable(string providerName, Guid userKey) + { + return await _twoFactorLoginService.DisableAsync(userKey, providerName); + } + + [HttpPost] + public async Task> DisableWithCode(string providerName, string code) + { + Guid key = _backOfficeSecurityAccessor.BackOfficeSecurity.CurrentUser.Key; + + return await _twoFactorLoginService.DisableWithCodeAsync(providerName, key, code); + } + + [HttpGet] + public ActionResult ViewPathForProviderName(string providerName) + { + var options = _twoFactorLoginViewOptions.Get(providerName); + return options.SetupViewPath; + } + } +} diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs index 24ecee08ef..0e878aef8b 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeAuth.cs @@ -45,6 +45,11 @@ namespace Umbraco.Extensions { o.Cookie.Name = Constants.Security.BackOfficeTwoFactorAuthenticationType; o.ExpireTimeSpan = TimeSpan.FromMinutes(5); + }) + .AddCookie(Constants.Security.BackOfficeTwoFactorRememberMeAuthenticationType, o => + { + o.Cookie.Name = Constants.Security.BackOfficeTwoFactorRememberMeAuthenticationType; + o.ExpireTimeSpan = TimeSpan.FromMinutes(5); }); builder.Services.ConfigureOptions(); diff --git a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs index 65ebcf3bd2..1545d25eab 100644 --- a/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs +++ b/src/Umbraco.Web.BackOffice/DependencyInjection/UmbracoBuilder.BackOfficeIdentity.cs @@ -42,7 +42,8 @@ namespace Umbraco.Extensions factory.GetRequiredService>(), factory.GetRequiredService(), factory.GetRequiredService(), - factory.GetRequiredService() + factory.GetRequiredService(), + factory.GetRequiredService() )) .AddUserManager() .AddSignInManager() @@ -64,7 +65,7 @@ namespace Umbraco.Extensions services.TryAddScoped(); services.TryAddSingleton(); - services.TryAddSingleton(); + services.TryAddSingleton(); return new BackOfficeIdentityBuilder(services); } diff --git a/src/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventAttribute.cs index 3c9cde13fd..64ac33b1aa 100644 --- a/src/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventAttribute.cs @@ -84,6 +84,12 @@ namespace Umbraco.Cms.Web.BackOffice.Filters case IEnumerable> dashboards: _eventAggregator.Publish(new SendingDashboardsNotification(dashboards, umbracoContext)); break; + case IEnumerable allowedChildren: + // Changing the Enumerable will generate a new instance, so we need to update the context result with the new content + var notification = new SendingAllowedChildrenNotification(allowedChildren, umbracoContext); + _eventAggregator.Publish(notification); + context.Result = new ObjectResult(notification.Children); + break; } } } diff --git a/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs b/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs index 463de51ff7..e7ff4351ed 100644 --- a/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs +++ b/src/Umbraco.Web.BackOffice/Security/BackOfficeSignInManager.cs @@ -6,10 +6,14 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Notifications; using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Web.Common.DependencyInjection; using Umbraco.Cms.Web.Common.Security; using Umbraco.Extensions; @@ -24,6 +28,7 @@ namespace Umbraco.Cms.Web.BackOffice.Security { private readonly BackOfficeUserManager _userManager; private readonly IBackOfficeExternalLoginProviders _externalLogins; + private readonly IEventAggregator _eventAggregator; private readonly GlobalSettings _globalSettings; protected override string AuthenticationType => Constants.Security.BackOfficeAuthenticationType; @@ -43,14 +48,32 @@ namespace Umbraco.Cms.Web.BackOffice.Security IOptions globalSettings, ILogger> logger, IAuthenticationSchemeProvider schemes, - IUserConfirmation confirmation) + IUserConfirmation confirmation, + IEventAggregator eventAggregator) : base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes, confirmation) { _userManager = userManager; _externalLogins = externalLogins; + _eventAggregator = eventAggregator; _globalSettings = globalSettings.Value; } + [Obsolete("Use ctor with all params")] + public BackOfficeSignInManager( + BackOfficeUserManager userManager, + IHttpContextAccessor contextAccessor, + IBackOfficeExternalLoginProviders externalLogins, + IUserClaimsPrincipalFactory claimsFactory, + IOptions optionsAccessor, + IOptions globalSettings, + ILogger> logger, + IAuthenticationSchemeProvider schemes, + IUserConfirmation confirmation) + : this(userManager, contextAccessor, externalLogins, claimsFactory, optionsAccessor, globalSettings, logger, schemes, confirmation, StaticServiceProvider.Instance.GetRequiredService()) + { + + } + /// /// Custom ExternalLoginSignInAsync overload for handling external sign in with auto-linking /// @@ -284,6 +307,31 @@ namespace Umbraco.Cms.Web.BackOffice.Security } } + protected override async Task SignInOrTwoFactorAsync(BackOfficeIdentityUser user, bool isPersistent, + string loginProvider = null, bool bypassTwoFactor = false) + { + var result = await base.SignInOrTwoFactorAsync(user, isPersistent, loginProvider, bypassTwoFactor); + + if (result.RequiresTwoFactor) + { + NotifyRequiresTwoFactor(user); + } + + return result; + } + + protected void NotifyRequiresTwoFactor(BackOfficeIdentityUser user) => Notify(user, + (currentUser) => new UserTwoFactorRequestedNotification(currentUser.Key) + ); + + private T Notify(BackOfficeIdentityUser currentUser, Func createNotification) where T : INotification + { + + var notification = createNotification(currentUser); + _eventAggregator.Publish(notification); + return notification; + } + private void LogFailedExternalLogin(ExternalLoginInfo loginInfo, BackOfficeIdentityUser user) => Logger.LogWarning("The AutoLinkOptions of the external authentication provider '{LoginProvider}' have refused the login based on the OnExternalLogin method. Affected user id: '{UserId}'", loginInfo.LoginProvider, user.Id); } diff --git a/src/Umbraco.Web.BackOffice/Security/DefaultBackOfficeTwoFactorOptions.cs b/src/Umbraco.Web.BackOffice/Security/DefaultBackOfficeTwoFactorOptions.cs new file mode 100644 index 0000000000..5ef0f53695 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Security/DefaultBackOfficeTwoFactorOptions.cs @@ -0,0 +1,10 @@ +using System; + +namespace Umbraco.Cms.Web.BackOffice.Security +{ + public class DefaultBackOfficeTwoFactorOptions : IBackOfficeTwoFactorOptions + { + public string GetTwoFactorView(string username) => "views\\common\\login-2fa.html"; + } + +} diff --git a/src/Umbraco.Web.BackOffice/Security/NoopBackOfficeTwoFactorOptions.cs b/src/Umbraco.Web.BackOffice/Security/NoopBackOfficeTwoFactorOptions.cs index 9e33b05757..09eefb4559 100644 --- a/src/Umbraco.Web.BackOffice/Security/NoopBackOfficeTwoFactorOptions.cs +++ b/src/Umbraco.Web.BackOffice/Security/NoopBackOfficeTwoFactorOptions.cs @@ -1,5 +1,8 @@ -namespace Umbraco.Cms.Web.BackOffice.Security +using System; + +namespace Umbraco.Cms.Web.BackOffice.Security { + [Obsolete("Not used anymore")] public class NoopBackOfficeTwoFactorOptions : IBackOfficeTwoFactorOptions { public string? GetTwoFactorView(string username) => null; diff --git a/src/Umbraco.Web.BackOffice/Security/TwoFactorLoginViewOptions.cs b/src/Umbraco.Web.BackOffice/Security/TwoFactorLoginViewOptions.cs new file mode 100644 index 0000000000..da06dd9e67 --- /dev/null +++ b/src/Umbraco.Web.BackOffice/Security/TwoFactorLoginViewOptions.cs @@ -0,0 +1,13 @@ +namespace Umbraco.Cms.Web.BackOffice.Security +{ + /// + /// Options used as named options for 2fa providers + /// + public class TwoFactorLoginViewOptions + { + /// + /// Gets or sets the path of the view to show when setting up this 2fa provider + /// + public string SetupViewPath { get; set; } + } +} diff --git a/src/Umbraco.Web.BackOffice/Trees/DictionaryTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/DictionaryTreeController.cs index 82c5c36297..67a6c892bc 100644 --- a/src/Umbraco.Web.BackOffice/Trees/DictionaryTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/DictionaryTreeController.cs @@ -129,7 +129,11 @@ namespace Umbraco.Cms.Web.BackOffice.Trees menu.Items.Add(LocalizedTextService, opensDialog: true); if (id != Constants.System.RootString) + { menu.Items.Add(LocalizedTextService, true, opensDialog: true); + menu.Items.Add(LocalizedTextService, true, opensDialog: true); + } + menu.Items.Add(new RefreshNode(LocalizedTextService, true)); diff --git a/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs index 77918a452f..bd5c22b147 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MemberGroupTreeController.cs @@ -1,14 +1,17 @@ +using System; using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Trees; using Umbraco.Cms.Web.Common.Attributes; using Umbraco.Cms.Web.Common.Authorization; +using Umbraco.Cms.Web.Common.DependencyInjection; using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Cms.Web.BackOffice.Trees @@ -21,14 +24,35 @@ namespace Umbraco.Cms.Web.BackOffice.Trees { private readonly IMemberGroupService _memberGroupService; + [ + ActivatorUtilitiesConstructor] + public MemberGroupTreeController( + ILocalizedTextService localizedTextService, + UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, + IMenuItemCollectionFactory menuItemCollectionFactory, + IMemberGroupService memberGroupService, + IEventAggregator eventAggregator, + IMemberTypeService memberTypeService) + : base(localizedTextService, umbracoApiControllerTypeCollection, menuItemCollectionFactory, eventAggregator, memberTypeService) + => _memberGroupService = memberGroupService; + + [Obsolete("Use ctor with all params")] public MemberGroupTreeController( ILocalizedTextService localizedTextService, UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, IMenuItemCollectionFactory menuItemCollectionFactory, IMemberGroupService memberGroupService, IEventAggregator eventAggregator) - : base(localizedTextService, umbracoApiControllerTypeCollection, menuItemCollectionFactory, eventAggregator) - => _memberGroupService = memberGroupService; + : this(localizedTextService, + umbracoApiControllerTypeCollection, + menuItemCollectionFactory, + memberGroupService, + eventAggregator, + StaticServiceProvider.Instance.GetRequiredService()) + { + + } + protected override IEnumerable GetTreeNodesFromService(string id, FormCollection queryStrings) => _memberGroupService.GetAll() diff --git a/src/Umbraco.Web.BackOffice/Trees/MemberTypeAndGroupTreeControllerBase.cs b/src/Umbraco.Web.BackOffice/Trees/MemberTypeAndGroupTreeControllerBase.cs index c8000da0ae..975ac23bbc 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MemberTypeAndGroupTreeControllerBase.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MemberTypeAndGroupTreeControllerBase.cs @@ -1,6 +1,8 @@ +using System; using System.Collections.Generic; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Actions; using Umbraco.Cms.Core.Events; @@ -8,6 +10,8 @@ using Umbraco.Cms.Core.Models.Trees; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Trees; using Umbraco.Cms.Web.Common.Attributes; +using Umbraco.Cms.Web.Common.DependencyInjection; +using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; namespace Umbraco.Cms.Web.BackOffice.Trees @@ -16,21 +20,46 @@ namespace Umbraco.Cms.Web.BackOffice.Trees [CoreTree] public abstract class MemberTypeAndGroupTreeControllerBase : TreeController { + private readonly IMemberTypeService _memberTypeService; + public IMenuItemCollectionFactory MenuItemCollectionFactory { get; } protected MemberTypeAndGroupTreeControllerBase( ILocalizedTextService localizedTextService, UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, IMenuItemCollectionFactory menuItemCollectionFactory, - IEventAggregator eventAggregator) + IEventAggregator eventAggregator, + IMemberTypeService memberTypeService) : base(localizedTextService, umbracoApiControllerTypeCollection, eventAggregator) { MenuItemCollectionFactory = menuItemCollectionFactory; + + _memberTypeService = memberTypeService; + } + + [Obsolete("Use ctor injecting IMemberTypeService")] + protected MemberTypeAndGroupTreeControllerBase( + ILocalizedTextService localizedTextService, + UmbracoApiControllerTypeCollection umbracoApiControllerTypeCollection, + IMenuItemCollectionFactory menuItemCollectionFactory, + IEventAggregator eventAggregator) + : this( + localizedTextService, + umbracoApiControllerTypeCollection, + menuItemCollectionFactory, + eventAggregator, + StaticServiceProvider.Instance.GetRequiredService()) + { } protected override ActionResult GetTreeNodes(string id, FormCollection queryStrings) { var nodes = new TreeNodeCollection(); + + // if the request is for folders only then just return + if (queryStrings["foldersonly"].ToString().IsNullOrWhiteSpace() == false && queryStrings["foldersonly"].ToString() == "1") + return nodes; + nodes.AddRange(GetTreeNodesFromService(id, queryStrings)); return nodes; } @@ -48,7 +77,13 @@ namespace Umbraco.Cms.Web.BackOffice.Trees } else { - //delete member type/group + var memberType = _memberTypeService.Get(int.Parse(id)); + if (memberType != null) + { + menu.Items.Add(LocalizedTextService, opensDialog: true); + } + + // delete member type/group menu.Items.Add(LocalizedTextService, opensDialog: true); } diff --git a/src/Umbraco.Web.BackOffice/Trees/MemberTypeTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/MemberTypeTreeController.cs index c9e77e7eff..8e74b6ec80 100644 --- a/src/Umbraco.Web.BackOffice/Trees/MemberTypeTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/MemberTypeTreeController.cs @@ -32,13 +32,12 @@ namespace Umbraco.Cms.Web.BackOffice.Trees UmbracoTreeSearcher treeSearcher, IMemberTypeService memberTypeService, IEventAggregator eventAggregator) - : base(localizedTextService, umbracoApiControllerTypeCollection, menuItemCollectionFactory, eventAggregator) + : base(localizedTextService, umbracoApiControllerTypeCollection, menuItemCollectionFactory, eventAggregator, memberTypeService) { _treeSearcher = treeSearcher; _memberTypeService = memberTypeService; } - protected override ActionResult CreateRootNode(FormCollection queryStrings) { var rootResult = base.CreateRootNode(queryStrings); diff --git a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs index fc9780016b..51af2ca22d 100644 --- a/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs +++ b/src/Umbraco.Web.Common/ApplicationBuilder/UmbracoApplicationBuilder.cs @@ -92,8 +92,6 @@ namespace Umbraco.Cms.Web.Common.ApplicationBuilder { UseUmbracoCoreMiddleware(); - AppBuilder.UseStatusCodePages(); - // Important we handle image manipulations before the static files, otherwise the querystring is just ignored. AppBuilder.UseImageSharp(); diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 5e2779e1dd..c583ead527 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -159,6 +159,7 @@ namespace Umbraco.Extensions )); builder.AddCoreInitialServices(); + builder.AddTelemetryProviders(); // aspnet app lifetime mgmt builder.Services.AddUnique(); diff --git a/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs b/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs index 8bdfda4d95..7928d122a7 100644 --- a/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Web.Common/Security/BackOfficeUserManager.cs @@ -74,6 +74,7 @@ namespace Umbraco.Cms.Web.Common.Security return await base.VerifyPasswordAsync(store, user, password); } + /// /// Override to check the user approval value as well as the user lock out date, by default this only checks the user's locked out date /// diff --git a/src/Umbraco.Web.Common/Security/MemberManager.cs b/src/Umbraco.Web.Common/Security/MemberManager.cs index f9f4f3d0bb..3c734ca8a7 100644 --- a/src/Umbraco.Web.Common/Security/MemberManager.cs +++ b/src/Umbraco.Web.Common/Security/MemberManager.cs @@ -45,9 +45,6 @@ namespace Umbraco.Cms.Web.Common.Security _httpContextAccessor = httpContextAccessor; } - /// - public override bool SupportsUserTwoFactor => true; - /// public async Task IsMemberAuthorizedAsync(IEnumerable? allowTypes = null, IEnumerable? allowGroups = null, IEnumerable? allowMembers = null) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbappheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbappheader.directive.js index 01e199c572..dd83f6546b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbappheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbappheader.directive.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function AppHeaderDirective(eventsService, appState, userService, focusService, overlayService, $timeout) { + function AppHeaderDirective(eventsService, appState, userService, focusService, $timeout, editorService) { function link(scope, element) { @@ -72,17 +72,15 @@ }; scope.avatarClick = function () { - - const dialog = { - view: "user", - position: "right", - name: "overlay-user", - close: function () { - overlayService.close(); - } + const userEditor = { + size: "small", + view: "views/common/infiniteeditors/user/user.html", + close: function() { + editorService.close(); + } }; - overlayService.open(dialog); + editorService.open(userEditor); }; scope.logoModal = { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js index 9986b9ce8a..d1c8d1ac85 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umblogin.directive.js @@ -443,12 +443,16 @@ vm.twoFactor.submitCallback = function submitCallback() { vm.onLogin(); } + vm.twoFactor.cancelCallback = function cancelCallback() { + vm.showLogin(); + } vm.twoFactor.view = viewPath; vm.view = "2fa-login"; SetTitle(); } function resetInputValidation() { + vm.loginStates.submitButton = "init"; vm.confirmPassword = ""; vm.password = ""; vm.login = ""; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbrangeslider.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbrangeslider.directive.js index d5c791281c..262f70f62b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbrangeslider.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbrangeslider.directive.js @@ -13,7 +13,7 @@ For extra details about options and events take a look here: https://refreshless
 	
- @@ -229,11 +229,13 @@ For extra details about options and events take a look here: https://refreshless var origins = slider.noUiSlider.getOrigins(); // Move tooltips into the origin element. The default stylesheet handles this. + if(tooltips && tooltips.length !== 0){ tooltips.forEach(function (tooltip, index) { - if (tooltip) { - origins[index].appendChild(tooltip); - } + if (tooltip) { + origins[index].appendChild(tooltip); + } }); + } slider.noUiSlider.on('update', function (values, handle, unencoded, tap, positions) { @@ -283,7 +285,7 @@ For extra details about options and events take a look here: https://refreshless offset = (textIsRtl && !isVertical ? 100 : 0) + (offset / handlesInPool) - lastOffset; // Filter to unique values - var tooltipValues = poolValues[poolIndex].filter((v, i, a) => a.indexOf(v) === i); + var tooltipValues = poolValues[poolIndex].filter((v, i, a) => a.indexOf(v) === i); // Center this tooltip over the affected handles tooltips[handleNumber].innerHTML = tooltipValues.join(separator); diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/umbraco.servervariables.js b/src/Umbraco.Web.UI.Client/src/common/mocks/umbraco.servervariables.js index 868bc4c6d5..7abefedfab 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/umbraco.servervariables.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/umbraco.servervariables.js @@ -20,7 +20,8 @@ Umbraco.Sys.ServerVariables = { "updateCheckApiBaseUrl": "/umbraco/Api/UpdateCheck/", "relationApiBaseUrl": "/umbraco/UmbracoApi/Relation/", "rteApiBaseUrl": "/umbraco/UmbracoApi/RichTextPreValue/", - "iconApiBaseUrl": "/umbraco/UmbracoApi/Icon/" + "iconApiBaseUrl": "/umbraco/UmbracoApi/Icon/", + "analyticsApiBaseUrl": "/umbraco/UmbracoApi/Consent/" }, umbracoSettings: { "umbracoPath": "/umbraco", diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/analytic.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/analytic.resource.js new file mode 100644 index 0000000000..fa3b203df2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/resources/analytic.resource.js @@ -0,0 +1,57 @@ +/** + * @ngdoc service + * @name umbraco.resources.consentResource + * @function + * + * @description + * Used by the health check dashboard to get checks and send requests to fix checks. + */ +(function () { + 'use strict'; + + function analyticResource($http, umbRequestHelper) { + + function getConsentLevel () { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "analyticsApiBaseUrl", + "GetConsentLevel")), + 'Server call failed for getting current consent level'); + } + + function getAllConsentLevels () { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "analyticsApiBaseUrl", + "GetAllLevels")), + 'Server call failed for getting current consent level'); + } + + function saveConsentLevel (value) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "analyticsApiBaseUrl", + "SetConsentLevel"), + { telemetryLevel : value } + ), + 'Server call failed for getting current consent level'); + } + + var resource = { + getConsentLevel: getConsentLevel, + getAllConsentLevels : getAllConsentLevels, + saveConsentLevel : saveConsentLevel + }; + + return resource; + + } + + + angular.module('umbraco.resources').factory('analyticResource', analyticResource); + + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/dictionary.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/dictionary.resource.js index 3bb01fbe92..38a96fbcda 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/dictionary.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/dictionary.resource.js @@ -1,4 +1,4 @@ -/** +/** * @ngdoc service * @name umbraco.resources.dictionaryResource * @description Loads in data for dictionary items @@ -96,6 +96,48 @@ function dictionaryResource($q, $http, $location, umbRequestHelper, umbDataForma "Failed to get item " + id); } + /** + * @ngdoc method + * @name umbraco.resources.dictionaryResource#move + * @methodOf umbraco.resources.dictionaryResource + * + * @description + * Moves a dictionary item underneath a new parentId + * + * ##usage + *
+      * dictionaryResource.move({ parentId: 1244, id: 123 })
+      *    .then(function() {
+      *        alert("node was moved");
+      *    }, function(err){
+      *      alert("node didnt move:" + err.data.Message);
+      *    });
+      * 
+ * @param {Object} args arguments object + * @param {int} args.id the int of the dictionary item to move + * @param {int} args.parentId the int of the parent dictionary item to move to + * @returns {Promise} resourcePromise object. + * + */ + function move (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.id) { + throw "args.id cannot be null"; + } + + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("dictionaryApiBaseUrl", "PostMove"), + { + parentId: args.parentId, + id: args.id + }, { responseType: 'text' })); + } + /** * @ngdoc method * @name umbraco.resources.dictionaryResource#save @@ -151,6 +193,7 @@ function dictionaryResource($q, $http, $location, umbRequestHelper, umbDataForma create: create, getById: getById, save: save, + move: move, getList : getList }; diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/twofactorlogin.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/twofactorlogin.resource.js new file mode 100644 index 0000000000..8ed308812e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/resources/twofactorlogin.resource.js @@ -0,0 +1,136 @@ +/** + * @ngdoc service + * @name umbraco.resources.twoFactorLoginResource + * @function + * + * @description + * Used by the users section to get users 2FA information + */ +(function () { + 'use strict'; + + function twoFactorLoginResource($http, umbRequestHelper) { + + /** + * @ngdoc method + * @name umbraco.resources.twoFactorLoginResource#viewPathForProviderName + * @methodOf umbraco.resources.twoFactorLoginResource + * + * @description + * Gets the view path for the specified two factor provider + * + * ##usage + *
+         * twoFactorLoginResource.viewPathForProviderName(providerName)
+         *    .then(function(viewPath) {
+         *        alert("It's here");
+         *    });
+         * 
+ * + * @returns {Promise} resourcePromise object containing the view path. + * + */ + function viewPathForProviderName(providerName) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "twoFactorLoginApiBaseUrl", + "ViewPathForProviderName", + {providerName : providerName })), + "Failed to retrieve data"); + } + + /** + * @ngdoc method + * @name umbraco.resources.twoFactorLoginResource#get2FAProvidersForUser + * @methodOf umbraco.resources.twoFactorLoginResource + * + * @description + * Gets the 2fa provider names that is available + * + * ##usage + *
+       * twoFactorLoginResource.get2FAProvidersForUser(userKey)
+       *    .then(function(providers) {
+       *        alert("It's here");
+       *    });
+       * 
+ * + * @returns {Promise} resourcePromise object containing the an array of { providerName, isEnabledOnUser} . + * + */ + function get2FAProvidersForUser(userId) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "twoFactorLoginApiBaseUrl", + "get2FAProvidersForUser", + { userId: userId })), + "Failed to retrieve data"); + } + + function setupInfo(providerName) { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "twoFactorLoginApiBaseUrl", + "setupInfo", + { providerName: providerName })), + "Failed to retrieve data"); + } + + function validateAndSave(providerName, secret, code) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "twoFactorLoginApiBaseUrl", + "validateAndSave", + { + providerName: providerName, + secret: secret, + code: code + })), + "Failed to retrieve data"); + } + function disable(providerName, userKey) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "twoFactorLoginApiBaseUrl", + "disable", + { + providerName: providerName, + userKey: userKey + })), + "Failed to retrieve data"); + } + function disableWithCode(providerName, code) { + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "twoFactorLoginApiBaseUrl", + "disableWithCode", + { + providerName: providerName, + code: code + })), + "Failed to retrieve data"); + } + + var resource = { + viewPathForProviderName: viewPathForProviderName, + get2FAProvidersForUser:get2FAProvidersForUser, + setupInfo:setupInfo, + validateAndSave:validateAndSave, + disable: disable, + disableWithCode: disableWithCode + }; + + return resource; + + } + + angular.module('umbraco.resources').factory('twoFactorLoginResource', twoFactorLoginResource); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/localization.service.js b/src/Umbraco.Web.UI.Client/src/common/services/localization.service.js index 805afae5b8..6911651af9 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/localization.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/localization.service.js @@ -200,6 +200,10 @@ angular.module('umbraco.services') * localizationService.localizeMany(["speechBubbles_templateErrorHeader", "speechBubbles_templateErrorText"]).then(function(data){ * var header = data[0]; * var message = data[1]; + * + * + * + * * notificationService.error(header, message); * }); *
diff --git a/src/Umbraco.Web.UI.Client/src/common/services/notifications.service.js b/src/Umbraco.Web.UI.Client/src/common/services/notifications.service.js index ad02520c5a..c9db8cf00e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/notifications.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/notifications.service.js @@ -273,7 +273,7 @@ angular.module('umbraco.services') */ removeAll: function () { angularHelper.safeApply($rootScope, function() { - nArray = []; + nArray.length = 0; }); }, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index 98543a7c68..3273b8a2fb 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -1599,6 +1599,11 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s syncContent(); }); + // When the element is removed from the DOM, we need to terminate + // any active watchers to ensure scopes are disposed and do not leak. + // No need to sync content as that has already happened. + args.editor.on('remove', () => stopWatch()); + args.editor.on('ObjectResized', function (e) { var srcAttr = $(e.target).attr("src"); var path = srcAttr.split("?")[0]; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/twofactor/configuretwofactor.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/twofactor/configuretwofactor.controller.js new file mode 100644 index 0000000000..382d9ef35e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/twofactor/configuretwofactor.controller.js @@ -0,0 +1,79 @@ +//used for the user editor overlay +angular.module("umbraco").controller("Umbraco.Editors.ConfigureTwoFactorController", + function ($scope, + localizationService, + notificationsService, + overlayService, + twoFactorLoginResource, + editorService) { + + + let vm = this; + vm.close = close; + vm.enable = enable; + vm.disable = disable; + vm.code = ""; + vm.buttonState = "init"; + + localizationService.localize("user_configureTwoFactor").then(function (value) { + vm.title = value; + }); + + function onInit() { + vm.code = ""; + + twoFactorLoginResource.get2FAProvidersForUser($scope.model.user.id) + .then(function (providers) { + vm.providers = providers; + }); + } + + function close() { + if ($scope.model.close) { + $scope.model.close(); + } + } + + function enable(providerName) { + twoFactorLoginResource.viewPathForProviderName(providerName) + .then(function (viewPath) { + var providerSettings = { + user: $scope.model.user, + providerName: providerName, + size: "small", + view: viewPath, + close: function () { + notificationsService.removeAll(); + editorService.close(); + onInit(); + } + }; + + editorService.open(providerSettings); + }).catch(onError); + } + + function disable(provider) { + + const disableTwoFactorSettings = { + provider, + user: vm.user, + size: "small", + view: "views/common/infiniteeditors/twofactor/disabletwofactor.html", + close: function () { + editorService.close(); + onInit(); + } + }; + + editorService.open(disableTwoFactorSettings); + } + + function onError(error) { + vm.buttonState = "error"; + overlayService.ysod(error); + } + + //initialize + onInit(); + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/twofactor/configuretwofactor.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/twofactor/configuretwofactor.html new file mode 100644 index 0000000000..15bb70b7f9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/twofactor/configuretwofactor.html @@ -0,0 +1,58 @@ +
+ + + + + + + + + + + + + + + + +
+

+ + +

+ + + + +
+ + +
+
+ + + + +
+ +
+
+
+ + + + + + + + + + + + +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/twofactor/disabletwofactor.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/twofactor/disabletwofactor.controller.js new file mode 100644 index 0000000000..b8e909e633 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/twofactor/disabletwofactor.controller.js @@ -0,0 +1,53 @@ +//used for the user editor overlay +angular.module("umbraco").controller("Umbraco.Editors.DisableTwoFactorController", + function ($scope, + localizationService, + notificationsService, + overlayService, + twoFactorLoginResource) { + + let vm = this; + vm.close = close; + vm.disableWithCode = disableWithCode; + vm.code = ""; + vm.buttonState = "init"; + vm.authForm = {}; + + if (!$scope.model.provider) { + notificationsService.error("No provider specified"); + } + vm.provider = $scope.model.provider; + vm.title = vm.provider.providerName; + + function close() { + if ($scope.model.close) { + $scope.model.close(); + } + } + + function disableWithCode() { + vm.authForm.token.$setValidity("token", true); + vm.buttonState = "busy"; + twoFactorLoginResource.disableWithCode(vm.provider.providerName, vm.code) + .then(onResponse) + .catch(onError); + } + + function onResponse(response) { + if (response) { + vm.buttonState = "success"; + localizationService.localize("user_2faProviderIsDisabledMsg").then(function (value) { + notificationsService.info(value); + }); + close(); + } else { + vm.buttonState = "error"; + vm.authForm.token.$setValidity("token", false); + } + } + + function onError(error) { + vm.buttonState = "error"; + overlayService.ysod(error); + } + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/twofactor/disabletwofactor.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/twofactor/disabletwofactor.html new file mode 100644 index 0000000000..11c2c813fd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/twofactor/disabletwofactor.html @@ -0,0 +1,56 @@ +
+ + +
+ + + + + + + + + + + + + + + + +
+ + Invalid code entered + +
+
+ +
+ +
+ +
+ + + + + + + + + + + + + + + +
+ +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/user/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/user/user.controller.js new file mode 100644 index 0000000000..7f9e546709 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/user/user.controller.js @@ -0,0 +1,219 @@ +angular.module("umbraco") + .controller("Umbraco.Editors.UserController", function ($scope, $location, $timeout, + dashboardResource, userService, historyService, eventsService, + externalLoginInfoService, authResource, + currentUserResource, formHelper, localizationService, editorService, twoFactorLoginResource) { + + let vm = this; + + vm.history = historyService.getCurrent(); + vm.showPasswordFields = false; + vm.changePasswordButtonState = "init"; + vm.hasTwoFactorProviders = false; + + localizationService.localize("general_user").then(function (value) { + vm.title = value; + }); + + // Set flag if any have deny local login, in which case we must disable all password functionality + vm.denyLocalLogin = externalLoginInfoService.hasDenyLocalLogin(); + // Only include login providers that have editable options + vm.externalLoginProviders = externalLoginInfoService.getLoginProvidersWithOptions(); + + vm.externalLinkLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLinkLoginsUrl; + var evts = []; + evts.push(eventsService.on("historyService.add", function (e, args) { + vm.history = args.all; + })); + evts.push(eventsService.on("historyService.remove", function (e, args) { + vm.history = args.all; + })); + evts.push(eventsService.on("historyService.removeAll", function (e, args) { + vm.history = []; + })); + + vm.logout = function () { + + //Add event listener for when there are pending changes on an editor which means our route was not successful + var pendingChangeEvent = eventsService.on("valFormManager.pendingChanges", function (e, args) { + //one time listener, remove the event + pendingChangeEvent(); + vm.close(); + }); + + + //perform the path change, if it is successful then the promise will resolve otherwise it will fail + vm.close(); + $location.path("/logout").search(''); + }; + + vm.gotoHistory = function (link) { + $location.path(link); + vm.close(); + }; + /* + //Manually update the remaining timeout seconds + function updateTimeout() { + $timeout(function () { + if (vm.remainingAuthSeconds > 0) { + vm.remainingAuthSeconds--; + $scope.$digest(); + //recurse + updateTimeout(); + } + + }, 1000, false); // 1 second, do NOT execute a global digest + } + */ + function updateUserInfo() { + //get the user + userService.getCurrentUser().then(function (user) { + vm.user = user; + if (vm.user) { + vm.remainingAuthSeconds = vm.user.remainingAuthSeconds; + vm.canEditProfile = _.indexOf(vm.user.allowedSections, "users") > -1; + //set the timer + //updateTimeout(); + + currentUserResource.getCurrentUserLinkedLogins().then(function (logins) { + + //reset all to be un-linked + vm.externalLoginProviders.forEach(provider => provider.linkedProviderKey = undefined); + + //set the linked logins + for (var login in logins) { + var found = _.find(vm.externalLoginProviders, function (i) { + return i.authType == login; + }); + if (found) { + found.linkedProviderKey = logins[login]; + } + } + }); + + //go get the config for the membership provider and add it to the model + authResource.getPasswordConfig(user.id).then(function (data) { + vm.changePasswordModel.config = data; + //ensure the hasPassword config option is set to true (the user of course has a password already assigned) + //this will ensure the oldPassword is shown so they can change it + // disable reset password functionality beacuse it does not make sense inside the backoffice + vm.changePasswordModel.config.hasPassword = true; + vm.changePasswordModel.config.disableToggle = true; + }); + + twoFactorLoginResource.get2FAProvidersForUser(vm.user.id).then(function (providers) { + vm.hasTwoFactorProviders = providers.length > 0; + }); + + } + }); + + + } + + vm.linkProvider = function (e) { + e.target.submit(); + } + + vm.unlink = function (e, loginProvider, providerKey) { + var result = confirm("Are you sure you want to unlink this account?"); + if (!result) { + e.preventDefault(); + return; + } + + authResource.unlinkLogin(loginProvider, providerKey).then(function (a, b, c) { + updateUserInfo(); + }); + } + + //create the initial model for change password + vm.changePasswordModel = { + config: {}, + value: {} + }; + + updateUserInfo(); + + + //remove all event handlers + $scope.$on('$destroy', function () { + for (var e = 0; e < evts.length; e++) { + evts[e](); + } + + }); + + vm.changePassword = function () { + + if (formHelper.submitForm({ scope: $scope })) { + + vm.changePasswordButtonState = "busy"; + + currentUserResource.changePassword(vm.changePasswordModel.value).then(function (data) { + + //reset old data + clearPasswordFields(); + + formHelper.resetForm({ scope: $scope }); + + vm.changePasswordButtonState = "success"; + $timeout(function () { + vm.togglePasswordFields(); + }, 2000); + + }, function (err) { + formHelper.resetForm({ scope: $scope, hasErrors: true }); + formHelper.handleError(err); + + vm.changePasswordButtonState = "error"; + + }); + + } + + }; + + vm.togglePasswordFields = function () { + clearPasswordFields(); + vm.showPasswordFields = !vm.showPasswordFields; + } + + function clearPasswordFields() { + vm.changePasswordModel.value.oldPassword = ""; + vm.changePasswordModel.value.newPassword = ""; + vm.changePasswordModel.value.confirm = ""; + } + + vm.editUser = function () { + $location + .path('/users/users/user/' + vm.user.id); + vm.close(); + } + + vm.toggleConfigureTwoFactor = function () { + + const configureTwoFactorSettings = { + create: true, + user: vm.user, + isCurrentUser: true,// From this view we are always current user (used by the overlay) + size: "small", + view: "views/common/infiniteeditors/twofactor/configuretwofactor.html", + close: function () { + editorService.close(); + } + }; + + editorService.open(configureTwoFactorSettings); + } + + vm.close = function () { + if ($scope.model.close) { + $scope.model.close(); + } + } + + dashboardResource.getDashboard("user-dialog").then(function (dashboard) { + vm.dashboard = dashboard; + }); + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/user/user.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/user/user.html new file mode 100644 index 0000000000..c67f65a7d2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/user/user.html @@ -0,0 +1,145 @@ +
+ + + + + + +
+ + + + +
+ + + + + + + + +
+
+
+
+ + + + + + + +
+ +
+ +
+
+ + +
+ + + +
+ +
+ +
+ +
+ +
+ + + + + + + + + +
+ +
+ +
+ Change password +
+ +
+ + + + + + + + + + +
+ +
+ +
+
+
{{tab.label}}
+
+
+
+
+
+
+ + + + + + + + + + + + + + + +
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/login-2fa.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/login-2fa.controller.js new file mode 100644 index 0000000000..6c57085c8b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/login-2fa.controller.js @@ -0,0 +1,38 @@ +angular.module("umbraco").controller("Umbraco.Login2faController", + function ($scope, userService, authResource) { + let vm = this; + vm.code = ""; + vm.provider = ""; + vm.providers = []; + vm.stateValidateButton = "init"; + vm.authForm = {}; + + authResource.get2FAProviders() + .then(function (data) { + vm.providers = data; + if (vm.providers.length > 0) { + vm.provider = vm.providers[0]; + } + }); + + vm.validate = function () { + vm.error = ""; + vm.stateValidateButton = "busy"; + vm.authForm.token.$setValidity('token', true); + + authResource.verify2FACode(vm.provider, vm.code) + .then(function (data) { + vm.stateValidateButton = "success"; + userService.setAuthenticationSuccessful(data); + $scope.vm.twoFactor.submitCallback(); + }) + .catch(function () { + vm.stateValidateButton = "error"; + vm.authForm.token.$setValidity('token', false); + }); + }; + + vm.goBack = function () { + $scope.vm.twoFactor.cancelCallback(); + } + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/login-2fa.html b/src/Umbraco.Web.UI.Client/src/views/common/login-2fa.html new file mode 100644 index 0000000000..fd444f180c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/login-2fa.html @@ -0,0 +1,44 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js deleted file mode 100644 index a98eacd702..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.controller.js +++ /dev/null @@ -1,194 +0,0 @@ -angular.module("umbraco") - .controller("Umbraco.Overlays.UserController", function ($scope, $location, $timeout, - dashboardResource, userService, historyService, eventsService, - externalLoginInfo, externalLoginInfoService, authResource, - currentUserResource, formHelper, localizationService) { - - $scope.history = historyService.getCurrent(); - //$scope.version = Umbraco.Sys.ServerVariables.application.version + " assembly: " + Umbraco.Sys.ServerVariables.application.assemblyVersion; - $scope.showPasswordFields = false; - $scope.changePasswordButtonState = "init"; - $scope.model.title = "user.name"; - //$scope.model.subtitle = "Umbraco version" + " " + $scope.version; - /* - if(!$scope.model.title) { - localizationService.localize("general_user").then(function(value){ - $scope.model.title = value; - }); - } - */ - - // Set flag if any have deny local login, in which case we must disable all password functionality - $scope.denyLocalLogin = externalLoginInfoService.hasDenyLocalLogin(); - // Only include login providers that have editable options - $scope.externalLoginProviders = externalLoginInfoService.getLoginProvidersWithOptions(); - - $scope.externalLinkLoginFormAction = Umbraco.Sys.ServerVariables.umbracoUrls.externalLinkLoginsUrl; - var evts = []; - evts.push(eventsService.on("historyService.add", function (e, args) { - $scope.history = args.all; - })); - evts.push(eventsService.on("historyService.remove", function (e, args) { - $scope.history = args.all; - })); - evts.push(eventsService.on("historyService.removeAll", function (e, args) { - $scope.history = []; - })); - - $scope.logout = function () { - - //Add event listener for when there are pending changes on an editor which means our route was not successful - var pendingChangeEvent = eventsService.on("valFormManager.pendingChanges", function (e, args) { - //one time listener, remove the event - pendingChangeEvent(); - $scope.model.close(); - }); - - - //perform the path change, if it is successful then the promise will resolve otherwise it will fail - $scope.model.close(); - $location.path("/logout").search(''); - }; - - $scope.gotoHistory = function (link) { - $location.path(link); - $scope.model.close(); - }; - /* - //Manually update the remaining timeout seconds - function updateTimeout() { - $timeout(function () { - if ($scope.remainingAuthSeconds > 0) { - $scope.remainingAuthSeconds--; - $scope.$digest(); - //recurse - updateTimeout(); - } - - }, 1000, false); // 1 second, do NOT execute a global digest - } - */ - function updateUserInfo() { - //get the user - userService.getCurrentUser().then(function (user) { - $scope.user = user; - if ($scope.user) { - $scope.model.title = user.name; - $scope.remainingAuthSeconds = $scope.user.remainingAuthSeconds; - $scope.canEditProfile = _.indexOf($scope.user.allowedSections, "users") > -1; - //set the timer - //updateTimeout(); - - currentUserResource.getCurrentUserLinkedLogins().then(function(logins) { - - //reset all to be un-linked - $scope.externalLoginProviders.forEach(provider => provider.linkedProviderKey = undefined); - - //set the linked logins - for (var login in logins) { - var found = _.find($scope.externalLoginProviders, function (i) { - return i.authType == login; - }); - if (found) { - found.linkedProviderKey = logins[login]; - } - } - }); - - //go get the config for the membership provider and add it to the model - authResource.getPasswordConfig(user.id).then(function (data) { - $scope.changePasswordModel.config = data; - //ensure the hasPassword config option is set to true (the user of course has a password already assigned) - //this will ensure the oldPassword is shown so they can change it - // disable reset password functionality beacuse it does not make sense inside the backoffice - $scope.changePasswordModel.config.hasPassword = true; - $scope.changePasswordModel.config.disableToggle = true; - }); - - } - }); - } - - $scope.linkProvider = function (e) { - e.target.submit(); - } - - $scope.unlink = function (e, loginProvider, providerKey) { - var result = confirm("Are you sure you want to unlink this account?"); - if (!result) { - e.preventDefault(); - return; - } - - authResource.unlinkLogin(loginProvider, providerKey).then(function (a, b, c) { - updateUserInfo(); - }); - } - - //create the initial model for change password - $scope.changePasswordModel = { - config: {}, - value: {} - }; - - updateUserInfo(); - - //remove all event handlers - $scope.$on('$destroy', function () { - for (var e = 0; e < evts.length; e++) { - evts[e](); - } - - }); - - $scope.changePassword = function() { - - if (formHelper.submitForm({ scope: $scope })) { - - $scope.changePasswordButtonState = "busy"; - - currentUserResource.changePassword($scope.changePasswordModel.value).then(function(data) { - - //reset old data - clearPasswordFields(); - - formHelper.resetForm({ scope: $scope }); - - $scope.changePasswordButtonState = "success"; - $timeout(function() { - $scope.togglePasswordFields(); - }, 2000); - - }, function (err) { - formHelper.resetForm({ scope: $scope, hasErrors: true }); - formHelper.handleError(err); - - $scope.changePasswordButtonState = "error"; - - }); - - } - - }; - - $scope.togglePasswordFields = function() { - clearPasswordFields(); - $scope.showPasswordFields = !$scope.showPasswordFields; - } - - function clearPasswordFields() { - $scope.changePasswordModel.value.oldPassword = ""; - $scope.changePasswordModel.value.newPassword = ""; - $scope.changePasswordModel.value.confirm = ""; - } - - $scope.editUser = function() { - $location - .path('/users/users/user/' + $scope.user.id); - $scope.model.close(); - } - - dashboardResource.getDashboard("user-dialog").then(function (dashboard) { - $scope.dashboard = dashboard; - }); - }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html deleted file mode 100644 index 24acef995e..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html +++ /dev/null @@ -1,143 +0,0 @@ -
-
- -
- Your profile -
- - - - - - - - - - -
- -
- -
- External login providers -
- -
- -
- -
-
- - -
- - -
- -
- -
- - -
-
- Your recent history -
- -
- -
- -
- Change password -
- -
- - - - - - - - - - -
- -
- -
-
-
{{tab.label}}
-
-
-
-
-
-
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html index a6df878997..f50722d421 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-property-file-upload.html @@ -1,20 +1,17 @@ -
+
+ ng-required="vm.required && !vm.files.length" />
+ ng-hide="vm.files.length > 0">

Click to upload

- + +
@@ -25,38 +22,31 @@ source="file.fileSrc" name="file.fileName" client-side="file.isClientSide" - client-side-data="file.fileData" - > + client-side-data="file.fileData"> +
diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/analytics.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/analytics.controller.js new file mode 100644 index 0000000000..094941b63a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/analytics.controller.js @@ -0,0 +1,98 @@ +(function () { + "use strict"; + + function AnalyticsController($q, analyticResource, localizationService, notificationsService) { + + let sliderRef = null; + + var vm = this; + vm.getConsentLevel = getConsentLevel; + vm.getAllConsentLevels = getAllConsentLevels; + vm.saveConsentLevel = saveConsentLevel; + vm.sliderChange = sliderChange; + vm.setup = setup; + vm.loading = true; + vm.consentLevel = ''; + vm.consentLevels = []; + vm.val = 1; + vm.sliderOptions = + { + "start": 1, + "step": 1, + "tooltips": [false], + "range": { + "min": 1, + "max": 3 + }, + pips: { + mode: 'values', + density: 50, + values: [1, 2, 3], + format: { + to: function (value) { + return vm.consentLevels[value - 1]; + }, + from: function (value) { + return Number(value); + } + } + } + }; + $q.all( + [getConsentLevel(), + getAllConsentLevels() + ]).then( () => { + vm.startPos = calculateStartPositionForSlider(); + vm.sliderVal = vm.consentLevels[vm.startPos - 1]; + vm.sliderOptions.start = vm.startPos; + vm.val = vm.startPos; + vm.sliderOptions.pips.format = { + to: function (value) { + return vm.consentLevels[value - 1]; + }, + from: function (value) { + return Number(value); + } + } + vm.loading = false; + if (sliderRef) { + sliderRef.noUiSlider.set(vm.startPos); + } + + }); + function setup(slider) { + sliderRef = slider; + } + + function getConsentLevel() { + return analyticResource.getConsentLevel().then(function (response) { + vm.consentLevel = response; + }) + } + function getAllConsentLevels(){ + return analyticResource.getAllConsentLevels().then(function (response) { + vm.consentLevels = response; + }) + } + function saveConsentLevel(){ + analyticResource.saveConsentLevel(vm.sliderVal); + localizationService.localize("analytics_analyticsLevelSavedSuccess").then(function(value) { + notificationsService.success(value); + }); + } + + function sliderChange(values) { + const result = Number(values[0]); + vm.sliderVal = vm.consentLevels[result - 1]; + } + + function calculateStartPositionForSlider(){ + let startPosition = vm.consentLevels.indexOf(vm.consentLevel) + 1; + if(startPosition === 0){ + return 2;// Default start value + } + return startPosition; + } + } + angular.module("umbraco").controller("Umbraco.Dashboard.AnalyticsController", AnalyticsController); + })(); diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/analytics.html b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/analytics.html new file mode 100644 index 0000000000..361b0c8bdc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/analytics.html @@ -0,0 +1,59 @@ +
+ + +

+ Consent for analytics +

+
+

In order to improve Umbraco and add new functionality based on as relevant information as possible, +
we would like to collect system- and usage information from your installation. +
We will NOT collect any personal data like content, code or users, and all data will be fully anonymous. +
+
We will on a regular basis share some of the overall learnings from these metrics. + Hopefully, you'll help us collect some valuable data.

+
+ + +
+ +

+

+ {{vm.sliderVal}} +
We'll only send an anonymous site ID to let us know that the site exists. +
+
+ {{vm.sliderVal}} +
We'll send site ID, umbraco version and packages installed +
+
+ {{vm.sliderVal}} + +
We'll send: +
- Site ID, umbraco version and packages installed +
- System information like Server OS and Webserver +
- Statistics, like number of content nodes and number of media items +
- Configuration settings, like modelsbuilder mode and used languages +
+
We might change/extend what we send on the detailed level in the future, but if so, it will be listed in + this view. + By choosing "detailed" I accept these future changes +
+

+
+
+ + +
+
+
+
+ + diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/move.controller.js b/src/Umbraco.Web.UI.Client/src/views/dictionary/move.controller.js new file mode 100644 index 0000000000..a99f09af21 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/move.controller.js @@ -0,0 +1,66 @@ +angular.module("umbraco") + .controller("Umbraco.Editors.Dictionary.MoveController", + function ($scope, dictionaryResource, treeService, navigationService, notificationsService, appState, eventsService) { + + $scope.dialogTreeApi = {}; + $scope.source = _.clone($scope.currentNode); + + function nodeSelectHandler(args) { + args.event.preventDefault(); + args.event.stopPropagation(); + + if ($scope.target) { + //un-select if there's a current one selected + $scope.target.selected = false; + } + $scope.target = args.node; + $scope.target.selected = true; + } + + $scope.move = function () { + + $scope.busy = true; + $scope.error = false; + + dictionaryResource.move({ parentId: $scope.target.id, id: $scope.source.id }) + .then(function (path) { + $scope.error = false; + $scope.success = true; + $scope.busy = false; + + //first we need to remove the node that launched the dialog + treeService.removeNode($scope.currentNode); + + //get the currently edited node (if any) + var activeNode = appState.getTreeState("selectedNode"); + + //we need to do a double sync here: first sync to the moved content - but don't activate the node, + //then sync to the currenlty edited content (note: this might not be the content that was moved!!) + + navigationService.syncTree({ tree: "dictionary", path: path, forceReload: true, activate: false }).then(function (args) { + if (activeNode) { + var activeNodePath = treeService.getPath(activeNode).join(); + //sync to this node now - depending on what was copied this might already be synced but might not be + navigationService.syncTree({ tree: "dictionary", path: activeNodePath, forceReload: false, activate: true }); + } + }); + + eventsService.emit('app.refreshEditor'); + + }, function (err) { + $scope.success = false; + $scope.error = err; + $scope.busy = false; + + }); + }; + + $scope.onTreeInit = function () { + $scope.dialogTreeApi.callbacks.treeNodeSelect(nodeSelectHandler); + }; + + $scope.close = function() { + navigationService.hideDialog(); + }; + + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/move.html b/src/Umbraco.Web.UI.Client/src/views/dictionary/move.html new file mode 100644 index 0000000000..d8accbb5d5 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/move.html @@ -0,0 +1,53 @@ +
+ +
+
+ +

+ Choose where to move {{source.name}} to in the tree structure below +

+ + + +
+
+
{{error.errorMsg}}
+
{{error.data.message}}
+
+
+ +
+
+ {{source.name}} was moved underneath {{target.name}} +
+ +
+ +
+ +
+ + +
+ +
+
+
+ + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html b/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html index 7dc3e499fe..f7e976de1e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html +++ b/src/Umbraco.Web.UI.Client/src/views/packages/views/repo.html @@ -300,9 +300,9 @@
External sources
diff --git a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js index 684ce6d2f0..f9d48d95e9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/user.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function UserEditController($scope, eventsService, $q, $location, $routeParams, formHelper, usersResource, + function UserEditController($scope, eventsService, $q, $location, $routeParams, formHelper, usersResource, twoFactorLoginResource, userService, contentEditingHelper, localizationService, mediaHelper, Upload, umbRequestHelper, usersHelper, authResource, dateHelper, editorService, overlayService, externalLoginInfoService) { @@ -16,6 +16,7 @@ }; vm.breadcrumbs = []; vm.showBackButton = true; + vm.hasTwoFactorProviders = false; vm.avatarFile = {}; vm.labels = {}; vm.maxFileSize = Umbraco.Sys.ServerVariables.umbracoSettings.maxFileSize + "KB"; @@ -38,6 +39,7 @@ vm.disableUser = disableUser; vm.enableUser = enableUser; vm.unlockUser = unlockUser; + vm.toggleConfigureTwoFactor = toggleConfigureTwoFactor; vm.resendInvite = resendInvite; vm.deleteNonLoggedInUser = deleteNonLoggedInUser; vm.changeAvatar = changeAvatar; @@ -101,6 +103,10 @@ $scope.$emit("$setAccessibleHeader", false, "general_user", false, vm.user.name, "", true); vm.loading = false; }); + + twoFactorLoginResource.get2FAProvidersForUser(vm.user.id).then(function (providers) { + vm.hasTwoFactorProviders = providers.length > 0; + }); }); } @@ -397,6 +403,23 @@ }); } + function toggleConfigureTwoFactor() { + + var configureTwoFactorSettings = { + create: true, + user: vm.user, + isCurrentUser: vm.user.isCurrentUser, + size: "small", + view: "views/common/infiniteeditors/twofactor/configuretwofactor.html", + close: function() { + editorService.close(); + } + }; + + editorService.open(configureTwoFactorSettings); + } + + function resendInvite() { vm.resendInviteButtonState = "busy"; diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js index 4b81e7c11c..f7e1590da8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/groups/groups.controller.js @@ -26,7 +26,7 @@ userService.getCurrentUser().then(function(user) { currentUser = user; // Get usergroups - userGroupsResource.getUserGroups({ onlyCurrentUserGroups: false }).then(function (userGroups) { + userGroupsResource.getUserGroups().then(function (userGroups) { // only allow editing and selection if user is member of the group or admin vm.userGroups = _.map(userGroups, function (ug) { diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.controller.js b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.controller.js index f1df6c3228..67d1efec85 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.controller.js @@ -6,6 +6,7 @@ var vm = this; vm.denyLocalLogin = externalLoginInfoService.hasDenyLocalLogin(); + } angular.module("umbraco").controller("Umbraco.Editors.Users.DetailsController", DetailsController); diff --git a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html index 5a4181c9f3..eaa92b7a6e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html +++ b/src/Umbraco.Web.UI.Client/src/views/users/views/user/details.html @@ -274,6 +274,16 @@ size="s">
+
+ + +
+
- Host.CreateDefaultBuilder(args) + public static IHostBuilder CreateHostBuilder(string[] args) + => Host.CreateDefaultBuilder(args) #if DEBUG - .ConfigureAppConfiguration(config - => config.AddJsonFile( - "appsettings.Local.json", - optional: true, - reloadOnChange: true)) + .ConfigureAppConfiguration(config => config.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: true)) #endif .ConfigureLogging(x => x.ClearProviders()) .ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup()); diff --git a/src/Umbraco.Web.UI/Startup.cs b/src/Umbraco.Web.UI/Startup.cs index 71c3dd008c..0176974b6b 100644 --- a/src/Umbraco.Web.UI/Startup.cs +++ b/src/Umbraco.Web.UI/Startup.cs @@ -20,7 +20,7 @@ namespace Umbraco.Cms.Web.UI /// The web hosting environment. /// The configuration. /// - /// Only a few services are possible to be injected here https://github.com/dotnet/aspnetcore/issues/9337 + /// Only a few services are possible to be injected here https://github.com/dotnet/aspnetcore/issues/9337. /// public Startup(IWebHostEnvironment webHostEnvironment, IConfiguration config) { @@ -34,18 +34,15 @@ namespace Umbraco.Cms.Web.UI /// The services. /// /// This method gets called by the runtime. Use this method to add services to the container. - /// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + /// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940. /// public void ConfigureServices(IServiceCollection services) { -#pragma warning disable IDE0022 // Use expression body for methods services.AddUmbraco(_env, _config) .AddBackOffice() .AddWebsite() .AddComposers() .Build(); -#pragma warning restore IDE0022 // Use expression body for methods - } /// @@ -59,11 +56,11 @@ namespace Umbraco.Cms.Web.UI { app.UseDeveloperExceptionPage(); } - #if (UseHttpsRedirect) - app.UseHttpsRedirection(); + app.UseHttpsRedirection(); #endif + app.UseUmbraco() .WithMiddleware(u => { diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index d400f787cb..536b993aa3 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -4,12 +4,20 @@ net6.0 Umbraco.Cms.Web.UI - + + + + $(DefaultItemExcludes);wwwroot/umbraco/** + + + bin/Release/Umbraco.Web.UI.xml + true + @@ -22,67 +30,17 @@ + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - true - Always - - - true - Always - - - - - - - - - - - + @@ -92,6 +50,7 @@ all + <_ContentIncludedByDefault Remove="wwwroot\umbraco\views\common\infiniteeditors\twofactor\enabletwofactor.html" /> @@ -103,8 +62,8 @@ $(ProjectDir)wwwroot/umbraco $(ProjectDir)umbraco/config/appsettings-schema.json - + @@ -115,35 +74,35 @@ - - - + - + + - + + + - - + @@ -153,8 +112,8 @@ - + diff --git a/src/Umbraco.Web.UI/appsettings.template.json b/src/Umbraco.Web.UI/appsettings.template.json index cef926fad2..2a291c5bc6 100644 --- a/src/Umbraco.Web.UI/appsettings.template.json +++ b/src/Umbraco.Web.UI/appsettings.template.json @@ -33,7 +33,7 @@ }, "KeepAlive": { "DisableKeepAliveTask": false, - "KeepAlivePingUrl": "{umbracoApplicationUrl}/api/keepalive/ping" + "KeepAlivePingUrl": "~/api/keepalive/ping" }, "RequestHandler": { "ConvertUrlsToAscii": "try" diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/umbraco/config/lang/da.xml index 5ec919874f..9916973405 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/da.xml @@ -565,11 +565,11 @@ Du tilføjer flere sprog under 'sprog' i menuen til venstre - ]]> + ]]> Kulturnavn + ]]> Ordbogsoversigt @@ -902,7 +902,7 @@ Database konfiguration installér knappen for at installere Umbraco %0% databasen - ]]> + ]]> Næste for at fortsætte.]]> Databasen er ikke fundet. Kontrollér venligst at informationen i database forbindelsesstrengen i "web.config" filen er korrekt.

@@ -981,7 +981,7 @@ /web.config filen og opdatére 'AppSetting' feltet UmbracoConfigurationStatus i bunden til '%0%'.]]> komme igang med det samme ved at klikke på "Start Umbraco" knappen nedenfor.
Hvis du er ny med Umbraco, kan du finde masser af ressourcer på vores 'getting started' sider. -]]>
+]]>
Start UmbracoFor at administrere dit website skal du blot åbne Umbraco administrationen og begynde at tilføje indhold, opdatere skabelonerne og stylesheets'ene eller tilføje ny funktionalitet.]]> Forbindelse til databasen fejlede. @@ -1028,6 +1028,12 @@ Umbraco: Nulstil adgangskode Dit brugernavn til at logge på Umbraco backoffice er: %0%

Klik her for at nulstille din adgangskode eller kopier/indsæt denne URL i din browser:

%1%

]]>
+ Sidste skridt + Det er påkrævet at du verificerer din identitet. + Vælg venligst en autentificeringsmetode + Kode + Indtast venligst koden fra dit device + Koden kunne ikke genkendes Skrivebord @@ -1065,7 +1071,7 @@ Gå til http://%4%/#/content/content/edit/%5% for at redigere. Ha' en dejlig dag! Mange hilsner fra Umbraco robotten - ]]> + ]]> Hej %0%

Dette er en automatisk mail for at informere dig om at opgaven '%1%' er blevet udførtpå siden '%2%' af brugeren '%3%'

@@ -1166,14 +1172,14 @@ Mange hilsner fra Umbraco robotten Udgivelsen kunne ikke udgives da publiceringsdato er sat + ]]>
+ ]]> + ]]> %0% kunne ikke udgives, fordi et 3. parts modul annullerede handlingen @@ -1453,24 +1459,24 @@ Mange hilsner fra Umbraco robotten @RenderBody() element. - ]]> + ]]> Definer en sektion @section { ... }. Herefter kan denne sektion flettes ind i overliggende skabelon ved at indsætte et @RenderSection element. - ]]> + ]]> Indsæt en sektion @RenderSection(name) element. Den underliggende skabelon skal have defineret en sektion via et @section [name]{ ... } element. - ]]> + ]]> Sektionsnavn Sektionen er obligatorisk @section -definition. - ]]> + ]]> Query builder sider returneret, på Returner @@ -1918,6 +1924,9 @@ Mange hilsner fra Umbraco robotten Ældste Sidst logget ind Ingen brugere er blevet tilføjet + Hvis du ønsker at slå denne autentificeringsmetode fra, så skal du nu indtaste koden fra dit device: + Denne autentificeringsmetode er slået til + Den valgte autentificeringsmetode er nu slået fra Validering diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index bf2de30f2e..6670f692b8 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -569,6 +569,8 @@ Modifying layout will result in loss of data for any existing content that is based on this configuration. + Dictionary item does not exist. + Parent item does not exist. There are no dictionary items. Create dictionary item @@ -1147,6 +1149,12 @@ To manage your website, simply open the Umbraco backoffice and start adding cont ]]> Umbraco: Security Code Your security code is: %0% + One last step + You have enabled 2-factor authentication and must verify your identity. + Please choose a 2-factor provider + Verification code + Please enter the verification code + Invalid code entered Dashboard @@ -2217,6 +2225,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Oldest Last login No user groups have been added + If you wish to disable this two-factor provider, then you must enter the code shown on your authentication device: + This two-factor provider is enabled + This two-factor provider is now disabled Validation @@ -2675,7 +2686,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Create new Element Type Custom stylesheet Add stylesheet - Editor apperance + Editor appearance Data models Catalogue appearance Background color diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index 873361169a..bc8f38d408 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -579,17 +579,19 @@ Modifying layout will result in loss of data for any existing content that is based on this configuration. + Dictionary item does not exist. + Parent item does not exist. There are no dictionary items. Create dictionary item %0%' below - ]]> + ]]> Culture Name + ]]> Dictionary overview @@ -860,6 +862,7 @@ URL User Username + Validate Value View Welcome... @@ -931,7 +934,7 @@ Database configuration install button to install the Umbraco %0% database - ]]> + ]]> Next to proceed.]]> Database not found! Please check that the information in the "connection string" of the "web.config" file is correct.

@@ -949,7 +952,7 @@

Don't worry - no content will be deleted and everything will continue working afterwards!

- ]]>
+ ]]> Press Next to proceed. ]]> @@ -995,19 +998,19 @@ + ]]> I want to start from scratch learn how) You can still choose to install Runway later on. Please go to the Developer section and choose Packages. - ]]> + ]]> You've just set up a clean Umbraco platform. What do you want to do next? Runway is installed This is our list of recommended modules, check off the ones you would like to install, or view the full list of modules - ]]> + ]]> Only recommended for experienced users I want to start with a simple website Included with Runway: Home page, Getting Started page, Installing Modules page.
Optional Modules: Top Navigation, Sitemap, Contact, Gallery. - ]]>
+ ]]> What is Runway Step 1/5 Accept license Step 2/5: Database configuration @@ -1098,72 +1101,78 @@ To manage your website, simply open the Umbraco backoffice and start adding cont -
- - - - - - - - - - - - + +
-
-
- - - + +
- - - + + +
-

+
+ +

+
+ + + + + + +
+
+

+

If you cannot click on the link, copy and paste this URL into your browser window:

+ + + + +
+ +%1% + +
+

+
+ + + +


+
+ + + + + + + ]]> + One last step + You have enabled 2-factor authentication and must verify your identity. + Please choose a 2-factor provider + Verification code + Please enter the verification code + Invalid code entered Dashboard @@ -1203,7 +1212,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Have a nice day! Cheers from the Umbraco robot - ]]> + ]]> The following languages have been modified %0% @@ -1220,70 +1229,70 @@ To manage your website, simply open the Umbraco backoffice and start adding cont -
- - - - - - - - - - - - + +
-
-
- - - + +
- - - + + +
-

+
+ +

+
+ + + + - - -
+
+
+ + + - -
+ + + - -
+

Hi %0%,

-

+

This is an automated mail to inform you that the task '%1%' has been performed on the page '%2%' by the user '%3%' -

- - - - - - -
- -
- EDIT
-
-

-

Update summary:

+

+ + + + + + +
+ +
+EDIT
+
+

+

Update summary:

%6%

-

+

Have a nice day!

Cheers from the Umbraco robot

-
-
-


-
-
- - - ]]> +
+ + + +


+ + + + + + + + ]]>
The following languages have been modified:

%0% - ]]>
+ ]]> [%0%] Notification about %1% performed on %2% Notifications @@ -1294,7 +1303,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont button and locating the package. Umbraco packages usually have a ".umb" or ".zip" extension. - ]]> + ]]> This will delete the package Include all child nodes Installed @@ -1386,22 +1395,22 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Insufficient user permissions to publish all descendant documents + ]]> + ]]> + ]]> + ]]> + ]]> + ]]> Validation failed for required language '%0%'. This @@ -1414,7 +1423,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Publish %0% and all its subpages Publish to publish %0% and thereby making its content publicly available.

You can publish this page and all its subpages by checking Include unpublished subpages below. - ]]>
+ ]]>
You have not configured any approved colors @@ -1692,23 +1701,23 @@ To manage your website, simply open the Umbraco backoffice and start adding cont @RenderBody() placeholder. - ]]> + ]]> Define a named section @section { ... }. This can be rendered in a specific area of the parent of this template, by using @RenderSection. - ]]> + ]]> Render a named section @RenderSection(name) placeholder. This renders an area of a child template which is wrapped in a corresponding @section [name]{ ... } definition. - ]]> + ]]> Section Name Section is mandatory @section definition, otherwise an error is shown. - ]]> + ]]> Query builder items returned, in I want @@ -2000,7 +2009,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Have a nice day! Cheers from the Umbraco robot - ]]> + ]]> No translator users found. Please create a translator user before you start sending content to translation @@ -2176,6 +2185,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont you. Click the circle above to upload your photo. Writer + Configure Two-Factor Change Your profile Your recent history @@ -2200,82 +2210,82 @@ To manage your website, simply open the Umbraco backoffice and start adding cont -
- - - - - - - - - - - - + +
-
-
- - - + +
- - - + + +
-

+
+ +

+
+ + + + + + +
+
+

If you cannot click on the link, copy and paste this URL into your browser window:

+ + + + +
+ +%3% + +
+

+
+ + + +


+ + + + + + +]]> Resending invitation... Delete User Are you sure you wish to delete this user account? @@ -2291,6 +2301,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Oldest Last login No user groups have been added + If you wish to disable this two-factor provider, then you must enter the code shown on your authentication device: + This two-factor provider is enabled + This two-factor provider is now disabled Validation @@ -2495,6 +2508,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Published Status Models Builder Health Check + Analytics Profiling Getting Started Install Umbraco Forms @@ -2760,7 +2774,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont Create new Element Type Custom stylesheet Add stylesheet - Editor apperance + Editor appearance Data models Catalogue appearance Background color @@ -2850,4 +2864,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont item returned items returned + + Consent for analytics + Analytics level saved! + diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml b/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml index d9ecc89673..8f94e8694d 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/nl.xml @@ -82,6 +82,7 @@ Toegang toestaan om een node te vertalen Toegang toestaan om een node op te slaan Toegang toestaan om Inhoudssjabloon aan te maken + Toegang toestaan om meldingen voor content nodes aan te maken Inhoud @@ -256,6 +257,7 @@ Statistieken Titel (optioneel) Alternatieve tekst (optioneel) + Bijschrift (optioneel) Type Depubliceren Concept @@ -694,6 +696,7 @@ Wissen Sluiten Sluit venster + Sluit paneel Comment Bevestig Beperken @@ -2219,6 +2222,9 @@ Echter, Runway biedt een gemakkelijke basis om je snel op weg te helpen. Als je Geen relaties voor dit relatietype. Relatietype Relaties + Is Afhankelijkheid + Ja + Nee Aan de slag diff --git a/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoPageResult.cs b/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoPageResult.cs index 0997b92a5c..97d38bbe53 100644 --- a/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoPageResult.cs +++ b/src/Umbraco.Web.Website/ActionResults/RedirectToUmbracoPageResult.cs @@ -2,9 +2,9 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Web; @@ -122,8 +122,9 @@ namespace Umbraco.Cms.Web.Website.ActionResults } HttpContext httpContext = context.HttpContext; - IIOHelper ioHelper = httpContext.RequestServices.GetRequiredService(); - string destinationUrl = ioHelper.ResolveUrl(Url); + IUrlHelperFactory urlHelperFactory = httpContext.RequestServices.GetRequiredService(); + IUrlHelper urlHelper = urlHelperFactory.GetUrlHelper(context); + string destinationUrl = urlHelper.Content(Url); if (_queryString.HasValue) { @@ -134,6 +135,5 @@ namespace Umbraco.Cms.Web.Website.ActionResults return Task.CompletedTask; } - } } diff --git a/src/Umbraco.Web.Website/Routing/ControllerActionSearcher.cs b/src/Umbraco.Web.Website/Routing/ControllerActionSearcher.cs index ecb36f8c9b..39e63925e3 100644 --- a/src/Umbraco.Web.Website/Routing/ControllerActionSearcher.cs +++ b/src/Umbraco.Web.Website/Routing/ControllerActionSearcher.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Linq; using Microsoft.AspNetCore.Http; @@ -31,13 +32,20 @@ namespace Umbraco.Cms.Web.Website.Routing _actionSelector = actionSelector; } + /// /// Determines if a custom controller can hijack the current route /// /// The controller type to find - public ControllerActionDescriptor? Find(HttpContext httpContext, string? controller, string? action) + public ControllerActionDescriptor Find(HttpContext httpContext, string controller, string action) => Find(httpContext, controller, action, null); + + /// + /// Determines if a custom controller can hijack the current route + /// + /// The controller type to find + public ControllerActionDescriptor? Find(HttpContext httpContext, string? controller, string? action, string? area) { - IReadOnlyList? candidates = FindControllerCandidates(httpContext, controller, action, DefaultActionName); + IReadOnlyList? candidates = FindControllerCandidates(httpContext, controller, action, DefaultActionName, area); if (candidates?.Count > 0) { @@ -47,6 +55,7 @@ namespace Umbraco.Cms.Web.Website.Routing return null; } + /// /// Return a list of controller candidates that match the custom controller and action names /// @@ -54,7 +63,8 @@ namespace Umbraco.Cms.Web.Website.Routing HttpContext httpContext, string? customControllerName, string? customActionName, - string defaultActionName) + string? defaultActionName, + string? area = null) { // Use aspnetcore's IActionSelector to do the finding since it uses an optimized cache lookup var routeValues = new RouteValueDictionary @@ -62,6 +72,12 @@ namespace Umbraco.Cms.Web.Website.Routing [ControllerToken] = customControllerName, [ActionToken] = customActionName, // first try to find the custom action }; + + if (area != null) + { + routeValues[AreaToken] = area; + } + var routeData = new RouteData(routeValues); var routeContext = new RouteContext(httpContext) { diff --git a/src/Umbraco.Web.Website/Routing/IControllerActionSearcher.cs b/src/Umbraco.Web.Website/Routing/IControllerActionSearcher.cs index 1d6504fbda..e2bcb5a5fd 100644 --- a/src/Umbraco.Web.Website/Routing/IControllerActionSearcher.cs +++ b/src/Umbraco.Web.Website/Routing/IControllerActionSearcher.cs @@ -1,3 +1,4 @@ +using System; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Controllers; @@ -5,6 +6,11 @@ namespace Umbraco.Cms.Web.Website.Routing { public interface IControllerActionSearcher { - ControllerActionDescriptor? Find(HttpContext httpContext, string? controller, string? action); + + ControllerActionDescriptor? Find(HttpContext httpContext, string controller, string action); + + ControllerActionDescriptor? Find(HttpContext httpContext, string controller, string action, string area) + => Find(httpContext, controller, action); + } } diff --git a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs index 12ca67ce17..f8398eac2d 100644 --- a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs +++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs @@ -240,7 +240,7 @@ namespace Umbraco.Cms.Web.Website.Routing [ActionToken] = postedInfo.ActionName }; - ControllerActionDescriptor? surfaceControllerDescriptor = _controllerActionSearcher.Find(httpContext, postedInfo.ControllerName, postedInfo.ActionName); + ControllerActionDescriptor? surfaceControllerDescriptor = _controllerActionSearcher.Find(httpContext, postedInfo.ControllerName, postedInfo.ActionName, postedInfo.Area); if (surfaceControllerDescriptor == null) { diff --git a/templates/Umbraco.Templates.nuspec b/templates/Umbraco.Templates.nuspec new file mode 100644 index 0000000000..823a925de0 --- /dev/null +++ b/templates/Umbraco.Templates.nuspec @@ -0,0 +1,31 @@ + + + + Umbraco.Templates + 1.0.0 + Umbraco HQ + Umbraco HQ + MIT + https://umbraco.com/ + icon.png + https://umbraco.com/dist/nuget/logo-small.png + false + Umbraco CMS templates for .NET Core Template Engine available through the dotnet CLI's new command + en-US + umbraco + + + + + + + + + + + + + + + + diff --git a/templates/UmbracoPackage/.template.config/dotnetcli.host.json b/templates/UmbracoPackage/.template.config/dotnetcli.host.json new file mode 100644 index 0000000000..0f64d676c1 --- /dev/null +++ b/templates/UmbracoPackage/.template.config/dotnetcli.host.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json.schemastore.org/dotnetcli.host", + "symbolInfo": { + "Framework": { + "longName": "Framework", + "shortName": "F" + }, + "UmbracoVersion": { + "longName": "version", + "shortName": "v" + } + } +} diff --git a/templates/UmbracoPackage/.template.config/ide.host.json b/templates/UmbracoPackage/.template.config/ide.host.json new file mode 100644 index 0000000000..baec9f98c6 --- /dev/null +++ b/templates/UmbracoPackage/.template.config/ide.host.json @@ -0,0 +1,15 @@ +{ + "$schema": "http://json.schemastore.org/vs-2017.3.host", + "order": 0, + "icon": "../../icon.png", + "description": { + "id": "UmbracoPackage", + "text": "Umbraco Package - An empty Umbraco CMS package/plugin" + }, + "symbolInfo": [ + { + "id": "UmbracoVersion", + "isVisible": "true" + } + ] +} diff --git a/templates/UmbracoPackage/.template.config/template.json b/templates/UmbracoPackage/.template.config/template.json new file mode 100644 index 0000000000..0d2c7055fb --- /dev/null +++ b/templates/UmbracoPackage/.template.config/template.json @@ -0,0 +1,103 @@ +{ + "$schema": "http://json.schemastore.org/template", + "author": "Umbraco HQ", + "classifications": [ + "Web", + "CMS", + "Umbraco", + "Package", + "Plugin" + ], + "name": "Umbraco Package", + "description": "An empty Umbraco package/plugin project ready to get started", + "groupIdentity": "Umbraco.Templates.UmbracoPackage", + "identity": "Umbraco.Templates.UmbracoPackage.CSharp", + "shortName": "umbracopackage", + "tags": { + "language": "C#", + "type": "project" + }, + "sourceName": "UmbracoPackage", + "defaultName": "UmbracoPackage1", + "preferNameDirectory": true, + "symbols": { + "Framework": { + "displayName": "Framework", + "description": "The target framework for the project.", + "type": "parameter", + "datatype": "choice", + "choices": [ + { + "displayName": ".NET 6.0", + "description": "Target net6.0", + "choice": "net6.0" + } + ], + "defaultValue": "net6.0", + "replaces": "net6.0" + }, + "UmbracoVersion": { + "displayName": "Umbraco version", + "description": "The version of Umbraco.Cms to add as PackageReference.", + "type": "parameter", + "datatype": "string", + "defaultValue": "10.0.0-rc", + "replaces": "UMBRACO_VERSION_FROM_TEMPLATE" + }, + "namespaceReplacer": { + "type": "generated", + "generator": "regex", + "dataType": "string", + "parameters": { + "source": "name", + "steps": [ + { + "regex": "\\s", + "replacement": "_" + }, + { + "regex": "-", + "replacement": "_" + }, + { + "regex": "^[^a-zA-Z_]+", + "replacement": "_" + } + ] + }, + "replaces": "UmbracoPackage" + }, + "msbuildReplacer": { + "type": "generated", + "generator": "regex", + "dataType": "string", + "parameters": { + "source": "name", + "steps": [ + { + "regex": "\\s", + "replacement": "" + }, + { + "regex": "\\.", + "replacement": "" + }, + { + "regex": "-", + "replacement": "" + }, + { + "regex": "^[^a-zA-Z_]+", + "replacement": "" + } + ] + }, + "replaces": "UmbracoPackageMsBuild" + } + }, + "primaryOutputs": [ + { + "path": "UmbracoPackage.csproj" + } + ] +} diff --git a/templates/UmbracoPackage/App_Plugins/UmbracoPackage/package.manifest b/templates/UmbracoPackage/App_Plugins/UmbracoPackage/package.manifest new file mode 100644 index 0000000000..906db79b7a --- /dev/null +++ b/templates/UmbracoPackage/App_Plugins/UmbracoPackage/package.manifest @@ -0,0 +1,5 @@ +{ + "name": "UmbracoPackage", + "version": "", + "allowPackageTelemetry": true +} \ No newline at end of file diff --git a/build/templates/UmbracoPackage/UmbracoPackage.csproj b/templates/UmbracoPackage/UmbracoPackage.csproj similarity index 90% rename from build/templates/UmbracoPackage/UmbracoPackage.csproj rename to templates/UmbracoPackage/UmbracoPackage.csproj index a26c959092..dc41d3a6e1 100644 --- a/build/templates/UmbracoPackage/UmbracoPackage.csproj +++ b/templates/UmbracoPackage/UmbracoPackage.csproj @@ -12,8 +12,8 @@ - - + + @@ -22,7 +22,7 @@ Always - True + true buildTransitive diff --git a/build/templates/UmbracoPackage/build/UmbracoPackage.targets b/templates/UmbracoPackage/build/UmbracoPackage.targets similarity index 78% rename from build/templates/UmbracoPackage/build/UmbracoPackage.targets rename to templates/UmbracoPackage/build/UmbracoPackage.targets index 5e3abf6ae1..33d3689179 100644 --- a/build/templates/UmbracoPackage/build/UmbracoPackage.targets +++ b/templates/UmbracoPackage/build/UmbracoPackage.targets @@ -9,11 +9,7 @@ - - + diff --git a/build/templates/UmbracoProject/.gitignore b/templates/UmbracoProject/.gitignore similarity index 98% rename from build/templates/UmbracoProject/.gitignore rename to templates/UmbracoProject/.gitignore index f4caa41045..602728d104 100644 --- a/build/templates/UmbracoProject/.gitignore +++ b/templates/UmbracoProject/.gitignore @@ -464,9 +464,6 @@ $RECYCLE.BIN/ # Umbraco log files **/umbraco/Logs/ -# Dont commit files that are generated and cached from the default ImageSharp location -**/umbraco/mediacache/ - # Umbraco backoffice language files # Nuget package Umbraco.Cms.StaticAssets will copy them in during dotnet build # Customize langguage files in /config/lang/{language}.user.xml diff --git a/templates/UmbracoProject/.template.config/dotnetcli.host.json b/templates/UmbracoProject/.template.config/dotnetcli.host.json new file mode 100644 index 0000000000..68f7f906be --- /dev/null +++ b/templates/UmbracoProject/.template.config/dotnetcli.host.json @@ -0,0 +1,55 @@ +{ + "$schema": "http://json.schemastore.org/dotnetcli.host", + "symbolInfo": { + "Framework": { + "longName": "Framework", + "shortName": "F" + }, + "UmbracoVersion": { + "longName": "version", + "shortName": "v" + }, + "UseHttpsRedirect": { + "longName": "use-https-redirect", + "shortName": "" + }, + "UseSqlCe": { + "longName": "SqlCe", + "shortName": "ce" + }, + "SkipRestore": { + "longName": "no-restore", + "shortName": "" + }, + "UnattendedUserName": { + "longName": "friendly-name", + "shortName": "" + }, + "UnattendedUserEmail": { + "longName": "email", + "shortName": "" + }, + "UnattendedUserPassword": { + "longName": "password", + "shortName": "" + }, + "ConnectionString": { + "longName": "connection-string", + "shortName": "" + }, + "NoNodesViewPath": { + "longName": "no-nodes-view-path", + "shortName": "" + }, + "PackageProjectName": { + "longName": "PackageTestSiteName", + "shortName": "p" + } + }, + "usageExamples": [ + "dotnet new umbraco -n MyNewProject", + "dotnet new umbraco -n MyNewProjectUsingSqlCE -ce", + "dotnet new umbraco -n MyNewProject --no-restore", + "dotnet new umbraco -n MyNewProject --friendly-name \"Friendly Admin User\" --email admin@example.com --password password1234 --connection-string \"Server=ConnectionStringHere\"" + ] +} diff --git a/templates/UmbracoProject/.template.config/ide.host.json b/templates/UmbracoProject/.template.config/ide.host.json new file mode 100644 index 0000000000..893b9cc3f9 --- /dev/null +++ b/templates/UmbracoProject/.template.config/ide.host.json @@ -0,0 +1,51 @@ +{ + "$schema": "http://json.schemastore.org/vs-2017.3.host", + "order": 0, + "icon": "../../icon.png", + "description": { + "id": "UmbracoProject", + "text": "Umbraco Web Application - An empty Umbraco CMS web application" + }, + "symbolInfo": [ + { + "id": "UmbracoVersion", + "isVisible": "true" + }, + { + "id": "UseHttpsRedirect", + "isVisible": "true" + }, + { + "id": "UseSqlCe", + "isVisible": "true" + }, + { + "id": "SkipRestore", + "isVisible": "true" + }, + { + "id": "UnattendedUserName", + "isVisible": "true" + }, + { + "id": "UnattendedUserEmail", + "isVisible": "true" + }, + { + "id": "UnattendedUserPassword", + "isVisible": "true" + }, + { + "id": "ConnectionString", + "isVisible": "true" + }, + { + "id": "NoNodesViewPath", + "isVisible": "true" + }, + { + "id": "PackageProjectName", + "isVisible": "true" + } + ] +} diff --git a/templates/UmbracoProject/.template.config/template.json b/templates/UmbracoProject/.template.config/template.json new file mode 100644 index 0000000000..29d464a50d --- /dev/null +++ b/templates/UmbracoProject/.template.config/template.json @@ -0,0 +1,324 @@ +{ + "$schema": "http://json.schemastore.org/template", + "author": "Umbraco HQ", + "classifications": [ + "Web", + "CMS", + "Umbraco" + ], + "name": "Umbraco Project", + "description": "An empty Umbraco project ready to get started", + "groupIdentity": "Umbraco.Templates.UmbracoProject", + "identity": "Umbraco.Templates.UmbracoProject.CSharp", + "shortName": "umbraco", + "tags": { + "language": "C#", + "type": "project" + }, + "sourceName": "UmbracoProject", + "defaultName": "UmbracoProject1", + "preferNameDirectory": true, + "symbols": { + "Framework": { + "displayName": "Framework", + "description": "The target framework for the project.", + "type": "parameter", + "datatype": "choice", + "choices": [ + { + "displayName": ".NET 6.0", + "description": "Target net6.0", + "choice": "net6.0" + } + ], + "defaultValue": "net6.0", + "replaces": "net6.0" + }, + "UmbracoVersion": { + "displayName": "Umbraco version", + "description": "The version of Umbraco.Cms to add as PackageReference.", + "type": "parameter", + "datatype": "string", + "defaultValue": "10.0.0-rc", + "replaces": "UMBRACO_VERSION_FROM_TEMPLATE" + }, + "UseHttpsRedirect": { + "displayName": "Use HTTPS redirect", + "description": "Adds code to Startup.cs to redirect HTTP to HTTPS and enables the UseHttps setting.", + "type": "parameter", + "datatype": "bool", + "defaultValue": "false" + }, + "UseSqlCe": { + "displayName": "Add dependencies to use SQL CE database provider", + "description": "Adds the required dependencies to use SQL Compact Edition as database provider (Windows only).", + "type": "parameter", + "datatype": "bool", + "defaultValue": "false" + }, + "SkipRestore": { + "displayName": "Skip restore", + "description": "If specified, skips the automatic restore of the project on create.", + "type": "parameter", + "datatype": "bool", + "defaultValue": "false" + }, + "UnattendedUserName": { + "displayName": "Unattended user name", + "description": "Used to specify the name of the default admin user when using unattended install (stored as plain text).", + "type": "parameter", + "datatype": "string", + "defaultValue": "" + }, + "UnattendedUserNameReplacer": { + "type": "generated", + "generator": "regex", + "dataType": "string", + "parameters": { + "source": "UnattendedUserName", + "steps": [ + { + "regex": "\\\\", + "replacement": "\\\\" + }, + { + "regex": "\\\"", + "replacement": "\\\"" + }, + { + "regex": "\\\n", + "replacement": "\\\n" + }, + { + "regex": "\\\t", + "replacement": "\\\t" + } + ] + }, + "replaces": "UNATTENDED_USER_NAME_FROM_TEMPLATE" + }, + "UnattendedUserEmail": { + "displayName": "Unattended user email", + "description": "Used to specify the email of the default admin user when using unattended install (stored as plain text).", + "type": "parameter", + "datatype": "string", + "defaultValue": "" + }, + "UnattendedUserEmailReplacer": { + "type": "generated", + "generator": "regex", + "dataType": "string", + "parameters": { + "source": "UnattendedUserEmail", + "steps": [ + { + "regex": "\\\\", + "replacement": "\\\\" + }, + { + "regex": "\\\"", + "replacement": "\\\"" + }, + { + "regex": "\\\n", + "replacement": "\\\n" + }, + { + "regex": "\\\t", + "replacement": "\\\t" + } + ] + }, + "replaces": "UNATTENDED_USER_EMAIL_FROM_TEMPLATE" + }, + "UnattendedUserPassword": { + "displayName": "Unattended user password", + "description": "Used to specify the password of the default admin user when using unattended install (stored as plain text).", + "type": "parameter", + "datatype": "string", + "defaultValue": "" + }, + "UnattendedUserPasswordReplacer": { + "type": "generated", + "generator": "regex", + "dataType": "string", + "parameters": { + "source": "UnattendedUserPassword", + "steps": [ + { + "regex": "\\\\", + "replacement": "\\\\" + }, + { + "regex": "\\\"", + "replacement": "\\\"" + }, + { + "regex": "\\\n", + "replacement": "\\\n" + }, + { + "regex": "\\\t", + "replacement": "\\\t" + } + ] + }, + "replaces": "UNATTENDED_USER_PASSWORD_FROM_TEMPLATE" + }, + "ConnectionString": { + "displayName": "Connection string", + "description": "Database connection string used by Umbraco.", + "type": "parameter", + "datatype": "string", + "defaultValue": "" + }, + "ConnectionStringReplacer": { + "type": "generated", + "generator": "regex", + "dataType": "string", + "parameters": { + "source": "ConnectionString", + "steps": [ + { + "regex": "\\\\", + "replacement": "\\\\" + }, + { + "regex": "\\\"", + "replacement": "\\\"" + }, + { + "regex": "\\\n", + "replacement": "\\\n" + }, + { + "regex": "\\\t", + "replacement": "\\\t" + } + ] + }, + "replaces": "CONNECTION_STRING_FROM_TEMPLATE" + }, + "HasConnectionString": { + "type": "computed", + "value": "(ConnectionString != \"\")" + }, + "UsingUnattenedInstall": { + "type": "computed", + "value": "(UnattendedUserName != \"\" && UnattendedUserEmail != \"\" && UnattendedUserPassword != \"\" && ConnectionString != \"\")" + }, + "NoNodesViewPath": { + "displayName": "No nodes view path", + "description": "Path to a custom view presented with the Umbraco installation contains no published content", + "type": "parameter", + "datatype": "string", + "defaultValue": "" + }, + "NoNodesViewPathReplacer": { + "type": "generated", + "generator": "regex", + "dataType": "string", + "parameters": { + "source": "NoNodesViewPath", + "steps": [ + { + "regex": "\\\\", + "replacement": "\\\\" + }, + { + "regex": "\\\"", + "replacement": "\\\"" + }, + { + "regex": "\\\n", + "replacement": "\\\n" + }, + { + "regex": "\\\t", + "replacement": "\\\t" + } + ] + }, + "replaces": "NO_NODES_VIEW_PATH_FROM_TEMPLATE" + }, + "HasNoNodesViewPath": { + "type": "computed", + "value": "(NoNodesViewPath != \"\")" + }, + "PackageProjectName": { + "displayName": "Umbraco package project name", + "description": "The name of the package project this should be a test site for.", + "type": "parameter", + "datatype": "string", + "defaultValue": "", + "replaces": "PACKAGE_PROJECT_NAME_FROM_TEMPLATE" + }, + "NamespaceReplacer": { + "type": "generated", + "generator": "regex", + "dataType": "string", + "parameters": { + "source": "name", + "steps": [ + { + "regex": "\\s", + "replacement": "_" + }, + { + "regex": "-", + "replacement": "_" + }, + { + "regex": "^[^a-zA-Z_]+", + "replacement": "_" + } + ] + }, + "replaces": "Umbraco.Cms.Web.UI" + }, + "HttpPort": { + "type": "generated", + "generator": "port", + "parameters": { + "fallback": 5000 + }, + "replaces": "HTTP_PORT_FROM_TEMPLATE" + }, + "HttpsPort": { + "type": "generated", + "generator": "port", + "parameters": { + "low": 44300, + "high": 44399, + "fallback": 5001 + }, + "replaces": "HTTPS_PORT_FROM_TEMPLATE" + }, + "TelemetryId": { + "type": "generated", + "generator": "guid", + "parameters": { + "defaultFormat": "d" + }, + "replaces": "TELEMETRYID_FROM_TEMPLATE" + } + }, + "primaryOutputs": [ + { + "path": "UmbracoProject.csproj" + } + ], + "postActions": [ + { + "condition": "(!SkipRestore)", + "description": "Restore NuGet packages required by this project", + "manualInstructions": [ + { + "text": "Run 'dotnet restore'" + } + ], + "actionId": "210D431B-A78B-4D2F-B762-4ED3E3EA9025", + "continueOnError": true + } + ] +} diff --git a/build/templates/UmbracoProject/Properties/launchSettings.json b/templates/UmbracoProject/Properties/launchSettings.json similarity index 100% rename from build/templates/UmbracoProject/Properties/launchSettings.json rename to templates/UmbracoProject/Properties/launchSettings.json diff --git a/templates/UmbracoProject/UmbracoProject.csproj b/templates/UmbracoProject/UmbracoProject.csproj new file mode 100644 index 0000000000..447dbcc76c --- /dev/null +++ b/templates/UmbracoProject/UmbracoProject.csproj @@ -0,0 +1,33 @@ + + + + net6.0 + Umbraco.Cms.Web.UI + + + + + + + + + + + + + + + + + + + true + + + + + false + false + + + diff --git a/build/templates/UmbracoProject/appsettings.Development.json b/templates/UmbracoProject/appsettings.Development.json similarity index 72% rename from build/templates/UmbracoProject/appsettings.Development.json rename to templates/UmbracoProject/appsettings.Development.json index dad70ac5d8..e132ce944d 100644 --- a/build/templates/UmbracoProject/appsettings.Development.json +++ b/templates/UmbracoProject/appsettings.Development.json @@ -1,5 +1,5 @@ { - "$schema" : "./umbraco/config/appsettings-schema.json", + "$schema": "./umbraco/config/appsettings-schema.json", "Serilog": { "MinimumLevel": { "Default": "Information" @@ -19,22 +19,22 @@ }, //#if (HasConnectionString) "ConnectionStrings": { - "umbracoDbDSN": "CONNECTION_FROM_TEMPLATE" + "umbracoDbDSN": "CONNECTION_STRING_FROM_TEMPLATE" }, //#endif "Umbraco": { "CMS": { - "Content": { - "MacroErrors": "Throw" - }, //#if (UsingUnattenedInstall) "Unattended": { "InstallUnattended": true, - "UnattendedUserName": "FRIENDLY_NAME_FROM_TEMPLATE", - "UnattendedUserEmail": "EMAIL_FROM_TEMPLATE", - "UnattendedUserPassword": "PASSWORD_FROM_TEMPLATE" + "UnattendedUserName": "UNATTENDED_USER_NAME_FROM_TEMPLATE", + "UnattendedUserEmail": "UNATTENDED_USER_EMAIL_FROM_TEMPLATE", + "UnattendedUserPassword": "UNATTENDED_USER_PASSWORD_FROM_TEMPLATE" }, //#endif + "Content": { + "MacroErrors": "Throw" + }, "Global": { "Smtp": { "From": "your@email.here", @@ -51,4 +51,4 @@ } } } -} \ No newline at end of file +} diff --git a/build/templates/UmbracoProject/appsettings.json b/templates/UmbracoProject/appsettings.json similarity index 55% rename from build/templates/UmbracoProject/appsettings.json rename to templates/UmbracoProject/appsettings.json index 99e877812c..896d67d5fa 100644 --- a/build/templates/UmbracoProject/appsettings.json +++ b/templates/UmbracoProject/appsettings.json @@ -1,5 +1,5 @@ { - "$schema" : "./umbraco/config/appsettings-schema.json", + "$schema": "./umbraco/config/appsettings-schema.json", "Serilog": { "MinimumLevel": { "Default": "Information", @@ -11,26 +11,19 @@ } }, "ConnectionStrings": { - "umbracoDbDSN": "" + "umbracoDbDSN": null }, "Umbraco": { "CMS": { - //#if (HasNoNodesViewPath || UseHttpsRedirect) "Global": { - "SanitizeTinyMce": true, - //#if (!HasNoNodesViewPath && UseHttpsRedirect) - "UseHttps": true - //#elseif (UseHttpsRedirect) + "Id": "TELEMETRYID_FROM_TEMPLATE", + //#if (UseHttpsRedirect) "UseHttps": true, //#endif //#if (HasNoNodesViewPath) - "NoNodesViewPath": "NO_NODES_VIEW_PATH_FROM_TEMPLATE" + "NoNodesViewPath": "NO_NODES_VIEW_PATH_FROM_TEMPLATE", //#endif - - }, - //#endif - "Hosting": { - "Debug": false + "SanitizeTinyMce": true }, "Content": { "ContentVersionCleanupPolicy": { diff --git a/build/templates/UmbracoPackage/.template.config/icon.png b/templates/icon.png similarity index 100% rename from build/templates/UmbracoPackage/.template.config/icon.png rename to templates/icon.png diff --git a/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/HelpPanel/systemInformation.ts b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/HelpPanel/systemInformation.ts index 363244e12f..dbc9d19427 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/HelpPanel/systemInformation.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/HelpPanel/systemInformation.ts @@ -20,7 +20,7 @@ context('System Information', () => { it('Check System Info Displays', () => { openSystemInformation(); - cy.get('.table').find('tr').should('have.length', 10); + cy.get('.table').find('tr').should('have.length', 13); cy.contains('Current Culture').parent().should('contain', 'en-US'); cy.contains('Current UI Culture').parent().should('contain', 'en-US'); }); diff --git a/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Tours/backofficeTour.ts b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Tours/backofficeTour.ts index dc1232e148..dbb82f53f4 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Tours/backofficeTour.ts +++ b/tests/Umbraco.Tests.AcceptanceTest/cypress/integration/Tours/backofficeTour.ts @@ -93,7 +93,7 @@ function runBackOfficeIntroTour(percentageComplete, buttonText, timeout) { cy.get('.umb-tour-step__counter', { timeout: timeout }).contains('9/13'); cy.get('.umb-tour-step__footer .umb-button').should('be.visible').click(); cy.get('.umb-tour-step__counter', { timeout: timeout }).contains('10/13'); - cy.get('.umb-overlay-drawer__align-right .umb-button').should('be.visible').click(); + cy.get('[data-element~="overlay-user"] [data-element="button-overlayClose"]').should('be.visible').click(); cy.get('.umb-tour-step__counter', { timeout: timeout }).contains('11/13'); cy.umbracoGlobalHelp().click() diff --git a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json index 4beacd8e3c..f65d14c10a 100644 --- a/tests/Umbraco.Tests.AcceptanceTest/package-lock.json +++ b/tests/Umbraco.Tests.AcceptanceTest/package-lock.json @@ -3530,9 +3530,9 @@ } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, "ms": { diff --git a/tests/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj b/tests/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj index ca53cc8ed0..4440943322 100644 --- a/tests/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj +++ b/tests/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj @@ -1,21 +1,26 @@ net6.0 - Exe + Exe false + false + + + + 0.13.1 @@ -26,7 +31,8 @@ 4.16.1 - + - + + diff --git a/tests/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj b/tests/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj index f03331bb49..fbd69e7812 100644 --- a/tests/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj +++ b/tests/Umbraco.Tests.Common/Umbraco.Tests.Common.csproj @@ -6,6 +6,7 @@ Umbraco.Cms.Tests Umbraco CMS Test Tools Contains commonly used tools to write tests for Umbraco CMS, such as various builders for content etc. + true diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MetricsConsentServiceTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MetricsConsentServiceTest.cs new file mode 100644 index 0000000000..70d0e80a33 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/MetricsConsentServiceTest.cs @@ -0,0 +1,40 @@ +using NUnit.Framework; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services +{ + [TestFixture] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] + public class MetricsConsentServiceTest : UmbracoIntegrationTest + { + private IMetricsConsentService MetricsConsentService => GetRequiredService(); + + private IKeyValueService KeyValueService => GetRequiredService(); + + [Test] + [TestCase(TelemetryLevel.Minimal)] + [TestCase(TelemetryLevel.Basic)] + [TestCase(TelemetryLevel.Detailed)] + public void Can_Store_Consent(TelemetryLevel level) + { + MetricsConsentService.SetConsentLevel(level); + + var actual = MetricsConsentService.GetConsentLevel(); + Assert.IsNotNull(actual); + Assert.AreEqual(level, actual); + } + + [Test] + public void Enum_Stored_as_string() + { + MetricsConsentService.SetConsentLevel(TelemetryLevel.Detailed); + + var stringValue = KeyValueService.GetValue(Cms.Core.Services.MetricsConsentService.Key); + + Assert.AreEqual("Detailed", stringValue); + } + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/TelemetryProviderTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/TelemetryProviderTests.cs new file mode 100644 index 0000000000..54392df0e4 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/TelemetryProviderTests.cs @@ -0,0 +1,352 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Cms.Infrastructure.Telemetry.Providers; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Services +{ + /// + /// Tests covering the SectionService + /// + [TestFixture] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] + public class TelemetryProviderTests : UmbracoIntegrationTest + { + private IContentTypeService ContentTypeService => GetRequiredService(); + + private IFileService FileService => GetRequiredService(); + + private IDomainService DomainService => GetRequiredService(); + + private IContentService ContentService => GetRequiredService(); + + private DomainTelemetryProvider DetailedTelemetryProviders => GetRequiredService(); + + private ContentTelemetryProvider ContentTelemetryProvider => GetRequiredService(); + + private LanguagesTelemetryProvider LanguagesTelemetryProvider => GetRequiredService(); + + private UserTelemetryProvider UserTelemetryProvider => GetRequiredService(); + + private MacroTelemetryProvider MacroTelemetryProvider => GetRequiredService(); + + private MediaTelemetryProvider MediaTelemetryProvider => GetRequiredService(); + + private PropertyEditorTelemetryProvider PropertyEditorTelemetryProvider => GetRequiredService(); + + private ILocalizationService LocalizationService => GetRequiredService(); + + private IUserService UserService => GetRequiredService(); + + private IMediaService MediaService => GetRequiredService(); + + private IMediaTypeService MediaTypeService => GetRequiredService(); + + private LanguageBuilder _languageBuilder = new(); + + private UserBuilder _userBuilder = new(); + + private UserGroupBuilder _userGroupBuilder = new(); + + private ContentTypeBuilder _contentTypeBuilder = new (); + + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + builder.Services.AddTransient(); + base.CustomTestSetup(builder); + } + + [Test] + public void Domain_Telemetry_Provider_Can_Get_Domains() + { + // Arrange + DomainService.Save(new UmbracoDomain("danish", "da-DK")); + + IEnumerable result = null; + // Act + result = DetailedTelemetryProviders.GetInformation(); + + + // Assert + Assert.AreEqual(1, result.First().Data); + } + + [Test] + public void SectionService_Can_Get_Allowed_Sections_For_User() + { + // Arrange + Template template = TemplateBuilder.CreateTextPageTemplate(); + FileService.SaveTemplate(template); + + ContentType contentType = ContentTypeBuilder.CreateTextPageContentType(defaultTemplateId: template.Id); + ContentTypeService.Save(contentType); + + Content blueprint = ContentBuilder.CreateTextpageContent(contentType, "hello", Constants.System.Root); + blueprint.SetValue("title", "blueprint 1"); + blueprint.SetValue("bodyText", "blueprint 2"); + blueprint.SetValue("keywords", "blueprint 3"); + blueprint.SetValue("description", "blueprint 4"); + + ContentService.SaveBlueprint(blueprint); + + IContent fromBlueprint = ContentService.CreateContentFromBlueprint(blueprint, "My test content"); + ContentService.Save(fromBlueprint); + + IEnumerable result = null; + // Act + result = ContentTelemetryProvider.GetInformation(); + + + // Assert + // TODO : Test multiple roots, with children + grandchildren + Assert.AreEqual(1, result.First().Data); + } + + [Test] + public void Language_Telemetry_Can_Get_Languages() + { + // Arrange + var langTwo = _languageBuilder.WithCultureInfo("da-DK").Build(); + var langThree = _languageBuilder.WithCultureInfo("se-SV").Build(); + + LocalizationService.Save(langTwo); + LocalizationService.Save(langThree); + + IEnumerable result = null; + + // Act + result = LanguagesTelemetryProvider.GetInformation(); + + // Assert + Assert.AreEqual(3, result.First().Data); + } + + [Test] + public void MacroTelemetry_Can_Get_Macros() + { + BuildMacros(); + var result = MacroTelemetryProvider.GetInformation().FirstOrDefault(x => x.Name == Constants.Telemetry.MacroCount); + Assert.AreEqual(3, result.Data); + } + + [Test] + public void MediaTelemetry_Can_Get_Media_In_Folders() + { + IMediaType folderType = MediaTypeService.Get(1031); + IMediaType imageMediaType = MediaTypeService.Get(1032); + + Media root = MediaBuilder.CreateMediaFolder(folderType, -1); + MediaService.Save(root); + int createdMediaCount = 10; + for (int i = 0; i < createdMediaCount; i++) + { + Media c1 = MediaBuilder.CreateMediaImage(imageMediaType, root.Id); + MediaService.Save(c1); + } + + var result = MediaTelemetryProvider.GetInformation().FirstOrDefault(x => x.Name == Constants.Telemetry.MediaCount); + Assert.AreEqual(createdMediaCount, result.Data); + } + + [Test] + public void MediaTelemetry_Can_Get_Media_In_Root() + { + IMediaType imageMediaType = MediaTypeService.Get(1032); + int createdMediaCount = 10; + for (int i = 0; i < createdMediaCount; i++) + { + Media c1 = MediaBuilder.CreateMediaImage(imageMediaType, -1); + MediaService.Save(c1); + } + + var result = MediaTelemetryProvider.GetInformation().FirstOrDefault(x => x.Name == Constants.Telemetry.MediaCount); + Assert.AreEqual(createdMediaCount, result.Data); + } + + [Test] + public void PropertyEditorTelemetry_Counts_Same_Editor_As_One() + { + ContentType ct2 = ContentTypeBuilder.CreateBasicContentType("ct2", "CT2", null); + AddPropType("title", -88, ct2); + ContentType ct3 = ContentTypeBuilder.CreateBasicContentType("ct3", "CT3", null); + AddPropType("title",-88, ct3); + ContentType ct5 = ContentTypeBuilder.CreateBasicContentType("ct5", "CT5", null); + AddPropType("blah", -88, ct5); + + ContentTypeService.Save(ct2); + ContentTypeService.Save(ct3); + ContentTypeService.Save(ct5); + + var properties = PropertyEditorTelemetryProvider.GetInformation().FirstOrDefault(x => x.Name == Constants.Telemetry.Properties); + var result = properties.Data as IEnumerable; + Assert.AreEqual(1, result?.Count()); + } + + [Test] + public void PropertyEditorTelemetry_Can_Get_All_PropertyTypes() + { + ContentType ct2 = ContentTypeBuilder.CreateBasicContentType("ct2", "CT2", null); + AddPropType("title", -88, ct2); + AddPropType("title",-99, ct2); + ContentType ct5 = ContentTypeBuilder.CreateBasicContentType("ct5", "CT5", null); + AddPropType("blah", -88, ct5); + + ContentTypeService.Save(ct2); + ContentTypeService.Save(ct5); + + var properties = PropertyEditorTelemetryProvider.GetInformation().FirstOrDefault(x => x.Name == Constants.Telemetry.Properties); + var result = properties.Data as IEnumerable; + Assert.AreEqual(2, result?.Count()); + } + + [Test] + public void UserTelemetry_Can_Get_Default_User() + { + var result = UserTelemetryProvider.GetInformation().FirstOrDefault(x => x.Name == Constants.Telemetry.UserCount); + + Assert.AreEqual(1, result.Data); + } + + [Test] + public void UserTelemetry_Can_Get_With_Saved_User() + { + var user = BuildUser(0); + + UserService.Save(user); + + var result = UserTelemetryProvider.GetInformation().FirstOrDefault(x => x.Name == Constants.Telemetry.UserCount); + + Assert.AreEqual(2, result.Data); + } + + [Test] + public void UserTelemetry_Can_Get_More_Users() + { + int totalUsers = 99; + var users = BuildUsers(totalUsers); + UserService.Save(users); + + var result = UserTelemetryProvider.GetInformation().FirstOrDefault(x => x.Name == Constants.Telemetry.UserCount); + + Assert.AreEqual(totalUsers + 1, result.Data); + } + + [Test] + public void UserTelemetry_Can_Get_Default_UserGroups() + { + var result = UserTelemetryProvider.GetInformation().FirstOrDefault(x => x.Name == Constants.Telemetry.UserGroupCount); + Assert.AreEqual(5, result.Data); + } + + [Test] + public void UserTelemetry_Can_Get_With_Saved_UserGroups() + { + var userGroup = BuildUserGroup("testGroup"); + + UserService.Save(userGroup); + var result = UserTelemetryProvider.GetInformation().FirstOrDefault(x => x.Name == Constants.Telemetry.UserGroupCount); + + Assert.AreEqual(6, result.Data); + } + + [Test] + public void UserTelemetry_Can_Get_More_UserGroups() + { + var userGroups = BuildUserGroups(100); + + + foreach (var userGroup in userGroups) + { + UserService.Save(userGroup); + } + + var result = UserTelemetryProvider.GetInformation().FirstOrDefault(x => x.Name == Constants.Telemetry.UserGroupCount); + + Assert.AreEqual(105, result.Data); + } + + private User BuildUser(int count) => + _userBuilder + .WithLogin($"username{count}", "test pass") + .WithName("Test" + count) + .WithEmail($"test{count}@test.com") + .Build(); + + private IEnumerable BuildUsers(int count) + { + for (int i = 0; i < count; i++) + { + yield return BuildUser(count); + } + } + + private IUserGroup BuildUserGroup(string alias) => + _userGroupBuilder + .WithAlias(alias) + .WithName(alias) + .WithAllowedSections(new List(){"A", "B"}) + .Build(); + + private IEnumerable BuildUserGroups(int count) + { + for (int i = 0; i < count; i++) + { + yield return BuildUserGroup(i.ToString()); + } + } + + private void BuildMacros() + { + IScopeProvider scopeProvider = ScopeProvider; + using (IScope scope = scopeProvider.CreateScope()) + { + var repository = new MacroRepository((IScopeAccessor)scopeProvider, AppCaches.Disabled, Mock.Of>(), ShortStringHelper); + + repository.Save(new Macro(ShortStringHelper, "test1", "Test1", "~/views/macropartials/test1.cshtml")); + repository.Save(new Macro(ShortStringHelper, "test2", "Test2", "~/views/macropartials/test2.cshtml")); + repository.Save(new Macro(ShortStringHelper, "test3", "Tet3", "~/views/macropartials/test3.cshtml")); + scope.Complete(); + } + } + + private void AddPropType(string alias, int dataTypeId, IContentType ct) + { + var contentCollection = new PropertyTypeCollection(true) + { + new PropertyType(ShortStringHelper, Constants.PropertyEditors.Aliases.TextBox, ValueStorageType.Ntext) { Alias = alias, Name = "Title", Description = string.Empty, Mandatory = false, SortOrder = 1, DataTypeId = dataTypeId }, + }; + var pg = new PropertyGroup(contentCollection) + { + Alias = "test", + Name = "test", + SortOrder = 1 + }; + ct.PropertyGroups.Add(pg); + } + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Telemetry/TelemetryServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Telemetry/TelemetryServiceTests.cs new file mode 100644 index 0000000000..e898ba49ce --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Telemetry/TelemetryServiceTests.cs @@ -0,0 +1,77 @@ +using System; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Telemetry; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.Telemetry +{ + + [TestFixture] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] + public class TelemetryServiceTests : UmbracoIntegrationTest + { + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + builder.Services.Configure(options => options.Id = Guid.NewGuid().ToString()); + } + + private ITelemetryService TelemetryService => GetRequiredService(); + private IMetricsConsentService MetricsConsentService => GetRequiredService(); + + [Test] + public void Expected_Detailed_Telemetry_Exists() + { + var expectedData = new string[] + { + Constants.Telemetry.RootCount, + Constants.Telemetry.DomainCount, + Constants.Telemetry.ExamineIndexCount, + Constants.Telemetry.LanguageCount, + Constants.Telemetry.MacroCount, + Constants.Telemetry.MediaCount, + Constants.Telemetry.MediaCount, + Constants.Telemetry.TemplateCount, + Constants.Telemetry.ContentCount, + Constants.Telemetry.DocumentTypeCount, + Constants.Telemetry.Properties, + Constants.Telemetry.UserCount, + Constants.Telemetry.UserGroupCount, + Constants.Telemetry.ServerOs, + Constants.Telemetry.ServerFramework, + Constants.Telemetry.OsLanguage, + Constants.Telemetry.WebServer, + Constants.Telemetry.ModelsBuilderMode, + Constants.Telemetry.CustomUmbracoPath, + Constants.Telemetry.AspEnvironment, + Constants.Telemetry.IsDebug, + Constants.Telemetry.DatabaseProvider, + }; + + MetricsConsentService.SetConsentLevel(TelemetryLevel.Detailed); + var success = TelemetryService.TryGetTelemetryReportData(out var telemetryReportData); + var detailed = telemetryReportData.Detailed.ToArray(); + + Assert.IsTrue(success); + Assert.Multiple(() => + { + Assert.IsNotNull(detailed); + Assert.AreEqual(expectedData.Length, detailed.Length); + + foreach (var expectedInfo in expectedData) + { + var expected = detailed.FirstOrDefault(x => x.Name == expectedInfo); + Assert.IsNotNull(expected, $"Expected {expectedInfo} to exists in the detailed list"); + } + }); + } + + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Security/BackOfficeUserStoreTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Security/BackOfficeUserStoreTests.cs index 3b719dc53a..fa52cde849 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Security/BackOfficeUserStoreTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Security/BackOfficeUserStoreTests.cs @@ -27,6 +27,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Security private IExternalLoginWithKeyService ExternalLoginService => GetRequiredService(); private IUmbracoMapper UmbracoMapper => GetRequiredService(); private ILocalizedTextService TextService => GetRequiredService(); + private ITwoFactorLoginService TwoFactorLoginService => GetRequiredService(); private BackOfficeUserStore GetUserStore() => new BackOfficeUserStore( @@ -37,7 +38,9 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Security new TestOptionsSnapshot(GlobalSettings), UmbracoMapper, new BackOfficeErrorDescriber(TextService), - AppCaches); + AppCaches, + TwoFactorLoginService + ); [Test] public async Task Can_Persist_Is_Approved() diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/UserDataServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/UserDataServiceTests.cs index 7417976369..3c638fb2ef 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/UserDataServiceTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Services/UserDataServiceTests.cs @@ -1,13 +1,20 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; using Moq; +using NPoco; using NUnit.Framework; using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Semver; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Telemetry.Providers; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services { @@ -86,10 +93,49 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services }); } - private UserDataService CreateUserDataService(string culture) + [Test] + [TestCase(ModelsMode.Nothing)] + [TestCase(ModelsMode.InMemoryAuto)] + [TestCase(ModelsMode.SourceCodeAuto)] + [TestCase(ModelsMode.SourceCodeManual)] + public void ReportsModelsModeCorrectly(ModelsMode modelsMode) + { + var userDataService = CreateUserDataService(modelsMode: modelsMode); + UserData[] userData = userDataService.GetUserData().ToArray(); + + var actual = userData.FirstOrDefault(x => x.Name == "Models Builder Mode"); + Assert.IsNotNull(actual?.Data); + Assert.AreEqual(modelsMode.ToString(), actual.Data); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public void ReportsDebugModeCorrectly(bool isDebug) + { + var userDataService = CreateUserDataService(isDebug: isDebug); + UserData[] userData = userDataService.GetUserData().ToArray(); + + var actual = userData.FirstOrDefault(x => x.Name == "Debug Mode"); + Assert.IsNotNull(actual?.Data); + Assert.AreEqual(isDebug.ToString(), actual.Data); + } + + private SystemInformationTelemetryProvider CreateUserDataService(string culture = "", ModelsMode modelsMode = ModelsMode.InMemoryAuto, bool isDebug = true) { var localizationService = CreateILocalizationService(culture); - return new UserDataService(_umbracoVersion, localizationService); + + var databaseMock = new Mock(); + databaseMock.Setup(x => x.DatabaseType.GetProviderName()).Returns("SQL"); + + return new SystemInformationTelemetryProvider( + _umbracoVersion, + localizationService, + Mock.Of>(x => x.CurrentValue == new ModelsBuilderSettings { ModelsMode = modelsMode }), + Mock.Of>(x => x.CurrentValue == new HostingSettings { Debug = isDebug }), + Mock.Of>(x => x.CurrentValue == new GlobalSettings()), + Mock.Of(), + Mock.Of(x=>x.CreateDatabase() == Mock.Of(y=>y.DatabaseType == DatabaseType.SQLite))); } private ILocalizationService CreateILocalizationService(string culture) diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/SystemInformationTelemetryProviderTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/SystemInformationTelemetryProviderTests.cs new file mode 100644 index 0000000000..42aae01281 --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/SystemInformationTelemetryProviderTests.cs @@ -0,0 +1,122 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Threading; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Moq; +using NPoco; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Configuration; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Telemetry.Providers; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Telemetry +{ + [TestFixture] + public class SystemInformationTelemetryProviderTests + { + [Test] + [TestCase(ModelsMode.Nothing)] + [TestCase(ModelsMode.InMemoryAuto)] + [TestCase(ModelsMode.SourceCodeAuto)] + [TestCase(ModelsMode.SourceCodeManual)] + public void ReportsModelsModeCorrectly(ModelsMode modelsMode) + { + var telemetryProvider = CreateProvider(modelsMode: modelsMode); + UsageInformation[] usageInformation = telemetryProvider.GetInformation().ToArray(); + + var actual = usageInformation.FirstOrDefault(x => x.Name == Constants.Telemetry.ModelsBuilderMode); + Assert.IsNotNull(actual?.Data); + Assert.AreEqual(modelsMode.ToString(), actual.Data); + } + + [Test] + [TestCase(true)] + [TestCase(false)] + public void ReportsDebugModeCorrectly(bool isDebug) + { + var telemetryProvider = CreateProvider(isDebug: isDebug); + UsageInformation[] usageInformation = telemetryProvider.GetInformation().ToArray(); + + var actual = usageInformation.FirstOrDefault(x => x.Name == Constants.Telemetry.IsDebug); + Assert.IsNotNull(actual?.Data); + Assert.AreEqual(isDebug, actual.Data); + } + + [Test] + [TestCase("en-US")] + [TestCase("de-DE")] + [TestCase("en-NZ")] + [TestCase("sv-SE")] + public void ReportsOsLanguageCorrectly(string culture) + { + Thread.CurrentThread.CurrentCulture = new CultureInfo(culture); + var telemetryProvider = CreateProvider(); + + UsageInformation[] usageInformation = telemetryProvider.GetInformation().ToArray(); + var actual = usageInformation.FirstOrDefault(x => x.Name == Constants.Telemetry.OsLanguage); + + Assert.NotNull(actual?.Data); + Assert.AreEqual(culture, actual.Data.ToString()); + } + + [Test] + [TestCase(GlobalSettings.StaticUmbracoPath, false)] + [TestCase("mycustompath", true)] + [TestCase("~/notUmbraco", true)] + [TestCase("/umbraco", true)] + [TestCase("umbraco", true)] + public void ReportsCustomUmbracoPathCorrectly(string path, bool isCustom) + { + var telemetryProvider = CreateProvider(umbracoPath: path); + + UsageInformation[] usageInformation = telemetryProvider.GetInformation().ToArray(); + var actual = usageInformation.FirstOrDefault(x => x.Name == Constants.Telemetry.CustomUmbracoPath); + + Assert.NotNull(actual?.Data); + Assert.AreEqual(isCustom, actual.Data); + } + + [Test] + [TestCase("Development")] + [TestCase("Staging")] + [TestCase("Production")] + public void ReportsCorrectAspEnvironment(string environment) + { + var telemetryProvider = CreateProvider(environment: environment); + + UsageInformation[] usageInformation = telemetryProvider.GetInformation().ToArray(); + var actual = usageInformation.FirstOrDefault(x => x.Name == Constants.Telemetry.AspEnvironment); + + Assert.NotNull(actual?.Data); + Assert.AreEqual(environment, actual.Data); + } + + private SystemInformationTelemetryProvider CreateProvider( + ModelsMode modelsMode = ModelsMode.InMemoryAuto, + bool isDebug = true, + string umbracoPath = "", + string environment = "") + { + var hostEnvironment = new Mock(); + hostEnvironment.Setup(x => x.EnvironmentName).Returns(environment); + + var databaseMock = new Mock(); + databaseMock.Setup(x => x.DatabaseType.GetProviderName()).Returns("SQL"); + + return new SystemInformationTelemetryProvider( + Mock.Of(), + Mock.Of(), + Mock.Of>(x => x.CurrentValue == new ModelsBuilderSettings{ ModelsMode = modelsMode }), + Mock.Of>(x => x.CurrentValue == new HostingSettings { Debug = isDebug }), + Mock.Of>(x => x.CurrentValue == new GlobalSettings{ UmbracoPath = umbracoPath }), + hostEnvironment.Object, + Mock.Of(x=>x.CreateDatabase() == Mock.Of(y=>y.DatabaseType == DatabaseType.SQLite))); + } + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/TelemetryServiceTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/TelemetryServiceTests.cs index 910ca7c792..67e046f1fa 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/TelemetryServiceTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Telemetry/TelemetryServiceTests.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; -using Microsoft.Extensions.Options; using Moq; using NUnit.Framework; using Umbraco.Cms.Core.Configuration; -using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Manifest; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Semver; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Telemetry; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Telemetry @@ -20,7 +20,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Telemetry { var version = CreateUmbracoVersion(9, 3, 1); var siteIdentifierServiceMock = new Mock(); - var sut = new TelemetryService(Mock.Of(), version, siteIdentifierServiceMock.Object); + var usageInformationServiceMock = new Mock(); + var sut = new TelemetryService(Mock.Of(), version, siteIdentifierServiceMock.Object, usageInformationServiceMock.Object, Mock.Of()); Guid guid; var result = sut.TryGetTelemetryReportData(out var telemetryReportData); @@ -31,7 +32,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Telemetry public void SkipsIfCantGetOrCreateId() { var version = CreateUmbracoVersion(9, 3, 1); - var sut = new TelemetryService(Mock.Of(), version, createSiteIdentifierService(false)); + var sut = new TelemetryService(Mock.Of(), version, createSiteIdentifierService(false), Mock.Of(), Mock.Of()); var result = sut.TryGetTelemetryReportData(out var telemetry); @@ -44,7 +45,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Telemetry { var version = CreateUmbracoVersion(9, 1, 1, "-rc", "-ad2f4k2d"); - var sut = new TelemetryService(Mock.Of(), version, createSiteIdentifierService()); + var metricsConsentService = new Mock(); + metricsConsentService.Setup(x => x.GetConsentLevel()).Returns(TelemetryLevel.Detailed); + var sut = new TelemetryService(Mock.Of(), version, createSiteIdentifierService(), Mock.Of(), metricsConsentService.Object); var result = sut.TryGetTelemetryReportData(out var telemetry); @@ -65,7 +68,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Telemetry new () { PackageName = noVersionPackageName } }; var manifestParser = CreateManifestParser(manifests); - var sut = new TelemetryService(manifestParser, version, createSiteIdentifierService()); + var metricsConsentService = new Mock(); + metricsConsentService.Setup(x => x.GetConsentLevel()).Returns(TelemetryLevel.Basic); + var sut = new TelemetryService(manifestParser, version, createSiteIdentifierService(), Mock.Of(), metricsConsentService.Object); var success = sut.TryGetTelemetryReportData(out var telemetry); @@ -93,7 +98,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Telemetry new () { PackageName = "TrackingAllowed", AllowPackageTelemetry = true }, }; var manifestParser = CreateManifestParser(manifests); - var sut = new TelemetryService(manifestParser, version, createSiteIdentifierService()); + var metricsConsentService = new Mock(); + metricsConsentService.Setup(x => x.GetConsentLevel()).Returns(TelemetryLevel.Basic); + var sut = new TelemetryService(manifestParser, version, createSiteIdentifierService(), Mock.Of(), metricsConsentService.Object); var success = sut.TryGetTelemetryReportData(out var telemetry); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj index ffdea855ce..64ae376541 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Tests.UnitTests.csproj @@ -3,15 +3,15 @@ Exe net6.0 - false Umbraco.Cms.Tests.UnitTests + false - + diff --git a/umbraco.sln b/umbraco.sln index b124ff573a..4ad2828396 100644 --- a/umbraco.sln +++ b/umbraco.sln @@ -15,7 +15,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs", "Docs", "{FD962632-1 ProjectSection(SolutionItems) = preProject .github\BUILD.md = .github\BUILD.md .github\CLEAR.md = .github\CLEAR.md - .github\CODE_OF_CONDUCT.md = .github\CODE_OF_CONDUCT.md .github\CONTRIBUTING.md = .github\CONTRIBUTING.md .github\CONTRIBUTING_DETAILED.md = .github\CONTRIBUTING_DETAILED.md .github\CONTRIBUTION_GUIDELINES.md = .github\CONTRIBUTION_GUIDELINES.md