2020-05-11 17:05:57 +02:00
using System ;
2020-05-25 19:37:16 +10:00
using System.Collections.Generic ;
2020-05-11 17:05:57 +02:00
using System.Globalization ;
2020-07-08 11:18:23 +02:00
using System.IO ;
2020-05-11 17:05:57 +02:00
using System.Linq ;
using System.Threading ;
using System.Threading.Tasks ;
2020-03-30 21:27:35 +02:00
using Microsoft.AspNetCore.Mvc ;
2020-08-21 14:52:47 +01:00
using Microsoft.Extensions.Options ;
2020-05-11 17:05:57 +02:00
using Umbraco.Core ;
2020-05-20 15:25:42 +10:00
using Umbraco.Core.BackOffice ;
2020-05-20 16:43:06 +10:00
using Umbraco.Core.Cache ;
2020-04-02 17:41:00 +11:00
using Umbraco.Core.Configuration ;
2020-05-11 17:05:57 +02:00
using Umbraco.Core.Configuration.Grid ;
2020-08-21 14:52:47 +01:00
using Umbraco.Core.Configuration.Models ;
2020-04-03 11:03:06 +11:00
using Umbraco.Core.Hosting ;
2020-08-04 14:30:42 +10:00
using Umbraco.Core.Logging ;
2020-09-22 10:01:00 +02:00
using Umbraco.Core.Security ;
2020-05-11 17:05:57 +02:00
using Umbraco.Core.Services ;
2020-04-02 17:41:00 +11:00
using Umbraco.Core.WebAssets ;
2020-05-20 15:25:42 +10:00
using Umbraco.Extensions ;
2020-03-30 21:27:35 +02:00
using Umbraco.Web.BackOffice.Filters ;
2020-03-31 10:57:56 +02:00
using Umbraco.Web.Common.ActionResults ;
2020-05-27 18:27:49 +10:00
using Umbraco.Web.Common.Attributes ;
2020-08-04 14:30:42 +10:00
using Umbraco.Web.Common.Filters ;
2020-06-02 13:28:30 +10:00
using Umbraco.Web.Common.Security ;
2020-05-20 15:25:42 +10:00
using Umbraco.Web.Models ;
2020-08-04 14:30:42 +10:00
using Umbraco.Web.Security ;
2020-04-02 21:19:42 +11:00
using Umbraco.Web.WebAssets ;
2020-05-14 20:59:29 +10:00
using Constants = Umbraco . Core . Constants ;
2020-03-30 21:27:35 +02:00
namespace Umbraco.Web.BackOffice.Controllers
{
2020-09-10 13:51:59 +02:00
//[UmbracoRequireHttps] //TODO Reintroduce
[DisableBrowserCache]
2020-05-27 18:27:49 +10:00
[PluginController(Constants.Web.Mvc.BackOfficeArea)]
2020-03-30 21:27:35 +02:00
public class BackOfficeController : Controller
{
2020-05-20 15:25:42 +10:00
private readonly BackOfficeUserManager _userManager ;
2020-03-30 21:27:35 +02:00
private readonly IRuntimeMinifier _runtimeMinifier ;
2020-08-21 14:52:47 +01:00
private readonly GlobalSettings _globalSettings ;
2020-04-03 11:03:06 +11:00
private readonly IHostingEnvironment _hostingEnvironment ;
2020-05-11 17:05:57 +02:00
private readonly IUmbracoContextAccessor _umbracoContextAccessor ;
private readonly ILocalizedTextService _textService ;
private readonly IGridConfig _gridConfig ;
2020-05-20 16:43:06 +10:00
private readonly BackOfficeServerVariables _backOfficeServerVariables ;
private readonly AppCaches _appCaches ;
2020-05-25 23:15:32 +10:00
private readonly BackOfficeSignInManager _signInManager ;
2020-09-22 10:01:00 +02:00
private readonly IBackofficeSecurityAccessor _backofficeSecurityAccessor ;
2020-08-04 14:30:42 +10:00
private readonly ILogger _logger ;
2020-03-30 21:27:35 +02:00
2020-05-20 15:25:42 +10:00
public BackOfficeController (
BackOfficeUserManager userManager ,
IRuntimeMinifier runtimeMinifier ,
2020-08-23 23:36:48 +02:00
IOptions < GlobalSettings > globalSettings ,
2020-05-20 15:25:42 +10:00
IHostingEnvironment hostingEnvironment ,
IUmbracoContextAccessor umbracoContextAccessor ,
ILocalizedTextService textService ,
2020-05-20 16:43:06 +10:00
IGridConfig gridConfig ,
BackOfficeServerVariables backOfficeServerVariables ,
2020-05-21 15:43:33 +10:00
AppCaches appCaches ,
2020-08-04 14:30:42 +10:00
BackOfficeSignInManager signInManager ,
2020-09-22 10:01:00 +02:00
IBackofficeSecurityAccessor backofficeSecurityAccessor ,
2020-08-04 14:30:42 +10:00
ILogger logger )
2020-03-30 21:27:35 +02:00
{
2020-05-20 15:25:42 +10:00
_userManager = userManager ;
2020-03-30 21:27:35 +02:00
_runtimeMinifier = runtimeMinifier ;
2020-08-21 14:52:47 +01:00
_globalSettings = globalSettings . Value ;
2020-04-03 11:03:06 +11:00
_hostingEnvironment = hostingEnvironment ;
2020-05-11 17:05:57 +02:00
_umbracoContextAccessor = umbracoContextAccessor ;
_textService = textService ;
_gridConfig = gridConfig ? ? throw new ArgumentNullException ( nameof ( gridConfig ) ) ;
2020-05-20 16:43:06 +10:00
_backOfficeServerVariables = backOfficeServerVariables ;
_appCaches = appCaches ;
2020-05-21 15:43:33 +10:00
_signInManager = signInManager ;
2020-09-22 10:01:00 +02:00
_backofficeSecurityAccessor = backofficeSecurityAccessor ;
2020-08-04 14:30:42 +10:00
_logger = logger ;
2020-03-30 21:27:35 +02:00
}
2020-05-13 14:49:00 +10:00
[HttpGet]
2020-05-21 15:43:33 +10:00
public async Task < IActionResult > Default ( )
2020-03-30 21:27:35 +02:00
{
2020-09-01 18:10:12 +02:00
var viewPath = Path . Combine ( _globalSettings . UmbracoPath , Constants . Web . Mvc . BackOfficeArea , nameof ( Default ) + ".cshtml" )
2020-08-07 00:48:32 +10:00
. Replace ( "\\" , "/" ) ; // convert to forward slashes since it's a virtual path
2020-09-10 13:51:59 +02:00
2020-05-21 15:43:33 +10:00
return await RenderDefaultOrProcessExternalLoginAsync (
2020-07-08 11:18:23 +02:00
( ) = > View ( viewPath ) ,
( ) = > View ( viewPath ) ) ;
2020-03-30 21:27:35 +02:00
}
2020-08-04 14:30:42 +10:00
[HttpGet]
public async Task < IActionResult > VerifyInvite ( string invite )
{
//if you are hitting VerifyInvite, you're already signed in as a different user, and the token is invalid
2020-08-04 20:29:48 +02:00
//you'll exit on one of the return RedirectToAction(nameof(Default)) but you're still logged in so you just get
2020-08-04 14:30:42 +10:00
//dumped at the default admin view with no detail
2020-09-22 10:01:00 +02:00
if ( _backofficeSecurityAccessor . BackofficeSecurity . IsAuthenticated ( ) )
2020-08-04 14:30:42 +10:00
{
await _signInManager . SignOutAsync ( ) ;
}
if ( invite = = null )
{
_logger . Warn < BackOfficeController > ( "VerifyUser endpoint reached with invalid token: NULL" ) ;
return RedirectToAction ( nameof ( Default ) ) ;
}
var parts = System . Net . WebUtility . UrlDecode ( invite ) . Split ( '|' ) ;
if ( parts . Length ! = 2 )
{
_logger . Warn < BackOfficeController > ( "VerifyUser endpoint reached with invalid token: {Invite}" , invite ) ;
return RedirectToAction ( nameof ( Default ) ) ;
}
var token = parts [ 1 ] ;
var decoded = token . FromUrlBase64 ( ) ;
if ( decoded . IsNullOrWhiteSpace ( ) )
{
_logger . Warn < BackOfficeController > ( "VerifyUser endpoint reached with invalid token: {Invite}" , invite ) ;
return RedirectToAction ( nameof ( Default ) ) ;
}
var id = parts [ 0 ] ;
var identityUser = await _userManager . FindByIdAsync ( id ) ;
if ( identityUser = = null )
{
_logger . Warn < BackOfficeController > ( "VerifyUser endpoint reached with non existing user: {UserId}" , id ) ;
return RedirectToAction ( nameof ( Default ) ) ;
}
var result = await _userManager . ConfirmEmailAsync ( identityUser , decoded ) ;
if ( result . Succeeded = = false )
{
_logger . Warn < BackOfficeController > ( "Could not verify email, Error: {Errors}, Token: {Invite}" , result . Errors . ToErrorMessage ( ) , invite ) ;
return new RedirectResult ( Url . Action ( nameof ( Default ) ) + "#/login/false?invite=3" ) ;
}
//sign the user in
DateTime ? previousLastLoginDate = identityUser . LastLoginDateUtc ;
await _signInManager . SignInAsync ( identityUser , false ) ;
2020-08-04 20:29:48 +02:00
//reset the lastlogindate back to previous as the user hasn't actually logged in, to add a flag or similar to BackOfficeSignInManager would be a breaking change
2020-08-04 14:30:42 +10:00
identityUser . LastLoginDateUtc = previousLastLoginDate ;
await _userManager . UpdateAsync ( identityUser ) ;
return new RedirectResult ( Url . Action ( nameof ( Default ) ) + "#/login/false?invite=1" ) ;
}
/// <summary>
/// This Action is used by the installer when an upgrade is detected but the admin user is not logged in. We need to
/// ensure the user is authenticated before the install takes place so we redirect here to show the standard login screen.
/// </summary>
/// <returns></returns>
[HttpGet]
[StatusCodeResult(System.Net.HttpStatusCode.ServiceUnavailable)]
public async Task < IActionResult > AuthorizeUpgrade ( )
{
2020-09-01 18:10:12 +02:00
var viewPath = Path . Combine ( _globalSettings . UmbracoPath , Umbraco . Core . Constants . Web . Mvc . BackOfficeArea , nameof ( AuthorizeUpgrade ) + ".cshtml" ) ;
2020-08-04 14:30:42 +10:00
return await RenderDefaultOrProcessExternalLoginAsync (
//The default view to render when there is no external login info or errors
( ) = > View ( viewPath ) ,
2020-08-04 20:29:48 +02:00
//The IActionResult to perform if external login is successful
2020-08-04 14:30:42 +10:00
( ) = > Redirect ( "/" ) ) ;
}
2020-03-30 21:27:35 +02:00
/// <summary>
/// Returns the JavaScript main file including all references found in manifests
/// </summary>
/// <returns></returns>
[MinifyJavaScriptResult(Order = 0)]
2020-05-13 14:49:00 +10:00
[HttpGet]
2020-03-30 21:27:35 +02:00
public async Task < IActionResult > Application ( )
{
2020-04-03 11:03:06 +11:00
var result = await _runtimeMinifier . GetScriptForLoadingBackOfficeAsync ( _globalSettings , _hostingEnvironment ) ;
2020-03-30 21:27:35 +02:00
return new JavaScriptResult ( result ) ;
}
2020-05-11 17:05:57 +02:00
/// <summary>
/// Get the json localized text for a given culture or the culture for the current user
/// </summary>
/// <param name="culture"></param>
/// <returns></returns>
[HttpGet]
2020-05-25 19:37:16 +10:00
public Dictionary < string , Dictionary < string , string > > LocalizedText ( string culture = null )
2020-05-11 17:05:57 +02:00
{
2020-09-22 10:01:00 +02:00
var isAuthenticated = _backofficeSecurityAccessor . BackofficeSecurity . IsAuthenticated ( ) ;
2020-05-11 17:05:57 +02:00
var cultureInfo = string . IsNullOrWhiteSpace ( culture )
//if the user is logged in, get their culture, otherwise default to 'en'
? isAuthenticated
//current culture is set at the very beginning of each request
? Thread . CurrentThread . CurrentCulture
: CultureInfo . GetCultureInfo ( _globalSettings . DefaultUILanguage )
: CultureInfo . GetCultureInfo ( culture ) ;
var allValues = _textService . GetAllStoredValues ( cultureInfo ) ;
var pathedValues = allValues . Select ( kv = >
{
var slashIndex = kv . Key . IndexOf ( '/' ) ;
var areaAlias = kv . Key . Substring ( 0 , slashIndex ) ;
var valueAlias = kv . Key . Substring ( slashIndex + 1 ) ;
return new
{
areaAlias ,
valueAlias ,
value = kv . Value
} ;
} ) ;
var nestedDictionary = pathedValues
. GroupBy ( pv = > pv . areaAlias )
. ToDictionary ( pv = > pv . Key , pv = >
pv . ToDictionary ( pve = > pve . valueAlias , pve = > pve . value ) ) ;
2020-05-25 19:37:16 +10:00
return nestedDictionary ;
2020-05-11 17:05:57 +02:00
}
2020-05-18 15:19:52 +02:00
[UmbracoAuthorize(Order = 0)]
2020-05-11 17:05:57 +02:00
[HttpGet]
2020-05-25 19:37:16 +10:00
public IEnumerable < IGridEditorConfig > GetGridConfig ( )
2020-05-11 17:05:57 +02:00
{
2020-05-25 19:37:16 +10:00
return _gridConfig . EditorsConfig . Editors ;
2020-05-11 17:05:57 +02:00
}
2020-05-20 15:25:42 +10:00
2020-05-20 16:43:06 +10:00
/// <summary>
/// Returns the JavaScript object representing the static server variables javascript object
/// </summary>
/// <returns></returns>
[UmbracoAuthorize(Order = 0)]
[MinifyJavaScriptResult(Order = 1)]
public async Task < JavaScriptResult > ServerVariables ( )
{
//cache the result if debugging is disabled
var serverVars = ServerVariablesParser . Parse ( await _backOfficeServerVariables . GetServerVariablesAsync ( ) ) ;
var result = _hostingEnvironment . IsDebugMode
? serverVars
: _appCaches . RuntimeCache . GetCacheItem < string > (
typeof ( BackOfficeController ) + "ServerVariables" ,
( ) = > serverVars ,
new TimeSpan ( 0 , 10 , 0 ) ) ;
return new JavaScriptResult ( result ) ;
}
2020-05-20 15:25:42 +10:00
[HttpGet]
2020-05-25 19:37:16 +10:00
public async Task < IActionResult > ValidatePasswordResetCode ( [ Bind ( Prefix = "u" ) ] int userId , [ Bind ( Prefix = "r" ) ] string resetCode )
2020-05-20 15:25:42 +10:00
{
var user = await _userManager . FindByIdAsync ( userId . ToString ( ) ) ;
if ( user ! = null )
{
var result = await _userManager . VerifyUserTokenAsync ( user , "ResetPassword" , "ResetPassword" , resetCode ) ;
if ( result )
{
//Add a flag and redirect for it to be displayed
TempData [ ViewDataExtensions . TokenPasswordResetCode ] = new ValidatePasswordResetCodeModel { UserId = userId , ResetCode = resetCode } ;
return RedirectToLocal ( Url . Action ( "Default" , "BackOffice" ) ) ;
}
}
//Add error and redirect for it to be displayed
TempData [ ViewDataExtensions . TokenPasswordResetCode ] = new [ ] { _textService . Localize ( "login/resetCodeExpired" ) } ;
return RedirectToLocal ( Url . Action ( "Default" , "BackOffice" ) ) ;
}
2020-05-21 15:43:33 +10:00
/// <summary>
/// Used by Default and AuthorizeUpgrade to render as per normal if there's no external login info,
/// otherwise process the external login info.
/// </summary>
/// <returns></returns>
2020-06-02 17:48:08 +10:00
private Task < IActionResult > RenderDefaultOrProcessExternalLoginAsync (
2020-05-25 19:37:16 +10:00
Func < IActionResult > defaultResponse ,
Func < IActionResult > externalSignInResponse )
2020-05-21 15:43:33 +10:00
{
if ( defaultResponse is null ) throw new ArgumentNullException ( nameof ( defaultResponse ) ) ;
if ( externalSignInResponse is null ) throw new ArgumentNullException ( nameof ( externalSignInResponse ) ) ;
ViewData . SetUmbracoPath ( _globalSettings . GetUmbracoMvcArea ( _hostingEnvironment ) ) ;
//check if there is the TempData with the any token name specified, if so, assign to view bag and render the view
if ( ViewData . FromTempData ( TempData , ViewDataExtensions . TokenExternalSignInError ) | |
ViewData . FromTempData ( TempData , ViewDataExtensions . TokenPasswordResetCode ) )
2020-06-02 17:48:08 +10:00
return Task . FromResult ( defaultResponse ( ) ) ;
2020-05-21 15:43:33 +10:00
2020-06-02 17:48:08 +10:00
return Task . FromResult ( defaultResponse ( ) ) ;
2020-05-21 15:43:33 +10:00
//First check if there's external login info, if there's not proceed as normal
// TODO: Review this, not sure if this will work as expected until we integrate OAuth
// TODO: Do we pass in XsrfKey ? need to investigate how this all works now
//var loginInfo = await _signInManager.GetExternalLoginInfoAsync();
//if (loginInfo == null || loginInfo.ExternalIdentity.IsAuthenticated == false)
//{
// return defaultResponse();
//}
////we're just logging in with an external source, not linking accounts
//return await ExternalSignInAsync(loginInfo, externalSignInResponse);
}
// Used for XSRF protection when adding external logins
private const string XsrfKey = "XsrfId" ;
2020-05-25 19:37:16 +10:00
private IActionResult RedirectToLocal ( string returnUrl )
2020-05-20 15:25:42 +10:00
{
if ( Url . IsLocalUrl ( returnUrl ) )
{
return Redirect ( returnUrl ) ;
}
return Redirect ( "/" ) ;
}
2020-05-21 15:46:43 +10:00
2020-05-19 09:52:58 +02:00
2020-03-30 21:27:35 +02:00
}
}