Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,26 @@
/// </summary>
/// <param name="ids">A set of document IDs for which to retrieve URLs.</param>
/// <returns>A task representing the asynchronous operation. The task result contains an <see cref="IActionResult"/> with a collection of URL information for each requested document.</returns>
[Obsolete("Please use the overload taking all parameters. Scheduled for removal in Umbraco 19.")]

Check warning on line 40 in src/Umbraco.Cms.Api.Management/Controllers/Document/DocumentUrlController.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Do not forget to remove this deprecated code someday.

See more on https://sonarcloud.io/project/issues?id=umbraco_Umbraco-CMS&issues=AZ8moIdTVu2cc6JieiVA&open=AZ8moIdTVu2cc6JieiVA&pullRequest=23265
[ApiExplorerSettings(IgnoreApi = true)]
public Task<IActionResult> GetUrls([FromQuery(Name = "id")] HashSet<Guid> ids)
=> GetUrls(ids, null);

/// <summary>
/// Retrieves the URLs for the documents identified by the specified set of IDs.
/// </summary>
/// <param name="ids">A set of document IDs for which to retrieve URLs.</param>
/// <param name="culture">An optional culture to restrict variant document URLs to. When omitted, URLs for all cultures are returned.</param>
/// <returns>A task representing the asynchronous operation. The task result contains an <see cref="IActionResult"/> with a collection of URL information for each requested document.</returns>
[MapToApiVersion("1.0")]
[HttpGet("urls")]
[ProducesResponseType(typeof(IEnumerable<DocumentUrlInfoResponseModel>), StatusCodes.Status200OK)]
[EndpointSummary("Gets URLs for a document.")]
[EndpointDescription("Gets the URLs for the document identified by the provided Id.")]
public async Task<IActionResult> GetUrls([FromQuery(Name = "id")] HashSet<Guid> ids)
public async Task<IActionResult> GetUrls([FromQuery(Name = "id")] HashSet<Guid> ids, [FromQuery] string? culture)
{
IEnumerable<IContent> items = _contentService.GetByIds(ids);

return Ok(await _documentUrlFactory.CreateUrlSetsAsync(items));
return Ok(await _documentUrlFactory.CreateUrlSetsAsync(items, culture));
}
}
26 changes: 22 additions & 4 deletions src/Umbraco.Cms.Api.Management/Factories/DocumentUrlFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,18 @@ public DocumentUrlFactory(
/// </summary>
/// <param name="content">The content item for which to generate URLs.</param>
/// <returns>A task representing the asynchronous operation. The task result contains an <see cref="IEnumerable{DocumentUrlInfo}"/> with the generated URLs.</returns>
public async Task<IEnumerable<DocumentUrlInfo>> CreateUrlsAsync(IContent content)
public Task<IEnumerable<DocumentUrlInfo>> CreateUrlsAsync(IContent content)
=> CreateUrlsAsync(content, culture: null);

/// <summary>
/// Asynchronously generates a collection of <see cref="DocumentUrlInfo"/> instances representing the URLs for the specified content item, optionally restricted to a single culture.
/// </summary>
/// <param name="content">The content item for which to generate URLs.</param>
/// <param name="culture">The culture to restrict variant content urls to, or <c>null</c> for all cultures.</param>
/// <returns>A task representing the asynchronous operation. The task result contains an <see cref="IEnumerable{DocumentUrlInfo}"/> with the generated URLs.</returns>
public async Task<IEnumerable<DocumentUrlInfo>> CreateUrlsAsync(IContent content, string? culture)
{
ISet<UrlInfo> urlInfos = await _publishedUrlInfoProvider.GetAllAsync(content);
ISet<UrlInfo> urlInfos = await _publishedUrlInfoProvider.GetAllAsync(content, culture);
return urlInfos
.Select(CreateDocumentUrlInfo)
.ToArray();
Expand All @@ -61,13 +70,22 @@ public async Task<IEnumerable<DocumentUrlInfo>> CreateUrlsAsync(IContent content
/// </summary>
/// <param name="contentItems">The collection of <see cref="IContent"/> items for which to generate URL sets.</param>
/// <returns>A task representing the asynchronous operation. The task result contains a collection of <see cref="DocumentUrlInfoResponseModel"/> objects, each representing the URL set for a content item.</returns>
public async Task<IEnumerable<DocumentUrlInfoResponseModel>> CreateUrlSetsAsync(IEnumerable<IContent> contentItems)
public Task<IEnumerable<DocumentUrlInfoResponseModel>> CreateUrlSetsAsync(IEnumerable<IContent> contentItems)
=> CreateUrlSetsAsync(contentItems, culture: null);

/// <summary>
/// Asynchronously creates URL sets for the specified collection of content items, optionally restricted to a single culture.
/// </summary>
/// <param name="contentItems">The collection of <see cref="IContent"/> items for which to generate URL sets.</param>
/// <param name="culture">The culture to restrict variant content urls to, or <c>null</c> for all cultures.</param>
/// <returns>A task representing the asynchronous operation. The task result contains a collection of <see cref="DocumentUrlInfoResponseModel"/> objects, each representing the URL set for a content item.</returns>
public async Task<IEnumerable<DocumentUrlInfoResponseModel>> CreateUrlSetsAsync(IEnumerable<IContent> contentItems, string? culture)
{
var documentUrlInfoResourceSets = new List<DocumentUrlInfoResponseModel>();

foreach (IContent content in contentItems)
{
IEnumerable<DocumentUrlInfo> urls = await CreateUrlsAsync(content);
IEnumerable<DocumentUrlInfo> urls = await CreateUrlsAsync(content, culture);
documentUrlInfoResourceSets.Add(new DocumentUrlInfoResponseModel(content.Key, urls));
}

Expand Down
20 changes: 20 additions & 0 deletions src/Umbraco.Cms.Api.Management/Factories/IDocumentUrlFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,33 @@
/// <returns>A task that represents the asynchronous operation. The task result contains a collection of DocumentUrlInfo.</returns>
Task<IEnumerable<DocumentUrlInfo>> CreateUrlsAsync(IContent content);

/// <summary>
/// Creates URLs asynchronously for the given content, optionally restricted to a single culture.
/// </summary>
/// <param name="content">The content to create URLs for.</param>
/// <param name="culture">The culture to restrict variant content urls to, or <c>null</c> for all cultures.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a collection of DocumentUrlInfo.</returns>
// TODO (V19): Remove the default implementation.

Check warning on line 24 in src/Umbraco.Cms.Api.Management/Factories/IDocumentUrlFactory.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this 'TODO' comment.

See more on https://sonarcloud.io/project/issues?id=umbraco_Umbraco-CMS&issues=AZ8moIePVu2cc6JieiVD&open=AZ8moIePVu2cc6JieiVD&pullRequest=23265
Task<IEnumerable<DocumentUrlInfo>> CreateUrlsAsync(IContent content, string? culture)
=> CreateUrlsAsync(content);
Comment thread
AndyButland marked this conversation as resolved.

/// <summary>
/// Asynchronously creates URL sets for the specified content items.
/// </summary>
/// <param name="contentItems">The content items for which to generate URL sets.</param>
/// <returns>A task representing the asynchronous operation, with a result containing an enumerable of <see cref="DocumentUrlInfoResponseModel"/> representing the URL sets.</returns>
Task<IEnumerable<DocumentUrlInfoResponseModel>> CreateUrlSetsAsync(IEnumerable<IContent> contentItems);

/// <summary>
/// Asynchronously creates URL sets for the specified content items, optionally restricted to a single culture.
/// </summary>
/// <param name="contentItems">The content items for which to generate URL sets.</param>
/// <param name="culture">The culture to restrict variant content urls to, or <c>null</c> for all cultures.</param>
/// <returns>A task representing the asynchronous operation, with a result containing an enumerable of <see cref="DocumentUrlInfoResponseModel"/> representing the URL sets.</returns>
// TODO (V19): Remove the default implementation.

Check warning on line 41 in src/Umbraco.Cms.Api.Management/Factories/IDocumentUrlFactory.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this 'TODO' comment.

See more on https://sonarcloud.io/project/issues?id=umbraco_Umbraco-CMS&issues=AZ8moIePVu2cc6JieiVE&open=AZ8moIePVu2cc6JieiVE&pullRequest=23265
Task<IEnumerable<DocumentUrlInfoResponseModel>> CreateUrlSetsAsync(IEnumerable<IContent> contentItems, string? culture)
=> CreateUrlSetsAsync(contentItems);
Comment thread
AndyButland marked this conversation as resolved.

/// <summary>
/// Asynchronously retrieves preview URL information for the specified content item.
/// </summary>
Expand Down
7 changes: 7 additions & 0 deletions src/Umbraco.Cms.Api.Management/OpenApi.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions src/Umbraco.Core/Routing/IPublishedUrlInfoProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,17 @@
/// <param name="content">The content to get urls for.</param>
/// <returns>Set of all published url infos.</returns>
Task<ISet<UrlInfo>> GetAllAsync(IContent content);

/// <summary>
/// Gets the published urls for a content item, optionally restricted to a single culture.
/// </summary>
/// <param name="content">The content to get urls for.</param>
/// <param name="culture">
/// The culture to restrict variant content urls to, or <c>null</c> to return urls for all cultures.
/// Ignored for invariant content, which always returns all of its domain urls.
/// </param>
Comment thread
AndyButland marked this conversation as resolved.
/// <returns>Set of published url infos.</returns>
// TODO (V19): Remove the default implementation.

Check warning on line 26 in src/Umbraco.Core/Routing/IPublishedUrlInfoProvider.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this 'TODO' comment.

See more on https://sonarcloud.io/project/issues?id=umbraco_Umbraco-CMS&issues=AZ8moIfXVu2cc6JieiVJ&open=AZ8moIfXVu2cc6JieiVJ&pullRequest=23265
Task<ISet<UrlInfo>> GetAllAsync(IContent content, string? culture)
=> GetAllAsync(content);
}
97 changes: 68 additions & 29 deletions src/Umbraco.Core/Routing/PublishedUrlInfoProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,41 +56,49 @@
}

/// <inheritdoc />
public async Task<ISet<UrlInfo>> GetAllAsync(IContent content)
public Task<ISet<UrlInfo>> GetAllAsync(IContent content)
=> GetAllAsync(content, culture: null);

/// <inheritdoc />
public async Task<ISet<UrlInfo>> GetAllAsync(IContent content, string? culture)
{
HashSet<UrlInfo> urlInfos = [];
var isInvariant = !content.ContentType.VariesByCulture();

IEnumerable<string> cultures = await GetCulturesForUrlLookupAsync(content);

foreach (var culture in cultures)
// Variant content restricted to a single culture, matched against the installed cultures (using their casing).
if (isInvariant is false && culture is not null)

Check warning on line 68 in src/Umbraco.Core/Routing/PublishedUrlInfoProvider.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Remove the unnecessary Boolean literal(s).

See more on https://sonarcloud.io/project/issues?id=umbraco_Umbraco-CMS&issues=AZ8moIfPVu2cc6JieiVI&open=AZ8moIfPVu2cc6JieiVI&pullRequest=23265
{
var url = _publishedUrlProvider.GetUrl(content.Key, culture: culture);
var matchedCulture = (await _languageService.GetAllIsoCodesAsync())
.FirstOrDefault(x => x.InvariantEquals(culture));

// Handle "could not get URL"
if (url is Constants.Routing.Unroutable or Constants.Routing.UrlProviderException)
{
// For invariant content, a missing URL just means there's no domain
// for this culture — not a problem worth reporting.
if (isInvariant)
{
continue;
}

urlInfos.Add(UrlInfo.AsMessage(_localizedTextService.Localize("content", "getUrlException"), UrlProviderAlias, culture));
continue;
}
// A specific culture was requested that is not an installed culture - there are no urls to report.
return matchedCulture is null
? new HashSet<UrlInfo>()
: await BuildUrlInfosAsync(content, [matchedCulture], scopedCulture: matchedCulture, isInvariant);
}

// Check for collision
Attempt<UrlInfo?> hasCollision = await VerifyCollisionAsync(content, url, culture);
// Invariant content (culture ignored), or all cultures.
IReadOnlyCollection<string> cultures = (await GetCulturesForUrlLookupAsync(content)).ToArray();
return await BuildUrlInfosAsync(content, cultures, scopedCulture: null, isInvariant);
}

if (hasCollision is { Success: true, Result: not null })
/// <summary>
/// Builds the set of <see cref="UrlInfo" /> for the given cultures, plus the "other" URLs (unless the content is
/// trashed). When <paramref name="scopedCulture" /> is set, the "other" URLs are filtered to that culture.
/// </summary>
private async Task<ISet<UrlInfo>> BuildUrlInfosAsync(
IContent content,
IReadOnlyCollection<string> cultures,
string? scopedCulture,
bool isInvariant)
{
var urlInfos = new HashSet<UrlInfo>();
foreach (var contentCulture in cultures)
{
UrlInfo? urlInfo = await GetCultureUrlInfoAsync(content, contentCulture, isInvariant);
if (urlInfo is not null)
{
urlInfos.Add(hasCollision.Result);
continue;
urlInfos.Add(urlInfo);
}

urlInfos.Add(UrlInfo.AsUrl(url, UrlProviderAlias, culture));
}

// If the content is trashed, we can't get the other URLs, as we have no parent structure to navigate through.
Expand All @@ -99,16 +107,47 @@
return urlInfos;
}

// Then get "other" urls - I.E. Not what you'd get with GetUrl(), this includes all the urls registered using domains.
// for these 'other' URLs, we don't check whether they are routable, collide, anything - we just report them.
foreach (UrlInfo otherUrl in _publishedUrlProvider.GetOtherUrls(content.Id).OrderBy(x => x.Message).ThenBy(x => x.Culture))
foreach (UrlInfo otherUrl in GetOtherUrls(content, scopedCulture))

Check notice on line 110 in src/Umbraco.Core/Routing/PublishedUrlInfoProvider.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (v17/dev)

✅ No longer an issue: Complex Method

GetAllAsync is no longer above the threshold for cyclomatic complexity
{
urlInfos.Add(otherUrl);
}

return urlInfos;
}

/// <summary>
/// Gets the <see cref="UrlInfo" /> for a single culture, or <c>null</c> if there is nothing to report
/// (an unroutable URL on invariant content). Reports a message for an unroutable URL or a collision.
/// </summary>
private async Task<UrlInfo?> GetCultureUrlInfoAsync(IContent content, string culture, bool isInvariant)
{
var url = _publishedUrlProvider.GetUrl(content.Key, culture: culture);

if (url is Constants.Routing.Unroutable or Constants.Routing.UrlProviderException)
{
// For invariant content, a missing URL just means there's no domain for this culture - not worth reporting.
return isInvariant
? null
: UrlInfo.AsMessage(_localizedTextService.Localize("content", "getUrlException"), UrlProviderAlias, culture);
}

Attempt<UrlInfo?> hasCollision = await VerifyCollisionAsync(content, url, culture);
return hasCollision is { Success: true, Result: not null }
? hasCollision.Result
: UrlInfo.AsUrl(url, UrlProviderAlias, culture);
}

/// <summary>
/// Gets the "other" URLs - i.e. not what you'd get with GetUrl(), including all the URLs registered using domains.
/// These are not checked for routability or collisions - they are just reported. When scoped to a single culture,
/// only the other URLs for that culture are returned.
/// </summary>
private IEnumerable<UrlInfo> GetOtherUrls(IContent content, string? scopedCulture)
=> _publishedUrlProvider.GetOtherUrls(content.Id)
.Where(x => scopedCulture is null || string.Equals(x.Culture, scopedCulture, StringComparison.OrdinalIgnoreCase))
.OrderBy(x => x.Message)
.ThenBy(x => x.Culture);

/// <summary>
/// Gets the cultures to query URLs for.
/// For invariant content, returns only cultures that have a domain assigned to the content
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,20 @@ export const urlHandlers = [
const ids = url.searchParams.getAll('id');
if (!ids.length) return new HttpResponse(null, { status: 400 });

// When a culture is provided, variant URLs are restricted to that culture (invariant URLs, which have
// no culture, are always kept). This mirrors the Management API's document URLs endpoint.
const culture = url.searchParams.get('culture');

const response: GetDocumentUrlsResponse = ids.map((id) => ({
id,
urlInfos: umbDocumentMockDb.url.getUrls(id).map((urlInfo) => ({
...urlInfo,
message: null,
provider: 'Default',
})),
urlInfos: umbDocumentMockDb.url
.getUrls(id)
.filter((urlInfo) => !culture || urlInfo.culture === null || urlInfo.culture === culture)
.map((urlInfo) => ({
...urlInfo,
message: null,
provider: 'Default',
})),
}));

return HttpResponse.json(response);
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading