2018-03-26 10:55:49 +02:00
using System.Globalization ;
2020-11-19 19:23:41 +11:00
using Microsoft.AspNetCore.Authorization ;
2020-05-26 18:50:32 +02:00
using Microsoft.AspNetCore.Mvc ;
2022-05-02 15:42:19 +02:00
using Microsoft.Extensions.DependencyInjection ;
2020-08-21 14:52:47 +01:00
using Microsoft.Extensions.Options ;
2021-02-18 11:06:02 +01:00
using Umbraco.Cms.Core.Configuration.Models ;
using Umbraco.Cms.Core.Mapping ;
using Umbraco.Cms.Core.Models ;
using Umbraco.Cms.Core.Services ;
using Umbraco.Cms.Web.Common.Attributes ;
using Umbraco.Cms.Web.Common.Authorization ;
2021-01-13 11:39:44 +01:00
using Umbraco.Extensions ;
2021-02-18 11:06:02 +01:00
using Constants = Umbraco . Cms . Core . Constants ;
using Language = Umbraco . Cms . Core . Models . ContentEditing . Language ;
2018-03-26 10:55:49 +02:00
2021-02-18 11:06:02 +01:00
namespace Umbraco.Cms.Web.BackOffice.Controllers
2018-03-26 10:55:49 +02:00
{
/// <summary>
/// Backoffice controller supporting the dashboard for language administration.
/// </summary>
2020-06-09 13:01:05 +10:00
[PluginController(Constants.Web.Mvc.BackOfficeApiArea)]
2018-03-26 10:55:49 +02:00
public class LanguageController : UmbracoAuthorizedJsonController
{
2020-05-26 18:50:32 +02:00
private readonly ILocalizationService _localizationService ;
2021-04-20 19:34:18 +02:00
private readonly IUmbracoMapper _umbracoMapper ;
2020-05-26 18:50:32 +02:00
2022-05-02 15:42:19 +02:00
[ActivatorUtilitiesConstructor]
public LanguageController ( ILocalizationService localizationService , IUmbracoMapper umbracoMapper )
2020-05-26 18:50:32 +02:00
{
_localizationService = localizationService ? ? throw new ArgumentNullException ( nameof ( localizationService ) ) ;
_umbracoMapper = umbracoMapper ? ? throw new ArgumentNullException ( nameof ( umbracoMapper ) ) ;
}
2022-05-02 15:42:19 +02:00
[Obsolete("Use the constructor without global settings instead, scheduled for removal in V11.")]
public LanguageController ( ILocalizationService localizationService , IUmbracoMapper umbracoMapper , IOptionsSnapshot < GlobalSettings > globalSettings )
: this ( localizationService , umbracoMapper )
{ }
2018-03-26 10:55:49 +02:00
/// <summary>
/// Returns all cultures available for creating languages.
/// </summary>
/// <returns></returns>
[HttpGet]
2018-03-30 01:20:40 +11:00
public IDictionary < string , string > GetAllCultures ( )
2022-05-02 15:42:19 +02:00
= > CultureInfo . GetCultures ( CultureTypes . AllCultures ) . DistinctBy ( x = > x . Name ) . OrderBy ( x = > x . EnglishName ) . ToDictionary ( x = > x . Name , x = > x . EnglishName ) ;
2018-03-26 10:55:49 +02:00
/// <summary>
/// Returns all currently configured languages.
/// </summary>
/// <returns></returns>
[HttpGet]
2022-04-01 11:09:51 +02:00
public IEnumerable < Language > ? GetAllLanguages ( )
2018-03-26 10:55:49 +02:00
{
2020-05-26 18:50:32 +02:00
var allLanguages = _localizationService . GetAllLanguages ( ) ;
2018-03-30 01:20:40 +11:00
2020-05-26 18:50:32 +02:00
return _umbracoMapper . Map < IEnumerable < ILanguage > , IEnumerable < Language > > ( allLanguages ) ;
2018-03-26 10:55:49 +02:00
}
2018-03-30 01:20:40 +11:00
[HttpGet]
2022-04-01 11:09:51 +02:00
public ActionResult < Language ? > GetLanguage ( int id )
2018-03-30 01:20:40 +11:00
{
2020-05-26 18:50:32 +02:00
var lang = _localizationService . GetLanguageById ( id ) ;
2018-03-30 01:20:40 +11:00
if ( lang = = null )
2022-05-02 15:42:19 +02:00
{
2020-05-26 18:50:32 +02:00
return NotFound ( ) ;
2022-05-02 15:42:19 +02:00
}
2018-03-30 01:20:40 +11:00
2020-05-26 18:50:32 +02:00
return _umbracoMapper . Map < Language > ( lang ) ;
2018-03-30 01:20:40 +11:00
}
2018-03-26 10:55:49 +02:00
/// <summary>
/// Deletes a language with a given ID
/// </summary>
2020-11-19 19:23:41 +11:00
[Authorize(Policy = AuthorizationPolicies.TreeAccessLanguages)]
2018-03-26 10:55:49 +02:00
[HttpDelete]
[HttpPost]
2020-05-26 18:50:32 +02:00
public IActionResult DeleteLanguage ( int id )
2018-03-26 10:55:49 +02:00
{
2020-05-26 18:50:32 +02:00
var language = _localizationService . GetLanguageById ( id ) ;
2018-07-06 16:01:54 +02:00
if ( language = = null )
{
return NotFound ( ) ;
}
2018-03-29 23:48:54 +11:00
2018-09-12 11:47:04 +02:00
// the service would not let us do it, but test here nevertheless
2018-09-13 14:11:17 +02:00
if ( language . IsDefault )
2018-03-27 16:33:28 +01:00
{
2018-09-12 11:47:04 +02:00
var message = $"Language '{language.IsoCode}' is currently set to 'default' and can not be deleted." ;
2021-06-25 10:29:18 -06:00
return ValidationProblem ( message ) ;
2018-07-06 16:01:54 +02:00
}
2018-09-13 14:59:45 +02:00
// service is happy deleting a language that's fallback for another language,
// will just remove it - so no need to check here
2020-05-26 18:50:32 +02:00
_localizationService . Delete ( language ) ;
2018-03-29 23:48:54 +11:00
return Ok ( ) ;
2018-03-26 10:55:49 +02:00
}
/// <summary>
2018-03-30 01:20:40 +11:00
/// Creates or saves a language
2018-03-26 10:55:49 +02:00
/// </summary>
2020-11-19 19:23:41 +11:00
[Authorize(Policy = AuthorizationPolicies.TreeAccessLanguages)]
2018-03-26 10:55:49 +02:00
[HttpPost]
2022-04-01 11:09:51 +02:00
public ActionResult < Language ? > SaveLanguage ( Language language )
2018-03-26 10:55:49 +02:00
{
2018-03-30 01:20:40 +11:00
if ( ! ModelState . IsValid )
2022-05-02 15:42:19 +02:00
{
2021-06-25 10:29:18 -06:00
return ValidationProblem ( ModelState ) ;
2022-05-02 15:42:19 +02:00
}
2018-03-30 01:20:40 +11:00
2019-01-26 10:52:19 -05:00
// this is prone to race conditions but the service will not let us proceed anyways
2020-05-26 18:50:32 +02:00
var existingByCulture = _localizationService . GetLanguageByIsoCode ( language . IsoCode ) ;
2018-03-30 01:20:40 +11:00
2019-03-12 08:20:43 +01:00
// the localization service might return the generic language even when queried for specific ones (e.g. "da" when queried for "da-DK")
// - we need to handle that explicitly
2019-10-14 19:04:51 +11:00
if ( existingByCulture ? . IsoCode ! = language . IsoCode )
2019-03-12 08:20:43 +01:00
{
2019-10-14 19:04:51 +11:00
existingByCulture = null ;
2019-03-12 08:20:43 +01:00
}
2019-10-14 19:04:51 +11:00
if ( existingByCulture ! = null & & language . Id ! = existingByCulture . Id )
2018-03-26 10:55:49 +02:00
{
2022-05-02 15:42:19 +02:00
// Someone is trying to create a language that already exist
2018-03-30 01:20:40 +11:00
ModelState . AddModelError ( "IsoCode" , "The language " + language . IsoCode + " already exists" ) ;
2021-06-25 10:29:18 -06:00
return ValidationProblem ( ModelState ) ;
2018-03-30 01:20:40 +11:00
}
2020-05-26 18:50:32 +02:00
var existingById = language . Id ! = default ? _localizationService . GetLanguageById ( language . Id ) : null ;
2019-10-14 19:04:51 +11:00
if ( existingById = = null )
2018-03-30 01:20:40 +11:00
{
2022-05-02 15:42:19 +02:00
// Creating a new lang...
2018-03-30 01:20:40 +11:00
CultureInfo culture ;
try
{
2022-04-01 11:09:51 +02:00
culture = CultureInfo . GetCultureInfo ( language . IsoCode ! ) ;
2018-03-30 01:20:40 +11:00
}
catch ( CultureNotFoundException )
2018-03-26 10:55:49 +02:00
{
2018-03-30 01:20:40 +11:00
ModelState . AddModelError ( "IsoCode" , "No Culture found with name " + language . IsoCode ) ;
2021-06-25 10:29:18 -06:00
return ValidationProblem ( ModelState ) ;
2018-03-26 10:55:49 +02:00
}
2018-03-30 01:20:40 +11:00
2018-09-13 14:59:45 +02:00
// create it (creating a new language cannot create a fallback cycle)
2022-05-02 15:42:19 +02:00
var newLang = new Core . Models . Language ( culture . Name , language . Name ? ? culture . EnglishName )
2018-03-30 01:20:40 +11:00
{
2018-07-18 12:27:14 +02:00
IsDefault = language . IsDefault ,
IsMandatory = language . IsMandatory ,
2018-07-12 20:52:02 +01:00
FallbackLanguageId = language . FallbackLanguageId
2018-03-30 01:20:40 +11:00
} ;
2018-07-06 15:23:21 +02:00
2020-05-26 18:50:32 +02:00
_localizationService . Save ( newLang ) ;
return _umbracoMapper . Map < Language > ( newLang ) ;
2018-03-26 10:55:49 +02:00
}
2022-05-02 15:42:19 +02:00
existingById . IsoCode = language . IsoCode ;
if ( ! string . IsNullOrEmpty ( language . Name ) )
{
existingById . CultureName = language . Name ;
}
2018-09-12 11:47:04 +02:00
// note that the service will prevent the default language from being "un-defaulted"
// but does not hurt to test here - though the UI should prevent it too
2019-10-14 19:04:51 +11:00
if ( existingById . IsDefault & & ! language . IsDefault )
2018-09-12 11:47:04 +02:00
{
2018-09-13 14:11:17 +02:00
ModelState . AddModelError ( "IsDefault" , "Cannot un-default the default language." ) ;
2021-06-25 10:29:18 -06:00
return ValidationProblem ( ModelState ) ;
2018-09-12 11:47:04 +02:00
}
2019-10-14 19:04:51 +11:00
existingById . IsDefault = language . IsDefault ;
2022-05-02 15:42:19 +02:00
existingById . IsMandatory = language . IsMandatory ;
2019-10-14 19:04:51 +11:00
existingById . FallbackLanguageId = language . FallbackLanguageId ;
2018-07-06 15:51:13 +02:00
2018-09-13 14:59:45 +02:00
// modifying an existing language can create a fallback, verify
2019-01-26 10:52:19 -05:00
// note that the service will check again, dealing with race conditions
2019-10-14 19:04:51 +11:00
if ( existingById . FallbackLanguageId . HasValue )
2018-07-06 15:51:13 +02:00
{
2020-05-26 18:50:32 +02:00
var languages = _localizationService . GetAllLanguages ( ) . ToDictionary ( x = > x . Id , x = > x ) ;
2019-10-14 19:04:51 +11:00
if ( ! languages . ContainsKey ( existingById . FallbackLanguageId . Value ) )
2018-09-13 14:59:45 +02:00
{
ModelState . AddModelError ( "FallbackLanguage" , "The selected fall back language does not exist." ) ;
2021-06-25 10:29:18 -06:00
return ValidationProblem ( ModelState ) ;
2018-09-13 14:59:45 +02:00
}
2022-05-02 15:42:19 +02:00
2019-10-14 19:04:51 +11:00
if ( CreatesCycle ( existingById , languages ) )
2018-09-13 14:59:45 +02:00
{
2019-10-14 19:04:51 +11:00
ModelState . AddModelError ( "FallbackLanguage" , $"The selected fall back language {languages[existingById.FallbackLanguageId.Value].IsoCode} would create a circular path." ) ;
2021-06-25 10:29:18 -06:00
return ValidationProblem ( ModelState ) ;
2018-09-13 14:59:45 +02:00
}
2018-07-06 15:51:13 +02:00
}
2020-05-26 18:50:32 +02:00
_localizationService . Save ( existingById ) ;
return _umbracoMapper . Map < Language > ( existingById ) ;
2018-09-13 14:59:45 +02:00
}
// see LocalizationService
private bool CreatesCycle ( ILanguage language , IDictionary < int , ILanguage > languages )
2018-07-06 15:23:21 +02:00
{
2018-09-13 14:59:45 +02:00
// a new language is not referenced yet, so cannot be part of a cycle
2022-05-02 15:42:19 +02:00
if ( ! language . HasIdentity )
{
return false ;
}
2018-07-06 15:51:13 +02:00
2018-09-13 14:59:45 +02:00
var id = language . FallbackLanguageId ;
while ( true ) // assuming languages does not already contains a cycle, this must end
2018-07-06 15:51:13 +02:00
{
2022-05-02 15:42:19 +02:00
if ( ! id . HasValue )
{
return false ; // no fallback means no cycle
}
if ( id . Value = = language . Id )
{
return true ; // back to language = cycle!
}
2018-09-13 14:59:45 +02:00
id = languages [ id . Value ] . FallbackLanguageId ; // else keep chaining
2018-07-06 15:51:13 +02:00
}
2018-07-12 20:52:02 +01:00
}
2018-03-26 10:55:49 +02:00
}
2018-03-29 23:48:54 +11:00
}