Skip to content

Commit 9896119

Browse files
authored
Merge branch 'v15/dev' into v15/feature/enable-document-rollback-as-entity-action
2 parents a2378e6 + 50ff23b commit 9896119

File tree

170 files changed

+1768
-615
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

170 files changed

+1768
-615
lines changed

src/Umbraco.Cms.Api.Management/Controllers/Document/CreateDocumentControllerBase.cs

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,18 @@ protected CreateDocumentControllerBase(IAuthorizationService authorizationServic
1818

1919
protected async Task<IActionResult> HandleRequest(CreateDocumentRequestModel requestModel, Func<Task<IActionResult>> authorizedHandler)
2020
{
21-
// TODO This have temporarily been uncommented, to support the client sends values from all cultures, even when the user do not have access to the languages.
22-
// The values are ignored in the ContentEditingService
21+
// We intentionally don't pass in cultures here.
22+
// This is to support the client sending values for all cultures even if the user doesn't have access to the language.
23+
// Values for unauthorized languages are later ignored in the ContentEditingService.
24+
AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
25+
User,
26+
ContentPermissionResource.WithKeys(ActionNew.ActionLetter, requestModel.Parent?.Id),
27+
AuthorizationPolicies.ContentPermissionByResource);
2328

24-
// IEnumerable<string> cultures = requestModel.Variants
25-
// .Where(v => v.Culture is not null)
26-
// .Select(v => v.Culture!);
27-
// AuthorizationResult authorizationResult = await _authorizationService.AuthorizeResourceAsync(
28-
// User,
29-
// ContentPermissionResource.WithKeys(ActionNew.ActionLetter, requestModel.Parent?.Id, cultures),
30-
// AuthorizationPolicies.ContentPermissionByResource);
31-
//
32-
// if (!authorizationResult.Succeeded)
33-
// {
34-
// return Forbidden();
35-
// }
29+
if (authorizationResult.Succeeded is false)
30+
{
31+
return Forbidden();
32+
}
3633

3734
return await authorizedHandler();
3835
}

src/Umbraco.Cms.Api.Management/Factories/DocumentUrlFactory.cs

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,38 @@
11
using Microsoft.Extensions.DependencyInjection;
2-
using Microsoft.Extensions.Logging;
32
using Umbraco.Cms.Api.Management.ViewModels.Document;
43
using Umbraco.Cms.Core.DependencyInjection;
54
using Umbraco.Cms.Core.Models;
6-
using Umbraco.Cms.Core.Models.PublishedContent;
7-
using Umbraco.Cms.Core.PublishedCache;
85
using Umbraco.Cms.Core.Routing;
96
using Umbraco.Cms.Core.Services;
10-
using Umbraco.Cms.Core.Services.Navigation;
11-
using Umbraco.Cms.Core.Web;
12-
using Umbraco.Extensions;
137

148
namespace Umbraco.Cms.Api.Management.Factories;
159

1610
public class DocumentUrlFactory : IDocumentUrlFactory
1711
{
18-
private readonly IDocumentUrlService _documentUrlService;
12+
private readonly IPublishedUrlInfoProvider _publishedUrlInfoProvider;
1913

2014

15+
[Obsolete("Use the constructor that takes all dependencies, scheduled for removal in v16")]
2116
public DocumentUrlFactory(IDocumentUrlService documentUrlService)
17+
: this(StaticServiceProvider.Instance.GetRequiredService<IPublishedUrlInfoProvider>())
2218
{
23-
_documentUrlService = documentUrlService;
19+
}
20+
21+
[Obsolete("Use the constructor that takes all dependencies, scheduled for removal in v16")]
22+
public DocumentUrlFactory(IDocumentUrlService documentUrlService, IPublishedUrlInfoProvider publishedUrlInfoProvider)
23+
: this(publishedUrlInfoProvider)
24+
{
25+
26+
}
27+
28+
public DocumentUrlFactory(IPublishedUrlInfoProvider publishedUrlInfoProvider)
29+
{
30+
_publishedUrlInfoProvider = publishedUrlInfoProvider;
2431
}
2532

2633
public async Task<IEnumerable<DocumentUrlInfo>> CreateUrlsAsync(IContent content)
2734
{
28-
IEnumerable<UrlInfo> urlInfos = await _documentUrlService.ListUrlsAsync(content.Key);
35+
ISet<UrlInfo> urlInfos = await _publishedUrlInfoProvider.GetAllAsync(content);
2936

3037
return urlInfos
3138
.Where(urlInfo => urlInfo.IsUrl)

src/Umbraco.Cms.Api.Management/Factories/IDocumentUrlFactory.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ namespace Umbraco.Cms.Api.Management.Factories;
66
public interface IDocumentUrlFactory
77
{
88
Task<IEnumerable<DocumentUrlInfo>> CreateUrlsAsync(IContent content);
9+
910
Task<IEnumerable<DocumentUrlInfoResponseModel>> CreateUrlSetsAsync(IEnumerable<IContent> contentItems);
1011
}

src/Umbraco.Core/DependencyInjection/UmbracoBuilder.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ private void AddCoreServices()
241241

242242
// register published router
243243
Services.AddUnique<IPublishedRouter, PublishedRouter>();
244+
Services.AddUnique<IPublishedUrlInfoProvider, PublishedUrlInfoProvider>();
244245

245246
Services.AddUnique<IEventMessagesFactory, DefaultEventMessagesFactory>();
246247
Services.AddUnique<IEventMessagesAccessor, HybridEventMessagesAccessor>();
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using Umbraco.Cms.Core.Models;
2+
3+
namespace Umbraco.Cms.Core.Routing;
4+
5+
public interface IPublishedUrlInfoProvider
6+
{
7+
/// <summary>
8+
/// Gets all published urls for a content item.
9+
/// </summary>
10+
/// <param name="content">The content to get urls for.</param>
11+
/// <returns>Set of all published url infos.</returns>
12+
Task<ISet<UrlInfo>> GetAllAsync(IContent content);
13+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
using Microsoft.Extensions.Logging;
2+
using Umbraco.Cms.Core.Models;
3+
using Umbraco.Cms.Core.Models.PublishedContent;
4+
using Umbraco.Cms.Core.Services;
5+
using Umbraco.Cms.Core.Web;
6+
using Umbraco.Extensions;
7+
8+
namespace Umbraco.Cms.Core.Routing;
9+
10+
public class PublishedUrlInfoProvider : IPublishedUrlInfoProvider
11+
{
12+
private readonly IPublishedUrlProvider _publishedUrlProvider;
13+
private readonly ILanguageService _languageService;
14+
private readonly IPublishedRouter _publishedRouter;
15+
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
16+
private readonly ILocalizedTextService _localizedTextService;
17+
private readonly ILogger<PublishedUrlInfoProvider> _logger;
18+
private readonly UriUtility _uriUtility;
19+
private readonly IVariationContextAccessor _variationContextAccessor;
20+
21+
public PublishedUrlInfoProvider(
22+
IPublishedUrlProvider publishedUrlProvider,
23+
ILanguageService languageService,
24+
IPublishedRouter publishedRouter,
25+
IUmbracoContextAccessor umbracoContextAccessor,
26+
ILocalizedTextService localizedTextService,
27+
ILogger<PublishedUrlInfoProvider> logger,
28+
UriUtility uriUtility,
29+
IVariationContextAccessor variationContextAccessor)
30+
{
31+
_publishedUrlProvider = publishedUrlProvider;
32+
_languageService = languageService;
33+
_publishedRouter = publishedRouter;
34+
_umbracoContextAccessor = umbracoContextAccessor;
35+
_localizedTextService = localizedTextService;
36+
_logger = logger;
37+
_uriUtility = uriUtility;
38+
_variationContextAccessor = variationContextAccessor;
39+
}
40+
41+
/// <inheritdoc />
42+
public async Task<ISet<UrlInfo>> GetAllAsync(IContent content)
43+
{
44+
HashSet<UrlInfo> urlInfos = [];
45+
var cultures = (await _languageService.GetAllAsync()).Select(x => x.IsoCode).ToArray();
46+
47+
// First we get the urls of all cultures, using the published router, meaning we respect any extensions.
48+
foreach (var culture in cultures)
49+
{
50+
var url = _publishedUrlProvider.GetUrl(content.Key, culture: culture);
51+
52+
// Handle "could not get URL"
53+
if (url is "#" or "#ex")
54+
{
55+
urlInfos.Add(UrlInfo.Message(_localizedTextService.Localize("content", "getUrlException"), culture));
56+
continue;
57+
}
58+
59+
// Check for collision
60+
Attempt<UrlInfo?> hasCollision = await VerifyCollisionAsync(content, url, culture);
61+
62+
if (hasCollision is { Success: true, Result: not null })
63+
{
64+
urlInfos.Add(hasCollision.Result);
65+
continue;
66+
}
67+
68+
urlInfos.Add(UrlInfo.Url(url, culture));
69+
}
70+
71+
// Then get "other" urls - I.E. Not what you'd get with GetUrl(), this includes all the urls registered using domains.
72+
// for these 'other' URLs, we don't check whether they are routable, collide, anything - we just report them.
73+
foreach (UrlInfo otherUrl in _publishedUrlProvider.GetOtherUrls(content.Id).OrderBy(x => x.Text).ThenBy(x => x.Culture))
74+
{
75+
urlInfos.Add(otherUrl);
76+
}
77+
78+
return urlInfos;
79+
}
80+
81+
private async Task<Attempt<UrlInfo?>> VerifyCollisionAsync(IContent content, string url, string culture)
82+
{
83+
var uri = new Uri(url.TrimEnd(Constants.CharArrays.ForwardSlash), UriKind.RelativeOrAbsolute);
84+
if (uri.IsAbsoluteUri is false)
85+
{
86+
uri = uri.MakeAbsolute(_umbracoContextAccessor.GetRequiredUmbracoContext().CleanedUmbracoUrl);
87+
}
88+
89+
uri = _uriUtility.UriToUmbraco(uri);
90+
IPublishedRequestBuilder builder = await _publishedRouter.CreateRequestAsync(uri);
91+
IPublishedRequest publishedRequest = await _publishedRouter.RouteRequestAsync(builder, new RouteRequestOptions(RouteDirection.Outbound));
92+
93+
if (publishedRequest.HasPublishedContent() is false)
94+
{
95+
if (_logger.IsEnabled(LogLevel.Debug))
96+
{
97+
const string logMsg = nameof(VerifyCollisionAsync) +
98+
" did not resolve a content item for original url: {Url}, translated to {TranslatedUrl} and culture: {Culture}";
99+
_logger.LogDebug(logMsg, url, uri, culture);
100+
}
101+
102+
var urlInfo = UrlInfo.Message(_localizedTextService.Localize("content", "routeErrorCannotRoute"), culture);
103+
return Attempt.Succeed(urlInfo);
104+
}
105+
106+
if (publishedRequest.IgnorePublishedContentCollisions)
107+
{
108+
return Attempt<UrlInfo?>.Fail();
109+
}
110+
111+
if (publishedRequest.PublishedContent?.Id != content.Id)
112+
{
113+
var collidingContent = publishedRequest.PublishedContent?.Key.ToString();
114+
115+
var urlInfo = UrlInfo.Message(_localizedTextService.Localize("content", "routeError", [collidingContent]), culture);
116+
return Attempt.Succeed(urlInfo);
117+
}
118+
119+
// No collision
120+
return Attempt<UrlInfo?>.Fail();
121+
}
122+
}

src/Umbraco.Core/Services/DocumentUrlService.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -525,6 +525,7 @@ public bool HasAny()
525525
}
526526

527527

528+
[Obsolete("This method is obsolete and will be removed in future versions. Use IPublishedUrlInfoProvider.GetAllAsync instead.")]
528529
public async Task<IEnumerable<UrlInfo>> ListUrlsAsync(Guid contentKey)
529530
{
530531
var result = new List<UrlInfo>();

src/Umbraco.Web.UI.Client/src/apps/app/app.context.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { UmbAppContextConfig } from './app-context-config.interface.js';
2+
import { UmbNetworkConnectionStatusManager } from './network-connection-status.manager.js';
23
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
34
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
45
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
@@ -13,6 +14,8 @@ export class UmbAppContext extends UmbContextBase<UmbAppContext> {
1314
this.#serverUrl = config.serverUrl;
1415
this.#backofficePath = config.backofficePath;
1516
this.#serverConnection = config.serverConnection;
17+
18+
new UmbNetworkConnectionStatusManager(this);
1619
}
1720

1821
getBackofficePath() {
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
2+
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
3+
import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api';
4+
import {
5+
UMB_NOTIFICATION_CONTEXT,
6+
type UmbNotificationContext,
7+
type UmbNotificationHandler,
8+
} from '@umbraco-cms/backoffice/notification';
9+
10+
export class UmbNetworkConnectionStatusManager extends UmbControllerBase {
11+
#notificationContext?: UmbNotificationContext;
12+
#offlineNotification?: UmbNotificationHandler;
13+
14+
#localize = new UmbLocalizationController(this);
15+
16+
constructor(host: UmbControllerHost) {
17+
super(host);
18+
19+
this.consumeContext(UMB_NOTIFICATION_CONTEXT, (notificationContext) => {
20+
this.#notificationContext = notificationContext;
21+
});
22+
23+
window.addEventListener('online', () => this.#onOnline());
24+
window.addEventListener('offline', () => this.#onOffline());
25+
}
26+
27+
#onOnline() {
28+
this.#offlineNotification?.close();
29+
this.#notificationContext?.peek('positive', {
30+
data: {
31+
headline: this.#localize.term('speechBubbles_onlineHeadline'),
32+
message: this.#localize.term('speechBubbles_onlineMessage'),
33+
},
34+
});
35+
}
36+
37+
#onOffline() {
38+
this.#offlineNotification = this.#notificationContext?.stay('danger', {
39+
data: {
40+
headline: this.#localize.term('speechBubbles_offlineHeadline'),
41+
message: this.#localize.term('speechBubbles_offlineMessage'),
42+
},
43+
});
44+
}
45+
46+
override destroy() {
47+
this.#offlineNotification?.close();
48+
this.removeEventListener('online', () => this.#onOnline());
49+
this.removeEventListener('offline', () => this.#onOffline());
50+
super.destroy();
51+
}
52+
}

src/Umbraco.Web.UI.Client/src/assets/lang/da-dk.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,9 @@ export default {
345345
clickToUpload: 'Klik for at uploade',
346346
orClickHereToUpload: 'eller klik her for at vælge filer',
347347
disallowedFileType: 'Kan ikke uploade denne fil, den har ikke en godkendt filtype',
348+
disallowedMediaType: "Kan ikke uploade denne fil, mediatypen med alias '%0%' er ikke tilladt her",
349+
invalidFileName: 'Kan ikke uploade denne fil, den har et ugyldigt filnavn',
350+
invalidFileSize: 'Kan ikke uploade denne fil, den er for stor',
348351
maxFileSize: 'Maks filstørrelse er',
349352
mediaRoot: 'Medie rod',
350353
moveToSameFolderFailed: 'Overordnet og destinations mappe kan ikke være den samme',
@@ -353,8 +356,6 @@ export default {
353356
dragAndDropYourFilesIntoTheArea:
354357
'Træk dine filer ind i dropzonen for, at uploade dem til\n mediebiblioteket.\n ',
355358
uploadNotAllowed: 'Upload er ikke tiladt på denne lokation',
356-
disallowedMediaType: "Cannot upload this file, the media type with alias '%0%' is not allowed here",
357-
invalidFileName: 'Cannot upload this file, it does not have a valid file name',
358359
},
359360
member: {
360361
createNewMember: 'Opret et nyt medlem',

0 commit comments

Comments
 (0)