Compare commits

...

140 Commits

Author SHA1 Message Date
Sebastiaan Janssen 5a07e34fb4 Bump version to 8.0.2 2019-04-26 15:15:17 +02:00
Stephan 1b5d614f5a Fix recycle bin moves
(cherry picked from commit 173df80e3d)

# Conflicts:
#	src/Umbraco.Core/Services/Implement/MediaService.cs
2019-04-26 15:03:09 +02:00
Sebastiaan Janssen abb08260eb v8: Imagepicker prevalue and grid setting/styles as infinite editor (#4971)
(cherry picked from commit f82dff597b)

# Conflicts:
#	src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js
2019-04-25 13:43:03 +02:00
Stephan 57c567c570 Fix paging error in NuCache
(cherry picked from commit 5d0010a993)
2019-04-23 14:08:19 +02:00
Stephan 424f4601ae Merge commit 'a2a247b413' into v8/8.0 2019-04-23 13:51:29 +02:00
Shannon a2a247b413 removes the enforcement of non duplicate property type aliases for now 2019-04-17 21:00:15 +10:00
Shannon 0b6a369047 When removing a property group, we need to first remove the group and then re-assign the property types so that duplicates are not added 2019-04-17 17:12:34 +10:00
Shannon 522fdefb8c Ensures that ALL membership properties are created by the installer, removes the logic that updates a membertype while fetching it, fixes the logic of mapping membership properties when fetching and they don't exist. 2019-04-17 16:42:17 +10:00
Shannon 4d8d10f07a adds comments, ignores test for now 2019-04-17 16:03:36 +10:00
Shannon 6f17129364 Make DropDownFlexibleConfiguration class public to allow configuration to be set when creating a new data type 2019-04-17 15:23:35 +10:00
Shannon 6f44e4fd09 Fixes INotifyCollectionChanged collections to raise the correct events, ensures that no duplicate property type aliases can be added/updated to 2019-04-16 17:37:42 +10:00
Shannon 02ddf80014 Adds a unit test 2019-04-16 15:39:28 +10:00
Shannon 723d9eddd1 unbreaks a ctor change preventing 8.0.2 from being dropped into the bin 2019-04-15 18:15:52 +10:00
Shannon fa655b812c Fix composers ordering 2019-04-15 17:55:22 +10:00
Shannon 576c10cd50 NuCache: fix vanishing content when refreshing content types 2019-04-15 12:56:45 +10:00
Kenn Jacobsen 179afe7e0d Add IsVisible extension on IPublishedElement 2019-04-11 17:44:16 +02:00
Shannon be7f42c454 Don't show the recycle bin in tree pickers/dialogs 2019-04-10 09:52:06 +10:00
Shannon 7e1ee26dee Fix spelling mistake 2019-04-09 10:34:59 +10:00
Stephan 1c9b449c46 Log more details about boot environment 2019-04-07 16:25:06 +02:00
Shannon e38095b727 NuCache: fix loading the media cache from local files, was missing media 2019-04-05 10:49:19 +11:00
Shannon b5796ad237 Enable configuration of NuCache BTree block size #5114 2019-04-05 10:44:36 +11:00
Stephan f985c437b5 Add content.SetValue overload for posted files 2019-04-04 09:28:34 +02:00
Andrew Deans 63fb8f5933 Fix case of query parameter 2019-04-04 09:12:40 +02:00
Shannon eb529783c4 Don't allow move in members listview 2019-04-03 16:22:13 +11:00
Shannon a8265db5f0 Reload listview after moving items 2019-04-03 10:45:04 +11:00
Shannon 7dcf2b1abb infinity editing dim-layer for umb-modelcolumn 2019-04-01 17:05:13 +11:00
Stephan 514452ddb9 Better ServerMessenger configuration 2019-04-01 16:30:37 +11:00
Stephan 1eb55a83c0 Merge branch 'patch-8.0.1' into v8/8.0 2019-03-30 12:13:12 +01:00
Stephan dfe6f2029a NuCache: better fixing, cleanup 2019-03-26 10:25:05 +01:00
Stephan 851c844c8b NuCache: fix panic exception 2019-03-26 10:25:05 +01:00
Stephan 7e413ed406 NuCache: troubleshooting 2019-03-26 10:25:05 +01:00
Stephan c56b8863ee NuCache: reorg some classes 2019-03-26 10:25:05 +01:00
Stephan 13a8548d3a NuCache: rename things 2019-03-26 10:25:05 +01:00
Stephan a70cb51559 NuCache: cleanup scoped objects 2019-03-26 10:25:05 +01:00
Kenn Jacobsen 8a56b3db16 Fix the "Language" dropdown in the "Culture and Hostnames" dialog 2019-03-26 20:10:50 +11:00
Bjarke Berg 4fcf2ce7bd Fixed issue when creating variant content templates, but the properties is invariant. Before this change an exception was thrown 2019-03-26 20:10:03 +11:00
Shannon 080ace90b2 Bugfix: https://github.com/umbraco/Umbraco-CMS/issues/4827 - Fixed MediaPickerValueConverter when config is Only Images 2019-03-26 20:09:12 +11:00
Shannon 2bfa81c707 fixes 4627 + makes front-end validations appear as soon as they are invalid again. 2019-03-26 20:07:42 +11:00
Sebastiaan Janssen 44cb5fd96b Apparently the highest minClientVersion allowed is 4.1.0 for now - see https://github.com/NuGet/NuGetGallery/blob/master/src/NuGetGallery/GalleryConstants.cs#L36 2019-03-19 09:24:41 +01:00
Robert b0b987f9cd Merge pull request #4710 from umbraco/temp8-4708-fix-for-exit-preview-with-null-culture
Fix for exit preview with null culture

(cherry picked from commit 9b25cdaa14)
2019-03-14 13:21:28 +01:00
Sebastiaan Janssen 3961e2533d Fix JS unit tests 2019-03-11 15:50:16 +01:00
Stephan b46f842400 Do not inject IEnumerable but Collection in UmbracoContextFactory
(cherry picked from commit 7e67bbea13)
2019-03-11 15:41:40 +01:00
Kenn Jacobsen 930acfeb68 Show the content app for media folders with content properties
(cherry picked from commit 9062b400d4)
2019-03-11 14:58:13 +01:00
Shannon b004c616ad Removes faulty CustomRouteUrlProvider
(cherry picked from commit 52beb01110)
2019-03-11 14:54:43 +01:00
Sebastiaan Janssen 9d64c4e551 Bugfix CoreRuntime
(cherry picked from commit 2209b37a4a and 7f31ddc250)
2019-03-11 14:45:48 +01:00
Sebastiaan Janssen 9a0aacd45a Upgrade to ModelsBuilder 8.0.2, support Custom Tool (#4881)
* Upgrade to ModelsBuilder 8.0.2, support Custom Tool

* Upgrade to ModelsBuilder 8.0.3

* Fix controllers

* Upgrade MB

* Fix previous commit

(cherry picked from commit c85c055424)
2019-03-11 14:37:59 +01:00
Kenn Jacobsen 29ff7bc340 Handle checkbox value conversion from Nested Content
(cherry picked from commit 2352b50d97)
2019-03-08 15:09:54 +01:00
Shannon 39a97fb768 Enable adding api controllers 2019-03-07 19:09:13 +11:00
Kenn Jacobsen 0852bd90d0 Always update URLs on save no matter which app is active
(cherry picked from commit 869d088e4b)
2019-03-06 17:26:36 +01:00
Niels Lyngsø e28365a0e5 hack for not using scope() in grid redordering
(cherry picked from commit a67163fd03)
2019-03-06 16:58:57 +01:00
Sebastiaan Janssen 0d16c79d0c Require higher NuGet version so we don't target VS15 any more (unsupported)
Making backups before we upgrade is no longer needed, removed from install.ps1
If people include the bin folder in their solution they will get errors upgrading now, we shouldn't need to try and fix this, there's no reason to include the bin folder in the solution - therefore install.core.ps1 is removed

(cherry picked from commit 82b78c7530)
2019-03-06 14:57:53 +01:00
Sebastiaan Janssen 7cc8983a25 Fix NuCache url segments
(cherry picked from commits 7a0f11bc6f and cfa94b1bb8)
2019-03-06 10:25:21 +01:00
Shannon e7f8baeca4 fixed correct destination url instead of #. 2019-03-05 16:14:06 +11:00
Stephan e716d14a0a Fix distributed cache handler for medias (#4751)
(cherry picked from commit 31b293aa4d)
2019-03-04 16:51:15 +01:00
Robert Foster 4c6a742007 Changed all references of umbracoUseSSL to Umbraco.Core.UseHttps to fix issue #4749
(cherry picked from commit 5aa38c1f43)
2019-03-04 15:33:08 +01:00
Sebastiaan Janssen 410241b61d 4766 fixed up other instances where nodeSelectHandler was being passed incorrect number of arguments
(cherry picked from commit 553bf6d304 and 4503967778)
2019-03-04 15:13:30 +01:00
Kenn Jacobsen 4e47fa9a70 Fix the "selected" checkmark in tree picker search results
(cherry picked from commit 09dc908332)
2019-03-04 15:10:55 +01:00
Robert Foster 9c5fb1fead Fix for Content App badges not being displayed (#4787)
(cherry picked from commit 055f983cbf)
2019-03-04 15:10:09 +01:00
Sebastiaan Janssen dc41db5fc1 Bumps version to 8.0.1 2019-03-04 15:08:23 +01:00
Sebastiaan Janssen 11b6a98df8 Bugfix exception during install, live models collision
(cherry picked from commit aa86dfa956)
2019-03-04 15:05:17 +01:00
Sebastiaan Janssen da9fc1e147 Make it possible to upload the same file twice or more (#4755)
(cherry picked from commit b5bda772af)
2019-03-04 15:04:27 +01:00
Sebastiaan Janssen 319c75757a Bugfix: Don't use ToString :'(
(cherry picked from commit ed6d54e64a)
2019-03-04 15:01:54 +01:00
Pete Duncanson 6e81e57c8e Removed System.Web.WebPages reference in nonodes.aspx as was failing a clean build
(cherry picked from commit f1d1c95709)
2019-03-04 14:58:05 +01:00
Sebastiaan Janssen 9ebe282ba6 Ensure we dont double insert
(cherry picked from commit 578eb8b32c and b86e7dc0e8)
2019-03-04 14:57:13 +01:00
Claus b8db430615 Revert "Fix UmbracoDatabaseFactory"
This reverts commit 716fc582a0.
2019-02-21 21:01:24 +01:00
Sebastiaan Janssen f9d51ba415 fix version 2019-02-21 19:32:11 +01:00
Stephan e07a17db29 Merge branch 'dev-v8' of https://github.com/umbraco/Umbraco-CMS into dev-v8 2019-02-21 19:17:01 +01:00
Stephan ad62983327 Introduce ILivePublishedModelFactory 2019-02-21 19:15:35 +01:00
Bjarke Berg fc8ae1b228 Merge remote-tracking branch 'origin/dev-v8' into dev-v8-fix 2019-02-21 16:24:31 +01:00
Stephan 48f27afbc9 Merge branch 'dev-v8' of https://github.com/umbraco/Umbraco-CMS into dev-v8 2019-02-21 16:23:20 +01:00
Stephan e1532f9215 Merge branch 'temp8-saving-event-cannot-publish-values' into dev-v8 2019-02-21 16:22:44 +01:00
Bjarke Berg a2279d03f6 Materializing the TypeContentSources before iterating them. 2019-02-21 16:13:21 +01:00
Bjarke Berg e52ff0ea03 merge fix 2019-02-21 14:57:41 +01:00
Bjarke Berg aaa8e881f8 Version number revert 2019-02-21 14:45:12 +01:00
Bjarke Berg 14ec6b8792 Merge branch 'dev-v8' into dev-v8-fix
# Conflicts:
#	src/Umbraco.Core/IO/ShadowWrapper.cs
2019-02-21 14:43:18 +01:00
Stephan 90feb463aa Cleanup 2019-02-21 14:13:37 +01:00
Stephan 73c4e734b3 Move to UniqueMediaPathScheme 2019-02-21 13:48:33 +01:00
Stephan c85b90d65d Combined Guids media path scheme 2019-02-21 13:48:14 +01:00
Stephan e923b940f9 Cleanup Url token encoding 2019-02-21 13:47:10 +01:00
Bjarke Berg d29d629eae Merge pull request #4688 from umbraco/temp8-4674-media-paths
Fix media paths length issues
2019-02-21 13:21:39 +01:00
Stephan 50b076ce38 Merge branch 'dev-v8' into temp8-saving-event-cannot-publish-values 2019-02-21 13:03:36 +01:00
Stephan badb55476b Merge branch 'dev-v8' of https://github.com/umbraco/Umbraco-CMS into dev-v8 2019-02-21 13:03:19 +01:00
Stephan 9ecd63eaed Move to UniqueMediaPathScheme 2019-02-21 12:20:10 +01:00
Stephan 5edd61107f Combined Guids media path scheme 2019-02-21 11:28:51 +01:00
Stephan c21039f1af Fix SqlServer bulk insert / unwrapping commands 2019-02-21 11:17:41 +01:00
Stephan 51f6c639bf Cleanup Url token encoding 2019-02-21 09:35:38 +01:00
Shannon d9a3fa443d fixes test 2019-02-21 15:29:36 +11:00
Shannon 13a1c02507 Fixes content service Saving events so that they are fired before the model's values are published when the service is publishing 2019-02-21 14:46:14 +11:00
Shannon 7a9bc19ffd Adds notes + unit test showing the problem 2019-02-21 12:08:00 +11:00
Stephan 3dfe653ef4 Better throwing on long path in filesystems 2019-02-20 17:04:29 +01:00
Stephan db4dc9006a Shorten filepaths 2019-02-20 17:04:29 +01:00
Stephan 212244b9f2 Hopefully fix PublishedMember 2019-02-20 17:04:29 +01:00
Stephan 12f7492d80 Reorg code 2019-02-20 17:04:29 +01:00
Claus d9d68cf6bf The content service is not returning the invalid properties when publishing fails
(cherry picked from commit d94e383ed7)
2019-02-20 16:17:39 +01:00
Claus d94e383ed7 The content service is not returning the invalid properties when publishing fails 2019-02-20 16:05:42 +01:00
Stephan 716fc582a0 Fix UmbracoDatabaseFactory 2019-02-20 15:52:21 +01:00
Stephan 50c9625cc6 Oops fix previous commit 2019-02-20 15:01:32 +01:00
Stephan 5cbc428590 Fix UserRepository 2019-02-20 15:00:23 +01:00
Stephan db4d88b2dc Shorten shadow filesystems identifiers 2019-02-20 14:10:15 +01:00
Stephan d9d54f7982 Merge branch 'temp8-fixes-published-content-children-method' into dev-v8
(cherry picked from commit 735d759208)
2019-02-20 13:36:10 +01:00
Shannon a61a2b4d97 Removes notes
(cherry picked from commit 717efb6b09)
2019-02-20 13:35:50 +01:00
Stephan 735d759208 Merge branch 'temp8-fixes-published-content-children-method' into dev-v8 2019-02-20 12:56:14 +01:00
Shannon 65f99a9bfd renames the method params to be consistent 2019-02-20 21:06:36 +11:00
Shannon 6c23066fb7 Fixes the Content.Children conundrum with ChildrenOfType method 2019-02-20 21:03:36 +11:00
Shannon 717efb6b09 Removes notes 2019-02-20 20:37:10 +11:00
Sebastiaan Janssen 91c52cffc8 And revert again 2019-02-19 15:57:40 +01:00
Sebastiaan Janssen 03e7779afb Update install screen 2019-02-19 15:57:06 +01:00
Sebastiaan Janssen 15562ec12f Bump version 2019-02-19 15:01:26 +01:00
Sebastiaan Janssen 6e8862954a Bump version 2019-02-19 15:00:58 +01:00
Warren Buckley 9c7ab96c92 Typo FilderedControllerFactory (#4650) 2019-02-19 14:23:26 +01:00
Stephan 49ca4bbc91 Fix MB in nuspec 2019-02-19 09:42:42 +01:00
Stephan 5121142760 Merge remote-tracking branch 'origin/temp8' into temp8 2019-02-19 08:48:59 +01:00
Stephan 4c53604bd0 Merge branch 'temp8-fixes-publishing-new-content' into temp8 2019-02-19 08:48:28 +01:00
Stephan bf329aa431 Merge branch 'temp8' into temp8-fixes-publishing-new-content 2019-02-19 08:45:54 +01:00
Bjarke Berg d456ee9a31 Merge pull request #4632 from umbraco/temp8-4438
Fixes 4438: Local variant picker squeezes the width of node-name-input, to make it scroll instead of disappear behind.
2019-02-19 08:36:33 +01:00
Stephan f06d720dbd Fix SqlServer detection 2019-02-19 08:12:48 +01:00
Bjarke Berg 87839f4f6a Merge pull request #4643 from umbraco/temp8-show-notification-msg-client-validation
v8 - Shows a notification message when client validation fails in the content editor, else it's not clear there are any problems
2019-02-19 07:54:55 +01:00
Shannon 02f8c4a106 Adds TODO note 2019-02-19 16:55:57 +11:00
Shannon 71ad7df787 adds code comments 2019-02-19 12:23:08 +11:00
Shannon 4b7520b6f8 Updates the validation warning for media and members 2019-02-19 12:06:55 +11:00
Shannon c06c996ccf Shows a notification message when client validation fails in the content editor, else it's not clear there are any problems. 2019-02-19 11:46:46 +11:00
Sebastiaan Janssen 9fe4d9c44e Make "add group" shortcut consistent 2019-02-18 22:00:25 +01:00
Carole Rennie Logan 560174a21f V8: Changing tab to group (#4391) 2019-02-18 21:44:32 +01:00
Kenn Jacobsen e34a0ec73b Update/fix the blur handling for the tags editor 2019-02-18 20:54:55 +01:00
Jan Skovgaard 152b318d78 v8: Remove open sans font (#4613) 2019-02-18 20:52:18 +01:00
BatJan 414c6b8788 Remove old preview functionality 2019-02-18 20:50:57 +01:00
Kenn Jacobsen 19fb1a55e3 Add missing dirty checks to mediatypes 2019-02-18 20:34:16 +01:00
Kenn Jacobsen b02a9c3e7a Add missing dirty checks to content type dirty checking 2019-02-18 20:22:19 +01:00
Shannon e6f8309d54 reverts the list view changes so they open normally 2019-02-18 17:01:34 +01:00
Shannon 3ecf4144e7 Fixes publishing new variant content that has validation errors 2019-02-19 02:51:05 +11:00
Sebastiaan Janssen 817dd21155 Allow for unselecting a color in the color picker 2019-02-18 16:42:05 +01:00
Sebastiaan Janssen 34216fd635 Create a valid date picker datatype 2019-02-18 16:16:31 +01:00
Shannon c81ff85b41 Merge branch 'kjac-v8-fix-dont-retain-cculture' into temp8 2019-02-19 01:52:26 +11:00
Shannon ca1ec0e4a3 Merge branch 'v8-fix-dont-retain-cculture' of https://github.com/kjac/Umbraco-CMS into kjac-v8-fix-dont-retain-cculture 2019-02-19 01:49:27 +11:00
Shannon f2ae41ada2 Fixes the culturerequest.interceptor so that it works in tests (and so circular deps aren't caused), internalizes ClientCulture and makes it a little less error prone. 2019-02-19 01:44:00 +11:00
Niels Lyngsø e1d990e6e8 Only show wrapper when it has content to show 2019-02-18 14:13:23 +01:00
Niels Lyngsø ce8b29bfcc Fixes #4438, makes variant-picker push the width of input, moed border of input to input-wrapper. 2019-02-18 13:35:16 +01:00
Kenn Jacobsen 1fe1762724 Retain cculture in breadcrumbs 2019-02-14 15:02:54 +01:00
Kenn Jacobsen 65670ab539 Merge branch 'temp8' into v8-fix-dont-retain-cculture 2019-02-14 14:01:16 +01:00
Kenn Jacobsen 2ab3889b23 Don't retain cculture when navigating away from the current content 2019-02-07 18:15:21 +01:00
219 changed files with 3341 additions and 2073 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata minClientVersion="3.4.4">
<metadata minClientVersion="4.1.0">
<id>UmbracoCms.Core</id>
<version>8.0.0</version>
<title>Umbraco Cms Core Binaries</title>
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata minClientVersion="3.4.4">
<metadata minClientVersion="4.1.0">
<id>UmbracoCms.Web</id>
<version>8.0.0</version>
<title>Umbraco Cms Core Binaries</title>
+2 -2
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata minClientVersion="3.4.4">
<metadata minClientVersion="4.1.0">
<id>UmbracoCms</id>
<version>8.0.0</version>
<title>Umbraco Cms</title>
@@ -25,7 +25,7 @@
not want this to happen as the alpha of the next major is, really, the next major already.
-->
<dependency id="Microsoft.AspNet.SignalR.Core" version="[2.4.0, 2.999999)" />
<dependency id="Umbraco.ModelsBuilder.Ui" version="[8.0.0-alpha.38]" />
<dependency id="Umbraco.ModelsBuilder.Ui" version="[8.0.4]" />
<dependency id="ImageProcessor.Web" version="[4.10.0.100,4.999999)" />
<dependency id="ImageProcessor.Web.Config" version="[2.5.0.100,2.999999)" />
<dependency id="Microsoft.CodeDom.Providers.DotNetCompilerPlatform" version="[2.0.1,2.999999)" />
+46 -36
View File
@@ -126,45 +126,55 @@
<dependentAssembly xdt:Locator="Condition(./_defaultNamespace:assemblyIdentity/@name='Microsoft.CodeAnalysis.CSharp')" xdt:Transform="Remove" />
<dependentAssembly xdt:Locator="Condition(./_defaultNamespace:assemblyIdentity/@name='log4net')" xdt:Transform="Remove" />
<dependentAssembly xdt:Locator="Condition(./_defaultNamespace:assemblyIdentity/@name='System.Data.SqlServerCe')" xdt:Transform="Remove" />
<dependentAssembly xdt:Locator="Condition(./_defaultNamespace:assemblyIdentity/@name='System.ValueTuple')" xdt:Transform="Remove" />
<dependentAssembly xdt:Locator="Condition(./_defaultNamespace:assemblyIdentity/@name='System.Net.Http.Formatting')" xdt:Transform="Remove" />
</assemblyBinding>
</runtime>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly xdt:Transform="Insert">
<assemblyIdentity name="Microsoft.Owin" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.0.0" newVersion="4.0.0.0" />
</dependentAssembly>
<dependentAssembly xdt:Transform="Insert">
<assemblyIdentity name="Microsoft.Owin.Security" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.0.0" newVersion="4.0.0.0" />
</dependentAssembly>
<dependentAssembly xdt:Transform="Insert">
<assemblyIdentity name="Microsoft.Owin.Security.Cookies" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.0.0" newVersion="4.0.0.0" />
</dependentAssembly>
<dependentAssembly xdt:Transform="Insert">
<assemblyIdentity name="Microsoft.Owin.Security.OAuth" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.0.0" newVersion="4.0.0.0" />
</dependentAssembly>
<dependentAssembly xdt:Transform="Insert">
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-11.0.0.0" newVersion="11.0.0.0" />
</dependentAssembly>
<dependentAssembly xdt:Transform="Insert">
<assemblyIdentity name="System.Collections.Immutable" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-1.2.1.0" newVersion="1.2.1.0" />
</dependentAssembly>
<dependentAssembly xdt:Transform="Insert">
<assemblyIdentity name="System.Web.Http" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-5.2.6.0" newVersion="5.2.6.0" />
</dependentAssembly>
<dependentAssembly xdt:Transform="Insert">
<assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-5.2.6.0" newVersion="5.2.6.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly xdt:Transform="Insert">
<assemblyIdentity name="Microsoft.Owin" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.1.0" newVersion="4.0.1.0" />
</dependentAssembly>
<dependentAssembly xdt:Transform="Insert">
<assemblyIdentity name="Microsoft.Owin.Security" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.1.0" newVersion="4.0.1.0" />
</dependentAssembly>
<dependentAssembly xdt:Transform="Insert">
<assemblyIdentity name="Microsoft.Owin.Security.Cookies" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.1.0" newVersion="4.0.1.0" />
</dependentAssembly>
<dependentAssembly xdt:Transform="Insert">
<assemblyIdentity name="Microsoft.Owin.Security.OAuth" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.1.0" newVersion="4.0.1.0" />
</dependentAssembly>
<dependentAssembly xdt:Transform="Insert">
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-12.0.0.0" newVersion="12.0.0.0" />
</dependentAssembly>
<dependentAssembly xdt:Transform="Insert">
<assemblyIdentity name="System.Collections.Immutable" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-1.2.3.0" newVersion="1.2.3.0" />
</dependentAssembly>
<dependentAssembly xdt:Transform="Insert">
<assemblyIdentity name="System.Web.Http" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-5.2.7.0" newVersion="5.2.7.0" />
</dependentAssembly>
<dependentAssembly xdt:Transform="Insert">
<assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-5.2.7.0" newVersion="5.2.7.0" />
</dependentAssembly>
<dependentAssembly xdt:Transform="Insert">
<assemblyIdentity name="System.ValueTuple" publicKeyToken="cc7b13ffcd2ddd51" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.3.0" newVersion="4.0.3.0" />
</dependentAssembly>
<dependentAssembly xdt:Transform="Insert">
<assemblyIdentity name="System.Net.Http.Formatting" publicKeyToken="31bf3856ad364e35" />
<bindingRedirect oldVersion="0.0.0.0-5.2.7.0" newVersion="5.2.7.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
<system.web.webPages.razor xdt:Transform="Remove" />
-93
View File
@@ -1,93 +0,0 @@
param($installPath, $toolsPath, $package, $project)
Write-Host "installPath:" "${installPath}"
Write-Host "toolsPath:" "${toolsPath}"
Write-Host " "
if ($project) {
$dateTime = Get-Date -Format yyyyMMdd-HHmmss
# Create paths and list them
$projectPath = (Get-Item $project.Properties.Item("FullPath").Value).FullName
Write-Host "projectPath:" "${projectPath}"
$backupPath = Join-Path $projectPath "App_Data\NuGetBackup\$dateTime"
Write-Host "backupPath:" "${backupPath}"
$copyLogsPath = Join-Path $backupPath "CopyLogs"
Write-Host "copyLogsPath:" "${copyLogsPath}"
$umbracoBinFolder = Join-Path $projectPath "bin"
Write-Host "umbracoBinFolder:" "${umbracoBinFolder}"
# Create backup folder and logs folder if it doesn't exist yet
New-Item -ItemType Directory -Force -Path $backupPath
New-Item -ItemType Directory -Force -Path $copyLogsPath
# After backing up, remove all umbraco dlls from bin folder in case dll files are included in the VS project
# See: http://issues.umbraco.org/issue/U4-4930
if(Test-Path $umbracoBinFolder) {
$umbracoBinBackupPath = Join-Path $backupPath "bin"
New-Item -ItemType Directory -Force -Path $umbracoBinBackupPath
robocopy $umbracoBinFolder $umbracoBinBackupPath /e /LOG:$copyLogsPath\UmbracoBinBackup.log
# Delete files Umbraco ships with
if(Test-Path $umbracoBinFolder\Microsoft.ApplicationBlocks.Data.dll) { Remove-Item $umbracoBinFolder\Microsoft.ApplicationBlocks.Data.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\System.Data.SqlServerCe.dll) { Remove-Item $umbracoBinFolder\System.Data.SqlServerCe.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\System.Data.SqlServerCe.Entity.dll) { Remove-Item $umbracoBinFolder\System.Data.SqlServerCe.Entity.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\Umbraco.Web.dll) { Remove-Item $umbracoBinFolder\Umbraco.Web.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\Umbraco.Core.dll) { Remove-Item $umbracoBinFolder\Umbraco.Core.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\Umbraco.ModelsBuilder.dll) { Remove-Item $umbracoBinFolder\Umbraco.ModelsBuilder.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\Umbraco.ModelsBuilder.AspNet.dll) { Remove-Item $umbracoBinFolder\Umbraco.ModelsBuilder.AspNet.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\Umbraco.Web.UI.dll) { Remove-Item $umbracoBinFolder\Umbraco.Web.UI.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\Umbraco.Examine.dll) { Remove-Item $umbracoBinFolder\Umbraco.Examine.dll -Force -Confirm:$false }
# Delete files Umbraco depends upon
$amd64Folder = Join-Path $umbracoBinFolder "amd64"
if(Test-Path $amd64Folder) { Remove-Item $amd64Folder -Force -Recurse -Confirm:$false }
$x86Folder = Join-Path $umbracoBinFolder "x86"
if(Test-Path $x86Folder) { Remove-Item $x86Folder -Force -Recurse -Confirm:$false }
if(Test-Path $umbracoBinFolder\AutoMapper.dll) { Remove-Item $umbracoBinFolder\AutoMapper.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\AutoMapper.Net4.dll) { Remove-Item $umbracoBinFolder\AutoMapper.Net4.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\ClientDependency.Core.dll) { Remove-Item $umbracoBinFolder\ClientDependency.Core.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\ClientDependency.Core.Mvc.dll) { Remove-Item $umbracoBinFolder\ClientDependency.Core.Mvc.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\CookComputing.XmlRpcV2.dll) { Remove-Item $umbracoBinFolder\CookComputing.XmlRpcV2.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\Examine.dll) { Remove-Item $umbracoBinFolder\Examine.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\HtmlAgilityPack.dll) { Remove-Item $umbracoBinFolder\HtmlAgilityPack.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\ICSharpCode.SharpZipLib.dll) { Remove-Item $umbracoBinFolder\ICSharpCode.SharpZipLib.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\ImageProcessor.dll) { Remove-Item $umbracoBinFolder\ImageProcessor.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\ImageProcessor.Web.dll) { Remove-Item $umbracoBinFolder\ImageProcessor.Web.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\Lucene.Net.dll) { Remove-Item $umbracoBinFolder\Lucene.Net.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\Microsoft.IO.RecyclableMemoryStream.dll) { Remove-Item $umbracoBinFolder\Microsoft.IO.RecyclableMemoryStream.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\Microsoft.AspNet.Identity.Core.dll) { Remove-Item $umbracoBinFolder\Microsoft.AspNet.Identity.Core.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\Microsoft.AspNet.Identity.Owin.dll) { Remove-Item $umbracoBinFolder\Microsoft.AspNet.Identity.Owin.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\Microsoft.CodeAnalysis.CSharp.dll) { Remove-Item $umbracoBinFolder\Microsoft.CodeAnalysis.CSharp.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\Microsoft.CodeAnalysis.dll) { Remove-Item $umbracoBinFolder\Microsoft.CodeAnalysis.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\Microsoft.Owin.dll) { Remove-Item $umbracoBinFolder\Microsoft.Owin.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\Microsoft.Owin.Host.SystemWeb.dll) { Remove-Item $umbracoBinFolder\Microsoft.Owin.Host.SystemWeb.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\Microsoft.Owin.Security.dll) { Remove-Item $umbracoBinFolder\Microsoft.Owin.Security.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\Microsoft.Owin.Security.Cookies.dll) { Remove-Item $umbracoBinFolder\Microsoft.Owin.Security.Cookies.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\Microsoft.Owin.Security.OAuth.dll) { Remove-Item $umbracoBinFolder\Microsoft.Owin.Security.OAuth.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\Microsoft.Web.Infrastructure.dll) { Remove-Item $umbracoBinFolder\Microsoft.Web.Infrastructure.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\Microsoft.Web.Helpers.dll) { Remove-Item $umbracoBinFolder\Microsoft.Web.Helpers.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\Microsoft.Web.Mvc.FixedDisplayModes.dll) { Remove-Item $umbracoBinFolder\Microsoft.Web.Mvc.FixedDisplayModes.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\MiniProfiler.dll) { Remove-Item $umbracoBinFolder\MiniProfiler.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\Newtonsoft.Json.dll) { Remove-Item $umbracoBinFolder\Newtonsoft.Json.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\Owin.dll) { Remove-Item $umbracoBinFolder\Owin.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\Semver.dll) { Remove-Item $umbracoBinFolder\Semver.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\System.Collections.Immutable.dll) { Remove-Item $umbracoBinFolder\System.Collections.Immutable.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\System.Reflection.Metadata.dll) { Remove-Item $umbracoBinFolder\System.Reflection.Metadata.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\System.Net.Http.Extensions.dll) { Remove-Item $umbracoBinFolder\System.Net.Http.Extensions.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\System.Net.Http.Formatting.dll) { Remove-Item $umbracoBinFolder\System.Net.Http.Formatting.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\System.Net.Http.Primitives.dll) { Remove-Item $umbracoBinFolder\System.Net.Http.Primitives.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\System.Web.Helpers.dll) { Remove-Item $umbracoBinFolder\System.Web.Helpers.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\System.Web.Http.dll) { Remove-Item $umbracoBinFolder\System.Web.Http.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\System.Web.Http.WebHost.dll) { Remove-Item $umbracoBinFolder\System.Web.Http.WebHost.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\System.Web.Mvc.dll) { Remove-Item $umbracoBinFolder\System.Web.Mvc.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\System.Web.Razor.dll) { Remove-Item $umbracoBinFolder\System.Web.Razor.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\System.Web.WebPages.dll) { Remove-Item $umbracoBinFolder\System.Web.WebPages.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\System.Web.WebPages.Deployment.dll) { Remove-Item $umbracoBinFolder\System.Web.WebPages.Deployment.dll -Force -Confirm:$false }
if(Test-Path $umbracoBinFolder\System.Web.WebPages.Razor.dll) { Remove-Item $umbracoBinFolder\System.Web.WebPages.Razor.dll -Force -Confirm:$false }
}
}
-27
View File
@@ -11,37 +11,15 @@ if ($project) {
# Create paths and list them
$projectPath = (Get-Item $project.Properties.Item("FullPath").Value).FullName
Write-Host "projectPath:" "${projectPath}"
$backupPath = Join-Path $projectPath "App_Data\NuGetBackup\$dateTime"
Write-Host "backupPath:" "${backupPath}"
$copyLogsPath = Join-Path $backupPath "CopyLogs"
Write-Host "copyLogsPath:" "${copyLogsPath}"
$webConfigSource = Join-Path $projectPath "Web.config"
Write-Host "webConfigSource:" "${webConfigSource}"
$configFolder = Join-Path $projectPath "Config"
Write-Host "configFolder:" "${configFolder}"
# Create backup folder and logs folder if it doesn't exist yet
New-Item -ItemType Directory -Force -Path $backupPath
New-Item -ItemType Directory -Force -Path $copyLogsPath
# Create a backup of original web.config
Copy-Item $webConfigSource $backupPath -Force
# Backup config files folder
if(Test-Path $configFolder) {
$umbracoBackupPath = Join-Path $backupPath "Config"
New-Item -ItemType Directory -Force -Path $umbracoBackupPath
robocopy $configFolder $umbracoBackupPath /e /LOG:$copyLogsPath\ConfigBackup.log
}
# Copy umbraco and umbraco_files from package to project folder
$umbracoFolder = Join-Path $projectPath "Umbraco"
New-Item -ItemType Directory -Force -Path $umbracoFolder
$umbracoFolderSource = Join-Path $installPath "UmbracoFiles\Umbraco"
$umbracoBackupPath = Join-Path $backupPath "Umbraco"
New-Item -ItemType Directory -Force -Path $umbracoBackupPath
robocopy $umbracoFolder $umbracoBackupPath /e /LOG:$copyLogsPath\UmbracoBackup.log
robocopy $umbracoFolderSource $umbracoFolder /is /it /e /xf UI.xml /LOG:$copyLogsPath\UmbracoCopy.log
$copyWebconfig = $true
@@ -100,11 +78,6 @@ if ($project) {
}
}
$installFolder = Join-Path $projectPath "Install"
if(Test-Path $installFolder) {
Remove-Item $installFolder -Force -Recurse -Confirm:$false
}
# Open appropriate readme
if($copyWebconfig -eq $true)
{
+2 -2
View File
@@ -18,5 +18,5 @@ using System.Resources;
[assembly: AssemblyVersion("8.0.0")]
// these are FYI and changed automatically
[assembly: AssemblyFileVersion("8.0.0")]
[assembly: AssemblyInformationalVersion("8.0.0-alpha.58")]
[assembly: AssemblyFileVersion("8.0.2")]
[assembly: AssemblyInformationalVersion("8.0.2")]
@@ -30,7 +30,7 @@ namespace Umbraco.Core.Composing
protected override IComponent CreateItem(IFactory factory, Type itemType)
{
using (_logger.DebugDuration<Composers>($"Creating {itemType.FullName}.", $"Created {itemType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds))
using (_logger.DebugDuration<ComponentCollectionBuilder>($"Creating {itemType.FullName}.", $"Created {itemType.FullName}.", thresholdMilliseconds: LogThresholdMilliseconds))
{
return base.CreateItem(factory, itemType);
}
+95 -38
View File
@@ -70,7 +70,23 @@ namespace Umbraco.Core.Composing
}
}
private IEnumerable<Type> PrepareComposerTypes()
internal IEnumerable<Type> PrepareComposerTypes()
{
var requirements = GetRequirements();
// only for debugging, this is verbose
//_logger.Debug<Composers>(GetComposersReport(requirements));
var sortedComposerTypes = SortComposers(requirements);
// bit verbose but should help for troubleshooting
//var text = "Ordered Composers: " + Environment.NewLine + string.Join(Environment.NewLine, sortedComposerTypes) + Environment.NewLine;
_logger.Debug<Composers>("Ordered Composers: {SortedComposerTypes}", sortedComposerTypes);
return sortedComposerTypes;
}
internal Dictionary<Type, List<Type>> GetRequirements(bool throwOnMissing = true)
{
// create a list, remove those that cannot be enabled due to runtime level
var composerTypeList = _composerTypes
@@ -89,25 +105,69 @@ namespace Umbraco.Core.Composing
// enable or disable composers
EnableDisableComposers(composerTypeList);
// sort the composers according to their dependencies
var requirements = new Dictionary<Type, List<Type>>();
foreach (var type in composerTypeList) requirements[type] = null;
foreach (var type in composerTypeList)
void GatherInterfaces<TAttribute>(Type type, Func<TAttribute, Type> getTypeInAttribute, HashSet<Type> iset, List<Type> set2)
where TAttribute : Attribute
{
GatherRequirementsFromRequireAttribute(type, composerTypeList, requirements);
GatherRequirementsFromRequiredByAttribute(type, composerTypeList, requirements);
foreach (var attribute in type.GetCustomAttributes<TAttribute>())
{
var typeInAttribute = getTypeInAttribute(attribute);
if (typeInAttribute != null && // if the attribute references a type ...
typeInAttribute.IsInterface && // ... which is an interface ...
typeof(IComposer).IsAssignableFrom(typeInAttribute) && // ... which implements IComposer ...
!iset.Contains(typeInAttribute)) // ... which is not already in the list
{
// add it to the new list
iset.Add(typeInAttribute);
set2.Add(typeInAttribute);
// add all its interfaces implementing IComposer
foreach (var i in typeInAttribute.GetInterfaces().Where(x => typeof(IComposer).IsAssignableFrom(x)))
{
iset.Add(i);
set2.Add(i);
}
}
}
}
// only for debugging, this is verbose
//_logger.Debug<Composers>(GetComposersReport(requirements));
// gather interfaces too
var interfaces = new HashSet<Type>(composerTypeList.SelectMany(x => x.GetInterfaces().Where(y => typeof(IComposer).IsAssignableFrom(y))));
composerTypeList.AddRange(interfaces);
var list1 = composerTypeList;
while (list1.Count > 0)
{
var list2 = new List<Type>();
foreach (var t in list1)
{
GatherInterfaces<ComposeAfterAttribute>(t, a => a.RequiredType, interfaces, list2);
GatherInterfaces<ComposeBeforeAttribute>(t, a => a.RequiringType, interfaces, list2);
}
composerTypeList.AddRange(list2);
list1 = list2;
}
// sort the composers according to their dependencies
var requirements = new Dictionary<Type, List<Type>>();
foreach (var type in composerTypeList)
requirements[type] = null;
foreach (var type in composerTypeList)
{
GatherRequirementsFromAfterAttribute(type, composerTypeList, requirements, throwOnMissing);
GatherRequirementsFromBeforeAttribute(type, composerTypeList, requirements);
}
return requirements;
}
internal IEnumerable<Type> SortComposers(Dictionary<Type, List<Type>> requirements)
{
// sort composers
var graph = new TopoGraph<Type, KeyValuePair<Type, List<Type>>>(kvp => kvp.Key, kvp => kvp.Value);
graph.AddItems(requirements);
List<Type> sortedComposerTypes;
try
{
sortedComposerTypes = graph.GetSortedItems().Select(x => x.Key).ToList();
sortedComposerTypes = graph.GetSortedItems().Select(x => x.Key).Where(x => !x.IsInterface).ToList();
}
catch (Exception e)
{
@@ -117,40 +177,37 @@ namespace Umbraco.Core.Composing
throw;
}
// bit verbose but should help for troubleshooting
//var text = "Ordered Composers: " + Environment.NewLine + string.Join(Environment.NewLine, sortedComposerTypes) + Environment.NewLine;
_logger.Debug<Composers>("Ordered Composers: {SortedComposerTypes}", sortedComposerTypes);
return sortedComposerTypes;
}
private static string GetComposersReport(Dictionary<Type, List<Type>> requirements)
internal static string GetComposersReport(Dictionary<Type, List<Type>> requirements)
{
var text = new StringBuilder();
text.AppendLine("Composers & Dependencies:");
text.AppendLine(" < compose before");
text.AppendLine(" > compose after");
text.AppendLine(" : implements");
text.AppendLine(" = depends");
text.AppendLine();
bool HasReq(IEnumerable<Type> types, Type type)
=> types.Any(x => type.IsAssignableFrom(x) && !x.IsInterface);
foreach (var kvp in requirements)
{
var type = kvp.Key;
text.AppendLine(type.FullName);
foreach (var attribute in type.GetCustomAttributes<ComposeAfterAttribute>())
text.AppendLine(" -> " + attribute.RequiredType + (attribute.Weak.HasValue
? (attribute.Weak.Value ? " (weak)" : (" (strong" + (requirements.ContainsKey(attribute.RequiredType) ? ", missing" : "") + ")"))
: ""));
foreach (var attribute in type.GetCustomAttributes<ComposeBeforeAttribute>())
text.AppendLine(" -< " + attribute.RequiringType);
foreach (var i in type.GetInterfaces())
{
text.AppendLine(" : " + i.FullName);
foreach (var attribute in i.GetCustomAttributes<ComposeAfterAttribute>())
text.AppendLine(" -> " + attribute.RequiredType + (attribute.Weak.HasValue
? (attribute.Weak.Value ? " (weak)" : (" (strong" + (requirements.ContainsKey(attribute.RequiredType) ? ", missing" : "") + ")"))
: ""));
foreach (var attribute in i.GetCustomAttributes<ComposeBeforeAttribute>())
text.AppendLine(" -< " + attribute.RequiringType);
var weak = !(attribute.RequiredType.IsInterface ? attribute.Weak == false : attribute.Weak != true);
text.AppendLine(" > " + attribute.RequiredType +
(weak ? " (weak" : " (strong") + (HasReq(requirements.Keys, attribute.RequiredType) ? ", found" : ", missing") + ")");
}
foreach (var attribute in type.GetCustomAttributes<ComposeBeforeAttribute>())
text.AppendLine(" < " + attribute.RequiringType);
foreach (var i in type.GetInterfaces())
text.AppendLine(" : " + i.FullName);
if (kvp.Value != null)
foreach (var t in kvp.Value)
text.AppendLine(" = " + t);
@@ -221,16 +278,16 @@ namespace Umbraco.Core.Composing
types.Remove(kvp.Key);
}
private static void GatherRequirementsFromRequireAttribute(Type type, ICollection<Type> types, IDictionary<Type, List<Type>> requirements)
private static void GatherRequirementsFromAfterAttribute(Type type, ICollection<Type> types, IDictionary<Type, List<Type>> requirements, bool throwOnMissing = true)
{
// get 'require' attributes
// these attributes are *not* inherited because we want to "custom-inherit" for interfaces only
var requireAttributes = type
var afterAttributes = type
.GetInterfaces().SelectMany(x => x.GetCustomAttributes<ComposeAfterAttribute>()) // those marking interfaces
.Concat(type.GetCustomAttributes<ComposeAfterAttribute>()); // those marking the composer
// what happens in case of conflicting attributes (different strong/weak for same type) is not specified.
foreach (var attr in requireAttributes)
foreach (var attr in afterAttributes)
{
if (attr.RequiredType == type) continue; // ignore self-requirements (+ exclude in implems, below)
@@ -238,13 +295,13 @@ namespace Umbraco.Core.Composing
// unless strong, and then require at least one enabled composer implementing that interface
if (attr.RequiredType.IsInterface)
{
var implems = types.Where(x => x != type && attr.RequiredType.IsAssignableFrom(x)).ToList();
var implems = types.Where(x => x != type && attr.RequiredType.IsAssignableFrom(x) && !x.IsInterface).ToList();
if (implems.Count > 0)
{
if (requirements[type] == null) requirements[type] = new List<Type>();
requirements[type].AddRange(implems);
}
else if (attr.Weak == false) // if explicitly set to !weak, is strong, else is weak
else if (attr.Weak == false && throwOnMissing) // if explicitly set to !weak, is strong, else is weak
throw new Exception($"Broken composer dependency: {type.FullName} -> {attr.RequiredType.FullName}.");
}
// requiring a class = require that the composer is enabled
@@ -256,28 +313,28 @@ namespace Umbraco.Core.Composing
if (requirements[type] == null) requirements[type] = new List<Type>();
requirements[type].Add(attr.RequiredType);
}
else if (attr.Weak != true) // if not explicitly set to weak, is strong
else if (attr.Weak != true && throwOnMissing) // if not explicitly set to weak, is strong
throw new Exception($"Broken composer dependency: {type.FullName} -> {attr.RequiredType.FullName}.");
}
}
}
private static void GatherRequirementsFromRequiredByAttribute(Type type, ICollection<Type> types, IDictionary<Type, List<Type>> requirements)
private static void GatherRequirementsFromBeforeAttribute(Type type, ICollection<Type> types, IDictionary<Type, List<Type>> requirements)
{
// get 'required' attributes
// these attributes are *not* inherited because we want to "custom-inherit" for interfaces only
var requiredAttributes = type
var beforeAttributes = type
.GetInterfaces().SelectMany(x => x.GetCustomAttributes<ComposeBeforeAttribute>()) // those marking interfaces
.Concat(type.GetCustomAttributes<ComposeBeforeAttribute>()); // those marking the composer
foreach (var attr in requiredAttributes)
foreach (var attr in beforeAttributes)
{
if (attr.RequiringType == type) continue; // ignore self-requirements (+ exclude in implems, below)
// required by an interface = by any enabled composer implementing this that interface
if (attr.RequiringType.IsInterface)
{
var implems = types.Where(x => x != type && attr.RequiringType.IsAssignableFrom(x)).ToList();
var implems = types.Where(x => x != type && attr.RequiringType.IsAssignableFrom(x) && !x.IsInterface).ToList();
foreach (var implem in implems)
{
if (requirements[implem] == null) requirements[implem] = new List<Type>();
@@ -79,7 +79,7 @@ namespace Umbraco.Core.Composing.CompositionExtensions
composition.RegisterUnique<IFileSystems>(factory => factory.GetInstance<IO.FileSystems>());
// register the scheme for media paths
composition.RegisterUnique<IMediaPathScheme, TwoGuidsMediaPathScheme>();
composition.RegisterUnique<IMediaPathScheme, UniqueMediaPathScheme>();
// register the IMediaFileSystem implementation
composition.RegisterFileSystem<IMediaFileSystem, MediaFileSystem>();
+1 -2
View File
@@ -8,7 +8,6 @@
/// <summary>
/// Compose.
/// </summary>
/// <param name="composition"></param>
void Compose(Composition composition);
}
}
}
+2 -4
View File
@@ -4,10 +4,8 @@
/// Represents a core <see cref="IComposer"/>.
/// </summary>
/// <remarks>
/// <para>All core composers are required by (compose before) all user composers,
/// and require (compose after) all runtime composers.</para>
/// <para>Core composers compose after the initial composer, and before user composers.</para>
/// </remarks>
[ComposeAfter(typeof(IRuntimeComposer))]
public interface ICoreComposer : IComposer
{ }
}
}
@@ -1,11 +0,0 @@
namespace Umbraco.Core.Composing
{
/// <summary>
/// Represents a runtime <see cref="IComposer"/>.
/// </summary>
/// <remarks>
/// <para>All runtime composers are required by (compose before) all core composers</para>
/// </remarks>
public interface IRuntimeComposer : IComposer
{ }
}
+2 -2
View File
@@ -4,9 +4,9 @@
/// Represents a user <see cref="IComposer"/>.
/// </summary>
/// <remarks>
/// <para>All user composers require (compose after) all core composers.</para>
/// <para>User composers compose after core composers, and before the final composer.</para>
/// </remarks>
[ComposeAfter(typeof(ICoreComposer))]
public interface IUserComposer : IComposer
{ }
}
}
@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
namespace Umbraco.Core.Composing
{
/// <summary>
/// Provides a base class for collections of types.
/// </summary>
public abstract class TypeCollectionBuilderBase<TCollection, TConstraint> : ICollectionBuilder<TCollection, Type>
where TCollection : class, IBuilderCollection<Type>
{
private readonly HashSet<Type> _types = new HashSet<Type>();
private Type Validate(Type type, string action)
{
if (!typeof(TConstraint).IsAssignableFrom(type))
throw new InvalidOperationException($"Cannot {action} type {type.FullName} as it does not inherit from/implement {typeof(TConstraint).FullName}.");
return type;
}
public void Add(Type type) => _types.Add(Validate(type, "add"));
public void Add<T>() => Add(typeof(T));
public void Add(IEnumerable<Type> types)
{
foreach (var type in types) Add(type);
}
public void Remove(Type type) => _types.Remove(Validate(type, "remove"));
public void Remove<T>() => Remove(typeof(T));
public TCollection CreateCollection(IFactory factory)
{
return factory.CreateInstance<TCollection>(_types);
}
public void RegisterWith(IRegister register)
{
register.Register(CreateCollection, Lifetime.Singleton);
}
}
}
@@ -18,46 +18,6 @@ namespace Umbraco.Core
/// </summary>
public static partial class CompositionExtensions
{
#region FileSystems
/// <summary>
/// Registers a filesystem.
/// </summary>
/// <typeparam name="TFileSystem">The type of the filesystem.</typeparam>
/// <typeparam name="TImplementing">The implementing type.</typeparam>
/// <param name="composition">The composition.</param>
/// <returns>The register.</returns>
public static void RegisterFileSystem<TFileSystem, TImplementing>(this Composition composition)
where TImplementing : FileSystemWrapper, TFileSystem
where TFileSystem : class
{
composition.RegisterUnique<TFileSystem>(factory =>
{
var fileSystems = factory.GetInstance<FileSystems>();
var supporting = factory.GetInstance<SupportingFileSystems>();
return fileSystems.GetFileSystem<TImplementing>(supporting.For<TFileSystem>());
});
}
/// <summary>
/// Registers a filesystem.
/// </summary>
/// <typeparam name="TFileSystem">The type of the filesystem.</typeparam>
/// <param name="composition">The composition.</param>
/// <returns>The register.</returns>
public static void RegisterFileSystem<TFileSystem>(this Composition composition)
where TFileSystem : FileSystemWrapper
{
composition.RegisterUnique(factory =>
{
var fileSystems = factory.GetInstance<FileSystems>();
var supporting = factory.GetInstance<SupportingFileSystems>();
return fileSystems.GetFileSystem<TFileSystem>(supporting.For<TFileSystem>());
});
}
#endregion
#region Collection Builders
/// <summary>
@@ -243,6 +203,28 @@ namespace Umbraco.Core
composition.RegisterUnique(_ => registrar);
}
/// <summary>
/// Sets the database server messenger options.
/// </summary>
/// <param name="composition">The composition.</param>
/// <param name="factory">A function creating the options.</param>
/// <remarks>Use DatabaseServerRegistrarAndMessengerComposer.GetDefaultOptions to get the options that Umbraco would use by default.</remarks>
public static void SetDatabaseServerMessengerOptions(this Composition composition, Func<IFactory, DatabaseServerMessengerOptions> factory)
{
composition.RegisterUnique(factory);
}
/// <summary>
/// Sets the database server messenger options.
/// </summary>
/// <param name="composition">The composition.</param>
/// <param name="options">Options.</param>
/// <remarks>Use DatabaseServerRegistrarAndMessengerComposer.GetDefaultOptions to get the options that Umbraco would use by default.</remarks>
public static void SetDatabaseServerMessengerOptions(this Composition composition, DatabaseServerMessengerOptions options)
{
composition.RegisterUnique(_ => options);
}
/// <summary>
/// Sets the short string helper.
/// </summary>
@@ -10,8 +10,6 @@ namespace Umbraco.Core
/// </summary>
public static partial class CompositionExtensions
{
#region Essentials
/// <summary>
/// Registers essential services.
/// </summary>
@@ -33,36 +31,5 @@ namespace Umbraco.Core
composition.RegisterUnique(typeLoader);
composition.RegisterUnique(state);
}
#endregion
#region Unique
/// <summary>
/// Registers a unique service as its own implementation.
/// </summary>
public static void RegisterUnique<TService>(this Composition composition)
=> composition.RegisterUnique(typeof(TService), typeof(TService));
/// <summary>
/// Registers a unique service with an implementation type.
/// </summary>
public static void RegisterUnique<TService, TImplementing>(this Composition composition)
=> composition.RegisterUnique(typeof(TService), typeof(TImplementing));
/// <summary>
/// Registers a unique service with an implementation type, for a target.
/// </summary>
public static void RegisterUniqueFor<TService, TTarget, TImplementing>(this Composition composition)
where TService : class
=> composition.RegisterUniqueFor<TService, TTarget>(typeof(TImplementing));
/// <summary>
/// Registers a unique service with an implementing instance.
/// </summary>
public static void RegisterUnique<TService>(this Composition composition, TService instance)
=> composition.RegisterUnique(typeof(TService), instance);
#endregion
}
}
@@ -0,0 +1,47 @@
using Umbraco.Core.Composing;
using Umbraco.Core.IO;
namespace Umbraco.Core
{
/// <summary>
/// Provides extension methods to the <see cref="Composition"/> class.
/// </summary>
public static partial class CompositionExtensions
{
/// <summary>
/// Registers a filesystem.
/// </summary>
/// <typeparam name="TFileSystem">The type of the filesystem.</typeparam>
/// <typeparam name="TImplementing">The implementing type.</typeparam>
/// <param name="composition">The composition.</param>
/// <returns>The register.</returns>
public static void RegisterFileSystem<TFileSystem, TImplementing>(this Composition composition)
where TImplementing : FileSystemWrapper, TFileSystem
where TFileSystem : class
{
composition.RegisterUnique<TFileSystem>(factory =>
{
var fileSystems = factory.GetInstance<FileSystems>();
var supporting = factory.GetInstance<SupportingFileSystems>();
return fileSystems.GetFileSystem<TImplementing>(supporting.For<TFileSystem>());
});
}
/// <summary>
/// Registers a filesystem.
/// </summary>
/// <typeparam name="TFileSystem">The type of the filesystem.</typeparam>
/// <param name="composition">The composition.</param>
/// <returns>The register.</returns>
public static void RegisterFileSystem<TFileSystem>(this Composition composition)
where TFileSystem : FileSystemWrapper
{
composition.RegisterUnique(factory =>
{
var fileSystems = factory.GetInstance<FileSystems>();
var supporting = factory.GetInstance<SupportingFileSystems>();
return fileSystems.GetFileSystem<TFileSystem>(supporting.For<TFileSystem>());
});
}
}
}
@@ -0,0 +1,35 @@
using Umbraco.Core.Composing;
namespace Umbraco.Core
{
/// <summary>
/// Provides extension methods to the <see cref="Composition"/> class.
/// </summary>
public static partial class CompositionExtensions
{
/// <summary>
/// Registers a unique service as its own implementation.
/// </summary>
public static void RegisterUnique<TService>(this Composition composition)
=> composition.RegisterUnique(typeof(TService), typeof(TService));
/// <summary>
/// Registers a unique service with an implementation type.
/// </summary>
public static void RegisterUnique<TService, TImplementing>(this Composition composition)
=> composition.RegisterUnique(typeof(TService), typeof(TImplementing));
/// <summary>
/// Registers a unique service with an implementation type, for a target.
/// </summary>
public static void RegisterUniqueFor<TService, TTarget, TImplementing>(this Composition composition)
where TService : class
=> composition.RegisterUniqueFor<TService, TTarget>(typeof(TImplementing));
/// <summary>
/// Registers a unique service with an implementing instance.
/// </summary>
public static void RegisterUnique<TService>(this Composition composition, TService instance)
=> composition.RegisterUnique(typeof(TService), instance);
}
}
@@ -128,6 +128,7 @@ namespace Umbraco.Core.Configuration
if (routes == null) throw new ArgumentNullException(nameof(routes));
//check if the current request matches a route, if so then it is reserved.
//TODO: This value should be cached! Else this is doing double routing in MVC every request!
var route = routes.GetRouteData(httpContext);
if (route != null)
return true;
+11 -5
View File
@@ -7,6 +7,7 @@ using System.Web;
using System.Xml.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NPoco.Expressions;
using Umbraco.Core.Composing;
using Umbraco.Core.IO;
using Umbraco.Core.Models;
@@ -52,8 +53,8 @@ namespace Umbraco.Core
return ContentStatus.Unpublished;
}
#endregion
/// <summary>
@@ -134,9 +135,14 @@ namespace Umbraco.Core
/// <summary>
/// Sets the posted file value of a property.
/// </summary>
/// <remarks>This really is for FileUpload fields only, and should be obsoleted. For anything else,
/// you need to store the file by yourself using Store and then figure out
/// how to deal with auto-fill properties (if any) and thumbnails (if any) by yourself.</remarks>
public static void SetValue(this IContentBase content, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, string propertyTypeAlias, string filename, HttpPostedFileBase postedFile, string culture = null, string segment = null)
{
content.SetValue(contentTypeBaseServiceProvider, propertyTypeAlias, postedFile.FileName, postedFile.InputStream, culture, segment);
}
/// <summary>
/// Sets the posted file value of a property.
/// </summary>
public static void SetValue(this IContentBase content, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider, string propertyTypeAlias, string filename, Stream filestream, string culture = null, string segment = null)
{
if (filename == null || filestream == null) return;
+17
View File
@@ -10,6 +10,23 @@ namespace Umbraco.Core
///</summary>
public static class EnumerableExtensions
{
internal static bool HasDuplicates<T>(this IEnumerable<T> items, bool includeNull)
{
var hs = new HashSet<T>();
foreach (var item in items)
{
if (item != null || includeNull)
{
if (!hs.Add(item))
{
return true;
}
}
}
return false;
}
/// <summary>
/// Wraps this object instance into an IEnumerable{T} consisting of a single item.
/// </summary>
+67
View File
@@ -40,5 +40,72 @@ namespace Umbraco.Core
public DecomposedGuid(Guid value) : this() => this.Value = value;
}
private static readonly char[] Base32Table =
{
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5'
};
/// <summary>
/// Converts a Guid into a base-32 string.
/// </summary>
/// <param name="guid">A Guid.</param>
/// <param name="length">The string length.</param>
/// <returns>A base-32 encoded string.</returns>
/// <remarks>
/// <para>A base-32 string representation of a Guid is the shortest, efficient, representation
/// that is case insensitive (base-64 is case sensitive).</para>
/// <para>Length must be 1-26, anything else becomes 26.</para>
/// </remarks>
public static string ToBase32String(Guid guid, int length = 26)
{
if (length <= 0 || length > 26)
length = 26;
var bytes = guid.ToByteArray(); // a Guid is 128 bits ie 16 bytes
// this could be optimized by making it unsafe,
// and fixing the table + bytes + chars (see Convert.ToBase64CharArray)
// each block of 5 bytes = 5*8 = 40 bits
// becomes 40 bits = 8*5 = 8 byte-32 chars
// a Guid is 3 blocks + 8 bits
// so it turns into a 3*8+2 = 26 chars string
var chars = new char[length];
var i = 0;
var j = 0;
while (i < 15)
{
if (j == length) break;
chars[j++] = Base32Table[(bytes[i] & 0b1111_1000) >> 3];
if (j == length) break;
chars[j++] = Base32Table[((bytes[i] & 0b0000_0111) << 2) | ((bytes[i + 1] & 0b1100_0000) >> 6)];
if (j == length) break;
chars[j++] = Base32Table[(bytes[i + 1] & 0b0011_1110) >> 1];
if (j == length) break;
chars[j++] = Base32Table[(bytes[i + 1] & 0b0000_0001) | ((bytes[i + 2] & 0b1111_0000) >> 4)];
if (j == length) break;
chars[j++] = Base32Table[((bytes[i + 2] & 0b0000_1111) << 1) | ((bytes[i + 3] & 0b1000_0000) >> 7)];
if (j == length) break;
chars[j++] = Base32Table[(bytes[i + 3] & 0b0111_1100) >> 2];
if (j == length) break;
chars[j++] = Base32Table[((bytes[i + 3] & 0b0000_0011) << 3) | ((bytes[i + 4] & 0b1110_0000) >> 5)];
if (j == length) break;
chars[j++] = Base32Table[bytes[i + 4] & 0b0001_1111];
i += 5;
}
if (j < length)
chars[j++] = Base32Table[(bytes[i] & 0b1111_1000) >> 3];
if (j < length)
chars[j] = Base32Table[(bytes[i] & 0b0000_0111) << 2];
return new string(chars);
}
}
}
+48 -18
View File
@@ -29,7 +29,7 @@ namespace Umbraco.Core.IO
// shadow support
private readonly List<ShadowWrapper> _shadowWrappers = new List<ShadowWrapper>();
private readonly object _shadowLocker = new object();
private static Guid _shadowCurrentId = Guid.Empty; // static - unique!!
private static string _shadowCurrentId; // static - unique!!
#region Constructor
// DI wants a public ctor
@@ -45,13 +45,13 @@ namespace Umbraco.Core.IO
_shadowWrappers.Clear();
_filesystems.Clear();
Volatile.Write(ref _wkfsInitialized, false);
_shadowCurrentId = Guid.Empty;
_shadowCurrentId = null;
}
// for tests only, totally unsafe
internal static void ResetShadowId()
{
_shadowCurrentId = Guid.Empty;
_shadowCurrentId = null;
}
// set by the scope provider when taking control of filesystems
@@ -126,11 +126,11 @@ namespace Umbraco.Core.IO
var scriptsFileSystem = new PhysicalFileSystem(SystemDirectories.Scripts);
var mvcViewsFileSystem = new PhysicalFileSystem(SystemDirectories.MvcViews);
_macroPartialFileSystem = new ShadowWrapper(macroPartialFileSystem, "Views/MacroPartials", IsScoped);
_partialViewsFileSystem = new ShadowWrapper(partialViewsFileSystem, "Views/Partials", IsScoped);
_macroPartialFileSystem = new ShadowWrapper(macroPartialFileSystem, "macro-partials", IsScoped);
_partialViewsFileSystem = new ShadowWrapper(partialViewsFileSystem, "partials", IsScoped);
_stylesheetsFileSystem = new ShadowWrapper(stylesheetsFileSystem, "css", IsScoped);
_scriptsFileSystem = new ShadowWrapper(scriptsFileSystem, "scripts", IsScoped);
_mvcViewsFileSystem = new ShadowWrapper(mvcViewsFileSystem, "Views", IsScoped);
_mvcViewsFileSystem = new ShadowWrapper(mvcViewsFileSystem, "views", IsScoped);
// TODO: do we need a lock here?
_shadowWrappers.Add(_macroPartialFileSystem);
@@ -146,6 +146,11 @@ namespace Umbraco.Core.IO
#region Providers
private readonly Dictionary<Type, string> _paths = new Dictionary<Type, string>();
// internal for tests
internal IReadOnlyDictionary<Type, string> Paths => _paths;
/// <summary>
/// Gets a strongly-typed filesystem.
/// </summary>
@@ -162,10 +167,33 @@ namespace Umbraco.Core.IO
return (TFileSystem) _filesystems.GetOrAdd(typeof(TFileSystem), _ => new Lazy<IFileSystem>(() =>
{
var name = typeof(TFileSystem).FullName;
if (name == null) throw new Exception("panic!");
var typeofTFileSystem = typeof(TFileSystem);
var shadowWrapper = CreateShadowWrapper(supporting, "typed/" + name);
// path must be unique and not collide with paths used in CreateWellKnownFileSystems
// for our well-known 'media' filesystem we can use the short 'media' path
// for others, put them under 'x/' and use ... something
string path;
if (typeofTFileSystem == typeof(MediaFileSystem))
{
path = "media";
}
else
{
lock (_paths)
{
if (!_paths.TryGetValue(typeofTFileSystem, out path))
{
path = Guid.NewGuid().ToString("N").Substring(0, 6);
while (_paths.ContainsValue(path)) // this can't loop forever, right?
path = Guid.NewGuid().ToString("N").Substring(0, 6);
_paths[typeofTFileSystem] = path;
}
}
path = "x/" + path;
}
var shadowWrapper = CreateShadowWrapper(supporting, path);
return _container.CreateInstance<TFileSystem>(shadowWrapper);
})).Value;
}
@@ -179,35 +207,37 @@ namespace Umbraco.Core.IO
// global shadow for the entire application, so great care should be taken to ensure that the
// application is *not* doing anything else when using a shadow.
internal ICompletable Shadow(Guid id)
internal ICompletable Shadow()
{
if (Volatile.Read(ref _wkfsInitialized) == false) EnsureWellKnownFileSystems();
var id = ShadowWrapper.CreateShadowId();
return new ShadowFileSystems(this, id); // will invoke BeginShadow and EndShadow
}
internal void BeginShadow(Guid id)
internal void BeginShadow(string id)
{
lock (_shadowLocker)
{
// if we throw here, it means that something very wrong happened.
if (_shadowCurrentId != Guid.Empty)
if (_shadowCurrentId != null)
throw new InvalidOperationException("Already shadowing.");
_shadowCurrentId = id;
_logger.Debug<ShadowFileSystems>("Shadow '{ShadowId}'", id);
_logger.Debug<ShadowFileSystems>("Shadow '{ShadowId}'", _shadowCurrentId);
foreach (var wrapper in _shadowWrappers)
wrapper.Shadow(id);
wrapper.Shadow(_shadowCurrentId);
}
}
internal void EndShadow(Guid id, bool completed)
internal void EndShadow(string id, bool completed)
{
lock (_shadowLocker)
{
// if we throw here, it means that something very wrong happened.
if (_shadowCurrentId == Guid.Empty)
if (_shadowCurrentId == null)
throw new InvalidOperationException("Not shadowing.");
if (id != _shadowCurrentId)
throw new InvalidOperationException("Not the current shadow.");
@@ -228,7 +258,7 @@ namespace Umbraco.Core.IO
}
}
_shadowCurrentId = Guid.Empty;
_shadowCurrentId = null;
if (exceptions.Count > 0)
throw new AggregateException(completed ? "Failed to apply all changes (see exceptions)." : "Failed to abort (see exceptions).", exceptions);
@@ -240,7 +270,7 @@ namespace Umbraco.Core.IO
lock (_shadowLocker)
{
var wrapper = new ShadowWrapper(filesystem, shadowPath, IsScoped);
if (_shadowCurrentId != Guid.Empty)
if (_shadowCurrentId != null)
wrapper.Shadow(_shadowCurrentId);
_shadowWrappers.Add(wrapper);
return wrapper;
@@ -7,16 +7,21 @@ namespace Umbraco.Core.IO.MediaPathSchemes
/// Implements a combined-guids media path scheme.
/// </summary>
/// <remarks>
/// <para>Path is "{combinedGuid}/{filename>}" where combinedGuid is a combination of itemGuid and propertyGuid.</para>
/// <para>Path is "{combinedGuid}/{filename}" where combinedGuid is a combination of itemGuid and propertyGuid.</para>
/// <para>This scheme is dangerous, as it does not prevent potential collisions.</para>
/// </remarks>
public class CombinedGuidsMediaPathScheme : IMediaPathScheme
{
private const int DirectoryLength = 8;
/// <inheritdoc />
public string GetFilePath(IMediaFileSystem fileSystem, Guid itemGuid, Guid propertyGuid, string filename, string previous = null)
{
// assumes that cuid and puid keys can be trusted - and that a single property type
// for a single content cannot store two different files with the same name
var directory = HexEncoder.Encode(GuidUtils.Combine(itemGuid, propertyGuid).ToByteArray()/*'/', 2, 4*/); // could use ext to fragment path eg 12/e4/f2/...
var combinedGuid = GuidUtils.Combine(itemGuid, propertyGuid);
var directory = GuidUtils.ToBase32String(combinedGuid, DirectoryLength); // see also HexEncoder, we may want to fragment path eg 12/e4/f3...
return Path.Combine(directory, filename).Replace('\\', '/');
}
@@ -0,0 +1,38 @@
using System;
using System.IO;
namespace Umbraco.Core.IO.MediaPathSchemes
{
/// <summary>
/// Implements a unique directory media path scheme.
/// </summary>
/// <remarks>
/// <para>This scheme provides short paths, yet handle potential collisions.</para>
/// </remarks>
public class UniqueMediaPathScheme : IMediaPathScheme
{
private const int DirectoryLength = 8;
/// <inheritdoc />
public string GetFilePath(IMediaFileSystem fileSystem, Guid itemGuid, Guid propertyGuid, string filename, string previous = null)
{
string directory;
// no point "combining" guids if all we want is some random guid - just get a new one
// and then, because we don't want collisions, ensure that the directory does not already exist
// (should be quite rare, but eh...)
do
{
var combinedGuid = Guid.NewGuid();
directory = GuidUtils.ToBase32String(combinedGuid, DirectoryLength); // see also HexEncoder, we may want to fragment path eg 12/e4/f3...
} while (fileSystem.DirectoryExists(directory));
return Path.Combine(directory, filename).Replace('\\', '/');
}
/// <inheritdoc />
public string GetDeleteDirectory(IMediaFileSystem fileSystem, string filepath) => Path.GetDirectoryName(filepath);
}
}
@@ -304,7 +304,13 @@ namespace Umbraco.Core.IO
// permissions to reach that path, but it may nevertheless be outside of
// our root path, due to relative segments, so better check
if (IOHelper.PathStartsWith(path, _rootPath, Path.DirectorySeparatorChar))
{
// this says that 4.7.2 supports long paths - but Windows does not
// https://docs.microsoft.com/en-us/dotnet/api/system.io.pathtoolongexception?view=netframework-4.7.2
if (path.Length > 260)
throw new PathTooLongException($"Path {path} is too long.");
return path;
}
// nothing prevents us to reach the file, security-wise, yet it is outside
// this filesystem's root - throw
+2 -2
View File
@@ -10,7 +10,7 @@ namespace Umbraco.Core.IO
private bool _completed;
// invoked by the filesystems when shadowing
public ShadowFileSystems(FileSystems fileSystems, Guid id)
public ShadowFileSystems(FileSystems fileSystems, string id)
{
_fileSystems = fileSystems;
Id = id;
@@ -19,7 +19,7 @@ namespace Umbraco.Core.IO
}
// for tests
public Guid Id { get; }
public string Id { get; }
// invoked by the scope when exiting, if completed
public void Complete()
+27 -1
View File
@@ -22,7 +22,33 @@ namespace Umbraco.Core.IO
_isScoped = isScoped;
}
internal void Shadow(Guid id)
public static string CreateShadowId()
{
const int retries = 50; // avoid infinite loop
const int idLength = 8; // 6 chars
// shorten a Guid to idLength chars, and see whether it collides
// with an existing directory or not - if it does, try again, and
// we should end up with a unique identifier eventually - but just
// detect infinite loops (just in case)
for (var i = 0; i < retries; i++)
{
var id = GuidUtils.ToBase32String(Guid.NewGuid(), idLength);
var virt = ShadowFsPath + "/" + id;
var shadowDir = IOHelper.MapPath(virt);
if (Directory.Exists(shadowDir))
continue;
Directory.CreateDirectory(shadowDir);
return id;
}
throw new Exception($"Could not get a shadow identifier (tried {retries} times)");
}
internal void Shadow(string id)
{
// note: no thread-safety here, because ShadowFs is thread-safe due to the check
// on ShadowFileSystemsScope.None - and if None is false then we should be running
@@ -226,6 +226,8 @@ namespace Umbraco.Core.Migrations.Install
_database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 32, UniqueId = 32.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastLockoutDate, Name = Constants.Conventions.Member.LastLockoutDateLabel, SortOrder = 4, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing });
_database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 33, UniqueId = 33.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastLoginDate, Name = Constants.Conventions.Member.LastLoginDateLabel, SortOrder = 5, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing });
_database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 34, UniqueId = 34.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = 11, Alias = Constants.Conventions.Member.LastPasswordChangeDate, Name = Constants.Conventions.Member.LastPasswordChangeDateLabel, SortOrder = 6, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte) ContentVariation.Nothing });
_database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 35, UniqueId = 35.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = null, Alias = Constants.Conventions.Member.PasswordQuestion, Name = Constants.Conventions.Member.PasswordQuestionLabel, SortOrder = 7, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing });
_database.Insert(Constants.DatabaseSchema.Tables.PropertyType, "id", false, new PropertyTypeDto { Id = 36, UniqueId = 36.ToGuid(), DataTypeId = Constants.DataTypes.LabelDateTime, ContentTypeId = 1044, PropertyTypeGroupId = null, Alias = Constants.Conventions.Member.PasswordAnswer, Name = Constants.Conventions.Member.PasswordAnswerLabel, SortOrder = 8, Mandatory = false, ValidationRegExp = null, Description = null, Variations = (byte)ContentVariation.Nothing });
}
@@ -281,7 +283,7 @@ namespace Umbraco.Core.Migrations.Install
_database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -37, EditorAlias = Constants.PropertyEditors.Aliases.ColorPicker, DbType = "Nvarchar" });
InsertDataTypeDto(Constants.DataTypes.DropDownSingle, Constants.PropertyEditors.Aliases.DropDownListFlexible, "Nvarchar", "{\"multiple\":false}");
_database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -40, EditorAlias = Constants.PropertyEditors.Aliases.RadioButtonList, DbType = "Nvarchar" });
_database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -41, EditorAlias = "Umbraco.Date", DbType = "Date" });
_database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -41, EditorAlias = "Umbraco.DateTime", DbType = "Date", Configuration = "{\"format\":\"YYYY-MM-DD\"}" });
InsertDataTypeDto(Constants.DataTypes.DropDownMultiple, Constants.PropertyEditors.Aliases.DropDownListFlexible, "Nvarchar", "{\"multiple\":true}");
_database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = -43, EditorAlias = Constants.PropertyEditors.Aliases.CheckBoxList, DbType = "Nvarchar" });
_database.Insert(Constants.DatabaseSchema.Tables.DataType, "pk", false, new DataTypeDto { NodeId = 1041, EditorAlias = Constants.PropertyEditors.Aliases.Tags, DbType = "Ntext",
@@ -5,6 +5,7 @@ using Umbraco.Core.Configuration;
using Umbraco.Core.Migrations.Upgrade.V_7_12_0;
using Umbraco.Core.Migrations.Upgrade.V_7_14_0;
using Umbraco.Core.Migrations.Upgrade.V_8_0_0;
using Umbraco.Core.Migrations.Upgrade.V_8_0_1;
namespace Umbraco.Core.Migrations.Upgrade
{
@@ -137,6 +138,8 @@ namespace Umbraco.Core.Migrations.Upgrade
To<RenameLabelAndRichTextPropertyEditorAliases>("{E0CBE54D-A84F-4A8F-9B13-900945FD7ED9}");
To<MergeDateAndDateTimePropertyEditor>("{78BAF571-90D0-4D28-8175-EF96316DA789}");
To<ChangeNuCacheJsonFormat>("{80C0A0CB-0DD5-4573-B000-C4B7C313C70D}");
//FINAL
@@ -167,7 +170,7 @@ namespace Umbraco.Core.Migrations.Upgrade
From("{init-7.12.4}").To("{init-7.10.0}"); // same as 7.12.0
From("{init-7.13.0}").To("{init-7.10.0}"); // same as 7.12.0
From("{init-7.13.1}").To("{init-7.10.0}"); // same as 7.12.0
// 7.14.0 has migrations, handle it...
// clone going from 7.10 to 1350617A (the last one before we started to merge 7.12 migrations), then
// clone going from CF51B39B (after 7.12 migrations) to 0009109C (the last one before we started to merge 7.12 migrations),
@@ -0,0 +1,16 @@
using Umbraco.Core.Migrations.PostMigrations;
namespace Umbraco.Core.Migrations.Upgrade.V_8_0_1
{
public class ChangeNuCacheJsonFormat : MigrationBase
{
public ChangeNuCacheJsonFormat(IMigrationContext context) : base(context)
{ }
public override void Migrate()
{
// nothing - just adding the post-migration
Context.AddPostMigration<RebuildPublishedSnapshot>();
}
}
}
@@ -1,7 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core.Collections;
using Umbraco.Core.Composing;
using Umbraco.Core.Exceptions;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
namespace Umbraco.Core.Models
{
@@ -101,35 +105,6 @@ namespace Umbraco.Core.Models
}
}
/// <summary>
/// Validates the content item's properties pass variant rules
/// </summary>
/// <para>If the content type is variant, then culture can be either '*' or an actual culture, but neither 'null' nor
/// 'empty'. If the content type is invariant, then culture can be either '*' or null or empty.</para>
public static Property[] ValidateProperties(this IContentBase content, string culture = "*")
{
// select invalid properties
return content.Properties.Where(x =>
{
// if culture is null, we validate invariant properties only
// if culture is '*' we validate both variant and invariant properties, automatically
// if culture is specific eg 'en-US' we both too, but explicitly
var varies = x.PropertyType.VariesByCulture();
if (culture == null)
return !(varies || x.IsValid(null)); // validate invariant property, invariant culture
if (culture == "*")
return !x.IsValid(culture); // validate property, all cultures
return varies
? !x.IsValid(culture) // validate variant property, explicit culture
: !x.IsValid(null); // validate invariant property, explicit culture
})
.ToArray();
}
public static void SetPublishInfo(this IContent content, string culture, string name, DateTime date)
{
if (string.IsNullOrWhiteSpace(name))
@@ -191,6 +166,13 @@ namespace Umbraco.Core.Models
content.CultureInfos.AddOrUpdate(culture, name, date);
}
/// <summary>
/// Sets the publishing values for names and properties.
/// </summary>
/// <param name="content"></param>
/// <param name="culture"></param>
/// <returns>A value indicating whether it was possible to publish the names and values for the specified
/// culture(s). The method may fail if required names are not set, but it does NOT validate property data</returns>
public static bool PublishCulture(this IContent content, string culture = "*")
{
culture = culture.NullOrWhiteSpaceAsNull();
@@ -201,10 +183,6 @@ namespace Umbraco.Core.Models
if (!content.ContentType.SupportsPropertyVariation(culture, "*", true))
throw new NotSupportedException($"Culture \"{culture}\" is not supported by content type \"{content.ContentType.Alias}\" with variation \"{content.ContentType.Variations}\".");
// the values we want to publish should be valid
if (content.ValidateProperties(culture).Any())
return false;
var alsoInvariant = false;
if (culture == "*") // all cultures
{
+2 -2
View File
@@ -13,7 +13,7 @@ namespace Umbraco.Core.Models
[DataContract(IsReference = true)]
public class ContentType : ContentTypeCompositionBase, IContentType
{
public const bool IsPublishingConst = true;
public const bool SupportsPublishingConst = true;
private int _defaultTemplate;
private IEnumerable<ITemplate> _allowedTemplates;
@@ -45,7 +45,7 @@ namespace Umbraco.Core.Models
public override ISimpleContentType ToSimple() => new SimpleContentType(this);
/// <inheritdoc />
public override bool IsPublishing => IsPublishingConst;
public override bool SupportsPublishing => SupportsPublishingConst;
//Custom comparer for enumerable
private static readonly DelegateEqualityComparer<IEnumerable<ITemplate>> TemplateComparer = new DelegateEqualityComparer<IEnumerable<ITemplate>>(
+24 -8
View File
@@ -40,7 +40,7 @@ namespace Umbraco.Core.Models
// actually OK as IsPublishing is constant
// ReSharper disable once VirtualMemberCallInConstructor
_noGroupPropertyTypes = new PropertyTypeCollection(IsPublishing);
_noGroupPropertyTypes = new PropertyTypeCollection(SupportsPublishing);
_noGroupPropertyTypes.CollectionChanged += PropertyTypesChanged;
_variations = ContentVariation.Nothing;
@@ -61,7 +61,7 @@ namespace Umbraco.Core.Models
// actually OK as IsPublishing is constant
// ReSharper disable once VirtualMemberCallInConstructor
_noGroupPropertyTypes = new PropertyTypeCollection(IsPublishing);
_noGroupPropertyTypes = new PropertyTypeCollection(SupportsPublishing);
_noGroupPropertyTypes.CollectionChanged += PropertyTypesChanged;
_variations = ContentVariation.Nothing;
@@ -70,7 +70,7 @@ namespace Umbraco.Core.Models
public abstract ISimpleContentType ToSimple();
/// <summary>
/// Gets a value indicating whether the content type is publishing.
/// Gets a value indicating whether the content type supports publishing.
/// </summary>
/// <remarks>
/// <para>A publishing content type supports draft and published values for properties.
@@ -80,7 +80,7 @@ namespace Umbraco.Core.Models
/// the draft or published value of a property returns the same thing, and publishing
/// a value property has no effect.</para>
/// </remarks>
public abstract bool IsPublishing { get; }
public abstract bool SupportsPublishing { get; }
//Custom comparer for enumerable
private static readonly DelegateEqualityComparer<IEnumerable<ContentTypeSort>> ContentTypeSortComparer =
@@ -95,6 +95,21 @@ namespace Umbraco.Core.Models
protected void PropertyTypesChanged(object sender, NotifyCollectionChangedEventArgs e)
{
//enable this to detect duplicate property aliases. We do want this, however making this change in a
//patch release might be a little dangerous
////detect if there are any duplicate aliases - this cannot be allowed
//if (e.Action == NotifyCollectionChangedAction.Add
// || e.Action == NotifyCollectionChangedAction.Replace)
//{
// var allAliases = _noGroupPropertyTypes.Concat(PropertyGroups.SelectMany(x => x.PropertyTypes)).Select(x => x.Alias);
// if (allAliases.HasDuplicates(false))
// {
// var newAliases = string.Join(", ", e.NewItems.Cast<PropertyType>().Select(x => x.Alias));
// throw new InvalidOperationException($"Other property types already exist with the aliases: {newAliases}");
// }
//}
OnPropertyChanged(nameof(PropertyTypes));
}
@@ -257,7 +272,7 @@ namespace Umbraco.Core.Models
{
if (_noGroupPropertyTypes != null)
_noGroupPropertyTypes.CollectionChanged -= PropertyTypesChanged;
_noGroupPropertyTypes = new PropertyTypeCollection(IsPublishing, value);
_noGroupPropertyTypes = new PropertyTypeCollection(SupportsPublishing, value);
_noGroupPropertyTypes.CollectionChanged += PropertyTypesChanged;
PropertyTypesChanged(_noGroupPropertyTypes, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
@@ -397,15 +412,16 @@ namespace Umbraco.Core.Models
var group = PropertyGroups[propertyGroupName];
if (group == null) return;
// re-assign the group's properties to no group
// first remove the group
PropertyGroups.RemoveItem(propertyGroupName);
// Then re-assign the group's properties to no group
foreach (var property in group.PropertyTypes)
{
property.PropertyGroupId = null;
_noGroupPropertyTypes.Add(property);
}
// actually remove the group
PropertyGroups.RemoveItem(propertyGroupName);
OnPropertyChanged(nameof(PropertyGroups));
}
@@ -226,7 +226,7 @@ namespace Umbraco.Core.Models
return null;
// create the new group
var group = new PropertyGroup(IsPublishing) { Name = name, SortOrder = 0 };
var group = new PropertyGroup(SupportsPublishing) { Name = name, SortOrder = 0 };
// check if it is inherited - there might be more than 1 but we want the 1st, to
// reuse its sort order - if there are more than 1 and they have different sort
+2 -2
View File
@@ -10,7 +10,7 @@ namespace Umbraco.Core.Models
[DataContract(IsReference = true)]
public class MediaType : ContentTypeCompositionBase, IMediaType
{
public const bool IsPublishingConst = false;
public const bool SupportsPublishingConst = false;
/// <summary>
/// Constuctor for creating a MediaType with the parent's id.
@@ -45,7 +45,7 @@ namespace Umbraco.Core.Models
public override ISimpleContentType ToSimple() => new SimpleContentType(this);
/// <inheritdoc />
public override bool IsPublishing => IsPublishingConst;
public override bool SupportsPublishing => SupportsPublishingConst;
/// <inheritdoc />
IMediaType IMediaType.DeepCloneWithResetIdentities(string newAlias) => (IMediaType)DeepCloneWithResetIdentities(newAlias);
+2 -2
View File
@@ -11,7 +11,7 @@ namespace Umbraco.Core.Models
[DataContract(IsReference = true)]
public class MemberType : ContentTypeCompositionBase, IMemberType
{
public const bool IsPublishingConst = false;
public const bool SupportsPublishingConst = false;
//Dictionary is divided into string: PropertyTypeAlias, Tuple: MemberCanEdit, VisibleOnProfile, PropertyTypeId
private string _alias;
@@ -35,7 +35,7 @@ namespace Umbraco.Core.Models
public override ISimpleContentType ToSimple() => new SimpleContentType(this);
/// <inheritdoc />
public override bool IsPublishing => IsPublishingConst;
public override bool SupportsPublishing => SupportsPublishingConst;
public override ContentVariation Variations
{
+4 -54
View File
@@ -184,7 +184,7 @@ namespace Umbraco.Core.Models
{
if (pvalue == null) return null;
return PropertyType.IsPublishing
return PropertyType.SupportsPublishing
? (published ? pvalue.PublishedValue : pvalue.EditedValue)
: pvalue.EditedValue;
}
@@ -244,7 +244,7 @@ namespace Umbraco.Core.Models
{
if (pvalue == null) return;
if (!PropertyType.IsPublishing)
if (!PropertyType.SupportsPublishing)
throw new NotSupportedException("Property type does not support publishing.");
var origValue = pvalue.PublishedValue;
pvalue.PublishedValue = PropertyType.ConvertAssignedValue(pvalue.EditedValue);
@@ -255,7 +255,7 @@ namespace Umbraco.Core.Models
{
if (pvalue == null) return;
if (!PropertyType.IsPublishing)
if (!PropertyType.SupportsPublishing)
throw new NotSupportedException("Property type does not support publishing.");
var origValue = pvalue.PublishedValue;
pvalue.PublishedValue = PropertyType.ConvertAssignedValue(null);
@@ -288,7 +288,7 @@ namespace Umbraco.Core.Models
{
var (pvalue, _) = GetPValue(culture, segment, true);
if (published && PropertyType.IsPublishing)
if (published && PropertyType.SupportsPublishing)
pvalue.PublishedValue = value;
else
pvalue.EditedValue = value;
@@ -332,56 +332,6 @@ namespace Umbraco.Core.Models
return (pvalue, change);
}
/// <summary>
/// Gets a value indicating whether the property has valid values.
/// </summary>
internal bool IsValid(string culture = "*", string segment = "*")
{
// TODO: validating values shouldn't be done here, this calls in to IsValidValue
culture = culture.NullOrWhiteSpaceAsNull();
segment = segment.NullOrWhiteSpaceAsNull();
// if validating invariant/neutral, and it is supported, validate
// (including ensuring that the value exists, if mandatory)
if ((culture == null || culture == "*") && (segment == null || segment == "*") && PropertyType.SupportsVariation(null, null))
if (!IsValidValue(_pvalue?.EditedValue))
return false;
// if validating only invariant/neutral, we are good
if (culture == null && segment == null)
return true;
// if nothing else to validate, we are good
if ((culture == null || culture == "*") && (segment == null || segment == "*") && !PropertyType.VariesByCulture())
return true;
// for anything else, validate the existing values (including mandatory),
// but we cannot validate mandatory globally (we don't know the possible cultures and segments)
if (_vvalues == null) return culture == "*" || IsValidValue(null);
var pvalues = _vvalues.Where(x =>
PropertyType.SupportsVariation(x.Value.Culture, x.Value.Segment, true) && // the value variation is ok
(culture == "*" || x.Value.Culture.InvariantEquals(culture)) && // the culture matches
(segment == "*" || x.Value.Segment.InvariantEquals(segment))) // the segment matches
.Select(x => x.Value)
.ToList();
return pvalues.Count == 0 || pvalues.All(x => IsValidValue(x.EditedValue));
}
/// <summary>
/// Boolean indicating whether the passed in value is valid
/// </summary>
/// <param name="value"></param>
/// <returns>True is property value is valid, otherwise false</returns>
private bool IsValidValue(object value)
{
// TODO: this shouldn't exist here, the model itself shouldn't be responsible for it's own validation and this requires singleton access
return PropertyType.IsPropertyValueValid(value);
}
protected override void PerformDeepClone(object clone)
{
base.PerformDeepClone(clone);
@@ -15,7 +15,7 @@ namespace Umbraco.Core.Models
public class PropertyCollection : KeyedCollection<string, Property>, INotifyCollectionChanged, IDeepCloneable
{
private readonly object _addLocker = new object();
internal Action OnAdd;
internal Func<Property, bool> AdditionValidator { get; set; }
/// <summary>
@@ -49,10 +49,12 @@ namespace Umbraco.Core.Models
/// </summary>
internal void Reset(IEnumerable<Property> properties)
{
//collection events will be raised in each of these calls
Clear();
//collection events will be raised in each of these calls
foreach (var property in properties)
Add(property);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
/// <summary>
@@ -60,8 +62,9 @@ namespace Umbraco.Core.Models
/// </summary>
protected override void SetItem(int index, Property property)
{
var oldItem = index >= 0 ? this[index] : property;
base.SetItem(index, property);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, property, index));
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, property, oldItem));
}
/// <summary>
@@ -120,10 +123,8 @@ namespace Umbraco.Core.Models
}
}
//collection events will be raised in InsertItem with Add
base.Add(property);
OnAdd?.Invoke();
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, property));
}
}
@@ -19,9 +19,6 @@ namespace Umbraco.Core.Models
{
private readonly ReaderWriterLockSlim _addLocker = new ReaderWriterLockSlim();
// TODO: this doesn't seem to be used anywhere
internal Action OnAdd;
internal PropertyGroupCollection()
{ }
@@ -37,16 +34,19 @@ namespace Umbraco.Core.Models
/// <remarks></remarks>
internal void Reset(IEnumerable<PropertyGroup> groups)
{
//collection events will be raised in each of these calls
Clear();
//collection events will be raised in each of these calls
foreach (var group in groups)
Add(group);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
protected override void SetItem(int index, PropertyGroup item)
{
var oldItem = index >= 0 ? this[index] : item;
base.SetItem(index, item);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, oldItem));
}
protected override void RemoveItem(int index)
@@ -84,6 +84,7 @@ namespace Umbraco.Core.Models
if (keyExists)
throw new Exception($"Naming conflict: Changing the name of PropertyGroup '{item.Name}' would result in duplicates");
//collection events will be raised in SetItem
SetItem(IndexOfKey(item.Id), item);
return;
}
@@ -96,16 +97,14 @@ namespace Umbraco.Core.Models
var exists = Contains(key);
if (exists)
{
//collection events will be raised in SetItem
SetItem(IndexOfKey(key), item);
return;
}
}
}
//collection events will be raised in InsertItem
base.Add(item);
OnAdd?.Invoke();
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item));
}
finally
{
+15 -14
View File
@@ -82,9 +82,22 @@ namespace Umbraco.Core.Models
}
/// <summary>
/// Gets a value indicating whether the content type, owning this property type, is publishing.
/// Gets a value indicating whether the content type owning this property type is publishing.
/// </summary>
public bool IsPublishing { get; internal set; }
/// <remarks>
/// <para>A publishing content type supports draft and published values for properties.
/// It is possible to retrieve either the draft (default) or published value of a property.
/// Setting the value always sets the draft value, which then needs to be published.</para>
/// <para>A non-publishing content type only supports one value for properties. Getting
/// the draft or published value of a property returns the same thing, and publishing
/// a value property has no effect.</para>
/// <para>When true, getting the property value returns the edited value by default, but
/// it is possible to get the published value using the appropriate 'published' method
/// parameter.</para>
/// <para>When false, getting the property value always return the edited value,
/// regardless of the 'published' method parameter.</para>
/// </remarks>
public bool SupportsPublishing { get; internal set; }
/// <summary>
/// Gets of sets the name of the property type.
@@ -355,18 +368,6 @@ namespace Umbraco.Core.Models
}
// TODO: this and other value validation methods should be a service level (not a model) thing. Changing this to internal for now
/// <summary>
/// Determines whether a value is valid for this property type.
/// </summary>
internal bool IsPropertyValueValid(object value)
{
var editor = Current.PropertyEditors[_propertyEditorAlias]; // TODO: inject
var configuration = Current.Services.DataTypeService.GetDataType(_dataTypeId).Configuration; // TODO: inject
var valueEditor = editor.GetValueEditor(configuration);
return !valueEditor.Validate(value, Mandatory, ValidationRegExp).Any();
}
/// <summary>
/// Sanitizes a property type alias.
/// </summary>
@@ -19,22 +19,19 @@ namespace Umbraco.Core.Models
[IgnoreDataMember]
private readonly ReaderWriterLockSlim _addLocker = new ReaderWriterLockSlim();
// TODO: This doesn't seem to be used
[IgnoreDataMember]
internal Action OnAdd;
internal PropertyTypeCollection(bool isPublishing)
internal PropertyTypeCollection(bool supportsPublishing)
{
IsPublishing = isPublishing;
SupportsPublishing = supportsPublishing;
}
public PropertyTypeCollection(bool isPublishing, IEnumerable<PropertyType> properties)
: this(isPublishing)
public PropertyTypeCollection(bool supportsPublishing, IEnumerable<PropertyType> properties)
: this(supportsPublishing)
{
Reset(properties);
}
public bool IsPublishing { get; }
public bool SupportsPublishing { get; }
/// <summary>
/// Resets the collection to only contain the <see cref="PropertyType"/> instances referenced in the <paramref name="properties"/> parameter.
@@ -43,43 +40,51 @@ namespace Umbraco.Core.Models
/// <remarks></remarks>
internal void Reset(IEnumerable<PropertyType> properties)
{
//collection events will be raised in each of these calls
Clear();
//collection events will be raised in each of these calls
foreach (var property in properties)
Add(property);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
Add(property);
}
protected override void SetItem(int index, PropertyType item)
{
item.IsPublishing = IsPublishing;
base.SetItem(index, item);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index));
item.SupportsPublishing = SupportsPublishing;
var oldItem = index >= 0 ? this[index] : item;
base.SetItem(index, item);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, oldItem));
item.PropertyChanged += Item_PropertyChanged;
}
protected override void RemoveItem(int index)
{
var removed = this[index];
base.RemoveItem(index);
removed.PropertyChanged -= Item_PropertyChanged;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed));
}
protected override void InsertItem(int index, PropertyType item)
{
item.IsPublishing = IsPublishing;
base.InsertItem(index, item);
item.SupportsPublishing = SupportsPublishing;
base.InsertItem(index, item);
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item));
item.PropertyChanged += Item_PropertyChanged;
}
protected override void ClearItems()
{
base.ClearItems();
foreach (var item in this)
item.PropertyChanged -= Item_PropertyChanged;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
// TODO: Instead of 'new' this should explicitly implement one of the collection interfaces members
internal new void Add(PropertyType item)
{
item.IsPublishing = IsPublishing;
item.SupportsPublishing = SupportsPublishing;
// TODO: this is not pretty and should be refactored
try
@@ -91,6 +96,7 @@ namespace Umbraco.Core.Models
var exists = Contains(key);
if (exists)
{
//collection events will be raised in SetItem
SetItem(IndexOfKey(key), item);
return;
}
@@ -103,10 +109,8 @@ namespace Umbraco.Core.Models
item.SortOrder = this.Max(x => x.SortOrder) + 1;
}
//collection events will be raised in InsertItem
base.Add(item);
OnAdd?.Invoke();
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item));
}
finally
{
@@ -115,6 +119,17 @@ namespace Umbraco.Core.Models
}
}
/// <summary>
/// Occurs when a property changes on a PropertyType that exists in this collection
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Item_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
var propType = (PropertyType)sender;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, propType, propType));
}
/// <summary>
/// Determines whether this collection contains a <see cref="Property"/> whose alias matches the specified PropertyType.
/// </summary>
@@ -155,7 +170,7 @@ namespace Umbraco.Core.Models
public object DeepClone()
{
var clone = new PropertyTypeCollection(IsPublishing);
var clone = new PropertyTypeCollection(SupportsPublishing);
foreach (var propertyType in this)
clone.Add((PropertyType) propertyType.DeepClone());
return clone;
@@ -0,0 +1,18 @@
namespace Umbraco.Core.Models.PublishedContent
{
/// <summary>
/// Provides a live published model creation service.
/// </summary>
public interface ILivePublishedModelFactory : IPublishedModelFactory
{
/// <summary>
/// Gets an object that can be used to synchronize access to the factory.
/// </summary>
object SyncRoot { get; }
/// <summary>
/// Refreshes the factory.
/// </summary>
void Refresh();
}
}
@@ -11,14 +11,14 @@ namespace Umbraco.Core.Models.PublishedContent
/// <summary>
/// Initializes a new instance of the <see cref="PublishedCultureInfo"/> class.
/// </summary>
public PublishedCultureInfo(string culture, string name, DateTime date)
public PublishedCultureInfo(string culture, string name, string urlSegment, DateTime date)
{
if (string.IsNullOrWhiteSpace(culture)) throw new ArgumentNullOrEmptyException(nameof(culture));
if (string.IsNullOrWhiteSpace(name)) throw new ArgumentNullOrEmptyException(nameof(name));
Culture = culture;
Name = name;
UrlSegment = name.ToUrlSegment(culture);
UrlSegment = urlSegment;
Date = date;
}
@@ -9,11 +9,10 @@ namespace Umbraco.Core.Persistence.Factories
{
internal static class MemberTypeReadOnlyFactory
{
public static IMemberType BuildEntity(MemberTypeReadOnlyDto dto, out bool needsSaving)
public static IMemberType BuildEntity(MemberTypeReadOnlyDto dto)
{
var standardPropertyTypes = Constants.Conventions.Member.GetStandardPropertyTypeStubs();
needsSaving = false;
var memberType = new MemberType(dto.ParentId);
try
@@ -41,27 +40,29 @@ namespace Umbraco.Core.Persistence.Factories
var propertyTypeGroupCollection = GetPropertyTypeGroupCollection(dto, memberType, standardPropertyTypes);
memberType.PropertyGroups = propertyTypeGroupCollection;
var propertyTypes = GetPropertyTypes(dto, memberType, standardPropertyTypes);
memberType.NoGroupPropertyTypes = GetNoGroupPropertyTypes(dto, memberType, standardPropertyTypes);
//By Convention we add 9 standard PropertyTypes - This is only here to support loading of types that didn't have these conventions before.
// By Convention we add 9 standard PropertyTypes - This is only here to support loading of types that didn't have these conventions before.
// In theory this should not happen! The only reason this did happen was because:
// A) we didn't install all of the default membership properties by default
// B) the existing data is super old when we didn't store membership properties at all (very very old)
// So what to do? We absolutely do not want to update the database when only a read was requested, this will cause problems
// so we should just add these virtual properties, they will have no ids but they will have aliases and this should be perfectly
// fine for any membership provider logic to work since neither membership providers or asp.net identity care about whether a property
// has an ID or not.
// When the member type is saved, all the properties will correctly be added.
//This will add this group if it doesn't exist, no need to error check here
memberType.AddPropertyGroup(Constants.Conventions.Member.StandardPropertiesGroupName);
foreach (var standardPropertyType in standardPropertyTypes)
{
if (dto.PropertyTypes.Any(x => x.Alias.Equals(standardPropertyType.Key))) continue;
//This will add the property if it doesn't exist, no need to error check here
memberType.AddPropertyType(standardPropertyType.Value, Constants.Conventions.Member.StandardPropertiesGroupName);
// beware!
// means that we can return a memberType "from database" that has some property types
// that do *not* come from the database and therefore are incomplete eg have no key,
// no id, no dataTypeDefinitionId - ouch! - better notify caller of the situation
needsSaving = true;
//Add the standard PropertyType to the current list
propertyTypes.Add(standardPropertyType.Value);
//Internal dictionary for adding "MemberCanEdit", "VisibleOnProfile", "IsSensitive" properties to each PropertyType
memberType.MemberTypePropertyTypes.Add(standardPropertyType.Key,
new MemberTypePropertyProfileAccess(false, false, false));
if (!memberType.MemberTypePropertyTypes.TryGetValue(standardPropertyType.Key, out var memberTypePropertyProfile))
memberType.MemberTypePropertyTypes[standardPropertyType.Key] = new MemberTypePropertyProfileAccess(false, false, false);
}
memberType.NoGroupPropertyTypes = propertyTypes;
return memberType;
}
@@ -78,7 +79,7 @@ namespace Umbraco.Core.Persistence.Factories
var propertyGroups = new PropertyGroupCollection();
foreach (var groupDto in dto.PropertyTypeGroups.Where(x => x.Id.HasValue))
{
var group = new PropertyGroup(MemberType.IsPublishingConst);
var group = new PropertyGroup(MemberType.SupportsPublishingConst);
// if the group is defined on the current member type,
// assign its identifier, else it will be zero
@@ -93,7 +94,7 @@ namespace Umbraco.Core.Persistence.Factories
group.Key = groupDto.UniqueId;
group.Name = groupDto.Text;
group.SortOrder = groupDto.SortOrder;
group.PropertyTypes = new PropertyTypeCollection(MemberType.IsPublishingConst);
group.PropertyTypes = new PropertyTypeCollection(MemberType.SupportsPublishingConst);
//Because we are likely to have a group with no PropertyTypes we need to ensure that these are excluded
var localGroupDto = groupDto;
@@ -147,13 +148,14 @@ namespace Umbraco.Core.Persistence.Factories
return propertyGroups;
}
private static List<PropertyType> GetPropertyTypes(MemberTypeReadOnlyDto dto, MemberType memberType, Dictionary<string, PropertyType> standardProps)
private static List<PropertyType> GetNoGroupPropertyTypes(MemberTypeReadOnlyDto dto, MemberType memberType, Dictionary<string, PropertyType> standardProps)
{
//Find PropertyTypes that does not belong to a PropertyTypeGroup
var propertyTypes = new List<PropertyType>();
foreach (var typeDto in dto.PropertyTypes.Where(x => (x.PropertyTypeGroupId.HasValue == false || x.PropertyTypeGroupId.Value == 0) && x.Id.HasValue))
{
//Internal dictionary for adding "MemberCanEdit" and "VisibleOnProfile" properties to each PropertyType
memberType.MemberTypePropertyTypes.Add(typeDto.Alias,
new MemberTypePropertyProfileAccess(typeDto.ViewOnProfile, typeDto.CanEdit, typeDto.IsSensitive));
@@ -121,7 +121,7 @@ namespace Umbraco.Core.Persistence.Factories
foreach (var property in properties)
{
if (property.PropertyType.IsPublishing)
if (property.PropertyType.SupportsPublishing)
{
//create the resulting hashset if it's not created and the entity varies by culture
if (entityVariesByCulture && editedCultures == null)
@@ -4,10 +4,7 @@ using System.Data;
using System.Data.SqlClient;
using System.Data.SqlServerCe;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NPoco;
using StackExchange.Profiling.Data;
using Umbraco.Core.Persistence.SqlSyntax;
namespace Umbraco.Core.Persistence
@@ -147,8 +147,23 @@ namespace Umbraco.Core.Persistence
private static TConnection GetTypedConnection<TConnection>(IDbConnection connection)
where TConnection : class, IDbConnection
{
var profiled = connection as ProfiledDbConnection;
return profiled == null ? connection as TConnection : profiled.WrappedConnection as TConnection;
var c = connection;
for (;;)
{
switch (c)
{
case TConnection ofType:
return ofType;
case RetryDbConnection retry:
c = retry.Inner;
break;
case ProfiledDbConnection profiled:
c = profiled.WrappedConnection;
break;
default:
throw new NotSupportedException(connection.GetType().FullName);
}
}
}
/// <summary>
@@ -160,8 +175,20 @@ namespace Umbraco.Core.Persistence
private static TTransaction GetTypedTransaction<TTransaction>(IDbTransaction transaction)
where TTransaction : class, IDbTransaction
{
var profiled = transaction as ProfiledDbTransaction;
return profiled == null ? transaction as TTransaction : profiled.WrappedTransaction as TTransaction;
var t = transaction;
for (;;)
{
switch (t)
{
case TTransaction ofType:
return ofType;
case ProfiledDbTransaction profiled:
t = profiled.WrappedTransaction;
break;
default:
throw new NotSupportedException(transaction.GetType().FullName);
}
}
}
/// <summary>
@@ -173,11 +200,23 @@ namespace Umbraco.Core.Persistence
private static TCommand GetTypedCommand<TCommand>(IDbCommand command)
where TCommand : class, IDbCommand
{
var faultHandling = command as FaultHandlingDbCommand;
if (faultHandling != null) command = faultHandling.Inner;
var profiled = command as ProfiledDbCommand;
if (profiled != null) command = profiled.InternalCommand;
return command as TCommand;
var c = command;
for (;;)
{
switch (c)
{
case TCommand ofType:
return ofType;
case FaultHandlingDbCommand faultHandling:
c = faultHandling.Inner;
break;
case ProfiledDbCommand profiled:
c = profiled.InternalCommand;
break;
default:
throw new NotSupportedException(command.GetType().FullName);
}
}
}
public static void TruncateTable(this IDatabase db, ISqlSyntaxProvider sqlSyntax, string tableName)
@@ -5,6 +5,7 @@ using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using NPoco;
using Umbraco.Core.Persistence.Querying;
@@ -1119,6 +1120,25 @@ namespace Umbraco.Core.Persistence
#endregion
#region Aliasing
internal static string GetAliasedField(this Sql<ISqlContext> sql, string field)
{
// get alias, if aliased
//
// regex looks for pattern "([\w+].[\w+]) AS ([\w+])" ie "(field) AS (alias)"
// and, if found & a group's field matches the field name, returns the alias
//
// so... if query contains "[umbracoNode].[nodeId] AS [umbracoNode__nodeId]"
// then GetAliased for "[umbracoNode].[nodeId]" returns "[umbracoNode__nodeId]"
var matches = sql.SqlContext.SqlSyntax.AliasRegex.Matches(sql.SQL);
var match = matches.Cast<Match>().FirstOrDefault(m => m.Groups[1].Value.InvariantEquals(field));
return match == null ? field : match.Groups[2].Value;
}
#endregion
#region Utilities
private static string[] GetColumns<TDto>(this Sql<ISqlContext> sql, string tableAlias = null, string referenceName = null, Expression<Func<TDto, object>>[] columnExpressions = null, bool withAlias = true)
@@ -25,7 +25,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
_templateRepository = templateRepository;
}
protected override bool IsPublishing => ContentType.IsPublishingConst;
protected override bool SupportsPublishing => ContentType.SupportsPublishingConst;
protected override IRepositoryCachePolicy<IContentType, int> CreateCachePolicy()
{
@@ -60,11 +60,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
if (ids.Any())
{
//NOTE: This logic should never be executed according to our cache policy
return ContentTypeQueryMapper.GetContentTypes(Database, SqlSyntax, IsPublishing, this, _templateRepository)
return ContentTypeQueryMapper.GetContentTypes(Database, SqlSyntax, SupportsPublishing, this, _templateRepository)
.Where(x => ids.Contains(x.Id));
}
return ContentTypeQueryMapper.GetContentTypes(Database, SqlSyntax, IsPublishing, this, _templateRepository);
return ContentTypeQueryMapper.GetContentTypes(Database, SqlSyntax, SupportsPublishing, this, _templateRepository);
}
protected override IEnumerable<IContentType> PerformGetAll(params Guid[] ids)
@@ -32,7 +32,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
: base(scopeAccessor, cache, logger)
{ }
protected abstract bool IsPublishing { get; }
protected abstract bool SupportsPublishing { get; }
public IEnumerable<MoveEventInfo<TEntity>> Move(TEntity moving, EntityContainer container)
{
@@ -1021,7 +1021,7 @@ AND umbracoNode.id <> @id",
var dtos = Database
.Fetch<PropertyTypeGroupDto>(sql);
var propertyGroups = PropertyGroupFactory.BuildEntity(dtos, IsPublishing, id, createDate, updateDate,CreatePropertyType);
var propertyGroups = PropertyGroupFactory.BuildEntity(dtos, SupportsPublishing, id, createDate, updateDate,CreatePropertyType);
return new PropertyGroupCollection(propertyGroups);
}
@@ -1057,7 +1057,7 @@ AND umbracoNode.id <> @id",
//Reset dirty properties
Parallel.ForEach(list, currentFile => currentFile.ResetDirtyProperties(false));
return new PropertyTypeCollection(IsPublishing, list);
return new PropertyTypeCollection(SupportsPublishing, list);
}
protected void ValidateAlias(PropertyType pt)
@@ -46,7 +46,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
protected override IIdentityUserLogin PerformGet(int id)
{
var sql = GetBaseQuery(false);
sql.Where(GetBaseWhereClause(), new { Id = id });
sql.Where(GetBaseWhereClause(), new { id = id });
var dto = Database.Fetch<ExternalLoginDto>(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault();
if (dto == null)
@@ -20,7 +20,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
: base(scopeAccessor, cache, logger)
{ }
protected override bool IsPublishing => MediaType.IsPublishingConst;
protected override bool SupportsPublishing => MediaType.SupportsPublishingConst;
protected override IRepositoryCachePolicy<IMediaType, int> CreateCachePolicy()
{
@@ -55,11 +55,11 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
if (ids.Any())
{
//NOTE: This logic should never be executed according to our cache policy
return ContentTypeQueryMapper.GetMediaTypes(Database, SqlSyntax, IsPublishing, this)
return ContentTypeQueryMapper.GetMediaTypes(Database, SqlSyntax, SupportsPublishing, this)
.Where(x => ids.Contains(x.Id));
}
return ContentTypeQueryMapper.GetMediaTypes(Database, SqlSyntax, IsPublishing, this);
return ContentTypeQueryMapper.GetMediaTypes(Database, SqlSyntax, SupportsPublishing, this);
}
protected override IEnumerable<IMediaType> PerformGetAll(params Guid[] ids)
@@ -21,7 +21,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
: base(scopeAccessor, cache, logger)
{ }
protected override bool IsPublishing => MemberType.IsPublishingConst;
protected override bool SupportsPublishing => MemberType.SupportsPublishingConst;
protected override IRepositoryCachePolicy<IMemberType, int> CreateCachePolicy()
{
@@ -308,13 +308,7 @@ namespace Umbraco.Core.Persistence.Repositories.Implement
if (dtos == null || dtos.Any() == false)
return Enumerable.Empty<IMemberType>();
return dtos.Select(x =>
{
bool needsSaving;
var memberType = MemberTypeReadOnlyFactory.BuildEntity(x, out needsSaving);
if (needsSaving) PersistUpdatedItem(memberType);
return memberType;
}).ToList();
return dtos.Select(MemberTypeReadOnlyFactory.BuildEntity).ToList();
}
/// <summary>
@@ -702,23 +702,6 @@ ORDER BY colName";
{
if (orderBy == null) throw new ArgumentNullException(nameof(orderBy));
// get the referenced column name and find the corresp mapped column name
var expressionMember = ExpressionHelper.GetMemberInfo(orderBy);
var mapper = _mapperCollection[typeof(IUser)];
var mappedField = mapper.Map(SqlContext.SqlSyntax, expressionMember.Name);
if (mappedField.IsNullOrWhiteSpace())
throw new ArgumentException("Could not find a mapping for the column specified in the orderBy clause");
return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, mappedField, orderDirection, includeUserGroups, excludeUserGroups, userState, filter);
}
private IEnumerable<IUser> GetPagedResultsByQuery(IQuery<IUser> query, long pageIndex, int pageSize, out long totalRecords,
string orderBy, Direction orderDirection = Direction.Ascending,
string[] includeUserGroups = null, string[] excludeUserGroups = null, UserState[] userState = null, IQuery<IUser> filter = null)
{
if (string.IsNullOrWhiteSpace(orderBy)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(orderBy));
Sql<ISqlContext> filterSql = null;
var customFilterWheres = filter?.GetWhereClauses().ToArray();
var hasCustomFilter = customFilterWheres != null && customFilterWheres.Length > 0;
@@ -734,7 +717,6 @@ ORDER BY colName";
filterSql.Append($"AND ({clause.Item1})", clause.Item2);
}
if (includeUserGroups != null && includeUserGroups.Length > 0)
{
const string subQuery = @"AND (umbracoUser.id IN (SELECT DISTINCT umbracoUser.id
@@ -808,7 +790,7 @@ ORDER BY colName";
sql = new SqlTranslator<IUser>(sql, query).Translate();
// get sorted and filtered sql
var sqlNodeIdsWithSort = ApplySort(ApplyFilter(sql, filterSql, query != null), orderDirection, orderBy);
var sqlNodeIdsWithSort = ApplySort(ApplyFilter(sql, filterSql, query != null), orderBy, orderDirection);
// get a page of results and total count
var pagedResult = Database.Page<UserDto>(pageIndex + 1, pageSize, sqlNodeIdsWithSort);
@@ -834,14 +816,36 @@ ORDER BY colName";
return sql;
}
private static Sql<ISqlContext> ApplySort(Sql<ISqlContext> sql, Direction orderDirection, string orderBy)
private Sql<ISqlContext> ApplySort(Sql<ISqlContext> sql, Expression<Func<IUser, object>> orderBy, Direction orderDirection)
{
if (string.IsNullOrEmpty(orderBy)) return sql;
if (orderBy == null) return sql;
var expressionMember = ExpressionHelper.GetMemberInfo(orderBy);
var mapper = _mapperCollection[typeof(IUser)];
var mappedField = mapper.Map(SqlContext.SqlSyntax, expressionMember.Name);
if (mappedField.IsNullOrWhiteSpace())
throw new ArgumentException("Could not find a mapping for the column specified in the orderBy clause");
// beware! NPoco paging code parses the query to isolate the ORDER BY fragment,
// using a regex that wants "([\w\.\[\]\(\)\s""`,]+)" - meaning that anything
// else in orderBy is going to break NPoco / not be detected
// beware! NPoco paging code (in PagingHelper) collapses everything [foo].[bar]
// to [bar] only, so we MUST use aliases, cannot use [table].[field]
// beware! pre-2012 SqlServer is using a convoluted syntax for paging, which
// includes "SELECT ROW_NUMBER() OVER (ORDER BY ...) poco_rn FROM SELECT (...",
// so anything added here MUST also be part of the inner SELECT statement, ie
// the original statement, AND must be using the proper alias, as the inner SELECT
// will hide the original table.field names entirely
var orderByField = sql.GetAliasedField(mappedField);
if (orderDirection == Direction.Ascending)
sql.OrderBy(orderBy);
sql.OrderBy(orderByField);
else
sql.OrderByDescending(orderBy);
sql.OrderByDescending(orderByField);
return sql;
}
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Linq;
using NPoco;
@@ -122,6 +123,12 @@ namespace Umbraco.Core.Persistence.SqlSyntax
SERVERPROPERTY('ResourceLastUpdateDateTime') ResourceLastUpdateDateTime,
SERVERPROPERTY('ProductLevel') ProductLevel;";
string GetString(IDataReader reader, int ordinal, string defaultValue)
=> reader.IsDBNull(ordinal) ? defaultValue : reader.GetString(ordinal);
int GetInt32(IDataReader reader, int ordinal, int defaultValue)
=> reader.IsDBNull(ordinal) ? defaultValue : reader.GetInt32(ordinal);
connection.ConnectionString = connectionString;
ServerVersionInfo version;
using (connection)
@@ -136,12 +143,12 @@ namespace Umbraco.Core.Persistence.SqlSyntax
reader.Read();
// InstanceName can be NULL for the default instance
version = new ServerVersionInfo(
reader.GetString(0),
reader.IsDBNull(2) ? "(default)" : reader.GetString(2),
reader.IsDBNull(3) ? "" : reader.GetString(3),
(EngineEdition) reader.GetInt32(5),
reader.GetString(7),
reader.GetString(9));
GetString(reader, 0, "Unknown"),
GetString(reader, 2, "(default)"),
GetString(reader, 3, string.Empty),
(EngineEdition) GetInt32(reader, 5, 0),
GetString(reader, 7, "DEFAULT"),
GetString(reader, 9, "Unknown"));
}
connection.Close();
}
@@ -290,7 +290,7 @@ namespace Umbraco.Core.PropertyEditors
/// </summary>
public IEnumerable<XElement> ConvertDbToXml(Property property, IDataTypeService dataTypeService, ILocalizationService localizationService, bool published)
{
published &= property.PropertyType.IsPublishing;
published &= property.PropertyType.SupportsPublishing;
var nodeName = property.PropertyType.Alias.ToSafeAlias();
@@ -1,8 +1,8 @@
namespace Umbraco.Core.PropertyEditors
{
internal class DropDownFlexibleConfiguration : ValueListConfiguration
public class DropDownFlexibleConfiguration : ValueListConfiguration
{
[ConfigurationField("multiple", "Enable multiple choice", "boolean", Description = "When checked, the dropdown will be a select multiple / combo box style dropdown.")]
public bool Multiple { get; set; }
}
}
}
@@ -36,6 +36,10 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters
if (source is int)
return (int)source == 1;
// this is required for correct true/false handling in nested content elements
if (source is long)
return (long)source == 1;
if (source is bool)
return (bool)source;
@@ -0,0 +1,33 @@
using System;
using Umbraco.Core.Models.PublishedContent;
namespace Umbraco.Core
{
/// <summary>
/// Provides extension methods for <see cref="IPublishedModelFactory"/>.
/// </summary>
public static class PublishedModelFactoryExtensions
{
/// <summary>
/// Executes an action with a safe live factory/
/// </summary>
/// <remarks>
/// <para>If the factory is a live factory, ensures it is refreshed and locked while executing the action.</para>
/// </remarks>
public static void WithSafeLiveFactory(this IPublishedModelFactory factory, Action action)
{
if (factory is ILivePublishedModelFactory liveFactory)
{
lock (liveFactory.SyncRoot)
{
liveFactory.Refresh();
action();
}
}
else
{
action();
}
}
}
}
@@ -5,11 +5,11 @@ using Umbraco.Core.IO;
namespace Umbraco.Core.Runtime
{
public class CoreRuntimeComponent : IComponent
public class CoreInitialComponent : IComponent
{
private readonly IEnumerable<Profile> _mapperProfiles;
public CoreRuntimeComponent(IEnumerable<Profile> mapperProfiles)
public CoreInitialComponent(IEnumerable<Profile> mapperProfiles)
{
_mapperProfiles = mapperProfiles;
}
@@ -24,7 +24,9 @@ using IntegerValidator = Umbraco.Core.PropertyEditors.Validators.IntegerValidato
namespace Umbraco.Core.Runtime
{
public class CoreRuntimeComposer : ComponentComposer<CoreRuntimeComponent>, IRuntimeComposer
// core's initial composer composes before all core composers
[ComposeBefore(typeof(ICoreComposer))]
public class CoreInitialComposer : ComponentComposer<CoreInitialComponent>
{
public override void Compose(Composition composition)
{
+9 -3
View File
@@ -5,6 +5,7 @@ using System.Linq;
using System.Reflection;
using System.Threading;
using System.Web;
using System.Web.Hosting;
using Umbraco.Core.Cache;
using Umbraco.Core.Composing;
using Umbraco.Core.Configuration;
@@ -70,10 +71,15 @@ namespace Umbraco.Core.Runtime
// objects.
using (var timer = profilingLogger.TraceDuration<CoreRuntime>(
$"Booting Umbraco {UmbracoVersion.SemanticVersion.ToSemanticString()} on {NetworkHelper.MachineName}.",
$"Booting Umbraco {UmbracoVersion.SemanticVersion.ToSemanticString()}.",
"Booted.",
"Boot failed."))
{
logger.Info<CoreRuntime>("Booting site '{HostingSiteName}', app '{HostingApplicationID}', path '{HostingPhysicalPath}', server '{MachineName}'.",
HostingEnvironment.SiteName,
HostingEnvironment.ApplicationID,
HostingEnvironment.ApplicationPhysicalPath,
NetworkHelper.MachineName);
logger.Debug<CoreRuntime>("Runtime: {Runtime}", GetType().FullName);
// application environment
@@ -168,7 +174,7 @@ namespace Umbraco.Core.Runtime
_state.BootFailedException = bfe;
}
timer.Fail(exception: bfe); // be sure to log the exception - even if we repeat ourselves
timer?.Fail(exception: bfe); // be sure to log the exception - even if we repeat ourselves
// if something goes wrong above, we may end up with no factory
// meaning nothing can get the runtime state, etc - so let's try
@@ -242,7 +248,7 @@ namespace Umbraco.Core.Runtime
}
catch
{
timer.Fail();
timer?.Fail();
throw;
}
}
+2 -2
View File
@@ -75,7 +75,7 @@ namespace Umbraco.Core.Scoping
// see note below
if (scopeFileSystems == true)
_fscope = fileSystems.Shadow(Guid.NewGuid());
_fscope = fileSystems.Shadow();
return;
}
@@ -105,7 +105,7 @@ namespace Umbraco.Core.Scoping
// every scoped FS to trigger the creation of shadow FS "on demand", and that would be
// pretty pointless since if scopeFileSystems is true, we *know* we want to shadow
if (scopeFileSystems == true)
_fscope = fileSystems.Shadow(Guid.NewGuid());
_fscope = fileSystems.Shadow();
}
}
@@ -2,39 +2,61 @@
namespace Umbraco.Core.Scoping
{
// base class for an object that will be enlisted in scope context, if any. it
// must be used in a 'using' block, and if not scoped, released when disposed,
// else when scope context runs enlisted actions
/// <summary>
/// Provides a base class for scope contextual objects.
/// </summary>
/// <remarks>
/// <para>A scope contextual object is enlisted in the current scope context,
/// if any, and released when the context exists. It must be used in a 'using'
/// block, and will be released when disposed, if not part of a scope.</para>
/// </remarks>
public abstract class ScopeContextualBase : IDisposable
{
private bool _using, _scoped;
private bool _scoped;
/// <summary>
/// Gets a contextual object.
/// </summary>
/// <typeparam name="T">The type of the object.</typeparam>
/// <param name="scopeProvider">A scope provider.</param>
/// <param name="key">A context key for the object.</param>
/// <param name="ctor">A function producing the contextual object.</param>
/// <returns>The contextual object.</returns>
/// <remarks>
/// <para></para>
/// </remarks>
public static T Get<T>(IScopeProvider scopeProvider, string key, Func<bool, T> ctor)
where T : ScopeContextualBase
{
// no scope context = create a non-scoped object
var scopeContext = scopeProvider.Context;
if (scopeContext == null)
return ctor(false);
// create & enlist the scoped object
var w = scopeContext.Enlist("ScopeContextualBase_" + key,
() => ctor(true),
(completed, item) => { item.Release(completed); });
if (w._using) throw new InvalidOperationException("panic: used.");
w._using = true;
w._scoped = true;
return w;
}
/// <inheritdoc />
/// <remarks>
/// <para>If not scoped, then this releases the contextual object.</para>
/// </remarks>
public void Dispose()
{
_using = false;
if (_scoped == false)
Release(true);
}
/// <summary>
/// Releases the contextual object.
/// </summary>
/// <param name="completed">A value indicating whether the scoped operation completed.</param>
public abstract void Release(bool completed);
}
}
@@ -31,16 +31,16 @@ namespace Umbraco.Core.Services
/// <param name="contentService"></param>
/// <param name="name"></param>
/// <param name="parentId"></param>
/// <param name="mediaTypeAlias"></param>
/// <param name="contentTypeAlias"></param>
/// <param name="userId"></param>
/// <returns></returns>
public static IContent CreateContent(this IContentService contentService, string name, Udi parentId, string mediaTypeAlias, int userId = Constants.Security.SuperUserId)
public static IContent CreateContent(this IContentService contentService, string name, Udi parentId, string contentTypeAlias, int userId = Constants.Security.SuperUserId)
{
var guidUdi = parentId as GuidUdi;
if (guidUdi == null)
throw new InvalidOperationException("The UDI provided isn't of type " + typeof(GuidUdi) + " which is required by content");
var parent = contentService.GetById(guidUdi.Guid);
return contentService.Create(name, parent, mediaTypeAlias, userId);
return contentService.Create(name, parent, contentTypeAlias, userId);
}
/// <summary>
+24 -24
View File
@@ -391,30 +391,30 @@ namespace Umbraco.Core.Services
/// </remarks>
IEnumerable<PublishResult> SaveAndPublishBranch(IContent content, bool force, string[] cultures, int userId = Constants.Security.SuperUserId);
/// <summary>
/// Saves and publishes a document branch.
/// </summary>
/// <param name="content">The root document.</param>
/// <param name="force">A value indicating whether to force-publish documents that are not already published.</param>
/// <param name="shouldPublish">A function determining cultures to publish.</param>
/// <param name="publishCultures">A function publishing cultures.</param>
/// <param name="userId">The identifier of the user performing the operation.</param>
/// <remarks>
/// <para>The <paramref name="force"/> parameter determines which documents are published. When <c>false</c>,
/// only those documents that are already published, are republished. When <c>true</c>, all documents are
/// published. The root of the branch is always published, regardless of <paramref name="force"/>.</para>
/// <para>The <paramref name="editing"/> parameter is a function which determines whether a document has
/// changes to publish (else there is no need to publish it). If one wants to publish only a selection of
/// cultures, one may want to check that only properties for these cultures have changed. Otherwise, other
/// cultures may trigger an unwanted republish.</para>
/// <para>The <paramref name="publishCultures"/> parameter is a function to execute to publish cultures, on
/// each document. It can publish all, one, or a selection of cultures. It returns a boolean indicating
/// whether the cultures could be published.</para>
/// </remarks>
IEnumerable<PublishResult> SaveAndPublishBranch(IContent content, bool force,
Func<IContent, HashSet<string>> shouldPublish,
Func<IContent, HashSet<string>, bool> publishCultures,
int userId = Constants.Security.SuperUserId);
///// <summary>
///// Saves and publishes a document branch.
///// </summary>
///// <param name="content">The root document.</param>
///// <param name="force">A value indicating whether to force-publish documents that are not already published.</param>
///// <param name="shouldPublish">A function determining cultures to publish.</param>
///// <param name="publishCultures">A function publishing cultures.</param>
///// <param name="userId">The identifier of the user performing the operation.</param>
///// <remarks>
///// <para>The <paramref name="force"/> parameter determines which documents are published. When <c>false</c>,
///// only those documents that are already published, are republished. When <c>true</c>, all documents are
///// published. The root of the branch is always published, regardless of <paramref name="force"/>.</para>
///// <para>The <paramref name="editing"/> parameter is a function which determines whether a document has
///// changes to publish (else there is no need to publish it). If one wants to publish only a selection of
///// cultures, one may want to check that only properties for these cultures have changed. Otherwise, other
///// cultures may trigger an unwanted republish.</para>
///// <para>The <paramref name="publishCultures"/> parameter is a function to execute to publish cultures, on
///// each document. It can publish all, one, or a selection of cultures. It returns a boolean indicating
///// whether the cultures could be published.</para>
///// </remarks>
//IEnumerable<PublishResult> SaveAndPublishBranch(IContent content, bool force,
// Func<IContent, HashSet<string>> shouldPublish,
// Func<IContent, HashSet<string>, bool> publishCultures,
// int userId = Constants.Security.SuperUserId);
/// <summary>
/// Unpublishes a document.
@@ -10,6 +10,7 @@ using Umbraco.Core.Models.Membership;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Persistence.Repositories;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Scoping;
using Umbraco.Core.Services.Changes;
@@ -27,6 +28,8 @@ namespace Umbraco.Core.Services.Implement
private readonly IDocumentBlueprintRepository _documentBlueprintRepository;
private readonly ILanguageRepository _languageRepository;
private IQuery<IContent> _queryNotTrashed;
//TODO: The non-lazy object should be injected
private readonly Lazy<PropertyValidationService> _propertyValidationService = new Lazy<PropertyValidationService>(() => new PropertyValidationService());
#region Constructors
@@ -193,7 +196,7 @@ namespace Umbraco.Core.Services.Implement
var content = new Content(name, parentId, contentType);
using (var scope = ScopeProvider.CreateScope())
{
CreateContent(scope, content, parent, userId, false);
CreateContent(scope, content, userId, false);
scope.Complete();
}
@@ -227,7 +230,7 @@ namespace Umbraco.Core.Services.Implement
throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); // causes rollback
var content = new Content(name, parent, contentType);
CreateContent(scope, content, parent, userId, false);
CreateContent(scope, content, userId, false);
scope.Complete();
return content;
@@ -261,7 +264,7 @@ namespace Umbraco.Core.Services.Implement
throw new ArgumentException("No content with that id.", nameof(parentId)); // causes rollback
var content = parentId > 0 ? new Content(name, parent, contentType) : new Content(name, parentId, contentType);
CreateContent(scope, content, parent, userId, true);
CreateContent(scope, content, userId, true);
scope.Complete();
return content;
@@ -293,14 +296,14 @@ namespace Umbraco.Core.Services.Implement
throw new ArgumentException("No content type with that alias.", nameof(contentTypeAlias)); // causes rollback
var content = new Content(name, parent, contentType);
CreateContent(scope, content, parent, userId, true);
CreateContent(scope, content, userId, true);
scope.Complete();
return content;
}
}
private void CreateContent(IScope scope, Content content, IContent parent, int userId, bool withIdentity)
private void CreateContent(IScope scope, IContent content, int userId, bool withIdentity)
{
content.CreatorId = userId;
content.WriterId = userId;
@@ -311,12 +314,12 @@ namespace Umbraco.Core.Services.Implement
// if saving is cancelled, content remains without an identity
var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs);
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving"))
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
return;
_documentRepository.Save(content);
scope.Events.Dispatch(Saved, this, saveEventArgs.ToContentSavedEventArgs(), "Saved");
scope.Events.Dispatch(Saved, this, saveEventArgs.ToContentSavedEventArgs(), nameof(Saved));
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IContent>(content, TreeChangeTypes.RefreshNode).ToEventArgs());
}
@@ -758,7 +761,7 @@ namespace Umbraco.Core.Services.Implement
using (var scope = ScopeProvider.CreateScope())
{
var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs);
if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving"))
if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
{
scope.Complete();
return OperationResult.Cancel(evtMsgs);
@@ -783,7 +786,7 @@ namespace Umbraco.Core.Services.Implement
if (raiseEvents)
{
scope.Events.Dispatch(Saved, this, saveEventArgs.ToContentSavedEventArgs(), "Saved");
scope.Events.Dispatch(Saved, this, saveEventArgs.ToContentSavedEventArgs(), nameof(Saved));
}
var changeType = TreeChangeTypes.RefreshNode;
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IContent>(content, changeType).ToEventArgs());
@@ -813,7 +816,7 @@ namespace Umbraco.Core.Services.Implement
using (var scope = ScopeProvider.CreateScope())
{
var saveEventArgs = new ContentSavingEventArgs(contentsA, evtMsgs);
if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving"))
if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
{
scope.Complete();
return OperationResult.Cancel(evtMsgs);
@@ -833,7 +836,7 @@ namespace Umbraco.Core.Services.Implement
if (raiseEvents)
{
scope.Events.Dispatch(Saved, this, saveEventArgs.ToContentSavedEventArgs(), "Saved");
scope.Events.Dispatch(Saved, this, saveEventArgs.ToContentSavedEventArgs(), nameof(Saved));
}
scope.Events.Dispatch(TreeChanged, this, treeChanges.ToEventArgs());
Audit(AuditType.Save, userId == -1 ? 0 : userId, Constants.System.Root, "Saved multiple content");
@@ -866,26 +869,51 @@ namespace Umbraco.Core.Services.Implement
throw new NotSupportedException($"Culture \"{culture}\" is not supported by invariant content types.");
}
// if culture is specific, first publish the invariant values, then publish the culture itself.
// if culture is '*', then publish them all (including variants)
// explicitly SaveAndPublish a specific culture also publishes invariant values
if (!culture.IsNullOrWhiteSpace() && culture != "*")
using (var scope = ScopeProvider.CreateScope())
{
// publish the invariant values
var publishInvariant = content.PublishCulture(null);
if (!publishInvariant)
scope.WriteLock(Constants.Locks.ContentTree);
var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs);
if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
Property[] invalidProperties;
// if culture is specific, first publish the invariant values, then publish the culture itself.
// if culture is '*', then publish them all (including variants)
// explicitly SaveAndPublish a specific culture also publishes invariant values
if (!culture.IsNullOrWhiteSpace() && culture != "*")
{
// publish the invariant values
var publishInvariant = content.PublishCulture(null);
if (!publishInvariant)
return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content);
//validate the property values
if (!_propertyValidationService.Value.IsPropertyDataValid(content, out invalidProperties))
return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content)
{
InvalidProperties = invalidProperties
};
}
// publish the culture(s)
var publishCulture = content.PublishCulture(culture);
if (!publishCulture)
return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content);
//validate the property values
if (!_propertyValidationService.Value.IsPropertyDataValid(content, out invalidProperties))
return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content)
{
InvalidProperties = invalidProperties
};
var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, userId, raiseEvents);
scope.Complete();
return result;
}
// publish the culture(s)
var publishCulture = content.PublishCulture(culture);
if (!publishCulture)
return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content);
// finally, "save publishing"
// what happens next depends on whether the content can be published or not
return CommitDocumentChanges(content, userId, raiseEvents);
}
/// <inheritdoc />
@@ -894,23 +922,40 @@ namespace Umbraco.Core.Services.Implement
if (content == null) throw new ArgumentNullException(nameof(content));
if (cultures == null) throw new ArgumentNullException(nameof(cultures));
var evtMsgs = EventMessagesFactory.Get();
var varies = content.ContentType.VariesByCulture();
if (cultures.Length == 0)
using (var scope = ScopeProvider.CreateScope())
{
//no cultures specified and doesn't vary, so publish it, else nothing to publish
return !varies
? SaveAndPublish(content, userId: userId, raiseEvents: raiseEvents)
: new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content);
scope.WriteLock(Constants.Locks.ContentTree);
var evtMsgs = EventMessagesFactory.Get();
var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs);
if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
var varies = content.ContentType.VariesByCulture();
if (cultures.Length == 0)
{
//no cultures specified and doesn't vary, so publish it, else nothing to publish
return !varies
? SaveAndPublish(content, userId: userId, raiseEvents: raiseEvents)
: new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content);
}
if (cultures.Select(content.PublishCulture).Any(isValid => !isValid))
return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content);
//validate the property values
if (!_propertyValidationService.Value.IsPropertyDataValid(content, out var invalidProperties))
return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content)
{
InvalidProperties = invalidProperties
};
var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, userId, raiseEvents);
scope.Complete();
return result;
}
// TODO: currently, no way to know which one failed
if (cultures.Select(content.PublishCulture).Any(isValid => !isValid))
return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content);
return CommitDocumentChanges(content, userId, raiseEvents);
}
/// <inheritdoc />
@@ -943,56 +988,87 @@ namespace Umbraco.Core.Services.Implement
if (!content.Published)
return new PublishResult(PublishResultType.SuccessUnpublishAlready, evtMsgs, content);
// all cultures = unpublish whole
if (culture == "*" || (!content.ContentType.VariesByCulture() && culture == null))
using (var scope = ScopeProvider.CreateScope())
{
content.PublishedState = PublishedState.Unpublishing;
}
else
{
// If the culture we want to unpublish was already unpublished, nothing to do.
// To check for that we need to lookup the persisted content item
var persisted = content.HasIdentity ? GetById(content.Id) : null;
scope.WriteLock(Constants.Locks.ContentTree);
if (persisted != null && !persisted.IsCulturePublished(culture))
return new PublishResult(PublishResultType.SuccessUnpublishAlready, evtMsgs, content);
var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs);
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
// unpublish the culture
content.UnpublishCulture(culture);
// all cultures = unpublish whole
if (culture == "*" || (!content.ContentType.VariesByCulture() && culture == null))
{
content.PublishedState = PublishedState.Unpublishing;
}
else
{
// If the culture we want to unpublish was already unpublished, nothing to do.
// To check for that we need to lookup the persisted content item
var persisted = content.HasIdentity ? GetById(content.Id) : null;
if (persisted != null && !persisted.IsCulturePublished(culture))
return new PublishResult(PublishResultType.SuccessUnpublishAlready, evtMsgs, content);
// unpublish the culture
content.UnpublishCulture(culture);
}
var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, userId);
scope.Complete();
return result;
}
// finally, "save publishing"
return CommitDocumentChanges(content, userId);
}
/// <summary>
/// Saves a document and publishes/unpublishes any pending publishing changes made to the document.
/// </summary>
/// <remarks>
/// <para>
/// This MUST NOT be called from within this service, this used to be a public API and must only be used outside of this service.
/// Internally in this service, calls must be made to CommitDocumentChangesInternal
/// </para>
///
/// <para>This is the underlying logic for both publishing and unpublishing any document</para>
/// <para>Pending publishing/unpublishing changes on a document are made with calls to <see cref="IContent.PublishCulture"/> and
/// <see cref="IContent.UnpublishCulture"/>.</para>
/// <para>Pending publishing/unpublishing changes on a document are made with calls to <see cref="ContentRepositoryExtensions.PublishCulture"/> and
/// <see cref="ContentRepositoryExtensions.UnpublishCulture"/>.</para>
/// <para>When publishing or unpublishing a single culture, or all cultures, use <see cref="SaveAndPublish"/>
/// and <see cref="Unpublish"/>. But if the flexibility to both publish and unpublish in a single operation is required
/// then this method needs to be used in combination with <see cref="IContent.PublishCulture"/> and <see cref="IContent.UnpublishCulture"/>
/// then this method needs to be used in combination with <see cref="ContentRepositoryExtensions.PublishCulture"/> and <see cref="ContentRepositoryExtensions.UnpublishCulture"/>
/// on the content itself - this prepares the content, but does not commit anything - and then, invoke
/// <see cref="CommitDocumentChanges"/> to actually commit the changes to the database.</para>
/// <para>The document is *always* saved, even when publishing fails.</para>
/// </remarks>
internal PublishResult CommitDocumentChanges(IContent content, int userId = Constants.Security.SuperUserId, bool raiseEvents = true)
internal PublishResult CommitDocumentChanges(IContent content,
int userId = Constants.Security.SuperUserId, bool raiseEvents = true)
{
using (var scope = ScopeProvider.CreateScope())
{
var evtMsgs = EventMessagesFactory.Get();
scope.WriteLock(Constants.Locks.ContentTree);
var result = CommitDocumentChangesInternal(scope, content, userId, raiseEvents);
var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs);
if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
var result = CommitDocumentChangesInternal(scope, content, saveEventArgs, userId, raiseEvents);
scope.Complete();
return result;
}
}
private PublishResult CommitDocumentChangesInternal(IScope scope, IContent content, int userId = Constants.Security.SuperUserId, bool raiseEvents = true, bool branchOne = false, bool branchRoot = false)
private PublishResult CommitDocumentChangesInternal(IScope scope, IContent content,
ContentSavingEventArgs saveEventArgs,
int userId = Constants.Security.SuperUserId, bool raiseEvents = true, bool branchOne = false, bool branchRoot = false)
{
var evtMsgs = EventMessagesFactory.Get();
if (scope == null) throw new ArgumentNullException(nameof(scope));
if (content == null) throw new ArgumentNullException(nameof(content));
if (saveEventArgs == null) throw new ArgumentNullException(nameof(saveEventArgs));
var evtMsgs = saveEventArgs.Messages;
PublishResult publishResult = null;
PublishResult unpublishResult = null;
@@ -1018,11 +1094,6 @@ namespace Umbraco.Core.Services.Implement
var changeType = isNew ? TreeChangeTypes.RefreshNode : TreeChangeTypes.RefreshBranch;
var previouslyPublished = content.HasIdentity && content.Published;
// always save
var saveEventArgs = new ContentSavingEventArgs(content, evtMsgs);
if (raiseEvents && scope.Events.DispatchCancelable(Saving, this, saveEventArgs, "Saving"))
return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, content);
if (publishing)
{
culturesUnpublishing = content.GetCulturesUnpublishing();
@@ -1058,7 +1129,7 @@ namespace Umbraco.Core.Services.Implement
// but: (a) it means we don't reproduce the PublishState logic here and (b) setting the
// PublishState to anything other than Publishing or Unpublishing - which is precisely
// what we want to do here - throws
content.Published = content.Published;
content.Published = content.Published;
}
}
@@ -1107,7 +1178,7 @@ namespace Umbraco.Core.Services.Implement
// raise the Saved event, always
if (raiseEvents)
{
scope.Events.Dispatch(Saved, this, saveEventArgs.ToContentSavedEventArgs(), "Saved");
scope.Events.Dispatch(Saved, this, saveEventArgs.ToContentSavedEventArgs(), nameof(Saved));
}
if (unpublishing) // we have tried to unpublish - won't happen in a branch
@@ -1242,6 +1313,13 @@ namespace Umbraco.Core.Services.Implement
.Distinct()
.ToList();
if (pendingCultures.Count == 0)
break; //shouldn't happen but no point in continuing if there's nothing there
var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs);
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
yield return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d);
var publishing = true;
foreach (var culture in pendingCultures)
{
@@ -1250,7 +1328,14 @@ namespace Umbraco.Core.Services.Implement
if (d.Trashed) continue; // won't publish
publishing &= d.PublishCulture(culture); //set the culture to be published
//publish the culture values and validate the property values, if validation fails, log the invalid properties so the develeper has an idea of what has failed
Property[] invalidProperties = null;
var tryPublish = d.PublishCulture(culture) && _propertyValidationService.Value.IsPropertyDataValid(d, out invalidProperties);
if (invalidProperties != null && invalidProperties.Length > 0)
Logger.Warn<ContentService>("Scheduled publishing will fail for document {DocumentId} and culture {Culture} because of invalid properties {InvalidProperties}",
d.Id, culture, string.Join(",", invalidProperties.Select(x => x.Alias)));
publishing &= tryPublish; //set the culture to be published
if (!publishing) break; // no point continuing
}
@@ -1259,7 +1344,8 @@ namespace Umbraco.Core.Services.Implement
else if (!publishing)
result = new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, d);
else
result = CommitDocumentChanges(d, d.WriterId);
result = CommitDocumentChangesInternal(scope, d, saveEventArgs, d.WriterId);
if (result.Success == false)
Logger.Error<ContentService>(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
@@ -1293,6 +1379,13 @@ namespace Umbraco.Core.Services.Implement
.Distinct()
.ToList();
if (pendingCultures.Count == 0)
break; //shouldn't happen but no point in continuing if there's nothing there
var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs);
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
yield return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d);
foreach (var c in pendingCultures)
{
//Clear this schedule for this culture
@@ -1301,13 +1394,11 @@ namespace Umbraco.Core.Services.Implement
d.UnpublishCulture(c);
}
if (pendingCultures.Count > 0)
{
result = CommitDocumentChanges(d, d.WriterId);
if (result.Success == false)
Logger.Error<ContentService>(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
yield return result;
}
result = CommitDocumentChangesInternal(scope, d, saveEventArgs, d.WriterId);
if (result.Success == false)
Logger.Error<ContentService>(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result);
yield return result;
}
else
{
@@ -1328,16 +1419,20 @@ namespace Umbraco.Core.Services.Implement
}
}
private bool SaveAndPublishBranch_PublishCultures(IContent c, HashSet<string> culturesToPublish)
// utility 'PublishCultures' func used by SaveAndPublishBranch
private bool SaveAndPublishBranch_PublishCultures(IContent content, HashSet<string> culturesToPublish)
{
//TODO: This does not support being able to return invalid property details to bubble up to the UI
// variant content type - publish specified cultures
// invariant content type - publish only the invariant culture
return c.ContentType.VariesByCulture()
? culturesToPublish.All(c.PublishCulture)
: c.PublishCulture();
return content.ContentType.VariesByCulture()
? culturesToPublish.All(culture => content.PublishCulture(culture) && _propertyValidationService.Value.IsPropertyDataValid(content, out _))
: content.PublishCulture() && _propertyValidationService.Value.IsPropertyDataValid(content, out _);
}
private HashSet<string> SaveAndPublishBranch_ShouldPublish3(ref HashSet<string> cultures, string c, bool published, bool edited, bool isRoot, bool force)
// utility 'ShouldPublish' func used by SaveAndPublishBranch
private HashSet<string> SaveAndPublishBranch_ShouldPublish(ref HashSet<string> cultures, string c, bool published, bool edited, bool isRoot, bool force)
{
// if published, republish
if (published)
@@ -1355,7 +1450,6 @@ namespace Umbraco.Core.Services.Implement
return cultures;
}
/// <inheritdoc />
public IEnumerable<PublishResult> SaveAndPublishBranch(IContent content, bool force, string culture = "*", int userId = Constants.Security.SuperUserId)
{
@@ -1374,10 +1468,10 @@ namespace Umbraco.Core.Services.Implement
HashSet<string> culturesToPublish = null;
if (!c.ContentType.VariesByCulture()) // invariant content type
return SaveAndPublishBranch_ShouldPublish3(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force);
return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force);
if (culture != "*") // variant content type, specific culture
return SaveAndPublishBranch_ShouldPublish3(ref culturesToPublish, culture, c.IsCulturePublished(culture), c.IsCultureEdited(culture), isRoot, force);
return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, culture, c.IsCulturePublished(culture), c.IsCultureEdited(culture), isRoot, force);
// variant content type, all cultures
if (c.Published)
@@ -1385,7 +1479,7 @@ namespace Umbraco.Core.Services.Implement
// then some (and maybe all) cultures will be 'already published' (unless forcing),
// others will have to 'republish this culture'
foreach (var x in c.AvailableCultures)
SaveAndPublishBranch_ShouldPublish3(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, force);
SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, force);
return culturesToPublish;
}
@@ -1414,7 +1508,7 @@ namespace Umbraco.Core.Services.Implement
HashSet<string> culturesToPublish = null;
if (!c.ContentType.VariesByCulture()) // invariant content type
return SaveAndPublishBranch_ShouldPublish3(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force);
return SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, "*", c.Published, c.Edited, isRoot, force);
// variant content type, specific cultures
if (c.Published)
@@ -1422,7 +1516,7 @@ namespace Umbraco.Core.Services.Implement
// then some (and maybe all) cultures will be 'already published' (unless forcing),
// others will have to 'republish this culture'
foreach (var x in cultures)
SaveAndPublishBranch_ShouldPublish3(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, force);
SaveAndPublishBranch_ShouldPublish(ref culturesToPublish, x, c.IsCulturePublished(x), c.IsCultureEdited(x), isRoot, force);
return culturesToPublish;
}
@@ -1435,8 +1529,7 @@ namespace Umbraco.Core.Services.Implement
return SaveAndPublishBranch(content, force, ShouldPublish, SaveAndPublishBranch_PublishCultures, userId);
}
/// <inheritdoc />
public IEnumerable<PublishResult> SaveAndPublishBranch(IContent document, bool force,
internal IEnumerable<PublishResult> SaveAndPublishBranch(IContent document, bool force,
Func<IContent, HashSet<string>> shouldPublish,
Func<IContent, HashSet<string>, bool> publishCultures,
int userId = Constants.Security.SuperUserId)
@@ -1460,7 +1553,7 @@ namespace Umbraco.Core.Services.Implement
throw new InvalidOperationException("Cannot mix PublishCulture and SaveAndPublishBranch.");
// deal with the branch root - if it fails, abort
var result = SaveAndPublishBranchOne(scope, document, shouldPublish, publishCultures, true, publishedDocuments, evtMsgs, userId);
var result = SaveAndPublishBranchItem(scope, document, shouldPublish, publishCultures, true, publishedDocuments, evtMsgs, userId);
if (result != null)
{
results.Add(result);
@@ -1491,7 +1584,7 @@ namespace Umbraco.Core.Services.Implement
}
// no need to check path here, parent has to be published here
result = SaveAndPublishBranchOne(scope, d, shouldPublish, publishCultures, false, publishedDocuments, evtMsgs, userId);
result = SaveAndPublishBranchItem(scope, d, shouldPublish, publishCultures, false, publishedDocuments, evtMsgs, userId);
if (result != null)
{
results.Add(result);
@@ -1521,7 +1614,7 @@ namespace Umbraco.Core.Services.Implement
// shouldPublish: a function determining whether the document has changes that need to be published
// note - 'force' is handled by 'editing'
// publishValues: a function publishing values (using the appropriate PublishCulture calls)
private PublishResult SaveAndPublishBranchOne(IScope scope, IContent document,
private PublishResult SaveAndPublishBranchItem(IScope scope, IContent document,
Func<IContent, HashSet<string>> shouldPublish,
Func<IContent, HashSet<string>, bool> publishCultures,
bool isRoot,
@@ -1534,11 +1627,18 @@ namespace Umbraco.Core.Services.Implement
if (culturesToPublish.Count == 0) // empty = already published
return new PublishResult(PublishResultType.SuccessPublishAlready, evtMsgs, document);
var saveEventArgs = new ContentSavingEventArgs(document, evtMsgs);
if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving)))
return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, document);
// publish & check if values are valid
if (!publishCultures(document, culturesToPublish))
{
//TODO: Based on this callback behavior there is no way to know which properties may have been invalid if this failed, see other results of FailedPublishContentInvalid
return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, document);
}
var result = CommitDocumentChangesInternal(scope, document, userId, branchOne: true, branchRoot: isRoot);
var result = CommitDocumentChangesInternal(scope, document, saveEventArgs, userId, branchOne: true, branchRoot: isRoot);
if (result.Success)
publishedDocuments.Add(document);
return result;
@@ -1802,9 +1902,8 @@ namespace Umbraco.Core.Services.Implement
content.ParentId = parentId;
// get the level delta (old pos to new pos)
var levelDelta = parent == null
? 1 - content.Level + (parentId == Constants.System.RecycleBinContent ? 1 : 0)
: parent.Level + 1 - content.Level;
// note that recycle bin (id:-20) level is 0!
var levelDelta = 1 - content.Level + (parent?.Level ?? 0);
var paths = new Dictionary<int, string>();
@@ -2513,7 +2612,6 @@ namespace Umbraco.Core.Services.Implement
return new PublishResult(PublishResultType.SuccessPublishCulture, evtMsgs, content);
}
Logger.Info<ContentService>("Document {ContentName} (id={ContentId}) has been published.", content.Name, content.Id);
return new PublishResult(evtMsgs, content);
}
@@ -2776,7 +2874,14 @@ namespace Umbraco.Core.Services.Implement
{
foreach (var property in blueprint.Properties)
{
content.SetValue(property.Alias, property.GetValue(culture), culture);
if (property.PropertyType.VariesByCulture())
{
content.SetValue(property.Alias, property.GetValue(culture), culture);
}
else
{
content.SetValue(property.Alias, property.GetValue());
}
}
content.Name = blueprint.Name;
@@ -289,7 +289,7 @@ namespace Umbraco.Core.Services.Implement
scope.Events.Dispatch(Saved, this, saveEventArgs);
scope.Events.Dispatch(TreeChanged, this, new TreeChange<IMedia>(media, TreeChangeTypes.RefreshNode).ToEventArgs());
}
if (withIdentity == false)
return;
@@ -716,7 +716,7 @@ namespace Umbraco.Core.Services.Implement
#endregion
#region Delete
/// <summary>
/// Permanently deletes an <see cref="IMedia"/> object
/// </summary>
@@ -975,9 +975,8 @@ namespace Umbraco.Core.Services.Implement
media.ParentId = parentId;
// get the level delta (old pos to new pos)
var levelDelta = parent == null
? 1 - media.Level + (parentId == Constants.System.RecycleBinMedia ? 1 : 0)
: parent.Level + 1 - media.Level;
// note that recycle bin (id:-20) level is 0!
var levelDelta = 1 - media.Level + (parent?.Level ?? 0);
var paths = new Dictionary<int, string>();
@@ -1024,6 +1023,7 @@ namespace Umbraco.Core.Services.Implement
/// <summary>
/// Empties the Recycle Bin by deleting all <see cref="IMedia"/> that resides in the bin
/// </summary>
/// <param name="userId">Optional Id of the User emptying the Recycle Bin</param>
public OperationResult EmptyRecycleBin()
{
var nodeObjectType = Constants.ObjectTypes.Media;
@@ -1175,7 +1175,7 @@ namespace Umbraco.Core.Services.Implement
var identity = int.MaxValue;
var memType = new MemberType(-1);
var propGroup = new PropertyGroup(MemberType.IsPublishingConst)
var propGroup = new PropertyGroup(MemberType.SupportsPublishingConst)
{
Name = "Membership",
Id = --identity
@@ -3,26 +3,14 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
using System.Xml.Linq;
using Semver;
using Umbraco.Core.Collections;
using Umbraco.Core.Events;
using Umbraco.Core.Exceptions;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Entities;
using Umbraco.Core.Models.Packaging;
using Umbraco.Core.Packaging;
using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Persistence.Repositories;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Scoping;
using Umbraco.Core.Strings;
using Content = Umbraco.Core.Models.Content;
namespace Umbraco.Core.Services.Implement
{
@@ -0,0 +1,135 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Umbraco.Core.Collections;
using Umbraco.Core.Composing;
using Umbraco.Core.Models;
using Umbraco.Core.PropertyEditors;
namespace Umbraco.Core.Services
{
//TODO: We should make this an interface and inject it into the ContentService
internal class PropertyValidationService
{
private readonly PropertyEditorCollection _propertyEditors;
private readonly IDataTypeService _dataTypeService;
public PropertyValidationService(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService)
{
_propertyEditors = propertyEditors;
_dataTypeService = dataTypeService;
}
//TODO: Remove this method in favor of the overload specifying all dependencies
public PropertyValidationService()
: this(Current.PropertyEditors, Current.Services.DataTypeService)
{
}
/// <summary>
/// Validates the content item's properties pass validation rules
/// </summary>
/// <para>If the content type is variant, then culture can be either '*' or an actual culture, but neither 'null' nor
/// 'empty'. If the content type is invariant, then culture can be either '*' or null or empty.</para>
public bool IsPropertyDataValid(IContentBase content, out Property[] invalidProperties, string culture = "*")
{
// select invalid properties
invalidProperties = content.Properties.Where(x =>
{
// if culture is null, we validate invariant properties only
// if culture is '*' we validate both variant and invariant properties, automatically
// if culture is specific eg 'en-US' we both too, but explicitly
var varies = x.PropertyType.VariesByCulture();
if (culture == null)
return !(varies || IsPropertyValid(x, null)); // validate invariant property, invariant culture
if (culture == "*")
return !IsPropertyValid(x, culture); // validate property, all cultures
return varies
? !IsPropertyValid(x, culture) // validate variant property, explicit culture
: !IsPropertyValid(x, null); // validate invariant property, explicit culture
})
.ToArray();
return invalidProperties.Length == 0;
}
/// <summary>
/// Gets a value indicating whether the property has valid values.
/// </summary>
public bool IsPropertyValid(Property property, string culture = "*", string segment = "*")
{
//NOTE - the pvalue and vvalues logic in here is borrowed directly from the Property.Values setter so if you are wondering what that's all about, look there.
// The underlying Property._pvalue and Property._vvalues are not exposed but we can re-create these values ourselves which is what it's doing.
culture = culture.NullOrWhiteSpaceAsNull();
segment = segment.NullOrWhiteSpaceAsNull();
Property.PropertyValue pvalue = null;
// if validating invariant/neutral, and it is supported, validate
// (including ensuring that the value exists, if mandatory)
if ((culture == null || culture == "*") && (segment == null || segment == "*") && property.PropertyType.SupportsVariation(null, null))
{
// validate pvalue (which is the invariant value)
pvalue = property.Values.FirstOrDefault(x => x.Culture == null && x.Segment == null);
if (!IsValidPropertyValue(property, pvalue?.EditedValue))
return false;
}
// if validating only invariant/neutral, we are good
if (culture == null && segment == null)
return true;
// if nothing else to validate, we are good
if ((culture == null || culture == "*") && (segment == null || segment == "*") && !property.PropertyType.VariesByCulture())
return true;
// for anything else, validate the existing values (including mandatory),
// but we cannot validate mandatory globally (we don't know the possible cultures and segments)
// validate vvalues (which are the variant values)
// if we don't have vvalues (property.Values is empty or only contains pvalue), validate null
if (property.Values.Count == (pvalue == null ? 0 : 1))
return culture == "*" || IsValidPropertyValue(property, null);
// else validate vvalues (but don't revalidate pvalue)
var pvalues = property.Values.Where(x =>
x != pvalue && // don't revalidate pvalue
property.PropertyType.SupportsVariation(x.Culture, x.Segment, true) && // the value variation is ok
(culture == "*" || x.Culture.InvariantEquals(culture)) && // the culture matches
(segment == "*" || x.Segment.InvariantEquals(segment))) // the segment matches
.ToList();
return pvalues.Count == 0 || pvalues.All(x => IsValidPropertyValue(property, x.EditedValue));
}
/// <summary>
/// Boolean indicating whether the passed in value is valid
/// </summary>
/// <param name="property"></param>
/// <param name="value"></param>
/// <returns>True is property value is valid, otherwise false</returns>
private bool IsValidPropertyValue(Property property, object value)
{
return IsPropertyValueValid(property.PropertyType, value);
}
/// <summary>
/// Determines whether a value is valid for this property type.
/// </summary>
private bool IsPropertyValueValid(PropertyType propertyType, object value)
{
var editor = _propertyEditors[propertyType.PropertyEditorAlias];
var configuration = _dataTypeService.GetDataType(propertyType.DataTypeId).Configuration;
var valueEditor = editor.GetValueEditor(configuration);
return !valueEditor.Validate(value, propertyType.Mandatory, propertyType.ValidationRegExp).Any();
}
}
}
+47 -62
View File
@@ -593,13 +593,14 @@ namespace Umbraco.Core
///<returns></returns>
public static string ToUrlBase64(this string input)
{
if (input == null) throw new ArgumentNullException("input");
if (input == null) throw new ArgumentNullException(nameof(input));
if (String.IsNullOrEmpty(input)) return String.Empty;
if (string.IsNullOrEmpty(input))
return string.Empty;
//return Convert.ToBase64String(bytes).Replace(".", "-").Replace("/", "_").Replace("=", ",");
var bytes = Encoding.UTF8.GetBytes(input);
return UrlTokenEncode(bytes);
//return Convert.ToBase64String(bytes).Replace(".", "-").Replace("/", "_").Replace("=", ",");
}
/// <summary>
@@ -609,14 +610,14 @@ namespace Umbraco.Core
/// <returns></returns>
public static string FromUrlBase64(this string input)
{
if (input == null) throw new ArgumentNullException("input");
if (input == null) throw new ArgumentNullException(nameof(input));
//if (input.IsInvalidBase64()) return null;
try
{
//var decodedBytes = Convert.FromBase64String(input.Replace("-", ".").Replace("_", "/").Replace(",", "="));
byte[] decodedBytes = UrlTokenDecode(input);
var decodedBytes = UrlTokenDecode(input);
return decodedBytes != null ? Encoding.UTF8.GetString(decodedBytes) : null;
}
catch (FormatException)
@@ -795,42 +796,40 @@ namespace Umbraco.Core
internal static byte[] UrlTokenDecode(string input)
{
if (input == null)
throw new ArgumentNullException(nameof(input));
if (input.Length == 0)
return Array.Empty<byte>();
// calc array size - must be groups of 4
var arrayLength = input.Length;
var remain = arrayLength % 4;
if (remain != 0) arrayLength += 4 - remain;
var inArray = new char[arrayLength];
for (var i = 0; i < input.Length; i++)
{
throw new ArgumentNullException("input");
}
int length = input.Length;
if (length < 1)
{
return new byte[0];
}
int num2 = input[length - 1] - '0';
if ((num2 < 0) || (num2 > 10))
{
return null;
}
char[] inArray = new char[(length - 1) + num2];
for (int i = 0; i < (length - 1); i++)
{
char ch = input[i];
var ch = input[i];
switch (ch)
{
case '-':
case '-': // restore '-' as '+'
inArray[i] = '+';
break;
case '_':
case '_': // restore '_' as '/'
inArray[i] = '/';
break;
default:
default: // keep char unchanged
inArray[i] = ch;
break;
}
}
for (int j = length - 1; j < inArray.Length; j++)
{
// pad with '='
for (var j = input.Length; j < inArray.Length; j++)
inArray[j] = '=';
}
return Convert.FromBase64CharArray(inArray, 0, inArray.Length);
}
@@ -842,54 +841,40 @@ namespace Umbraco.Core
internal static string UrlTokenEncode(byte[] input)
{
if (input == null)
throw new ArgumentNullException(nameof(input));
if (input.Length == 0)
return string.Empty;
// base-64 digits are A-Z, a-z, 0-9, + and /
// the = char is used for trailing padding
var str = Convert.ToBase64String(input);
var pos = str.IndexOf('=');
if (pos < 0) pos = str.Length;
// replace chars that would cause problems in urls
var chArray = new char[pos];
for (var i = 0; i < pos; i++)
{
throw new ArgumentNullException("input");
}
if (input.Length < 1)
{
return String.Empty;
}
string str = null;
int index = 0;
char[] chArray = null;
str = Convert.ToBase64String(input);
if (str == null)
{
return null;
}
index = str.Length;
while (index > 0)
{
if (str[index - 1] != '=')
{
break;
}
index--;
}
chArray = new char[index + 1];
chArray[index] = (char)((0x30 + str.Length) - index);
for (int i = 0; i < index; i++)
{
char ch = str[i];
var ch = str[i];
switch (ch)
{
case '+':
case '+': // replace '+' with '-'
chArray[i] = '-';
break;
case '/':
case '/': // replace '/' with '_'
chArray[i] = '_';
break;
case '=':
chArray[i] = ch;
break;
default:
default: // keep char unchanged
chArray[i] = ch;
break;
}
}
return new string(chArray);
}
+11 -4
View File
@@ -161,14 +161,13 @@
<Compile Include="Composing\ComponentComposer.cs" />
<Compile Include="Composing\Composers.cs" />
<Compile Include="Composing\Composition.cs" />
<Compile Include="CompositionExtensions_Accessors.cs" />
<Compile Include="CompositionExtensions.cs" />
<Compile Include="Composing\DisableAttribute.cs" />
<Compile Include="Composing\DisableComposerAttribute.cs" />
<Compile Include="Composing\EnableAttribute.cs" />
<Compile Include="Composing\EnableComposerAttribute.cs" />
<Compile Include="Composing\IComposer.cs" />
<Compile Include="Composing\ICoreComposer.cs" />
<Compile Include="Composing\IRuntimeComposer.cs" />
<Compile Include="Composing\IComponent.cs" />
<Compile Include="Composing\IUserComposer.cs" />
<Compile Include="Compose\ManifestWatcherComposer.cs" />
@@ -187,6 +186,8 @@
<Compile Include="Composing\CompositionExtensions\Repositories.cs" />
<Compile Include="Composing\CompositionExtensions\Services.cs" />
<Compile Include="CompositionExtensions_Essentials.cs" />
<Compile Include="CompositionExtensions_FileSystems.cs" />
<Compile Include="CompositionExtensions_Uniques.cs" />
<Compile Include="FactoryExtensions.cs" />
<Compile Include="Composing\RegisterFactory.cs" />
<Compile Include="Composing\Current.cs" />
@@ -205,9 +206,15 @@
<Compile Include="Composing\TypeFinder.cs" />
<Compile Include="Composing\TypeHelper.cs" />
<Compile Include="Composing\TypeLoader.cs" />
<Compile Include="IO\MediaPathSchemes\UniqueMediaPathScheme.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\MergeDateAndDateTimePropertyEditor.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_1\ChangeNuCacheJsonFormat.cs" />
<Compile Include="Models\PublishedContent\ILivePublishedModelFactory.cs" />
<Compile Include="PropertyEditors\DateTimeConfiguration.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\RenameLabelAndRichTextPropertyEditorAliases.cs" />
<Compile Include="PublishedModelFactoryExtensions.cs" />
<Compile Include="Services\PropertyValidationService.cs" />
<Compile Include="Composing\TypeCollectionBuilderBase.cs" />
<Compile Include="TypeLoaderExtensions.cs" />
<Compile Include="Composing\WeightAttribute.cs" />
<Compile Include="Composing\WeightedCollectionBuilderBase.cs" />
@@ -524,7 +531,7 @@
<Compile Include="RuntimeLevelReason.cs" />
<Compile Include="RuntimeOptions.cs" />
<Compile Include="Runtime\CoreRuntime.cs" />
<Compile Include="Runtime\CoreRuntimeComponent.cs" />
<Compile Include="Runtime\CoreInitialComponent.cs" />
<Compile Include="CustomBooleanTypeConverter.cs" />
<Compile Include="Migrations\Expressions\Common\ExecutableBuilder.cs" />
<Compile Include="Migrations\Expressions\Common\IExecutableBuilder.cs" />
@@ -1305,7 +1312,7 @@
<Compile Include="ReflectionUtilities.cs" />
<Compile Include="RuntimeLevel.cs" />
<Compile Include="RuntimeState.cs" />
<Compile Include="Runtime\CoreRuntimeComposer.cs" />
<Compile Include="Runtime\CoreInitialComposer.cs" />
<Compile Include="SafeCallContext.cs" />
<Compile Include="Scoping\IInstanceIdentifiable.cs" />
<Compile Include="Scoping\IScope.cs" />
@@ -158,7 +158,7 @@ namespace Umbraco.Tests.Cache
new TestDefaultCultureAccessor(),
TestObjects.GetUmbracoSettings(),
TestObjects.GetGlobalSettings(),
Enumerable.Empty<IUrlProvider>(),
new UrlProviderCollection(Enumerable.Empty<IUrlProvider>()),
Mock.Of<IUserService>());
// just assert it does not throw
+214 -28
View File
@@ -5,6 +5,7 @@ using Moq;
using NUnit.Framework;
using Umbraco.Core.Scoping;
using Umbraco.Web.PublishedCache.NuCache;
using Umbraco.Web.PublishedCache.NuCache.Snap;
namespace Umbraco.Tests.Cache
{
@@ -388,8 +389,7 @@ namespace Umbraco.Tests.Cache
// collect liveGen
GC.Collect();
SnapDictionary<int, string>.GenerationObject genObj;
Assert.IsTrue(d.Test.GenerationObjects.TryPeek(out genObj));
Assert.IsTrue(d.Test.GenObjs.TryPeek(out var genObj));
genObj = null;
// in Release mode, it works, but in Debug mode, the weak reference is still alive
@@ -399,14 +399,14 @@ namespace Umbraco.Tests.Cache
GC.Collect();
#endif
Assert.IsTrue(d.Test.GenerationObjects.TryPeek(out genObj));
Assert.IsFalse(genObj.WeakReference.IsAlive); // snapshot is gone, along with its reference
Assert.IsTrue(d.Test.GenObjs.TryPeek(out genObj));
Assert.IsFalse(genObj.WeakGenRef.IsAlive); // snapshot is gone, along with its reference
await d.CollectAsync();
Assert.AreEqual(0, d.Test.GetValues(1).Length); // null value is gone
Assert.AreEqual(0, d.Count); // item is gone
Assert.AreEqual(0, d.Test.GenerationObjects.Count);
Assert.AreEqual(0, d.Test.GenObjs.Count);
Assert.AreEqual(0, d.SnapCount); // snapshot is gone
Assert.AreEqual(0, d.GenCount); // and generation has been dequeued
}
@@ -632,7 +632,7 @@ namespace Umbraco.Tests.Cache
Assert.AreEqual(1, d.Test.LiveGen);
Assert.IsTrue(d.Test.NextGen);
using (d.GetWriter(GetScopeProvider()))
using (d.GetScopedWriteLock(GetScopeProvider()))
{
var s1 = d.CreateSnapshot();
@@ -685,7 +685,7 @@ namespace Umbraco.Tests.Cache
Assert.IsFalse(d.Test.NextGen);
Assert.AreEqual("uno", s2.Get(1));
using (d.GetWriter(GetScopeProvider()))
using (d.GetScopedWriteLock(GetScopeProvider()))
{
// gen 3
Assert.AreEqual(2, d.Test.GetValues(1).Length);
@@ -712,16 +712,102 @@ namespace Umbraco.Tests.Cache
}
[Test]
public void NestedWriteLocking()
public void NestedWriteLocking1()
{
var d = new SnapDictionary<int, string>();
var t = d.Test;
t.CollectAuto = false;
Assert.AreEqual(0, d.CreateSnapshot().Gen);
// no scope context: writers nest, last one to be disposed commits
var scopeProvider = GetScopeProvider();
using (var w1 = d.GetScopedWriteLock(scopeProvider))
{
Assert.AreEqual(1, t.LiveGen);
Assert.AreEqual(1, t.WLocked);
Assert.IsTrue(t.NextGen);
using (var w2 = d.GetScopedWriteLock(scopeProvider))
{
Assert.AreEqual(1, t.LiveGen);
Assert.AreEqual(2, t.WLocked);
Assert.IsTrue(t.NextGen);
Assert.AreNotSame(w1, w2); // get a new writer each time
d.Set(1, "one");
Assert.AreEqual(0, d.CreateSnapshot().Gen);
}
Assert.AreEqual(1, t.LiveGen);
Assert.AreEqual(1, t.WLocked);
Assert.IsTrue(t.NextGen);
Assert.AreEqual(0, d.CreateSnapshot().Gen);
}
Assert.AreEqual(1, t.LiveGen);
Assert.AreEqual(0, t.WLocked);
Assert.IsTrue(t.NextGen);
Assert.AreEqual(1, d.CreateSnapshot().Gen);
}
[Test]
public void NestedWriteLocking2()
{
var d = new SnapDictionary<int, string>();
d.Test.CollectAuto = false;
var scopeProvider = GetScopeProvider();
using (d.GetWriter(scopeProvider))
Assert.AreEqual(0, d.CreateSnapshot().Gen);
// scope context: writers enlist
var scopeContext = new ScopeContext();
var scopeProvider = GetScopeProvider(scopeContext);
using (var w1 = d.GetScopedWriteLock(scopeProvider))
{
using (d.GetWriter(scopeProvider))
using (var w2 = d.GetScopedWriteLock(scopeProvider))
{
Assert.AreSame(w1, w2);
d.Set(1, "one");
}
}
}
[Test]
public void NestedWriteLocking3()
{
var d = new SnapDictionary<int, string>();
var t = d.Test;
t.CollectAuto = false;
Assert.AreEqual(0, d.CreateSnapshot().Gen);
var scopeContext = new ScopeContext();
var scopeProvider1 = GetScopeProvider();
var scopeProvider2 = GetScopeProvider(scopeContext);
using (var w1 = d.GetScopedWriteLock(scopeProvider1))
{
Assert.AreEqual(1, t.LiveGen);
Assert.AreEqual(1, t.WLocked);
Assert.IsTrue(t.NextGen);
using (var w2 = d.GetScopedWriteLock(scopeProvider2))
{
Assert.AreEqual(1, t.LiveGen);
Assert.AreEqual(2, t.WLocked);
Assert.IsTrue(t.NextGen);
Assert.AreNotSame(w1, w2);
d.Set(1, "one");
}
}
@@ -764,7 +850,7 @@ namespace Umbraco.Tests.Cache
var scopeProvider = GetScopeProvider();
using (d.GetWriter(scopeProvider))
using (d.GetScopedWriteLock(scopeProvider))
{
// gen 3
Assert.AreEqual(2, d.Test.GetValues(1).Length);
@@ -809,7 +895,7 @@ namespace Umbraco.Tests.Cache
var scopeProvider = GetScopeProvider();
using (d.GetWriter(scopeProvider))
using (d.GetScopedWriteLock(scopeProvider))
{
// creating a snapshot in a write-lock does NOT return the "current" content
// it uses the previous snapshot, so new snapshot created only on release
@@ -846,9 +932,10 @@ namespace Umbraco.Tests.Cache
Assert.AreEqual(2, s2.Gen);
Assert.AreEqual("uno", s2.Get(1));
var scopeProvider = GetScopeProvider(true);
var scopeContext = new ScopeContext();
var scopeProvider = GetScopeProvider(scopeContext);
using (d.GetWriter(scopeProvider))
using (d.GetScopedWriteLock(scopeProvider))
{
// creating a snapshot in a write-lock does NOT return the "current" content
// it uses the previous snapshot, so new snapshot created only on release
@@ -867,7 +954,7 @@ namespace Umbraco.Tests.Cache
Assert.AreEqual(2, s4.Gen);
Assert.AreEqual("uno", s4.Get(1));
((ScopeContext) scopeProvider.Context).ScopeExit(true);
scopeContext.ScopeExit(true);
var s5 = d.CreateSnapshot();
Assert.AreEqual(3, s5.Gen);
@@ -878,7 +965,8 @@ namespace Umbraco.Tests.Cache
public void ScopeLocking2()
{
var d = new SnapDictionary<int, string>();
d.Test.CollectAuto = false;
var t = d.Test;
t.CollectAuto = false;
// gen 1
d.Set(1, "one");
@@ -891,12 +979,13 @@ namespace Umbraco.Tests.Cache
Assert.AreEqual(2, s2.Gen);
Assert.AreEqual("uno", s2.Get(1));
var scopeProviderMock = new Mock<IScopeProvider>();
var scopeContext = new ScopeContext();
scopeProviderMock.Setup(x => x.Context).Returns(scopeContext);
var scopeProvider = scopeProviderMock.Object;
Assert.AreEqual(2, t.LiveGen);
Assert.IsFalse(t.NextGen);
using (d.GetWriter(scopeProvider))
var scopeContext = new ScopeContext();
var scopeProvider = GetScopeProvider(scopeContext);
using (d.GetScopedWriteLock(scopeProvider))
{
// creating a snapshot in a write-lock does NOT return the "current" content
// it uses the previous snapshot, so new snapshot created only on release
@@ -905,18 +994,35 @@ namespace Umbraco.Tests.Cache
Assert.AreEqual(2, s3.Gen);
Assert.AreEqual("uno", s3.Get(1));
// we made some changes, so a next gen is required
Assert.AreEqual(3, t.LiveGen);
Assert.IsTrue(t.NextGen);
Assert.AreEqual(1, t.WLocked);
// but live snapshot contains changes
var ls = d.Test.LiveSnapshot;
var ls = t.LiveSnapshot;
Assert.AreEqual("ein", ls.Get(1));
Assert.AreEqual(3, ls.Gen);
}
// nothing is committed until scope exits
Assert.AreEqual(3, t.LiveGen);
Assert.IsTrue(t.NextGen);
Assert.AreEqual(1, t.WLocked);
// no changes until exit
var s4 = d.CreateSnapshot();
Assert.AreEqual(2, s4.Gen);
Assert.AreEqual("uno", s4.Get(1));
scopeContext.ScopeExit(false);
// now things have changed
Assert.AreEqual(2, t.LiveGen);
Assert.IsFalse(t.NextGen);
Assert.AreEqual(0, t.WLocked);
// no changes since not completed
var s5 = d.CreateSnapshot();
Assert.AreEqual(2, s5.Gen);
Assert.AreEqual("uno", s5.Get(1));
@@ -955,12 +1061,92 @@ namespace Umbraco.Tests.Cache
Assert.AreEqual("four", all[3]);
}
private IScopeProvider GetScopeProvider(bool withContext = false)
[Test]
public void DontPanic()
{
var scopeProviderMock = new Mock<IScopeProvider>();
var scopeContext = withContext ? new ScopeContext() : null;
scopeProviderMock.Setup(x => x.Context).Returns(scopeContext);
var scopeProvider = scopeProviderMock.Object;
var d = new SnapDictionary<int, string>();
d.Test.CollectAuto = false;
Assert.IsNull(d.Test.GenObj);
// gen 1
d.Set(1, "one");
Assert.IsTrue(d.Test.NextGen);
Assert.AreEqual(1, d.Test.LiveGen);
Assert.IsNull(d.Test.GenObj);
var s1 = d.CreateSnapshot();
Assert.IsFalse(d.Test.NextGen);
Assert.AreEqual(1, d.Test.LiveGen);
Assert.IsNotNull(d.Test.GenObj);
Assert.AreEqual(1, d.Test.GenObj.Gen);
Assert.AreEqual(1, s1.Gen);
Assert.AreEqual("one", s1.Get(1));
d.Set(1, "uno");
Assert.IsTrue(d.Test.NextGen);
Assert.AreEqual(2, d.Test.LiveGen);
Assert.IsNotNull(d.Test.GenObj);
Assert.AreEqual(1, d.Test.GenObj.Gen);
var scopeContext = new ScopeContext();
var scopeProvider = GetScopeProvider(scopeContext);
// scopeProvider.Context == scopeContext -> writer is scoped
// writer is scope contextual and scoped
// when disposed, nothing happens
// when the context exists, the writer is released
using (d.GetScopedWriteLock(scopeProvider))
{
d.Set(1, "ein");
Assert.IsTrue(d.Test.NextGen);
Assert.AreEqual(3, d.Test.LiveGen);
Assert.IsNotNull(d.Test.GenObj);
Assert.AreEqual(2, d.Test.GenObj.Gen);
}
// writer has not released
Assert.AreEqual(1, d.Test.WLocked);
Assert.IsNotNull(d.Test.GenObj);
Assert.AreEqual(2, d.Test.GenObj.Gen);
// nothing changed
Assert.IsTrue(d.Test.NextGen);
Assert.AreEqual(3, d.Test.LiveGen);
// panic!
var s2 = d.CreateSnapshot();
Assert.AreEqual(1, d.Test.WLocked);
Assert.IsNotNull(d.Test.GenObj);
Assert.AreEqual(2, d.Test.GenObj.Gen);
Assert.AreEqual(3, d.Test.LiveGen);
Assert.IsTrue(d.Test.NextGen);
// release writer
scopeContext.ScopeExit(true);
Assert.AreEqual(0, d.Test.WLocked);
Assert.IsNotNull(d.Test.GenObj);
Assert.AreEqual(2, d.Test.GenObj.Gen);
Assert.AreEqual(3, d.Test.LiveGen);
Assert.IsTrue(d.Test.NextGen);
var s3 = d.CreateSnapshot();
Assert.AreEqual(0, d.Test.WLocked);
Assert.IsNotNull(d.Test.GenObj);
Assert.AreEqual(3, d.Test.GenObj.Gen);
Assert.AreEqual(3, d.Test.LiveGen);
Assert.IsFalse(d.Test.NextGen);
}
private IScopeProvider GetScopeProvider(ScopeContext scopeContext = null)
{
var scopeProvider = Mock.Of<IScopeProvider>();
Mock.Get(scopeProvider)
.Setup(x => x.Context).Returns(scopeContext);
return scopeProvider;
}
}
@@ -4,6 +4,7 @@ using System.Linq;
using Moq;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Cache;
using Umbraco.Core.Compose;
using Umbraco.Core.Composing;
using Umbraco.Core.IO;
@@ -299,11 +300,19 @@ namespace Umbraco.Tests.Components
composers = new Composers(composition, types, Mock.Of<IProfilingLogger>());
Composed.Clear();
Assert.Throws<Exception>(() => composers.Compose());
Console.WriteLine("throws:");
composers = new Composers(composition, types, Mock.Of<IProfilingLogger>());
var requirements = composers.GetRequirements(false);
Console.WriteLine(Composers.GetComposersReport(requirements));
types = new[] { typeof(Composer2) };
composers = new Composers(composition, types, Mock.Of<IProfilingLogger>());
Composed.Clear();
Assert.Throws<Exception>(() => composers.Compose());
Console.WriteLine("throws:");
composers = new Composers(composition, types, Mock.Of<IProfilingLogger>());
requirements = composers.GetRequirements(false);
Console.WriteLine(Composers.GetComposersReport(requirements));
types = new[] { typeof(Composer12) };
composers = new Composers(composition, types, Mock.Of<IProfilingLogger>());
@@ -349,6 +358,25 @@ namespace Umbraco.Tests.Components
Assert.AreEqual(typeof(Composer27), Composed[1]);
}
[Test]
public void AllComposers()
{
var typeLoader = new TypeLoader(AppCaches.Disabled.RuntimeCache, IOHelper.MapPath("~/App_Data/TEMP"), Mock.Of<IProfilingLogger>());
var register = MockRegister();
var composition = new Composition(register, typeLoader, Mock.Of<IProfilingLogger>(), MockRuntimeState(RuntimeLevel.Run));
var types = typeLoader.GetTypes<IComposer>().Where(x => x.FullName.StartsWith("Umbraco.Core.") || x.FullName.StartsWith("Umbraco.Web"));
var composers = new Composers(composition, types, Mock.Of<IProfilingLogger>());
var requirements = composers.GetRequirements();
var report = Composers.GetComposersReport(requirements);
Console.WriteLine(report);
var composerTypes = composers.SortComposers(requirements);
foreach (var type in composerTypes)
Console.WriteLine(type);
}
#region Compothings
public class TestComposerBase : IComposer
@@ -15,6 +15,14 @@ namespace Umbraco.Tests.CoreThings
Assert.AreEqual(GuidUtils.Combine(a, b).ToByteArray(), Combine(a, b));
}
[Test]
public void GuidThingTest()
{
var guid = new Guid("f918382f-2bba-453f-a3e2-1f594016ed3b");
Assert.AreEqual("f22br4n0fm5fli5c", GuidUtils.ToBase32String(guid, 16));
Assert.AreEqual("f22br4n0f", GuidUtils.ToBase32String(guid, 9));
}
// Reference implementation taken from original code.
private static byte[] Combine(Guid guid1, Guid guid2)
{
+1 -1
View File
@@ -33,7 +33,7 @@ namespace Umbraco.Tests.IO
composition.Register(_ => Mock.Of<ILogger>());
composition.Register(_ => Mock.Of<IDataTypeService>());
composition.Register(_ => Mock.Of<IContentSection>());
composition.RegisterUnique<IMediaPathScheme, OriginalMediaPathScheme>();
composition.RegisterUnique<IMediaPathScheme, UniqueMediaPathScheme>();
composition.Configs.Add(SettingsForTests.GetDefaultGlobalSettings);
composition.Configs.Add(SettingsForTests.GetDefaultUmbracoSettings);
@@ -1,7 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using NUnit.Framework;
@@ -44,6 +42,34 @@ namespace Umbraco.Tests.IO
return "/Media/" + path;
}
private string Repeat(string pattern, int count)
{
var text = new StringBuilder();
for (var i = 0; i < count; i++)
text.Append(pattern);
return text.ToString();
}
[Test]
public void SaveFileTest()
{
var basePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "FileSysTests");
using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
_fileSystem.AddFile("sub/f3.txt", ms);
Assert.IsTrue(File.Exists(Path.Combine(basePath, "sub/f3.txt")));
var path = Repeat("bah/bah/", 50);
Assert.Less(260, path.Length);
Assert.Throws<PathTooLongException>(() =>
{
using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
_fileSystem.AddFile(path + "f3.txt", ms);
});
}
[Test]
public void GetFullPathTest()
{
@@ -60,14 +86,31 @@ namespace Umbraco.Tests.IO
// - does properly normalize separators
// - does throw on invalid paths
// works
var path = _fileSystem.GetFullPath("foo.tmp");
Assert.AreEqual(Path.Combine(basePath, @"foo.tmp"), path);
// a very long relative path, which ends up being a short path, works
path = Repeat("bah/../", 50);
Assert.Less(260, path.Length);
path = _fileSystem.GetFullPath(path + "foo.tmp");
Assert.AreEqual(Path.Combine(basePath, @"foo.tmp"), path);
// works too
path = _fileSystem.GetFullPath("foo/bar.tmp");
Assert.AreEqual(Path.Combine(basePath, @"foo\bar.tmp"), path);
// that path is invalid as it would be outside the root directory
Assert.Throws<FileSecurityException>(() => _fileSystem.GetFullPath("../../foo.tmp"));
// a very long path, which ends up being very long, works
path = Repeat("bah/bah/", 50);
Assert.Less(260, path.Length);
Assert.Throws<PathTooLongException>(() =>
{
path = _fileSystem.GetFullPath(path + "foo.tmp");
Assert.Less(260, path.Length); // gets a >260 path and it's fine (but Windows will not like it)
});
}
}
}
+12 -11
View File
@@ -405,10 +405,10 @@ namespace Umbraco.Tests.IO
sw.AddFile("sub/f1.txt", ms);
Assert.IsTrue(phy.FileExists("sub/f1.txt"));
Guid id;
string id;
// explicit shadow without scope does not work
sw.Shadow(id = Guid.NewGuid());
sw.Shadow(id = ShadowWrapper.CreateShadowId());
Assert.IsTrue(Directory.Exists(shadowfs + "/" + id));
using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
sw.AddFile("sub/f2.txt", ms);
@@ -419,7 +419,7 @@ namespace Umbraco.Tests.IO
// shadow with scope but no complete does not complete
scopedFileSystems = true; // pretend we have a scope
var scope = new ShadowFileSystems(fileSystems, id = Guid.NewGuid());
var scope = new ShadowFileSystems(fileSystems, id = ShadowWrapper.CreateShadowId());
Assert.IsTrue(Directory.Exists(shadowfs + "/" + id));
using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
sw.AddFile("sub/f3.txt", ms);
@@ -428,10 +428,11 @@ namespace Umbraco.Tests.IO
Assert.AreEqual(1, dirs.Length);
Assert.AreEqual((shadowfs + "/" + id).Replace('\\', '/'), dirs[0].Replace('\\', '/'));
dirs = Directory.GetDirectories(dirs[0]);
var typedDir = dirs.FirstOrDefault(x => x.Replace('\\', '/').EndsWith("/typed"));
var typedDir = dirs.FirstOrDefault(x => x.Replace('\\', '/').EndsWith("/x"));
Assert.IsNotNull(typedDir);
dirs = Directory.GetDirectories(typedDir);
var scopedDir = dirs.FirstOrDefault(x => x.Replace('\\', '/').EndsWith("/Umbraco.Tests.IO.ShadowFileSystemTests+FS")); // this is where files go
var suid = fileSystems.Paths[typeof(FS)];
var scopedDir = dirs.FirstOrDefault(x => x.Replace('\\', '/').EndsWith("/" + suid)); // this is where files go
Assert.IsNotNull(scopedDir);
scope.Dispose();
scopedFileSystems = false;
@@ -440,7 +441,7 @@ namespace Umbraco.Tests.IO
// shadow with scope and complete does complete
scopedFileSystems = true; // pretend we have a scope
scope = new ShadowFileSystems(fileSystems, id = Guid.NewGuid());
scope = new ShadowFileSystems(fileSystems, id = ShadowWrapper.CreateShadowId());
Assert.IsTrue(Directory.Exists(shadowfs + "/" + id));
using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
sw.AddFile("sub/f4.txt", ms);
@@ -456,7 +457,7 @@ namespace Umbraco.Tests.IO
// test scope for "another thread"
scopedFileSystems = true; // pretend we have a scope
scope = new ShadowFileSystems(fileSystems, id = Guid.NewGuid());
scope = new ShadowFileSystems(fileSystems, id = ShadowWrapper.CreateShadowId());
Assert.IsTrue(Directory.Exists(shadowfs + "/" + id));
using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
sw.AddFile("sub/f5.txt", ms);
@@ -498,10 +499,10 @@ namespace Umbraco.Tests.IO
sw.AddFile("sub/f1.txt", ms);
Assert.IsTrue(phy.FileExists("sub/f1.txt"));
Guid id;
string id;
scopedFileSystems = true; // pretend we have a scope
var scope = new ShadowFileSystems(fileSystems, id = Guid.NewGuid());
var scope = new ShadowFileSystems(fileSystems, id = ShadowWrapper.CreateShadowId());
Assert.IsTrue(Directory.Exists(shadowfs + "/" + id));
using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
sw.AddFile("sub/f2.txt", ms);
@@ -551,10 +552,10 @@ namespace Umbraco.Tests.IO
sw.AddFile("sub/f1.txt", ms);
Assert.IsTrue(phy.FileExists("sub/f1.txt"));
Guid id;
string id;
scopedFileSystems = true; // pretend we have a scope
var scope = new ShadowFileSystems(fileSystems, id = Guid.NewGuid());
var scope = new ShadowFileSystems(fileSystems, id = ShadowWrapper.CreateShadowId());
Assert.IsTrue(Directory.Exists(shadowfs + "/" + id));
using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo")))
sw.AddFile("sub/f2.txt", ms);
+43 -9
View File
@@ -15,13 +15,47 @@ namespace Umbraco.Tests.Models
[TestFixture]
public class ContentTypeTests : UmbracoTestBase
{
[Test]
[Ignore("Ignoring this test until we actually enforce this, see comments in ContentTypeBase.PropertyTypesChanged")]
public void Cannot_Add_Duplicate_Property_Aliases()
{
var contentType = MockedContentTypes.CreateBasicContentType();
contentType.PropertyGroups.Add(new PropertyGroup(new PropertyTypeCollection(false, new[]
{
new PropertyType("testPropertyEditor", ValueStorageType.Nvarchar){ Alias = "myPropertyType" }
})));
Assert.Throws<InvalidOperationException>(() =>
contentType.PropertyTypeCollection.Add(
new PropertyType("testPropertyEditor", ValueStorageType.Nvarchar) { Alias = "myPropertyType" }));
}
[Test]
[Ignore("Ignoring this test until we actually enforce this, see comments in ContentTypeBase.PropertyTypesChanged")]
public void Cannot_Update_Duplicate_Property_Aliases()
{
var contentType = MockedContentTypes.CreateBasicContentType();
contentType.PropertyGroups.Add(new PropertyGroup(new PropertyTypeCollection(false, new[]
{
new PropertyType("testPropertyEditor", ValueStorageType.Nvarchar){ Alias = "myPropertyType" }
})));
contentType.PropertyTypeCollection.Add(new PropertyType("testPropertyEditor", ValueStorageType.Nvarchar) { Alias = "myPropertyType2" });
var toUpdate = contentType.PropertyTypeCollection["myPropertyType2"];
Assert.Throws<InvalidOperationException>(() => toUpdate.Alias = "myPropertyType");
}
[Test]
public void Can_Deep_Clone_Content_Type_Sort()
{
var contentType = new ContentTypeSort(new Lazy<int>(() => 3), 4, "test");
var clone = (ContentTypeSort) contentType.DeepClone();
var clone = (ContentTypeSort)contentType.DeepClone();
Assert.AreNotSame(clone, contentType);
Assert.AreEqual(clone, contentType);
Assert.AreEqual(clone.Id.Value, contentType.Id.Value);
@@ -54,7 +88,7 @@ namespace Umbraco.Tests.Models
contentType.Id = 10;
contentType.CreateDate = DateTime.Now;
contentType.CreatorId = 22;
contentType.SetDefaultTemplate(new Template((string) "Test Template", (string) "testTemplate")
contentType.SetDefaultTemplate(new Template((string)"Test Template", (string)"testTemplate")
{
Id = 88
});
@@ -117,12 +151,12 @@ namespace Umbraco.Tests.Models
{
group.Id = ++i;
}
contentType.AllowedTemplates = new[] { new Template((string) "Name", (string) "name") { Id = 200 }, new Template((string) "Name2", (string) "name2") { Id = 201 } };
contentType.AllowedTemplates = new[] { new Template((string)"Name", (string)"name") { Id = 200 }, new Template((string)"Name2", (string)"name2") { Id = 201 } };
contentType.AllowedContentTypes = new[] { new ContentTypeSort(new Lazy<int>(() => 888), 8, "sub"), new ContentTypeSort(new Lazy<int>(() => 889), 9, "sub2") };
contentType.Id = 10;
contentType.CreateDate = DateTime.Now;
contentType.CreatorId = 22;
contentType.SetDefaultTemplate(new Template((string) "Test Template", (string) "testTemplate")
contentType.SetDefaultTemplate(new Template((string)"Test Template", (string)"testTemplate")
{
Id = 88
});
@@ -167,12 +201,12 @@ namespace Umbraco.Tests.Models
{
group.Id = ++i;
}
contentType.AllowedTemplates = new[] { new Template((string) "Name", (string) "name") { Id = 200 }, new Template((string) "Name2", (string) "name2") { Id = 201 } };
contentType.AllowedContentTypes = new[] {new ContentTypeSort(new Lazy<int>(() => 888), 8, "sub"), new ContentTypeSort(new Lazy<int>(() => 889), 9, "sub2")};
contentType.AllowedTemplates = new[] { new Template((string)"Name", (string)"name") { Id = 200 }, new Template((string)"Name2", (string)"name2") { Id = 201 } };
contentType.AllowedContentTypes = new[] { new ContentTypeSort(new Lazy<int>(() => 888), 8, "sub"), new ContentTypeSort(new Lazy<int>(() => 889), 9, "sub2") };
contentType.Id = 10;
contentType.CreateDate = DateTime.Now;
contentType.CreatorId = 22;
contentType.SetDefaultTemplate(new Template((string) "Test Template", (string) "testTemplate")
contentType.SetDefaultTemplate(new Template((string)"Test Template", (string)"testTemplate")
{
Id = 88
});
@@ -264,12 +298,12 @@ namespace Umbraco.Tests.Models
{
propertyType.Id = ++i;
}
contentType.AllowedTemplates = new[] { new Template((string) "Name", (string) "name") { Id = 200 }, new Template((string) "Name2", (string) "name2") { Id = 201 } };
contentType.AllowedTemplates = new[] { new Template((string)"Name", (string)"name") { Id = 200 }, new Template((string)"Name2", (string)"name2") { Id = 201 } };
contentType.AllowedContentTypes = new[] { new ContentTypeSort(new Lazy<int>(() => 888), 8, "sub"), new ContentTypeSort(new Lazy<int>(() => 889), 9, "sub2") };
contentType.Id = 10;
contentType.CreateDate = DateTime.Now;
contentType.CreatorId = 22;
contentType.SetDefaultTemplate(new Template((string) "Test Template", (string) "testTemplate")
contentType.SetDefaultTemplate(new Template((string)"Test Template", (string)"testTemplate")
{
Id = 88
});
+14 -8
View File
@@ -135,7 +135,7 @@ namespace Umbraco.Tests.Models
Assert.Throws<NotSupportedException>(() => prop.PublishValues());
// change
propertyType.IsPublishing = true;
propertyType.SupportsPublishing = true;
// can get value
// and now published value is null
@@ -364,6 +364,7 @@ namespace Umbraco.Tests.Models
[Test]
public void ContentPublishValuesWithMixedPropertyTypeVariations()
{
var propertyValidationService = new PropertyValidationService(Current.Factory.GetInstance<PropertyEditorCollection>(), Current.Factory.GetInstance<ServiceContext>().DataTypeService);
const string langFr = "fr-FR";
// content type varies by Culture
@@ -383,11 +384,15 @@ namespace Umbraco.Tests.Models
content.SetCultureName("hello", langFr);
Assert.IsFalse(content.PublishCulture(langFr)); // fails because prop1 is mandatory
Assert.IsTrue(content.PublishCulture(langFr)); // succeeds because names are ok (not validating properties here)
Assert.IsFalse(propertyValidationService.IsPropertyDataValid(content, out _, langFr));// fails because prop1 is mandatory
content.SetValue("prop1", "a", langFr);
Assert.IsFalse(content.PublishCulture(langFr)); // fails because prop2 is mandatory and invariant
Assert.IsTrue(content.PublishCulture(langFr)); // succeeds because names are ok (not validating properties here)
Assert.IsFalse(propertyValidationService.IsPropertyDataValid(content, out _, langFr));// fails because prop2 is mandatory and invariant
content.SetValue("prop2", "x");
Assert.IsTrue(content.PublishCulture(langFr)); // now it's ok
Assert.IsTrue(content.PublishCulture(langFr)); // still ok...
Assert.IsTrue(propertyValidationService.IsPropertyDataValid(content, out _, langFr));// now it's ok
Assert.AreEqual("a", content.GetValue("prop1", langFr, published: true));
Assert.AreEqual("x", content.GetValue("prop2", published: true));
@@ -480,20 +485,21 @@ namespace Umbraco.Tests.Models
[Test]
public void ValidationTests()
{
var propertyType = new PropertyType("editor", ValueStorageType.Nvarchar) { Alias = "prop", IsPublishing = true };
var propertyType = new PropertyType("editor", ValueStorageType.Nvarchar) { Alias = "prop", SupportsPublishing = true };
var prop = new Property(propertyType);
prop.SetValue("a");
Assert.AreEqual("a", prop.GetValue());
Assert.IsNull(prop.GetValue(published: true));
var propertyValidationService = new PropertyValidationService(Current.Factory.GetInstance<PropertyEditorCollection>(), Current.Factory.GetInstance<ServiceContext>().DataTypeService);
Assert.IsTrue(prop.IsValid());
Assert.IsTrue(propertyValidationService.IsPropertyValid(prop));
propertyType.Mandatory = true;
Assert.IsTrue(prop.IsValid());
Assert.IsTrue(propertyValidationService.IsPropertyValid(prop));
prop.SetValue(null);
Assert.IsFalse(prop.IsValid());
Assert.IsFalse(propertyValidationService.IsPropertyValid(prop));
// can publish, even though invalid
prop.PublishValues();
@@ -232,6 +232,35 @@ namespace Umbraco.Tests.Persistence.Repositories
}
}
/// <summary>
/// This demonstates an issue found: https://github.com/umbraco/Umbraco-CMS/issues/4963#issuecomment-483516698
/// </summary>
[Test]
[Ignore("Still testing")]
public void Bug_Changing_Built_In_Member_Type_Property_Type_Aliases_Results_In_Exception()
{
//TODO: Fix this bug and then change this test
var provider = TestObjects.GetScopeProvider(Logger);
using (var scope = provider.CreateScope())
{
var repository = CreateRepository(provider);
IMemberType memberType = MockedContentTypes.CreateSimpleMemberType();
repository.Save(memberType);
foreach (var stub in Constants.Conventions.Member.GetStandardPropertyTypeStubs())
{
var prop = memberType.PropertyTypes.First(x => x.Alias == stub.Key);
prop.Alias = prop.Alias + "__0000";
}
repository.Save(memberType);
//Assert.Throws<ArgumentException>(() => );
}
}
[Test]
public void Built_In_Member_Type_Properties_Are_Automatically_Added_When_Creating()
{
@@ -15,6 +15,7 @@ using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Scoping;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Changes;
using Umbraco.Core.Strings;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.Testing.Objects;
using Umbraco.Tests.Testing.Objects.Accessors;
@@ -180,8 +181,9 @@ namespace Umbraco.Tests.PublishedContent
dataSource,
globalSettings,
new SiteDomainHelper(),
contentTypeServiceBaseFactory,
Mock.Of<IEntityXmlSerializer>());
Mock.Of<IEntityXmlSerializer>(),
Mock.Of<IPublishedModelFactory>(),
new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }));
// invariant is the current default
_variationAccesor.VariationContext = new VariationContext();
@@ -40,7 +40,7 @@ namespace Umbraco.Tests.Routing
{
base.SetUp();
WebRuntimeComponent.CreateRoutes(
WebFinalComponent.CreateRoutes(
new TestUmbracoContextAccessor(),
TestObjects.GetGlobalSettings(),
new SurfaceControllerTypeCollection(Enumerable.Empty<Type>()),
@@ -81,7 +81,7 @@ namespace Umbraco.Tests.Runtimes
var composerTypes = typeLoader.GetTypes<IComposer>() // all of them
.Where(x => !x.FullName.StartsWith("Umbraco.Tests.")) // exclude test components
.Where(x => x != typeof(WebRuntimeComposer)); // exclude web runtime
.Where(x => x != typeof(WebInitialComposer)); // exclude web runtime
var composers = new Composers(composition, composerTypes, profilingLogger);
composers.Compose();
@@ -18,7 +18,7 @@ namespace Umbraco.Tests.Runtimes
new PluginViewEngine()
};
WebRuntimeComponent.WrapViewEngines(engines);
WebInitialComponent.WrapViewEngines(engines);
Assert.That(engines.Count, Is.EqualTo(2));
Assert.That(engines[0], Is.InstanceOf<ProfilingViewEngine>());
@@ -34,7 +34,7 @@ namespace Umbraco.Tests.Runtimes
new PluginViewEngine()
};
WebRuntimeComponent.WrapViewEngines(engines);
WebInitialComponent.WrapViewEngines(engines);
Assert.That(engines.Count, Is.EqualTo(2));
Assert.That(((ProfilingViewEngine)engines[0]).Inner, Is.InstanceOf<RenderViewEngine>());
@@ -50,7 +50,7 @@ namespace Umbraco.Tests.Runtimes
profiledEngine
};
WebRuntimeComponent.WrapViewEngines(engines);
WebInitialComponent.WrapViewEngines(engines);
Assert.That(engines[0], Is.SameAs(profiledEngine));
}
@@ -58,7 +58,7 @@ namespace Umbraco.Tests.Runtimes
[Test]
public void WrapViewEngines_CollectionIsNull_DoesNotThrow()
{
Assert.DoesNotThrow(() => WebRuntimeComponent.WrapViewEngines(null));
Assert.DoesNotThrow(() => WebInitialComponent.WrapViewEngines(null));
}
}
}
@@ -17,6 +17,7 @@ using Umbraco.Core.Persistence.Repositories;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Implement;
using Umbraco.Core.Strings;
using Umbraco.Core.Sync;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.Testing;
@@ -97,8 +98,10 @@ namespace Umbraco.Tests.Scoping
documentRepository, mediaRepository, memberRepository,
DefaultCultureAccessor,
new DatabaseDataSource(),
Factory.GetInstance<IGlobalSettings>(), new SiteDomainHelper(), contentTypeServiceBaseFactory,
Factory.GetInstance<IEntityXmlSerializer>());
Factory.GetInstance<IGlobalSettings>(), new SiteDomainHelper(),
Factory.GetInstance<IEntityXmlSerializer>(),
Mock.Of<IPublishedModelFactory>(),
new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }));
}
protected UmbracoContext GetUmbracoContextNu(string url, int templateId = 1234, RouteData routeData = null, bool setSingleton = false, IUmbracoSettingsSection umbracoSettings = null, IEnumerable<IUrlProvider> urlProviders = null)
@@ -1,5 +1,6 @@
using System.Linq;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Events;
using Umbraco.Core.Models;
using Umbraco.Core.Persistence.Repositories.Implement;
@@ -30,7 +31,7 @@ namespace Umbraco.Tests.Services
}
[Test]
public void SavingTest()
public void Saving_Culture()
{
var languageService = ServiceContext.LocalizationService;
@@ -80,13 +81,68 @@ namespace Umbraco.Tests.Services
ContentService.Saving += OnSaving;
ContentService.Saved += OnSaved;
contentService.Save(document);
ContentService.Saving -= OnSaving;
ContentService.Saved -= OnSaved;
try
{
contentService.Save(document);
}
finally
{
ContentService.Saving -= OnSaving;
ContentService.Saved -= OnSaved;
}
}
[Test]
public void PublishingTest()
public void Saving_Set_Value()
{
var contentTypeService = ServiceContext.ContentTypeService;
var contentType = MockedContentTypes.CreateTextPageContentType();
ServiceContext.FileService.SaveTemplate(contentType.DefaultTemplate);
contentTypeService.Save(contentType);
var contentService = ServiceContext.ContentService;
IContent document = new Content("content", -1, contentType);
void OnSaving(IContentService sender, ContentSavingEventArgs e)
{
var saved = e.SavedEntities.First();
Assert.IsTrue(document.GetValue<string>("title").IsNullOrWhiteSpace());
saved.SetValue("title", "title");
}
void OnSaved(IContentService sender, ContentSavedEventArgs e)
{
var saved = e.SavedEntities.First();
Assert.AreSame("title", document.GetValue<string>("title"));
//we're only dealing with invariant here
var propValue = saved.Properties["title"].Values.First(x => x.Culture == null && x.Segment == null);
Assert.AreEqual("title", propValue.EditedValue);
Assert.IsNull(propValue.PublishedValue);
}
ContentService.Saving += OnSaving;
ContentService.Saved += OnSaved;
try
{
contentService.Save(document);
}
finally
{
ContentService.Saving -= OnSaving;
ContentService.Saved -= OnSaved;
}
}
[Test]
public void Publishing_Culture()
{
var languageService = ServiceContext.LocalizationService;
@@ -136,9 +192,15 @@ namespace Umbraco.Tests.Services
ContentService.Publishing += OnPublishing;
ContentService.Published += OnPublished;
contentService.SaveAndPublish(document, "fr-FR");
ContentService.Publishing -= OnPublishing;
ContentService.Published -= OnPublished;
try
{
contentService.SaveAndPublish(document, "fr-FR");
}
finally
{
ContentService.Publishing -= OnPublishing;
ContentService.Published -= OnPublished;
}
document = contentService.GetById(document.Id);
@@ -148,7 +210,105 @@ namespace Umbraco.Tests.Services
}
[Test]
public void UnpublishingTest()
public void Publishing_Set_Value()
{
var contentTypeService = ServiceContext.ContentTypeService;
var contentType = MockedContentTypes.CreateTextPageContentType();
ServiceContext.FileService.SaveTemplate(contentType.DefaultTemplate);
contentTypeService.Save(contentType);
var contentService = ServiceContext.ContentService;
IContent document = new Content("content", -1, contentType);
void OnSaving(IContentService sender, ContentSavingEventArgs e)
{
var saved = e.SavedEntities.First();
Assert.IsTrue(document.GetValue<string>("title").IsNullOrWhiteSpace());
saved.SetValue("title", "title");
}
void OnSaved(IContentService sender, ContentSavedEventArgs e)
{
var saved = e.SavedEntities.First();
Assert.AreSame("title", document.GetValue<string>("title"));
//we're only dealing with invariant here
var propValue = saved.Properties["title"].Values.First(x => x.Culture == null && x.Segment == null);
Assert.AreEqual("title", propValue.EditedValue);
Assert.AreEqual("title", propValue.PublishedValue);
}
//We are binding to Saving (not Publishing), because the Publishing event is really just used for cancelling, it should not be
//used for setting values and it won't actually work! This is because the Publishing event is raised AFTER the values on the model
//are published, but Saving is raised BEFORE.
ContentService.Saving += OnSaving;
ContentService.Saved += OnSaved;
try
{
contentService.SaveAndPublish(document);
}
finally
{
ContentService.Saving -= OnSaving;
ContentService.Saved -= OnSaved;
}
}
[Test]
public void Publishing_Set_Mandatory_Value()
{
var contentTypeService = ServiceContext.ContentTypeService;
var contentType = MockedContentTypes.CreateTextPageContentType();
var titleProperty = contentType.PropertyTypes.First(x => x.Alias == "title");
titleProperty.Mandatory = true; // make this required!
ServiceContext.FileService.SaveTemplate(contentType.DefaultTemplate);
contentTypeService.Save(contentType);
var contentService = ServiceContext.ContentService;
IContent document = new Content("content", -1, contentType);
var result = contentService.SaveAndPublish(document);
Assert.IsFalse(result.Success);
Assert.AreEqual("title", result.InvalidProperties.First().Alias);
// when a service operation fails, the object is dirty and should not be re-used,
// re-create it
document = new Content("content", -1, contentType);
void OnSaving(IContentService sender, ContentSavingEventArgs e)
{
var saved = e.SavedEntities.First();
Assert.IsTrue(document.GetValue<string>("title").IsNullOrWhiteSpace());
saved.SetValue("title", "title");
}
//We are binding to Saving (not Publishing), because the Publishing event is really just used for cancelling, it should not be
//used for setting values and it won't actually work! This is because the Publishing event is raised AFTER the values on the model
//are published, but Saving is raised BEFORE.
ContentService.Saving += OnSaving;
try
{
result = contentService.SaveAndPublish(document);
Assert.IsTrue(result.Success); //will succeed now because we were able to specify the required value in the Saving event
}
finally
{
ContentService.Saving -= OnSaving;
}
}
[Test]
public void Unpublishing_Culture()
{
var languageService = ServiceContext.LocalizationService;
@@ -206,9 +366,15 @@ namespace Umbraco.Tests.Services
ContentService.Publishing += OnPublishing;
ContentService.Published += OnPublished;
contentService.CommitDocumentChanges(document);
ContentService.Publishing -= OnPublishing;
ContentService.Published -= OnPublished;
try
{
contentService.CommitDocumentChanges(document);
}
finally
{
ContentService.Publishing -= OnPublishing;
ContentService.Published -= OnPublished;
}
document = contentService.GetById(document.Id);
@@ -18,6 +18,7 @@ using Umbraco.Core.Services.Implement;
using Umbraco.Tests.Testing;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Umbraco.Core.Cache;
using Umbraco.Core.PropertyEditors;
using Umbraco.Tests.LegacyXmlPublishedCache;
namespace Umbraco.Tests.Services
@@ -986,7 +987,10 @@ namespace Umbraco.Tests.Services
Assert.IsTrue(parent.Published);
// content cannot publish values because they are invalid
Assert.IsNotEmpty(content.ValidateProperties());
var propertyValidationService = new PropertyValidationService(Factory.GetInstance<PropertyEditorCollection>(), ServiceContext.DataTypeService);
var isValid = propertyValidationService.IsPropertyDataValid(content, out var invalidProperties);
Assert.IsFalse(isValid);
Assert.IsNotEmpty(invalidProperties);
// and therefore cannot be published,
// because it did not have a published version at all
@@ -15,6 +15,7 @@ using Umbraco.Core.Persistence.Dtos;
using Umbraco.Core.Persistence.Repositories;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Core.Strings;
using Umbraco.Core.Sync;
using Umbraco.Tests.Testing;
using Umbraco.Web;
@@ -70,8 +71,10 @@ namespace Umbraco.Tests.Services
documentRepository, mediaRepository, memberRepository,
DefaultCultureAccessor,
new DatabaseDataSource(),
Factory.GetInstance<IGlobalSettings>(), new SiteDomainHelper(), contentTypeServiceBaseFactory,
Factory.GetInstance<IEntityXmlSerializer>());
Factory.GetInstance<IGlobalSettings>(), new SiteDomainHelper(),
Factory.GetInstance<IEntityXmlSerializer>(),
Mock.Of<IPublishedModelFactory>(),
new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }));
}
public class LocalServerMessenger : ServerMessengerBase
@@ -1767,7 +1767,7 @@ h1, h2, h3, h4, h5, h6, p, blockquote {
h1, h2, h3, h4, h5, h6
{
font-family:'Open Sans', sans-serif;
font-family:'Lato', sans-serif;
}
h2, h3
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Composing;
@@ -202,6 +203,20 @@ namespace Umbraco.Tests.Strings
Assert.AreEqual(expected, result);
}
[TestCase("hello", "aGVsbG8")]
[TestCase("tad", "dGFk")]
[TestCase("AmqGr+Fd!~ééé", "QW1xR3IrRmQhfsOpw6nDqQ")]
public void UrlTokenEncoding(string value, string expected)
{
var bytes = Encoding.UTF8.GetBytes(value);
Console.WriteLine("base64: " + Convert.ToBase64String(bytes));
var encoded = StringExtensions.UrlTokenEncode(bytes);
Assert.AreEqual(expected, encoded);
var backBytes = StringExtensions.UrlTokenDecode(encoded);
var backString = Encoding.UTF8.GetString(backBytes);
Assert.AreEqual(value, backString);
}
// FORMAT STRINGS
@@ -116,7 +116,7 @@ namespace Umbraco.Tests.TestHelpers
var umbracoSettings = GetUmbracoSettings();
var globalSettings = GetGlobalSettings();
var urlProviders = Enumerable.Empty<IUrlProvider>();
var urlProviders = new UrlProviderCollection(Enumerable.Empty<IUrlProvider>());
if (accessor == null) accessor = new TestUmbracoContextAccessor();
+1 -1
View File
@@ -243,7 +243,7 @@ namespace Umbraco.Tests.Testing
Composition.WithCollectionBuilder<PropertyValueConverterCollectionBuilder>();
Composition.RegisterUnique<IPublishedContentTypeFactory, PublishedContentTypeFactory>();
Composition.RegisterUnique<IMediaPathScheme, OriginalMediaPathScheme>();
Composition.RegisterUnique<IMediaPathScheme, UniqueMediaPathScheme>();
// register empty content apps collection
Composition.WithCollectionBuilder<ContentAppFactoryCollectionBuilder>();
@@ -71,7 +71,7 @@ namespace Umbraco.Tests.Web.Mvc
new TestDefaultCultureAccessor(),
TestObjects.GetUmbracoSettings(),
globalSettings,
Enumerable.Empty<IUrlProvider>(),
new UrlProviderCollection(Enumerable.Empty<IUrlProvider>()),
Mock.Of<IUserService>());
var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of<HttpContextBase>());
@@ -100,7 +100,7 @@ namespace Umbraco.Tests.Web.Mvc
new TestDefaultCultureAccessor(),
TestObjects.GetUmbracoSettings(),
globalSettings,
Enumerable.Empty<IUrlProvider>(),
new UrlProviderCollection(Enumerable.Empty<IUrlProvider>()),
Mock.Of<IUserService>());
var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of<HttpContextBase>());
@@ -129,7 +129,7 @@ namespace Umbraco.Tests.Web.Mvc
new TestDefaultCultureAccessor(),
TestObjects.GetUmbracoSettings(),
globalSettings,
Enumerable.Empty<IUrlProvider>(),
new UrlProviderCollection(Enumerable.Empty<IUrlProvider>()),
Mock.Of<IUserService>());
var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of<HttpContextBase>());
@@ -158,7 +158,7 @@ namespace Umbraco.Tests.Web.Mvc
new TestDefaultCultureAccessor(),
TestObjects.GetUmbracoSettings(),
globalSettings,
Enumerable.Empty<IUrlProvider>(),
new UrlProviderCollection(Enumerable.Empty<IUrlProvider>()),
Mock.Of<IUserService>());
var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of<HttpContextBase>());
@@ -46,7 +46,7 @@ namespace Umbraco.Tests.Web.Mvc
new TestDefaultCultureAccessor(),
TestObjects.GetUmbracoSettings(),
globalSettings,
Enumerable.Empty<IUrlProvider>(),
new UrlProviderCollection(Enumerable.Empty<IUrlProvider>()),
Mock.Of<IUserService>());
var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of<HttpContextBase>());
@@ -73,7 +73,7 @@ namespace Umbraco.Tests.Web.Mvc
new TestDefaultCultureAccessor(),
TestObjects.GetUmbracoSettings(),
globalSettings,
Enumerable.Empty<IUrlProvider>(),
new UrlProviderCollection(Enumerable.Empty<IUrlProvider>()),
Mock.Of<IUserService>());
var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of<HttpContextBase>());
@@ -103,7 +103,7 @@ namespace Umbraco.Tests.Web.Mvc
new TestDefaultCultureAccessor(),
Mock.Of<IUmbracoSettingsSection>(section => section.WebRouting == Mock.Of<IWebRoutingSection>(routingSection => routingSection.UrlProviderMode == "Auto")),
globalSettings,
Enumerable.Empty<IUrlProvider>(),
new UrlProviderCollection(Enumerable.Empty<IUrlProvider>()),
Mock.Of<IUserService>());
var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of<HttpContextBase>());
@@ -140,7 +140,7 @@ namespace Umbraco.Tests.Web.Mvc
new TestDefaultCultureAccessor(),
Mock.Of<IUmbracoSettingsSection>(section => section.WebRouting == webRoutingSettings),
globalSettings,
Enumerable.Empty<IUrlProvider>(),
new UrlProviderCollection(Enumerable.Empty<IUrlProvider>()),
Mock.Of<IUserService>());
var umbracoContextReference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of<HttpContextBase>());
@@ -104,7 +104,7 @@ namespace Umbraco.Tests.Web
new TestDefaultCultureAccessor(),
Mock.Of<IUmbracoSettingsSection>(section => section.WebRouting == Mock.Of<IWebRoutingSection>(routingSection => routingSection.UrlProviderMode == "Auto")),
globalSettings,
new[] { testUrlProvider.Object },
new UrlProviderCollection(new[] { testUrlProvider.Object }),
Mock.Of<IUserService>());
using (var reference = umbracoContextFactory.EnsureUmbracoContext(Mock.Of<HttpContextBase>()))

Some files were not shown because too many files have changed in this diff Show More