2020-05-26 18:50:32 +02:00
using System ;
using System.Collections.Generic ;
2018-03-26 10:55:49 +02:00
using System.Globalization ;
using System.Linq ;
2020-05-26 18:50:32 +02:00
using Microsoft.AspNetCore.Mvc ;
2018-03-30 01:20:40 +11:00
using Umbraco.Core ;
2020-05-26 18:50:32 +02:00
using Umbraco.Core.Configuration ;
using Umbraco.Core.Mapping ;
2018-04-04 01:59:51 +10:00
using Umbraco.Core.Models ;
2020-05-26 18:50:32 +02:00
using Umbraco.Core.Services ;
using Umbraco.Web.BackOffice.Filters ;
using Umbraco.Web.Common.Attributes ;
using Umbraco.Web.Common.Exceptions ;
using Umbraco.Web.Editors ;
2018-04-04 01:59:51 +10:00
using Language = Umbraco . Web . Models . ContentEditing . Language ;
2018-03-26 10:55:49 +02:00
2020-05-26 18:50:32 +02:00
namespace Umbraco.Web.BackOffice.Controllers
2018-03-26 10:55:49 +02:00
{
/// <summary>
/// Backoffice controller supporting the dashboard for language administration.
/// </summary>
[PluginController("UmbracoApi")]
2020-05-28 15:20:02 +02:00
//[PrefixlessBodyModelValidator]
2018-03-26 10:55:49 +02:00
public class LanguageController : UmbracoAuthorizedJsonController
{
2020-05-26 18:50:32 +02:00
private readonly ILocalizationService _localizationService ;
private readonly UmbracoMapper _umbracoMapper ;
private readonly IGlobalSettings _globalSettings ;
public LanguageController ( ILocalizationService localizationService ,
UmbracoMapper umbracoMapper ,
IGlobalSettings globalSettings )
{
_localizationService = localizationService ? ? throw new ArgumentNullException ( nameof ( localizationService ) ) ;
_umbracoMapper = umbracoMapper ? ? throw new ArgumentNullException ( nameof ( umbracoMapper ) ) ;
_globalSettings = globalSettings ? ? throw new ArgumentNullException ( nameof ( globalSettings ) ) ;
}
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 ( )
2018-03-26 10:55:49 +02:00
{
2018-11-08 16:33:19 +01:00
// get cultures - new-ing instances to get proper display name,
// in the current culture, and not the cached one
// (see notes in Language class about culture info names)
return CultureInfo . GetCultures ( CultureTypes . AllCultures )
. Where ( x = > ! x . Name . IsNullOrWhiteSpace ( ) )
. Select ( x = > new CultureInfo ( x . Name ) ) // important!
. OrderBy ( x = > x . DisplayName )
. ToDictionary ( x = > x . Name , x = > x . DisplayName ) ;
2018-03-26 10:55:49 +02:00
}
/// <summary>
/// Returns all currently configured languages.
/// </summary>
/// <returns></returns>
[HttpGet]
2018-03-30 01:20:40 +11: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]
2020-05-26 18:50:32 +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 )
2020-05-26 18:50:32 +02:00
return NotFound ( ) ;
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-05-26 18:50:32 +02:00
[TypeFilter(typeof(UmbracoTreeAuthorizeAttribute), Arguments = new object[] { new string [ ] { Constants . Trees . Languages } } ) ]
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." ;
2020-05-26 18:50:32 +02:00
throw HttpResponseException . CreateNotificationValidationErrorResponse ( 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
2018-03-27 16:33:28 +01:00
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-05-26 18:50:32 +02:00
[TypeFilter(typeof(UmbracoTreeAuthorizeAttribute), Arguments = new object[] { new string [ ] { Constants . Trees . Languages } } ) ]
2018-03-26 10:55:49 +02:00
[HttpPost]
2018-03-30 01:20:40 +11:00
public Language SaveLanguage ( Language language )
2018-03-26 10:55:49 +02:00
{
2018-03-30 01:20:40 +11:00
if ( ! ModelState . IsValid )
2020-05-26 18:50:32 +02:00
throw HttpResponseException . CreateValidationErrorResponse ( ModelState ) ;
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
{
2018-09-12 11:47:04 +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" ) ;
2020-05-26 18:50:32 +02:00
throw HttpResponseException . CreateValidationErrorResponse ( 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
{
2019-10-14 19:04:51 +11:00
//Creating a new lang...
2018-03-30 01:20:40 +11:00
CultureInfo culture ;
try
{
culture = CultureInfo . GetCultureInfo ( language . IsoCode ) ;
}
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 ) ;
2020-05-26 18:50:32 +02:00
throw HttpResponseException . CreateValidationErrorResponse ( 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)
2020-05-26 18:50:32 +02:00
var newLang = new Core . Models . Language ( _globalSettings , culture . Name )
2018-03-30 01:20:40 +11:00
{
CultureName = culture . DisplayName ,
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
}
2019-10-14 19:04:51 +11:00
existingById . IsMandatory = language . IsMandatory ;
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." ) ;
2020-05-26 18:50:32 +02:00
throw HttpResponseException . CreateValidationErrorResponse ( ModelState ) ;
2018-09-12 11:47:04 +02:00
}
2019-10-14 19:04:51 +11:00
existingById . IsDefault = language . IsDefault ;
existingById . FallbackLanguageId = language . FallbackLanguageId ;
existingById . IsoCode = language . IsoCode ;
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." ) ;
2020-05-26 18:50:32 +02:00
throw HttpResponseException . CreateValidationErrorResponse ( ModelState ) ;
2018-09-13 14:59:45 +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." ) ;
2020-05-26 18:50:32 +02:00
throw HttpResponseException . CreateValidationErrorResponse ( 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
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
{
2018-09-13 14:59:45 +02:00
if ( ! id . HasValue ) return false ; // no fallback means no cycle
if ( id . Value = = language . Id ) return true ; // back to language = cycle!
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
}