Compare commits

...
95 Commits
Author SHA1 Message Date
365621023a Merge pull request from GHSA-cfr5-7p54-4qg8
* Add controller attributes

* Fix up RedirectUrlManagementController to not delete redirects when not enabled

---------

Co-authored-by: Zeegaan <nge@umbraco.dk>
2023-12-11 13:59:59 +01:00
865a3880b9 Merge pull request from GHSA-8qp8-9rpw-j46c
* Added Exception handling and replicated error and info message

* Fixed code so that it works for v8. Added Random Delay to prevent User Enumeration

---------

Co-authored-by: jey <jey@umbraco.dk>
2023-12-11 13:59:35 +01:00
Elitsa MarinovskaandGitHub 1751ff8a8c Merge pull request from GHSA-335x-5wcm-8jv2
* Always taking into account permissions

* Updating with the correct action letter and setting the response when access is denied
2023-12-11 13:39:56 +01:00
Bjarke Berg 7d4a786a7c https://github.com/umbraco/Umbraco-CMS/security/advisories/GHSA-6324-52pr-h4p5 2023-12-11 13:01:21 +01:00
Bjarke Berg 72e996f54d https://github.com/umbraco/Umbraco-CMS/security/advisories/GHSA-xxc6-35r7-796w 2023-12-11 13:01:05 +01:00
Bjarke Berg e8346caa8c Merge remote-tracking branch 'Umbraco-CMS/release/8.18' into v8/8.18
# Conflicts:
#	src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs
2023-12-11 07:39:26 +01:00
Bjarke BergandGitHub fe076b65f4 V8 implementation of https://github.com/umbraco/Umbraco-CMS/pull/15206 (#15324)
* V8 implementation of https://github.com/umbraco/Umbraco-CMS/pull/15206

* Adapt to v8 legacy code style and possibilities
2023-11-30 09:23:35 +01:00
bb3bfdb54d V8: Backport UsePagedSqlQuery to v8 (#14827)
* backport NuCacheSetting to v8

* Refactor to use v8 standards

---------

Co-authored-by: Zeegaan <nge@umbraco.dk>
2023-11-30 08:11:25 +01:00
Bjarke Berg 04a872fe42 Bump version (without port 2023-11-30 08:08:33 +01:00
9f80baa50b V8: Backport UsePagedSqlQuery to v8 (#14827)
* backport NuCacheSetting to v8

* Refactor to use v8 standards

---------

Co-authored-by: Zeegaan <nge@umbraco.dk>
2023-09-22 10:34:25 +02:00
Nikolaj dac9986ed2 Bump version 2023-08-28 12:32:59 +02:00
Nikolaj 1e1affd328 Merge fixup 2023-08-28 12:27:05 +02:00
Nikolaj ca6d92277a Merge remote-tracking branch 'origin/v8/dev' into v8/8.18
# Conflicts:
#	src/Umbraco.Web/Editors/ImagesController.cs
#	src/Umbraco.Web/PropertyEditors/RichTextEditorPastedImages.cs
2023-08-28 12:24:22 +02:00
MoleandGitHub c8427274b4 Backport #14657 to V8 (#14703)
* Backport the validator

* Update fileupload to use filestream validator

* Use validator for image cropper

* Validate avatar uploads

* Add validation to media controller

* Add translations

* Add tests

* Fix unit tests

* Don't locate the validator in PostSetAvatarInternal

* Move stream validation to immediately before media creation.
2023-08-28 10:47:07 +02:00
40ef4e804d Add PR #14721 to v8 (#14732)
* Add commit 1198c76d67
manually, as files have moved on newer version

* Update src/Umbraco.Core/Persistence/Factories/UserGroupFactory.cs

* SafeCast instead of regular casting

---------

Co-authored-by: Zeegaan <nge@umbraco.dk>
2023-08-28 10:41:57 +02:00
nateandSebastiaan Janssen 53395db518 Update ConvertRelatedLinksToMultiUrlPicker.cs 2023-07-20 14:45:31 +02:00
Jacob OvergaardandGitHub 7217aa9cd8 Persist base64 images from RTE (#14537)
Add method to find and persist base64 images from the RTE property editor to the media library
2023-07-18 14:10:06 +02:00
3cbff23404 V8: TinyMCE - Images upload, drag & drop, and resize (#14500)
* add GetProcessedImageUrl controller to get a new crop url when resizing images in the rich text editor

* tinymce: add extra allowed elements

* cherry-pick fix for imageUploadHandler from v10

* tinymce: handle insert & resize of media

* tinymce: cherry-pick bugfix from v10 where not all editors were able to activate because a random id might start with a number which is not supported out-of-the-box in html

* tinymce: remove unused parameter

* tinymce: cherry-pick bugfix from v10 where sometimes a target anchor would be processed even though it didn't have a url

* tinymce: check that the toolbar is active before getting toolbarItems

* Cherry-pick blob image fix from v10 to v8, and re-factor to work with .NET framework

---------

Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
2023-07-05 10:40:32 +02:00
Justin NevilleandGitHub 13bff13110 Umbraco 8 - SQL Server 2022 NuCache Error Using JSON Serializer (#14173) 2023-05-19 11:38:05 +02:00
Nikolaj 9799d722f6 Bump version 2023-05-03 09:20:16 +02:00
Nikolaj GeisleandGitHub 7c9364a532 V8: Encode imagepath to handle special characters (#14192)
* Use webutility to encode imagepath

* Align encoding with same as other versions
2023-05-03 07:55:04 +02:00
Nikolaj ca1453ab4e Bump version 2023-03-23 08:45:55 +01:00
Nikolaj 8962c1e636 Convert upload path to absolute 2023-03-23 08:39:44 +01:00
MAIN\KirillandSebastiaan Janssen 196b29c5eb Complete fix for https://github.com/umbraco/Umbraco-CMS/issues/8090 issue.
(cherry picked from commit 309e7fe371)
2023-03-15 08:19:03 +01:00
MAIN\KirillandSebastiaan Janssen 309e7fe371 Complete fix for https://github.com/umbraco/Umbraco-CMS/issues/8090 issue. 2023-03-15 08:16:27 +01:00
Nikolaj 7e4060e90c Bump version 2023-03-14 12:48:04 +01:00
Nikolaj 2d1e40ed5e Merge remote-tracking branch 'origin/v8/dev' into v8/8.18 2023-03-14 12:45:38 +01:00
Bjarke BergandGitHub f4fdd07eff Add allow list for absolute urls (#13962)
* Add allowlist of media hosts.
Not added to the interface to avoid breaking change

* Cleanup
2023-03-14 12:44:28 +01:00
Nikolaj 3ab1957a74 Check the tmpimg path is in the expected folder 2023-03-14 10:38:38 +01:00
MoleandGitHub 352ef47225 Don't replace hyphens and underscores with empty (#13897) 2023-03-07 11:32:34 +01:00
JesseandSebastiaan Janssen c0c106a107 added fix for ncNodeName filter in list view (#13086)
(cherry picked from commit e87701c482)
(cherry picked from commit a3c259482c)
2023-01-17 09:54:21 +01:00
Bjarke Berg 65e3f001c0 https://github.com/umbraco/Umbraco-CMS/issues/12276 (#12295)
Skip surrogate pairs (e.g. emojis) when making clean stings.

(cherry picked from commit 285c7e75a2)
2022-10-04 07:12:19 +02:00
Jacob OvergaardandGitHub db5a0bad7c V8: upgrade backoffice dependencies (#12919) 2022-09-05 15:30:48 +02:00
Nikolaj f29b6d93e6 Bump version to 8.18.5 2022-08-10 10:52:28 +02:00
Nikolaj b934fb7d90 Merge branch 'v8/dev' into v8/8.18 2022-08-10 08:58:03 +02:00
Bjarke BergandGitHub d0f9c9e931 Merge pull request #12812 from umbraco/v8/bugfix/do_not_expose_full_path_when_uploading_content_types
V8: Only expose the filename when uploading content types
2022-08-09 15:58:01 +02:00
Nikolaj 263b9c74f5 Port e2dee8b8c5 to V8 2022-08-09 15:20:34 +02:00
Nikolaj cbc8724df7 Use key as user alias 2022-08-05 13:43:08 +02:00
Bjarke BergandGitHub a4dcdbc5cd Merge pull request #12779 from umbraco/v8/feature/use-RandomNumberGenerator_instead_of_Random_when_generating_passwords
V8: Replace usage of Random().Next with RNGCryptoServiceProvider
2022-08-05 10:02:16 +02:00
nikolajlauridsen 21d5f1b754 Replace usage of Random().Next with RNGCryptoServiceProvider 2022-08-04 10:53:00 +02:00
Sebastiaan Janssen 907a5a69d3 Force the allowed avatar image types 2022-08-02 11:22:34 +02:00
Ronald BarendseandGitHub 087e682f79 v8: Fix Block List settings exception and optimize PVCs (#12594)
* Don't use MapModelType to get model type

* Optimize block list item activation (cache constructors)

* Fix exceptions in NestedContentSingleValueConverter (zero content types or multiple stored items)
2022-06-21 13:47:36 +02:00
Ronald Barendse 6c7053c449 Change web projects TargetFrameworkMoniker to 4.5.2 to stop VS 2022 err 2022-06-20 16:47:28 +02:00
Jacob OvergaardandGitHub 211c86ccbc update jquery to 3.6.0 and jquery-ui to 1.13.1 because of CVE-2021-41184 (#12534) 2022-06-10 08:43:32 +02:00
Bjarke Berg 22df4190bb Fix bug in ReportSiteTask, that means it only send telemetry once per reboot. 2022-06-05 10:52:49 +02:00
MoleandGitHub 7d99708065 Look for the password prop in memberShipProperties (#12395) 2022-05-18 15:02:29 +02:00
Sebastiaan Janssen 59a6abaa84 Bump version to 8.18.4 2022-05-11 11:32:35 +02:00
Sebastiaan Janssen ee1fe5f2d8 Merge branch 'v8/dev' into v8/8.18 2022-05-11 11:31:28 +02:00
74f17eb3f6 Added the Logic to fire OnSaved And OnChanged events when copying (#12330)
* Added the Logic to fire OnSaved And OnChanged events when copying

* Call OnSaving during copy

Co-authored-by: Aleksander <alf@umrabco.dk>
2022-05-11 11:30:20 +02:00
Sebastiaan Janssen 796eb9b25a Merge branch 'v8/dev' into v8/8.18 2022-05-11 11:29:04 +02:00
Paul JohnsonandSebastiaan Janssen 76bcf58953 Avoid creating indexes/keys for tables which don't exist yet. 2022-05-11 11:26:32 +02:00
Søren KottalandSebastiaan Janssen e8c18cda56 Porting fix from #12109
Possible NullReferenceException in MultiUrlPickerValueConverter
2022-05-11 11:24:20 +02:00
9daa15f1fc Ported over the fixes in #11858 "Check media Parent for permissions when setting correct MediaType" to target v8 (#12233)
* Ported over the fixes in the v9 PR #11858 "Check media Parent for permissions when setting correct MediaType" to v8

* reverted weird formatting in lang file

* Fixes

Co-authored-by: Elitsa Marinovska <elm@umbraco.dk>
2022-04-21 13:44:54 +02:00
MoleandGitHub 400f323db4 Do full save on first member login (#12216) 2022-04-20 14:21:36 +02:00
Sebastiaan Janssen 2c0a380d8b Relaxes newtonsoft.json version range, as per #12231 2022-04-07 09:21:42 +02:00
Sebastiaan Janssen 8a73bbb900 Bump version to 8.18.3 2022-04-04 13:12:57 +02:00
Paul JohnsonandSebastiaan Janssen 99629e4712 Fix issue - changing a document type broke the nucache data structure 2022-04-04 13:10:56 +02:00
Sebastiaan Janssen 27ae938099 Bump to 8.18.2 2022-03-23 10:17:26 +01:00
Sebastiaan Janssen edb69eea36 Cherry pick change away from Umbraco TV from v9 2022-03-21 16:16:40 +01:00
30bb416ff8 Add missing null checks for redirect URLs (#12154)
* Fixes missing null checks as reported in #10066

* Fixed check for null or empty in NPoco where clause.

Co-authored-by: Andy Butland <abutland73@gmail.com>
2022-03-21 14:48:55 +01:00
Sebastiaan Janssen a9b8090d6b Bump version to 8.18.1 2022-03-16 16:49:16 +01:00
Anders BjernerandSebastiaan Janssen 76ecb295cc Fixed issue with TryConvertValueToCrlType method incorrectly enclosing string values in double quotes 2022-03-16 16:42:22 +01:00
Sebastiaan Janssen ce817551a5 Merge branch 'v8/contrib' into v8/dev 2022-03-07 11:13:13 +01:00
inetzoandGitHub 32cd9fed49 Make GridPropertyEditor dirty if a control is removed (#12097)
The currentForm.$setDirty(); was missing when removing an control from a grid.
2022-03-05 17:09:35 +00:00
Nikolaj GeisleandGitHub cabb91dbe1 Merge pull request #12058 from umbraco/v8/bugfix/history-cleanup-make-contenttype-dirty
V8: Fix history cleanup not making content type dirty
2022-03-02 12:22:38 +01:00
Nikolaj GeisleandGitHub 633bc8fd8a Merge pull request #12069 from umbraco/v8/bugfix/missing-information-on-saving-variants
V8: Fix missing EditedCultures and Name in content saving notifications
2022-03-02 11:06:40 +01:00
9de4cf4444 Update src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs
Co-authored-by: Nikolaj Geisle <70372949+Zeegaan@users.noreply.github.com>
2022-03-02 10:45:28 +01:00
nikolajlauridsen ffe9a14664 Remove unused helper method 2022-03-02 09:58:30 +01:00
nikolajlauridsen 1fb4b905c2 Remove pointless assert 2022-03-02 09:58:03 +01:00
nikolajlauridsen 4b8a16af7c Add null check when mapping history cleanup 2022-03-02 09:25:31 +01:00
nikolajlauridsen f6c4af745f Remove no longer relevant unit tests 2022-03-02 08:32:14 +01:00
Bjarke Berg 6dd8cb496b Aligned IsDirty behavior of HsitoryCleanup with how PropertyGroups and types behave.. 2022-03-01 12:51:20 +01:00
Bjarke Berg 8d6394e8e3 Fixed other issues with isDirty, that was true even when no changes was done 2022-03-01 10:34:13 +01:00
159f92d6f8 V8: Fix missing site identifier (#12035)
* Add SiteIdentifierService

* Use SiteIdentifier service to create site identifier if it doesn't exist

* Use default timeout for telemetry request

* Use SiteIdentifierService in TelemetryIdentifierStep

* Send SiteId when requesting dashboard

* Make empty GUID an invalid site identifier

Co-authored-by: nikolajlauridsen <nel@umbraco.dk>
2022-02-28 14:11:59 +01:00
nikolajlauridsen 75f8c09e85 Map dirty cultures to EditedCultures on content before saving 2022-02-28 13:04:08 +01:00
nikolajlauridsen 62de668380 Set the name on the content when setting default culture variant name 2022-02-28 13:03:40 +01:00
Andy ButlandandGitHub 96349e4754 Extended check for locally installed LocalDb to include the most recent version 15. (#12031) 2022-02-27 18:10:31 +01:00
Bjarne FyrstenborgandSebastiaan Janssen 1ec98139c6 Media entry editor listview children (#12061)
(cherry picked from commit 6fd45f653c)

# Conflicts:
#	src/Umbraco.Web.UI.Client/src/common/services/mediapreview.service.js
2022-02-27 17:59:10 +01:00
Mole 7bb85c9ed6 Handle setting HistoryCleanup to null 2022-02-25 13:09:31 +01:00
Mole 25ac6b275e Remove ResetWereDirtyProperties override 2022-02-25 11:44:00 +01:00
Mole db8fe55d08 Remove dirty HistoryCleanup properties when HistoryCleanup is replaced 2022-02-25 11:30:34 +01:00
Mole 86fcb8e715 Ensure replacing the entire HistoryCleanup registers as dirty 2022-02-25 09:41:20 +01:00
Mole c85486b866 Fix tests 2022-02-25 09:39:35 +01:00
Mole 4e2302ab6e Remove overrides 2022-02-25 09:39:04 +01:00
Mole a87e7a12d0 Flow changed properties back to ContentType using PropertyChanged 2022-02-25 08:37:53 +01:00
nikolajlauridsen 2fc8f62195 Implement more overrides for BeingDirtyBase 2022-02-24 14:31:40 +01:00
nikolajlauridsen 586c4e8db3 Add tests for content type being dirty 2022-02-24 13:13:38 +01:00
nikolajlauridsen 5c663fd0f0 Override IsDirty and ResetDirtyProperties in ContentType
In order to call it in both HistoryCleanup and base.
2022-02-24 13:13:08 +01:00
nikolajlauridsen 4f89cb1a34 Make HistoryCleanup inherit from BeingDirtyBase and implement dirty properties 2022-02-24 13:12:02 +01:00
Bjarke BergandSebastiaan Janssen 424db16ab1 Fix issue with namespace in api docs 2022-02-24 12:54:44 +01:00
Sebastiaan Janssen e9be3dd4a5 Bump version to 8.18.0 2022-02-24 11:24:19 +01:00
Bjarke Berg 8ff1d49346 Added noop migration just to change state, so this database do not just directly to 9.3 when migrating 2022-02-17 14:09:26 +01:00
MoleandGitHub 1473e41a20 V8: Fix deleting file on external filesystem (#11998)
* Use IFileSystem to check if folder exists when deleting file

* Rename CanDeleteDirectory back to IsDirectory
2022-02-17 09:35:10 +01:00
Sebastiaan Janssen 2038898f4c Bump version to 8.18.0-rc2 2022-02-17 09:25:11 +01:00
Ronald BarendseandGitHub 40c08f81c4 Add support for readonly properties (#11730) 2022-02-16 11:42:34 +01:00
101 changed files with 31673 additions and 17074 deletions
+1 -1
View File
@@ -30,7 +30,7 @@
<dependency id="Microsoft.AspNet.WebApi.Client" version="[5.2.7,5.999999)" />
<dependency id="Microsoft.Owin" version="[4.0.1,4.999999)" />
<dependency id="MiniProfiler" version="[4.0.138,4.999999)" />
<dependency id="Newtonsoft.Json" version="[12.0.1,12.999999)" />
<dependency id="Newtonsoft.Json" version="[12.0.1,13.999999)" />
<dependency id="Semver" version="[2.0.4,2.999999)" />
<dependency id="Serilog" version="[2.10.0,2.999999)" />
<dependency id="Serilog.Enrichers.Process" version="[2.0.2,2.999999)" />
@@ -15,8 +15,8 @@
<div class="level{{item.level}}"><span class="xref">{{item.name.0.value}}</span></div>
</div>
{{/inheritance.0}}
<h6><strong>{{__global.namespace}}</strong>:{{namespace}}</h6>
<h6><strong>{{__global.assembly}}</strong>:{{assemblies.0}}.dll</h6>
<h6><strong>{{__global.namespace}}</strong>: {{{namespace.specName.0.value}}}</h6>
<h6><strong>{{__global.assembly}}</strong>: {{assemblies.0}}.dll</h6>
<h5 id="{{id}}_syntax">{{__global.syntax}}</h5>
<div class="codewrapper">
<pre><code class="lang-{{_lang}} hljs">{{syntax.content.0.value}}</code></pre>
+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.18.0")]
[assembly: AssemblyInformationalVersion("8.18.0-rc")]
[assembly: AssemblyFileVersion("8.18.10")]
[assembly: AssemblyInformationalVersion("8.18.10")]
@@ -79,7 +79,7 @@ namespace Umbraco.Core.Compose
get
{
var httpContext = HttpContext.Current == null ? (HttpContextBase) null : new HttpContextWrapper(HttpContext.Current);
var ip = httpContext.GetCurrentRequestIpAddress();
var ip = httpContext?.GetCurrentRequestIpAddress();
if (ip == null || ip.ToLowerInvariant().StartsWith("unknown")) ip = "";
return ip;
}
@@ -83,6 +83,7 @@ namespace Umbraco.Core.Composing.CompositionExtensions
composition.RegisterUnique<ITelemetryService, TelemetryService>();
composition.RegisterUnique<IHtmlSanitizer, NoopHtmlSanitizer>();
composition.RegisterUnique<IFileStreamSecurityValidator, FileStreamSecurityValidator>();
return composition;
}
@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Configuration;
using Umbraco.Core.Macros;
@@ -34,6 +35,9 @@ namespace Umbraco.Core.Configuration.UmbracoSettings
[ConfigurationProperty("disallowedUploadFiles")]
internal CommaDelimitedConfigurationElement DisallowedUploadFiles => GetOptionalDelimitedElement("disallowedUploadFiles", new[] {"ashx", "aspx", "ascx", "config", "cshtml", "vbhtml", "asmx", "air", "axd", "xamlx"});
[ConfigurationProperty("allowedMediaHosts")]
internal CommaDelimitedConfigurationElement AllowedMediaHosts => GetOptionalDelimitedElement("allowedMediaHosts", new string[0]);
[ConfigurationProperty("allowedUploadFiles")]
internal CommaDelimitedConfigurationElement AllowedUploadFiles => GetOptionalDelimitedElement("allowedUploadFiles", new string[0]);
@@ -49,6 +53,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings
[ConfigurationProperty("hideBackofficeLogo")]
internal InnerTextConfigurationElement<bool> HideBackOfficeLogo => GetOptionalTextElement("hideBackofficeLogo", false);
string IContentSection.NotificationEmailAddress => Notifications.NotificationEmailAddress;
bool IContentSection.DisableHtmlEmail => Notifications.DisableHtmlEmail;
@@ -0,0 +1,27 @@
using System;
using System.Security.Cryptography;
namespace Umbraco.Core
{
public static class CryptoServiceProviderExtensions
{
/// <summary>
/// Generates a random int withing a specified range.
/// </summary>
/// <param name="provider">Random bytes provider.</param>
/// <param name="minValue">The minimum value of the resulting int.</param>
/// <param name="maxValue">The maximum value of the resulting int.</param>
/// <returns>A random integer that falls withing the specified range.</returns>
public static int GetInt32(this RNGCryptoServiceProvider provider, int minValue, int maxValue)
{
var randomBytes = new byte[4];
provider.GetBytes(randomBytes);
var randomInt = Math.Abs(BitConverter.ToInt32(randomBytes, 0));
// We call do mod to ensure that the value is within the specified range.
return randomInt % (maxValue - minValue + 1) + minValue;
}
}
}
@@ -1,4 +1,6 @@
using Umbraco.Core.Migrations.Install;
using System.Linq;
using NPoco;
using Umbraco.Core.Migrations.Install;
namespace Umbraco.Core.Migrations.Upgrade.Common
{
@@ -14,9 +16,20 @@ namespace Umbraco.Core.Migrations.Upgrade.Common
Delete.KeysAndIndexes(Constants.DatabaseSchema.Tables.KeyValue).Do();
Delete.KeysAndIndexes(Constants.DatabaseSchema.Tables.PropertyData).Do();
var existingTables = SqlSyntax.GetTablesInSchema(Context.Database).ToHashSet();
// re-create *all* keys and indexes
foreach (var x in DatabaseSchemaCreator.OrderedTables)
Create.KeysAndIndexes(x).Do();
foreach (var entityClass in DatabaseSchemaCreator.OrderedTables)
{
var tableNameAttribute = entityClass.FirstAttribute<TableNameAttribute>();
if (tableNameAttribute == null)
continue;
if (!existingTables.Contains(tableNameAttribute.Value))
continue;
Create.KeysAndIndexes(entityClass).Do();
}
}
}
}
@@ -213,6 +213,7 @@ namespace Umbraco.Core.Migrations.Upgrade
//FINAL
To<AddContentVersionCleanupFeature>("{8BAF5E6C-DCB7-41AE-824F-4215AE4F1F98}");
To<NoopMigration>("{03482BB0-CF13-475C-845E-ECB8319DBE3C}");
}
}
}
@@ -60,6 +60,9 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_0_0
var links = new List<LinkDto>();
foreach (var relatedLink in relatedLinks)
{
if (relatedLink == null)
continue;
GuidUdi udi = null;
if (relatedLink.IsInternal)
{
@@ -1,17 +1,37 @@
using System.Runtime.Serialization;
using Umbraco.Core.Models.Entities;
namespace Umbraco.Core.Models.ContentEditing
{
[DataContract(Name = "historyCleanup", Namespace = "")]
public class HistoryCleanup
public class HistoryCleanup : BeingDirtyBase
{
private bool _preventCleanup;
[DataMember(Name = "preventCleanup")]
public bool PreventCleanup { get; set; }
public bool PreventCleanup
{
get => _preventCleanup;
set => SetPropertyValueAndDetectChanges(value, ref _preventCleanup, nameof(PreventCleanup));
}
private int? _keepAllVersionsNewerThanDays;
[DataMember(Name = "keepAllVersionsNewerThanDays")]
public int? KeepAllVersionsNewerThanDays { get; set; }
public int? KeepAllVersionsNewerThanDays
{
get => _keepAllVersionsNewerThanDays;
set => SetPropertyValueAndDetectChanges(value, ref _keepAllVersionsNewerThanDays, nameof(KeepAllVersionsNewerThanDays));
}
private int? _keepLatestVersionPerDayForDays;
[DataMember(Name = "keepLatestVersionPerDayForDays")]
public int? KeepLatestVersionPerDayForDays { get; set; }
public int? KeepLatestVersionPerDayForDays
{
get => _keepLatestVersionPerDayForDays;
set => SetPropertyValueAndDetectChanges(value, ref _keepLatestVersionPerDayForDays,
nameof(KeepLatestVersionPerDayForDays));
}
}
}
+19 -4
View File
@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using Umbraco.Core.Models.ContentEditing;
@@ -50,8 +50,6 @@ namespace Umbraco.Core.Models
/// <inheritdoc />
public override bool SupportsPublishing => SupportsPublishingConst;
//Custom comparer for enumerable
private static readonly DelegateEqualityComparer<IEnumerable<ITemplate>> TemplateComparer = new DelegateEqualityComparer<IEnumerable<ITemplate>>(
(templates, enumerable) => templates.UnsortedSequenceEqual(enumerable),
@@ -98,7 +96,14 @@ namespace Umbraco.Core.Models
}
}
public HistoryCleanup HistoryCleanup { get; set; }
private HistoryCleanup _historyCleanup;
public HistoryCleanup HistoryCleanup
{
get => _historyCleanup;
set => SetPropertyValueAndDetectChanges(value, ref _historyCleanup, nameof(HistoryCleanup));
}
/// <summary>
/// Determines if AllowedTemplates contains templateId
@@ -165,5 +170,15 @@ namespace Umbraco.Core.Models
/// <inheritdoc />
IContentType IContentType.DeepCloneWithResetIdentities(string newAlias) => (IContentType)DeepCloneWithResetIdentities(newAlias);
public override bool IsDirty()
{
bool dirtyEntity = base.IsDirty();
bool dirtyHistoryCleanup = HistoryCleanup.IsDirty();
return dirtyEntity || dirtyHistoryCleanup;
}
}
}
@@ -15,6 +15,7 @@ namespace Umbraco.Core.Models.Entities
public abstract class BeingDirtyBase : IRememberBeingDirty
{
private bool _withChanges = true; // should we track changes?
private bool _withChangedEvent = true; // should we fire the event when changes are registered
private Dictionary<string, bool> _currentChanges; // which properties have changed?
private Dictionary<string, bool> _savedChanges; // which properties had changed at last commit?
@@ -117,7 +118,10 @@ namespace Umbraco.Core.Models.Entities
_currentChanges[propertyName] = true;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
if (_withChangedEvent)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
/// <summary>
@@ -136,6 +140,22 @@ namespace Umbraco.Core.Models.Entities
_withChanges = true;
}
/// <summary>
/// Disables the property changed event.
/// </summary>
protected void DisablePropertyChangedEvent()
{
_withChangedEvent = false;
}
/// <summary>
/// Enables the property changed event.
/// </summary>
protected void EnablePropertyChangedEvent()
{
_withChangedEvent = true;
}
/// <summary>
/// Sets a property value, detects changes and manages the dirty flag.
/// </summary>
@@ -217,6 +217,15 @@ namespace Umbraco.Core.Packaging
Directory.CreateDirectory(IOHelper.MapPath(_mediaFolderPath));
var packPath = _mediaFolderPath.EnsureEndsWith('/') + (definition.Name + "_" + definition.Version).Replace(' ', '_') + ".zip";
var absolutePackPath = Path.GetFullPath(IOHelper.MapPath(packPath));
var expectedRoot = Path.GetFullPath(IOHelper.MapPath(_mediaFolderPath.EnsureEndsWith('/')));
if (absolutePackPath.StartsWith(expectedRoot) == false)
{
throw new IOException("Invalid path due to the package name");
}
ZipPackage(temporaryPath, IOHelper.MapPath(packPath));
//we need to update the package path and save it
@@ -71,7 +71,7 @@ namespace Umbraco.Core.Persistence.Factories
}
if (entity.HasIdentity)
dto.Id = short.Parse(entity.Id.ToString());
dto.Id = entity.Id.SafeCast<int>();
return dto;
}
@@ -1,4 +1,5 @@
using NPoco;
using System.Data.Common;
namespace Umbraco.Core.Persistence
{
@@ -20,5 +21,7 @@ namespace Umbraco.Core.Persistence
/// Gets a value indicating whether the database is currently in a transaction.
/// </summary>
bool InTransaction { get; }
void AddParameter(DbCommand cmd, PocoColumn column, object value);
}
}
+2 -2
View File
@@ -103,8 +103,8 @@ namespace Umbraco.Core.Persistence
if (string.IsNullOrWhiteSpace(programFiles)) return;
// detect 14, 13, 12, 11
for (var i = 14; i > 10; i--)
// detect (17, 16) 15, 14, 13, 12, 11 - future-proofing by a couple of versions
for (var i = 17; i > 10; i--)
{
var exe = Path.Combine(programFiles, $@"Microsoft SQL Server\{i}0\Tools\Binn\SqlLocalDB.exe");
if (File.Exists(exe) == false) continue;
@@ -156,7 +156,8 @@ namespace Umbraco.Core.Persistence
var recordValues = new string[columns.Length];
for (var columnIndex = 0; columnIndex < columns.Length; columnIndex++)
{
database.AddParameter(command, columns[columnIndex].Value.GetValue(record));
var column = columns[columnIndex].Value;
database.AddParameter(command, column, column.GetValue(record));
recordValues[columnIndex] = prefix + parameterIndex++;
}
recordsValues[commandRecordIndex] = "(" + string.Join(",", recordValues) + ")";
@@ -174,13 +174,13 @@ JOIN umbracoNode ON umbracoRedirectUrl.contentKey=umbracoNode.uniqueID");
var urlHash = url.GenerateHash<SHA1>();
var sql = GetBaseQuery(false)
.Where<RedirectUrlDto>(x => x.Url == url && x.UrlHash == urlHash &&
(x.Culture == culture.ToLower() || x.Culture == string.Empty))
(x.Culture == culture.ToLower() || x.Culture == null || x.Culture == string.Empty))
.OrderByDescending<RedirectUrlDto>(x => x.CreateDateUtc);
var dtos = Database.Fetch<RedirectUrlDto>(sql);
var dto = dtos.FirstOrDefault(f => f.Culture == culture.ToLower());
if (dto == null)
dto = dtos.FirstOrDefault(f => f.Culture == string.Empty);
dto = dtos.FirstOrDefault(f => string.IsNullOrWhiteSpace(f.Culture));
return dto == null ? null : Map(dto);
}
@@ -1,12 +1,14 @@
using System;
using System.Data;
using System.Data.Common;
using System.Data.SqlClient;
using System.Linq;
using System.Text;
using NPoco;
using StackExchange.Profiling;
using Umbraco.Core.Logging;
using Umbraco.Core.Persistence.FaultHandling;
using Umbraco.Core.Persistence.Mappers;
namespace Umbraco.Core.Persistence
{
@@ -88,6 +90,22 @@ namespace Umbraco.Core.Persistence
return command;
}
public void AddParameter(DbCommand cmd, PocoColumn column, object value)
{
if (DatabaseType.IsSqlServer())
{
if (column.ColumnType == typeof(byte[]) && value == null)
{
value = new SqlParameter("_bytes", SqlDbType.VarBinary, -1)
{
Value = DBNull.Value
};
}
}
base.AddParameter(cmd, value);
}
#endregion
#region Testing, Debugging and Troubleshooting
@@ -145,16 +145,25 @@ namespace Umbraco.Core.PropertyEditors
/// <returns></returns>
internal Attempt<object> TryConvertValueToCrlType(object value)
{
if (value is JToken jsonValue)
if (value is JValue jsonValue)
{
if (jsonValue is JContainer && jsonValue.HasValues == false)
// Calling the "ToString(Formatting)" method on a string value results in a new
// string enclosed in double quotes (which we don't want), as the method is declared
// in the JToken class. Calling either of the "ToString()" or "ToString(CultureInfo)"
// methods hits the overridden methods in the JValue class, which correctly doesn't
// enclose the value in double quotes.
value = jsonValue.ToString(CultureInfo.InvariantCulture);
}
else if (value is JToken jsonToken)
{
if (jsonToken is JContainer && jsonToken.HasValues == false)
{
// Empty JSON array/object
value = null;
}
else
{
value = jsonValue.ToString(Formatting.None);
value = jsonToken.ToString(Formatting.None);
}
}
@@ -100,20 +100,20 @@ namespace Umbraco.Core.Security
if (userService == null) throw new ArgumentNullException("userService");
if (entityService == null) throw new ArgumentNullException("entityService");
bool? hasPathAccess = null;
bool hasPathAccess;
entity = null;
if (nodeId == Constants.System.Root)
hasPathAccess = user.HasContentRootAccess(entityService, appCaches);
else if (nodeId == Constants.System.RecycleBinContent)
hasPathAccess = user.HasContentBinAccess(entityService, appCaches);
if (hasPathAccess.HasValue)
return hasPathAccess.Value ? ContentAccess.Granted : ContentAccess.Denied;
entity = entityService.Get(nodeId, UmbracoObjectTypes.Document);
if (entity == null) return ContentAccess.NotFound;
hasPathAccess = user.HasContentPathAccess(entity, entityService, appCaches);
else
{
entity = entityService.Get(nodeId, UmbracoObjectTypes.Document);
if (entity == null)
return ContentAccess.NotFound;
hasPathAccess = user.HasContentPathAccess(entity, entityService, appCaches);
}
if (hasPathAccess == false)
return ContentAccess.Denied;
@@ -121,8 +121,11 @@ namespace Umbraco.Core.Security
if (permissionsToCheck == null || permissionsToCheck.Length == 0)
return ContentAccess.Granted;
// if there is no entity for this id, then just use the id as the path (i.e. -1 or -20)
var path = entity == null ? nodeId.ToString() : entity.Path;
//get the implicit/inherited permissions for the user for this path
return CheckPermissionsPath(entity.Path, user, userService, permissionsToCheck)
return CheckPermissionsPath(path, user, userService, permissionsToCheck)
? ContentAccess.Granted
: ContentAccess.Denied;
}
@@ -153,20 +156,20 @@ namespace Umbraco.Core.Security
if (contentService == null) throw new ArgumentNullException("contentService");
if (entityService == null) throw new ArgumentNullException("entityService");
bool? hasPathAccess = null;
bool hasPathAccess;
contentItem = null;
if (nodeId == Constants.System.Root)
hasPathAccess = user.HasContentRootAccess(entityService, appCaches);
else if (nodeId == Constants.System.RecycleBinContent)
hasPathAccess = user.HasContentBinAccess(entityService, appCaches);
if (hasPathAccess.HasValue)
return hasPathAccess.Value ? ContentAccess.Granted : ContentAccess.Denied;
contentItem = contentService.GetById(nodeId);
if (contentItem == null) return ContentAccess.NotFound;
hasPathAccess = user.HasPathAccess(contentItem, entityService, appCaches);
else
{
contentItem = contentService.GetById(nodeId);
if (contentItem == null)
return ContentAccess.NotFound;
hasPathAccess = user.HasPathAccess(contentItem, entityService, appCaches);
}
if (hasPathAccess == false)
return ContentAccess.Denied;
@@ -174,16 +177,18 @@ namespace Umbraco.Core.Security
if (permissionsToCheck == null || permissionsToCheck.Length == 0)
return ContentAccess.Granted;
// if there is no content item for this id, then just use the id as the path (i.e. -1 or -20)
var path = contentItem == null ? nodeId.ToString() : contentItem.Path;
//get the implicit/inherited permissions for the user for this path
return CheckPermissionsPath(contentItem.Path, user, userService, permissionsToCheck)
return CheckPermissionsPath(path, user, userService, permissionsToCheck)
? ContentAccess.Granted
: ContentAccess.Denied;
}
private static bool CheckPermissionsPath(string path, IUser user, IUserService userService, params char[] permissionsToCheck)
{
//get the implicit/inherited permissions for the user for this path,
//if there is no content item for this id, than just use the id as the path (i.e. -1 or -20)
//get the implicit/inherited permissions for the user for this path
var permission = userService.GetPermissionsForPath(user, path);
var allowed = true;
@@ -0,0 +1,42 @@
using System.Collections.Generic;
using System.IO;
namespace Umbraco.Core.Security
{
public class FileStreamSecurityValidator : IFileStreamSecurityValidator
{
private readonly IEnumerable<IFileStreamSecurityAnalyzer> _fileAnalyzers;
public FileStreamSecurityValidator(IEnumerable<IFileStreamSecurityAnalyzer> fileAnalyzers)
{
_fileAnalyzers = fileAnalyzers;
}
/// <summary>
/// Analyzes whether the file content is considered safe with registered IFileStreamSecurityAnalyzers
/// </summary>
/// <param name="fileStream">Needs to be a Read seekable stream</param>
/// <returns>Whether the file is considered safe after running the necessary analyzers</returns>
public bool IsConsideredSafe(Stream fileStream)
{
foreach (var fileAnalyzer in _fileAnalyzers)
{
fileStream.Seek(0, SeekOrigin.Begin);
if (!fileAnalyzer.ShouldHandle(fileStream))
{
continue;
}
fileStream.Seek(0, SeekOrigin.Begin);
if (fileAnalyzer.IsConsideredSafe(fileStream) == false)
{
return false;
}
}
fileStream.Seek(0, SeekOrigin.Begin);
// If no analyzer we consider the file to be safe as the implementer has the possibility to add additional analyzers
// Or all analyzers deem te file to be safe
return true;
}
}
}
@@ -0,0 +1,23 @@
using System.IO;
namespace Umbraco.Core.Security
{
public interface IFileStreamSecurityAnalyzer
{
/// <summary>
/// Indicates whether the analyzer should process the file
/// The implementation should be considerably faster than IsConsideredSafe
/// </summary>
/// <param name="fileStream"></param>
/// <returns></returns>
bool ShouldHandle(Stream fileStream);
/// <summary>
/// Analyzes whether the file content is considered safe
/// </summary>
/// <param name="fileStream">Needs to be a Read/Write seekable stream</param>
/// <returns>Whether the file is considered safe</returns>
bool IsConsideredSafe(Stream fileStream);
}
}
@@ -0,0 +1,15 @@
using System.IO;
namespace Umbraco.Core.Security
{
public interface IFileStreamSecurityValidator
{
/// <summary>
/// Analyzes whether the file content is considered safe with registered IFileStreamSecurityAnalyzers
/// </summary>
/// <param name="fileStream">Needs to be a Read seekable stream</param>
/// <returns>Whether the file is considered safe after running the necessary analyzers</returns>
bool IsConsideredSafe(Stream fileStream);
}
}
@@ -965,7 +965,7 @@ namespace Umbraco.Core.Security
protected string GetCurrentRequestIpAddress()
{
var httpContext = HttpContext.Current == null ? (HttpContextBase) null : new HttpContextWrapper(HttpContext.Current);
return httpContext.GetCurrentRequestIpAddress();
return httpContext?.GetCurrentRequestIpAddress();
}
}
@@ -704,7 +704,29 @@ namespace Umbraco.Core.Services.Implement
}
copy.ParentId = containerId;
var saveEventArgs = new SaveEventArgs<TItem>(copy);
if (OnSavingCancelled(scope, saveEventArgs))
{
scope.Complete();
return OperationResult.Attempt.Fail<MoveOperationStatusType, TItem>(MoveOperationStatusType.FailedCancelledByEvent, evtMsgs); // causes rollback
}
Repository.Save(copy);
// handle events for the copied node
// figure out impacted content types
var changes = ComposeContentTypeChanges(copy).ToArray();
var args = changes.ToEventArgs();
OnUowRefreshedEntity(args);
OnChanged(scope, args);
saveEventArgs.CanCancel = false;
OnSaved(scope, saveEventArgs);
// Since this overload doesn't accept a user id, set the super users.
Audit(AuditType.Save, Constants.Security.SuperUserId, copy.Id);
scope.Complete();
}
catch (DataOperationException<MoveOperationStatusType> ex)
@@ -343,11 +343,14 @@ namespace Umbraco.Core.Strings
var isUpper = char.IsUpper(c); // false for digits, symbols...
//var isLower = char.IsLower(c); // false for digits, symbols...
// what should I do with surrogates?
// no idea, really, so they are not supported at the moment
// what should I do with surrogates? - E.g emojis like 🎈
// no idea, really, so they are not supported at the moment and we just continue
var isPair = char.IsSurrogate(c);
if (isPair)
throw new NotSupportedException("Surrogate pairs are not supported.");
{
continue;
}
switch (state)
{
@@ -0,0 +1,29 @@
using System;
namespace Umbraco.Core.Telemetry
{
/// <summary>
/// Used to get and create the site identifier
/// </summary>
public interface ISiteIdentifierService
{
/// <summary>
/// Tries to get the site identifier
/// </summary>
/// <returns></returns>
bool TryGetSiteIdentifier(out Guid siteIdentifier);
/// <summary>
/// Tries to get the site identifier or otherwise create it if it doesn't exist.
/// </summary>
/// <param name="siteIdentifier"></param>
/// <returns></returns>
bool TryGetOrCreateSiteIdentifier(out Guid siteIdentifier);
/// <summary>
/// Creates the site identifier and writes it to config.
/// </summary>
bool TryCreateSiteIdentifier(out Guid createdGuid);
}
}
+4 -19
View File
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using Umbraco.Core.Configuration;
using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.Manifest;
using Umbraco.Core.Telemetry.Models;
@@ -10,7 +9,7 @@ namespace Umbraco.Core.Telemetry
/// <inheritdoc/>
internal class TelemetryService : ITelemetryService
{
private readonly IUmbracoSettingsSection _settings;
private readonly ISiteIdentifierService _siteIdentifierService;
private readonly ManifestParser _manifestParser;
/// <summary>
@@ -18,16 +17,16 @@ namespace Umbraco.Core.Telemetry
/// </summary>
public TelemetryService(
ManifestParser manifestParser,
IUmbracoSettingsSection settings)
ISiteIdentifierService siteIdentifierService)
{
_manifestParser = manifestParser;
_settings = settings;
_siteIdentifierService = siteIdentifierService;
}
/// <inheritdoc/>
public bool TryGetTelemetryReportData(out TelemetryReportData telemetryReportData)
{
if (TryGetTelemetryId(out Guid telemetryId) is false)
if (_siteIdentifierService.TryGetOrCreateSiteIdentifier(out Guid telemetryId) is false)
{
telemetryReportData = null;
return false;
@@ -42,20 +41,6 @@ namespace Umbraco.Core.Telemetry
return true;
}
private bool TryGetTelemetryId(out Guid telemetryId)
{
// Parse telemetry string as a GUID & verify its a GUID and not some random string
// since users may have messed with or decided to empty the app setting or put in something random
if (Guid.TryParse(_settings.BackOffice.Id, out var parsedTelemetryId) is false)
{
telemetryId = Guid.Empty;
return false;
}
telemetryId = parsedTelemetryId;
return true;
}
private IEnumerable<PackageTelemetry> GetPackageTelemetry()
{
List<PackageTelemetry> packages = new ();
+5
View File
@@ -136,6 +136,7 @@
<Compile Include="Collections\EventClearingObservableCollection.cs" />
<Compile Include="Constants-Sql.cs" />
<Compile Include="Constants-SqlTemplates.cs" />
<Compile Include="CryptoServiceProviderExtensions.cs" />
<Compile Include="Events\UnattendedInstallEventArgs.cs" />
<Compile Include="Help\HelpPageSettings.cs" />
<Compile Include="Help\IHelpPageSettings.cs" />
@@ -196,6 +197,9 @@
<Compile Include="PropertyEditors\PropertyCacheCompression.cs" />
<Compile Include="PropertyEditors\IPropertyCacheCompression.cs" />
<Compile Include="PropertyEditors\UnPublishedContentPropertyCacheCompressionOptions.cs" />
<Compile Include="Security\FileStreamSecurityValidator.cs" />
<Compile Include="Security\IFileStreamSecurityAnalyzer.cs" />
<Compile Include="Security\IFileStreamSecurityValidator.cs" />
<Compile Include="Security\IHtmlSanitizer.cs" />
<Compile Include="Security\NoopHtmlSanitizer.cs" />
<Compile Include="Serialization\AutoInterningStringConverter.cs" />
@@ -400,6 +404,7 @@
<Compile Include="Services\DateTypeServiceExtensions.cs" />
<Compile Include="Services\PropertyValidationService.cs" />
<Compile Include="Composing\TypeCollectionBuilderBase.cs" />
<Compile Include="Telemetry\ISiteIdentifierService.cs" />
<Compile Include="Telemetry\ITelemetryService.cs" />
<Compile Include="Telemetry\Models\PackageTelemetry.cs" />
<Compile Include="Telemetry\Models\TelemetryReportData.cs" />
@@ -0,0 +1,107 @@
using System.Linq;
using NUnit.Framework;
using Umbraco.Core.Models;
using Umbraco.Core.Models.ContentEditing;
using Umbraco.Tests.TestHelpers.Entities;
using Umbraco.Tests.Testing;
namespace Umbraco.Tests.Models
{
[TestFixture]
public class ContentTypeHistoryCleanupTests : UmbracoTestBase
{
[Test]
public void Changing_Keep_all_Makes_ContentType_Dirty()
{
var contentType = MockedContentTypes.CreateBasicContentType();
Assert.IsFalse(contentType.IsDirty());
var newValue = 2;
contentType.HistoryCleanup.KeepAllVersionsNewerThanDays = newValue;
Assert.IsTrue(contentType.IsDirty());
Assert.AreEqual(newValue, contentType.HistoryCleanup.KeepAllVersionsNewerThanDays);
}
[Test]
public void Changing_Keep_latest_Makes_ContentType_Dirty()
{
var contentType = MockedContentTypes.CreateBasicContentType();
Assert.IsFalse(contentType.IsDirty());
var newValue = 2;
contentType.HistoryCleanup.KeepLatestVersionPerDayForDays = newValue;
Assert.IsTrue(contentType.IsDirty());
Assert.AreEqual(newValue, contentType.HistoryCleanup.KeepLatestVersionPerDayForDays);
}
[Test]
public void Changing_Prevent_Cleanup_Makes_ContentType_Dirty()
{
var contentType = MockedContentTypes.CreateBasicContentType();
Assert.IsFalse(contentType.IsDirty());
var newValue = true;
contentType.HistoryCleanup.PreventCleanup = newValue;
Assert.IsTrue(contentType.IsDirty());
Assert.AreEqual(newValue, contentType.HistoryCleanup.PreventCleanup);
}
[Test]
public void Replacing_History_Cleanup_Registers_As_Dirty()
{
var contentType = MockedContentTypes.CreateBasicContentType();
Assert.IsFalse(contentType.IsDirty());
contentType.HistoryCleanup = new HistoryCleanup();
Assert.IsTrue(contentType.IsDirty());
Assert.IsTrue(contentType.IsPropertyDirty(nameof(contentType.HistoryCleanup)));
}
[Test]
public void Replacing_History_Cleanup_Removes_Old_Dirty_History_Properties()
{
var contentType = MockedContentTypes.CreateBasicContentType();
contentType.Alias = "NewValue";
contentType.HistoryCleanup.KeepAllVersionsNewerThanDays = 2;
contentType.PropertyChanged += (sender, args) =>
{
// Ensure that property changed is only invoked for history cleanup
Assert.AreEqual(nameof(contentType.HistoryCleanup), args.PropertyName);
};
// Since we're replacing the entire HistoryCleanup the changed property is no longer dirty, the entire HistoryCleanup is
contentType.HistoryCleanup = new HistoryCleanup();
Assert.Multiple(() =>
{
Assert.IsTrue(contentType.IsDirty());
Assert.IsFalse(contentType.WasDirty());
Assert.AreEqual(2, contentType.GetDirtyProperties().Count());
Assert.IsTrue(contentType.IsPropertyDirty(nameof(contentType.HistoryCleanup)));
Assert.IsTrue(contentType.IsPropertyDirty(nameof(contentType.Alias)));
});
}
[Test]
public void Old_History_Cleanup_Reference_Doesnt_Make_Content_Type_Dirty()
{
var contentType = MockedContentTypes.CreateBasicContentType();
var oldHistoryCleanup = contentType.HistoryCleanup;
contentType.HistoryCleanup = new HistoryCleanup();
contentType.ResetDirtyProperties();
contentType.ResetWereDirtyProperties();
oldHistoryCleanup.KeepAllVersionsNewerThanDays = 2;
Assert.IsFalse(contentType.IsDirty());
Assert.IsFalse(contentType.WasDirty());
}
}
}
@@ -20,6 +20,7 @@ using Umbraco.Web.Models;
using Umbraco.Web;
using Umbraco.Web.PropertyEditors;
using System.Text;
using Umbraco.Core.Security;
namespace Umbraco.Tests.PropertyEditors
{
@@ -95,7 +96,16 @@ namespace Umbraco.Tests.PropertyEditors
}
};
var dataTypeService = new TestObjects.TestDataTypeService(
new DataType(new ImageCropperPropertyEditor(Mock.Of<ILogger>(), mediaFileSystem, Mock.Of<IContentSection>(), Mock.Of<IDataTypeService>())) { Id = 1, Configuration = imageCropperConfiguration });
new DataType(new ImageCropperPropertyEditor(
Mock.Of<ILogger>(),
mediaFileSystem,
Mock.Of<IContentSection>(),
Mock.Of<IDataTypeService>(),
Mock.Of<IFileStreamSecurityValidator>()))
{
Id = 1,
Configuration = imageCropperConfiguration
});
var factory = new PublishedContentTypeFactory(Mock.Of<IPublishedModelFactory>(), new PropertyValueConverterCollection(Array.Empty<IPropertyValueConverter>()), dataTypeService);
@@ -0,0 +1,129 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Moq;
using NUnit.Framework;
using Umbraco.Core.Security;
namespace Umbraco.Tests.Security
{
[TestFixture]
public class FileStreamSecurityValidatorTests
{
[Test]
public void IsConsideredSafe_True_NoAnalyzersPresent()
{
// Arrange
var sut = new FileStreamSecurityValidator(Enumerable.Empty<IFileStreamSecurityAnalyzer>());
using var memoryStream = new MemoryStream();
using var streamWriter = new StreamWriter(memoryStream);
streamWriter.Write("TestContent");
streamWriter.Flush();
memoryStream.Seek(0, SeekOrigin.Begin);
// Act
var validationResult = sut.IsConsideredSafe(memoryStream);
// Assert
Assert.IsTrue(validationResult);
}
[Test]
public void IsConsideredSafe_True_NoAnalyzerMatchesType()
{
// Arrange
var analyzerOne = new Mock<IFileStreamSecurityAnalyzer>();
analyzerOne.Setup(analyzer => analyzer.ShouldHandle(It.IsAny<Stream>()))
.Returns(false);
var analyzerTwo = new Mock<IFileStreamSecurityAnalyzer>();
analyzerTwo.Setup(analyzer => analyzer.ShouldHandle(It.IsAny<Stream>()))
.Returns(false);
var sut = new FileStreamSecurityValidator(new List<IFileStreamSecurityAnalyzer>{analyzerOne.Object,analyzerTwo.Object});
using var memoryStream = new MemoryStream();
using var streamWriter = new StreamWriter(memoryStream);
streamWriter.Write("TestContent");
streamWriter.Flush();
memoryStream.Seek(0, SeekOrigin.Begin);
// Act
var validationResult = sut.IsConsideredSafe(memoryStream);
// Assert
Assert.IsTrue(validationResult);
}
[Test]
public void IsConsideredSafe_True_AllMatchingAnalyzersReturnTrue()
{
// Arrange
var matchingAnalyzerOne = new Mock<IFileStreamSecurityAnalyzer>();
matchingAnalyzerOne.Setup(analyzer => analyzer.ShouldHandle(It.IsAny<Stream>()))
.Returns(true);
matchingAnalyzerOne.Setup(analyzer => analyzer.IsConsideredSafe(It.IsAny<Stream>()))
.Returns(true);
var matchingAnalyzerTwo = new Mock<IFileStreamSecurityAnalyzer>();
matchingAnalyzerTwo.Setup(analyzer => analyzer.ShouldHandle(It.IsAny<Stream>()))
.Returns(true);
matchingAnalyzerTwo.Setup(analyzer => analyzer.IsConsideredSafe(It.IsAny<Stream>()))
.Returns(true);
var unmatchedAnalyzer = new Mock<IFileStreamSecurityAnalyzer>();
unmatchedAnalyzer.Setup(analyzer => analyzer.ShouldHandle(It.IsAny<Stream>()))
.Returns(false);
var sut = new FileStreamSecurityValidator(new List<IFileStreamSecurityAnalyzer>{matchingAnalyzerOne.Object,matchingAnalyzerTwo.Object});
using var memoryStream = new MemoryStream();
using var streamWriter = new StreamWriter(memoryStream);
streamWriter.Write("TestContent");
streamWriter.Flush();
memoryStream.Seek(0, SeekOrigin.Begin);
// Act
var validationResult = sut.IsConsideredSafe(memoryStream);
// Assert
Assert.IsTrue(validationResult);
}
[Test]
public void IsConsideredSafe_False_AnyMatchingAnalyzersReturnFalse()
{
// Arrange
var saveMatchingAnalyzer = new Mock<IFileStreamSecurityAnalyzer>();
saveMatchingAnalyzer.Setup(analyzer => analyzer.ShouldHandle(It.IsAny<Stream>()))
.Returns(true);
saveMatchingAnalyzer.Setup(analyzer => analyzer.IsConsideredSafe(It.IsAny<Stream>()))
.Returns(true);
var unsafeMatchingAnalyzer = new Mock<IFileStreamSecurityAnalyzer>();
unsafeMatchingAnalyzer.Setup(analyzer => analyzer.ShouldHandle(It.IsAny<Stream>()))
.Returns(true);
unsafeMatchingAnalyzer.Setup(analyzer => analyzer.IsConsideredSafe(It.IsAny<Stream>()))
.Returns(false);
var unmatchedAnalyzer = new Mock<IFileStreamSecurityAnalyzer>();
unmatchedAnalyzer.Setup(analyzer => analyzer.ShouldHandle(It.IsAny<Stream>()))
.Returns(false);
var sut = new FileStreamSecurityValidator(new List<IFileStreamSecurityAnalyzer>{saveMatchingAnalyzer.Object,unsafeMatchingAnalyzer.Object});
using var memoryStream = new MemoryStream();
using var streamWriter = new StreamWriter(memoryStream);
streamWriter.Write("TestContent");
streamWriter.Flush();
memoryStream.Seek(0, SeekOrigin.Begin);
// Act
var validationResult = sut.IsConsideredSafe(memoryStream);
// Assert
Assert.IsFalse(validationResult);
}
}
}
@@ -116,6 +116,11 @@ namespace Umbraco.Tests.Testing
throw new NotImplementedException();
}
public void AddParameter(DbCommand cmd, PocoColumn column, object value)
{
throw new NotImplementedException();
}
public DbCommand CreateCommand(DbConnection connection, CommandType commandType, string sql, params object[] args)
{
throw new NotImplementedException();
+2
View File
@@ -145,6 +145,7 @@
<Compile Include="ModelsBuilder\StringExtensions.cs" />
<Compile Include="ModelsBuilder\UmbracoApplicationTests.cs" />
<Compile Include="Models\ContentScheduleTests.cs" />
<Compile Include="Models\ContentTypeHistoryCleanupTests.cs" />
<Compile Include="Models\CultureImpactTests.cs" />
<Compile Include="Models\ImageProcessorImageUrlGeneratorTest.cs" />
<Compile Include="Models\PathValidationTests.cs" />
@@ -172,6 +173,7 @@
<Compile Include="Runtimes\StandaloneTests.cs" />
<Compile Include="Routing\GetContentUrlsTests.cs" />
<Compile Include="Scheduling\ContentVersionCleanup_Tests_UnitTests.cs" />
<Compile Include="Security\FileStreamSecurityValidatorTests.cs" />
<Compile Include="Serialization\AutoInterningStringConverterTests.cs" />
<Compile Include="Scoping\ScopeUnitTests.cs" />
<Compile Include="Services\AmbiguousEventTests.cs" />
+29684 -16413
View File
File diff suppressed because it is too large Load Diff
+16 -17
View File
@@ -10,23 +10,23 @@
"watch": "gulp watch"
},
"engines": {
"node": ">=10.00.0"
"node": ">=16.0.0"
},
"dependencies": {
"ace-builds": "1.4.2",
"angular": "1.8.2",
"angular-animate": "1.7.5",
"angular-aria": "1.7.9",
"angular-chart.js": "^1.1.1",
"angular-cookies": "1.7.5",
"angular-dynamic-locale": "0.1.37",
"angular-i18n": "1.7.5",
"angular-animate": "1.8.2",
"angular-aria": "1.8.2",
"angular-chart.js": "1.1.1",
"angular-cookies": "1.8.2",
"angular-dynamic-locale": "0.1.38",
"angular-i18n": "1.8.2",
"angular-local-storage": "0.7.1",
"angular-messages": "1.7.5",
"angular-mocks": "1.7.5",
"angular-route": "1.7.5",
"angular-sanitize": "1.7.5",
"angular-touch": "1.7.5",
"angular-messages": "1.8.2",
"angular-mocks": "1.8.2",
"angular-route": "1.8.2",
"angular-sanitize": "1.8.2",
"angular-touch": "1.8.2",
"angular-ui-sortable": "0.19.0",
"animejs": "2.2.0",
"bootstrap-social": "5.1.1",
@@ -35,15 +35,14 @@
"diff": "3.5.0",
"flatpickr": "4.6.9",
"font-awesome": "4.7.0",
"jquery": "^3.6.0",
"jquery-ui-dist": "1.12.1",
"jquery": "3.6.1",
"jquery-ui-dist": "1.13.2",
"jquery-ui-touch-punch": "0.2.3",
"lazyload-js": "1.0.0",
"moment": "2.22.2",
"moment": "2.29.4",
"ng-file-upload": "12.2.13",
"nouislider": "15.5.0",
"npm": "^6.14.7",
"signalr": "2.4.0",
"signalr": "2.4.3",
"spectrum-colorpicker2": "2.0.8",
"tinymce": "4.9.11",
"typeahead.js": "0.11.1",
@@ -182,23 +182,31 @@
}
);
$scope.propertyEditorDisabled = function (property) {
$scope.propertyEditorInherited = function (property) {
if (property.unlockInvariantValue) {
return false;
return false;
}
var contentLanguage = $scope.content.language;
var canEditCulture = !contentLanguage ||
// If the property culture equals the content culture it can be edited
property.culture === contentLanguage.culture ||
// A culture-invariant property can only be edited by the default language variant
(property.culture == null && contentLanguage.isDefault);
// If the property culture equals the content culture it can be edited
property.culture === contentLanguage.culture ||
// A culture-invariant property can only be edited by the default language variant
(property.culture == null && contentLanguage.isDefault);
var canEditSegment = property.segment === $scope.content.segment;
return !canEditCulture || !canEditSegment;
}
};
$scope.propertyEditorDisabled = function (property) {
if (property.readonly) {
return true;
}
return $scope.propertyEditorInherited(property);
};
}
var directive = {
@@ -4,7 +4,7 @@
* @description
* This Resource perfomrs actions to common authentication tasks for the Umbraco backoffice user
*
* @requires $q
* @requires $q
* @requires $http
* @requires umbRequestHelper
* @requires angularHelper
@@ -28,7 +28,7 @@ function authResource($q, $http, umbRequestHelper, angularHelper) {
* });
* </pre>
* @returns {Promise} resourcePromise object
*
*
*/
get2FAProviders: function () {
@@ -118,7 +118,7 @@ function authResource($q, $http, umbRequestHelper, angularHelper) {
* .then(function(data) {
* //Do stuff for login...
* });
* </pre>
* </pre>
* @param {string} login Username of backoffice user
* @param {string} password Password of backoffice user
* @returns {Promise} resourcePromise object
@@ -147,9 +147,9 @@ function authResource($q, $http, umbRequestHelper, angularHelper) {
* There are not parameters for this since when the user has clicked on their invite email they will be partially
* logged in (but they will not be approved) so we need to use this method to verify the non approved logged in user's details.
* Using the getCurrentUser will not work since that only works for approved users
* @returns {}
* @returns {}
*/
getCurrentInvitedUser: function () {
getCurrentInvitedUser: function () {
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
@@ -173,7 +173,7 @@ function authResource($q, $http, umbRequestHelper, angularHelper) {
* .then(function(data) {
* //Do stuff for password reset request...
* });
* </pre>
* </pre>
* @param {string} email Email address of backoffice user
* @returns {Promise} resourcePromise object
*
@@ -203,7 +203,7 @@ function authResource($q, $http, umbRequestHelper, angularHelper) {
"PostRequestPasswordReset"), {
email: email
}),
'Request password reset failed for email ' + email);
'An email with password reset instructions will be sent to the specified address if it matched our records');
},
/**
@@ -220,7 +220,7 @@ function authResource($q, $http, umbRequestHelper, angularHelper) {
* .then(function(data) {
* //Allow reset of password
* });
* </pre>
* </pre>
* @param {integer} userId User Id
* @param {string} resetCode Password reset code
* @returns {Promise} resourcePromise object
@@ -251,14 +251,14 @@ function authResource($q, $http, umbRequestHelper, angularHelper) {
}),
'Password reset code validation failed for userId ' + userId + ', code' + resetCode);
},
/**
* @ngdoc method
* @name umbraco.resources.currentUserResource#getMembershipProviderConfig
* @methodOf umbraco.resources.currentUserResource
*
* @description
* Gets the configuration of the user membership provider which is used to configure the change password form
* Gets the configuration of the user membership provider which is used to configure the change password form
*/
getMembershipProviderConfig: function () {
return umbRequestHelper.resourcePromise(
@@ -283,7 +283,7 @@ function authResource($q, $http, umbRequestHelper, angularHelper) {
* .then(function(data) {
* //Password set
* });
* </pre>
* </pre>
* @param {integer} userId User Id
* @param {string} password New password
* @param {string} confirmPassword Confirmation of new password
@@ -1,9 +1,9 @@
/**
* @ngdoc service
* @name umbraco.services.mediaHelper
* @description A helper object used for dealing with media items
**/
function mediaHelper(umbRequestHelper, $log) {
* @ngdoc service
* @name umbraco.services.mediaHelper
* @description A helper object used for dealing with media items
**/
function mediaHelper(umbRequestHelper, $http, $log) {
//container of fileresolvers
var _mediaFileResolvers = {};
@@ -304,11 +304,6 @@ function mediaHelper(umbRequestHelper, $log) {
return imagePath;
}
// Check if file is a svg
if (this.getFileExtension(imagePath) === "svg") {
return imagePath;
}
// If the path is not an image we cannot get a thumb
if (!this.detectIfImageByExtension(imagePath)) {
return null;
@@ -399,6 +394,61 @@ function mediaHelper(umbRequestHelper, $log) {
var lowered = filePath.toLowerCase();
var ext = lowered.substr(lowered.lastIndexOf(".") + 1);
return ext;
},
/**
* @ngdoc function
* @name umbraco.services.mediaHelper#getProcessedImageUrl
* @methodOf umbraco.services.mediaHelper
* @function
*
* @description
* Returns image URL with configured crop and other processing parameters.
*
* @param {string} imagePath Raw image path
* @param {object} options Object describing image generation parameters:
* {
* width: <int>
* height: <int>
* focalPoint: {
* left: <int>
* top: <int>
* },
* mode: <string>
* cacheBusterValue: <string>
* crop: {
* x1: <int>
* x2: <int>
* y1: <int>
* y2: <int>
* },
* }
*/
getProcessedImageUrl: function (imagePath, options) {
if (!options) {
return imagePath;
}
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"imagesApiBaseUrl",
"GetProcessedImageUrl",
{
imagePath,
width: options.width,
height: options.height,
focalPointLeft: options.focalPoint ? options.focalPoint.left : null,
focalPointTop: options.focalPoint ? options.focalPoint.top : null,
mode: options.mode,
cacheBusterValue: options.cacheBusterValue,
cropX1: options.crop ? options.crop.x1 : null,
cropX2: options.crop ? options.crop.x2 : null,
cropY1: options.crop ? options.crop.y1 : null,
cropY2: options.crop ? options.crop.y2 : null
})),
"Failed to retrieve processed image URL for image: " + imagePath);
}
};
@@ -7,11 +7,11 @@
* A service containing all logic for all of the Umbraco TinyMCE plugins
*/
function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, stylesheetResource, macroResource, macroService,
$routeParams, umbRequestHelper, angularHelper, userService, editorService, entityResource, eventsService, localStorageService) {
$routeParams, umbRequestHelper, angularHelper, userService, editorService, entityResource, eventsService, localStorageService, mediaHelper) {
//These are absolutely required in order for the macros to render inline
//we put these as extended elements because they get merged on top of the normal allowed elements by tiny mce
var extendedValidElements = "@[id|class|style],-div[id|dir|class|align|style],ins[datetime|cite],-ul[class|style],-li[class|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align],span[id|class|style|lang]";
var extendedValidElements = "@[id|class|style],-div[id|dir|class|align|style],ins[datetime|cite],-ul[class|style],-li[class|style],-h1[id|dir|class|align|style],-h2[id|dir|class|align|style],-h3[id|dir|class|align|style],-h4[id|dir|class|align|style],-h5[id|dir|class|align|style],-h6[id|style|dir|class|align],span[id|class|style|lang],figure,figcaption";
var fallbackStyles = [{ title: "Page header", block: "h2" }, { title: "Section header", block: "h3" }, { title: "Paragraph header", block: "h4" }, { title: "Normal", block: "p" }, { title: "Quote", block: "blockquote" }, { title: "Code", block: "code" }];
// these languages are available for localization
var availableLanguages = [
@@ -222,9 +222,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
}
function uploadImageHandler(blobInfo, success, failure, progress){
let xhr, formData;
xhr = new XMLHttpRequest();
const xhr = new XMLHttpRequest();
xhr.open('POST', Umbraco.Sys.ServerVariables.umbracoUrls.tinyMceApiBaseUrl + 'UploadImage');
xhr.onloadstart = function(e) {
@@ -248,17 +246,29 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
};
xhr.onload = function () {
let json;
if (xhr.status < 200 || xhr.status >= 300) {
failure('HTTP Error: ' + xhr.status);
return;
}
json = JSON.parse(xhr.responseText);
const data = xhr.responseText;
if (!data.length > 1) {
failure('Unrecognized text string: ' + data);
return;
}
let json = {};
try {
json = JSON.parse(data);
} catch (e) {
failure('Invalid JSON: ' + data + ' - ' + e.message);
return;
}
if (!json || typeof json.tmpLocation !== 'string') {
failure('Invalid JSON: ' + xhr.responseText);
failure('Invalid JSON: ' + data);
return;
}
@@ -271,7 +281,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
success(blobInfo.blobUri());
};
formData = new FormData();
const formData = new FormData();
formData.append('file', blobInfo.blob(), blobInfo.blob().name);
xhr.send(formData);
@@ -292,21 +302,25 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
}
function sizeImageInEditor(editor, imageDomElement, imgUrl) {
var size = editor.dom.getSize(imageDomElement);
if (editor.settings.maxImageSize && editor.settings.maxImageSize !== 0) {
var newSize = imageHelper.scaleToMaxSize(editor.settings.maxImageSize, size.w, size.h);
editor.dom.setAttrib(imageDomElement, 'width', newSize.width);
editor.dom.setAttrib(imageDomElement, 'height', newSize.height);
// Images inserted via Media Picker will have a URL we can use for ImageResizer QueryStrings
// Images pasted/dragged in are not persisted to media until saved & thus will need to be added
if(imgUrl){
var src = imgUrl + "?width=" + newSize.width + "&height=" + newSize.height;
editor.dom.setAttrib(imageDomElement, 'data-mce-src', src);
if (imgUrl) {
mediaHelper.getProcessedImageUrl(imgUrl,
{
width: newSize.width,
height: newSize.height
})
.then(function (resizedImgUrl) {
editor.dom.setAttrib(imageDomElement, 'data-mce-src', resizedImgUrl);
});
}
editor.execCommand("mceAutoResize", false, null, null);
@@ -431,7 +445,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
if (args.htmlId) {
config.selector = "#" + args.htmlId;
config.selector = `[id="${args.htmlId}"]`;
} else if (args.target) {
config.target = args.target;
}
@@ -651,21 +665,19 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
stateSelector: 'img[data-udi]',
onclick: function () {
var selectedElm = editor.selection.getNode(),
currentTarget,
imgDomElement;
currentTarget;
if (selectedElm.nodeName === 'IMG') {
var img = $(selectedElm);
imgDomElement = selectedElm;
var hasUdi = img.attr("data-udi") ? true : false;
var hasDataTmpImg = img.attr("data-tmpimg") ? true : false;
currentTarget = {
altText: img.attr("alt"),
url: img.attr("src")
url: img.attr("src"),
caption: img.attr('data-caption')
};
if (hasUdi) {
@@ -682,85 +694,80 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
userService.getCurrentUser().then(function (userData) {
if (callback) {
angularHelper.safeApply($rootScope, function() {
callback(currentTarget, userData, imgDomElement);
callback(currentTarget, userData);
});
}
});
}
});
},
insertMediaInEditor: function (editor, img, imgDomElement) {
/**
* @ngdoc method
* @name umbraco.services.tinyMceService#insetMediaInEditor
* @methodOf umbraco.services.tinyMceService
*
* @description
* Inserts the image element in tinymce plugin
*
* @param {Object} editor the TinyMCE editor instance
*/
insertMediaInEditor: function (editor, img) {
if (img) {
// imgElement is only definied if updating an image
// if null/undefinied then its a BRAND new image
if(imgDomElement){
// Check if the img src has changed
// If it has we will need to do some resizing/recalc again
var hasImageSrcChanged = false;
// We need to create a NEW DOM <img> element to insert
// setting an attribute of ID to __mcenew, so we can gather a reference to the node, to be able to update its size accordingly to the size of the image.
var data = {
alt: img.altText || "",
src: (img.url) ? img.url : "nothing.jpg",
id: "__mcenew",
"data-udi": img.udi,
"data-caption": img.caption
};
var newImage = editor.dom.createHTML('img', data);
var parentElement = editor.selection.getNode().parentElement;
if(img.url !== editor.dom.getAttrib(imgDomElement, "src")){
hasImageSrcChanged = true;
if (img.caption) {
var figCaption = editor.dom.createHTML('figcaption', {}, img.caption);
var combined = newImage + figCaption;
if (parentElement.nodeName !== 'FIGURE') {
var fragment = editor.dom.createHTML('figure', {}, combined);
editor.selection.setContent(fragment);
}
// If null/undefinied it will remove the attribute
editor.dom.setAttrib(imgDomElement, "alt", img.altText);
// It's possible to pick a NEW image - so need to ensure this gets updated
if(img.udi){
editor.dom.setAttrib(imgDomElement, "data-udi", img.udi);
else {
parentElement.innerHTML = combined;
}
// It's possible to pick a NEW image - so need to ensure this gets updated
if(img.url){
editor.dom.setAttrib(imgDomElement, "src", img.url);
}
// Remove width & height attributes (ONLY if imgSrc changed)
// So native image size is used as this needed to re-calc width & height
// For the function sizeImageInEditor() & apply the image resizing querystrings etc..
if(hasImageSrcChanged){
editor.dom.setAttrib(imgDomElement, "width", null);
editor.dom.setAttrib(imgDomElement, "height", null);
//Re-calc the image dimensions
sizeImageInEditor(editor, imgDomElement, img.url);
}
} else{
// We need to create a NEW DOM <img> element to insert
// setting an attribute of ID to __mcenew, so we can gather a reference to the node, to be able to update its size accordingly to the size of the image.
var data = {
alt: img.altText || "",
src: (img.url) ? img.url : "nothing.jpg",
id: "__mcenew",
"data-udi": img.udi
};
editor.selection.setContent(editor.dom.createHTML('img', data));
// Using settimeout to wait for a DoM-render, so we can find the new element by ID.
$timeout(function () {
var imgElm = editor.dom.get("__mcenew");
editor.dom.setAttrib(imgElm, "id", null);
// When image is loaded we are ready to call sizeImageInEditor.
var onImageLoaded = function() {
sizeImageInEditor(editor, imgElm, img.url);
editor.fire("Change");
}
// Check if image already is loaded.
if(imgElm.complete === true) {
onImageLoaded();
} else {
imgElm.onload = onImageLoaded;
}
});
}
else {
//if caption is removed, remove the figure element
if (parentElement.nodeName === 'FIGURE') {
parentElement.parentElement.innerHTML = newImage;
}
else {
editor.selection.setContent(newImage);
}
}
// Using settimeout to wait for a DoM-render, so we can find the new element by ID.
$timeout(function () {
var imgElm = editor.dom.get("__mcenew");
editor.dom.setAttrib(imgElm, "id", null);
// When image is loaded we are ready to call sizeImageInEditor.
var onImageLoaded = function() {
sizeImageInEditor(editor, imgElm, img.url);
editor.fire("Change");
}
// Check if image already is loaded.
if(imgElm.complete === true) {
onImageLoaded();
} else {
imgElm.onload = onImageLoaded;
}
});
}
},
@@ -1243,7 +1250,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
// the href might be an external url, so check the value for an anchor/qs
// href has the anchor re-appended later, hence the reset here to avoid duplicating the anchor
if (!target.anchor) {
if (!target.anchor && href) {
var urlParts = href.split(/(#|\?)/);
if (urlParts.length === 3) {
href = urlParts[0];
@@ -1444,7 +1451,8 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
// Then we need to add an event listener to the editor
// That will update native browser drag & drop events
// To update the icon to show you can NOT drop something into the editor
var toolbarItems = args.editor.settings.toolbar.split(" ");
var toolbarItems = args.editor.settings.toolbar === false ? [] : args.editor.settings.toolbar.split(" ");
if(isMediaPickerEnabled(toolbarItems) === false){
// Wire up the event listener
args.editor.on('dragend dragover draggesture dragdrop drop drag', function (e) {
@@ -1602,17 +1610,27 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s
syncContent();
});
// When the element is removed from the DOM, we need to terminate
// any active watchers to ensure scopes are disposed and do not leak.
// No need to sync content as that has already happened.
args.editor.on('remove', () => stopWatch());
args.editor.on('ObjectResized', function (e) {
var qs = "?width=" + e.width + "&height=" + e.height + "&mode=max";
var srcAttr = $(e.target).attr("src");
var path = srcAttr.split("?")[0];
$(e.target).attr("data-mce-src", path + qs);
mediaHelper.getProcessedImageUrl(path, {
width: e.width,
height: e.height,
mode: "max"
}).then(function (resizedPath) {
$(e.target).attr("data-mce-src", resizedPath);
});
syncContent();
});
args.editor.on('Dirty', function (e) {
syncContent(); // Set model.value to the RTE's content
syncContent(); // Set model.value to the RTE's content
});
let self = this;
@@ -80,7 +80,7 @@
});
var saveProperties = _.map(realProperties, function (p) {
var saveProperty = _.pick(p, 'id', 'alias', 'description', 'validation', 'label', 'sortOrder', 'dataTypeId', 'groupId', 'memberCanEdit', 'showOnMemberProfile', 'isSensitiveData', 'allowCultureVariant', 'allowSegmentVariant', 'labelOnTop');
var saveProperty = _.pick(p, 'id', 'alias', 'description', 'validation', 'label', 'sortOrder', 'dataTypeId', 'dataTypeKey', 'groupId', 'memberCanEdit', 'showOnMemberProfile', 'isSensitiveData', 'allowCultureVariant', 'allowSegmentVariant', 'labelOnTop');
return saveProperty;
});
@@ -128,13 +128,13 @@
<!-- Links -->
<div class="umb-help-section" data-element="help-links" ng-if="vm.hasAccessToSettings">
<a data-element="help-link-umbraco-tv" class="umb-help-badge" href="https://umbraco.tv?utm_source=core&utm_medium=help&utm_content=link&utm_campaign=tv" target="_blank" rel="noopener">
<a data-element="help-link-umbraco-tv" class="umb-help-badge" href="https://umbra.co/ulb" target="_blank" rel="noopener">
<umb-icon icon="icon-tv-old" class="umb-help-badge__icon"></umb-icon>
<div class="umb-help-badge__title">
<localize key="help_umbracoTv">Visit umbraco.tv</localize>
<localize key="help_umbracoLearningBase">Watch our free tutorial videos</localize>
</div>
<small>
<localize key="help_theBestUmbracoVideoTutorials">The best Umbraco video tutorials</localize>
<localize key="help_umbracoLearningBaseDescription">on the Umbraco Learning Base</localize>
</small>
</a>
@@ -3,25 +3,35 @@ angular.module("umbraco")
function ($scope, localizationService, entityResource, editorService, overlayService, eventsService, mediaHelper) {
var unsubscribe = [];
var vm = this;
const vm = this;
vm.loading = true;
vm.model = $scope.model;
vm.mediaEntry = vm.model.mediaEntry;
vm.currentCrop = null;
localizationService.localizeMany([
vm.model.createFlow ? "general_cancel" : "general_close",
vm.model.createFlow ? "general_create" : "buttons_submitChanges"
]).then(function (data) {
vm.closeLabel = data[0];
vm.submitLabel = data[1];
});
vm.title = "";
vm.focalPointChanged = focalPointChanged;
vm.onImageLoaded = onImageLoaded;
vm.openMedia = openMedia;
vm.repickMedia = repickMedia;
vm.selectCrop = selectCrop;
vm.deselectCrop = deselectCrop;
vm.resetCrop = resetCrop;
vm.submitAndClose = submitAndClose;
vm.close = close;
function init() {
localizationService.localizeMany([
vm.model.createFlow ? "general_cancel" : "general_close",
vm.model.createFlow ? "general_create" : "buttons_submitChanges"
]).then(data => {
vm.closeLabel = data[0];
vm.submitLabel = data[1];
});
updateMedia();
unsubscribe.push(eventsService.on("editors.media.saved", function(name, args) {
@@ -35,6 +45,7 @@ angular.module("umbraco")
function updateMedia() {
vm.loading = true;
entityResource.getById(vm.mediaEntry.mediaKey, "Media").then(function (mediaEntity) {
vm.media = mediaEntity;
vm.imageSrc = mediaHelper.resolveFileFromEntity(mediaEntity, true);
@@ -42,11 +53,11 @@ angular.module("umbraco")
vm.hasDimensions = false;
vm.isCroppable = false;
localizationService.localize("mediaPicker_editMediaEntryLabel", [vm.media.name, vm.model.documentName]).then(function (data) {
localizationService.localize("mediaPicker_editMediaEntryLabel", [vm.media.name, vm.model.documentName]).then(data => {
vm.title = data;
});
}, function () {
localizationService.localize("mediaPicker_deletedItem").then(function (localized) {
localizationService.localize("mediaPicker_deletedItem").then(localized => {
vm.media = {
name: localized,
icon: "icon-picture",
@@ -58,15 +69,12 @@ angular.module("umbraco")
});
});
}
vm.onImageLoaded = onImageLoaded;
function onImageLoaded(isCroppable, hasDimensions) {
vm.isCroppable = isCroppable;
vm.hasDimensions = hasDimensions;
};
vm.repickMedia = repickMedia;
}
function repickMedia() {
vm.model.propertyEditor.changeMediaFor(vm.model.mediaEntry, onMediaReplaced);
}
@@ -82,24 +90,23 @@ angular.module("umbraco")
//
updateMedia();
}
vm.openMedia = openMedia;
function openMedia() {
var mediaEditor = {
const mediaEditor = {
id: vm.mediaEntry.mediaKey,
submit: function () {
submit: () => {
editorService.close();
},
close: function () {
close: () => {
editorService.close();
}
};
editorService.mediaEditor(mediaEditor);
}
vm.focalPointChanged = function(left, top) {
function focalPointChanged(left, top) {
//update the model focalpoint value
vm.mediaEntry.focalPoint = {
left: left,
@@ -109,22 +116,17 @@ angular.module("umbraco")
//set form to dirty to track changes
setDirty();
}
vm.selectCrop = selectCrop;
function selectCrop(targetCrop) {
vm.currentCrop = targetCrop;
setDirty();
// TODO: start watchin values of crop, first when changed set to dirty.
};
vm.deselectCrop = deselectCrop;
}
function deselectCrop() {
vm.currentCrop = null;
};
vm.resetCrop = resetCrop;
}
function resetCrop() {
if (vm.currentCrop) {
$scope.$evalAsync( () => {
@@ -138,18 +140,22 @@ angular.module("umbraco")
vm.imageCropperForm.$setDirty();
}
vm.submitAndClose = function () {
function submitAndClose() {
if (vm.model && vm.model.submit) {
vm.model.submit(vm.model);
}
}
vm.close = function () {
if (vm.model && vm.model.close) {
if (vm.model.createFlow === true || vm.imageCropperForm.$dirty === true) {
var labels = vm.model.createFlow === true ? ["mediaPicker_confirmCancelMediaEntryCreationHeadline", "mediaPicker_confirmCancelMediaEntryCreationMessage"] : ["prompt_discardChanges", "mediaPicker_confirmCancelMediaEntryHasChanges"];
localizationService.localizeMany(labels).then(function (localizations) {
function close() {
if (vm.model && vm.model.close)
{
if (vm.model.createFlow === true || vm.imageCropperForm.$dirty === true)
{
const labelKeys = vm.model.createFlow === true
? ["mediaPicker_confirmCancelMediaEntryCreationHeadline", "mediaPicker_confirmCancelMediaEntryCreationMessage"]
: ["prompt_discardChanges", "mediaPicker_confirmCancelMediaEntryHasChanges"];
localizationService.localizeMany(labelKeys).then(localizations => {
const confirm = {
title: localizations[0],
view: "default",
@@ -157,11 +163,11 @@ angular.module("umbraco")
submitButtonLabelKey: "general_discard",
submitButtonStyle: "danger",
closeButtonLabelKey: "prompt_stay",
submit: function () {
submit: () => {
overlayService.close();
vm.model.close(vm.model);
},
close: function () {
close: () => {
overlayService.close();
}
};
@@ -175,6 +181,7 @@ angular.module("umbraco")
}
init();
$scope.$on("$destroy", function () {
unsubscribe.forEach(x => x());
});
@@ -195,7 +195,7 @@
</div>
<div ng-messages="vm.requestPasswordResetForm.$error" class="control-group" ng-show="vm.requestPasswordResetForm.$invalid">
<p ng-message="auth" class="text-error" role="alert" tabindex="0">{{vm.errorMsg}}</p>
<p ng-message="auth" class="text-info" role="alert" tabindex="0">{{vm.errorMsg}}</p>
</div>
<div class="control-group" ng-show="vm.showEmailResetConfirmation">
@@ -11,7 +11,7 @@
data-element="property-{{property.alias}}"
ng-repeat="property in tab.properties track by property.alias"
property="property"
show-inherit="propertyEditorDisabled(property)"
show-inherit="propertyEditorInherited(property)"
inherits-from="defaultVariant.displayName">
<div ng-class="{'o-40 cursor-not-allowed': propertyEditorDisabled(property) }">
@@ -41,7 +41,7 @@
data-element="property-{{property.alias}}"
ng-repeat="property in group.properties track by property.alias"
property="property"
show-inherit="propertyEditorDisabled(property)"
show-inherit="propertyEditorInherited(property)"
inherits-from="defaultVariant.displayName">
<div ng-class="{'o-40 cursor-not-allowed': propertyEditorDisabled(property) }">
@@ -45,10 +45,10 @@
show-inherit="vm.model.variants.length > 1 && !property.culture && !activeVariant.language.isDefault"
inherits-from="defaultVariant.language.name">
<div ng-class="{'o-40 cursor-not-allowed': vm.model.variants.length > 1 && !activeVariant.language.isDefault && !property.culture && !property.unlockInvariantValue}">
<div ng-class="{'o-40 cursor-not-allowed': property.readonly || (vm.model.variants.length > 1 && !activeVariant.language.isDefault && !property.culture && !property.unlockInvariantValue)}">
<umb-property-editor
model="property"
preview="vm.model.variants.length > 1 && !activeVariant.language.isDefault && !property.culture && !property.unlockInvariantValue">
preview="property.readonly || (vm.model.variants.length > 1 && !activeVariant.language.isDefault && !property.culture && !property.unlockInvariantValue)">
</umb-property-editor>
</div>
@@ -23,7 +23,7 @@
</li>
<li class="umb-content-grid__details-item" ng-repeat="property in contentProperties">
<div class="umb-content-grid__details-label">{{ property.header }}:</div>
<div class="umb-content-grid__details-value">{{ item[property.alias] }}</div>
<div class="umb-content-grid__details-value">{{ item[property.alias].expression ? item[property.alias].expression({value: item[property.alias].value}) : item[property.alias] }}</div>
</li>
</ul>
@@ -57,10 +57,10 @@
</umb-variant-state>
</div>
<div class="umb-table-cell" ng-repeat="column in vm.itemProperties track by column.alias">
<span title="{{column.header}}: {{item[column.alias]}}">
<span title="{{column.header}}: {{item[column.alias].expression ? item[column.alias].expression({value: item[column.alias].value}) : item[column.alias]}}">
<div ng-if="!column.isSensitive">
{{item[column.alias]}}
{{item[column.alias].expression ? item[column.alias].expression({value: item[column.alias].value}) : item[column.alias]}}
</div>
<em ng-show="column.isSensitive" class="muted">
@@ -17,7 +17,7 @@
<localize key="settingsDashboard_bulletPointTwo">Ask a question in the <a class="btn-link -underline" href="https://our.umbraco.com/forum" target="_blank" rel="noopener">Community Forum</a></localize>
</li>
<li>
<localize key="settingsDashboard_bulletPointThree">Watch our <a class="btn-link -underline" href="https://umbraco.tv" target="_blank" rel="noopener">tutorial videos</a> (some are free, some require a subscription)</localize>
<localize key="settingsDashboard_bulletPointTutorials">Watch our free <a class="btn-link -underline" href="https://umbra.co/ulb" target="_blank" rel="noopener">tutorial videos on the Umbraco Learning Base</a></localize>
</li>
<li>
<localize key="settingsDashboard_bulletPointFour">Find out about our <a class="btn-link -underline" href="https://umbraco.com/products/" target="_blank" rel="noopener">productivity boosting tools and commercial support</a></localize>
@@ -158,7 +158,7 @@ function MemberEditController($scope, $routeParams, $location, $http, $q, appSta
//it's a child item, just sync the ui node to the parent
navigationService.syncTree({ tree: "member", path: path.substring(0, path.lastIndexOf(",")).split(","), forceReload: initialLoad !== true });
//if this is a child of a list view and it's the initial load of the editor, we need to get the tree node
//if this is a child of a list view and it's the initial load of the editor, we need to get the tree node
// from the server so that we can load in the actions menu.
umbRequestHelper.resourcePromise(
$http.get(content.treeNodeUrl),
@@ -189,7 +189,7 @@ function MemberEditController($scope, $routeParams, $location, $http, $q, appSta
$scope.page.saveButtonState = "busy";
//anytime a user is changing a member's password without the oldPassword, we are in effect resetting it so we need to set that flag here
var passwordProp = _.find(contentEditingHelper.getAllProps($scope.content), function (e) { return e.alias === '_umb_password' });
var passwordProp = _.find($scope.content.membershipProperties, function (e) { return e.alias === '_umb_password' });
if (passwordProp && passwordProp.value && (typeof passwordProp.value.reset !== 'undefined') && !passwordProp.value.reset) {
//so if the admin is not explicitly resetting the password, flag it for resetting if a new password is being entered
passwordProp.value.reset = !passwordProp.value.oldPassword && passwordProp.config.allowManuallyChangingPassword;
@@ -667,6 +667,7 @@ angular.module("umbraco")
$scope.removeControl = function (cell, $index) {
$scope.currentControl = null;
cell.controls.splice($index, 1);
currentForm.$setDirty();
};
$scope.percentage = function (spans) {
@@ -146,6 +146,7 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time
}
var listParamsForCurrent = $routeParams.id == $routeParams.list;
$scope.options = {
useInfiniteEditor: $scope.model.config.useInfiniteEditor === true,
pageSize: $scope.model.config.pageSize ? $scope.model.config.pageSize : 10,
@@ -169,6 +170,7 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time
allowBulkDelete: $scope.model.config.bulkActionPermissions.allowBulkDelete,
cultureName: $routeParams.cculture ? $routeParams.cculture : $routeParams.mculture
};
_.each($scope.options.includeProperties, function (property) {
property.nameExp = !!property.nameTemplate
? $interpolate(property.nameTemplate)
@@ -268,12 +270,13 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time
$scope.getContent = function (contentId) {
$scope.reloadView($scope.contentId, true);
}
};
$scope.reloadView = function (id, reloadActiveNode) {
if (!id) {
return;
}
$scope.viewLoaded = false;
$scope.folders = [];
@@ -327,7 +330,7 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time
$scope.options.pageNumber = 1;
$scope.reloadView($scope.contentId);
}
}
};
$scope.onSearchStartTyping = function() {
$scope.viewLoaded = false;
@@ -689,9 +692,18 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time
}
if (e.nameExp) {
var newValue = e.nameExp({ value });
if (newValue && (newValue = newValue.trim())) {
value = newValue;
if (/{{.*\s*\w+\s*\|\s*\w+\s*.*}}/.test(e.nameTemplate)) { //check whether the name template has a filter
value = {
value,
expression: e.nameExp
};
}
else {
var newValue = e.nameExp({ value });
if (newValue && (newValue = newValue.trim())) {
value = newValue;
}
}
}
@@ -708,13 +720,15 @@ function listViewController($scope, $interpolate, $routeParams, $injector, $time
}
function initView() {
var id = $routeParams.id;
if (id === undefined) {
// no ID found in route params - don't list anything as we don't know for sure where we are
return;
}
$scope.contentId = id;
// Get current id for node to load it's children
$scope.contentId = editorState.current ? editorState.current.id : id;
$scope.isTrashed = editorState.current ? editorState.current.trashed : id === "-20" || id === "-21";
$scope.options.allowBulkPublish = $scope.options.allowBulkPublish && !$scope.isTrashed;
+2 -2
View File
@@ -348,9 +348,9 @@
<WebProjectProperties>
<UseIIS>False</UseIIS>
<AutoAssignPort>True</AutoAssignPort>
<DevelopmentServerPort>8180</DevelopmentServerPort>
<DevelopmentServerPort>8189</DevelopmentServerPort>
<DevelopmentServerVPath>/</DevelopmentServerVPath>
<IISUrl>http://localhost:8180</IISUrl>
<IISUrl>http://localhost:8189</IISUrl>
<NTLMAuthentication>False</NTLMAuthentication>
<UseCustomServer>False</UseCustomServer>
<CustomServerUrl>
@@ -339,6 +339,7 @@
<key alias="renameFolderFailed">Omdøbning af mappen med id %0% fejlede</key>
<key alias="dragAndDropYourFilesIntoTheArea">Træk dine filer ind i dropzonen for, at uploade dem til mediebiblioteket.</key>
<key alias="uploadNotAllowed">Upload er ikke tiladt på denne lokation</key>
<key alias="fileSecurityValidationFailure">En eller flere fil sikkerhedsvalideringer har fejlet</key>
</area>
<area alias="member">
<key alias="createNewMember">Opret et nyt medlem</key>
@@ -329,6 +329,7 @@
<key alias="orClickHereToUpload">or click here to choose files</key>
<key alias="dragFilesHereToUpload">You can drag files here to upload</key>
<key alias="disallowedFileType">Cannot upload this file, it does not have an approved file type</key>
<key alias="disallowedMediaType">Cannot upload this file, the media type with alias '%0%' is not allowed here</key>
<key alias="invalidFileName">Cannot upload this file, it does not have a valid file name</key>
<key alias="maxFileSize">Max file size is</key>
<key alias="mediaRoot">Media root</key>
@@ -337,6 +338,7 @@
<key alias="createFolderFailed">Failed to create a folder under parent id %0%</key>
<key alias="renameFolderFailed">Failed to rename the folder with id %0%</key>
<key alias="dragAndDropYourFilesIntoTheArea">Drag and drop your file(s) into the area</key>
<key alias="fileSecurityValidationFailure">One or more file security validations have failed</key>
</area>
<area alias="member">
<key alias="createNewMember">Create a new member</key>
@@ -1415,6 +1417,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
<key alias="theBestUmbracoVideoTutorials">The best Umbraco video tutorials</key>
<key alias="umbracoForum">Visit our.umbraco.com</key>
<key alias="umbracoTv">Visit umbraco.tv</key>
<key alias="umbracoLearningBase">Watch our free tutorial videos</key>
<key alias="umbracoLearningBaseDescription">on the Umbraco Learning Base</key>
</area>
<area alias="settings">
<key alias="defaulttemplate">Default template</key>
@@ -1448,6 +1452,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
<key alias="invalidUserPermissionsText">Insufficient user permissions, could not complete the operation</key>
<key alias="operationCancelledHeader">Cancelled</key>
<key alias="operationCancelledText">Operation was cancelled by a 3rd party add-in</key>
<key alias="folderUploadNotAllowed">This file is being uploaded as part of a folder, but creating a new folder is not allowed here</key>
<key alias="folderCreationNotAllowed">Creating a new folder is not allowed here</key>
<key alias="contentPublishedFailedByEvent">Publishing was cancelled by a 3rd party add-in</key>
<key alias="contentTypeDublicatePropertyType">Property type already exists</key>
<key alias="contentTypePropertyTypeCreated">Property type created</key>
@@ -2506,9 +2512,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
Ask a question in the <a class="btn-link -underline" href="https://our.umbraco.com/forum" target="_blank" rel="noopener">Community Forum</a>
]]>
</key>
<key alias="bulletPointThree">
<key alias="bulletPointTutorials">
<![CDATA[
Watch our <a class="btn-link -underline" href="https://umbraco.tv" target="_blank" rel="noopener">tutorial videos</a> (some are free, some require a subscription)
Watch our free <a class="btn-link -underline" href="https://umbra.co/ulb" target="_blank" rel="noopener">tutorial videos on the Umbraco Learning Base</a>
]]>
</key>
<key alias="bulletPointFour">
@@ -331,8 +331,9 @@
<area alias="media">
<key alias="clickToUpload">Click to upload</key>
<key alias="orClickHereToUpload">or click here to choose files</key>
<key alias="dragFilesHereToUpload">You can drag files here to upload.</key>
<key alias="dragFilesHereToUpload">You can drag files here to upload.</key>
<key alias="disallowedFileType">Cannot upload this file, it does not have an approved file type</key>
<key alias="disallowedMediaType">Cannot upload this file, the media type with alias '%0%' is not allowed here</key>
<key alias="invalidFileName">Cannot upload this file, it does not have a valid file name</key>
<key alias="maxFileSize">Max file size is</key>
<key alias="mediaRoot">Media root</key>
@@ -343,6 +344,7 @@
<key alias="renameFolderFailed">Failed to rename the folder with id %0%</key>
<key alias="dragAndDropYourFilesIntoTheArea">Drag and drop your file(s) into the area</key>
<key alias="uploadNotAllowed">Upload is not allowed in this location.</key>
<key alias="fileSecurityValidationFailure">One or more file security validations have failed</key>
</area>
<area alias="member">
<key alias="createNewMember">Create a new member</key>
@@ -1425,6 +1427,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
<key alias="theBestUmbracoVideoTutorials">The best Umbraco video tutorials</key>
<key alias="umbracoForum">Visit our.umbraco.com</key>
<key alias="umbracoTv">Visit umbraco.tv</key>
<key alias="umbracoLearningBase">Watch our free tutorial videos</key>
<key alias="umbracoLearningBaseDescription">on the Umbraco Learning Base</key>
</area>
<area alias="settings">
<key alias="defaulttemplate">Default template</key>
@@ -1460,6 +1464,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
<key alias="invalidUserPermissionsText">Insufficient user permissions, could not complete the operation</key>
<key alias="operationCancelledHeader">Cancelled</key>
<key alias="operationCancelledText">Operation was cancelled by a 3rd party add-in</key>
<key alias="folderUploadNotAllowed">This file is being uploaded as part of a folder, but creating a new folder is not allowed here</key>
<key alias="folderCreationNotAllowed">Creating a new folder is not allowed here</key>
<key alias="contentPublishedFailedByEvent">Publishing was cancelled by a 3rd party add-in</key>
<key alias="contentTypeDublicatePropertyType">Property type already exists</key>
<key alias="contentTypePropertyTypeCreated">Property type created</key>
<key alias="contentTypePropertyTypeCreatedText"><![CDATA[Name: %0% <br /> DataType: %1%]]></key>
@@ -2544,9 +2551,9 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
Ask a question in the <a class="btn-link -underline" href="https://our.umbraco.com/forum" target="_blank" rel="noopener">Community Forum</a>
]]>
</key>
<key alias="bulletPointThree">
<key alias="bulletPointTutorials">
<![CDATA[
Watch our <a class="btn-link -underline" href="https://umbraco.tv" target="_blank" rel="noopener">tutorial videos</a> (some are free, some require a subscription)
Watch our free <a class="btn-link -underline" href="https://umbra.co/ulb" target="_blank" rel="noopener">tutorial videos on the Umbraco Learning Base</a>
]]>
</key>
<key alias="bulletPointFour">
@@ -336,6 +336,7 @@
<key alias="renameFolderFailed">Kan de map met id %0% niet hernoemen</key>
<key alias="dragAndDropYourFilesIntoTheArea">Sleep en zet je bestand(en) neer in dit gebied</key>
<key alias="uploadNotAllowed">Upload is niet toegelaten in deze locatie.</key>
<key alias="fileSecurityValidationFailure">Een of meerdere veiligheid validaties zijn gefaald voor het bestand</key>
</area>
<area alias="member">
<key alias="createNewMember">Maak nieuw lid aan</key>
@@ -335,7 +335,12 @@ namespace Umbraco.Web.Editors
{
throw new HttpResponseException(HttpStatusCode.BadRequest);
}
var identityUser = await SignInManager.UserManager.FindByEmailAsync(model.Email);
Random random = new Random();
await Task.Delay(random.Next(400, 2500)); // To randomize response time preventing user enumeration
if (identityUser != null)
{
var user = Services.UserService.GetByEmail(model.Email);
@@ -349,11 +354,20 @@ namespace Umbraco.Web.Editors
UserExtensions.GetUserCulture(identityUser.Culture, Services.TextService, GlobalSettings),
new[] { identityUser.UserName, callbackUrl });
await UserManager.SendEmailAsync(identityUser.Id,
Services.TextService.Localize("login", "resetPasswordEmailCopySubject",
// Ensure the culture of the found user is used for the email!
UserExtensions.GetUserCulture(identityUser.Culture, Services.TextService, GlobalSettings)),
message);
try
{
await UserManager.SendEmailAsync(identityUser.Id,
Services.TextService.Localize("login", "resetPasswordEmailCopySubject",
// Ensure the culture of the found user is used for the email!
UserExtensions.GetUserCulture(identityUser.Culture, Services.TextService,
GlobalSettings)),
message);
}
catch (Exception ex)
{
Logger.Error<AuthenticationController>(ex, "Error sending email, please check your SMTP configuration: {ErrorMessage}", ex.Message);
return Request.CreateResponse(HttpStatusCode.OK);
}
UserManager.RaiseForgotPasswordRequestedEvent(user.Id);
}
+18 -9
View File
@@ -306,7 +306,7 @@ namespace Umbraco.Web.Editors
switch (type)
{
case Core.Constants.Trees.PartialViews:
if (IsDirectory(virtualPath, SystemDirectories.PartialViews))
if (IsDirectory(virtualPath, SystemDirectories.PartialViews, Current.FileSystems.PartialViewsFileSystem))
{
Services.FileService.DeletePartialViewFolder(virtualPath);
return Request.CreateResponse(HttpStatusCode.OK);
@@ -318,7 +318,7 @@ namespace Umbraco.Web.Editors
return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No Partial View or folder found with the specified path");
case Core.Constants.Trees.PartialViewMacros:
if (IsDirectory(virtualPath, SystemDirectories.MacroPartials))
if (IsDirectory(virtualPath, SystemDirectories.MacroPartials, Current.FileSystems.MacroPartialsFileSystem))
{
Services.FileService.DeletePartialViewMacroFolder(virtualPath);
return Request.CreateResponse(HttpStatusCode.OK);
@@ -330,7 +330,7 @@ namespace Umbraco.Web.Editors
return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No Partial View Macro or folder found with the specified path");
case Core.Constants.Trees.Scripts:
if (IsDirectory(virtualPath, SystemDirectories.Scripts))
if (IsDirectory(virtualPath, SystemDirectories.Scripts, Current.FileSystems.ScriptsFileSystem))
{
Services.FileService.DeleteScriptFolder(virtualPath);
return Request.CreateResponse(HttpStatusCode.OK);
@@ -343,7 +343,7 @@ namespace Umbraco.Web.Editors
return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No Script or folder found with the specified path");
case Core.Constants.Trees.Stylesheets:
if (IsDirectory(virtualPath, SystemDirectories.Css))
if (IsDirectory(virtualPath, SystemDirectories.Css, Current.FileSystems.StylesheetsFileSystem))
{
Services.FileService.DeleteStyleSheetFolder(virtualPath);
return Request.CreateResponse(HttpStatusCode.OK);
@@ -634,14 +634,23 @@ namespace Umbraco.Web.Editors
return value;
}
private bool IsDirectory(string virtualPath, string systemDirectory)
private bool IsDirectory(string virtualPath, string systemDirectory, IFileSystem fileSystem)
{
var path = IOHelper.MapPath(systemDirectory + "/" + virtualPath);
var dirInfo = new DirectoryInfo(path);
// If you turn off indexing in Windows this will have the attribute:
// `FileAttributes.Directory | FileAttributes.NotContentIndexed`
return (dirInfo.Attributes & FileAttributes.Directory) != 0;
// If it's a physical filesystem check with directory info
if (fileSystem.CanAddPhysical)
{
var dirInfo = new DirectoryInfo(path);
// If you turn off indexing in Windows this will have the attribute:
// `FileAttributes.Directory | FileAttributes.NotContentIndexed`
return (dirInfo.Attributes & FileAttributes.Directory) != 0;
}
// Otherwise check the filesystem abstraction to see if the folder exists
// Since this is used for delete, it presumably exists if we're trying to delete it
return fileSystem.DirectoryExists(path);
}
// this is an internal class for passing stylesheet data from the client to the controller while editing
+16 -2
View File
@@ -376,7 +376,7 @@ namespace Umbraco.Web.Editors
[HttpPost]
public IDictionary<string, ContentItemDisplay> GetEmptyByAliases(ContentTypesByAliases contentTypesByAliases)
{
// It's important to do this operation within a scope to reduce the amount of readlock queries.
// It's important to do this operation within a scope to reduce the amount of readlock queries.
using var scope = _scopeProvider.CreateScope(autoComplete: true);
var contentTypes = contentTypesByAliases.ContentTypeAliases.Select(alias => Services.ContentTypeService.Get(alias));
return GetEmpties(contentTypes, contentTypesByAliases.ParentId).ToDictionary(x => x.ContentTypeAlias);
@@ -2019,6 +2019,8 @@ namespace Umbraco.Web.Editors
var variantIndex = 0;
var defaultCulture = _allLangs.Value.Values.FirstOrDefault(x => x.IsDefault)?.IsoCode;
//loop through each variant, set the correct name and property values
foreach (var variant in contentSave.Variants)
{
@@ -2033,6 +2035,12 @@ namespace Umbraco.Web.Editors
if (variant.Culture.IsNullOrWhiteSpace())
throw new InvalidOperationException($"Cannot set culture name without a culture.");
contentSave.PersistedContent.SetCultureName(variant.Name, variant.Culture);
// If the variant culture is the default culture we also want to update the name on the Content itself.
if (variant.Culture.Equals(defaultCulture, StringComparison.InvariantCultureIgnoreCase))
{
contentSave.PersistedContent.Name = variant.Name;
}
}
else
{
@@ -2072,6 +2080,12 @@ namespace Umbraco.Web.Editors
variantIndex++;
}
// Map IsDirty cultures to edited cultures, to make it easier to verify changes on specific variants on Saving and Saved events.
var editedCultures = contentSave.PersistedContent.CultureInfos.Values
.Where(x => x.IsDirty())
.Select(x => x.Culture);
contentSave.PersistedContent.SetCultureEdited(editedCultures);
// handle template
if (string.IsNullOrWhiteSpace(contentSave.TemplateAlias)) // cleared: clear if not already null
{
@@ -2423,7 +2437,7 @@ namespace Umbraco.Web.Editors
Items = results.Select(x => new ContentVersionMetaViewModel(x))
};
}
[HttpPost]
[EnsureUserPermissionForContent("contentId", ActionUpdate.ActionLetter)]
public HttpResponseMessage PostSetContentVersionPreventCleanup(int contentId, int versionId, bool preventCleanup)
@@ -521,7 +521,7 @@ namespace Umbraco.Web.Editors
[HttpPost]
public HttpResponseMessage Import(string file)
{
var filePath = Path.Combine(IOHelper.MapPath(SystemDirectories.Data), file);
var filePath = Path.Combine(IOHelper.MapPath(SystemDirectories.TempFileUploads), file);
if (string.IsNullOrEmpty(file) || !System.IO.File.Exists(filePath))
{
return Request.CreateResponse(HttpStatusCode.NotFound);
@@ -576,35 +576,35 @@ namespace Umbraco.Web.Editors
var fileName = file.Headers.ContentDisposition.FileName.Trim(Constants.CharArrays.DoubleQuote);
var ext = fileName.Substring(fileName.LastIndexOf('.') + 1).ToLower();
var destFileName = Path.Combine(root, fileName);
if (Path.GetFullPath(destFileName).StartsWith(Path.GetFullPath(root)))
var destFilePath = Path.Combine(root, fileName);
if (Path.GetFullPath(destFilePath).StartsWith(Path.GetFullPath(root)))
{
try
{
// due to a bug before 8.7.0 we didn't delete temp files, so we need to make sure to delete before
// moving else you get errors and the upload fails without a message in the UI (there's a JS error)
if(System.IO.File.Exists(destFileName))
System.IO.File.Delete(destFileName);
if(System.IO.File.Exists(destFilePath))
System.IO.File.Delete(destFilePath);
// renaming the file because MultipartFormDataStreamProvider has created a random fileName instead of using the name from the
// content-disposition for more than 6 years now. Creating a CustomMultipartDataStreamProvider deriving from MultipartFormDataStreamProvider
// seems like a cleaner option, but I'm not sure where to put it and renaming only takes one line of code.
System.IO.File.Move(result.FileData[0].LocalFileName, destFileName);
System.IO.File.Move(result.FileData[0].LocalFileName, destFilePath);
}
catch (Exception ex)
{
Logger.Error<ContentTypeController, string>(ex, "Error uploading udt file to App_Data: {File}", destFileName);
Logger.Error<ContentTypeController, string>(ex, "Error uploading udt file to App_Data: {File}", destFilePath);
}
if (ext.InvariantEquals("udt"))
{
model.TempFileName = destFileName;
model.TempFileName = fileName;
var xd = new XmlDocument
{
XmlResolver = null
};
xd.Load(model.TempFileName);
xd.Load(destFilePath);
model.Alias = xd.DocumentElement?.SelectSingleNode("//DocumentType/Info/Alias")?.FirstChild.Value;
model.Name = xd.DocumentElement?.SelectSingleNode("//DocumentType/Info/Name")?.FirstChild.Value;
@@ -612,7 +612,7 @@ namespace Umbraco.Web.Editors
else
{
// Cleanup the temp file
System.IO.File.Delete(destFileName);
System.IO.File.Delete(destFilePath);
model.Notifications.Add(new Notification(
Services.TextService.Localize("speechBubbles", "operationFailedHeader"),
Services.TextService.Localize("media", "disallowedFileType"),
@@ -12,6 +12,8 @@ using Umbraco.Web.WebApi;
using System.Linq;
using Newtonsoft.Json;
using Umbraco.Core;
using Umbraco.Core.Security;
using Umbraco.Web.Composing;
using Umbraco.Web.Security;
using Umbraco.Web.WebApi.Filters;
@@ -155,7 +157,9 @@ namespace Umbraco.Web.Editors
public async Task<HttpResponseMessage> PostSetAvatar()
{
//borrow the logic from the user controller
return await UsersController.PostSetAvatarInternal(Request, Services.UserService, AppCaches.RuntimeCache, Security.GetUserId().ResultOr(0));
// This is unbelievable... This controller has no real constructor using DI, instead everything is resolved from the Current service locator.
// So I guess we'll just do that again here
return await UsersController.PostSetAvatarInternal(Request, Services.UserService, Current.Factory.GetInstance<IFileStreamSecurityValidator>(), AppCaches.RuntimeCache, Security.GetUserId().ResultOr(0));
}
/// <summary>
+19 -6
View File
@@ -20,6 +20,7 @@ using Umbraco.Core.Dashboards;
using Umbraco.Core.Models;
using Umbraco.Web.Services;
using System.Web.Http;
using Umbraco.Core.Telemetry;
namespace Umbraco.Web.Editors
{
@@ -34,18 +35,28 @@ namespace Umbraco.Web.Editors
{
private readonly IDashboardService _dashboardService;
private readonly IContentDashboardSettings _dashboardSettings;
private readonly ISiteIdentifierService _siteIdentifierService;
/// <summary>
/// Initializes a new instance of the <see cref="DashboardController"/> with all its dependencies.
/// </summary>
public DashboardController(IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor,
ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger,
IRuntimeState runtimeState, IDashboardService dashboardService, UmbracoHelper umbracoHelper,
IContentDashboardSettings dashboardSettings)
public DashboardController(
IGlobalSettings globalSettings,
IUmbracoContextAccessor umbracoContextAccessor,
ISqlContext sqlContext,
ServiceContext services,
AppCaches appCaches,
IProfilingLogger logger,
IRuntimeState runtimeState,
IDashboardService dashboardService,
UmbracoHelper umbracoHelper,
IContentDashboardSettings dashboardSettings,
ISiteIdentifierService siteIdentifierService)
: base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper)
{
_dashboardService = dashboardService;
_dashboardSettings = dashboardSettings;
_siteIdentifierService = siteIdentifierService;
}
//we have just one instance of HttpClient shared for the entire application
@@ -60,17 +71,19 @@ namespace Umbraco.Web.Editors
var language = user.Language;
var version = UmbracoVersion.SemanticVersion.ToSemanticString();
var isAdmin = user.IsAdmin();
_siteIdentifierService.TryGetOrCreateSiteIdentifier(out var siteIdentifier);
VerifyDashboardSource(baseUrl);
var url = string.Format("{0}{1}?section={2}&allowed={3}&lang={4}&version={5}&admin={6}",
var url = string.Format("{0}{1}?section={2}&allowed={3}&lang={4}&version={5}&admin={6}&siteid={7}",
baseUrl,
_dashboardSettings.ContentDashboardPath,
section,
allowedSections,
language,
version,
isAdmin);
isAdmin,
siteIdentifier.ToString());
var key = "umbraco-dynamic-dashboard-" + language + allowedSections.Replace(",", "-") + section;
var content = AppCaches.RuntimeCache.GetCacheItem<JObject>(key);
@@ -16,6 +16,7 @@ using Umbraco.Web.Actions;
using Umbraco.Web.Composing;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Security;
using Umbraco.Web.WebApi;
namespace Umbraco.Web.Editors.Filters
{
@@ -117,7 +118,7 @@ namespace Umbraco.Web.Editors.Filters
break;
case ContentSaveAction.Schedule:
permissionToCheck.Add(ActionUpdate.ActionLetter);
permissionToCheck.Add(ActionToPublish.ActionLetter);
permissionToCheck.Add(ActionPublish.ActionLetter);
contentToCheck = contentItem.PersistedContent;
contentIdToCheck = contentToCheck.Id;
break;
@@ -217,6 +218,9 @@ namespace Umbraco.Web.Editors.Filters
if (accessResult == ContentPermissionsHelper.ContentAccess.NotFound)
throw new HttpResponseException(HttpStatusCode.NotFound);
if(accessResult == ContentPermissionsHelper.ContentAccess.Denied)
actionContext.Response = actionContext.Request.CreateUserNoAccessResponse();
return accessResult == ContentPermissionsHelper.ContentAccess.Granted;
}
}
+92 -6
View File
@@ -2,14 +2,13 @@
using System.IO;
using System.Net;
using System.Net.Http;
using System.Web;
using Umbraco.Core.Composing;
using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.IO;
using Umbraco.Core.Models;
using Umbraco.Web.Media;
using Umbraco.Web.Mvc;
using Umbraco.Web.WebApi;
using Constants = Umbraco.Core.Constants;
namespace Umbraco.Web.Editors
{
@@ -60,20 +59,29 @@ namespace Umbraco.Web.Editors
/// </remarks>
public HttpResponseMessage GetResized(string imagePath, int width)
{
// We have to use HttpUtility to encode the path here, for non-ASCII characters
// We cannot use the WebUtility, as we only want to encode the path, and not the entire string
var encodedImagePath = HttpUtility.UrlPathEncode(imagePath);
var ext = Path.GetExtension(imagePath);
// check if imagePath is local to prevent open redirect
if (!IsAllowed(encodedImagePath))
{
return Request.CreateResponse(HttpStatusCode.Unauthorized);
}
// we need to check if it is an image by extension
if (_contentSection.IsImageFile(ext) == false)
return Request.CreateResponse(HttpStatusCode.NotFound);
//redirect to ImageProcessor thumbnail with rnd generated from last modified time of original media file
var response = Request.CreateResponse(HttpStatusCode.Found);
DateTimeOffset? imageLastModified = null;
try
{
imageLastModified = _mediaFileSystem.GetLastModified(imagePath);
}
catch (Exception)
{
@@ -84,11 +92,89 @@ namespace Umbraco.Web.Editors
}
var rnd = imageLastModified.HasValue ? $"&rnd={imageLastModified:yyyyMMddHHmmss}" : null;
var imageUrl = _imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(imagePath) { UpScale = false, Width = width, AnimationProcessMode = "first", ImageCropMode = "max", CacheBusterValue = rnd });
var imageUrl = _imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(encodedImagePath) { UpScale = false, Width = width, AnimationProcessMode = "first", ImageCropMode = "max", CacheBusterValue = rnd });
var response = Request.CreateResponse(HttpStatusCode.Found);
response.Headers.Location = new Uri(imageUrl, UriKind.RelativeOrAbsolute);
return response;
}
private bool IsAllowed(string encodedImagePath)
{
if(Uri.IsWellFormedUriString(encodedImagePath, UriKind.Relative))
{
return true;
}
if (_contentSection is ContentElement contentElement)
{
var builder = new UriBuilder(encodedImagePath);
foreach (var allowedMediaHost in contentElement.AllowedMediaHosts)
{
if (string.Equals(builder.Host, allowedMediaHost, StringComparison.InvariantCultureIgnoreCase))
{
return true;
}
}
}
return false;
}
/// <summary>
/// Gets a processed image for the image at the given path
/// </summary>
/// <param name="imagePath"></param>
/// <param name="width"></param>
/// <param name="height"></param>
/// <param name="focalPointLeft"></param>
/// <param name="focalPointTop"></param>
/// <param name="mode"></param>
/// <param name="cacheBusterValue"></param>
/// <param name="cropX1"></param>
/// <param name="cropX2"></param>
/// <param name="cropY1"></param>
/// <param name="cropY2"></param>
/// <returns></returns>
/// <remarks>
/// If there is no media, image property or image file is found then this will return not found.
/// </remarks>
public string GetProcessedImageUrl(
string imagePath,
int? width = null,
int? height = null,
decimal? focalPointLeft = null,
decimal? focalPointTop = null,
string mode = "max",
string cacheBusterValue = "",
decimal? cropX1 = null,
decimal? cropX2 = null,
decimal? cropY1 = null,
decimal? cropY2 = null)
{
var options = new ImageUrlGenerationOptions(imagePath)
{
Width = width,
Height = height,
ImageCropMode = mode,
CacheBusterValue = cacheBusterValue
};
if (focalPointLeft.HasValue && focalPointTop.HasValue)
{
options.FocalPoint =
new ImageUrlGenerationOptions.FocalPointPosition(focalPointLeft.Value, focalPointTop.Value);
}
else if (cropX1.HasValue && cropX2.HasValue && cropY1.HasValue && cropY2.HasValue)
{
options.Crop =
new ImageUrlGenerationOptions.CropCoordinates(cropX1.Value, cropY1.Value, cropX2.Value, cropY2.Value);
}
return _imageUrlGenerator.GetImageUrl(options);
}
}
}
@@ -18,6 +18,7 @@ namespace Umbraco.Web.Editors
/// </summary>
[PluginController("UmbracoApi")]
[PrefixlessBodyModelValidator]
[UmbracoApplicationAuthorize(Core.Constants.Applications.Settings)]
public class LanguageController : UmbracoAuthorizedJsonController
{
/// <summary>
+182 -53
View File
@@ -37,6 +37,7 @@ using Umbraco.Web.ContentApps;
using Umbraco.Web.Editors.Binders;
using Umbraco.Web.Editors.Filters;
using Umbraco.Core.Models.Entities;
using Umbraco.Core.Security;
namespace Umbraco.Web.Editors
{
@@ -49,10 +50,39 @@ namespace Umbraco.Web.Editors
[MediaControllerControllerConfiguration]
public class MediaController : ContentControllerBase
{
public MediaController(PropertyEditorCollection propertyEditors, IGlobalSettings globalSettings, IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services, AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper)
[Obsolete("Use the constructor specifying all dependencies instead.")]
public MediaController(PropertyEditorCollection propertyEditors, IGlobalSettings globalSettings,
IUmbracoContextAccessor umbracoContextAccessor, ISqlContext sqlContext, ServiceContext services,
AppCaches appCaches, IProfilingLogger logger, IRuntimeState runtimeState, UmbracoHelper umbracoHelper)
: this(propertyEditors,
globalSettings,
umbracoContextAccessor,
sqlContext,
services,
appCaches,
logger,
runtimeState,
umbracoHelper,
Current.Factory.GetInstance<IFileStreamSecurityValidator>())
{
}
public MediaController(
PropertyEditorCollection propertyEditors,
IGlobalSettings globalSettings,
IUmbracoContextAccessor umbracoContextAccessor,
ISqlContext sqlContext,
ServiceContext services,
AppCaches appCaches,
IProfilingLogger logger,
IRuntimeState runtimeState,
UmbracoHelper umbracoHelper,
IFileStreamSecurityValidator fileStreamSecurityValidator)
: base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper)
{
_propertyEditors = propertyEditors ?? throw new ArgumentNullException(nameof(propertyEditors));
_fileStreamSecurityValidator = fileStreamSecurityValidator;
}
/// <summary>
@@ -236,6 +266,7 @@ namespace Umbraco.Web.Editors
private int[] _userStartNodes;
private readonly PropertyEditorCollection _propertyEditors;
private readonly IFileStreamSecurityValidator _fileStreamSecurityValidator;
protected int[] UserStartNodes
{
@@ -598,6 +629,13 @@ namespace Umbraco.Web.Editors
{
var intParentId = GetParentIdAsInt(folder.ParentId, validatePermissions:true);
var isFolderAllowed = IsFolderCreationAllowedHere(intParentId);
if (isFolderAllowed == false)
{
throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse(
Services.TextService.Localize("speechBubbles", "folderCreationNotAllowed")));
}
var mediaService = Services.MediaService;
var f = mediaService.CreateMedia(folder.Name, intParentId, Constants.Conventions.MediaTypes.Folder);
@@ -640,10 +678,16 @@ namespace Umbraco.Web.Editors
var tempFiles = new PostedFiles();
var mediaService = Services.MediaService;
var localizedTextService = Services.TextService;
//in case we pass a path with a folder in it, we will create it and upload media to it.
if (result.FormData.ContainsKey("path"))
{
if (!IsFolderCreationAllowedHere(parentId))
{
AddCancelMessage(tempFiles, message: "speechBubbles/folderUploadNotAllowed");
return Request.CreateResponse(HttpStatusCode.OK, tempFiles);
}
var folders = result.FormData["path"].Split(Constants.CharArrays.ForwardSlash);
@@ -653,7 +697,7 @@ namespace Umbraco.Web.Editors
IMedia folderMediaItem;
//if uploading directly to media root and not a subfolder
if (parentId == -1)
if (parentId == Constants.System.Root)
{
//look for matching folder
folderMediaItem =
@@ -691,6 +735,44 @@ namespace Umbraco.Web.Editors
}
}
var mediaTypeAlias = string.Empty;
var allMediaTypes = Services.MediaTypeService.GetAll().ToList();
var allowedContentTypes = new HashSet<IMediaType>();
if (parentId != Constants.System.Root)
{
var mediaFolderItem = mediaService.GetById(parentId);
var mediaFolderType = allMediaTypes.FirstOrDefault(x => x.Alias == mediaFolderItem.ContentType.Alias);
if (mediaFolderType != null)
{
IMediaType mediaTypeItem = null;
foreach (ContentTypeSort allowedContentType in mediaFolderType.AllowedContentTypes)
{
IMediaType checkMediaTypeItem = allMediaTypes.FirstOrDefault(x => x.Id == allowedContentType.Id.Value);
allowedContentTypes.Add(checkMediaTypeItem);
var fileProperty = checkMediaTypeItem?.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == Constants.Conventions.Media.File);
if (fileProperty != null)
{
mediaTypeItem = checkMediaTypeItem;
}
}
//Only set the permission-based mediaType if we only allow 1 specific file under this parent.
if (allowedContentTypes.Count == 1 && mediaTypeItem != null)
{
mediaTypeAlias = mediaTypeItem.Alias;
}
}
}
else
{
var typesAllowedAtRoot = allMediaTypes.Where(x => x.AllowedAsRoot).ToList();
allowedContentTypes.UnionWith(typesAllowedAtRoot);
}
//get the files
foreach (var file in result.FileData)
{
@@ -698,81 +780,105 @@ namespace Umbraco.Web.Editors
var safeFileName = fileName.ToSafeFileName();
var ext = safeFileName.Substring(safeFileName.LastIndexOf('.') + 1).ToLower();
if (Current.Configs.Settings().Content.IsFileAllowedForUpload(ext))
if (!Current.Configs.Settings().Content.IsFileAllowedForUpload(ext))
{
var mediaType = Constants.Conventions.MediaTypes.File;
tempFiles.Notifications.Add(new Notification(
localizedTextService.Localize("speechBubbles", "operationFailedHeader"),
localizedTextService.Localize("media", "disallowedFileType"),
NotificationStyle.Warning));
continue;
}
if (string.IsNullOrEmpty(mediaTypeAlias))
{
mediaTypeAlias = Constants.Conventions.MediaTypes.File;
if (result.FormData["contentTypeAlias"] == Constants.Conventions.MediaTypes.AutoSelect)
{
var mediaTypes = Services.MediaTypeService.GetAll();
// Look up MediaTypes
foreach (var mediaTypeItem in mediaTypes)
foreach (var mediaTypeItem in allMediaTypes)
{
var fileProperty = mediaTypeItem.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == "umbracoFile");
if (fileProperty != null) {
var dataTypeKey = fileProperty.DataTypeKey;
var dataType = Services.DataTypeService.GetDataType(dataTypeKey);
if (dataType != null && dataType.Configuration is IFileExtensionsConfig fileExtensionsConfig) {
var fileExtensions = fileExtensionsConfig.FileExtensions;
if (fileExtensions != null)
{
if (fileExtensions.Where(x => x.Value == ext).Count() != 0)
{
mediaType = mediaTypeItem.Alias;
break;
}
}
}
var fileProperty = mediaTypeItem.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == Constants.Conventions.Media.File);
if (fileProperty == null)
{
continue;
}
var dataTypeKey = fileProperty.DataTypeKey;
var dataType = Services.DataTypeService.GetDataType(dataTypeKey);
if (dataType == null || dataType.Configuration is not IFileExtensionsConfig fileExtensionsConfig)
{
continue;
}
var fileExtensions = fileExtensionsConfig.FileExtensions;
if (fileExtensions == null || fileExtensions.All(x => x.Value != ext))
{
continue;
}
mediaTypeAlias = mediaTypeItem.Alias;
break;
}
// If media type is still File then let's check if it's an image.
if (mediaType == Constants.Conventions.MediaTypes.File && Current.Configs.Settings().Content.ImageFileTypes.Contains(ext))
if (mediaTypeAlias == Constants.Conventions.MediaTypes.File && Current.Configs.Settings().Content.ImageFileTypes.Contains(ext))
{
mediaType = Constants.Conventions.MediaTypes.Image;
mediaTypeAlias = Constants.Conventions.MediaTypes.Image;
}
}
else
{
mediaType = result.FormData["contentTypeAlias"];
mediaTypeAlias = result.FormData["contentTypeAlias"];
}
}
var mediaItemName = fileName.ToFriendlyName();
if (allowedContentTypes.Any(x => x.Alias == mediaTypeAlias) == false)
{
tempFiles.Notifications.Add(new Notification(
localizedTextService.Localize("speechBubbles", "operationFailedHeader"),
localizedTextService.Localize("media", "disallowedMediaType", new[] { mediaTypeAlias }),
NotificationStyle.Warning));
continue;
}
var f = mediaService.CreateMedia(mediaItemName, parentId, mediaType, Security.CurrentUser.Id);
var fileInfo = new FileInfo(file.LocalFileName);
var mediaFileStream = fileInfo.OpenReadWithRetry();
if (mediaFileStream == null)
{
throw new InvalidOperationException("Could not acquire file stream");
}
var fileInfo = new FileInfo(file.LocalFileName);
var fs = fileInfo.OpenReadWithRetry();
if (fs == null) throw new InvalidOperationException("Could not acquire file stream");
using (fs)
{
f.SetValue(Services.ContentTypeBaseServices, Constants.Conventions.Media.File,fileName, fs);
}
if (_fileStreamSecurityValidator != null && _fileStreamSecurityValidator.IsConsideredSafe(mediaFileStream) is false)
{
tempFiles.Notifications.Add(new Notification(
localizedTextService.Localize("speechBubbles", "operationFailedHeader"),
localizedTextService.Localize("media", "fileSecurityValidationFailure"),
NotificationStyle.Warning));
continue;
}
var saveResult = mediaService.Save(f, Security.CurrentUser.Id);
if (saveResult == false)
{
AddCancelMessage(tempFiles,
message: Services.TextService.Localize("speechBubbles", "operationCancelledText") + " -- " + mediaItemName);
}
else
{
tempFiles.UploadedFiles.Add(new ContentPropertyFile
{
FileName = fileName,
PropertyAlias = Constants.Conventions.Media.File,
TempFilePath = file.LocalFileName
});
}
var mediaItemName = fileName.ToFriendlyName();
var createdMediaItem = mediaService.CreateMedia(mediaItemName, parentId, mediaTypeAlias, Security.CurrentUser.Id);
using (mediaFileStream)
{
createdMediaItem.SetValue(Services.ContentTypeBaseServices, Constants.Conventions.Media.File, fileName, mediaFileStream);
}
var saveResult = mediaService.Save(createdMediaItem, Security.CurrentUser.Id);
if (saveResult == false)
{
AddCancelMessage(tempFiles, message: "speechBubbles/operationCancelledText" + " -- " + mediaItemName);
}
else
{
tempFiles.Notifications.Add(new Notification(
Services.TextService.Localize("speechBubbles", "operationFailedHeader"),
Services.TextService.Localize("media", "disallowedFileType"),
NotificationStyle.Warning));
tempFiles.UploadedFiles.Add(new ContentPropertyFile
{
FileName = fileName,
PropertyAlias = Constants.Conventions.Media.File,
TempFilePath = file.LocalFileName
});
}
}
@@ -792,6 +898,29 @@ namespace Umbraco.Web.Editors
return Request.CreateResponse(HttpStatusCode.OK, tempFiles);
}
private bool IsFolderCreationAllowedHere(int parentId)
{
var allMediaTypes = Services.MediaTypeService.GetAll().ToList();
var isFolderAllowed = false;
if (parentId == Constants.System.Root)
{
var typesAllowedAtRoot = allMediaTypes.Where(ct => ct.AllowedAsRoot).ToList();
isFolderAllowed = typesAllowedAtRoot.Any(x => x.Alias == Constants.Conventions.MediaTypes.Folder);
}
else
{
var parentMediaType = Services.MediaService.GetById(parentId);
var mediaFolderType = allMediaTypes.FirstOrDefault(x => x.Alias == parentMediaType.ContentType.Alias);
if (mediaFolderType != null)
{
isFolderAllowed =
mediaFolderType.AllowedContentTypes.Any(x => x.Alias == Constants.Conventions.MediaTypes.Folder);
}
}
return isFolderAllowed;
}
private IMedia FindInChildren(int mediaId, string nameToFind, string contentTypeAlias)
{
const int pageSize = 500;
@@ -5,9 +5,11 @@ using Umbraco.Web.Composing;
using Umbraco.Web.PublishedCache;
using Umbraco.Web.PublishedCache.NuCache;
using Umbraco.Web.WebApi;
using Umbraco.Web.WebApi.Filters;
namespace Umbraco.Web.Editors
{
[UmbracoApplicationAuthorize(Core.Constants.Applications.Settings)]
public class NuCacheStatusController : UmbracoAuthorizedApiController
{
private readonly IPublishedSnapshotService _publishedSnapshotService;
@@ -25,6 +25,8 @@ namespace Umbraco.Web.Editors
_logger = logger;
}
private static bool IsEnabled => Current.Configs.Settings().WebRouting.DisableRedirectUrlTracking == false;
/// <summary>
/// Returns true/false of whether redirect tracking is enabled or not
/// </summary>
@@ -32,9 +34,8 @@ namespace Umbraco.Web.Editors
[HttpGet]
public IHttpActionResult GetEnableState()
{
var enabled = Current.Configs.Settings().WebRouting.DisableRedirectUrlTracking == false;
var userIsAdmin = UmbracoContext.Security.CurrentUser.IsAdmin();
return Ok(new { enabled, userIsAdmin });
return Ok(new { enabled = IsEnabled, userIsAdmin });
}
//add paging
@@ -83,6 +84,11 @@ namespace Umbraco.Web.Editors
[HttpPost]
public IHttpActionResult DeleteRedirectUrl(Guid id)
{
if (IsEnabled is false)
{
return BadRequest("Redirect tracking is disabled.");
}
var redirectUrlService = Services.RedirectUrlService;
redirectUrlService.Delete(id);
return Ok();
@@ -3,6 +3,7 @@ using System.Linq;
using Umbraco.Core;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Mvc;
using Umbraco.Web.WebApi.Filters;
namespace Umbraco.Web.Editors
{
@@ -10,6 +11,7 @@ namespace Umbraco.Web.Editors
/// The API controller used for retrieving available stylesheets
/// </summary>
[PluginController("UmbracoApi")]
[UmbracoApplicationAuthorize(Core.Constants.Applications.Settings)]
public class StylesheetController : UmbracoAuthorizedJsonController
{
public IEnumerable<Stylesheet> GetAll()
@@ -32,4 +34,4 @@ namespace Umbraco.Web.Editors
}
}
}
}
+27 -1
View File
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
@@ -28,6 +29,18 @@ namespace Umbraco.Web.Editors
private IMediaService _mediaService;
private IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider;
private readonly Dictionary<string, string> _fileContentTypeMappings = new()
{
{ "image/png", "png" },
{ "image/jpeg", "jpg" },
{ "image/gif", "gif" },
{ "image/bmp", "bmp" },
{ "image/x-icon", "ico" },
{ "image/svg+xml", "svg" },
{ "image/tiff", "tiff" },
{ "image/webp", "webp" },
};
public TinyMceController(IMediaService mediaService, IContentTypeBaseServiceProvider contentTypeBaseServiceProvider)
{
@@ -76,7 +89,20 @@ namespace Umbraco.Web.Editors
var file = result.FileData[0];
var fileName = file.Headers.ContentDisposition.FileName.Trim(Constants.CharArrays.DoubleQuote).TrimEnd();
var safeFileName = fileName.ToSafeFileName();
var ext = safeFileName.Substring(safeFileName.LastIndexOf('.') + 1).ToLower();
string ext;
var fileExtensionIndex = safeFileName.LastIndexOf('.');
if (fileExtensionIndex is not -1)
{
ext = safeFileName.Substring(fileExtensionIndex + 1).ToLowerInvariant();
}
else
{
_fileContentTypeMappings.TryGetValue(file.Headers.ContentType.MediaType, out var fileExtension);
ext = fileExtension ?? string.Empty;
// safeFileName will not have a file extension, so we need to add it back
safeFileName += $".{ext}";
}
if (Current.Configs.Settings().Content.IsFileAllowedForUpload(ext) == false || Current.Configs.Settings().Content.ImageFileTypes.Contains(ext) == false)
{
+35 -5
View File
@@ -7,6 +7,7 @@ using System.Net;
using System.Net.Http;
using System.Runtime.Serialization;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
@@ -49,6 +50,7 @@ namespace Umbraco.Web.Editors
public class UsersController : UmbracoAuthorizedJsonController
{
private readonly IUmbracoSettingsSection _umbracoSettingsSection;
private readonly IFileStreamSecurityValidator _fileStreamSecurityValidator;
public UsersController(
IGlobalSettings globalSettings,
@@ -59,10 +61,27 @@ namespace Umbraco.Web.Editors
IProfilingLogger logger,
IRuntimeState runtimeState,
UmbracoHelper umbracoHelper,
IUmbracoSettingsSection umbracoSettingsSection)
IUmbracoSettingsSection umbracoSettingsSection,
IFileStreamSecurityValidator fileStreamSecurityValidator)
: base(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper)
{
_umbracoSettingsSection = umbracoSettingsSection;
_fileStreamSecurityValidator = fileStreamSecurityValidator;
}
[Obsolete("Use the overload specifying all dependencies instead")]
public UsersController(
IGlobalSettings globalSettings,
IUmbracoContextAccessor umbracoContextAccessor,
ISqlContext sqlContext,
ServiceContext services,
AppCaches appCaches,
IProfilingLogger logger,
IRuntimeState runtimeState,
UmbracoHelper umbracoHelper,
IUmbracoSettingsSection umbracoSettingsSection)
: this(globalSettings, umbracoContextAccessor, sqlContext, services, appCaches, logger, runtimeState, umbracoHelper, umbracoSettingsSection, Current.Factory.GetInstance<IFileStreamSecurityValidator>())
{
}
/// <summary>
@@ -83,10 +102,15 @@ namespace Umbraco.Web.Editors
[AdminUsersAuthorize]
public async Task<HttpResponseMessage> PostSetAvatar(int id)
{
return await PostSetAvatarInternal(Request, Services.UserService, AppCaches.RuntimeCache, id);
return await PostSetAvatarInternal(Request, Services.UserService, _fileStreamSecurityValidator, AppCaches.RuntimeCache, id);
}
internal static async Task<HttpResponseMessage> PostSetAvatarInternal(HttpRequestMessage request, IUserService userService, IAppCache cache, int id)
internal static async Task<HttpResponseMessage> PostSetAvatarInternal(
HttpRequestMessage request,
IUserService userService,
IFileStreamSecurityValidator fileStreamSecurityValidator,
IAppCache cache,
int id)
{
if (request.Content.IsMimeMultipartContent() == false)
{
@@ -120,14 +144,20 @@ namespace Umbraco.Web.Editors
var fileName = file.Headers.ContentDisposition.FileName.Trim(Constants.CharArrays.DoubleQuote).TrimEnd();
var safeFileName = fileName.ToSafeFileName();
var ext = safeFileName.Substring(safeFileName.LastIndexOf('.') + 1).ToLower();
const string allowedAvatarFileTypes = "jpeg,jpg,gif,bmp,png,tiff,tif,webp";
if (Current.Configs.Settings().Content.DisallowedUploadFiles.Contains(ext) == false)
if (allowedAvatarFileTypes.Contains(ext) == true && Current.Configs.Settings().Content.DisallowedUploadFiles.Contains(ext) == false)
{
//generate a path of known data, we don't want this path to be guessable
user.Avatar = "UserAvatars/" + (user.Id + safeFileName).GenerateHash<SHA1>() + "." + ext;
using (var fs = System.IO.File.OpenRead(file.LocalFileName))
{
if (fileStreamSecurityValidator.IsConsideredSafe(fs) is false)
{
return request.CreateValidationErrorResponse("The uploaded file is not permitted due to security reasons");
}
Current.MediaFileSystem.AddFile(user.Avatar, fs, true);
}
@@ -549,7 +579,7 @@ namespace Umbraco.Web.Editors
var emailBody = Services.TextService.Localize("user", "inviteEmailCopyFormat",
//Ensure the culture of the found user is used for the email!
UserExtensions.GetUserCulture(to.Language, Services.TextService, GlobalSettings),
new[] { userDisplay.Name, from, message, inviteUri.ToString(), fromEmail });
new[] { userDisplay.Name, from, Regex.Replace(WebUtility.HtmlEncode(message),"(\r\n|\r|\n)", "<br/>"), inviteUri.ToString(), fromEmail });
await UserManager.EmailService.SendAsync(
//send the special UmbracoEmailMessage which configures it's own sender
@@ -1,10 +1,6 @@
using System;
using System.IO;
using System.Threading.Tasks;
using System.Xml.Linq;
using System.Threading.Tasks;
using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Telemetry;
using Umbraco.Web.Install.Models;
namespace Umbraco.Web.Install.InstallSteps
@@ -14,54 +10,20 @@ namespace Umbraco.Web.Install.InstallSteps
PerformsAppRestart = false)]
internal class TelemetryIdentifierStep : InstallSetupStep<object>
{
private readonly IProfilingLogger _logger;
private readonly IUmbracoSettingsSection _settings;
private readonly ISiteIdentifierService _siteIdentifierService;
public TelemetryIdentifierStep(IProfilingLogger logger, IUmbracoSettingsSection settings)
public TelemetryIdentifierStep(
IUmbracoSettingsSection settings,
ISiteIdentifierService siteIdentifierService)
{
_logger = logger;
_settings = settings;
_siteIdentifierService = siteIdentifierService;
}
public override Task<InstallSetupResult> ExecuteAsync(object model)
{
// Generate GUID
var telemetrySiteIdentifier = Guid.NewGuid();
// Modify the XML to add a new GUID site identifier
// hack: ensure this does not trigger a restart
using (ChangesMonitor.Suspended())
{
var umbracoSettingsPath = IOHelper.MapPath(SystemFiles.UmbracoSettings);
if(File.Exists(umbracoSettingsPath) == false)
{
// Log an error
_logger.Error<TelemetryIdentifierStep>("Unable to find umbracoSettings.config file to add telemetry site identifier");
return Task.FromResult<InstallSetupResult>(null);
}
try
{
var umbracoConfigXml = XDocument.Load(umbracoSettingsPath, LoadOptions.PreserveWhitespace);
if (umbracoConfigXml.Root != null)
{
var backofficeElement = umbracoConfigXml.Root.Element("backOffice");
if (backofficeElement == null)
return Task.FromResult<InstallSetupResult>(null);
// Will add ID attribute if it does not exist
backofficeElement.SetAttributeValue("id", telemetrySiteIdentifier.ToString());
// Save file back down
umbracoConfigXml.Save(umbracoSettingsPath, SaveOptions.DisableFormatting);
}
}
catch (Exception ex)
{
_logger.Error<TelemetryIdentifierStep>(ex, "Couldn't update umbracoSettings.config with a backoffice with a telemetry site identifier");
}
}
_siteIdentifierService.TryCreateSiteIdentifier(out _);
return Task.FromResult<InstallSetupResult>(null);
}
@@ -87,8 +87,7 @@ namespace Umbraco.Web.Models.Mapping
{
MapSaveToTypeBase<DocumentTypeSave, PropertyTypeBasic>(source, target, context);
MapComposition(source, target, alias => _contentTypeService.Get(alias));
target.HistoryCleanup = source.HistoryCleanup;
MapHistoryCleanup(source, target);
target.AllowedTemplates = source.AllowedTemplates
.Where(x => x != null)
@@ -99,6 +98,34 @@ namespace Umbraco.Web.Models.Mapping
target.SetDefaultTemplate(source.DefaultTemplate == null ? null : _fileService.GetTemplate(source.DefaultTemplate));
}
private static void MapHistoryCleanup(DocumentTypeSave source, IContentType target)
{
// If source history cleanup is null we don't have to map all properties
if (source.HistoryCleanup is null)
{
target.HistoryCleanup = null;
return;
}
// We need to reset the dirty properties, because it is otherwise true, just because the json serializer has set properties
target.HistoryCleanup.ResetDirtyProperties(false);
if (target.HistoryCleanup.PreventCleanup != source.HistoryCleanup.PreventCleanup)
{
target.HistoryCleanup.PreventCleanup = source.HistoryCleanup.PreventCleanup;
}
if (target.HistoryCleanup.KeepAllVersionsNewerThanDays != source.HistoryCleanup.KeepAllVersionsNewerThanDays)
{
target.HistoryCleanup.KeepAllVersionsNewerThanDays = source.HistoryCleanup.KeepAllVersionsNewerThanDays;
}
if (target.HistoryCleanup.KeepLatestVersionPerDayForDays !=
source.HistoryCleanup.KeepLatestVersionPerDayForDays)
{
target.HistoryCleanup.KeepLatestVersionPerDayForDays = source.HistoryCleanup.KeepLatestVersionPerDayForDays;
}
}
// no MapAll - take care
private void Map(MediaTypeSave source, IMediaType target, MapperContext context)
{
@@ -253,7 +280,12 @@ namespace Umbraco.Web.Models.Mapping
target.Id = source.Id;
if (source.GroupId > 0)
target.PropertyGroupId = new Lazy<int>(() => source.GroupId, false);
{
if (target.PropertyGroupId?.Value != source.GroupId)
{
target.PropertyGroupId = new Lazy<int>(() => source.GroupId, false);
}
}
target.Alias = source.Alias;
target.Description = source.Description;
@@ -476,13 +508,23 @@ namespace Umbraco.Web.Models.Mapping
// ensure no duplicate alias, then assign the group properties collection
EnsureUniqueAliases(destProperties);
destGroup.PropertyTypes = new PropertyTypeCollection(isPublishing, destProperties);
if (destGroup.PropertyTypes.SupportsPublishing != isPublishing || destGroup.PropertyTypes.SequenceEqual(destProperties) is false)
{
destGroup.PropertyTypes = new PropertyTypeCollection(isPublishing, destProperties);
}
destGroups.Add(destGroup);
}
// ensure no duplicate name, then assign the groups collection
EnsureUniqueAliases(destGroups);
target.PropertyGroups = new PropertyGroupCollection(destGroups);
if (target.PropertyGroups.SequenceEqual(destGroups) is false)
{
target.PropertyGroups = new PropertyGroupCollection(destGroups);
}
// because the property groups collection was rebuilt, there is no need to remove
// the old groups - they are just gone and will be cleared by the repository
@@ -96,7 +96,7 @@ namespace Umbraco.Web.Models.Mapping
// Umbraco.Code.MapAll -Udi -Trashed
private static void Map(IUser source, EntityBasic target, MapperContext context)
{
target.Alias = source.Username;
target.Alias = source.Key.ToString();
target.Icon = Constants.Icons.User;
target.Id = source.Id;
target.Key = source.Key;
@@ -2,11 +2,13 @@
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Security;
using Umbraco.Core.Services;
using Umbraco.Web.Media;
@@ -22,16 +24,27 @@ namespace Umbraco.Web.PropertyEditors
{
private readonly IMediaFileSystem _mediaFileSystem;
private readonly IContentSection _contentSection;
private readonly IFileStreamSecurityValidator _fileStreamSecurityValidator;
private readonly UploadAutoFillProperties _uploadAutoFillProperties;
public FileUploadPropertyEditor(ILogger logger, IMediaFileSystem mediaFileSystem, IContentSection contentSection)
public FileUploadPropertyEditor(
ILogger logger,
IMediaFileSystem mediaFileSystem,
IContentSection contentSection,
IFileStreamSecurityValidator fileStreamSecurityValidator)
: base(logger)
{
_mediaFileSystem = mediaFileSystem ?? throw new ArgumentNullException(nameof(mediaFileSystem));
_contentSection = contentSection;
_fileStreamSecurityValidator = fileStreamSecurityValidator;
_uploadAutoFillProperties = new UploadAutoFillProperties(_mediaFileSystem, logger, contentSection);
}
[Obsolete("Use constructor that accepts IFileStreamSecurityValidator.")]
public FileUploadPropertyEditor(ILogger logger, IMediaFileSystem mediaFileSystem, IContentSection contentSection)
: this(logger, mediaFileSystem, contentSection, Current.Factory.GetInstance<IFileStreamSecurityValidator>())
{
}
/// <inheritdoc />
protected override IConfigurationEditor CreateConfigurationEditor() => new FileUploadConfigurationEditor();
@@ -42,7 +55,7 @@ namespace Umbraco.Web.PropertyEditors
/// <returns>The corresponding property value editor.</returns>
protected override IDataValueEditor CreateValueEditor()
{
var editor = new FileUploadPropertyValueEditor(Attribute, _mediaFileSystem);
var editor = new FileUploadPropertyValueEditor(Attribute, _mediaFileSystem, _fileStreamSecurityValidator);
editor.Validators.Add(new UploadFileTypeValidator());
return editor;
}
@@ -7,6 +7,7 @@ using Umbraco.Core;
using Umbraco.Core.IO;
using Umbraco.Core.Models.Editors;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Security;
namespace Umbraco.Web.PropertyEditors
{
@@ -16,11 +17,16 @@ namespace Umbraco.Web.PropertyEditors
internal class FileUploadPropertyValueEditor : DataValueEditor
{
private readonly IMediaFileSystem _mediaFileSystem;
private readonly IFileStreamSecurityValidator _fileStreamSecurityValidator;
public FileUploadPropertyValueEditor(DataEditorAttribute attribute, IMediaFileSystem mediaFileSystem)
public FileUploadPropertyValueEditor(
DataEditorAttribute attribute,
IMediaFileSystem mediaFileSystem,
IFileStreamSecurityValidator fileStreamSecurityValidator)
: base(attribute)
{
_mediaFileSystem = mediaFileSystem ?? throw new ArgumentNullException(nameof(mediaFileSystem));
_fileStreamSecurityValidator = fileStreamSecurityValidator;
}
/// <summary>
@@ -109,6 +115,11 @@ namespace Umbraco.Web.PropertyEditors
using (var filestream = File.OpenRead(file.TempFilePath))
{
if (_fileStreamSecurityValidator.IsConsideredSafe(filestream) is false)
{
return null;
}
// TODO: Here it would make sense to do the auto-fill properties stuff but the API doesn't allow us to do that right
// since we'd need to be able to return values for other properties from these methods
@@ -4,11 +4,13 @@ using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Security;
using Umbraco.Core.Services;
using Umbraco.Web.Media;
@@ -30,29 +32,42 @@ namespace Umbraco.Web.PropertyEditors
private readonly IMediaFileSystem _mediaFileSystem;
private readonly IContentSection _contentSettings;
private readonly IDataTypeService _dataTypeService;
private readonly IFileStreamSecurityValidator _fileStreamSecurityValidator;
private readonly UploadAutoFillProperties _autoFillProperties;
/// <summary>
/// Initializes a new instance of the <see cref="ImageCropperPropertyEditor"/> class.
/// </summary>
public ImageCropperPropertyEditor(ILogger logger, IMediaFileSystem mediaFileSystem, IContentSection contentSettings, IDataTypeService dataTypeService)
public ImageCropperPropertyEditor(
ILogger logger,
IMediaFileSystem mediaFileSystem,
IContentSection contentSettings,
IDataTypeService dataTypeService,
IFileStreamSecurityValidator fileStreamSecurityValidator)
: base(logger)
{
_mediaFileSystem = mediaFileSystem ?? throw new ArgumentNullException(nameof(mediaFileSystem));
_contentSettings = contentSettings ?? throw new ArgumentNullException(nameof(contentSettings));
_dataTypeService = dataTypeService;
_fileStreamSecurityValidator = fileStreamSecurityValidator;
// TODO: inject?
_autoFillProperties = new UploadAutoFillProperties(_mediaFileSystem, logger, _contentSettings);
}
[Obsolete("Use constructor that accepts IFileStreamSecurityValidator.")]
public ImageCropperPropertyEditor(ILogger logger, IMediaFileSystem mediaFileSystem, IContentSection contentSettings, IDataTypeService dataTypeService)
: this(logger, mediaFileSystem, contentSettings, dataTypeService, Current.Factory.GetInstance<IFileStreamSecurityValidator>())
{
}
public string GetMediaPath(object value) => GetFileSrcFromPropertyValue(value, out _, false);
/// <summary>
/// Creates the corresponding property value editor.
/// </summary>
/// <returns>The corresponding property value editor.</returns>
protected override IDataValueEditor CreateValueEditor() => new ImageCropperPropertyValueEditor(Attribute, Logger, _mediaFileSystem);
protected override IDataValueEditor CreateValueEditor() => new ImageCropperPropertyValueEditor(Attribute, Logger, _mediaFileSystem, _fileStreamSecurityValidator);
/// <summary>
/// Creates the corresponding preValue editor.
@@ -131,7 +146,7 @@ namespace Umbraco.Web.PropertyEditors
/// Returns the "src" property from the json structure if the value is formatted correctly
/// </summary>
/// <param name="propVal"></param>
/// <param name="deserializedValue">The deserialized <see cref="JObject"/> value</param>
/// <param name="deserializedValue">The deserialized <see cref="JObject"/> value</param>
/// <param name="relative">Should the path returned be the application relative path</param>
/// <returns></returns>
private string GetFileSrcFromPropertyValue(object propVal, out JObject deserializedValue, bool relative = true)
@@ -8,6 +8,7 @@ using Umbraco.Core.Models;
using Umbraco.Core.Models.Editors;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.PropertyEditors.ValueConverters;
using Umbraco.Core.Security;
using Umbraco.Core.Services;
using File = System.IO.File;
@@ -20,12 +21,18 @@ namespace Umbraco.Web.PropertyEditors
{
private readonly ILogger _logger;
private readonly IMediaFileSystem _mediaFileSystem;
private readonly IFileStreamSecurityValidator _fileStreamSecurityValidator;
public ImageCropperPropertyValueEditor(DataEditorAttribute attribute, ILogger logger, IMediaFileSystem mediaFileSystem)
public ImageCropperPropertyValueEditor(
DataEditorAttribute attribute,
ILogger logger,
IMediaFileSystem mediaFileSystem,
IFileStreamSecurityValidator fileStreamSecurityValidator)
: base(attribute)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_mediaFileSystem = mediaFileSystem ?? throw new ArgumentNullException(nameof(mediaFileSystem));
_fileStreamSecurityValidator = fileStreamSecurityValidator;
}
/// <summary>
@@ -166,6 +173,11 @@ namespace Umbraco.Web.PropertyEditors
// TODO: Here it would make sense to do the auto-fill properties stuff but the API doesn't allow us to do that right
// since we'd need to be able to return values for other properties from these methods
if (_fileStreamSecurityValidator.IsConsideredSafe(filestream) is false)
{
return null;
}
_mediaFileSystem.AddFile(filepath, filestream, true); // must overwrite!
}
@@ -18,6 +18,10 @@ namespace Umbraco.Web.PropertyEditors
private readonly ILogger _logger;
private readonly IMediaService _mediaService;
private readonly IContentTypeBaseServiceProvider _contentTypeBaseServiceProvider;
private readonly string _tempFolderAbsolutePath;
// An array to contain a list of URLs that we have already processed to avoid dupes
private static readonly Dictionary<string, GuidUdi> UploadedImages = new();
const string TemporaryImageDataAttribute = "data-tmpimg";
@@ -27,6 +31,69 @@ namespace Umbraco.Web.PropertyEditors
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
_mediaService = mediaService ?? throw new ArgumentNullException(nameof(mediaService));
_contentTypeBaseServiceProvider = contentTypeBaseServiceProvider ?? throw new ArgumentNullException(nameof(contentTypeBaseServiceProvider));
_tempFolderAbsolutePath = Path.GetFullPath(IOHelper.MapPath(SystemDirectories.TempImageUploads));
}
/// <summary>
/// Used by the RTE (and grid RTE) for converting inline base64 images to Media items
/// </summary>
/// <param name="html"></param>
/// <param name="mediaParentFolder"></param>
/// <param name="userId"></param>
/// <param name="imageUrlGenerator"></param>
/// <returns></returns>
internal string FindAndPersistBase64Images(string html, Guid mediaParentFolder, int userId, IImageUrlGenerator imageUrlGenerator)
{
// Find all img's that has data-tmpimg attribute
// Use HTML Agility Pack - https://html-agility-pack.net
var htmlDoc = new HtmlDocument();
htmlDoc.LoadHtml(html);
var imagesWithDataUris = htmlDoc.DocumentNode.SelectNodes("//img");
if (imagesWithDataUris == null || imagesWithDataUris.Count == 0)
return html;
foreach (var img in imagesWithDataUris)
{
var srcValue = img.GetAttributeValue("src", string.Empty);
// Ignore src-less images
if (string.IsNullOrEmpty(srcValue))
continue;
// Take only images that have a "data:image" uri into consideration
if (!srcValue.StartsWith("data:image"))
continue;
// Create tmp image by scanning the srcValue
// the value will look like "data:image/jpg;base64,abc" where the first part
// is the mimetype and the second (after the comma) is the image blob
var tokens = srcValue.Split(',');
var dataUriInfo = tokens[0];
var mimeType = dataUriInfo.Split(';')[0].Replace("data:", string.Empty);
var base64imageString = tokens[1];
// Create an unique folder path to help with concurrent users to avoid filename clash
var imageTempPath = IOHelper.MapPath(SystemDirectories.TempImageUploads + IOHelper.DirSepChar + Guid.NewGuid().ToString());
// Ensure image temp path exists
if (Directory.Exists(imageTempPath) == false)
Directory.CreateDirectory(imageTempPath);
// To get the filename, we simply manipulate the mimetype into a filename
var filePath = mimeType.Replace('/', '.');
var tmpImgPath = imageTempPath + IOHelper.DirSepChar + filePath;
var absoluteTmpImgPath = IOHelper.MapPath(tmpImgPath);
// Convert the base64 content to a byte array and save the bytes directly to a file
// this method should work for most use-cases
System.IO.File.WriteAllBytes(absoluteTmpImgPath, Convert.FromBase64String(base64imageString));
// When the temp file has been created, we can persist it
PersistMediaItem(mediaParentFolder, userId, img, tmpImgPath, imageUrlGenerator);
}
return htmlDoc.DocumentNode.OuterHtml;
}
/// <summary>
@@ -35,6 +102,7 @@ namespace Umbraco.Web.PropertyEditors
/// <param name="html"></param>
/// <param name="mediaParentFolder"></param>
/// <param name="userId"></param>
/// <param name="imageUrlGenerator"></param>
/// <returns></returns>
internal string FindAndPersistPastedTempImages(string html, Guid mediaParentFolder, int userId, IImageUrlGenerator imageUrlGenerator)
{
@@ -47,10 +115,6 @@ namespace Umbraco.Web.PropertyEditors
if (tmpImages == null || tmpImages.Count == 0)
return html;
// An array to contain a list of URLs that
// we have already processed to avoid dupes
var uploadedImages = new Dictionary<string, GuidUdi>();
foreach (var img in tmpImages)
{
// The data attribute contains the path to the tmp img to persist as a media item
@@ -59,85 +123,101 @@ namespace Umbraco.Web.PropertyEditors
if (string.IsNullOrEmpty(tmpImgPath))
continue;
var absoluteTempImagePath = IOHelper.MapPath(tmpImgPath);
var fileName = Path.GetFileName(absoluteTempImagePath);
var safeFileName = fileName.ToSafeFileName();
var mediaItemName = safeFileName.ToFriendlyName();
IMedia mediaFile;
GuidUdi udi;
if (uploadedImages.ContainsKey(tmpImgPath) == false)
{
if (mediaParentFolder == Guid.Empty)
mediaFile = _mediaService.CreateMedia(mediaItemName, Constants.System.Root, Constants.Conventions.MediaTypes.Image, userId);
else
mediaFile = _mediaService.CreateMedia(mediaItemName, mediaParentFolder, Constants.Conventions.MediaTypes.Image, userId);
var fileInfo = new FileInfo(absoluteTempImagePath);
var fileStream = fileInfo.OpenReadWithRetry();
if (fileStream == null) throw new InvalidOperationException("Could not acquire file stream");
using (fileStream)
{
mediaFile.SetValue(_contentTypeBaseServiceProvider, Constants.Conventions.Media.File, safeFileName, fileStream);
}
_mediaService.Save(mediaFile, userId);
udi = mediaFile.GetUdi();
}
else
{
// Already been uploaded & we have it's UDI
udi = uploadedImages[tmpImgPath];
}
// Add the UDI to the img element as new data attribute
img.SetAttributeValue("data-udi", udi.ToString());
// Get the new persisted image URL
var mediaTyped = _umbracoContextAccessor?.UmbracoContext?.Media.GetById(udi.Guid);
if (mediaTyped == null)
throw new PanicException($"Could not find media by id {udi.Guid} or there was no UmbracoContext available.");
var location = mediaTyped.Url();
// Find the width & height attributes as we need to set the imageprocessor QueryString
var width = img.GetAttributeValue("width", int.MinValue);
var height = img.GetAttributeValue("height", int.MinValue);
if (width != int.MinValue && height != int.MinValue)
{
location = imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(location) { ImageCropMode = "max", Width = width, Height = height });
}
img.SetAttributeValue("src", location);
// Remove the data attribute (so we do not re-process this)
img.Attributes.Remove(TemporaryImageDataAttribute);
// Add to the dictionary to avoid dupes
if (uploadedImages.ContainsKey(tmpImgPath) == false)
{
uploadedImages.Add(tmpImgPath, udi);
// Delete folder & image now its saved in media
// The folder should contain one image - as a unique guid folder created
// for each image uploaded from TinyMceController
var folderName = Path.GetDirectoryName(absoluteTempImagePath);
try
{
Directory.Delete(folderName, true);
}
catch (Exception ex)
{
_logger.Error<string>(typeof(HtmlImageSourceParser), ex, "Could not delete temp file or folder {FileName}", absoluteTempImagePath);
}
}
PersistMediaItem(mediaParentFolder, userId, img, tmpImgPath, imageUrlGenerator);
}
return htmlDoc.DocumentNode.OuterHtml;
}
private void PersistMediaItem(Guid mediaParentFolder, int userId, HtmlNode img, string tmpImgPath, IImageUrlGenerator imageUrlGenerator)
{
var absoluteTempImagePath = Path.GetFullPath(IOHelper.MapPath(tmpImgPath));
if (IsValidPath(absoluteTempImagePath) == false)
{
return;
}
var fileName = Path.GetFileName(absoluteTempImagePath);
var safeFileName = fileName.ToSafeFileName();
var mediaItemName = safeFileName.ToFriendlyName();
IMedia mediaFile;
GuidUdi udi;
if (UploadedImages.ContainsKey(tmpImgPath) == false)
{
if (mediaParentFolder == Guid.Empty)
mediaFile = _mediaService.CreateMedia(mediaItemName, Constants.System.Root, Constants.Conventions.MediaTypes.Image, userId);
else
mediaFile = _mediaService.CreateMedia(mediaItemName, mediaParentFolder, Constants.Conventions.MediaTypes.Image, userId);
var fileInfo = new FileInfo(absoluteTempImagePath);
var fileStream = fileInfo.OpenReadWithRetry();
if (fileStream == null) throw new InvalidOperationException("Could not acquire file stream");
using (fileStream)
{
mediaFile.SetValue(_contentTypeBaseServiceProvider, Constants.Conventions.Media.File, safeFileName, fileStream);
}
_mediaService.Save(mediaFile, userId);
udi = mediaFile.GetUdi();
}
else
{
// Already been uploaded & we have it's UDI
udi = UploadedImages[tmpImgPath];
}
// Add the UDI to the img element as new data attribute
img.SetAttributeValue("data-udi", udi.ToString());
// Get the new persisted image URL
var mediaTyped = _umbracoContextAccessor?.UmbracoContext?.Media.GetById(udi.Guid);
if (mediaTyped == null)
throw new PanicException($"Could not find media by id {udi.Guid} or there was no UmbracoContext available.");
var location = mediaTyped.Url();
// Find the width & height attributes as we need to set the imageprocessor QueryString
var width = img.GetAttributeValue("width", int.MinValue);
var height = img.GetAttributeValue("height", int.MinValue);
if (width != int.MinValue && height != int.MinValue)
{
location = imageUrlGenerator.GetImageUrl(new ImageUrlGenerationOptions(location) { ImageCropMode = "max", Width = width, Height = height });
}
img.SetAttributeValue("src", location);
// Remove the data attribute (so we do not re-process this)
img.Attributes.Remove(TemporaryImageDataAttribute);
// Add to the dictionary to avoid dupes
if (UploadedImages.ContainsKey(tmpImgPath) == false)
{
UploadedImages.Add(tmpImgPath, udi);
// Delete folder & image now its saved in media
// The folder should contain one image - as a unique guid folder created
// for each image uploaded from TinyMceController
var folderName = Path.GetDirectoryName(absoluteTempImagePath);
try
{
Directory.Delete(folderName, true);
}
catch (Exception ex)
{
_logger.Error<string>(typeof(HtmlImageSourceParser), ex, "Could not delete temp file or folder {FileName}", absoluteTempImagePath);
}
}
}
private bool IsValidPath(string imagePath)
{
return imagePath.StartsWith(_tempFolderAbsolutePath, StringComparison.OrdinalIgnoreCase);
}
}
}
@@ -174,7 +174,8 @@ namespace Umbraco.Web.PropertyEditors
var mediaParent = config?.MediaParentId;
var mediaParentId = mediaParent == null ? Guid.Empty : mediaParent.Guid;
var parseAndSavedTempImages = _pastedImages.FindAndPersistPastedTempImages(editorValue.Value.ToString(), mediaParentId, userId, _imageUrlGenerator);
var parseAndSaveDataUriImages = _pastedImages.FindAndPersistBase64Images(editorValue.Value.ToString(), mediaParentId, userId, _imageUrlGenerator);
var parseAndSavedTempImages = _pastedImages.FindAndPersistPastedTempImages(parseAndSaveDataUriImages, mediaParentId, userId, _imageUrlGenerator);
var editorValueWithMediaUrlsRemoved = _imageSourceParser.RemoveImageSources(parseAndSavedTempImages);
var parsed = MacroTagParser.FormatRichTextContentForPersistence(editorValueWithMediaUrlsRemoved);
var sanitized = _htmlSanitizer.Sanitize(parsed);
@@ -49,11 +49,14 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
public Type GetModelType(Guid contentTypeKey)
{
var publishedContentType = GetContentType(contentTypeKey);
if (publishedContentType != null)
if (publishedContentType != null && publishedContentType.IsElement)
{
var modelType = ModelType.For(publishedContentType.Alias);
return _publishedModelFactory.MapModelType(modelType);
// TODO Get the model type without having to construct a list
var listType = _publishedModelFactory.CreateModelList(publishedContentType.Alias).GetType();
if (listType.GenericTypeArguments.Length == 1)
{
return listType.GenericTypeArguments[0];
}
}
return typeof(IPublishedElement);
@@ -10,7 +10,6 @@ using Umbraco.Core.PropertyEditors.ValueConverters;
namespace Umbraco.Web.PropertyEditors.ValueConverters
{
[DefaultPropertyValueConverter(typeof(JsonValueConverter))]
public class BlockListPropertyValueConverter : PropertyValueConverterBase
{
@@ -30,7 +29,8 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
=> propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.BlockList);
/// <inheritdoc />
public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof(BlockListModel);
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
=> typeof(BlockListModel);
/// <inheritdoc />
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
@@ -38,24 +38,27 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
/// <inheritdoc />
public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview)
{
return source?.ToString();
}
=> source?.ToString();
/// <inheritdoc />
public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
{
// NOTE: The intermediate object is just a json string, we don't actually convert from source -> intermediate since source is always just a json string
// NOTE: The intermediate object is just a JSON string, we don't actually convert from source -> intermediate since source is always just a JSON string
using (_proflog.DebugDuration<BlockListPropertyValueConverter>($"ConvertPropertyToBlockList ({propertyType.DataType.Id})"))
{
var value = (string)inter;
// Short-circuit on empty values
if (string.IsNullOrWhiteSpace(value)) return BlockListModel.Empty;
if (string.IsNullOrWhiteSpace(value))
{
return BlockListModel.Empty;
}
var converted = _blockListEditorDataConverter.Deserialize(value);
if (converted.BlockValue.ContentData.Count == 0) return BlockListModel.Empty;
if (converted.BlockValue.ContentData.Count == 0)
{
return BlockListModel.Empty;
}
var blockListLayout = converted.Layout.ToObject<IEnumerable<BlockListLayoutItem>>();
@@ -68,77 +71,133 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
var contentPublishedElements = new Dictionary<Guid, IPublishedElement>();
foreach (var data in converted.BlockValue.ContentData)
{
if (!blockConfigMap.ContainsKey(data.ContentTypeKey)) continue;
if (!blockConfigMap.ContainsKey(data.ContentTypeKey))
{
continue;
}
var element = _blockConverter.ConvertToElement(data, referenceCacheLevel, preview);
if (element == null) continue;
if (element == null)
{
continue;
}
contentPublishedElements[element.Key] = element;
}
// If there are no content elements, it doesn't matter what is stored in layout
if (contentPublishedElements.Count == 0) return BlockListModel.Empty;
if (contentPublishedElements.Count == 0)
{
return BlockListModel.Empty;
}
// Convert the settings data
var settingsPublishedElements = new Dictionary<Guid, IPublishedElement>();
foreach (var data in converted.BlockValue.SettingsData)
{
if (!validSettingsElementTypes.Contains(data.ContentTypeKey)) continue;
if (!validSettingsElementTypes.Contains(data.ContentTypeKey))
{
continue;
}
var element = _blockConverter.ConvertToElement(data, referenceCacheLevel, preview);
if (element == null) continue;
if (element == null)
{
continue;
}
settingsPublishedElements[element.Key] = element;
}
var layout = new List<BlockListItem>();
// Cache constructors locally (it's tied to the current IPublishedSnapshot and IPublishedModelFactory)
var blockListItemActivator = new BlockListItemActivator(_blockConverter);
var list = new List<BlockListItem>();
foreach (var layoutItem in blockListLayout)
{
// Get the content reference
var contentGuidUdi = (GuidUdi)layoutItem.ContentUdi;
if (!contentPublishedElements.TryGetValue(contentGuidUdi.Guid, out var contentData))
{
continue;
}
if (!contentData.ContentType.TryGetKey(out var contentTypeKey))
{
throw new InvalidOperationException("The content type was not of type " + typeof(IPublishedContentType2));
}
if (!blockConfigMap.TryGetValue(contentTypeKey, out var blockConfig))
{
continue;
}
// Get the setting reference
IPublishedElement settingsData = null;
var settingGuidUdi = layoutItem.SettingsUdi != null ? (GuidUdi)layoutItem.SettingsUdi : null;
var settingGuidUdi = (GuidUdi)layoutItem.SettingsUdi;
if (settingGuidUdi != null)
{
settingsPublishedElements.TryGetValue(settingGuidUdi.Guid, out settingsData);
}
// This can happen if they have a settings type, save content, remove the settings type, and display the front-end page before saving the content again
// We also ensure that the content types match, since maybe the settings type has been changed after this has been persisted
if (settingsData != null)
{
if (!settingsData.ContentType.TryGetKey(out var settingsElementTypeKey))
{
throw new InvalidOperationException("The settings element type was not of type " + typeof(IPublishedContentType2));
}
if (!blockConfig.SettingsElementTypeKey.HasValue || settingsElementTypeKey != blockConfig.SettingsElementTypeKey)
{
settingsData = null;
}
}
// Get settings type from configuration
var settingsType = blockConfig.SettingsElementTypeKey.HasValue
? _blockConverter.GetModelType(blockConfig.SettingsElementTypeKey.Value)
: typeof(IPublishedElement);
// Create instance (use content/settings type from configuration)
var layoutRef = blockListItemActivator.CreateInstance(blockConfig.ContentElementTypeKey, blockConfig.SettingsElementTypeKey, contentGuidUdi, contentData, settingGuidUdi, settingsData);
// TODO: This should be optimized/cached, as calling Activator.CreateInstance is slow
var layoutType = typeof(BlockListItem<,>).MakeGenericType(contentData.GetType(), settingsType);
var layoutRef = (BlockListItem)Activator.CreateInstance(layoutType, contentGuidUdi, contentData, settingGuidUdi, settingsData);
layout.Add(layoutRef);
list.Add(layoutRef);
}
var model = new BlockListModel(layout);
return model;
return new BlockListModel(list);
}
}
private class BlockListItemActivator
{
private readonly BlockEditorConverter _blockConverter;
private readonly Dictionary<(Guid, Guid?), Func<Udi, IPublishedElement, Udi, IPublishedElement, BlockListItem>> _contructorCache = new();
public BlockListItemActivator(BlockEditorConverter blockConverter)
=> _blockConverter = blockConverter;
public BlockListItem CreateInstance(Guid contentTypeKey, Guid? settingsTypeKey, Udi contentUdi, IPublishedElement contentData, Udi settingsUdi, IPublishedElement settingsData)
{
if (!_contructorCache.TryGetValue((contentTypeKey, settingsTypeKey), out var constructor))
{
constructor = _contructorCache[(contentTypeKey, settingsTypeKey)] = EmitConstructor(contentTypeKey, settingsTypeKey);
}
return constructor(contentUdi, contentData, settingsUdi, settingsData);
}
private Func<Udi, IPublishedElement, Udi, IPublishedElement, BlockListItem> EmitConstructor(Guid contentTypeKey, Guid? settingsTypeKey)
{
var contentType = _blockConverter.GetModelType(contentTypeKey);
var settingsType = settingsTypeKey.HasValue ? _blockConverter.GetModelType(settingsTypeKey.Value) : typeof(IPublishedElement);
var type = typeof(BlockListItem<,>).MakeGenericType(contentType, settingsType);
var constructor = type.GetConstructor(new[] { typeof(Udi), contentType, typeof(Udi), settingsType });
if (constructor == null)
{
throw new InvalidOperationException($"Could not find the required public constructor on {type}.");
}
// We use unsafe here, because we know the contructor parameter count and types match
return ReflectionUtilities.EmitConstructorUnsafe<Func<Udi, IPublishedElement, Udi, IPublishedElement, BlockListItem>>(constructor);
}
}
}
}
@@ -42,7 +42,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
{
var maxNumber = propertyType.DataType.ConfigurationAs<MultiUrlPickerConfiguration>().MaxNumber;
if (inter == null)
if (string.IsNullOrWhiteSpace(inter?.ToString()))
{
return maxNumber == 1 ? null : Enumerable.Empty<Link>();
}
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
@@ -25,9 +25,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
/// </summary>
public NestedContentManyValueConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor, IPublishedModelFactory publishedModelFactory, IProfilingLogger proflog)
: base(publishedSnapshotAccessor, publishedModelFactory)
{
_proflog = proflog;
}
=> _proflog = proflog;
/// <inheritdoc />
public override bool IsConverter(IPublishedPropertyType propertyType)
@@ -37,6 +35,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
{
var contentTypes = propertyType.DataType.ConfigurationAs<NestedContentConfiguration>().ContentTypes;
return contentTypes.Length == 1
? typeof(IEnumerable<>).MakeGenericType(ModelType.For(contentTypes[0].Alias))
: typeof(IEnumerable<IPublishedElement>);
@@ -48,9 +47,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
/// <inheritdoc />
public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview)
{
return source?.ToString();
}
=> source?.ToString();
/// <inheritdoc />
public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
@@ -64,16 +61,24 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
: new List<IPublishedElement>();
var value = (string)inter;
if (string.IsNullOrWhiteSpace(value)) return elements;
if (string.IsNullOrWhiteSpace(value))
{
return elements;
}
var objects = JsonConvert.DeserializeObject<List<JObject>>(value);
if (objects.Count == 0) return elements;
if (objects.Count == 0)
{
return elements;
}
foreach (var sourceObject in objects)
{
var element = ConvertToElement(sourceObject, referenceCacheLevel, preview);
if (element != null)
{
elements.Add(element);
}
}
return elements;
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
@@ -24,9 +24,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
/// </summary>
public NestedContentSingleValueConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor, IPublishedModelFactory publishedModelFactory, IProfilingLogger proflog)
: base(publishedSnapshotAccessor, publishedModelFactory)
{
_proflog = proflog;
}
=> _proflog = proflog;
/// <inheritdoc />
public override bool IsConverter(IPublishedPropertyType propertyType)
@@ -36,9 +34,10 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
{
var contentTypes = propertyType.DataType.ConfigurationAs<NestedContentConfiguration>().ContentTypes;
return contentTypes.Length > 1
? typeof(IPublishedElement)
: ModelType.For(contentTypes[0].Alias);
return contentTypes.Length == 1
? ModelType.For(contentTypes[0].Alias)
: typeof(IPublishedElement);
}
/// <inheritdoc />
@@ -47,9 +46,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
/// <inheritdoc />
public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview)
{
return source?.ToString();
}
=> source?.ToString();
/// <inheritdoc />
public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
@@ -57,14 +54,18 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
using (_proflog.DebugDuration<NestedContentSingleValueConverter>($"ConvertPropertyToNestedContent ({propertyType.DataType.Id})"))
{
var value = (string)inter;
if (string.IsNullOrWhiteSpace(value)) return null;
if (string.IsNullOrWhiteSpace(value))
{
return null;
}
var objects = JsonConvert.DeserializeObject<List<JObject>>(value);
if (objects.Count == 0)
{
return null;
if (objects.Count > 1)
throw new InvalidOperationException();
}
// Only return the first (existing data might contain more than is currently configured)
return ConvertToElement(objects[0], referenceCacheLevel, preview);
}
}
@@ -12,52 +12,54 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
{
private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
protected IPublishedModelFactory PublishedModelFactory { get; }
protected NestedContentValueConverterBase(IPublishedSnapshotAccessor publishedSnapshotAccessor, IPublishedModelFactory publishedModelFactory)
{
_publishedSnapshotAccessor = publishedSnapshotAccessor;
PublishedModelFactory = publishedModelFactory;
}
protected IPublishedModelFactory PublishedModelFactory { get; }
public static bool IsNested(IPublishedPropertyType publishedProperty)
{
return publishedProperty.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.NestedContent);
}
=> publishedProperty.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.NestedContent);
public static bool IsNestedSingle(IPublishedPropertyType publishedProperty)
private static bool IsSingle(IPublishedPropertyType publishedProperty)
{
if (!IsNested(publishedProperty))
return false;
var config = publishedProperty.DataType.ConfigurationAs<NestedContentConfiguration>();
return config.MinItems == 1 && config.MaxItems == 1;
}
public static bool IsNestedSingle(IPublishedPropertyType publishedProperty)
=> IsNested(publishedProperty) && IsSingle(publishedProperty);
public static bool IsNestedMany(IPublishedPropertyType publishedProperty)
{
return IsNested(publishedProperty) && !IsNestedSingle(publishedProperty);
}
=> IsNested(publishedProperty) && !IsSingle(publishedProperty);
protected IPublishedElement ConvertToElement(JObject sourceObject, PropertyCacheLevel referenceCacheLevel, bool preview)
{
var elementTypeAlias = sourceObject[NestedContentPropertyEditor.ContentTypeAliasPropertyKey]?.ToObject<string>();
if (string.IsNullOrEmpty(elementTypeAlias))
{
return null;
}
// only convert element types - content types will cause an exception when PublishedModelFactory creates the model
// Only convert element types - content types will cause an exception when PublishedModelFactory creates the model
var publishedContentType = _publishedSnapshotAccessor.PublishedSnapshot.Content.GetContentType(elementTypeAlias);
if (publishedContentType == null || publishedContentType.IsElement == false)
{
return null;
}
var propertyValues = sourceObject.ToObject<Dictionary<string, object>>();
if (!propertyValues.TryGetValue("key", out var keyo)
|| !Guid.TryParse(keyo.ToString(), out var key))
if (!propertyValues.TryGetValue("key", out var keyo) || !Guid.TryParse(keyo.ToString(), out var key))
{
key = Guid.Empty;
}
IPublishedElement element = new PublishedElement(publishedContentType, key, propertyValues, preview, referenceCacheLevel, _publishedSnapshotAccessor);
element = PublishedModelFactory.CreateModel(element);
return element;
}
}
@@ -433,10 +433,18 @@ namespace Umbraco.Web.PublishedCache.NuCache
refreshedIdsA.Contains(x.ContentTypeId) &&
BuildKit(x, out _)))
{
// replacing the node: must preserve the parents
// replacing the node: must preserve the relations
var node = GetHead(_contentNodes, kit.Node.Id)?.Value;
if (node != null)
{
// Preserve children
kit.Node.FirstChildContentId = node.FirstChildContentId;
kit.Node.LastChildContentId = node.LastChildContentId;
// Also preserve siblings
kit.Node.NextSiblingContentId = node.NextSiblingContentId;
kit.Node.PreviousSiblingContentId = node.PreviousSiblingContentId;
}
SetValueLocked(_contentNodes, kit.Node.Id, kit.Node);
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.Linq;
using NPoco;
@@ -224,18 +225,13 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
.Append(SqlObjectTypeNotTrashed(scope.SqlContext, Constants.ObjectTypes.Document))
.Append(SqlOrderByLevelIdSortOrder(scope.SqlContext));
// Use a more efficient COUNT query
var sqlCountQuery = SqlContentSourcesCount(scope)
.Append(SqlObjectTypeNotTrashed(scope.SqlContext, Constants.ObjectTypes.Document));
var sqlCount = scope.SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl");
var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document);
// We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout.
// We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that.
foreach (var row in scope.Database.QueryPaged<ContentSourceDto>(PageSize, sql, sqlCount))
foreach (var row in GetContentNodeDtos(sql, scope))
{
yield return CreateContentNodeKit(row, serializer);
}
@@ -248,18 +244,12 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
.Append(SqlWhereNodeIdX(scope.SqlContext, id))
.Append(SqlOrderByLevelIdSortOrder(scope.SqlContext));
// Use a more efficient COUNT query
var sqlCountQuery = SqlContentSourcesCount(scope, SqlContentSourcesSelectUmbracoNodeJoin)
.Append(SqlObjectTypeNotTrashed(scope.SqlContext, Constants.ObjectTypes.Document))
.Append(SqlWhereNodeIdX(scope.SqlContext, id));
var sqlCount = scope.SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl");
var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document);
// We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout.
// We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that.
foreach (var row in scope.Database.QueryPaged<ContentSourceDto>(PageSize, sql, sqlCount))
foreach (var row in GetContentNodeDtos(sql, scope))
{
yield return CreateContentNodeKit(row, serializer);
}
@@ -274,18 +264,12 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
.WhereIn<ContentDto>(x => x.ContentTypeId, ids)
.Append(SqlOrderByLevelIdSortOrder(scope.SqlContext));
// Use a more efficient COUNT query
var sqlCountQuery = SqlContentSourcesCount(scope)
.Append(SqlObjectTypeNotTrashed(scope.SqlContext, Constants.ObjectTypes.Document))
.WhereIn<ContentDto>(x => x.ContentTypeId, ids);
var sqlCount = scope.SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl");
var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Document);
// We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout.
// We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that.
foreach (var row in scope.Database.QueryPaged<ContentSourceDto>(PageSize, sql, sqlCount))
foreach (var row in GetContentNodeDtos(sql, scope))
{
yield return CreateContentNodeKit(row, serializer);
}
@@ -312,17 +296,12 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
.Append(SqlObjectTypeNotTrashed(scope.SqlContext, Constants.ObjectTypes.Media))
.Append(SqlOrderByLevelIdSortOrder(scope.SqlContext));
// Use a more efficient COUNT query
var sqlCountQuery = SqlMediaSourcesCount(scope)
.Append(SqlObjectTypeNotTrashed(scope.SqlContext, Constants.ObjectTypes.Media));
var sqlCount = scope.SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl");
var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media);
// We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout.
// We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that.
foreach (var row in scope.Database.QueryPaged<ContentSourceDto>(PageSize, sql, sqlCount))
foreach (var row in GetMediaNodeDtos(sql, scope))
{
yield return CreateMediaNodeKit(row, serializer);
}
@@ -335,18 +314,12 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
.Append(SqlWhereNodeIdX(scope.SqlContext, id))
.Append(SqlOrderByLevelIdSortOrder(scope.SqlContext));
// Use a more efficient COUNT query
var sqlCountQuery = SqlMediaSourcesCount(scope, SqlContentSourcesSelectUmbracoNodeJoin)
.Append(SqlObjectTypeNotTrashed(scope.SqlContext, Constants.ObjectTypes.Media))
.Append(SqlWhereNodeIdX(scope.SqlContext, id));
var sqlCount = scope.SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl");
var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media);
// We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout.
// We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that.
foreach (var row in scope.Database.QueryPaged<ContentSourceDto>(PageSize, sql, sqlCount))
foreach (var row in GetMediaNodeDtos(sql, scope))
{
yield return CreateMediaNodeKit(row, serializer);
}
@@ -361,18 +334,12 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
.WhereIn<ContentDto>(x => x.ContentTypeId, ids)
.Append(SqlOrderByLevelIdSortOrder(scope.SqlContext));
// Use a more efficient COUNT query
var sqlCountQuery = SqlMediaSourcesCount(scope)
.Append(SqlObjectTypeNotTrashed(scope.SqlContext, Constants.ObjectTypes.Media))
.WhereIn<ContentDto>(x => x.ContentTypeId, ids);
var sqlCount = scope.SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl");
var serializer = _contentCacheDataSerializerFactory.Create(ContentCacheDataSerializerEntityType.Media);
// We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout.
// We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that.
foreach (var row in scope.Database.QueryPaged<ContentSourceDto>(PageSize, sql, sqlCount))
foreach (var row in GetMediaNodeDtos(sql, scope))
{
yield return CreateMediaNodeKit(row, serializer);
}
@@ -486,6 +453,57 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource
return s;
}
private IEnumerable<ContentSourceDto> GetContentNodeDtos(Sql<ISqlContext> sql, IScope scope)
{
var usePagedSqlQuery = ConfigurationManager.AppSettings[NuCacheSerializerComponent.Nucache_UsePagedSqlQuery_Key];
// We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout.
// We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that.
// QueryPaged is very slow on large sites however, so use fetch if UsePagedSqlQuery is disabled.
IEnumerable<ContentSourceDto> dtos;
if (usePagedSqlQuery == "false")
{
dtos = scope.Database.Fetch<ContentSourceDto>(sql);
}
else
{
// Use a more efficient COUNT query
var sqlCountQuery = SqlContentSourcesCount(scope)
.Append(SqlObjectTypeNotTrashed(scope.SqlContext, Constants.ObjectTypes.Document));
var sqlCount = scope.SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl");
dtos = scope.Database.QueryPaged<ContentSourceDto>(PageSize, sql, sqlCount);
}
return dtos;
}
private IEnumerable<ContentSourceDto> GetMediaNodeDtos(Sql<ISqlContext> sql, IScope scope)
{
var usePagedSqlQuery = ConfigurationManager.AppSettings[NuCacheSerializerComponent.Nucache_UsePagedSqlQuery_Key];
// We need to page here. We don't want to iterate over every single row in one connection cuz this can cause an SQL Timeout.
// We also want to read with a db reader and not load everything into memory, QueryPaged lets us do that.
// QueryPaged is very slow on large sites however, so use fetch if UsePagedSqlQuery is disabled.
IEnumerable<ContentSourceDto> dtos;
if (usePagedSqlQuery == "false")
{
dtos = scope.Database.Fetch<ContentSourceDto>(sql);
}
else
{
// Use a more efficient COUNT query
var sqlCountQuery = SqlMediaSourcesCount(scope)
.Append(SqlObjectTypeNotTrashed(scope.SqlContext, Constants.ObjectTypes.Media));
var sqlCount = scope.SqlContext.Sql("SELECT COUNT(*) FROM (").Append(sqlCountQuery).Append(") npoco_tbl");
dtos = scope.Database.QueryPaged<ContentSourceDto>(PageSize, sql, sqlCount);
}
return dtos;
}
}
}
@@ -17,6 +17,7 @@ namespace Umbraco.Web.PublishedCache.NuCache
{
internal const string Nucache_Serializer_Key = "Umbraco.Web.PublishedCache.NuCache.Serializer";
internal const string Nucache_UnPublishedContentCompression_Key = "Umbraco.Web.PublishedCache.NuCache.CompressUnPublishedContent";
internal const string Nucache_UsePagedSqlQuery_Key = "Umbraco.NuCache.UsePagedSqlQuery";
private const string JSON_SERIALIZER_VALUE = "JSON";
private readonly Lazy<IPublishedSnapshotService> _service;
private readonly IKeyValueService _keyValueService;
@@ -39,7 +39,9 @@ using Umbraco.Web.WebApi;
using Current = Umbraco.Web.Composing.Current;
using Umbraco.Web.PropertyEditors;
using Umbraco.Core.Models;
using Umbraco.Core.Telemetry;
using Umbraco.Web.Models;
using Umbraco.Web.Telemetry;
namespace Umbraco.Web.Runtime
{
@@ -58,6 +60,8 @@ namespace Umbraco.Web.Runtime
composition.ComposeWebMappingProfiles();
composition.RegisterUnique<ISiteIdentifierService, SiteIdentifierService>();
//register the install components
//NOTE: i tried to not have these registered if we weren't installing or upgrading but post install when the site restarts
//it still needs to use the install controller so we can't do that
@@ -239,7 +239,9 @@ namespace Umbraco.Web.Search
var allLangs = _languageService.GetAllLanguages().Select(x => x.IsoCode.ToLowerInvariant()).ToList();
// the chars [*-_] in the query will mess everything up so let's remove those
query = Regex.Replace(query, "[\\*\\-_]", "");
// However we cannot just remove - and _ since these signify a space, so we instead replace them with that.
query = Regex.Replace(query, "[\\*]", string.Empty);
query = Regex.Replace(query, "[\\-_]", " ");
//check if text is surrounded by single or double quotes, if so, then exact match
var surroundedByQuotes = Regex.IsMatch(query, "^\".*?\"$")
@@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Threading.Tasks;
using System.Web;
using System.Web.Security;
@@ -294,21 +295,22 @@ namespace Umbraco.Web.Security
passwordValidator.RequiredLength,
passwordValidator.RequireNonLetterOrDigit ? 2 : 0);
var random = new Random();
var passwordChars = password.ToCharArray();
if (passwordValidator.RequireDigit && passwordChars.ContainsAny(Enumerable.Range(48, 58).Select(x => (char)x)))
password += Convert.ToChar(random.Next(48, 58)); // 0-9
using (var numberGenerator = new RNGCryptoServiceProvider())
{
if (passwordValidator.RequireDigit && passwordChars.ContainsAny(Enumerable.Range(48, 58).Select(x => (char)x)))
password += Convert.ToChar(numberGenerator.GetInt32(48, 58)); // 0-9
if (passwordValidator.RequireLowercase && passwordChars.ContainsAny(Enumerable.Range(97, 123).Select(x => (char)x)))
password += Convert.ToChar(random.Next(97, 123)); // a-z
if (passwordValidator.RequireLowercase && passwordChars.ContainsAny(Enumerable.Range(97, 123).Select(x => (char)x)))
password += Convert.ToChar(numberGenerator.GetInt32(97, 123)); // a-z
if (passwordValidator.RequireUppercase && passwordChars.ContainsAny(Enumerable.Range(65, 91).Select(x => (char)x)))
password += Convert.ToChar(random.Next(65, 91)); // A-Z
if (passwordValidator.RequireUppercase && passwordChars.ContainsAny(Enumerable.Range(65, 91).Select(x => (char)x)))
password += Convert.ToChar(numberGenerator.GetInt32(65, 91)); // A-Z
if (passwordValidator.RequireNonLetterOrDigit && passwordChars.ContainsAny(Enumerable.Range(33, 48).Select(x => (char)x)))
password += Convert.ToChar(random.Next(33, 48)); // symbols !"#$%&'()*+,-./
if (passwordValidator.RequireNonLetterOrDigit && passwordChars.ContainsAny(Enumerable.Range(33, 48).Select(x => (char)x)))
password += Convert.ToChar(numberGenerator.GetInt32(33, 48)); // symbols !"#$%&'()*+,-./
}
return password;
}
@@ -587,6 +587,13 @@ namespace Umbraco.Web.Security.Providers
requiresFullSave = true;
}
// If the last login date is default prior to setting it, it means that this value has never been set
// and therefore there's no property data created for it yet, which means that we can't just update that property
// and need to do a full save.
if (member.LastLoginDate == default)
{
requiresFullSave = true;
}
member.LastLoginDate = DateTime.Now;
Current.Logger.Info<UmbracoMembershipProviderBase, string, string>("Login attempt succeeded for username {Username} from IP address {IpAddress}", username, GetCurrentRequestIpAddress());
+7 -7
View File
@@ -48,13 +48,16 @@ namespace Umbraco.Web.Telemetry
try
{
// Send data to LIVE telemetry
_httpClient.BaseAddress = new Uri("https://telemetry.umbraco.com/");
if (_httpClient.BaseAddress == null)
{
// Send data to LIVE telemetry
_httpClient.BaseAddress = new Uri("https://telemetry.umbraco.com/");
#if DEBUG
// Send data to DEBUG telemetry service
_httpClient.BaseAddress = new Uri("https://telemetry.rainbowsrock.net/");
// Send data to DEBUG telemetry service
_httpClient.BaseAddress = new Uri("https://telemetry.rainbowsrock.net/");
#endif
}
_httpClient.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/json");
@@ -62,9 +65,6 @@ namespace Umbraco.Web.Telemetry
{
request.Content = new StringContent(JsonConvert.SerializeObject(telemetryReportData), Encoding.UTF8, "application/json"); //CONTENT-TYPE header
// Set a low timeout - no need to use a larger default timeout for this POST request
_httpClient.Timeout = new TimeSpan(0, 0, 1);
// Make a HTTP Post to telemetry service
// https://telemetry.umbraco.com/installs/
// Fire & Forget, do not need to know if its a 200, 500 etc
@@ -0,0 +1,100 @@
using System;
using System.IO;
using System.Xml.Linq;
using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Telemetry;
using Umbraco.Web.Install;
namespace Umbraco.Web.Telemetry
{
internal class SiteIdentifierService : ISiteIdentifierService
{
private readonly IUmbracoSettingsSection _settings;
private readonly ILogger _logger;
public SiteIdentifierService(IUmbracoSettingsSection settings, ILogger logger)
{
_settings = settings;
_logger = logger;
}
public bool TryGetSiteIdentifier(out Guid siteIdentifier)
{
// Parse telemetry string as a GUID & verify its a GUID and not some random string
// since users may have messed with or decided to empty the app setting or put in something random
if (Guid.TryParse(_settings.BackOffice.Id, out var parsedTelemetryId) is false
|| parsedTelemetryId == Guid.Empty)
{
siteIdentifier = Guid.Empty;
return false;
}
siteIdentifier = parsedTelemetryId;
return true;
}
public bool TryGetOrCreateSiteIdentifier(out Guid siteIdentifier)
{
if (TryGetSiteIdentifier(out var existingId))
{
siteIdentifier = existingId;
return true;
}
if (TryCreateSiteIdentifier(out var createdId))
{
siteIdentifier = createdId;
return true;
}
siteIdentifier = Guid.Empty;
return false;
}
public bool TryCreateSiteIdentifier(out Guid createdGuid)
{
createdGuid = Guid.NewGuid();
// Modify the XML to add a new GUID site identifier
// hack: ensure this does not trigger a restart
using (ChangesMonitor.Suspended())
{
var umbracoSettingsPath = IOHelper.MapPath(SystemFiles.UmbracoSettings);
if (File.Exists(umbracoSettingsPath) is false)
{
_logger.Error<SiteIdentifierService>("Unable to find umbracoSettings.config file to add telemetry site identifier");
return false;
}
try
{
var umbracoConfigXml = XDocument.Load(umbracoSettingsPath, LoadOptions.PreserveWhitespace);
if (umbracoConfigXml.Root != null)
{
var backofficeElement = umbracoConfigXml.Root.Element("backOffice");
if (backofficeElement is null)
{
return false;
}
// Will add ID attribute if it does not exist
backofficeElement.SetAttributeValue("id", createdGuid.ToString());
// Save file back down
umbracoConfigXml.Save(umbracoSettingsPath, SaveOptions.DisableFormatting);
}
}
catch (Exception ex)
{
_logger.Error<SiteIdentifierService>(ex, "Couldn't update umbracoSettings.config with a backoffice with a telemetry site identifier");
return false;
}
}
return true;
}
}
}
@@ -12,7 +12,7 @@ namespace Umbraco.Web.Telemetry
private readonly ITelemetryService _telemetryService;
private BackgroundTaskRunner<IBackgroundTask> _telemetryReporterRunner;
public TelemetryComponent(IProfilingLogger logger, IUmbracoSettingsSection settings, ITelemetryService telemetryService)
public TelemetryComponent(IProfilingLogger logger, ITelemetryService telemetryService)
{
_logger = logger;
_telemetryService = telemetryService;
+1
View File
@@ -347,6 +347,7 @@
<Compile Include="Models\LinkType.cs" />
<Compile Include="Models\TemplateQuery\OperatorFactory.cs" />
<Compile Include="Telemetry\ReportSiteTask.cs" />
<Compile Include="Telemetry\SiteIdentifierService.cs" />
<Compile Include="Telemetry\TelemetryComponent.cs" />
<Compile Include="Telemetry\TelemetryComposer.cs" />
<Compile Include="Templates\HtmlLocalLinkParser.cs" />

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