Merge branch 'dev-v7.7' into temp-U4-10059

# Conflicts:
#	src/Umbraco.Web.UI.Client/src/views/users/group.html
#	src/Umbraco.Web.UI.Client/src/views/users/user.html
#	src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml
This commit is contained in:
Shannon
2017-08-01 11:31:52 +10:00
76 changed files with 2381 additions and 1516 deletions
+3 -3
View File
@@ -21,9 +21,9 @@
<dependency id="Microsoft.AspNet.WebApi" version="[5.2.3,6.0.0)" />
<dependency id="Microsoft.AspNet.Identity.Owin" version="[2.2.1, 3.0.0)" />
<dependency id="Microsoft.AspNet.SignalR.Core" version="[2.2.1, 3.0.0)" />
<dependency id="Microsoft.Owin.Security.Cookies" version="[3.0.1, 4.0.0)" />
<dependency id="Microsoft.Owin.Security.OAuth" version="[3.0.1, 4.0.0)" />
<dependency id="Microsoft.Owin.Host.SystemWeb" version="[3.0.1, 4.0.0)" />
<dependency id="Microsoft.Owin.Security.Cookies" version="[3.1.0, 4.0.0)" />
<dependency id="Microsoft.Owin.Security.OAuth" version="[3.1.0, 4.0.0)" />
<dependency id="Microsoft.Owin.Host.SystemWeb" version="[3.1.0, 4.0.0)" />
<dependency id="MiniProfiler" version="[2.1.0, 3.0.0)" />
<dependency id="HtmlAgilityPack" version="[1.4.9.5, 2.0.0)" />
<dependency id="Lucene.Net" version="[2.9.4.1, 3.0.0.0)" />
+8
View File
@@ -40,6 +40,14 @@ namespace Umbraco.Core.Models.Rdbms
[Column("userPassword")]
[Length(500)]
public string Password { get; set; }
/// <summary>
/// This will represent a JSON structure of how the password has been created (i.e hash algorithm, iterations)
/// </summary>
[Column("passwordConfig")]
[NullSetting(NullSetting = NullSettings.Null)]
[Length(500)]
public string PasswordConfig { get; set; }
[Column("userEmail")]
public string Email { get; set; }
+8 -2
View File
@@ -23,7 +23,13 @@ namespace Umbraco.Core.Models
/// A list of 5 different sized avatar URLs
/// </returns>
internal static string[] GetCurrentUserAvatarUrls(this IUser user, IUserService userService, ICacheProvider staticCache)
{
{
//check if the user has explicitly removed all avatars including a gravatar, this will be possible and the value will be "none"
if (user.Avatar == "none")
{
return new string[0];
}
if (user.Avatar.IsNullOrWhiteSpace())
{
var gravatarHash = user.Email.ToMd5();
@@ -60,7 +66,7 @@ namespace Umbraco.Core.Models
};
}
return null;
return new string[0];
}
//use the custom avatar
@@ -36,7 +36,7 @@ namespace Umbraco.Core.Persistence.Factories
entity.DisableChangeTracking();
entity.CreateDate = d.createDate;
entity.CreatorId = d.nodeUser;
entity.CreatorId = d.nodeUser == null ? 0 : d.nodeUser;
entity.Id = d.id;
entity.Key = d.uniqueID;
entity.Level = d.level;
@@ -1,7 +1,10 @@
using System.Linq;
using System.Web.Security;
using Newtonsoft.Json;
using Umbraco.Core.Logging;
using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using Umbraco.Core.Persistence.SqlSyntax;
using Umbraco.Core.Security;
namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSevenZero
{
@@ -28,6 +31,19 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSevenZe
if (columns.Any(x => x.TableName.InvariantEquals("umbracoUser") && x.ColumnName.InvariantEquals("invitedDate")) == false)
Create.Column("invitedDate").OnTable("umbracoUser").AsDateTime().Nullable();
if (columns.Any(x => x.TableName.InvariantEquals("umbracoUser") && x.ColumnName.InvariantEquals("passwordConfig")) == false)
{
Create.Column("passwordConfig").OnTable("umbracoUser").AsString(500).Nullable();
//Check if we have a known config, we only want to store config for hashing
var membershipProvider = MembershipProviderExtensions.GetUsersMembershipProvider();
if (membershipProvider.PasswordFormat == MembershipPasswordFormat.Hashed)
{
var json = JsonConvert.SerializeObject(new { hashAlgorithm = Membership.HashAlgorithmType });
Execute.Sql("UPDATE umbracoUser SET passwordConfig = '" + json + "'");
}
}
}
public override void Down()
@@ -4,6 +4,8 @@ using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Web.Security;
using Newtonsoft.Json;
using Umbraco.Core.Logging;
using Umbraco.Core.Models.EntityBase;
using Umbraco.Core.Models.Membership;
@@ -15,6 +17,7 @@ using Umbraco.Core.Persistence.Querying;
using Umbraco.Core.Persistence.Relators;
using Umbraco.Core.Persistence.SqlSyntax;
using Umbraco.Core.Persistence.UnitOfWork;
using Umbraco.Core.Security;
namespace Umbraco.Core.Persistence.Repositories
{
@@ -23,12 +26,23 @@ namespace Umbraco.Core.Persistence.Repositories
/// </summary>
internal class UserRepository : PetaPocoRepositoryBase<int, IUser>, IUserRepository
{
//private readonly CacheHelper _cacheHelper;
public UserRepository(IScopeUnitOfWork work, CacheHelper cacheHelper, ILogger logger, ISqlSyntaxProvider sqlSyntax)
private readonly IDictionary<string, string> _passwordConfiguration;
/// <summary>
/// Constructor
/// </summary>
/// <param name="work"></param>
/// <param name="cacheHelper"></param>
/// <param name="logger"></param>
/// <param name="sqlSyntax"></param>
/// <param name="passwordConfiguration">
/// A dictionary specifying the configuration for user passwords. If this is null then no password configuration will be persisted or read.
/// </param>
public UserRepository(IScopeUnitOfWork work, CacheHelper cacheHelper, ILogger logger, ISqlSyntaxProvider sqlSyntax,
IDictionary<string, string> passwordConfiguration = null)
: base(work, cacheHelper, logger, sqlSyntax)
{
//_cacheHelper = cacheHelper;
_passwordConfiguration = passwordConfiguration;
}
#region Overrides of RepositoryBase<int,IUser>
@@ -43,8 +57,8 @@ namespace Umbraco.Core.Persistence.Repositories
.OrderBy<UserStartNodeDto>(d => d.Id, SqlSyntax);
var dto = Database.Fetch<UserDto, UserGroupDto, UserGroup2AppDto, UserStartNodeDto, UserDto>(new UserGroupRelator().Map, sql)
.FirstOrDefault();
.FirstOrDefault();
if (dto == null)
return null;
@@ -124,7 +138,7 @@ namespace Umbraco.Core.Persistence.Repositories
{
var sql = GetBaseQuery("umbracoUser.*");
sql.Where(GetBaseWhereClause(), new { Id = id });
dto = Database.FirstOrDefault<UserDto>(sql);
dto = Database.FirstOrDefault<UserDto>(sql);
}
if (dto == null)
@@ -136,14 +150,14 @@ namespace Umbraco.Core.Persistence.Repositories
public IProfile GetProfile(string username)
{
var sql = GetBaseQuery(false).Where<UserDto>(userDto => userDto.UserName == username, SqlSyntax);
var sql = GetBaseQuery(false).Where<UserDto>(userDto => userDto.UserName == username, SqlSyntax);
var dto = Database.Fetch<UserDto>(sql)
.FirstOrDefault();
if (dto == null)
return null;
return null;
return new UserProfile(dto.Id, dto.UserName);
}
@@ -173,8 +187,8 @@ UNION
SELECT '5CountOfInvited' AS colName, COUNT(id) AS num FROM umbracoUser WHERE lastLoginDate IS NULL AND userDisabled = 1 AND invitedDate IS NOT NULL
ORDER BY colName";
var result = Database.Fetch<dynamic>(sql);
var result = Database.Fetch<dynamic>(sql);
return new Dictionary<UserState, int>
{
{UserState.All, result[0].num},
@@ -190,7 +204,7 @@ ORDER BY colName";
var sql = GetQueryWithGroups();
if (ids.Any())
{
sql.Where("umbracoUser.id in (@ids)", new {ids = ids});
sql.Where("umbracoUser.id in (@ids)", new { ids = ids });
}
sql //must be included for relator to work
.OrderBy<UserDto>(d => d.Id, SqlSyntax)
@@ -198,11 +212,11 @@ ORDER BY colName";
.OrderBy<UserStartNodeDto>(d => d.Id, SqlSyntax);
var users = ConvertFromDtos(Database.Fetch<UserDto, UserGroupDto, UserGroup2AppDto, UserStartNodeDto, UserDto>(new UserGroupRelator().Map, sql))
.ToArray(); // important so we don't iterate twice, if we don't do this we can end up with null values in cache if we were caching.
.ToArray(); // important so we don't iterate twice, if we don't do this we can end up with null values in cache if we were caching.
return users;
}
}
protected override IEnumerable<IUser> PerformGetByQuery(IQuery<IUser> query)
{
var sqlClause = GetQueryWithGroups();
@@ -217,15 +231,15 @@ ORDER BY colName";
.DistinctBy(x => x.Id);
var users = ConvertFromDtos(dtos)
.ToArray(); // important so we don't iterate twice, if we don't do this we can end up with null values in cache if we were caching.
.ToArray(); // important so we don't iterate twice, if we don't do this we can end up with null values in cache if we were caching.
return users;
}
}
#endregion
#region Overrides of PetaPocoRepositoryBase<int,IUser>
protected override Sql GetBaseQuery(bool isCount)
{
var sql = new Sql();
@@ -295,19 +309,27 @@ ORDER BY colName";
protected override Guid NodeObjectTypeId
{
get { throw new NotImplementedException(); }
}
}
protected override void PersistNewItem(IUser entity)
{
((User)entity).AddingEntity();
//ensure security stamp if non
//ensure security stamp if non
if (entity.SecurityStamp.IsNullOrWhiteSpace())
{
entity.SecurityStamp = Guid.NewGuid().ToString();
}
var userDto = UserFactory.BuildDto(entity);
}
var userDto = UserFactory.BuildDto(entity);
//Check if we have a known config, we only want to store config for hashing
//TODO: This logic will need to be updated when we do http://issues.umbraco.org/issue/U4-10089
if (_passwordConfiguration != null && _passwordConfiguration.Count > 0)
{
var json = JsonConvert.SerializeObject(_passwordConfiguration);
userDto.PasswordConfig = json;
}
var id = Convert.ToInt32(Database.Insert(userDto));
entity.Id = id;
@@ -329,8 +351,8 @@ ORDER BY colName";
//lookup all assigned
var assigned = entity.Groups == null || entity.Groups.Any() == false
? new List<UserGroupDto>()
: Database.Fetch<UserGroupDto>("SELECT * FROM umbracoUserGroup WHERE userGroupAlias IN (@aliases)", new { aliases = entity.Groups.Select(x => x.Alias) });
: Database.Fetch<UserGroupDto>("SELECT * FROM umbracoUserGroup WHERE userGroupAlias IN (@aliases)", new { aliases = entity.Groups.Select(x => x.Alias) });
foreach (var groupDto in assigned)
{
var dto = new User2UserGroupDto
@@ -348,18 +370,18 @@ ORDER BY colName";
protected override void PersistUpdatedItem(IUser entity)
{
//Updates Modified date
((User)entity).UpdatingEntity();
//ensure security stamp if non
((User)entity).UpdatingEntity();
//ensure security stamp if non
if (entity.SecurityStamp.IsNullOrWhiteSpace())
{
entity.SecurityStamp = Guid.NewGuid().ToString();
}
var userDto = UserFactory.BuildDto(entity);
//build list of columns to check for saving - we don't want to save the password if it hasn't changed!
//List the columns to save, NOTE: would be nice to not have hard coded strings here but no real good way around that
var userDto = UserFactory.BuildDto(entity);
//build list of columns to check for saving - we don't want to save the password if it hasn't changed!
//List the columns to save, NOTE: would be nice to not have hard coded strings here but no real good way around that
var colsToSave = new Dictionary<string, string>()
{
{"userDisabled", "IsApproved"},
@@ -367,8 +389,8 @@ ORDER BY colName";
{"startStructureID", "StartContentId"},
{"startMediaID", "StartMediaId"},
{"userName", "Name"},
{"userLogin", "Username"},
{"userEmail", "Email"},
{"userLogin", "Username"},
{"userEmail", "Email"},
{"userLanguage", "Language"},
{"securityStampToken", "SecurityStamp"},
{"lastLockoutDate", "LastLockoutDate"},
@@ -401,6 +423,16 @@ ORDER BY colName";
userDto.SecurityStampToken = entity.SecurityStamp = Guid.NewGuid().ToString();
changedCols.Add("securityStampToken");
}
//Check if we have a known config, we only want to store config for hashing
//TODO: This logic will need to be updated when we do http://issues.umbraco.org/issue/U4-10089
if (_passwordConfiguration != null && _passwordConfiguration.Count > 0)
{
var json = JsonConvert.SerializeObject(_passwordConfiguration);
userDto.PasswordConfig = json;
changedCols.Add("passwordConfig");
}
}
//only update the changed cols
@@ -454,7 +486,7 @@ ORDER BY colName";
//remove the ones not assigned to the entity
var toDelete = assignedIds.Except(entityStartIds).ToArray();
if (toDelete.Length > 0)
Database.Delete<UserStartNodeDto>("WHERE UserId = @UserId AND startNode IN (@startNodes)", new {UserId = entity.Id, startNodes = toDelete});
Database.Delete<UserStartNodeDto>("WHERE UserId = @UserId AND startNode IN (@startNodes)", new { UserId = entity.Id, startNodes = toDelete });
//add the ones not currently in the db
var toAdd = entityStartIds.Except(assignedIds).ToArray();
foreach (var i in toAdd)
@@ -494,8 +526,8 @@ ORDER BY colName";
.Where<UserDto>(x => x.UserName == username);
return Database.ExecuteScalar<int>(sql) > 0;
}
}
/// <summary>
/// Gets a list of <see cref="IUser"/> objects associated with a given group
/// </summary>
@@ -545,8 +577,8 @@ ORDER BY colName";
var mappedField = mapper.Map(expressionMember.Name);
if (mappedField.IsNullOrWhiteSpace())
throw new ArgumentException("Could not find a mapping for the column specified in the orderBy clause");
throw new ArgumentException("Could not find a mapping for the column specified in the orderBy clause");
long tr;
var results = GetPagedResultsByQuery(query, Convert.ToInt64(pageIndex), pageSize, out tr, mappedField, Direction.Ascending);
totalRecords = Convert.ToInt32(tr);
@@ -582,13 +614,13 @@ ORDER BY colName";
throw new ArgumentException("Could not find a mapping for the column specified in the orderBy clause");
return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, mappedField, orderDirection, userGroups, userState, filter);
}
private IEnumerable<IUser> GetPagedResultsByQuery(IQuery<IUser> query, long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection,
string[] userGroups = null,
UserState[] userState = null,
}
private IEnumerable<IUser> GetPagedResultsByQuery(IQuery<IUser> query, long pageIndex, int pageSize, out long totalRecords, string orderBy, Direction orderDirection,
string[] userGroups = null,
UserState[] userState = null,
IQuery<IUser> filter = null)
{
if (string.IsNullOrWhiteSpace(orderBy)) throw new ArgumentException("Value cannot be null or whitespace.", "orderBy");
@@ -612,7 +644,7 @@ ORDER BY colName";
INNER JOIN umbracoUser2UserGroup ON umbracoUser2UserGroup.userId = umbracoUser.id
INNER JOIN umbracoUserGroup ON umbracoUserGroup.id = umbracoUser2UserGroup.userGroupId
WHERE umbracoUserGroup.userGroupAlias IN (@userGroups)))";
filterSql.Append(subQuery, new {userGroups= userGroups});
filterSql.Append(subQuery, new { userGroups = userGroups });
}
if (userState != null && userState.Length > 0)
{
@@ -650,12 +682,12 @@ ORDER BY colName";
}
// Get base query for returning IDs
var sqlBaseIds = GetBaseQuery("id");
var sqlBaseIds = GetBaseQuery("id");
if (query == null) query = new Query<IUser>();
var translatorIds = new SqlTranslator<IUser>(sqlBaseIds, query);
var sqlQueryIds = translatorIds.Translate();
//get sorted and filtered sql
var sqlNodeIdsWithSort = GetSortedSqlForPagedResults(
GetFilteredSqlForPagedResults(sqlQueryIds, filterSql),
@@ -675,11 +707,11 @@ ORDER BY colName";
string sqlStringCount, sqlStringPage;
Database.BuildPageQueries<UserDto>(pageIndex * pageSize, pageSize, sqlNodeIdsWithSort.SQL, ref args, out sqlStringCount, out sqlStringPage);
var sqlQueryFull = GetBaseQuery("umbracoUser.*, umbracoUserGroup.*, umbracoUserGroup2App.*, umbracoUserStartNode.*");
var sqlQueryFull = GetBaseQuery("umbracoUser.*, umbracoUserGroup.*, umbracoUserGroup2App.*, umbracoUserStartNode.*");
var fullQueryWithPagedInnerJoin = sqlQueryFull
.Append("INNER JOIN (")
//join the paged query with the paged query arguments
.Append("INNER JOIN (")
//join the paged query with the paged query arguments
.Append(sqlStringPage, args)
.Append(") temp ")
.Append("ON umbracoUser.id = temp.id");
@@ -1,6 +1,7 @@
using Umbraco.Core.Configuration;
using System;
using System.ComponentModel;
using System.Web.Security;
using Umbraco.Core.Cache;
using Umbraco.Core.Configuration.UmbracoSettings;
using Umbraco.Core.IO;
@@ -9,6 +10,7 @@ using Umbraco.Core.Logging;
using Umbraco.Core.Persistence.Repositories;
using Umbraco.Core.Persistence.SqlSyntax;
using Umbraco.Core.Persistence.UnitOfWork;
using Umbraco.Core.Security;
namespace Umbraco.Core.Persistence
{
@@ -314,11 +316,18 @@ namespace Umbraco.Core.Persistence
public virtual IUserRepository CreateUserRepository(IScopeUnitOfWork uow)
{
var userMembershipProvider = MembershipProviderExtensions.GetUsersMembershipProvider();
var passwordConfig = userMembershipProvider == null || userMembershipProvider.PasswordFormat != MembershipPasswordFormat.Hashed
? null
: new System.Collections.Generic.Dictionary<string, string> {{"hashAlgorithm", Membership.HashAlgorithmType}};
return new UserRepository(
uow,
//Need to cache users - we look up user information more than anything in the back office!
_cacheHelper,
_logger, _sqlSyntax);
_logger,
_sqlSyntax,
passwordConfig);
}
internal virtual IMacroRepository CreateMacroRepository(IScopeUnitOfWork uow)
+26 -4
View File
@@ -277,7 +277,7 @@ namespace Umbraco.Core
uniqInfos.Add(info.FullName);
hashCombiner.AddFileSystemItem(info);
}
}
}
return ConvertHashToInt64(hashCombiner.GetCombinedHashCode());
}
@@ -336,6 +336,9 @@ namespace Umbraco.Core
#region Cache
private const int ListFileOpenReadTimeout = 4000; // milliseconds
private const int ListFileOpenWriteTimeout = 2000; // milliseconds
/// <summary>
/// Attemps to retrieve the list of types from the cache.
/// </summary>
@@ -381,7 +384,7 @@ namespace Umbraco.Core
if (File.Exists(filePath) == false)
return cache;
using (var stream = File.OpenRead(filePath))
using (var stream = GetFileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, ListFileOpenReadTimeout))
using (var reader = new StreamReader(stream))
{
while (true)
@@ -451,7 +454,7 @@ namespace Umbraco.Core
{
var filePath = GetPluginListFilePath();
using (var stream = File.Open(filePath, FileMode.Create, FileAccess.ReadWrite))
using (var stream = GetFileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, ListFileOpenWriteTimeout))
using (var writer = new StreamWriter(stream))
{
foreach (var typeList in _types.Values)
@@ -471,7 +474,26 @@ namespace Umbraco.Core
// at the moment we write the cache to disk every time we update it. ideally we defer the writing
// since all the updates are going to happen in a row when Umbraco starts. that being said, the
// file is small enough, so it is not a priority.
WriteCache();
WriteCache();
}
private static Stream GetFileStream(string path, FileMode fileMode, FileAccess fileAccess, FileShare fileShare, int timeoutMilliseconds)
{
const int pauseMilliseconds = 250;
var attempts = timeoutMilliseconds / pauseMilliseconds;
while (true)
{
try
{
return new FileStream(path, fileMode, fileAccess, fileShare);
}
catch
{
if (--attempts == 0)
throw;
Thread.Sleep(pauseMilliseconds);
}
}
}
#endregion
@@ -141,7 +141,9 @@ namespace Umbraco.Core.Security
/// Initializes the user manager with the correct options
/// </summary>
/// <param name="manager"></param>
/// <param name="membershipProvider"></param>
/// <param name="membershipProvider">
/// The <see cref="MembershipProviderBase"/> for the users called UsersMembershipProvider
/// </param>
/// <param name="dataProtectionProvider"></param>
/// <returns></returns>
protected void InitUserManager(
@@ -157,11 +159,10 @@ namespace Umbraco.Core.Security
};
// Configure validation logic for passwords
var provider = MembershipProviderExtensions.GetUsersMembershipProvider();
manager.PasswordValidator = new MembershipProviderPasswordValidator(provider);
manager.PasswordValidator = new MembershipProviderPasswordValidator(membershipProvider);
//use a custom hasher based on our membership provider
manager.PasswordHasher = new MembershipPasswordHasher(membershipProvider);
manager.PasswordHasher = GetDefaultPasswordHasher(membershipProvider);
if (dataProtectionProvider != null)
{
@@ -196,6 +197,76 @@ namespace Umbraco.Core.Security
//manager.SmsService = new SmsService();
}
/// <summary>
/// This will determine which password hasher to use based on what is defined in config
/// </summary>
/// <returns></returns>
protected virtual IPasswordHasher GetDefaultPasswordHasher(MembershipProviderBase provider)
{
//if the current user membership provider is unkown (this would be rare), then return the default password hasher
if (provider.IsUmbracoUsersProvider() == false)
return new PasswordHasher();
//if the configured provider has legacy features enabled, then return the membership provider password hasher
if (provider.AllowManuallyChangingPassword || provider.DefaultUseLegacyEncoding)
return new MembershipProviderPasswordHasher(provider);
//we can use the user aware password hasher (which will be the default and preferred way)
return new UserAwareMembershipProviderPasswordHasher(provider);
}
/// <summary>
/// Gets/sets the default back office user password checker
/// </summary>
public IBackOfficeUserPasswordChecker BackOfficeUserPasswordChecker { get; set; }
/// <summary>
/// Helper method to generate a password for a user based on the current password validator
/// </summary>
/// <returns></returns>
public string GeneratePassword()
{
var passwordValidator = PasswordValidator as PasswordValidator;
if (passwordValidator == null)
{
var membershipPasswordHasher = PasswordHasher as IMembershipProviderPasswordHasher;
//get the real password validator, this should not be null but in some very rare cases it could be, in which case
//we need to create a default password validator to use since we have no idea what it actually is or what it's rules are
//this is an Edge Case!
passwordValidator = PasswordValidator as PasswordValidator
?? (membershipPasswordHasher != null
? new MembershipProviderPasswordValidator(membershipPasswordHasher.MembershipProvider)
: new PasswordValidator());
}
var password = Membership.GeneratePassword(
passwordValidator.RequiredLength,
passwordValidator.RequireNonLetterOrDigit ? 2 : 0);
var random = new Random();
var passwordChars = password.ToCharArray();
if (passwordValidator.RequireDigit && passwordChars.ContainsAny(Enumerable.Range(48, 58).Select(x => (char)x)))
password += Convert.ToChar(random.Next(48, 58)); // 0-9
if (passwordValidator.RequireLowercase && passwordChars.ContainsAny(Enumerable.Range(97, 123).Select(x => (char)x)))
password += Convert.ToChar(random.Next(97, 123)); // a-z
if (passwordValidator.RequireUppercase && passwordChars.ContainsAny(Enumerable.Range(65, 91).Select(x => (char)x)))
password += Convert.ToChar(random.Next(65, 91)); // A-Z
if (passwordValidator.RequireNonLetterOrDigit && passwordChars.ContainsAny(Enumerable.Range(33, 48).Select(x => (char)x)))
password += Convert.ToChar(random.Next(33, 48)); // symbols !"#$%&'()*+,-./
return password;
}
#region Overrides for password logic
/// <summary>
/// Logic used to validate a username and password
@@ -243,44 +314,88 @@ namespace Umbraco.Core.Security
return await base.CheckPasswordAsync(user, password);
}
/// <summary>
/// Gets/sets the default back office user password checker
/// </summary>
public IBackOfficeUserPasswordChecker BackOfficeUserPasswordChecker { get; set; }
public override Task<IdentityResult> ChangePasswordAsync(int userId, string currentPassword, string newPassword)
{
return base.ChangePasswordAsync(userId, currentPassword, newPassword);
}
/// <summary>
/// Helper method to generate a password for a user based on the current password validator
/// Override to determine how to hash the password
/// </summary>
/// <param name="store"></param>
/// <param name="user"></param>
/// <param name="password"></param>
/// <returns></returns>
protected override async Task<bool> VerifyPasswordAsync(IUserPasswordStore<T, int> store, T user, string password)
{
var userAwarePasswordHasher = PasswordHasher as IUserAwarePasswordHasher<BackOfficeIdentityUser, int>;
if (userAwarePasswordHasher == null)
return await base.VerifyPasswordAsync(store, user, password);
var hash = await store.GetPasswordHashAsync(user);
return userAwarePasswordHasher.VerifyHashedPassword(user, hash, password) != PasswordVerificationResult.Failed;
}
/// <summary>
/// Override to determine how to hash the password
/// </summary>
/// <param name="passwordStore"></param>
/// <param name="user"></param>
/// <param name="newPassword"></param>
/// <returns></returns>
/// <remarks>
/// This method is called anytime the password needs to be hashed for storage (i.e. including when reset password is used)
/// </remarks>
protected override async Task<IdentityResult> UpdatePassword(IUserPasswordStore<T, int> passwordStore, T user, string newPassword)
{
var userAwarePasswordHasher = PasswordHasher as IUserAwarePasswordHasher<BackOfficeIdentityUser, int>;
if (userAwarePasswordHasher == null)
return await base.UpdatePassword(passwordStore, user, newPassword);
var result = await PasswordValidator.ValidateAsync(newPassword);
if (result.Succeeded == false)
return result;
await passwordStore.SetPasswordHashAsync(user, userAwarePasswordHasher.HashPassword(user, newPassword));
await UpdateSecurityStampInternal(user);
return IdentityResult.Success;
}
/// <summary>
/// This is copied from the underlying .NET base class since they decied to not expose it
/// </summary>
/// <param name="user"></param>
/// <returns></returns>
private async Task UpdateSecurityStampInternal(BackOfficeIdentityUser user)
{
if (SupportsUserSecurityStamp == false)
return;
await GetSecurityStore().SetSecurityStampAsync(user, NewSecurityStamp());
}
/// <summary>
/// This is copied from the underlying .NET base class since they decied to not expose it
/// </summary>
/// <returns></returns>
public string GeneratePassword()
private IUserSecurityStampStore<BackOfficeIdentityUser, int> GetSecurityStore()
{
var passwordValidator = PasswordValidator as PasswordValidator;
if (passwordValidator != null)
{
var password = Membership.GeneratePassword(
passwordValidator.RequiredLength,
passwordValidator.RequireNonLetterOrDigit ? 2 : 0);
var random = new Random();
var passwordChars = password.ToCharArray();
if (passwordValidator.RequireDigit && passwordChars.ContainsAny(Enumerable.Range(48, 58).Select(x => (char)x)))
password += Convert.ToChar(random.Next(48, 58)); // 0-9
if (passwordValidator.RequireLowercase && passwordChars.ContainsAny(Enumerable.Range(97, 123).Select(x => (char)x)))
password += Convert.ToChar(random.Next(97, 123)); // a-z
if (passwordValidator.RequireUppercase && passwordChars.ContainsAny(Enumerable.Range(65, 91).Select(x => (char)x)))
password += Convert.ToChar(random.Next(65, 91)); // A-Z
if (passwordValidator.RequireNonLetterOrDigit && passwordChars.ContainsAny(Enumerable.Range(33, 48).Select(x => (char)x)))
password += Convert.ToChar(random.Next(33, 48)); // symbols !"#$%&'()*+,-./
return password;
}
throw new NotSupportedException("Cannot generate a password since the type of the password validator (" + PasswordValidator.GetType() + ") is not known");
var store = Store as IUserSecurityStampStore<BackOfficeIdentityUser, int>;
if (store == null)
throw new NotSupportedException("The current user store does not implement " + typeof(IUserSecurityStampStore<>));
return store;
}
/// <summary>
/// This is copied from the underlying .NET base class since they decied to not expose it
/// </summary>
/// <returns></returns>
private static string NewSecurityStamp()
{
return Guid.NewGuid().ToString();
}
#endregion
}
}
@@ -0,0 +1,12 @@
using Microsoft.AspNet.Identity;
namespace Umbraco.Core.Security
{
/// <summary>
/// A password hasher that is based on the rules configured for a membership provider
/// </summary>
public interface IMembershipProviderPasswordHasher : IPasswordHasher
{
MembershipProviderBase MembershipProvider { get; }
}
}
@@ -0,0 +1,18 @@
using System;
using Microsoft.AspNet.Identity;
namespace Umbraco.Core.Security
{
/// <summary>
/// A password hasher that is User aware so that it can process the hashing based on the user's settings
/// </summary>
/// <typeparam name="TKey"></typeparam>
/// <typeparam name="TUser"></typeparam>
public interface IUserAwarePasswordHasher<in TUser, TKey>
where TUser : class, IUser<TKey>
where TKey : IEquatable<TKey>
{
string HashPassword(TUser user, string password);
PasswordVerificationResult VerifyHashedPassword(TUser user, string hashedPassword, string providedPassword);
}
}
@@ -1,34 +0,0 @@
using System;
using Microsoft.AspNet.Identity;
namespace Umbraco.Core.Security
{
/// <summary>
/// A custom password hasher that conforms to the current password hashing done in Umbraco
/// </summary>
internal class MembershipPasswordHasher : IPasswordHasher
{
private readonly MembershipProviderBase _provider;
public MembershipPasswordHasher(MembershipProviderBase provider)
{
_provider = provider;
}
public string HashPassword(string password)
{
return _provider.HashPasswordForStorage(password);
}
public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
{
if (string.IsNullOrWhiteSpace(hashedPassword)) throw new ArgumentException("Value cannot be null or whitespace.", "hashedPassword");
return _provider.VerifyPassword(providedPassword, hashedPassword)
? PasswordVerificationResult.Success
: PasswordVerificationResult.Failed;
}
}
}
@@ -33,19 +33,19 @@ namespace Umbraco.Core.Security
}
/// <summary>
/// Providers can override this setting, default is 7
/// Providers can override this setting, default is 10
/// </summary>
public virtual int DefaultMinPasswordLength
{
get { return 7; }
get { return 10; }
}
/// <summary>
/// Providers can override this setting, default is 1
/// Providers can override this setting, default is 0
/// </summary>
public virtual int DefaultMinNonAlphanumericChars
{
get { return 1; }
get { return 0; }
}
/// <summary>
@@ -225,7 +225,7 @@ namespace Umbraco.Core.Security
base.Initialize(name, config);
_enablePasswordRetrieval = config.GetValue("enablePasswordRetrieval", false);
_enablePasswordReset = config.GetValue("enablePasswordReset", false);
_enablePasswordReset = config.GetValue("enablePasswordReset", true);
_requiresQuestionAndAnswer = config.GetValue("requiresQuestionAndAnswer", false);
_requiresUniqueEmail = config.GetValue("requiresUniqueEmail", true);
_maxInvalidPasswordAttempts = GetIntValue(config, "maxInvalidPasswordAttempts", 5, false, 0);
@@ -0,0 +1,34 @@
using Microsoft.AspNet.Identity;
namespace Umbraco.Core.Security
{
/// <summary>
/// A password hasher that conforms to the password hashing done with membership providers
/// </summary>
public class MembershipProviderPasswordHasher : IMembershipProviderPasswordHasher
{
/// <summary>
/// Exposes the underlying MembershipProvider
/// </summary>
public MembershipProviderBase MembershipProvider { get; private set; }
public MembershipProviderPasswordHasher(MembershipProviderBase provider)
{
MembershipProvider = provider;
}
public string HashPassword(string password)
{
return MembershipProvider.HashPasswordForStorage(password);
}
public PasswordVerificationResult VerifyHashedPassword(string hashedPassword, string providedPassword)
{
return MembershipProvider.VerifyPassword(providedPassword, hashedPassword)
? PasswordVerificationResult.Success
: PasswordVerificationResult.Failed;
}
}
}
@@ -0,0 +1,30 @@
using System;
using Microsoft.AspNet.Identity;
using Umbraco.Core.Models.Identity;
namespace Umbraco.Core.Security
{
/// <summary>
/// The default password hasher that is User aware so that it can process the hashing based on the user's settings
/// </summary>
public class UserAwareMembershipProviderPasswordHasher : MembershipProviderPasswordHasher, IUserAwarePasswordHasher<BackOfficeIdentityUser, int>
{
public UserAwareMembershipProviderPasswordHasher(MembershipProviderBase provider) : base(provider)
{
}
public string HashPassword(BackOfficeIdentityUser user, string password)
{
//TODO: Implement the logic for this, we need to lookup the password format for the user and hash accordingly: http://issues.umbraco.org/issue/U4-10089
//NOTE: For now this just falls back to the hashing we are currently using
return base.HashPassword(password);
}
public PasswordVerificationResult VerifyHashedPassword(BackOfficeIdentityUser user, string hashedPassword, string providedPassword)
{
//TODO: Implement the logic for this, we need to lookup the password format for the user and hash accordingly: http://issues.umbraco.org/issue/U4-10089
//NOTE: For now this just falls back to the hashing we are currently using
return base.VerifyHashedPassword(hashedPassword, providedPassword);
}
}
}
+4 -1
View File
@@ -663,10 +663,13 @@
<Compile Include="Security\BackOfficeUserValidator.cs" />
<Compile Include="Security\IBackOfficeUserManagerMarker.cs" />
<Compile Include="Security\IBackOfficeUserPasswordChecker.cs" />
<Compile Include="Security\MembershipPasswordHasher.cs" />
<Compile Include="Security\IMembershipProviderPasswordHasher.cs" />
<Compile Include="Security\IUserAwarePasswordHasher.cs" />
<Compile Include="Security\MembershipProviderPasswordHasher.cs" />
<Compile Include="Security\EmailService.cs" />
<Compile Include="Security\MembershipProviderPasswordValidator.cs" />
<Compile Include="Security\OwinExtensions.cs" />
<Compile Include="Security\UserAwareMembershipProviderPasswordHasher.cs" />
<Compile Include="SemVersionExtensions.cs" />
<Compile Include="Serialization\NoTypeConverterJsonConverter.cs" />
<Compile Include="Serialization\StreamResultExtensions.cs" />
@@ -116,7 +116,6 @@
<ItemGroup>
<None Include="App.config" />
<None Include="packages.config" />
<None Include="_TraceEventProgrammersGuide.docx" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Umbraco.Core\Umbraco.Core.csproj">
@@ -136,10 +135,6 @@
<Analyzer Include="..\packages\Microsoft.CodeAnalysis.Analyzers.1.1.0\analyzers\dotnet\cs\Microsoft.CodeAnalysis.Analyzers.dll" />
<Analyzer Include="..\packages\Microsoft.CodeAnalysis.Analyzers.1.1.0\analyzers\dotnet\cs\Microsoft.CodeAnalysis.CSharp.Analyzers.dll" />
</ItemGroup>
<ItemGroup>
<Content Include="TraceEvent.ReadMe.txt" />
<Content Include="TraceEvent.ReleaseNotes.txt" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<PropertyGroup>
<PostBuildEvent>
+163 -153
View File
@@ -1,215 +1,225 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<configSections>
<sectionGroup name="umbracoConfiguration">
<section name="FileSystemProviders" type="Umbraco.Core.Configuration.FileSystemProvidersSection, Umbraco.Core" requirePermission="false" />
</sectionGroup>
</configSections>
<umbracoConfiguration>
<FileSystemProviders>
<Provider alias="media" type="Umbraco.Core.IO.PhysicalFileSystem, Umbraco.Core">
<Parameters>
<add key="rootPath" value="Media\" />
<add key="rootUrl" value="/Media/" />
</Parameters>
</Provider>
<!-- Macros -->
<Provider alias="macros" type="Umbraco.Core.IO.PhysicalFileSystem, Umbraco.Core">
<Parameters>
<add key="rootPath" value="App_Data\Macros" />
<add key="rootUrl" value="/Macros/" />
</Parameters>
</Provider>
<!-- Scripts -->
<Provider alias="scripts" type="Umbraco.Core.IO.PhysicalFileSystem, Umbraco.Core">
<Parameters>
<add key="rootPath" value="scripts\" />
<add key="rootUrl" value="/scripts/" />
</Parameters>
</Provider>
<!-- Stylesheets -->
<Provider alias="stylesheets" type="Umbraco.Core.IO.PhysicalFileSystem, Umbraco.Core">
<Parameters>
<add key="rootPath" value="css\" />
<add key="rootUrl" value="/css/" />
</Parameters>
</Provider>
<!-- Templates -->
<Provider alias="masterpages" type="Umbraco.Core.IO.PhysicalFileSystem, Umbraco.Core">
<Parameters>
<add key="rootPath" value="masterpages\" />
<add key="rootUrl" value="/masterpages/" />
</Parameters>
</Provider>
<Provider alias="views" type="Umbraco.Core.IO.PhysicalFileSystem, Umbraco.Core">
<Parameters>
<add key="rootPath" value="views\" />
<add key="rootUrl" value="/views/" />
</Parameters>
</Provider>
</FileSystemProviders>
</umbracoConfiguration>
<appSettings>
<add key="umbracoConfigurationStatus" value="6.0.0" />
<add key="umbracoReservedUrls" value="~/config/splashes/booting.aspx,~/install/default.aspx,~/config/splashes/noNodes.aspx,~/VSEnterpriseHelper.axd" />
<add key="umbracoReservedPaths" value="~/install/" />
<add key="umbracoPath" value="~/umbraco" />
<add key="umbracoHideTopLevelNodeFromPath" value="true" />
<add key="umbracoUseDirectoryUrls" value="false" />
<add key="umbracoTimeOutInMinutes" value="20" />
<add key="umbracoDefaultUILanguage" value="en" />
<add key="umbracoUseSSL" value="false" />
</appSettings>
<connectionStrings>
<add name="umbracoDbDSN" connectionString="Datasource=|DataDirectory|UmbracoPetaPocoTests.sdf;Flush Interval=1;" providerName="System.Data.SqlServerCe.4.0" />
</connectionStrings>
<sectionGroup name="umbracoConfiguration">
<section name="FileSystemProviders" type="Umbraco.Core.Configuration.FileSystemProvidersSection, Umbraco.Core" requirePermission="false" />
</sectionGroup>
<system.data>
<DbProviderFactories>
<remove invariant="System.Data.SqlServerCe.4.0" />
<add name="Microsoft SQL Server Compact Data Provider 4.0" invariant="System.Data.SqlServerCe.4.0" description=".NET Framework Data Provider for Microsoft SQL Server Compact" type="System.Data.SqlServerCe.SqlCeProviderFactory, System.Data.SqlServerCe, Version=4.0.0.1, Culture=neutral, PublicKeyToken=89845dcd8080cc91" />
<remove invariant="MySql.Data.MySqlClient" />
<add name="MySQL Data Provider" invariant="MySql.Data.MySqlClient" description=".Net Framework Data Provider for MySQL" type="MySql.Data.MySqlClient.MySqlClientFactory, MySql.Data, Version=6.6.4.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d" />
</DbProviderFactories>
</system.data>
</configSections>
<system.web>
<httpRuntime targetFramework="4.5" />
<compilation defaultLanguage="c#" debug="true" batch="false" targetFramework="4.0"></compilation>
<machineKey validationKey="5E7B955FCE36F5F2A867C2A0D85DC61E7FEA9E15F1561E8386F78BFE9EE23FF18B21E6A44AA17300B3B9D5DBEB37AA61A2C73884A5BBEDA6D3B14BA408A7A8CD" decryptionKey="116B853D031219E404E088FCA0986D6CF2DFA77E1957B59FCC9404B8CA3909A1" validation="SHA1" decryption="AES" />
<!--<trust level="Medium" originUrl=".*"/>-->
<!-- Sitemap provider-->
<siteMap defaultProvider="UmbracoSiteMapProvider" enabled="true">
<providers>
<clear />
<add name="UmbracoSiteMapProvider" type="umbraco.presentation.nodeFactory.UmbracoSiteMapProvider" defaultDescriptionAlias="description" securityTrimmingEnabled="true" />
</providers>
</siteMap>
</system.web>
<umbracoConfiguration>
<FileSystemProviders>
<Provider alias="media" type="Umbraco.Core.IO.PhysicalFileSystem, Umbraco.Core">
<Parameters>
<add key="rootPath" value="Media\" />
<add key="rootUrl" value="/Media/" />
</Parameters>
</Provider>
<!-- Macros -->
<Provider alias="macros" type="Umbraco.Core.IO.PhysicalFileSystem, Umbraco.Core">
<Parameters>
<add key="rootPath" value="App_Data\Macros" />
<add key="rootUrl" value="/Macros/" />
</Parameters>
</Provider>
<!-- Scripts -->
<Provider alias="scripts" type="Umbraco.Core.IO.PhysicalFileSystem, Umbraco.Core">
<Parameters>
<add key="rootPath" value="scripts\" />
<add key="rootUrl" value="/scripts/" />
</Parameters>
</Provider>
<!-- Stylesheets -->
<Provider alias="stylesheets" type="Umbraco.Core.IO.PhysicalFileSystem, Umbraco.Core">
<Parameters>
<add key="rootPath" value="css\" />
<add key="rootUrl" value="/css/" />
</Parameters>
</Provider>
<!-- Templates -->
<Provider alias="masterpages" type="Umbraco.Core.IO.PhysicalFileSystem, Umbraco.Core">
<Parameters>
<add key="rootPath" value="masterpages\" />
<add key="rootUrl" value="/masterpages/" />
</Parameters>
</Provider>
<Provider alias="views" type="Umbraco.Core.IO.PhysicalFileSystem, Umbraco.Core">
<Parameters>
<add key="rootPath" value="views\" />
<add key="rootUrl" value="/views/" />
</Parameters>
</Provider>
</FileSystemProviders>
</umbracoConfiguration>
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" /></startup>
<runtime>
<appSettings>
<add key="umbracoConfigurationStatus" value="6.0.0" />
<add key="umbracoReservedUrls" value="~/config/splashes/booting.aspx,~/install/default.aspx,~/config/splashes/noNodes.aspx,~/VSEnterpriseHelper.axd" />
<add key="umbracoReservedPaths" value="~/install/" />
<add key="umbracoPath" value="~/umbraco" />
<add key="umbracoHideTopLevelNodeFromPath" value="true" />
<add key="umbracoUseDirectoryUrls" value="false" />
<add key="umbracoTimeOutInMinutes" value="20" />
<add key="umbracoDefaultUILanguage" value="en" />
<add key="umbracoUseSSL" value="false" />
</appSettings>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<connectionStrings>
<add name="umbracoDbDSN" connectionString="Datasource=|DataDirectory|UmbracoPetaPocoTests.sdf;Flush Interval=1;" providerName="System.Data.SqlServerCe.4.0" />
</connectionStrings>
<dependentAssembly>
<system.data>
<DbProviderFactories>
<remove invariant="System.Data.SqlServerCe.4.0" />
<add name="Microsoft SQL Server Compact Data Provider 4.0" invariant="System.Data.SqlServerCe.4.0" description=".NET Framework Data Provider for Microsoft SQL Server Compact" type="System.Data.SqlServerCe.SqlCeProviderFactory, System.Data.SqlServerCe, Version=4.0.0.1, Culture=neutral, PublicKeyToken=89845dcd8080cc91" />
<remove invariant="MySql.Data.MySqlClient" />
<add name="MySQL Data Provider" invariant="MySql.Data.MySqlClient" description=".Net Framework Data Provider for MySQL" type="MySql.Data.MySqlClient.MySqlClientFactory, MySql.Data, Version=6.6.4.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d" />
</DbProviderFactories>
</system.data>
<assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<system.web>
<httpRuntime targetFramework="4.5" />
<compilation defaultLanguage="c#" debug="true" batch="false" targetFramework="4.0"></compilation>
<machineKey validationKey="5E7B955FCE36F5F2A867C2A0D85DC61E7FEA9E15F1561E8386F78BFE9EE23FF18B21E6A44AA17300B3B9D5DBEB37AA61A2C73884A5BBEDA6D3B14BA408A7A8CD" decryptionKey="116B853D031219E404E088FCA0986D6CF2DFA77E1957B59FCC9404B8CA3909A1" validation="SHA1" decryption="AES" />
<!--<trust level="Medium" originUrl=".*"/>-->
<!-- Sitemap provider-->
<siteMap defaultProvider="UmbracoSiteMapProvider" enabled="true">
<providers>
<clear />
<add name="UmbracoSiteMapProvider" type="umbraco.presentation.nodeFactory.UmbracoSiteMapProvider" defaultDescriptionAlias="description" securityTrimmingEnabled="true" />
</providers>
</siteMap>
<!-- Membership Provider -->
<membership defaultProvider="UmbracoMembershipProvider" userIsOnlineTimeWindow="15">
<providers>
<clear />
<add name="UmbracoMembershipProvider" type="Umbraco.Web.Security.Providers.MembersMembershipProvider, Umbraco" minRequiredNonalphanumericCharacters="0" minRequiredPasswordLength="4" useLegacyEncoding="false" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="false" defaultMemberTypeAlias="Member" passwordFormat="Hashed" />
<add name="UsersMembershipProvider" type="Umbraco.Web.Security.Providers.UsersMembershipProvider, Umbraco" minRequiredNonalphanumericCharacters="0" minRequiredPasswordLength="4" useLegacyEncoding="false" enablePasswordRetrieval="false" enablePasswordReset="false" requiresQuestionAndAnswer="false" passwordFormat="Hashed" allowManuallyChangingPassword="false" />
</providers>
</membership>
</system.web>
<bindingRedirect oldVersion="0.0.0.0-5.2.3.0" newVersion="5.2.3.0" />
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<runtime>
</dependentAssembly>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Web.WebPages" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-3.0.0.0" newVersion="3.0.0.0" />
<bindingRedirect oldVersion="0.0.0.0-5.2.3.0" newVersion="5.2.3.0" />
</dependentAssembly>
</dependentAssembly>
<dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Web.Helpers" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<assemblyIdentity name="System.Web.WebPages" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-2.0.0.0" newVersion="2.0.0.0" />
<bindingRedirect oldVersion="0.0.0.0-3.0.0.0" newVersion="3.0.0.0" />
</dependentAssembly>
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-10.0.0.0" newVersion="10.0.0.0" />
</dependentAssembly>
<dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="MiniProfiler" publicKeyToken="b44f9351044011a3" culture="neutral" />
<assemblyIdentity name="System.Web.Helpers" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-2.1.0.0" newVersion="2.1.0.0" />
<bindingRedirect oldVersion="0.0.0.0-2.0.0.0" newVersion="2.0.0.0" />
</dependentAssembly>
</dependentAssembly>
<dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="NuGet.Core" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-1.0.11220.104" newVersion="1.0.11220.104" />
<bindingRedirect oldVersion="0.0.0.0-10.0.0.0" newVersion="10.0.0.0" />
</dependentAssembly>
</dependentAssembly>
<dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Net.Http" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<assemblyIdentity name="MiniProfiler" publicKeyToken="b44f9351044011a3" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-4.0.0.0" newVersion="4.0.0.0" />
<bindingRedirect oldVersion="0.0.0.0-2.1.0.0" newVersion="2.1.0.0" />
</dependentAssembly>
</dependentAssembly>
<dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="Microsoft.Owin" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<assemblyIdentity name="NuGet.Core" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-3.1.0.0" newVersion="3.1.0.0" />
<bindingRedirect oldVersion="0.0.0.0-1.0.11220.104" newVersion="1.0.11220.104" />
</dependentAssembly>
</dependentAssembly>
<dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="Microsoft.Owin.Security.OAuth" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<assemblyIdentity name="System.Net.Http" publicKeyToken="b03f5f7f11d50a3a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-3.1.0.0" newVersion="3.1.0.0" />
<bindingRedirect oldVersion="0.0.0.0-4.0.0.0" newVersion="4.0.0.0" />
</dependentAssembly>
</dependentAssembly>
<dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="Microsoft.Owin.Security" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<assemblyIdentity name="Microsoft.Owin" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-3.1.0.0" newVersion="3.1.0.0" />
<bindingRedirect oldVersion="0.0.0.0-3.1.0.0" newVersion="3.1.0.0" />
</dependentAssembly>
</dependentAssembly>
<dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="Microsoft.Owin.Security.Cookies" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<assemblyIdentity name="Microsoft.Owin.Security.OAuth" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-3.1.0.0" newVersion="3.1.0.0" />
<bindingRedirect oldVersion="0.0.0.0-3.1.0.0" newVersion="3.1.0.0" />
</dependentAssembly>
</dependentAssembly>
<dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Web.Http" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<assemblyIdentity name="Microsoft.Owin.Security" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-5.2.3.0" newVersion="5.2.3.0" />
<bindingRedirect oldVersion="0.0.0.0-3.1.0.0" newVersion="3.1.0.0" />
</dependentAssembly>
</dependentAssembly>
<dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="System.Net.Http.Formatting" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<assemblyIdentity name="Microsoft.Owin.Security.Cookies" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-5.2.3.0" newVersion="5.2.3.0" />
<bindingRedirect oldVersion="0.0.0.0-3.1.0.0" newVersion="3.1.0.0" />
</dependentAssembly>
</dependentAssembly>
<dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="log4net" publicKeyToken="669e0ddf0bb1aa2a" culture="neutral" />
<assemblyIdentity name="System.Web.Http" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-2.0.8.0" newVersion="2.0.8.0" />
<bindingRedirect oldVersion="0.0.0.0-5.2.3.0" newVersion="5.2.3.0" />
</dependentAssembly>
</dependentAssembly>
</assemblyBinding>
<dependentAssembly>
</runtime>
<assemblyIdentity name="System.Net.Http.Formatting" publicKeyToken="31bf3856ad364e35" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-5.2.3.0" newVersion="5.2.3.0" />
</dependentAssembly>
<dependentAssembly>
<assemblyIdentity name="log4net" publicKeyToken="669e0ddf0bb1aa2a" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-2.0.8.0" newVersion="2.0.8.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
@@ -145,7 +145,7 @@ namespace Umbraco.Tests.Membership
Assert.AreEqual("test", provider.Name);
Assert.AreEqual(MembershipProviderBase.GetDefaultAppName(), provider.ApplicationName);
Assert.AreEqual(false, provider.EnablePasswordRetrieval);
Assert.AreEqual(false, provider.EnablePasswordReset);
Assert.AreEqual(true, provider.EnablePasswordReset);
Assert.AreEqual(false, provider.RequiresQuestionAndAnswer);
Assert.AreEqual(true, provider.RequiresUniqueEmail);
Assert.AreEqual(5, provider.MaxInvalidPasswordAttempts);
@@ -294,7 +294,7 @@ AnotherContentFinder
{
var trees = _manager.ResolveAttributedTrees();
// commit 6c5e35ec2cbfa31be6790d1228e0c2faf5f55bc8 brings the count down to 14
Assert.AreEqual(9, trees.Count());
Assert.AreEqual(8, trees.Count());
}
[Test]
@@ -70,6 +70,7 @@ namespace Umbraco.Tests.TestHelpers
settings.Setup(x => x.Content.UmbracoLibraryCacheDuration).Returns(1800);
settings.Setup(x => x.WebRouting.UrlProviderMode).Returns("AutoLegacy");
settings.Setup(x => x.Templates.DefaultRenderingEngine).Returns(RenderingEngine.Mvc);
settings.Setup(x => x.Providers.DefaultBackOfficeUserProvider).Returns("UsersMembershipProvider");
return settings.Object;
}
@@ -21,154 +21,140 @@
* </pre>
*/
angular.module('umbraco.services')
.factory('searchService', function ($q, $log, entityResource, contentResource, umbRequestHelper) {
.factory('searchService', function ($q, $log, entityResource, contentResource, umbRequestHelper, $injector, searchResultFormatter) {
function configureMemberResult(member) {
member.menuUrl = umbRequestHelper.getApiUrl("memberTreeBaseUrl", "GetMenu", [{ id: member.id }, { application: 'member' }]);
member.editorPath = "member/member/edit/" + (member.key ? member.key : member.id);
angular.extend(member.metaData, { treeAlias: "member" });
member.subTitle = member.metaData.Email;
}
function configureMediaResult(media)
{
media.menuUrl = umbRequestHelper.getApiUrl("mediaTreeBaseUrl", "GetMenu", [{ id: media.id }, { application: 'media' }]);
media.editorPath = "media/media/edit/" + media.id;
angular.extend(media.metaData, { treeAlias: "media" });
}
function configureContentResult(content) {
content.menuUrl = umbRequestHelper.getApiUrl("contentTreeBaseUrl", "GetMenu", [{ id: content.id }, { application: 'content' }]);
content.editorPath = "content/content/edit/" + content.id;
angular.extend(content.metaData, { treeAlias: "content" });
content.subTitle = content.metaData.Url;
}
return {
return {
/**
* @ngdoc method
* @name umbraco.services.searchService#searchMembers
* @methodOf umbraco.services.searchService
*
* @description
* Searches the default member search index
* @param {Object} args argument object
* @param {String} args.term seach term
* @returns {Promise} returns promise containing all matching members
*/
searchMembers: function (args) {
/**
* @ngdoc method
* @name umbraco.services.searchService#searchMembers
* @methodOf umbraco.services.searchService
*
* @description
* Searches the default member search index
* @param {Object} args argument object
* @param {String} args.term seach term
* @returns {Promise} returns promise containing all matching members
*/
searchMembers: function(args) {
if (!args.term) {
throw "args.term is required";
}
if (!args.term) {
throw "args.term is required";
}
return entityResource.search(args.term, "Member", args.searchFrom).then(function (data) {
_.each(data, function(item) {
configureMemberResult(item);
});
return data;
});
},
/**
* @ngdoc method
* @name umbraco.services.searchService#searchContent
* @methodOf umbraco.services.searchService
*
* @description
* Searches the default internal content search index
* @param {Object} args argument object
* @param {String} args.term seach term
* @returns {Promise} returns promise containing all matching content items
*/
searchContent: function(args) {
if (!args.term) {
throw "args.term is required";
}
return entityResource.search(args.term, "Document", args.searchFrom, args.canceler).then(function (data) {
_.each(data, function (item) {
configureContentResult(item);
return entityResource.search(args.term, "Member", args.searchFrom).then(function (data) {
_.each(data, function (item) {
configureMemberResult(item);
});
return data;
});
return data;
});
},
},
/**
* @ngdoc method
* @name umbraco.services.searchService#searchMedia
* @methodOf umbraco.services.searchService
*
* @description
* Searches the default media search index
* @param {Object} args argument object
* @param {String} args.term seach term
* @returns {Promise} returns promise containing all matching media items
*/
searchMedia: function(args) {
/**
* @ngdoc method
* @name umbraco.services.searchService#searchContent
* @methodOf umbraco.services.searchService
*
* @description
* Searches the default internal content search index
* @param {Object} args argument object
* @param {String} args.term seach term
* @returns {Promise} returns promise containing all matching content items
*/
searchContent: function (args) {
if (!args.term) {
throw "args.term is required";
}
if (!args.term) {
throw "args.term is required";
}
return entityResource.search(args.term, "Media", args.searchFrom).then(function (data) {
_.each(data, function (item) {
configureMediaResult(item);
return entityResource.search(args.term, "Document", args.searchFrom, args.canceler).then(function (data) {
_.each(data, function (item) {
configureContentResult(item);
});
return data;
});
return data;
});
},
},
/**
* @ngdoc method
* @name umbraco.services.searchService#searchAll
* @methodOf umbraco.services.searchService
*
* @description
* Searches all available indexes and returns all results in one collection
* @param {Object} args argument object
* @param {String} args.term seach term
* @returns {Promise} returns promise containing all matching items
*/
searchAll: function (args) {
if (!args.term) {
throw "args.term is required";
}
/**
* @ngdoc method
* @name umbraco.services.searchService#searchMedia
* @methodOf umbraco.services.searchService
*
* @description
* Searches the default media search index
* @param {Object} args argument object
* @param {String} args.term seach term
* @returns {Promise} returns promise containing all matching media items
*/
searchMedia: function (args) {
return entityResource.searchAll(args.term, args.canceler).then(function (data) {
if (!args.term) {
throw "args.term is required";
}
_.each(data, function(resultByType) {
switch(resultByType.type) {
case "Document":
_.each(resultByType.results, function (item) {
configureContentResult(item);
});
break;
case "Media":
_.each(resultByType.results, function (item) {
configureMediaResult(item);
});
break;
case "Member":
_.each(resultByType.results, function (item) {
configureMemberResult(item);
});
break;
}
return entityResource.search(args.term, "Media", args.searchFrom).then(function (data) {
_.each(data, function (item) {
configureMediaResult(item);
});
return data;
});
},
/**
* @ngdoc method
* @name umbraco.services.searchService#searchAll
* @methodOf umbraco.services.searchService
*
* @description
* Searches all available indexes and returns all results in one collection
* @param {Object} args argument object
* @param {String} args.term seach term
* @returns {Promise} returns promise containing all matching items
*/
searchAll: function (args) {
if (!args.term) {
throw "args.term is required";
}
return entityResource.searchAll(args.term, args.canceler).then(function (data) {
_.each(data, function (resultByType) {
//we need to format the search result data to include things like the subtitle, urls, etc...
// this is done with registered angular services as part of the SearchableTreeAttribute, if that
// is not found, than we format with the default formatter
var formatterMethod = searchResultFormatter.configureDefaultResult;
//check if a custom formatter is specified...
if (resultByType.jsSvc) {
var searchFormatterService = $injector.get(resultByType.jsSvc);
if (searchFormatterService) {
if (!resultByType.jsMethod) {
resultByType.jsMethod = "format";
}
formatterMethod = searchFormatterService[resultByType.jsMethod];
if (!formatterMethod) {
throw "The method " + resultByType.jsMethod + " on the angular service " + resultByType.jsSvc + " could not be found";
}
}
}
//now apply the formatter for each result
_.each(resultByType.results, function (item) {
formatterMethod.apply(this, [item, resultByType.treeAlias, resultByType.appAlias]);
});
});
return data;
});
return data;
});
},
},
//TODO: This doesn't do anything!
setCurrent: function(sectionAlias) {
//TODO: This doesn't do anything!
setCurrent: function (sectionAlias) {
var currentSection = sectionAlias;
}
};
});
var currentSection = sectionAlias;
}
};
});
@@ -0,0 +1,37 @@
function searchResultFormatter(umbRequestHelper) {
function configureDefaultResult(content, treeAlias, appAlias) {
content.editorPath = appAlias + "/" + treeAlias + "/edit/" + content.id;
angular.extend(content.metaData, { treeAlias: treeAlias });
}
function configureContentResult(content, treeAlias, appAlias) {
content.menuUrl = umbRequestHelper.getApiUrl("contentTreeBaseUrl", "GetMenu", [{ id: content.id }, { application: appAlias }]);
content.editorPath = appAlias + "/" + treeAlias + "/edit/" + content.id;
angular.extend(content.metaData, { treeAlias: treeAlias });
content.subTitle = content.metaData.Url;
}
function configureMemberResult(member, treeAlias, appAlias) {
member.menuUrl = umbRequestHelper.getApiUrl("memberTreeBaseUrl", "GetMenu", [{ id: member.id }, { application: appAlias }]);
member.editorPath = appAlias + "/" + treeAlias + "/edit/" + (member.key ? member.key : member.id);
angular.extend(member.metaData, { treeAlias: treeAlias });
member.subTitle = member.metaData.Email;
}
function configureMediaResult(media, treeAlias, appAlias) {
media.menuUrl = umbRequestHelper.getApiUrl("mediaTreeBaseUrl", "GetMenu", [{ id: media.id }, { application: appAlias }]);
media.editorPath = appAlias + "/" + treeAlias + "/edit/" + media.id;
angular.extend(media.metaData, { treeAlias: treeAlias });
}
return {
configureContentResult: configureContentResult,
configureMemberResult: configureMemberResult,
configureMediaResult: configureMediaResult,
configureDefaultResult: configureDefaultResult
};
}
angular.module('umbraco.services').factory('searchResultFormatter', searchResultFormatter);
@@ -15,21 +15,21 @@ function SearchController($scope, searchService, $log, $location, navigationServ
$scope.selectedResult = -1;
$scope.navigateResults = function(ev){
$scope.navigateResults = function (ev) {
//38: up 40: down, 13: enter
switch(ev.keyCode){
switch (ev.keyCode) {
case 38:
iterateResults(true);
iterateResults(true);
break;
case 40:
iterateResults(false);
iterateResults(false);
break;
case 13:
if ($scope.selectedItem) {
$location.path($scope.selectedItem.editorPath);
navigationService.hideSearch();
}
}
break;
}
};
@@ -39,50 +39,50 @@ function SearchController($scope, searchService, $log, $location, navigationServ
var groupIndex = -1;
var itemIndex = -1;
$scope.selectedItem = undefined;
function iterateResults(up){
function iterateResults(up) {
//default group
if(!group){
if (!group) {
group = $scope.groups[0];
groupIndex = 0;
}
if(up){
if(itemIndex === 0){
if(groupIndex === 0){
gotoGroup($scope.groups.length-1, true);
}else{
gotoGroup(groupIndex-1, true);
if (up) {
if (itemIndex === 0) {
if (groupIndex === 0) {
gotoGroup($scope.groups.length - 1, true);
} else {
gotoGroup(groupIndex - 1, true);
}
}else{
gotoItem(itemIndex-1);
} else {
gotoItem(itemIndex - 1);
}
}else{
if(itemIndex < group.results.length-1){
gotoItem(itemIndex+1);
}else{
if(groupIndex === $scope.groups.length-1){
} else {
if (itemIndex < group.results.length - 1) {
gotoItem(itemIndex + 1);
} else {
if (groupIndex === $scope.groups.length - 1) {
gotoGroup(0);
}else{
gotoGroup(groupIndex+1);
} else {
gotoGroup(groupIndex + 1);
}
}
}
}
function gotoGroup(index, up){
function gotoGroup(index, up) {
groupIndex = index;
group = $scope.groups[groupIndex];
if(up){
gotoItem(group.results.length-1);
}else{
gotoItem(0);
if (up) {
gotoItem(group.results.length - 1);
} else {
gotoItem(0);
}
}
function gotoItem(index){
function gotoItem(index) {
itemIndex = index;
$scope.selectedItem = group.results[itemIndex];
}
@@ -91,7 +91,7 @@ function SearchController($scope, searchService, $log, $location, navigationServ
var canceler = null;
$scope.$watch("searchTerm", _.debounce(function (newVal, oldVal) {
$scope.$apply(function() {
$scope.$apply(function () {
if ($scope.searchTerm) {
if (newVal !== null && newVal !== undefined && newVal !== oldVal) {
$scope.isSearching = true;
@@ -107,8 +107,16 @@ function SearchController($scope, searchService, $log, $location, navigationServ
canceler = $q.defer();
}
searchService.searchAll({ term: $scope.searchTerm, canceler: canceler }).then(function(result) {
$scope.groups = _.filter(result, function (group) { return group.results.length > 0; });
searchService.searchAll({ term: $scope.searchTerm, canceler: canceler }).then(function (result) {
//result is a dictionary of group Title and it's results
var filtered = {};
_.each(result, function (value, key) {
if (value.results.length > 0) {
filtered[key] = value;
}
});
$scope.groups = filtered;
//set back to null so it can be re-created
canceler = null;
});
@@ -124,6 +124,7 @@
@import "components/umb-nested-content.less";
@import "components/umb-checkmark.less";
@import "components/umb-list.less";
@import "components/umb-box.less";
@import "components/buttons/umb-button.less";
@import "components/buttons/umb-button-group.less";
@@ -140,7 +141,6 @@
@import "components/users/umb-user-group-preview.less";
@import "components/users/umb-user-preview.less";
@import "components/users/umb-user-picker-list.less";
@import "components/users/umb-user.less";
@import "components/users/umb-permission.less";
@@ -0,0 +1,20 @@
.umb-box {
border: 1px solid @gray-8;
border-radius: 3px;
margin-bottom: 30px;
}
.umb-box-header {
padding: 10px 20px;
background-color: @gray-10;
}
.umb-box-header-title {
font-size: 16px;
color: @black;
font-weight: bold;
}
.umb-box-content {
padding: 20px;
}
@@ -1,37 +0,0 @@
.test-group {
border: 1px solid @gray-8;
border-radius: 3px;
margin-bottom: 30px;
}
.test-group-title {
padding: 10px 20px;
margin-bottom: 0;
background-color: @gray-10;
color: @black;
font-size: 16px;
font-weight: bold;
color: @black;
}
.test-group-sub-header {
padding: 10px 20px;
border-top: 1px solid @gray-8;
border-bottom: 1px solid @gray-8;
font-weight: bold;
color: @black;
}
/*
.test-group-sub-header:first-of-type {
border-top: none;
}
*/
.test-group-sub-header:last-of-type {
border-bottom: none;
}
.test-group-content {
padding: 20px;
}
+35 -13
View File
@@ -53,22 +53,21 @@
border-color: @turquoise-d1;
}
.umb-tree li.root > div {
padding: 0;
.umb-tree li.root > div:first-child {
padding: 0;
}
.umb-tree li.root > div h5 {
margin: 0;
width: 100%;
display: flex;
align-items: center;
.umb-tree li.root > div h5, .umb-tree li.root > div h6 {
margin: 0;
width: 100%;
display: flex;
align-items: center;
}
.umb-tree li.root > div h5 > a, .umb-tree-header {
display: flex;
padding: 20px 0 20px 20px;
box-sizing: border-box;
.umb-tree li.root > div:first-child h5 > a, .umb-tree-header {
display: flex;
padding: 20px 0 20px 20px;
box-sizing: border-box;
}
.umb-tree * {
@@ -96,14 +95,22 @@
text-decoration: none
}
.umb-tree div {
/*.umb-tree div.tree-node {
padding: 5px 0 5px 0;
position: relative;
overflow: hidden;
display: flex;
flex-wrap: nowrap;
align-items: center;
}*/
.umb-tree div {
padding: 5px 0 5px 0;
position: relative;
overflow: hidden;
display: flex;
flex-wrap: nowrap;
align-items: center;
}
.umb-tree a.noSpr {
@@ -192,6 +199,21 @@
/*color:@turquoise;*/
}
.umb-tree div.umb-search-group {
position: inherit;
display: inherit;
}
.umb-tree div.umb-search-group:hover {
background: inherit;
}
.umb-tree div.umb-search-group h6 {
/*color: @gray-5;*/
padding: 10px 0 10px 20px;
font-weight: inherit;
background:@gray-10;
}
.umb-tree .umb-search-group-item {
padding-left: 20px;
}
@@ -91,7 +91,8 @@ angular.module("umbraco").controller("Umbraco.Overlays.LinkPickerController",
userService.getCurrentUser().then(function (userData) {
$scope.mediaPickerOverlay = {
view: "mediapicker",
startNodeId: userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0],
startNodeId: userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0],
startNodeIsVirtual: userData.startMediaIds.length !== 1,
show: true,
submit: function(model) {
var media = model.selectedImages[0];
@@ -1,104 +1,106 @@
<div id="leftcolumn" ng-controller="Umbraco.NavigationController"
ng-mouseleave="leaveTree($event)" ng-mouseenter="enterTree($event)">
ng-mouseleave="leaveTree($event)" ng-mouseenter="enterTree($event)">
<umb-sections sections="sections" ng-if="authenticated">
</umb-sections>
<umb-sections sections="sections" ng-if="authenticated">
</umb-sections>
<!-- navigation container -->
<div id="navigation" ng-show="showNavigation" class="fill umb-modalcolumn" ng-animate="'slide'" nav-resize>
<!-- navigation container -->
<div id="navigation" ng-show="showNavigation" class="fill umb-modalcolumn" ng-animate="'slide'" nav-resize>
<div ng-swipe-left="nav.hideNavigation()" class="navigation-inner-container span6">
<div ng-swipe-left="nav.hideNavigation()" class="navigation-inner-container span6">
<!-- the search -->
<div ng-controller="Umbraco.SearchController" ng-if="authenticated">
<!-- the search -->
<div ng-controller="Umbraco.SearchController" ng-if="authenticated">
<!-- Search form -->
<div id="search-form">
<div class="umb-modalcolumn-header">
<!-- Search form -->
<div id="search-form">
<div class="umb-modalcolumn-header">
<form class="form-search" novalidate>
<i class="icon-search"></i>
<input type="text"
hotkey="ctrl+space"
id="search-field"
ng-model="searchTerm"
class="umb-search-field search-query search-input"
localize="placeholder"
placeholder="@placeholders_search"
ng-keydown="navigateResults($event)"/>
</form>
<form class="form-search" novalidate>
<i class="icon-search"></i>
<input type="text"
hotkey="ctrl+space"
id="search-field"
ng-model="searchTerm"
class="umb-search-field search-query search-input"
localize="placeholder"
placeholder="@placeholders_search"
ng-keydown="navigateResults($event)" />
</form>
</div>
</div>
<!-- Search results -->
<div id="search-results" class="umb-modalcolumn-body" ng-show="showSearchResults">
<ul class="umb-tree">
<li class="root">
<div>
<h5 class="umb-tree-header"><localize key="general_searchResults">Search results</localize></h5>
</div>
<div class="umb-search-group" ng-repeat="(key, group) in groups">
<h6 class="umb-tree-header">{{key}}</h6>
<ul>
<li ng-repeat="result in group.results" ng-class="{'current':selectedItem == result}">
<div class="umb-search-group-item">
<a class="umb-search-group-item-link" ng-class="{'first':$first}" ng-click="searchHide()" ng-href="#/{{result.editorPath}}">
<div class="umb-search-group-item-name">
<i class="icon umb-tree-icon sprTree {{result.icon}}"></i>
{{result.name}}
</div>
<small class="search-subtitle" ng-show="result.subTitle">
{{result.subTitle}}
</small>
</a>
<a href ng-hide="!result.menuUrl" ng-click="searchShowMenu($event, {node: result})" class="umb-options"><i></i><i></i><i></i></a>
</div>
</div>
</li>
</ul>
<!-- Search results -->
<div id="search-results" class="umb-modalcolumn-body" ng-show="showSearchResults">
</div>
<ul class="umb-tree">
<li class="root">
<div>
<h5 class="umb-tree-header"><localize key="general_searchResults">Search results</localize></h5>
</div>
<ul class="umb-search-group" ng-repeat="group in groups">
<li ng-repeat="result in group.results" ng-class="{'current':selectedItem == result}">
<div class="umb-search-group-item">
<a class="umb-search-group-item-link" ng-class="{'first':$first}" ng-click="searchHide()" ng-href="#/{{result.editorPath}}">
<div class="umb-search-group-item-name">
<i class="icon umb-tree-icon sprTree {{result.icon}}"></i>
{{result.name}}
</div>
<small class="search-subtitle" ng-show="result.subTitle">
{{result.subTitle}}
</small>
</a>
<a href ng-click="searchShowMenu($event, {node: result})" class="umb-options"><i></i><i></i><i></i></a>
</div>
</li>
</ul>
</li>
</ul>
</div>
</div>
<!-- the tree -->
<div id="tree" class="umb-modalcolumn-body" ng-if="authenticated">
<umb-tree
cachekey="_"
eventhandler="treeEventHandler"
section="{{currentSection}}" >
</umb-tree>
</div>
</li>
</ul>
</div>
<div class="offset6" id="navOffset" style="z-index: 10">
</div>
<!-- The context menu -->
<div id='contextMenu' class="umb-modalcolumn fill shadow" ng-swipe-left="nav.hideMenu()" ng-show="showContextMenu" ng-animate="'slide'">
<umb-context-menu
menu-dialog-title="{{menuDialogTitle}}"
current-section="{{currentSection}}"
current-node="menuNode"
menu-actions="menuActions">
</umb-context-menu>
</div>
<!-- Tree dialogs -->
<div id="dialog" class='umb-modalcolumn fill shadow'
ng-swipe-left="nav.hideDialog()"
ng-show="showContextMenuDialog" ng-animate="'slide'">
<div class='umb-modalcolumn-header'>
<h1>{{menuDialogTitle}}</h1>
</div>
<div class='umb-modalcolumn-body'>
</div>
</div>
</div>
<!-- the tree -->
<div id="tree" class="umb-modalcolumn-body" ng-if="authenticated">
<umb-tree cachekey="_"
eventhandler="treeEventHandler"
section="{{currentSection}}">
</umb-tree>
</div>
</div>
<div class="offset6" id="navOffset" style="z-index: 10">
<!-- The context menu -->
<div id='contextMenu' class="umb-modalcolumn fill shadow" ng-swipe-left="nav.hideMenu()" ng-show="showContextMenu" ng-animate="'slide'">
<umb-context-menu menu-dialog-title="{{menuDialogTitle}}"
current-section="{{currentSection}}"
current-node="menuNode"
menu-actions="menuActions">
</umb-context-menu>
</div>
<!-- Tree dialogs -->
<div id="dialog" class='umb-modalcolumn fill shadow'
ng-swipe-left="nav.hideDialog()"
ng-show="showContextMenuDialog" ng-animate="'slide'">
<div class='umb-modalcolumn-header'>
<h1>{{menuDialogTitle}}</h1>
</div>
<div class='umb-modalcolumn-body'>
</div>
</div>
</div>
</div>
</div>
@@ -278,8 +278,8 @@ function MediaFolderBrowserDashboardController($rootScope, $scope, $location, co
currentUser = user;
// check if the user start node is the dashboard
if (currentUser.startMediaIds.length === 0 || currentUser.startMediaIds.indexOf(-1) >= 0) {
// check if the user has access to the root which they will require to see this dashboard
if (currentUser.startMediaIds.indexOf(-1) >= 0) {
//get the system media listview
contentTypeResource.getPropertyTypeScaffold(-96)
@@ -303,7 +303,7 @@ function MediaFolderBrowserDashboardController($rootScope, $scope, $location, co
});
} else {
} else if (currentUser.startMediaIds.length > 0){
// redirect to start node
$location.path("/media/media/edit/" + (currentUser.startMediaIds.length === 0 ? -1 : currentUser.startMediaIds[0]));
}
@@ -287,6 +287,7 @@ angular.module("umbraco")
showDetails: true,
disableFolderSelect: true,
startNodeId: userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0],
startNodeIsVirtual: userData.startMediaIds.length !== 1,
view: "mediapicker",
show: true,
submit: function(model) {
@@ -1,6 +1,6 @@
function textAreaController($rootScope, $scope, $log) {
$scope.model.maxlength = false;
if($scope.model.config.maxChars) {
if ($scope.model.config && $scope.model.config.maxChars) {
$scope.model.maxlength = true;
if($scope.model.value == undefined) {
$scope.model.count = ($scope.model.config.maxChars * 1);
@@ -10,7 +10,7 @@ function textAreaController($rootScope, $scope, $log) {
}
$scope.model.change = function() {
if($scope.model.config.maxChars) {
if ($scope.model.config && $scope.model.config.maxChars) {
if($scope.model.value == undefined) {
$scope.model.count = ($scope.model.config.maxChars * 1);
} else {
@@ -1,6 +1,6 @@
function textboxController($rootScope, $scope, $log) {
$scope.model.maxlength = false;
if($scope.model.config.maxChars) {
if ($scope.model.config && $scope.model.config.maxChars) {
$scope.model.maxlength = true;
if($scope.model.value == undefined) {
$scope.model.count = ($scope.model.config.maxChars * 1);
@@ -10,7 +10,7 @@ function textboxController($rootScope, $scope, $log) {
}
$scope.model.change = function() {
if($scope.model.config.maxChars) {
if ($scope.model.config && $scope.model.config.maxChars) {
if($scope.model.value == undefined) {
$scope.model.count = ($scope.model.config.maxChars * 1);
} else {
@@ -21,9 +21,11 @@
<div class="umb-package-details__main-content">
<div class="test-group">
<div class="test-group-title"><localize key="user_access">Access</localize></div>
<div class="test-group-content block-form">
<div class="umb-box">
<div class="umb-box-header">
<div class="umb-box-header-title"><localize key="user_access">Access</localize></div>
</div>
<div class="umb-box-content block-form">
<umb-control-group style="margin-bottom: 20px;" label="@main_sections" description="@user_sectionsHelp">
<umb-node-preview
@@ -87,9 +89,11 @@
</div>
</div>
<div class="test-group">
<div class="test-group-title"><localize key="user_permissionsDefault">Default permissions</localize></div>
<div class="test-group-content block-form">
<div class="umb-box">
<div class="umb-box-header">
<div class="umb-box-header-title"><localize key="user_permissionsDefault">Default permissions</localize></div>
</div>
<div class="umb-box-content block-form">
<umb-control-group
ng-repeat="(category, permissions) in vm.userGroup.defaultPermissions"
label="{{ category }}">
@@ -103,9 +107,11 @@
</div>
</div>
<div class="test-group">
<div class="test-group-title"><localize key="user_permissionsGranular">Granular permissions</localize></div>
<div class="test-group-content block-form">
<div class="umb-box">
<div class="umb-box-header">
<div class="umb-box-header-title"><localize key="user_permissionsGranular">Granular permissions</localize></div>
</div>
<div class="umb-box-content block-form">
<umb-control-group label="Nodes" description="@user_permissionsGranularHelp">
<umb-node-preview
@@ -20,9 +20,11 @@
<div class="umb-package-details__main-content">
<div class="test-group">
<div class="test-group-title"><localize key="user_profile">Profile</localize></div>
<div class="test-group-content block-form">
<div class="umb-box">
<div class="umb-box-header">
<div class="umb-box-header-title"><localize key="user_profile">Profile</localize></div>
</div>
<div class="umb-box-content block-form">
<umb-control-group label="@general_email">
<input type="text"
@@ -64,9 +66,11 @@
</div>
</div>
<div class="test-group" ng-if="!vm.user.isCurrentUser">
<div class="test-group-title"><localize key="user_access">Access</localize></div>
<div class="test-group-content block-form">
<div class="umb-box" ng-if="!vm.user.isCurrentUser">
<div class="umb-box-header">
<div class="umb-box-header-title"><localize key="user_access">Access</localize></div>
</div>
<div class="umb-box-content block-form">
<umb-control-group style="margin-bottom: 25px;" label="@general_groups" description="@user_groupsHelp">
@@ -138,34 +142,41 @@
<div class="umb-package-details__section">
<div style="margin-bottom: 30px;">
<div style="margin-bottom: 20px; padding-bottom: 20px; border-bottom: 1px solid #d8d7d9;">
<ng-form name="avatarForm" class="flex flex-column justify-center items-center">
<umb-avatar style="margin-bottom: 10px;"
color="secondary"
size="xxl"
name="{{vm.user.name}}"
img-src="{{vm.user.avatars[3]}}"
img-srcset="{{vm.user.avatars[4]}} 2x, {{vm.user.avatars[4]}} 3x">
</umb-avatar>
<umb-avatar style="margin-bottom: 15px;"
color="secondary"
size="xxl"
name="{{vm.user.name}}"
img-src="{{vm.user.avatars[3]}}"
img-srcset="{{vm.user.avatars[4]}} 2x, {{vm.user.avatars[4]}} 3x">
</umb-avatar>
<umb-progress-bar style="max-width: 120px;"
ng-if="vm.avatarFile.uploadStatus === 'uploading'"
progress="{{ vm.avatarFile.uploadProgress }}"
size="s">
</umb-progress-bar>
<div class="flex items-center" ng-if="vm.avatarFile.uploadStatus !== 'uploading'">
<a href=""
class="umb-user-group-preview__action"
ngf-select
ng-model="filesHolder"
ngf-change="changeAvatar($files, $event)"
ngf-multiple="false"
ngf-pattern="{{vm.acceptedFileTypes}}"
ngf-max-size="{{ vm.maxFileSize }}">
<localize key="user_changePhoto">Change photo</localize>
</a>
<a href="" ng-if="vm.user.avatars" class="umb-user-group-preview__action umb-user-group-preview__action--red" ng-click="vm.clearAvatar()" prevent-default>
<localize key="user_removePhoto">Remove photo</localize>
</a>
<a href=""
class="file-select bold"
ng-if="vm.avatarFile.uploadStatus !== 'uploading'"
ngf-select
ng-model="filesHolder"
ngf-change="changeAvatar($files, $event)"
ngf-multiple="false"
ngf-pattern="{{vm.acceptedFileTypes}}"
ngf-max-size="{{ vm.maxFileSize }}">
Change photo
</a>
</div>
</ng-form>
@@ -1,7 +1,7 @@
(function () {
"use strict";
function UserGroupsController($scope, $timeout, $location, userGroupsResource, formHelper) {
function UserGroupsController($scope, $timeout, $location, userGroupsResource, formHelper, localizationService) {
var vm = this;
@@ -59,14 +59,26 @@
}
function deleteUserGroups() {
if (vm.selection.length > 0) {
userGroupsResource.deleteUserGroups(vm.selection).then(function (data) {
clearSelection();
onInit();
formHelper.showNotifications(data);
}, function(error) {
formHelper.showNotifications(error.data);
});
if(vm.selection.length > 0) {
localizationService.localize("defaultdialogs_confirmdelete")
.then(function(value) {
var confirmResponse = confirm(value);
if (confirmResponse === true) {
userGroupsResource.deleteUserGroups(vm.selection).then(function (data) {
clearSelection();
onInit();
formHelper.showNotifications(data);
}, function(error) {
formHelper.showNotifications(error.data);
});
}
});
}
}
@@ -13,6 +13,25 @@
</umb-button>
</umb-editor-sub-header-content-left>
<umb-editor-sub-header-content-right ng-if="vm.selection.length === 0">
<umb-editor-sub-header-section>
<div class="form-search -no-margin-bottom pull-right">
<div class="inner-addon left-addon">
<i class="icon icon-search"></i>
<input class="form-control search-input"
type="text"
localize="placeholder"
placeholder="@placeholders_filter"
ng-model="vm.filter"
prevent-enter-submit
no-dirty-check>
</div>
</div>
</umb-editor-sub-header-section>
</umb-editor-sub-header-content-right>
<!-- With selection -->
<umb-editor-sub-header-content-left ng-if="vm.selection.length > 0">
<umb-editor-sub-header-section>
@@ -40,8 +59,14 @@
</umb-editor-sub-header>
<div style="margin-bottom: 20px;" class="flex items-center">
<div style="font-size: 16px;">
<span class="bold"><localize key="general_groups">Groups</localize> </span> <span>({{vm.userGroups.length}})</span>
</div>
</div>
<div class="umb-list">
<a class="umb-list-item" ng-click="vm.clickUserGroup(group)" ng-class="{'umb-list-item--selected': group.selected}" href="" ng-repeat="group in vm.userGroups">
<a class="umb-list-item" ng-click="vm.clickUserGroup(group)" ng-class="{'umb-list-item--selected': group.selected}" href="" ng-repeat="group in vm.userGroups | filter:vm.filter">
<div style="margin-right: 25px;">
<div class="umb-list-checkbox" ng-class="{'umb-list-checkbox--visible': vm.selection.length > 0}" ng-click="vm.selectUserGroup(group, vm.selection, $event)" >
<umb-checkmark checked="group.selected"></umb-checkmark>
+272 -194
View File
@@ -80,13 +80,17 @@
<key alias="domainExists">Domain '%0%' has already been assigned</key>
<key alias="domainUpdated">Domain '%0%' has been updated</key>
<key alias="orEdit">Edit Current Domains</key>
<key alias="domainHelp"><![CDATA[Valid domain names are: "example.com", "www.example.com", "example.com:8080" or
<key alias="domainHelp">
<![CDATA[Valid domain names are: "example.com", "www.example.com", "example.com:8080" or
"https://www.example.com/". One-level paths in domains are supported, eg. "example.com/en". However, they
should be avoided. Better use the culture setting above.]]></key>
should be avoided. Better use the culture setting above.]]>
</key>
<key alias="inherit">Inherit</key>
<key alias="setLanguage">Culture</key>
<key alias="setLanguageHelp"><![CDATA[Set the culture for nodes below the current node,<br /> or inherit culture from parent nodes. Will also apply<br />
to the current node, unless a domain below applies too.]]></key>
<key alias="setLanguageHelp">
<![CDATA[Set the culture for nodes below the current node,<br /> or inherit culture from parent nodes. Will also apply<br />
to the current node, unless a domain below applies too.]]>
</key>
<key alias="setDomains">Domains</key>
</area>
<area alias="auditTrails">
@@ -216,7 +220,7 @@
<area alias="media">
<key alias="clickToUpload">Click to upload</key>
<key alias="dropFilesHere">Drop your files here...</key>
<key alias="urls">Link to media</key>
<key alias="urls">Link to media</key>
<key alias="orClickHereToUpload">or click here to choose files</key>
<key alias="onlyAllowedFiles">Only allowed file types are</key>
<key alias="maxFileSize">Max file size is</key>
@@ -325,10 +329,14 @@
<key alias="siterepublishHelp">The website cache will be refreshed. All published content will be updated, while unpublished content will stay unpublished.</key>
<key alias="tableColumns">Number of columns</key>
<key alias="tableRows">Number of rows</key>
<key alias="templateContentAreaHelp"><![CDATA[<strong>Set a placeholder id</strong> by setting an ID on your placeholder you can inject content into this template from child templates,
by referring this ID using a <code>&lt;asp:content /&gt;</code> element.]]></key>
<key alias="templateContentPlaceHolderHelp"><![CDATA[<strong>Select a placeholder id</strong> from the list below. You can only
choose Id's from the current template's master.]]></key>
<key alias="templateContentAreaHelp">
<![CDATA[<strong>Set a placeholder id</strong> by setting an ID on your placeholder you can inject content into this template from child templates,
by referring this ID using a <code>&lt;asp:content /&gt;</code> element.]]>
</key>
<key alias="templateContentPlaceHolderHelp">
<![CDATA[<strong>Select a placeholder id</strong> from the list below. You can only
choose Id's from the current template's master.]]>
</key>
<key alias="thumbnailimageclickfororiginal">Click on the image to see full size</key>
<key alias="treepicker">Pick item</key>
<key alias="viewCacheItem">View Cache Item</key>
@@ -362,9 +370,11 @@
<key alias="selectSnippet">Select snippet</key>
</area>
<area alias="dictionaryItem">
<key alias="description"><![CDATA[
<key alias="description">
<![CDATA[
Edit the different language versions for the dictionary item '<em>%0%</em>' below<br/>You can add additional languages under the 'languages' in the menu on the left
]]></key>
]]>
</key>
<key alias="displayName">Culture Name</key>
<key alias="changeKey">Edit the key of the dictionary item.</key>
<key alias="changeKeyError">
@@ -597,32 +607,32 @@
</area>
<area alias="shortcuts">
<key alias="addTab">Add tab</key>
<key alias="addProperty">Add property</key>
<key alias="addEditor">Add editor</key>
<key alias="addTemplate">Add template</key>
<key alias="addChildNode">Add child node</key>
<key alias="addChild">Add child</key>
<key alias="addTab">Add tab</key>
<key alias="addProperty">Add property</key>
<key alias="addEditor">Add editor</key>
<key alias="addTemplate">Add template</key>
<key alias="addChildNode">Add child node</key>
<key alias="addChild">Add child</key>
<key alias="editDataType">Edit data type</key>
<key alias="editDataType">Edit data type</key>
<key alias="navigateSections">Navigate sections</key>
<key alias="navigateSections">Navigate sections</key>
<key alias="shortcut">Shortcuts</key>
<key alias="showShortcuts">show shortcuts</key>
<key alias="shortcut">Shortcuts</key>
<key alias="showShortcuts">show shortcuts</key>
<key alias="toggleListView">Toggle list view</key>
<key alias="toggleAllowAsRoot">Toggle allow as root</key>
<key alias="toggleListView">Toggle list view</key>
<key alias="toggleAllowAsRoot">Toggle allow as root</key>
<key alias="commentLine">Comment/Uncomment lines</key>
<key alias="removeLine">Remove line</key>
<key alias="copyLineUp">Copy Lines Up</key>
<key alias="copyLineDown">Copy Lines Down</key>
<key alias="moveLineUp">Move Lines Up</key>
<key alias="moveLineDown">Move Lines Down</key>
<key alias="generalHeader">General</key>
<key alias="editorHeader">Editor</key>
<key alias="commentLine">Comment/Uncomment lines</key>
<key alias="removeLine">Remove line</key>
<key alias="copyLineUp">Copy Lines Up</key>
<key alias="copyLineDown">Copy Lines Down</key>
<key alias="moveLineUp">Move Lines Up</key>
<key alias="moveLineDown">Move Lines Down</key>
<key alias="generalHeader">General</key>
<key alias="editorHeader">Editor</key>
</area>
<area alias="graphicheadline">
@@ -641,34 +651,45 @@
<key alias="databaseErrorWebConfig">Could not save the web.config file. Please modify the connection string manually.</key>
<key alias="databaseFound">Your database has been found and is identified as</key>
<key alias="databaseHeader">Database configuration</key>
<key alias="databaseInstall"><![CDATA[
<key alias="databaseInstall">
<![CDATA[
Press the <strong>install</strong> button to install the Umbraco %0% database
]]></key>
]]>
</key>
<key alias="databaseInstallDone"><![CDATA[Umbraco %0% has now been copied to your database. Press <strong>Next</strong> to proceed.]]></key>
<key alias="databaseNotFound"><![CDATA[<p>Database not found! Please check that the information in the "connection string" of the "web.config" file is correct.</p>
<key alias="databaseNotFound">
<![CDATA[<p>Database not found! Please check that the information in the "connection string" of the "web.config" file is correct.</p>
<p>To proceed, please edit the "web.config" file (using Visual Studio or your favourite text editor), scroll to the bottom, add the connection string for your database in the key named "UmbracoDbDSN" and save the file. </p>
<p>
Click the <strong>retry</strong> button when
done.<br /><a href="http://our.umbraco.org/documentation/Using-Umbraco/Config-files/webconfig7" target="_blank">
More information on editing web.config here.</a></p>]]></key>
<key alias="databaseText"><![CDATA[To complete this step, you must know some information regarding your database server ("connection string").<br />
More information on editing web.config here.</a></p>]]>
</key>
<key alias="databaseText">
<![CDATA[To complete this step, you must know some information regarding your database server ("connection string").<br />
Please contact your ISP if necessary.
If you're installing on a local machine or server you might need information from your system administrator.]]></key>
<key alias="databaseUpgrade"><![CDATA[
If you're installing on a local machine or server you might need information from your system administrator.]]>
</key>
<key alias="databaseUpgrade">
<![CDATA[
<p>
Press the <strong>upgrade</strong> button to upgrade your database to Umbraco %0%</p>
<p>
Don't worry - no content will be deleted and everything will continue working afterwards!
</p>
]]></key>
<key alias="databaseUpgradeDone"><![CDATA[Your database has been upgraded to the final version %0%.<br />Press <strong>Next</strong> to
proceed. ]]></key>
]]>
</key>
<key alias="databaseUpgradeDone">
<![CDATA[Your database has been upgraded to the final version %0%.<br />Press <strong>Next</strong> to
proceed. ]]>
</key>
<key alias="databaseUpToDate"><![CDATA[Your current database is up-to-date!. Click <strong>next</strong> to continue the configuration wizard]]></key>
<key alias="defaultUserChangePass"><![CDATA[<strong>The Default users' password needs to be changed!</strong>]]></key>
<key alias="defaultUserDisabled"><![CDATA[<strong>The Default user has been disabled or has no access to Umbraco!</strong></p><p>No further actions needs to be taken. Click <b>Next</b> to proceed.]]></key>
<key alias="defaultUserPassChanged"><![CDATA[<strong>The Default user's password has been successfully changed since the installation!</strong></p><p>No further actions needs to be taken. Click <strong>Next</strong> to proceed.]]></key>
<key alias="defaultUserPasswordChanged">The password is changed!</key>
<key alias="defaultUserText"><![CDATA[
<key alias="defaultUserText">
<![CDATA[
<p>
Umbraco creates a default user with a login <strong>('admin')</strong> and password <strong>('default')</strong>. It's <strong>important</strong> that the password is
changed to something unique.
@@ -676,48 +697,64 @@
<p>
This step will check the default user's password and suggest if it needs to be changed.
</p>
]]></key>
]]>
</key>
<key alias="greatStart">Get a great start, watch our introduction videos</key>
<key alias="licenseText">By clicking the next button (or modifying the umbracoConfigurationStatus in web.config), you accept the license for this software as specified in the box below. Notice that this Umbraco distribution consists of two different licenses, the open source MIT license for the framework and the Umbraco freeware license that covers the UI.</key>
<key alias="None">Not installed yet.</key>
<key alias="permissionsAffectedFolders">Affected files and folders</key>
<key alias="permissionsAffectedFoldersMoreInfo">More information on setting up permissions for Umbraco here</key>
<key alias="permissionsAffectedFoldersText">You need to grant ASP.NET modify permissions to the following files/folders</key>
<key alias="permissionsAlmostPerfect"><![CDATA[<strong>Your permission settings are almost perfect!</strong><br /><br />
You can run Umbraco without problems, but you will not be able to install packages which are recommended to take full advantage of Umbraco.]]></key>
<key alias="permissionsAlmostPerfect">
<![CDATA[<strong>Your permission settings are almost perfect!</strong><br /><br />
You can run Umbraco without problems, but you will not be able to install packages which are recommended to take full advantage of Umbraco.]]>
</key>
<key alias="permissionsHowtoResolve">How to Resolve</key>
<key alias="permissionsHowtoResolveLink">Click here to read the text version</key>
<key alias="permissionsHowtoResolveText"><![CDATA[Watch our <strong>video tutorial</strong> on setting up folder permissions for Umbraco or read the text version.]]></key>
<key alias="permissionsMaybeAnIssue"><![CDATA[<strong>Your permission settings might be an issue!</strong>
<key alias="permissionsMaybeAnIssue">
<![CDATA[<strong>Your permission settings might be an issue!</strong>
<br/><br />
You can run Umbraco without problems, but you will not be able to create folders or install packages which are recommended to take full advantage of Umbraco.]]></key>
<key alias="permissionsNotReady"><![CDATA[<strong>Your permission settings are not ready for Umbraco!</strong>
You can run Umbraco without problems, but you will not be able to create folders or install packages which are recommended to take full advantage of Umbraco.]]>
</key>
<key alias="permissionsNotReady">
<![CDATA[<strong>Your permission settings are not ready for Umbraco!</strong>
<br /><br />
In order to run Umbraco, you'll need to update your permission settings.]]></key>
<key alias="permissionsPerfect"><![CDATA[<strong>Your permission settings are perfect!</strong><br /><br />
You are ready to run Umbraco and install packages!]]></key>
In order to run Umbraco, you'll need to update your permission settings.]]>
</key>
<key alias="permissionsPerfect">
<![CDATA[<strong>Your permission settings are perfect!</strong><br /><br />
You are ready to run Umbraco and install packages!]]>
</key>
<key alias="permissionsResolveFolderIssues">Resolving folder issue</key>
<key alias="permissionsResolveFolderIssuesLink">Follow this link for more information on problems with ASP.NET and creating folders</key>
<key alias="permissionsSettingUpPermissions">Setting up folder permissions</key>
<key alias="permissionsText"><![CDATA[
<key alias="permissionsText">
<![CDATA[
Umbraco needs write/modify access to certain directories in order to store files like pictures and PDF's.
It also stores temporary data (aka: cache) for enhancing the performance of your website.
]]></key>
]]>
</key>
<key alias="runwayFromScratch">I want to start from scratch</key>
<key alias="runwayFromScratchText"><![CDATA[
<key alias="runwayFromScratchText">
<![CDATA[
Your website is completely empty at the moment, so that's perfect if you want to start from scratch and create your own document types and templates.
(<a href="http://Umbraco.tv/documentation/videos/for-site-builders/foundation/document-types">learn how</a>)
You can still choose to install Runway later on. Please go to the Developer section and choose Packages.
]]></key>
]]>
</key>
<key alias="runwayHeader">You've just set up a clean Umbraco platform. What do you want to do next?</key>
<key alias="runwayInstalled">Runway is installed</key>
<key alias="runwayInstalledText"><![CDATA[
<key alias="runwayInstalledText">
<![CDATA[
You have the foundation in place. Select what modules you wish to install on top of it.<br />
This is our list of recommended modules, check off the ones you would like to install, or view the <a href="#" onclick="toggleModules(); return false;" id="toggleModuleList">full list of modules</a>
]]></key>
]]>
</key>
<key alias="runwayOnlyProUsers">Only recommended for experienced users</key>
<key alias="runwaySimpleSite">I want to start with a simple website</key>
<key alias="runwaySimpleSiteText"><![CDATA[
<key alias="runwaySimpleSiteText">
<![CDATA[
<p>
"Runway" is a simple website providing some basic document types and templates. The installer can set up Runway for you automatically,
but you can easily edit, extend or remove it. It's not necessary and you can perfectly use Umbraco without it. However,
@@ -728,7 +765,8 @@
<em>Included with Runway:</em> Home page, Getting Started page, Installing Modules page.<br />
<em>Optional Modules:</em> Top Navigation, Sitemap, Contact, Gallery.
</small>
]]></key>
]]>
</key>
<key alias="runwayWhatIsRunway">What is Runway</key>
<key alias="step1">Step 1/5 Accept license</key>
<key alias="step2">Step 2/5: Database configuration</key>
@@ -736,24 +774,36 @@
<key alias="step4">Step 4/5: Check Umbraco security</key>
<key alias="step5">Step 5/5: Umbraco is ready to get you started</key>
<key alias="thankYou">Thank you for choosing Umbraco</key>
<key alias="theEndBrowseSite"><![CDATA[<h3>Browse your new site</h3>
You installed Runway, so why not see how your new website looks.]]></key>
<key alias="theEndFurtherHelp"><![CDATA[<h3>Further help and information</h3>
Get help from our award winning community, browse the documentation or watch some free videos on how to build a simple site, how to use packages and a quick guide to the Umbraco terminology]]></key>
<key alias="theEndBrowseSite">
<![CDATA[<h3>Browse your new site</h3>
You installed Runway, so why not see how your new website looks.]]>
</key>
<key alias="theEndFurtherHelp">
<![CDATA[<h3>Further help and information</h3>
Get help from our award winning community, browse the documentation or watch some free videos on how to build a simple site, how to use packages and a quick guide to the Umbraco terminology]]>
</key>
<key alias="theEndHeader">Umbraco %0% is installed and ready for use</key>
<key alias="theEndInstallFailed"><![CDATA[To finish the installation, you'll need to
manually edit the <strong>/web.config file</strong> and update the AppSetting key <strong>UmbracoConfigurationStatus</strong> in the bottom to the value of <strong>'%0%'</strong>.]]></key>
<key alias="theEndInstallSuccess"><![CDATA[You can get <strong>started instantly</strong> by clicking the "Launch Umbraco" button below. <br />If you are <strong>new to Umbraco</strong>,
you can find plenty of resources on our getting started pages.]]></key>
<key alias="theEndOpenUmbraco"><![CDATA[<h3>Launch Umbraco</h3>
To manage your website, simply open the Umbraco back office and start adding content, updating the templates and stylesheets or add new functionality]]></key>
<key alias="theEndInstallFailed">
<![CDATA[To finish the installation, you'll need to
manually edit the <strong>/web.config file</strong> and update the AppSetting key <strong>UmbracoConfigurationStatus</strong> in the bottom to the value of <strong>'%0%'</strong>.]]>
</key>
<key alias="theEndInstallSuccess">
<![CDATA[You can get <strong>started instantly</strong> by clicking the "Launch Umbraco" button below. <br />If you are <strong>new to Umbraco</strong>,
you can find plenty of resources on our getting started pages.]]>
</key>
<key alias="theEndOpenUmbraco">
<![CDATA[<h3>Launch Umbraco</h3>
To manage your website, simply open the Umbraco back office and start adding content, updating the templates and stylesheets or add new functionality]]>
</key>
<key alias="Unavailable">Connection to database failed.</key>
<key alias="Version3">Umbraco Version 3</key>
<key alias="Version4">Umbraco Version 4</key>
<key alias="watch">Watch</key>
<key alias="welcomeIntro"><![CDATA[This wizard will guide you through the process of configuring <strong>Umbraco %0%</strong> for a fresh install or upgrading from version 3.0.
<key alias="welcomeIntro">
<![CDATA[This wizard will guide you through the process of configuring <strong>Umbraco %0%</strong> for a fresh install or upgrading from version 3.0.
<br /><br />
Press <strong>"next"</strong> to start the wizard.]]></key>
Press <strong>"next"</strong> to start the wizard.]]>
</key>
</area>
<area alias="language">
<key alias="cultureCode">Culture Code</key>
@@ -808,7 +858,8 @@ To manage your website, simply open the Umbraco back office and start adding con
</area>
<area alias="notifications">
<key alias="editNotifications">Edit your notification for %0%</key>
<key alias="mailBody"><![CDATA[
<key alias="mailBody">
<![CDATA[
Hi %0%
This is an automated mail to inform you that the task '%1%'
@@ -820,8 +871,10 @@ To manage your website, simply open the Umbraco back office and start adding con
Have a nice day!
Cheers from the Umbraco robot
]]></key>
<key alias="mailBodyHtml"><![CDATA[<p>Hi %0%</p>
]]>
</key>
<key alias="mailBodyHtml">
<![CDATA[<p>Hi %0%</p>
<p>This is an automated mail to inform you that the task <strong>'%1%'</strong>
has been performed on the page <a href="http://%4%/#/content/content/edit/%5%"><strong>'%2%'</strong></a>
@@ -847,15 +900,18 @@ To manage your website, simply open the Umbraco back office and start adding con
<p>Have a nice day!<br /><br />
Cheers from the Umbraco robot
</p>]]></key>
</p>]]>
</key>
<key alias="mailSubject">[%0%] Notification about %1% performed on %2%</key>
<key alias="notifications">Notifications</key>
</area>
<area alias="packager">
<key alias="chooseLocalPackageText"><![CDATA[
<key alias="chooseLocalPackageText">
<![CDATA[
Choose Package from your machine, by clicking the Browse<br />
button and locating the package. Umbraco packages usually have a ".zip" extension.
]]></key>
]]>
</key>
<key alias="dropHere">Drop to upload</key>
<key alias="orClickHereToUpload">or click here to choose files</key>
<key alias="uploadPackage">Upload package</key>
@@ -895,8 +951,10 @@ To manage your website, simply open the Umbraco back office and start adding con
<key alias="packageMetaData">Package meta data</key>
<key alias="packageName">Package name</key>
<key alias="packageNoItemsHeader">Package doesn't contain any items</key>
<key alias="packageNoItemsText"><![CDATA[This package file doesn't contain any items to uninstall.<br/><br/>
You can safely remove this from the system by clicking "uninstall package" below.]]></key>
<key alias="packageNoItemsText">
<![CDATA[This package file doesn't contain any items to uninstall.<br/><br/>
You can safely remove this from the system by clicking "uninstall package" below.]]>
</key>
<key alias="packageNoUpgrades">No upgrades available</key>
<key alias="packageOptions">Package options</key>
<key alias="packageReadme">Package readme</key>
@@ -905,9 +963,11 @@ To manage your website, simply open the Umbraco back office and start adding con
<key alias="packageUninstalledHeader">Package was uninstalled</key>
<key alias="packageUninstalledText">The package was successfully uninstalled</key>
<key alias="packageUninstallHeader">Uninstall package</key>
<key alias="packageUninstallText"><![CDATA[You can unselect items you do not wish to remove, at this time, below. When you click "confirm uninstall" all checked-off items will be removed.<br />
<key alias="packageUninstallText">
<![CDATA[You can unselect items you do not wish to remove, at this time, below. When you click "confirm uninstall" all checked-off items will be removed.<br />
<span style="color: Red; font-weight: bold;">Notice:</span> any documents, media etc depending on the items you remove, will stop working, and could lead to system instability,
so uninstall with caution. If in doubt, contact the package author.]]></key>
so uninstall with caution. If in doubt, contact the package author.]]>
</key>
<key alias="packageUpgradeDownload">Download update from the repository</key>
<key alias="packageUpgradeHeader">Upgrade package</key>
<key alias="packageUpgradeInstructions">Upgrade instructions</key>
@@ -956,27 +1016,37 @@ To manage your website, simply open the Umbraco back office and start adding con
%0% could not be published because the item is scheduled for release.
]]>
</key>
<key alias="contentPublishedFailedExpired"><![CDATA[
<key alias="contentPublishedFailedExpired">
<![CDATA[
%0% could not be published because the item has expired.
]]></key>
<key alias="contentPublishedFailedInvalid"><![CDATA[
]]>
</key>
<key alias="contentPublishedFailedInvalid">
<![CDATA[
%0% could not be published because these properties: %1% did not pass validation rules.
]]></key>
<key alias="contentPublishedFailedByEvent"><![CDATA[
]]>
</key>
<key alias="contentPublishedFailedByEvent">
<![CDATA[
%0% could not be published, a 3rd party add-in cancelled the action.
]]></key>
<key alias="contentPublishedFailedByParent"><![CDATA[
]]>
</key>
<key alias="contentPublishedFailedByParent">
<![CDATA[
%0% can not be published, because a parent page is not published.
]]></key>
]]>
</key>
<key alias="includeUnpublished">Include unpublished subpages</key>
<key alias="inProgress">Publishing in progress - please wait...</key>
<key alias="inProgressCounter">%0% out of %1% pages have been published...</key>
<key alias="nodePublish">%0% has been published</key>
<key alias="nodePublishAll">%0% and subpages have been published</key>
<key alias="publishAll">Publish %0% and all its subpages</key>
<key alias="publishHelp"><![CDATA[Click <em>Publish</em> to publish <strong>%0%</strong> and thereby making its content publicly available.<br/><br />
<key alias="publishHelp">
<![CDATA[Click <em>Publish</em> to publish <strong>%0%</strong> and thereby making its content publicly available.<br/><br />
You can publish this page and all its subpages by checking <em>Include unpublished subpages</em> below.
]]></key>
]]>
</key>
</area>
<area alias="colorpicker">
<key alias="noColors">You have not configured any approved colours</key>
@@ -1061,8 +1131,8 @@ To manage your website, simply open the Umbraco back office and start adding con
<key alias="sortPleaseWait"><![CDATA[ Please wait. Items are being sorted, this can take a while.<br/> <br/> Do not close this window during sorting]]></key>
</area>
<area alias="speechBubbles">
<key alias="validationFailedHeader">Validation</key>
<key alias="validationFailedMessage">Validation errors must be fixed before the item can be saved</key>
<key alias="validationFailedHeader">Validation</key>
<key alias="validationFailedMessage">Validation errors must be fixed before the item can be saved</key>
<key alias="operationFailedHeader">Failed</key>
<key alias="operationSavedHeader">Saved</key>
<key alias="invalidUserPermissionsText">Insufficient user permissions, could not complete the operation</key>
@@ -1149,7 +1219,7 @@ To manage your website, simply open the Umbraco back office and start adding con
<key alias="preview">Preview</key>
<key alias="styles">Styles</key>
</area>
<area alias="template">
<key alias="edittemplate">Edit template</key>
@@ -1159,10 +1229,10 @@ To manage your website, simply open the Umbraco back office and start adding con
<key alias="insert">Insert</key>
<key alias="insertDesc">Choose what to insert into your template</key>
<key alias="insertDictionaryItem">Dictionary item</key>
<key alias="insertDictionaryItemDesc">A dictionary item is a placeholder for a translatable piece of text, which makes it easy to create designs for multilingual websites.</key>
<key alias="insertMacro">Macro</key>
<key alias="insertMacroDesc">
A Macro is a configurable component which is great for
@@ -1172,13 +1242,13 @@ To manage your website, simply open the Umbraco back office and start adding con
<key alias="insertPageField">Value</key>
<key alias="insertPageFieldDesc">Displays the value of a named field from the current page, with options to modify the value or fallback to alternative values.</key>
<key alias="insertPartialView">Partial view</key>
<key alias="insertPartialViewDesc">
A partial view is a separate template file which can be rendered inside another
template, it's great for reusing markup or for separating complex templates into separate files.
</key>
<key alias="mastertemplate">Master template</key>
<key alias="noMastertemplate">No master template</key>
<key alias="noMaster">No master</key>
@@ -1190,8 +1260,8 @@ To manage your website, simply open the Umbraco back office and start adding con
<code>@RenderBody()</code> placeholder.
]]>
</key>
<key alias="defineSection">Define a named section</key>
<key alias="defineSectionDesc">
<![CDATA[
@@ -1214,20 +1284,20 @@ To manage your website, simply open the Umbraco back office and start adding con
<key alias="sectionMandatoryDesc">
If mandatory, the child template must contain a <code>@section</code> definition, otherwise an error is shown.
</key>
<key alias="queryBuilder">Query builder</key>
<key alias="buildQuery">Build a query</key>
<key alias="itemsReturned">items returned, in</key>
<key alias="iWant">I want</key>
<key alias="iWant">I want</key>
<key alias="allContent">all content</key>
<key alias="contentOfType">content of type &quot;%0%&quot;</key>
<key alias="from">from</key>
<key alias="websiteRoot">my website</key>
<key alias="where">where</key>
<key alias="and">and</key>
<key alias="is">is</key>
<key alias="isNot">is not</key>
<key alias="before">before</key>
@@ -1250,12 +1320,12 @@ To manage your website, simply open the Umbraco back office and start adding con
<key alias="orderBy">order by</key>
<key alias="ascending">ascending</key>
<key alias="descending">descending</key>
<key alias="descending">descending</key>
<key alias="template">Template</key>
</area>
<area alias="grid">
<key alias="insertControl">Choose type of content</key>
<key alias="chooseLayout">Choose a layout</key>
@@ -1302,63 +1372,63 @@ To manage your website, simply open the Umbraco back office and start adding con
<area alias="contentTypeEditor">
<key alias="compositions">Compositions</key>
<key alias="noTabs">You have not added any tabs</key>
<key alias="addNewTab">Add new tab</key>
<key alias="addAnotherTab">Add another tab</key>
<key alias="inheritedFrom">Inherited from</key>
<key alias="addProperty">Add property</key>
<key alias="requiredLabel">Required label</key>
<key alias="compositions">Compositions</key>
<key alias="noTabs">You have not added any tabs</key>
<key alias="addNewTab">Add new tab</key>
<key alias="addAnotherTab">Add another tab</key>
<key alias="inheritedFrom">Inherited from</key>
<key alias="addProperty">Add property</key>
<key alias="requiredLabel">Required label</key>
<key alias="enableListViewHeading">Enable list view</key>
<key alias="enableListViewDescription">Configures the content item to show a sortable and searchable list of its children, the children will not be shown in the tree</key>
<key alias="enableListViewHeading">Enable list view</key>
<key alias="enableListViewDescription">Configures the content item to show a sortable and searchable list of its children, the children will not be shown in the tree</key>
<key alias="allowedTemplatesHeading">Allowed Templates</key>
<key alias="allowedTemplatesDescription">Choose which templates editors are allowed to use on content of this type</key>
<key alias="allowedTemplatesHeading">Allowed Templates</key>
<key alias="allowedTemplatesDescription">Choose which templates editors are allowed to use on content of this type</key>
<key alias="allowAsRootHeading">Allow as root</key>
<key alias="allowAsRootDescription">Allow editors to create content of this type in the root of the content tree</key>
<key alias="allowAsRootCheckbox">Yes - allow content of this type in the root</key>
<key alias="allowAsRootHeading">Allow as root</key>
<key alias="allowAsRootDescription">Allow editors to create content of this type in the root of the content tree</key>
<key alias="allowAsRootCheckbox">Yes - allow content of this type in the root</key>
<key alias="childNodesHeading">Allowed child node types</key>
<key alias="childNodesDescription">Allow content of the specified types to be created underneath content of this type</key>
<key alias="childNodesHeading">Allowed child node types</key>
<key alias="childNodesDescription">Allow content of the specified types to be created underneath content of this type</key>
<key alias="chooseChildNode">Choose child node</key>
<key alias="chooseChildNode">Choose child node</key>
<key alias="compositionsDescription">Inherit tabs and properties from an existing document type. New tabs will be added to the current document type or merged if a tab with an identical name exists.</key>
<key alias="compositionInUse">This content type is used in a composition, and therefore cannot be composed itself.</key>
<key alias="noAvailableCompositions">There are no content types available to use as a composition.</key>
<key alias="compositionsDescription">Inherit tabs and properties from an existing document type. New tabs will be added to the current document type or merged if a tab with an identical name exists.</key>
<key alias="compositionInUse">This content type is used in a composition, and therefore cannot be composed itself.</key>
<key alias="noAvailableCompositions">There are no content types available to use as a composition.</key>
<key alias="availableEditors">Available editors</key>
<key alias="reuse">Reuse</key>
<key alias="editorSettings">Editor settings</key>
<key alias="availableEditors">Available editors</key>
<key alias="reuse">Reuse</key>
<key alias="editorSettings">Editor settings</key>
<key alias="configuration">Configuration</key>
<key alias="configuration">Configuration</key>
<key alias="yesDelete">Yes, delete</key>
<key alias="yesDelete">Yes, delete</key>
<key alias="movedUnderneath">was moved underneath</key>
<key alias="copiedUnderneath">was copied underneath</key>
<key alias="folderToMove">Select the folder to move</key>
<key alias="folderToCopy">Select the folder to copy</key>
<key alias="structureBelow">to in the tree structure below</key>
<key alias="movedUnderneath">was moved underneath</key>
<key alias="copiedUnderneath">was copied underneath</key>
<key alias="folderToMove">Select the folder to move</key>
<key alias="folderToCopy">Select the folder to copy</key>
<key alias="structureBelow">to in the tree structure below</key>
<key alias="allDocumentTypes">All Document types</key>
<key alias="allDocuments">All Documents</key>
<key alias="allMediaItems">All media items</key>
<key alias="allDocumentTypes">All Document types</key>
<key alias="allDocuments">All Documents</key>
<key alias="allMediaItems">All media items</key>
<key alias="usingThisDocument">using this document type will be deleted permanently, please confirm you want to delete these as well.</key>
<key alias="usingThisMedia">using this media type will be deleted permanently, please confirm you want to delete these as well.</key>
<key alias="usingThisMember">using this member type will be deleted permanently, please confirm you want to delete these as well</key>
<key alias="usingThisDocument">using this document type will be deleted permanently, please confirm you want to delete these as well.</key>
<key alias="usingThisMedia">using this media type will be deleted permanently, please confirm you want to delete these as well.</key>
<key alias="usingThisMember">using this member type will be deleted permanently, please confirm you want to delete these as well</key>
<key alias="andAllDocuments">and all documents using this type</key>
<key alias="andAllMediaItems">and all media items using this type</key>
<key alias="andAllMembers">and all members using this type</key>
<key alias="andAllDocuments">and all documents using this type</key>
<key alias="andAllMediaItems">and all media items using this type</key>
<key alias="andAllMembers">and all members using this type</key>
<key alias="thisEditorUpdateSettings">using this editor will get updated with the new settings</key>
<key alias="thisEditorUpdateSettings">using this editor will get updated with the new settings</key>
<key alias="memberCanEdit">Member can edit</key>
<key alias="showOnMemberProfile">Show on member profile</key>
<key alias="memberCanEdit">Member can edit</key>
<key alias="showOnMemberProfile">Show on member profile</key>
</area>
@@ -1403,10 +1473,12 @@ To manage your website, simply open the Umbraco back office and start adding con
</area>
<area alias="translation">
<key alias="assignedTasks">Tasks assigned to you</key>
<key alias="assignedTasksHelp"><![CDATA[ The list below shows translation tasks <strong>assigned to you</strong>. To see a detailed view including comments, click on "Details" or just the page name.
<key alias="assignedTasksHelp">
<![CDATA[ The list below shows translation tasks <strong>assigned to you</strong>. To see a detailed view including comments, click on "Details" or just the page name.
You can also download the page as XML directly by clicking the "Download Xml" link. <br/>
To close a translation task, please go to the Details view and click the "Close" button.
]]></key>
]]>
</key>
<key alias="closeTask">close task</key>
<key alias="details">Translation details</key>
<key alias="downloadAllAsXml">Download all translation tasks as XML</key>
@@ -1414,7 +1486,8 @@ To manage your website, simply open the Umbraco back office and start adding con
<key alias="DownloadXmlDTD">Download XML DTD</key>
<key alias="fields">Fields</key>
<key alias="includeSubpages">Include subpages</key>
<key alias="mailBody"><![CDATA[
<key alias="mailBody">
<![CDATA[
Hi %0%
This is an automated mail to inform you that the document '%1%'
@@ -1428,14 +1501,17 @@ To manage your website, simply open the Umbraco back office and start adding con
Have a nice day!
Cheers from the Umbraco robot
]]></key>
]]>
</key>
<key alias="mailSubject">[%0%] Translation task for %1%</key>
<key alias="noTranslators">No translator users found. Please create a translator user before you start sending content to translation</key>
<key alias="ownedTasks">Tasks created by you</key>
<key alias="ownedTasksHelp"><![CDATA[ The list below shows pages <strong>created by you</strong>. To see a detailed view including comments,
<key alias="ownedTasksHelp">
<![CDATA[ The list below shows pages <strong>created by you</strong>. To see a detailed view including comments,
click on "Details" or just the page name. You can also download the page as XML directly by clicking the "Download Xml" link.
To close a translation task, please go to the Details view and click the "Close" button.
]]></key>
]]>
</key>
<key alias="pageHasBeenSendToTranslation">The page '%0%' has been send to translation</key>
<key alias="noLanguageSelected">Please select the language that the content should be translated into</key>
<key alias="sendToTranslate">Send the page '%0%' to translation</key>
@@ -1451,7 +1527,9 @@ To manage your website, simply open the Umbraco back office and start adding con
<key alias="uploadTranslationXml">Upload translation XML</key>
</area>
<area alias="treeHeaders">
<key alias="content">Content</key>
<key alias="contentBlueprints">Content Blueprints</key>
<key alias="media">Media</key>
<key alias="cacheBrowser">Cache Browser</key>
<key alias="contentRecycleBin">Recycle Bin</key>
<key alias="createdPackages">Created packages</key>
@@ -1563,74 +1641,74 @@ To manage your website, simply open the Umbraco back office and start adding con
<key alias="invalidEmail">Invalid email</key>
</area>
<area alias="healthcheck">
<!-- The following keys get these tokens passed in:
<!-- The following keys get these tokens passed in:
0: Current value
1: Recommended value
2: XPath
3: Configuration file path
-->
<key alias="checkSuccessMessage">Value is set to the recommended value: '%0%'.</key>
<key alias="rectifySuccessMessage">Value was set to '%1%' for XPath '%2%' in configuration file '%3%'.</key>
<key alias="rectifySuccessMessage">Value was set to '%1%' for XPath '%2%' in configuration file '%3%'.</key>
<key alias="checkErrorMessageDifferentExpectedValue">Expected value '%1%' for '%2%' in configuration file '%3%', but found '%0%'.</key>
<key alias="checkErrorMessageUnexpectedValue">Found unexpected value '%0%' for '%2%' in configuration file '%3%'.</key>
<!-- The following keys get these tokens passed in:
<!-- The following keys get these tokens passed in:
0: Current value
1: Recommended value
-->
<key alias="customErrorsCheckSuccessMessage">Custom errors are set to '%0%'.</key>
<key alias="customErrorsCheckErrorMessage">Custom errors are currently set to '%0%'. It is recommended to set this to '%1%' before go live.</key>
<key alias="customErrorsCheckRectifySuccessMessage">Custom errors successfully set to '%0%'.</key>
<key alias="customErrorsCheckSuccessMessage">Custom errors are set to '%0%'.</key>
<key alias="customErrorsCheckErrorMessage">Custom errors are currently set to '%0%'. It is recommended to set this to '%1%' before go live.</key>
<key alias="customErrorsCheckRectifySuccessMessage">Custom errors successfully set to '%0%'.</key>
<key alias="macroErrorModeCheckSuccessMessage">MacroErrors are set to '%0%'.</key>
<key alias="macroErrorModeCheckErrorMessage">MacroErrors are set to '%0%' which will prevent some or all pages in your site from loading completely if there are any errors in macros. Rectifying this will set the value to '%1%'.</key>
<key alias="macroErrorModeCheckRectifySuccessMessage">MacroErrors are now set to '%0%'.</key>
<key alias="macroErrorModeCheckSuccessMessage">MacroErrors are set to '%0%'.</key>
<key alias="macroErrorModeCheckErrorMessage">MacroErrors are set to '%0%' which will prevent some or all pages in your site from loading completely if there are any errors in macros. Rectifying this will set the value to '%1%'.</key>
<key alias="macroErrorModeCheckRectifySuccessMessage">MacroErrors are now set to '%0%'.</key>
<!-- The following keys get these tokens passed in:
<!-- The following keys get these tokens passed in:
0: Current value
1: Recommended value
2: Server version
-->
<key alias="trySkipIisCustomErrorsCheckSuccessMessage">Try Skip IIS Custom Errors is set to '%0%' and you're using IIS version '%1%'.</key>
<key alias="trySkipIisCustomErrorsCheckErrorMessage">Try Skip IIS Custom Errors is currently '%0%'. It is recommended to set this to '%1%' for your IIS version (%2%).</key>
<key alias="trySkipIisCustomErrorsCheckRectifySuccessMessage">Try Skip IIS Custom Errors successfully set to '%0%'.</key>
<key alias="trySkipIisCustomErrorsCheckSuccessMessage">Try Skip IIS Custom Errors is set to '%0%' and you're using IIS version '%1%'.</key>
<key alias="trySkipIisCustomErrorsCheckErrorMessage">Try Skip IIS Custom Errors is currently '%0%'. It is recommended to set this to '%1%' for your IIS version (%2%).</key>
<key alias="trySkipIisCustomErrorsCheckRectifySuccessMessage">Try Skip IIS Custom Errors successfully set to '%0%'.</key>
<!-- The following keys get predefined tokens passed in that are not all the same, like above -->
<key alias="configurationServiceFileNotFound">File does not exist: '%0%'.</key>
<key alias="configurationServiceNodeNotFound"><![CDATA[Unable to find <strong>'%0%'</strong> in config file <strong>'%1%'</strong>.]]></key>
<key alias="configurationServiceError">There was an error, check log for full error: %0%.</key>
<!-- The following keys get predefined tokens passed in that are not all the same, like above -->
<key alias="configurationServiceFileNotFound">File does not exist: '%0%'.</key>
<key alias="configurationServiceNodeNotFound"><![CDATA[Unable to find <strong>'%0%'</strong> in config file <strong>'%1%'</strong>.]]></key>
<key alias="configurationServiceError">There was an error, check log for full error: %0%.</key>
<key alias="xmlDataIntegrityCheckMembers">Members - Total XML: %0%, Total: %1%, Total invalid: %2%</key>
<key alias="xmlDataIntegrityCheckMedia">Media - Total XML: %0%, Total: %1%, Total invalid: %2%</key>
<key alias="xmlDataIntegrityCheckContent">Content - Total XML: %0%, Total published: %1%, Total invalid: %2%</key>
<key alias="xmlDataIntegrityCheckMembers">Members - Total XML: %0%, Total: %1%, Total invalid: %2%</key>
<key alias="xmlDataIntegrityCheckMedia">Media - Total XML: %0%, Total: %1%, Total invalid: %2%</key>
<key alias="xmlDataIntegrityCheckContent">Content - Total XML: %0%, Total published: %1%, Total invalid: %2%</key>
<key alias="httpsCheckValidCertificate">Your website's certificate is valid.</key>
<key alias="httpsCheckInvalidCertificate">Certificate validation error: '%0%'</key>
<key alias="httpsCheckInvalidCertificate">Certificate validation error: '%0%'</key>
<key alias="httpsCheckExpiredCertificate">Your website's SSL certificate has expired.</key>
<key alias="httpsCheckExpiringCertificate">Your website's SSL certificate is expiring in %0% days.</key>
<key alias="httpsCheckInvalidUrl">Error pinging the URL %0% - '%1%'</key>
<key alias="httpsCheckIsCurrentSchemeHttps">You are currently %0% viewing the site using the HTTPS scheme.</key>
<key alias="httpsCheckConfigurationRectifyNotPossible">The appSetting 'umbracoUseSSL' is set to 'false' in your web.config file. Once you access this site using the HTTPS scheme, that should be set to 'true'.</key>
<key alias="httpsCheckConfigurationCheckResult">The appSetting 'umbracoUseSSL' is set to '%0%' in your web.config file, your cookies are %1% marked as secure.</key>
<key alias="httpsCheckEnableHttpsError">Could not update the 'umbracoUseSSL' setting in your web.config file. Error: %0%</key>
<key alias="httpsCheckInvalidUrl">Error pinging the URL %0% - '%1%'</key>
<key alias="httpsCheckIsCurrentSchemeHttps">You are currently %0% viewing the site using the HTTPS scheme.</key>
<key alias="httpsCheckConfigurationRectifyNotPossible">The appSetting 'umbracoUseSSL' is set to 'false' in your web.config file. Once you access this site using the HTTPS scheme, that should be set to 'true'.</key>
<key alias="httpsCheckConfigurationCheckResult">The appSetting 'umbracoUseSSL' is set to '%0%' in your web.config file, your cookies are %1% marked as secure.</key>
<key alias="httpsCheckEnableHttpsError">Could not update the 'umbracoUseSSL' setting in your web.config file. Error: %0%</key>
<!-- The following keys don't get tokens passed in -->
<key alias="httpsCheckEnableHttpsButton">Enable HTTPS</key>
<key alias="httpsCheckEnableHttpsDescription">Sets umbracoSSL setting to true in the appSettings of the web.config file.</key>
<key alias="httpsCheckEnableHttpsSuccess">The appSetting 'umbracoUseSSL' is now set to 'true' in your web.config file, your cookies will be marked as secure.</key>
<!-- The following keys don't get tokens passed in -->
<key alias="httpsCheckEnableHttpsButton">Enable HTTPS</key>
<key alias="httpsCheckEnableHttpsDescription">Sets umbracoSSL setting to true in the appSettings of the web.config file.</key>
<key alias="httpsCheckEnableHttpsSuccess">The appSetting 'umbracoUseSSL' is now set to 'true' in your web.config file, your cookies will be marked as secure.</key>
<key alias="rectifyButton">Fix</key>
<key alias="rectifyButton">Fix</key>
<key alias="cannotRectifyShouldNotEqual">Cannot fix a check with a value comparison type of 'ShouldNotEqual'.</key>
<key alias="cannotRectifyShouldEqualWithValue">Cannot fix a check with a value comparison type of 'ShouldEqual' with a provided value.</key>
<key alias="valueToRectifyNotProvided">Value to fix check not provided.</key>
<key alias="compilationDebugCheckSuccessMessage">Debug compilation mode is disabled.</key>
<key alias="compilationDebugCheckErrorMessage">Debug compilation mode is currently enabled. It is recommended to disable this setting before go live.</key>
<key alias="compilationDebugCheckRectifySuccessMessage">Debug compilation mode successfully disabled.</key>
<key alias="compilationDebugCheckSuccessMessage">Debug compilation mode is disabled.</key>
<key alias="compilationDebugCheckErrorMessage">Debug compilation mode is currently enabled. It is recommended to disable this setting before go live.</key>
<key alias="compilationDebugCheckRectifySuccessMessage">Debug compilation mode successfully disabled.</key>
<key alias="traceModeCheckSuccessMessage">Trace mode is disabled.</key>
<key alias="traceModeCheckErrorMessage">Trace mode is currently enabled. It is recommended to disable this setting before go live.</key>
<key alias="traceModeCheckRectifySuccessMessage">Trace mode successfully disabled.</key>
<key alias="traceModeCheckSuccessMessage">Trace mode is disabled.</key>
<key alias="traceModeCheckErrorMessage">Trace mode is currently enabled. It is recommended to disable this setting before go live.</key>
<key alias="traceModeCheckRectifySuccessMessage">Trace mode successfully disabled.</key>
<key alias="folderPermissionsCheckMessage">All folders have the correct permissions set.</key>
<!-- The following keys get these tokens passed in:
@@ -1456,7 +1456,9 @@ To manage your website, simply open the Umbraco back office and start adding con
<key alias="uploadTranslationXml">Upload translation XML</key>
</area>
<area alias="treeHeaders">
<key alias="content">Content</key>
<key alias="contentBlueprints">Content Blueprints</key>
<key alias="media">Media</key>
<key alias="cacheBrowser">Cache Browser</key>
<key alias="contentRecycleBin">Recycle Bin</key>
<key alias="createdPackages">Created packages</key>
@@ -1500,6 +1502,7 @@ To manage your website, simply open the Umbraco back office and start adding con
<key alias="administrators">Administrator</key>
<key alias="categoryField">Category field</key>
<key alias="changePassword">Change Your Password</key>
<key alias="changePhoto">Change photo</key>
<key alias="newPassword">New password</key>
<key alias="confirmNewPassword">Confirm new password</key>
<key alias="changePasswordDescription">You can change your password for accessing the Umbraco Back Office by filling out the form below and click the 'Change Password' button</key>
@@ -1539,6 +1542,7 @@ To manage your website, simply open the Umbraco back office and start adding con
<key alias="permissionReplaceChildren">Replace child node permissions</key>
<key alias="permissionSelectedPages">You are currently modifying permissions for the pages:</key>
<key alias="permissionSelectPages">Select pages to modify their permissions</key>
<key alias="removePhoto">Remove photo</key>
<key alias="permissionsDefault">Default permissions</key>
<key alias="permissionsGranular">Granular permissions</key>
<key alias="permissionsGranularHelp">Set permissions for specific nodes</key>
+1 -1
View File
@@ -247,7 +247,7 @@
<providers>
<clear />
<add name="UmbracoMembershipProvider" type="Umbraco.Web.Security.Providers.MembersMembershipProvider, Umbraco" minRequiredNonalphanumericCharacters="0" minRequiredPasswordLength="10" useLegacyEncoding="false" enablePasswordRetrieval="false" enablePasswordReset="false" requiresQuestionAndAnswer="false" defaultMemberTypeAlias="Member" passwordFormat="Hashed" allowManuallyChangingPassword="false" />
<add name="UsersMembershipProvider" type="Umbraco.Web.Security.Providers.UsersMembershipProvider, Umbraco" minRequiredNonalphanumericCharacters="0" minRequiredPasswordLength="10" useLegacyEncoding="false" enablePasswordRetrieval="false" enablePasswordReset="false" requiresQuestionAndAnswer="false" passwordFormat="Hashed" allowManuallyChangingPassword="false" />
<add name="UsersMembershipProvider" type="Umbraco.Web.Security.Providers.UsersMembershipProvider, Umbraco" />
</providers>
</membership>
<!-- Role Provider -->
@@ -56,7 +56,9 @@ namespace Umbraco.Web.Editors
/// <returns></returns>
[WebApi.UmbracoAuthorize(requireApproval: false)]
public IDictionary<string, object> GetMembershipProviderConfig()
{
{
//TODO: Check if the current PasswordValidator is an IMembershipProviderPasswordValidator, if
//it's not than we should return some generic defaults
var provider = Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider();
return provider.GetConfiguration(Services.UserService);
}
@@ -87,9 +87,10 @@ namespace Umbraco.Web.Editors
/// <returns>
/// If the password is being reset it will return the newly reset password, otherwise will return an empty value
/// </returns>
public ModelWithNotifications<string> PostChangePassword(ChangingPasswordModel data)
public async Task<ModelWithNotifications<string>> PostChangePassword(ChangingPasswordModel data)
{
var passwordChangeResult = PasswordChangeControllerHelper.ChangePassword(Security.CurrentUser, data, ModelState, Members);
var passwordChanger = new PasswordChanger(Logger, Services.UserService);
var passwordChangeResult = await passwordChanger.ChangePasswordWithIdentityAsync(Security.CurrentUser, data, ModelState, UserManager);
if (passwordChangeResult.Success)
{
+38 -340
View File
@@ -21,6 +21,8 @@ using Umbraco.Core.Persistence.DatabaseModelDefinitions;
using System.Web.Http.Controllers;
using Umbraco.Core.Models.EntityBase;
using Umbraco.Core.Xml;
using Umbraco.Web.Search;
using Umbraco.Web.Trees;
namespace Umbraco.Web.Editors
{
@@ -53,6 +55,8 @@ namespace Umbraco.Web.Editors
}
}
private readonly UmbracoTreeSearcher _treeSearcher = new UmbracoTreeSearcher();
/// <summary>
/// Returns an Umbraco alias given a string
/// </summary>
@@ -103,42 +107,40 @@ namespace Umbraco.Web.Editors
///
/// The reason a user is allowed to search individual entity types that they are not allowed to edit is because those search
/// methods might be used in things like pickers in the content editor.
/// </remarks>
/// </remarks>
[HttpGet]
public IEnumerable<EntityTypeSearchResult> SearchAll(string query)
public IDictionary<string, TreeSearchResult> SearchAll(string query)
{
var result = new Dictionary<string, TreeSearchResult>();
if (string.IsNullOrEmpty(query))
return Enumerable.Empty<EntityTypeSearchResult>();
return result;
var allowedSections = Security.CurrentUser.AllowedSections.ToArray();
var result = new List<EntityTypeSearchResult>();
if (allowedSections.InvariantContains(Constants.Applications.Content))
var searchableTrees = SearchableTreeResolver.Current.GetSearchableTrees();
foreach (var searchableTree in searchableTrees)
{
result.Add(new EntityTypeSearchResult
if (allowedSections.Contains(searchableTree.Value.AppAlias))
{
var tree = Services.ApplicationTreeService.GetByAlias(searchableTree.Key);
if (tree == null) continue; //shouldn't occur
var searchableTreeAttribute = searchableTree.Value.SearchableTree.GetType().GetCustomAttribute<SearchableTreeAttribute>(false);
var treeAttribute = tree.GetTreeAttribute();
long total;
result[treeAttribute.GetRootNodeDisplayName(Services.TextService)] = new TreeSearchResult
{
Results = ExamineSearch(query, UmbracoEntityTypes.Document),
EntityType = UmbracoEntityTypes.Document.ToString()
});
}
if (allowedSections.InvariantContains(Constants.Applications.Media))
{
result.Add(new EntityTypeSearchResult
{
Results = ExamineSearch(query, UmbracoEntityTypes.Media),
EntityType = UmbracoEntityTypes.Media.ToString()
});
}
if (allowedSections.InvariantContains(Constants.Applications.Members))
{
result.Add(new EntityTypeSearchResult
{
Results = ExamineSearch(query, UmbracoEntityTypes.Member),
EntityType = UmbracoEntityTypes.Member.ToString()
});
}
Results = searchableTree.Value.SearchableTree.Search(query, 200, 0, out total),
TreeAlias = searchableTree.Key,
AppAlias = searchableTree.Value.AppAlias,
JsFormatterService = searchableTreeAttribute == null ? "" : searchableTreeAttribute.ServiceName,
JsFormatterMethod = searchableTreeAttribute == null ? "" : searchableTreeAttribute.MethodName
};
}
}
return result;
}
@@ -458,8 +460,8 @@ namespace Umbraco.Web.Editors
//the EntityService cannot search members of a certain type, this is currently not supported and would require
//quite a bit of plumbing to do in the Services/Repository, we'll revert to a paged search
int total;
var searchResult = ExamineSearch(filter ?? "", type, pageSize, pageNumber - 1, out total, id);
long total;
var searchResult = _treeSearcher.ExamineSearch(Umbraco, filter ?? "", type, pageSize, pageNumber - 1, out total, id);
return new PagedResult<EntityBasic>(total, pageNumber, pageSize)
{
@@ -613,316 +615,12 @@ namespace Umbraco.Web.Editors
/// <param name="entityType"></param>
/// <param name="searchFrom"></param>
/// <returns></returns>
private IEnumerable<EntityBasic> ExamineSearch(string query, UmbracoEntityTypes entityType, string searchFrom = null)
private IEnumerable<SearchResultItem> ExamineSearch(string query, UmbracoEntityTypes entityType, string searchFrom = null)
{
int total;
return ExamineSearch(query, entityType, 200, 0, out total, searchFrom);
long total;
return _treeSearcher.ExamineSearch(Umbraco, query, entityType, 200, 0, out total, searchFrom);
}
private void AddExamineSearchFrom(string searchFrom, StringBuilder sb)
{
//if searchFrom is specified and it is greater than 0
int mediaSearchFrom;
if (searchFrom != null && int.TryParse(searchFrom, out mediaSearchFrom) && mediaSearchFrom > 0)
{
sb.Append("+__Path: \\-1*\\,");
sb.Append(mediaSearchFrom.ToString(CultureInfo.InvariantCulture));
sb.Append("\\,* ");
}
}
private void AddExamineUserStartNode(int[] startNodes, StringBuilder sb)
{
//make sure only what the user is configured to view is found
if (startNodes.Length > 0)
sb.Append("+(");
foreach (var startNode in startNodes)
{
if (startNode > 0)
{
// descendants
// "__Path: -1*,1234,*" -- the first "*" stands for path-to-1234
sb.Append("__Path: \\-1*\\,");
sb.Append(startNode.ToString(CultureInfo.InvariantCulture));
sb.Append("\\,* ");
// self
// "__Path: -1*,1234" -- the first "*" stands for path-to-1234
sb.Append("__Path: \\-1*\\,");
sb.Append(startNode.ToString(CultureInfo.InvariantCulture));
sb.Append(" ");
}
}
if (startNodes.Length > 0)
sb.Append(")");
}
/// <summary>
/// Searches for results based on the entity type
/// </summary>
/// <param name="query"></param>
/// <param name="entityType"></param>
/// <param name="totalFound"></param>
/// <param name="searchFrom">
/// A starting point for the search, generally a node id, but for members this is a member type alias
/// </param>
/// <param name="pageSize"></param>
/// <param name="pageIndex"></param>
/// <returns></returns>
private IEnumerable<EntityBasic> ExamineSearch(string query, UmbracoEntityTypes entityType, int pageSize, int pageIndex, out int totalFound, string searchFrom = null)
{
//TODO: We need to update this to support paging
var sb = new StringBuilder();
string type;
var searcher = Constants.Examine.InternalSearcher;
var fields = new[] { "id", "__NodeId" };
totalFound = 0;
//TODO: WE should really just allow passing in a lucene raw query
switch (entityType)
{
case UmbracoEntityTypes.Member:
searcher = Constants.Examine.InternalMemberSearcher;
type = "member";
fields = new[] { "id", "__NodeId", "email", "loginName"};
if (searchFrom != null && searchFrom != Constants.Conventions.MemberTypes.AllMembersListId && searchFrom.Trim() != "-1")
{
sb.Append("+__NodeTypeAlias:");
sb.Append(searchFrom);
sb.Append(" ");
}
break;
case UmbracoEntityTypes.Media:
type = "media";
var mediaStartNodeIds = Security.CurrentUser.CalculateMediaStartNodeIds(Services.EntityService);
if (mediaStartNodeIds.Length == 0) return Enumerable.Empty<EntityBasic>();
AddExamineSearchFrom(searchFrom, sb);
AddExamineUserStartNode(mediaStartNodeIds, sb);
break;
case UmbracoEntityTypes.Document:
type = "content";
var contentStartNodeIds = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService);
if (contentStartNodeIds.Length == 0) return Enumerable.Empty<EntityBasic>();
AddExamineSearchFrom(searchFrom, sb);
AddExamineUserStartNode(contentStartNodeIds, sb);
break;
default:
throw new NotSupportedException("The " + typeof(EntityController) + " currently does not support searching against object type " + entityType);
}
var internalSearcher = ExamineManager.Instance.SearchProviderCollection[searcher];
//build a lucene query:
// the __nodeName will be boosted 10x without wildcards
// then __nodeName will be matched normally with wildcards
// the rest will be normal without wildcards
//check if text is surrounded by single or double quotes, if so, then exact match
var surroundedByQuotes = Regex.IsMatch(query, "^\".*?\"$")
|| Regex.IsMatch(query, "^\'.*?\'$");
if (surroundedByQuotes)
{
//strip quotes, escape string, the replace again
query = query.Trim(new[] { '\"', '\'' });
query = Lucene.Net.QueryParsers.QueryParser.Escape(query);
//nothing to search
if (searchFrom.IsNullOrWhiteSpace() && query.IsNullOrWhiteSpace())
{
totalFound = 0;
return new List<EntityBasic>();
}
//update the query with the query term
if (query.IsNullOrWhiteSpace() == false)
{
//add back the surrounding quotes
query = string.Format("{0}{1}{0}", "\"", query);
//node name exactly boost x 10
sb.Append("+(__nodeName: (");
sb.Append(query.ToLower());
sb.Append(")^10.0 ");
foreach (var f in fields)
{
//additional fields normally
sb.Append(f);
sb.Append(": (");
sb.Append(query);
sb.Append(") ");
}
sb.Append(") ");
}
}
else
{
var trimmed = query.Trim(new[] {'\"', '\''});
//nothing to search
if (searchFrom.IsNullOrWhiteSpace() && trimmed.IsNullOrWhiteSpace())
{
totalFound = 0;
return new List<EntityBasic>();
}
//update the query with the query term
if (trimmed.IsNullOrWhiteSpace() == false)
{
query = Lucene.Net.QueryParsers.QueryParser.Escape(query);
var querywords = query.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
//node name exactly boost x 10
sb.Append("+(__nodeName:");
sb.Append("\"");
sb.Append(query.ToLower());
sb.Append("\"");
sb.Append("^10.0 ");
//node name normally with wildcards
sb.Append(" __nodeName:");
sb.Append("(");
foreach (var w in querywords)
{
sb.Append(w.ToLower());
sb.Append("* ");
}
sb.Append(") ");
foreach (var f in fields)
{
//additional fields normally
sb.Append(f);
sb.Append(":");
sb.Append("(");
foreach (var w in querywords)
{
sb.Append(w.ToLower());
sb.Append("* ");
}
sb.Append(")");
sb.Append(" ");
}
sb.Append(") ");
}
}
//must match index type
sb.Append("+__IndexType:");
sb.Append(type);
var raw = internalSearcher.CreateSearchCriteria().RawQuery(sb.ToString());
var result = internalSearcher
//only return the number of items specified to read up to the amount of records to fill from 0 -> the number of items on the page requested
.Search(raw, pageSize * (pageIndex + 1));
totalFound = result.TotalItemCount;
var pagedResult = result.Skip(pageIndex);
switch (entityType)
{
case UmbracoEntityTypes.Member:
return MemberFromSearchResults(pagedResult.ToArray());
case UmbracoEntityTypes.Media:
return MediaFromSearchResults(pagedResult);
case UmbracoEntityTypes.Document:
return ContentFromSearchResults(pagedResult);
default:
throw new NotSupportedException("The " + typeof(EntityController) + " currently does not support searching against object type " + entityType);
}
}
/// <summary>
/// Returns a collection of entities for media based on search results
/// </summary>
/// <param name="results"></param>
/// <returns></returns>
private IEnumerable<EntityBasic> MemberFromSearchResults(SearchResult[] results)
{
var mapped = Mapper.Map<IEnumerable<EntityBasic>>(results).ToArray();
//add additional data
foreach (var m in mapped)
{
//if no icon could be mapped, it will be set to document, so change it to picture
if (m.Icon == "icon-document")
{
m.Icon = "icon-user";
}
var searchResult = results.First(x => x.Id.ToInvariantString() == m.Id.ToString());
if (searchResult.Fields.ContainsKey("email") && searchResult.Fields["email"] != null)
{
m.AdditionalData["Email"] = results.First(x => x.Id.ToInvariantString() == m.Id.ToString()).Fields["email"];
}
if (searchResult.Fields.ContainsKey("__key") && searchResult.Fields["__key"] != null)
{
Guid key;
if (Guid.TryParse(searchResult.Fields["__key"], out key))
{
m.Key = key;
}
}
}
return mapped;
}
/// <summary>
/// Returns a collection of entities for media based on search results
/// </summary>
/// <param name="results"></param>
/// <returns></returns>
private IEnumerable<EntityBasic> MediaFromSearchResults(IEnumerable<SearchResult> results)
{
var mapped = Mapper.Map<IEnumerable<EntityBasic>>(results).ToArray();
//add additional data
foreach (var m in mapped)
{
//if no icon could be mapped, it will be set to document, so change it to picture
if (m.Icon == "icon-document")
{
m.Icon = "icon-picture";
}
}
return mapped;
}
/// <summary>
/// Returns a collection of entities for content based on search results
/// </summary>
/// <param name="results"></param>
/// <returns></returns>
private IEnumerable<EntityBasic> ContentFromSearchResults(IEnumerable<SearchResult> results)
{
var mapped = Mapper.Map<IEnumerable<EntityBasic>>(results).ToArray();
//add additional data
foreach (var m in mapped)
{
var intId = m.Id.TryConvertTo<int>();
if (intId.Success)
{
m.AdditionalData["Url"] = Umbraco.NiceUrl(intId.Result);
}
}
return mapped;
}
private IEnumerable<EntityBasic> GetResultForChildren(int id, UmbracoEntityTypes entityType)
{
@@ -1,39 +0,0 @@
using System;
using System.Linq;
using System.Web.Http.ModelBinding;
using Umbraco.Core;
using Umbraco.Core.Models.Membership;
using Umbraco.Web.Models;
using Umbraco.Web.Security;
namespace Umbraco.Web.Editors
{
internal class PasswordChangeControllerHelper
{
public static Attempt<PasswordChangedModel> ChangePassword(
IUser currentUser,
ChangingPasswordModel data,
ModelStateDictionary modelState,
MembershipHelper membersHelper)
{
var userProvider = Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider();
if (userProvider.RequiresQuestionAndAnswer)
{
throw new NotSupportedException("Currently the user editor does not support providers that have RequiresQuestionAndAnswer specified");
}
var passwordChangeResult = membersHelper.ChangePassword(currentUser.Username, data, userProvider);
if (passwordChangeResult.Success == false)
{
//it wasn't successful, so add the change error to the model state
var fieldName = passwordChangeResult.Result.ChangeError.MemberNames.FirstOrDefault() ?? "password";
modelState.AddModelError(fieldName,
passwordChangeResult.Result.ChangeError.ErrorMessage);
}
return passwordChangeResult;
}
}
}
+246
View File
@@ -0,0 +1,246 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using System.Web.Http.ModelBinding;
using System.Web.Security;
using Microsoft.AspNet.Identity;
using umbraco.cms.businesslogic.packager;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Models.Identity;
using Umbraco.Core.Security;
using Umbraco.Core.Services;
using Umbraco.Web.Models;
using Umbraco.Web.Security;
using IUser = Umbraco.Core.Models.Membership.IUser;
namespace Umbraco.Web.Editors
{
internal class PasswordChanger
{
private readonly ILogger _logger;
private readonly IUserService _userService;
public PasswordChanger(ILogger logger, IUserService userService)
{
_logger = logger;
_userService = userService;
}
public async Task<Attempt<PasswordChangedModel>> ChangePasswordWithIdentityAsync(
IUser currentUser,
ChangingPasswordModel passwordModel,
ModelStateDictionary modelState,
BackOfficeUserManager<BackOfficeIdentityUser> userMgr)
{
if (passwordModel == null) throw new ArgumentNullException("passwordModel");
if (userMgr == null) throw new ArgumentNullException("userMgr");
//check if this identity implementation is powered by an underlying membership provider (it will be in most cases)
var membershipPasswordHasher = userMgr.PasswordHasher as IMembershipProviderPasswordHasher;
//check if this identity implementation is powered by an IUserAwarePasswordHasher (it will be by default in 7.7+ but not for upgrades)
var userAwarePasswordHasher = userMgr.PasswordHasher as IUserAwarePasswordHasher<BackOfficeIdentityUser, int>;
if (membershipPasswordHasher != null && userAwarePasswordHasher == null)
{
//if this isn't using an IUserAwarePasswordHasher, then fallback to the old way
if (membershipPasswordHasher.MembershipProvider.RequiresQuestionAndAnswer)
throw new NotSupportedException("Currently the user editor does not support providers that have RequiresQuestionAndAnswer specified");
return ChangePasswordWithMembershipProvider(currentUser.Username, passwordModel, membershipPasswordHasher.MembershipProvider);
}
//if we are here, then a IUserAwarePasswordHasher is available, however we cannot proceed in that case if for some odd reason
//the user has configured the membership provider to not be hashed. This will actually never occur because the BackOfficeUserManager
//will throw if it's not hashed, but we should make sure to check anyways (i.e. in case we want to unit test!)
if (membershipPasswordHasher != null && membershipPasswordHasher.MembershipProvider.PasswordFormat != MembershipPasswordFormat.Hashed)
{
throw new InvalidOperationException("The membership provider cannot have a password format of " + membershipPasswordHasher.MembershipProvider.PasswordFormat + " and be configured with secured hashed passwords");
}
//Are we resetting the password??
if (passwordModel.Reset.HasValue && passwordModel.Reset.Value)
{
//ok, we should be able to reset it
var resetToken = await userMgr.GeneratePasswordResetTokenAsync(currentUser.Id);
var newPass = userMgr.GeneratePassword();
var resetResult = await userMgr.ResetPasswordAsync(currentUser.Id, resetToken, newPass);
if (resetResult.Succeeded == false)
{
var errors = string.Join(". ", resetResult.Errors);
_logger.Warn<PasswordChanger>(string.Format("Could not reset member password {0}", errors));
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not reset password, errors: " + errors, new[] { "resetPassword" }) });
}
return Attempt.Succeed(new PasswordChangedModel { ResetPassword = newPass });
}
//we're not resetting it so we need to try to change it.
if (passwordModel.NewPassword.IsNullOrWhiteSpace())
{
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Cannot set an empty password", new[] { "value" }) });
}
//we cannot arbitrarily change the password without knowing the old one and no old password was supplied - need to return an error
//TODO: What if the current user is admin? We should allow manually changing then?
if (passwordModel.OldPassword.IsNullOrWhiteSpace())
{
//if password retrieval is not enabled but there is no old password we cannot continue
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password cannot be changed without the old password", new[] { "oldPassword" }) });
}
if (passwordModel.OldPassword.IsNullOrWhiteSpace() == false)
{
//if an old password is suplied try to change it
var changeResult = await userMgr.ChangePasswordAsync(currentUser.Id, passwordModel.OldPassword, passwordModel.NewPassword);
if (changeResult.Succeeded == false)
{
var errors = string.Join(". ", changeResult.Errors);
_logger.Warn<PasswordChanger>(string.Format("Could not change member password {0}", errors));
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, errors: " + errors, new[] { "value" }) });
}
return Attempt.Succeed(new PasswordChangedModel());
}
//We shouldn't really get here
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, invalid information supplied", new[] { "value" }) });
}
/// <summary>
/// Changes password for a member/user given the membership provider and the password change model
/// </summary>
/// <param name="username"></param>
/// <param name="passwordModel"></param>
/// <param name="membershipProvider"></param>
/// <returns></returns>
public Attempt<PasswordChangedModel> ChangePasswordWithMembershipProvider(string username, ChangingPasswordModel passwordModel, MembershipProvider membershipProvider)
{
// YES! It is completely insane how many options you have to take into account based on the membership provider. yikes!
if (passwordModel == null) throw new ArgumentNullException("passwordModel");
if (membershipProvider == null) throw new ArgumentNullException("membershipProvider");
//Are we resetting the password??
if (passwordModel.Reset.HasValue && passwordModel.Reset.Value)
{
var canReset = membershipProvider.CanResetPassword(_userService);
if (canReset == false)
{
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password reset is not enabled", new[] { "resetPassword" }) });
}
if (membershipProvider.RequiresQuestionAndAnswer && passwordModel.Answer.IsNullOrWhiteSpace())
{
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password reset requires a password answer", new[] { "resetPassword" }) });
}
//ok, we should be able to reset it
try
{
var newPass = membershipProvider.ResetPassword(
username,
membershipProvider.RequiresQuestionAndAnswer ? passwordModel.Answer : null);
//return the generated pword
return Attempt.Succeed(new PasswordChangedModel { ResetPassword = newPass });
}
catch (Exception ex)
{
_logger.WarnWithException<PasswordChanger>("Could not reset member password", ex);
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not reset password, error: " + ex.Message + " (see log for full details)", new[] { "resetPassword" }) });
}
}
//we're not resetting it so we need to try to change it.
if (passwordModel.NewPassword.IsNullOrWhiteSpace())
{
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Cannot set an empty password", new[] { "value" }) });
}
//This is an edge case and is only necessary for backwards compatibility:
var umbracoBaseProvider = membershipProvider as MembershipProviderBase;
if (umbracoBaseProvider != null && umbracoBaseProvider.AllowManuallyChangingPassword)
{
//this provider allows manually changing the password without the old password, so we can just do it
try
{
var result = umbracoBaseProvider.ChangePassword(username, "", passwordModel.NewPassword);
return result == false
? Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, invalid username or password", new[] { "value" }) })
: Attempt.Succeed(new PasswordChangedModel());
}
catch (Exception ex)
{
_logger.WarnWithException<PasswordChanger>("Could not change member password", ex);
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, error: " + ex.Message + " (see log for full details)", new[] { "value" }) });
}
}
//The provider does not support manually chaning the password but no old password supplied - need to return an error
if (passwordModel.OldPassword.IsNullOrWhiteSpace() && membershipProvider.EnablePasswordRetrieval == false)
{
//if password retrieval is not enabled but there is no old password we cannot continue
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password cannot be changed without the old password", new[] { "oldPassword" }) });
}
if (passwordModel.OldPassword.IsNullOrWhiteSpace() == false)
{
//if an old password is suplied try to change it
try
{
var result = membershipProvider.ChangePassword(username, passwordModel.OldPassword, passwordModel.NewPassword);
return result == false
? Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, invalid username or password", new[] { "oldPassword" }) })
: Attempt.Succeed(new PasswordChangedModel());
}
catch (Exception ex)
{
_logger.WarnWithException<PasswordChanger>("Could not change member password", ex);
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, error: " + ex.Message + " (see log for full details)", new[] { "value" }) });
}
}
if (membershipProvider.EnablePasswordRetrieval == false)
{
//we cannot continue if we cannot get the current password
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password cannot be changed without the old password", new[] { "oldPassword" }) });
}
if (membershipProvider.RequiresQuestionAndAnswer && passwordModel.Answer.IsNullOrWhiteSpace())
{
//if the question answer is required but there isn't one, we cannot continue
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password cannot be changed without the password answer", new[] { "value" }) });
}
//lets try to get the old one so we can change it
try
{
var oldPassword = membershipProvider.GetPassword(
username,
membershipProvider.RequiresQuestionAndAnswer ? passwordModel.Answer : null);
try
{
var result = membershipProvider.ChangePassword(username, oldPassword, passwordModel.NewPassword);
return result == false
? Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password", new[] { "value" }) })
: Attempt.Succeed(new PasswordChangedModel());
}
catch (Exception ex1)
{
_logger.WarnWithException<PasswordChanger>("Could not change member password", ex1);
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, error: " + ex1.Message + " (see log for full details)", new[] { "value" }) });
}
}
catch (Exception ex2)
{
_logger.WarnWithException<PasswordChanger>("Could not retrieve member password", ex2);
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, error: " + ex2.Message + " (see log for full details)", new[] { "value" }) });
}
}
}
}
+21 -5
View File
@@ -151,12 +151,26 @@ namespace Umbraco.Web.Editors
var filePath = found.Avatar;
found.Avatar = null;
//if the filePath is already null it will mean that the user doesn't have a custom avatar and their gravatar is currently
//being used (if they have one). This means they want to remove their gravatar too which we can do by setting a special value
//for the avatar.
if (filePath.IsNullOrWhiteSpace() == false)
{
found.Avatar = null;
}
else
{
//set a special value to indicate to not have any avatar
found.Avatar = "none";
}
Services.UserService.Save(found);
if (FileSystemProviderManager.Current.MediaFileSystem.FileExists(filePath))
FileSystemProviderManager.Current.MediaFileSystem.DeleteFile(filePath);
if (filePath.IsNullOrWhiteSpace() == false)
{
if (FileSystemProviderManager.Current.MediaFileSystem.FileExists(filePath))
FileSystemProviderManager.Current.MediaFileSystem.DeleteFile(filePath);
}
return Request.CreateResponse(HttpStatusCode.OK, found.GetCurrentUserAvatarUrls(Services.UserService, ApplicationContext.ApplicationCache.StaticCache));
}
@@ -401,7 +415,7 @@ namespace Umbraco.Web.Editors
/// </summary>
/// <param name="userSave"></param>
/// <returns></returns>
public UserDisplay PostSaveUser(UserSave userSave)
public async Task<UserDisplay> PostSaveUser(UserSave userSave)
{
if (userSave == null) throw new ArgumentNullException("userSave");
@@ -457,7 +471,9 @@ namespace Umbraco.Web.Editors
var resetPasswordValue = string.Empty;
if (userSave.ChangePassword != null)
{
var passwordChangeResult = PasswordChangeControllerHelper.ChangePassword(found, userSave.ChangePassword, ModelState, Members);
var passwordChanger = new PasswordChanger(Logger, Services.UserService);
var passwordChangeResult = await passwordChanger.ChangePasswordWithIdentityAsync(found, userSave.ChangePassword, ModelState, UserManager);
if (passwordChangeResult.Success)
{
//depending on how the provider is configured, the password may be reset so let's store that for later
@@ -32,6 +32,7 @@ namespace Umbraco.Web.Install.InstallSteps
_applicationContext = applicationContext;
}
//TODO: Change all logic in this step to use ASP.NET Identity NOT MembershipProviders
private MembershipProvider CurrentProvider
{
get
@@ -1,19 +0,0 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
using Examine;
namespace Umbraco.Web.Models.ContentEditing
{
/// <summary>
/// Represents a search result by entity type
/// </summary>
[DataContract(Name = "searchResult", Namespace = "")]
public class EntityTypeSearchResult
{
[DataMember(Name = "type")]
public string EntityType { get; set; }
[DataMember(Name = "results")]
public IEnumerable<EntityBasic> Results { get; set; }
}
}
@@ -1,25 +1,15 @@
namespace Umbraco.Web.Models.ContentEditing
using System.Runtime.Serialization;
namespace Umbraco.Web.Models.ContentEditing
{
public class SearchResultItem
{
[DataContract(Name = "searchResult", Namespace = "")]
public class SearchResultItem : EntityBasic
{
/// <summary>
/// The string representation of the ID, used for Web responses
/// The score of the search result
/// </summary>
public string Id { get; set; }
/// <summary>
/// The name/title of the search result item
/// </summary>
public string Title { get; set; }
/// <summary>
/// The rank of the search result
/// </summary>
public int Rank { get; set; }
/// <summary>
/// Description/Synopsis of the item
/// </summary>
public string Description { get; set; }
[DataMember(Name = "score")]
public float Score { get; set; }
}
}
@@ -0,0 +1,35 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
using Examine;
namespace Umbraco.Web.Models.ContentEditing
{
/// <summary>
/// Represents a search result by entity type
/// </summary>
[DataContract(Name = "searchResult", Namespace = "")]
public class TreeSearchResult
{
[DataMember(Name = "appAlias")]
public string AppAlias { get; set; }
[DataMember(Name = "treeAlias")]
public string TreeAlias { get; set; }
/// <summary>
/// This is optional but if specified should be the name of an angular service to format the search result.
/// </summary>
[DataMember(Name = "jsSvc")]
public string JsFormatterService { get; set; }
/// <summary>
/// This is optional but if specified should be the name of a method on the jsSvc angular service to use, if not
/// specfied than it will expect the method to be called `format(searchResult, appAlias, treeAlias)`
/// </summary>
[DataMember(Name = "jsMethod")]
public string JsFormatterMethod { get; set; }
[DataMember(Name = "results")]
public IEnumerable<SearchResultItem> Results { get; set; }
}
}
@@ -37,10 +37,16 @@ namespace Umbraco.Web.Models.ContentEditing
/// </summary>
[DataMember(Name = "remainingAuthSeconds")]
public double SecondsUntilTimeout { get; set; }
/// <summary>
/// The user's calculated start nodes based on the start nodes they have assigned directly to them and via the groups they're assigned to
/// </summary>
[DataMember(Name = "startContentIds")]
public int[] StartContentIds { get; set; }
/// <summary>
/// The user's calculated start nodes based on the start nodes they have assigned directly to them and via the groups they're assigned to
/// </summary>
[DataMember(Name = "startMediaIds")]
public int[] StartMediaIds { get; set; }
@@ -6,6 +6,7 @@ using Examine;
using Examine.LuceneEngine.Providers;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Models.EntityBase;
using Umbraco.Core.Models.Mapping;
using Umbraco.Core.Models.Membership;
using Umbraco.Web.Models.ContentEditing;
@@ -28,7 +29,7 @@ namespace Umbraco.Web.Models.Mapping
{
basic.Icon = "icon-user";
}
});
});
config.CreateMap<PropertyType, EntityBasic>()
.ForMember(x => x.Udi, expression => expression.Ignore())
@@ -64,20 +65,7 @@ namespace Umbraco.Web.Models.Mapping
.ForMember(basic => basic.ParentId, expression => expression.UseValue(-1))
.ForMember(dto => dto.Trashed, expression => expression.Ignore())
.ForMember(x => x.AdditionalData, expression => expression.Ignore());
//config.CreateMap<EntityBasic, ITemplate>()
// .ConstructUsing(basic => new Template(basic.Name, basic.Alias)
// {
// Id = Convert.ToInt32(basic.Id),
// Key = basic.Key
// })
// .ForMember(t => t.Path, expression => expression.Ignore())
// .ForMember(t => t.Id, expression => expression.MapFrom(template => Convert.ToInt32(template.Id)))
// .ForMember(x => x.VirtualPath, expression => expression.Ignore())
// .ForMember(x => x.CreateDate, expression => expression.Ignore())
// .ForMember(x => x.UpdateDate, expression => expression.Ignore())
// .ForMember(x => x.Content, expression => expression.Ignore());
config.CreateMap<EntityBasic, ContentTypeSort>()
.ForMember(x => x.Id, expression => expression.MapFrom(entity => new Lazy<int>(() => Convert.ToInt32(entity.Id))))
.ForMember(x => x.SortOrder, expression => expression.Ignore());
@@ -89,8 +77,32 @@ namespace Umbraco.Web.Models.Mapping
.ForMember(dto => dto.Trashed, expression => expression.Ignore())
.ForMember(x => x.AdditionalData, expression => expression.Ignore());
config.CreateMap<SearchResult, EntityBasic>()
config.CreateMap<UmbracoEntity, SearchResultItem>()
.ForMember(x => x.Udi, expression => expression.MapFrom(x => Udi.Create(UmbracoObjectTypesExtensions.GetUdiType(x.NodeObjectTypeId), x.Key)))
.ForMember(basic => basic.Icon, expression => expression.MapFrom(entity => entity.ContentTypeIcon))
.ForMember(dto => dto.Trashed, expression => expression.Ignore())
.ForMember(x => x.Alias, expression => expression.Ignore())
.ForMember(x => x.Score, expression => expression.Ignore())
.AfterMap((entity, basic) =>
{
if (basic.Icon.IsNullOrWhiteSpace())
{
if (entity.NodeObjectTypeId == Constants.ObjectTypes.MemberGuid)
basic.Icon = "icon-user";
else if (entity.NodeObjectTypeId == Constants.ObjectTypes.DataTypeGuid)
basic.Icon = "icon-autofill";
else if (entity.NodeObjectTypeId == Constants.ObjectTypes.DocumentTypeGuid)
basic.Icon = "icon-item-arrangement";
else if (entity.NodeObjectTypeId == Constants.ObjectTypes.MediaTypeGuid)
basic.Icon = "icon-thumbnails";
else if (entity.NodeObjectTypeId == Constants.ObjectTypes.TemplateTypeGuid)
basic.Icon = "icon-newspaper-alt";
}
});
config.CreateMap<SearchResult, SearchResultItem>()
//default to document icon
.ForMember(x => x.Score, expression => expression.MapFrom(result => result.Score))
.ForMember(x => x.Udi, expression => expression.Ignore())
.ForMember(x => x.Icon, expression => expression.Ignore())
.ForMember(x => x.Id, expression => expression.MapFrom(result => result.Id))
@@ -156,11 +168,11 @@ namespace Umbraco.Web.Models.Mapping
}
});
config.CreateMap<ISearchResults, IEnumerable<EntityBasic>>()
.ConvertUsing(results => results.Select(Mapper.Map<EntityBasic>).ToList());
config.CreateMap<ISearchResults, IEnumerable<SearchResultItem>>()
.ConvertUsing(results => results.Select(Mapper.Map<SearchResultItem>).ToList());
config.CreateMap<IEnumerable<SearchResult>, IEnumerable<EntityBasic>>()
.ConvertUsing(results => results.Select(Mapper.Map<EntityBasic>).ToList());
config.CreateMap<IEnumerable<SearchResult>, IEnumerable<SearchResultItem>>()
.ConvertUsing(results => results.Select(Mapper.Map<SearchResultItem>).ToList());
}
}
}
+17 -6
View File
@@ -9,6 +9,7 @@ using Umbraco.Web.Trees;
using Umbraco.Web.WebApi;
using umbraco;
using umbraco.interfaces;
using Umbraco.Web.Search;
namespace Umbraco.Web
{
@@ -48,12 +49,22 @@ namespace Umbraco.Web
return resolver.ResolveTypes<ITree>();
}
/// <summary>
/// Returns all classes attributed with RestExtensionAttribute attribute
/// </summary>
/// <param name="resolver"></param>
/// <returns></returns>
internal static IEnumerable<Type> ResolveRestExtensions(this PluginManager resolver)
/// <summary>
/// Returns all available <see cref="ISearchableTree"/> in application
/// </summary>
/// <param name="resolver"></param>
/// <returns></returns>
internal static IEnumerable<Type> ResolveSearchableTrees(this PluginManager resolver)
{
return resolver.ResolveTypes<ISearchableTree>();
}
/// <summary>
/// Returns all classes attributed with RestExtensionAttribute attribute
/// </summary>
/// <param name="resolver"></param>
/// <returns></returns>
internal static IEnumerable<Type> ResolveRestExtensions(this PluginManager resolver)
{
return resolver.ResolveAttributedTypes<Umbraco.Web.BaseRest.RestExtensionAttribute>();
}
+27
View File
@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using Umbraco.Web.Models.ContentEditing;
namespace Umbraco.Web.Search
{
public interface ISearchableTree
{
/// <summary>
/// The alias of the tree that the <see cref="ISearchableTree"/> belongs to
/// </summary>
string TreeAlias { get; }
/// <summary>
/// Searches for results based on the entity type
/// </summary>
/// <param name="query"></param>
/// <param name="pageSize"></param>
/// <param name="pageIndex"></param>
/// <param name="totalFound"></param>
/// <param name="searchFrom">
/// A starting point for the search, generally a node id, but for members this is a member type alias
/// </param>
/// <returns></returns>
IEnumerable<SearchResultItem> Search(string query, int pageSize, long pageIndex, out long totalFound, string searchFrom = null);
}
}
@@ -0,0 +1,17 @@
namespace Umbraco.Web.Search
{
public class SearchableApplicationTree
{
public SearchableApplicationTree(string appAlias, string treeAlias, ISearchableTree searchableTree)
{
AppAlias = appAlias;
TreeAlias = treeAlias;
SearchableTree = searchableTree;
}
public string AppAlias { get; private set; }
public string TreeAlias { get; private set; }
public ISearchableTree SearchableTree { get; private set; }
}
}
@@ -0,0 +1,35 @@
using System;
namespace Umbraco.Web.Search
{
[AttributeUsage(AttributeTargets.Class)]
public sealed class SearchableTreeAttribute : Attribute
{
/// <summary>
/// This constructor defines both the angular service and method name to use
/// </summary>
/// <param name="serviceName"></param>
/// <param name="methodName"></param>
public SearchableTreeAttribute(string serviceName, string methodName)
{
if (string.IsNullOrWhiteSpace(serviceName)) throw new ArgumentException("Value cannot be null or whitespace.", "serviceName");
if (string.IsNullOrWhiteSpace(methodName)) throw new ArgumentException("Value cannot be null or whitespace.", "methodName");
MethodName = methodName;
ServiceName = serviceName;
}
/// <summary>
/// This constructor will assume that the method name equals `format(searchResult, appAlias, treeAlias)`
/// </summary>
/// <param name="serviceName"></param>
public SearchableTreeAttribute(string serviceName)
{
if (string.IsNullOrWhiteSpace(serviceName)) throw new ArgumentException("Value cannot be null or whitespace.", "serviceName");
MethodName = "";
ServiceName = serviceName;
}
public string MethodName { get; private set; }
public string ServiceName { get; private set; }
}
}
@@ -0,0 +1,18 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
namespace Umbraco.Web.Search
{
internal class SearchableTreeCollection : KeyedCollection<string, SearchableApplicationTree>
{
protected override string GetKeyForItem(SearchableApplicationTree item)
{
return item.TreeAlias;
}
public IReadOnlyDictionary<string, SearchableApplicationTree> AsReadOnlyDictionary()
{
return new ReadOnlyDictionary<string, SearchableApplicationTree>(Dictionary);
}
}
}
@@ -0,0 +1,51 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.Linq;
using Umbraco.Core;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.ObjectResolution;
using Umbraco.Core.Services;
using Umbraco.Web.Editors;
using Umbraco.Web.Trees;
namespace Umbraco.Web.Search
{
/// <summary>
/// A resolver to return the collection of searchable trees
/// </summary>
/// <remarks>
/// This collection has a request scoped lifetime therefore any instance of ISearchableTree needs to support being a request scoped lifetime
/// </remarks>
public class SearchableTreeResolver : LazyManyObjectsResolverBase<SearchableTreeResolver, ISearchableTree>
{
private readonly IApplicationTreeService _treeService;
public SearchableTreeResolver(IServiceProvider serviceProvider, ILogger logger, IApplicationTreeService treeService, Func<IEnumerable<Type>> searchableTrees)
: base(serviceProvider, logger, searchableTrees, ObjectLifetimeScope.HttpRequest)
{
_treeService = treeService;
}
/// <summary>
/// Returns the a dictionary of tree alias with it's affiliated <see cref="ISearchableTree"/>
/// </summary>
public IReadOnlyDictionary<string, SearchableApplicationTree> GetSearchableTrees()
{
var appTrees = _treeService.GetAll().ToArray();
var collection = new SearchableTreeCollection();
var searchableTrees = Values.ToArray();
foreach (var searchableTree in searchableTrees)
{
var found = appTrees.FirstOrDefault(x => x.Alias == searchableTree.TreeAlias);
if (found != null)
{
collection.Add(new SearchableApplicationTree(found.ApplicationAlias, found.Alias, searchableTree));
}
}
return collection.AsReadOnlyDictionary();
}
}
}
@@ -0,0 +1,335 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using AutoMapper;
using Examine;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Services;
using Umbraco.Web.Models.ContentEditing;
namespace Umbraco.Web.Search
{
internal class UmbracoTreeSearcher
{
/// <summary>
/// Searches for results based on the entity type
/// </summary>
/// <param name="umbracoHelper"></param>
/// <param name="query"></param>
/// <param name="entityType"></param>
/// <param name="totalFound"></param>
/// <param name="searchFrom">
/// A starting point for the search, generally a node id, but for members this is a member type alias
/// </param>
/// <param name="pageSize"></param>
/// <param name="pageIndex"></param>
/// <returns></returns>
public IEnumerable<SearchResultItem> ExamineSearch(
UmbracoHelper umbracoHelper,
string query,
UmbracoEntityTypes entityType,
int pageSize,
long pageIndex, out long totalFound, string searchFrom = null)
{
var sb = new StringBuilder();
string type;
var searcher = Constants.Examine.InternalSearcher;
var fields = new[] { "id", "__NodeId" };
var umbracoContext = umbracoHelper.UmbracoContext;
var appContext = umbracoContext.Application;
//TODO: WE should really just allow passing in a lucene raw query
switch (entityType)
{
case UmbracoEntityTypes.Member:
searcher = Constants.Examine.InternalMemberSearcher;
type = "member";
fields = new[] { "id", "__NodeId", "email", "loginName" };
if (searchFrom != null && searchFrom != Constants.Conventions.MemberTypes.AllMembersListId && searchFrom.Trim() != "-1")
{
sb.Append("+__NodeTypeAlias:");
sb.Append(searchFrom);
sb.Append(" ");
}
break;
case UmbracoEntityTypes.Media:
type = "media";
var allMediaStartNodes = umbracoContext.Security.CurrentUser.CalculateMediaStartNodeIds(appContext.Services.EntityService);
AppendPath(sb, UmbracoObjectTypes.Media, allMediaStartNodes, searchFrom, appContext.Services.EntityService);
break;
case UmbracoEntityTypes.Document:
type = "content";
var allContentStartNodes = umbracoContext.Security.CurrentUser.CalculateContentStartNodeIds(appContext.Services.EntityService);
AppendPath(sb, UmbracoObjectTypes.Document, allContentStartNodes, searchFrom, appContext.Services.EntityService);
break;
default:
throw new NotSupportedException("The " + typeof(UmbracoTreeSearcher) + " currently does not support searching against object type " + entityType);
}
var internalSearcher = ExamineManager.Instance.SearchProviderCollection[searcher];
//build a lucene query:
// the __nodeName will be boosted 10x without wildcards
// then __nodeName will be matched normally with wildcards
// the rest will be normal without wildcards
//check if text is surrounded by single or double quotes, if so, then exact match
var surroundedByQuotes = Regex.IsMatch(query, "^\".*?\"$")
|| Regex.IsMatch(query, "^\'.*?\'$");
if (surroundedByQuotes)
{
//strip quotes, escape string, the replace again
query = query.Trim(new[] { '\"', '\'' });
query = Lucene.Net.QueryParsers.QueryParser.Escape(query);
//nothing to search
if (searchFrom.IsNullOrWhiteSpace() && query.IsNullOrWhiteSpace())
{
totalFound = 0;
return new List<SearchResultItem>();
}
//update the query with the query term
if (query.IsNullOrWhiteSpace() == false)
{
//add back the surrounding quotes
query = string.Format("{0}{1}{0}", "\"", query);
//node name exactly boost x 10
sb.Append("+(__nodeName: (");
sb.Append(query.ToLower());
sb.Append(")^10.0 ");
foreach (var f in fields)
{
//additional fields normally
sb.Append(f);
sb.Append(": (");
sb.Append(query);
sb.Append(") ");
}
sb.Append(") ");
}
}
else
{
var trimmed = query.Trim(new[] { '\"', '\'' });
//nothing to search
if (searchFrom.IsNullOrWhiteSpace() && trimmed.IsNullOrWhiteSpace())
{
totalFound = 0;
return new List<SearchResultItem>();
}
//update the query with the query term
if (trimmed.IsNullOrWhiteSpace() == false)
{
query = Lucene.Net.QueryParsers.QueryParser.Escape(query);
var querywords = query.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
//node name exactly boost x 10
sb.Append("+(__nodeName:");
sb.Append("\"");
sb.Append(query.ToLower());
sb.Append("\"");
sb.Append("^10.0 ");
//node name normally with wildcards
sb.Append(" __nodeName:");
sb.Append("(");
foreach (var w in querywords)
{
sb.Append(w.ToLower());
sb.Append("* ");
}
sb.Append(") ");
foreach (var f in fields)
{
//additional fields normally
sb.Append(f);
sb.Append(":");
sb.Append("(");
foreach (var w in querywords)
{
sb.Append(w.ToLower());
sb.Append("* ");
}
sb.Append(")");
sb.Append(" ");
}
sb.Append(") ");
}
}
//must match index type
sb.Append("+__IndexType:");
sb.Append(type);
var raw = internalSearcher.CreateSearchCriteria().RawQuery(sb.ToString());
var result = internalSearcher
//only return the number of items specified to read up to the amount of records to fill from 0 -> the number of items on the page requested
.Search(raw, Convert.ToInt32(pageSize * (pageIndex + 1)));
totalFound = result.TotalItemCount;
var pagedResult = result.Skip(Convert.ToInt32(pageIndex));
switch (entityType)
{
case UmbracoEntityTypes.Member:
return MemberFromSearchResults(pagedResult.ToArray());
case UmbracoEntityTypes.Media:
return MediaFromSearchResults(pagedResult);
case UmbracoEntityTypes.Document:
return ContentFromSearchResults(umbracoHelper, pagedResult);
default:
throw new NotSupportedException("The " + typeof(UmbracoTreeSearcher) + " currently does not support searching against object type " + entityType);
}
}
private void AppendPath(StringBuilder sb, UmbracoObjectTypes objectType, int[] startNodeIds, string searchFrom, IEntityService entityService)
{
if (sb == null) throw new ArgumentNullException("sb");
if (entityService == null) throw new ArgumentNullException("entityService");
int searchFromId;
var entityPath = int.TryParse(searchFrom, out searchFromId) && searchFromId > 0
? entityService.GetAllPaths(objectType, searchFromId).FirstOrDefault()
: null;
if (entityPath != null)
{
// find... only what's underneath
sb.Append("+__Path:");
AppendPath(sb, entityPath.Path, false);
sb.Append(" ");
}
else if (startNodeIds.Length == 0)
{
// make sure we don't find anything
sb.Append("+__Path:none ");
}
else if (startNodeIds.Contains(-1) == false) // -1 = no restriction
{
var entityPaths = entityService.GetAllPaths(objectType, startNodeIds);
// for each start node, find the start node, and what's underneath
// +__Path:(-1*,1234 -1*,1234,* -1*,5678 -1*,5678,* ...)
sb.Append("+__Path:(");
var first = true;
foreach (var ep in entityPaths)
{
if (first)
first = false;
else
sb.Append(" ");
AppendPath(sb, ep.Path, true);
}
sb.Append(") ");
}
}
private void AppendPath(StringBuilder sb, string path, bool includeThisNode)
{
path = path.Replace("-", "\\-").Replace(",", "\\,");
if (includeThisNode)
{
sb.Append(path);
sb.Append(" ");
}
sb.Append(path);
sb.Append("\\,*");
}
/// <summary>
/// Returns a collection of entities for media based on search results
/// </summary>
/// <param name="results"></param>
/// <returns></returns>
private IEnumerable<SearchResultItem> MemberFromSearchResults(SearchResult[] results)
{
var mapped = Mapper.Map<IEnumerable<SearchResultItem>>(results).ToArray();
//add additional data
foreach (var m in mapped)
{
//if no icon could be mapped, it will be set to document, so change it to picture
if (m.Icon == "icon-document")
{
m.Icon = "icon-user";
}
var searchResult = results.First(x => x.Id.ToInvariantString() == m.Id.ToString());
if (searchResult.Fields.ContainsKey("email") && searchResult.Fields["email"] != null)
{
m.AdditionalData["Email"] = results.First(x => x.Id.ToInvariantString() == m.Id.ToString()).Fields["email"];
}
if (searchResult.Fields.ContainsKey("__key") && searchResult.Fields["__key"] != null)
{
Guid key;
if (Guid.TryParse(searchResult.Fields["__key"], out key))
{
m.Key = key;
}
}
}
return mapped;
}
/// <summary>
/// Returns a collection of entities for media based on search results
/// </summary>
/// <param name="results"></param>
/// <returns></returns>
private IEnumerable<SearchResultItem> MediaFromSearchResults(IEnumerable<SearchResult> results)
{
var mapped = Mapper.Map<IEnumerable<SearchResultItem>>(results).ToArray();
//add additional data
foreach (var m in mapped)
{
//if no icon could be mapped, it will be set to document, so change it to picture
if (m.Icon == "icon-document")
{
m.Icon = "icon-picture";
}
}
return mapped;
}
/// <summary>
/// Returns a collection of entities for content based on search results
/// </summary>
/// <param name="umbracoHelper"></param>
/// <param name="results"></param>
/// <returns></returns>
private IEnumerable<SearchResultItem> ContentFromSearchResults(UmbracoHelper umbracoHelper, IEnumerable<SearchResult> results)
{
var mapped = Mapper.Map<IEnumerable<SearchResultItem>>(results).ToArray();
//add additional data
foreach (var m in mapped)
{
var intId = m.Id.TryConvertTo<int>();
if (intId.Success)
{
m.AdditionalData["Url"] = umbracoHelper.NiceUrl(intId.Result);
}
}
return mapped;
}
}
}
+3 -122
View File
@@ -13,6 +13,7 @@ using Umbraco.Core.Security;
using Umbraco.Web.Models;
using Umbraco.Web.PublishedCache;
using Umbraco.Core.Cache;
using Umbraco.Web.Editors;
using Umbraco.Web.Security.Providers;
using MPE = global::Umbraco.Core.Security.MembershipProviderExtensions;
@@ -655,128 +656,8 @@ namespace Umbraco.Web.Security
/// <returns></returns>
public virtual Attempt<PasswordChangedModel> ChangePassword(string username, ChangingPasswordModel passwordModel, MembershipProvider membershipProvider)
{
// YES! It is completely insane how many options you have to take into account based on the membership provider. yikes!
if (passwordModel == null) throw new ArgumentNullException("passwordModel");
if (membershipProvider == null) throw new ArgumentNullException("membershipProvider");
//Are we resetting the password??
if (passwordModel.Reset.HasValue && passwordModel.Reset.Value)
{
var canReset = membershipProvider.CanResetPassword(_applicationContext.Services.UserService);
if (canReset == false)
{
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password reset is not enabled", new[] { "resetPassword" }) });
}
if (membershipProvider.RequiresQuestionAndAnswer && passwordModel.Answer.IsNullOrWhiteSpace())
{
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password reset requires a password answer", new[] { "resetPassword" }) });
}
//ok, we should be able to reset it
try
{
var newPass = membershipProvider.ResetPassword(
username,
membershipProvider.RequiresQuestionAndAnswer ? passwordModel.Answer : null);
//return the generated pword
return Attempt.Succeed(new PasswordChangedModel { ResetPassword = newPass });
}
catch (Exception ex)
{
LogHelper.WarnWithException<WebSecurity>("Could not reset member password", ex);
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not reset password, error: " + ex.Message + " (see log for full details)", new[] { "resetPassword" }) });
}
}
//we're not resetting it so we need to try to change it.
if (passwordModel.NewPassword.IsNullOrWhiteSpace())
{
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Cannot set an empty password", new[] { "value" }) });
}
//This is an edge case and is only necessary for backwards compatibility:
var umbracoBaseProvider = membershipProvider as MembershipProviderBase;
if (umbracoBaseProvider != null && umbracoBaseProvider.AllowManuallyChangingPassword)
{
//this provider allows manually changing the password without the old password, so we can just do it
try
{
var result = umbracoBaseProvider.ChangePassword(username, "", passwordModel.NewPassword);
return result == false
? Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, invalid username or password", new[] { "value" }) })
: Attempt.Succeed(new PasswordChangedModel());
}
catch (Exception ex)
{
LogHelper.WarnWithException<WebSecurity>("Could not change member password", ex);
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, error: " + ex.Message + " (see log for full details)", new[] { "value" }) });
}
}
//The provider does not support manually chaning the password but no old password supplied - need to return an error
if (passwordModel.OldPassword.IsNullOrWhiteSpace() && membershipProvider.EnablePasswordRetrieval == false)
{
//if password retrieval is not enabled but there is no old password we cannot continue
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password cannot be changed without the old password", new[] { "oldPassword" }) });
}
if (passwordModel.OldPassword.IsNullOrWhiteSpace() == false)
{
//if an old password is suplied try to change it
try
{
var result = membershipProvider.ChangePassword(username, passwordModel.OldPassword, passwordModel.NewPassword);
return result == false
? Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, invalid username or password", new[] { "oldPassword" }) })
: Attempt.Succeed(new PasswordChangedModel());
}
catch (Exception ex)
{
LogHelper.WarnWithException<WebSecurity>("Could not change member password", ex);
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, error: " + ex.Message + " (see log for full details)", new[] { "value" }) });
}
}
if (membershipProvider.EnablePasswordRetrieval == false)
{
//we cannot continue if we cannot get the current password
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password cannot be changed without the old password", new[] { "oldPassword" }) });
}
if (membershipProvider.RequiresQuestionAndAnswer && passwordModel.Answer.IsNullOrWhiteSpace())
{
//if the question answer is required but there isn't one, we cannot continue
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Password cannot be changed without the password answer", new[] { "value" }) });
}
//lets try to get the old one so we can change it
try
{
var oldPassword = membershipProvider.GetPassword(
username,
membershipProvider.RequiresQuestionAndAnswer ? passwordModel.Answer : null);
try
{
var result = membershipProvider.ChangePassword(username, oldPassword, passwordModel.NewPassword);
return result == false
? Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password", new[] { "value" }) })
: Attempt.Succeed(new PasswordChangedModel());
}
catch (Exception ex1)
{
LogHelper.WarnWithException<WebSecurity>("Could not change member password", ex1);
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, error: " + ex1.Message + " (see log for full details)", new[] { "value" }) });
}
}
catch (Exception ex2)
{
LogHelper.WarnWithException<WebSecurity>("Could not retrieve member password", ex2);
return Attempt.Fail(new PasswordChangedModel { ChangeError = new ValidationResult("Could not change password, error: " + ex2.Message + " (see log for full details)", new[] { "value" }) });
}
var passwordChanger = new PasswordChanger(_applicationContext.ProfilingLogger.Logger, _applicationContext.Services.UserService);
return passwordChanger.ChangePasswordWithMembershipProvider(username, passwordModel, membershipProvider);
}
/// <summary>
+6 -2
View File
@@ -170,10 +170,14 @@ namespace Umbraco.Web.Security
/// <param name="username"></param>
/// <param name="password"></param>
/// <returns></returns>
/// <remarks>
/// This uses ASP.NET Identity to perform the validation
/// </remarks>
public virtual bool ValidateBackOfficeCredentials(string username, string password)
{
var membershipProvider = Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider();
return membershipProvider != null && membershipProvider.ValidateUser(username, password);
var backofficeuser = Mapper.Map<BackOfficeIdentityUser>(CurrentUser);
backofficeuser.UserName = username;
return UserManager.CheckPasswordAsync(backofficeuser, password).Result;
}
[EditorBrowsable(EditorBrowsableState.Never)]
@@ -55,7 +55,7 @@ namespace Umbraco.Web.Trees
Constants.System.Root.ToString(CultureInfo.InvariantCulture),
queryStrings,
application);
//this will be null if it cannot convert to ta single root section
if (result != null)
return result;
@@ -123,16 +123,16 @@ namespace Umbraco.Web.Trees
//This should really never happen if we've successfully got the children above.
throw new InvalidOperationException("Could not create root node for tree " + configTree.Alias);
}
//if the root node has a route path, we cannot create a single root section because by specifying the route path this would
//if the root node has a route path, we cannot create a single root section because by specifying the route path this would
//override the dashboard route and that means there can be no dashboard for that section which is a breaking change.
if (rootNode.Result.RoutePath.IsNullOrWhiteSpace() == false
&& rootNode.Result.RoutePath != "#"
if (rootNode.Result.RoutePath.IsNullOrWhiteSpace() == false
&& rootNode.Result.RoutePath != "#"
&& rootNode.Result.RoutePath != application)
{
{
//null indicates this cannot be converted
return null;
}
}
var sectionRoot = SectionRootNode.CreateSingleTreeSectionRoot(
rootId,
@@ -140,10 +140,10 @@ namespace Umbraco.Web.Trees
rootNode.Result.MenuUrl,
rootNode.Result.Name,
byControllerAttempt.Result);
//This can't be done currently because the root will default to routing to a dashboard and if we disable dashboards for a section
//This can't be done currently because the root will default to routing to a dashboard and if we disable dashboards for a section
//that is really considered a breaking change. See above.
//sectionRoot.RoutePath = rootNode.Result.RoutePath;
//sectionRoot.RoutePath = rootNode.Result.RoutePath;
foreach (var d in rootNode.Result.AdditionalData)
{
@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -18,6 +19,7 @@ using Umbraco.Web.Mvc;
using Umbraco.Web.WebApi;
using umbraco.BusinessLogic;
using umbraco.cms.presentation.Trees;
using Umbraco.Core.Services;
using ApplicationTree = Umbraco.Core.Models.ApplicationTree;
using IAuthorizationFilter = System.Web.Http.Filters.IAuthorizationFilter;
using UrlHelper = System.Web.Http.Routing.UrlHelper;
@@ -26,6 +28,48 @@ namespace Umbraco.Web.Trees
{
internal static class ApplicationTreeExtensions
{
private static readonly ConcurrentDictionary<Type, TreeAttribute> TreeAttributeCache = new ConcurrentDictionary<Type, TreeAttribute>();
internal static TreeAttribute GetTreeAttribute(this Type treeControllerType)
{
return TreeAttributeCache.GetOrAdd(treeControllerType, type =>
{
//Locate the tree attribute
var treeAttributes = type
.GetCustomAttributes<TreeAttribute>(false)
.ToArray();
if (treeAttributes.Length == 0)
{
throw new InvalidOperationException("The Tree controller is missing the " + typeof(TreeAttribute).FullName + " attribute");
}
//assign the properties of this object to those of the metadata attribute
return treeAttributes[0];
});
}
internal static TreeAttribute GetTreeAttribute(this ApplicationTree tree)
{
return tree.GetRuntimeType().GetTreeAttribute();
}
internal static string GetRootNodeDisplayName(this TreeAttribute attribute, ILocalizedTextService textService)
{
//if title is defined, return that
if (string.IsNullOrEmpty(attribute.Title) == false)
return attribute.Title;
//try to look up a tree header matching the tree alias
var localizedLabel = textService.Localize("treeHeaders/" + attribute.Alias);
if (string.IsNullOrEmpty(localizedLabel) == false)
return localizedLabel;
//is returned to signal that a label was not found
return "[" + attribute.Alias + "]";
}
internal static Attempt<Type> TryGetControllerTree(this ApplicationTree appTree)
{
+24 -15
View File
@@ -19,6 +19,8 @@ using umbraco.businesslogic;
using umbraco.businesslogic.Actions;
using umbraco.cms.businesslogic.web;
using umbraco.interfaces;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Search;
using Constants = Umbraco.Core.Constants;
namespace Umbraco.Web.Trees
@@ -26,8 +28,8 @@ namespace Umbraco.Web.Trees
//We will not allow the tree to render unless the user has access to any of the sections that the tree gets rendered
// this is not ideal but until we change permissions to be tree based (not section) there's not much else we can do here.
[UmbracoApplicationAuthorize(
Constants.Applications.Content,
Constants.Applications.Media,
Constants.Applications.Content,
Constants.Applications.Media,
Constants.Applications.Users,
Constants.Applications.Settings,
Constants.Applications.Developer,
@@ -36,9 +38,12 @@ namespace Umbraco.Web.Trees
[Tree(Constants.Applications.Content, Constants.Trees.Content)]
[PluginController("UmbracoTrees")]
[CoreTree]
public class ContentTreeController : ContentTreeControllerBase
[SearchableTree("searchResultFormatter", "configureContentResult")]
public class ContentTreeController : ContentTreeControllerBase, ISearchableTree
{
private readonly UmbracoTreeSearcher _treeSearcher = new UmbracoTreeSearcher();
protected override TreeNode CreateRootNode(FormDataCollection queryStrings)
{
var node = base.CreateRootNode(queryStrings);
@@ -67,7 +72,7 @@ namespace Umbraco.Web.Trees
{
get { return _userStartNodes ?? (_userStartNodes = Security.CurrentUser.CalculateContentStartNodeIds(Services.EntityService)); }
}
/// <summary>
/// Creates a tree node for a content item based on an UmbracoEntity
/// </summary>
@@ -77,7 +82,7 @@ namespace Umbraco.Web.Trees
/// <returns></returns>
protected override TreeNode GetSingleTreeNode(IUmbracoEntity e, string parentId, FormDataCollection queryStrings)
{
var entity = (UmbracoEntity) e;
var entity = (UmbracoEntity)e;
var allowedUserOptions = GetAllowedUserMenuItemsForNode(e);
if (CanUserAccessNode(e, allowedUserOptions))
@@ -90,7 +95,7 @@ namespace Umbraco.Web.Trees
entity,
Constants.ObjectTypes.DocumentGuid,
parentId,
queryStrings,
queryStrings,
entity.HasChildren && (isContainer == false));
node.AdditionalData.Add("contentType", entity.ContentTypeAlias);
@@ -100,7 +105,7 @@ namespace Umbraco.Web.Trees
node.AdditionalData.Add("isContainer", true);
node.SetContainerStyle();
}
if (entity.IsPublished == false)
node.SetNotPublishedStyle();
@@ -153,7 +158,7 @@ namespace Umbraco.Web.Trees
// add default actions for *all* users
menu.Items.Add<ActionRePublish>(ui.Text("actions", ActionRePublish.Instance.Alias)).ConvertLegacyMenuItem(null, "content", "content");
menu.Items.Add<RefreshNode, ActionRefresh>(ui.Text("actions", ActionRefresh.Instance.Alias), true);
return menu;
}
@@ -172,11 +177,11 @@ namespace Umbraco.Web.Trees
var nodeMenu = GetAllNodeMenuItems(item);
var allowedMenuItems = GetAllowedUserMenuItemsForNode(item);
FilterUserAllowedMenuItems(nodeMenu, allowedMenuItems);
//if the media item is in the recycle bin, don't have a default menu, just show the regular menu
if (item.Path.Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries).Contains(RecycleBinId.ToInvariantString()))
if (item.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Contains(RecycleBinId.ToInvariantString()))
{
nodeMenu.DefaultMenuAlias = null;
nodeMenu.Items.Insert(2, new MenuItem(ActionRestore.Instance, ui.Text("actions", ActionRestore.Instance.Alias)));
@@ -184,9 +189,9 @@ namespace Umbraco.Web.Trees
else
{
//set the default to create
nodeMenu.DefaultMenuAlias = ActionNew.Instance.Alias;
nodeMenu.DefaultMenuAlias = ActionNew.Instance.Alias;
}
return nodeMenu;
}
@@ -229,7 +234,7 @@ namespace Umbraco.Web.Trees
menu.Items.Add<ActionNew>(ui.Text("actions", ActionNew.Instance.Alias));
menu.Items.Add<ActionDelete>(ui.Text("actions", ActionDelete.Instance.Alias));
menu.Items.Add<ActionCreateBlueprintFromContent>(ui.Text("actions", ActionCreateBlueprintFromContent.Instance.Alias));
menu.Items.Add<ActionCreateBlueprintFromContent>(ui.Text("actions", ActionCreateBlueprintFromContent.Instance.Alias));
//need to ensure some of these are converted to the legacy system - until we upgrade them all to be angularized.
menu.Items.Add<ActionMove>(ui.Text("actions", ActionMove.Instance.Alias), true);
@@ -245,7 +250,7 @@ namespace Umbraco.Web.Trees
menu.Items.Add<ActionAssignDomain>(ui.Text("actions", ActionAssignDomain.Instance.Alias)).ConvertLegacyMenuItem(item, "content", "content");
menu.Items.Add<ActionRights>(ui.Text("actions", ActionRights.Instance.Alias), true);
menu.Items.Add<ActionProtect>(ui.Text("actions", ActionProtect.Instance.Alias), true).ConvertLegacyMenuItem(item, "content", "content");
menu.Items.Add<ActionNotify>(ui.Text("actions", ActionNotify.Instance.Alias), true).ConvertLegacyMenuItem(item, "content", "content");
menu.Items.Add<ActionSendToTranslate>(ui.Text("actions", ActionSendToTranslate.Instance.Alias)).ConvertLegacyMenuItem(item, "content", "content");
@@ -254,5 +259,9 @@ namespace Umbraco.Web.Trees
return menu;
}
public IEnumerable<SearchResultItem> Search(string query, int pageSize, long pageIndex, out long totalFound, string searchFrom = null)
{
return _treeSearcher.ExamineSearch(Umbraco, query, UmbracoEntityTypes.Document, pageSize, pageIndex, out totalFound, searchFrom);
}
}
}
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Formatting;
using AutoMapper;
using umbraco;
using umbraco.BusinessLogic.Actions;
using Umbraco.Core;
@@ -9,6 +11,8 @@ using Umbraco.Core.Models;
using Umbraco.Web.Models.Trees;
using Umbraco.Web.WebApi.Filters;
using Umbraco.Core.Services;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Search;
namespace Umbraco.Web.Trees
{
@@ -17,7 +21,7 @@ namespace Umbraco.Web.Trees
[Mvc.PluginController("UmbracoTrees")]
[CoreTree]
[LegacyBaseTree(typeof(loadNodeTypes))]
public class ContentTypeTreeController : TreeController
public class ContentTypeTreeController : TreeController, ISearchableTree
{
protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings)
{
@@ -137,5 +141,11 @@ namespace Umbraco.Web.Trees
return menu;
}
public IEnumerable<SearchResultItem> Search(string query, int pageSize, long pageIndex, out long totalFound, string searchFrom = null)
{
var results = Services.EntityService.GetPagedDescendantsFromRoot(UmbracoObjectTypes.DocumentType, pageIndex, pageSize, out totalFound, filter: query);
return Mapper.Map<IEnumerable<SearchResultItem>>(results);
}
}
}
@@ -5,6 +5,7 @@ using System.Linq;
using System.Net;
using System.Net.Http.Formatting;
using System.Web.Http;
using AutoMapper;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Web.Models.Trees;
@@ -14,6 +15,8 @@ using umbraco;
using umbraco.BusinessLogic.Actions;
using Umbraco.Core.Models.EntityBase;
using Umbraco.Core.Services;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Search;
using Constants = Umbraco.Core.Constants;
namespace Umbraco.Web.Trees
@@ -22,7 +25,7 @@ namespace Umbraco.Web.Trees
[Tree(Constants.Applications.Developer, Constants.Trees.DataTypes, null, sortOrder:1)]
[PluginController("UmbracoTrees")]
[CoreTree]
public class DataTypeTreeController : TreeController
public class DataTypeTreeController : TreeController, ISearchableTree
{
protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings)
{
@@ -123,5 +126,11 @@ namespace Umbraco.Web.Trees
return menu;
}
public IEnumerable<SearchResultItem> Search(string query, int pageSize, long pageIndex, out long totalFound, string searchFrom = null)
{
var results = Services.EntityService.GetPagedDescendantsFromRoot(UmbracoObjectTypes.DataType, pageIndex, pageSize, out totalFound, filter: query);
return Mapper.Map<IEnumerable<SearchResultItem>>(results);
}
}
}
-10
View File
@@ -1,10 +0,0 @@
using System.Collections.Generic;
using Umbraco.Web.Models.ContentEditing;
namespace Umbraco.Web.Trees
{
public interface ISearchableTree
{
IEnumerable<SearchResultItem> Search(string searchText);
}
}
+12 -1
View File
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http.Formatting;
@@ -13,6 +14,8 @@ using Umbraco.Web.Mvc;
using Umbraco.Web.WebApi.Filters;
using umbraco;
using umbraco.BusinessLogic.Actions;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Search;
using Constants = Umbraco.Core.Constants;
namespace Umbraco.Web.Trees
@@ -29,8 +32,11 @@ namespace Umbraco.Web.Trees
[Tree(Constants.Applications.Media, Constants.Trees.Media)]
[PluginController("UmbracoTrees")]
[CoreTree]
public class MediaTreeController : ContentTreeControllerBase
[SearchableTree("searchResultFormatter", "configureMediaResult")]
public class MediaTreeController : ContentTreeControllerBase, ISearchableTree
{
private readonly UmbracoTreeSearcher _treeSearcher = new UmbracoTreeSearcher();
protected override TreeNode CreateRootNode(FormDataCollection queryStrings)
{
var node = base.CreateRootNode(queryStrings);
@@ -166,5 +172,10 @@ namespace Umbraco.Web.Trees
}
return Security.CurrentUser.HasPathAccess(media, Services.EntityService);
}
public IEnumerable<SearchResultItem> Search(string query, int pageSize, long pageIndex, out long totalFound, string searchFrom = null)
{
return _treeSearcher.ExamineSearch(Umbraco, query, UmbracoEntityTypes.Media, pageSize, pageIndex, out totalFound, searchFrom);
}
}
}
@@ -1,6 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Formatting;
using AutoMapper;
using umbraco;
using umbraco.BusinessLogic.Actions;
using Umbraco.Core;
@@ -9,6 +11,8 @@ using Umbraco.Core.Models;
using Umbraco.Web.Models.Trees;
using Umbraco.Web.WebApi.Filters;
using Umbraco.Core.Services;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Search;
namespace Umbraco.Web.Trees
{
@@ -17,7 +21,7 @@ namespace Umbraco.Web.Trees
[Mvc.PluginController("UmbracoTrees")]
[CoreTree]
[LegacyBaseTree(typeof(loadMediaTypes))]
public class MediaTypeTreeController : TreeController
public class MediaTypeTreeController : TreeController, ISearchableTree
{
protected override TreeNodeCollection GetTreeNodes(string id, FormDataCollection queryStrings)
{
@@ -47,7 +51,7 @@ namespace Umbraco.Web.Trees
.OrderBy(entity => entity.Name)
.Select(dt =>
{
var node = CreateTreeNode(dt, Constants.ObjectTypes.MediaTypeGuid, id, queryStrings, "icon-item-arrangement",
var node = CreateTreeNode(dt, Constants.ObjectTypes.MediaTypeGuid, id, queryStrings, "icon-thumbnails",
//NOTE: Since 7.4+ child type creation is enabled by a config option. It defaults to on, but can be disabled if we decide to.
//We need this check to keep supporting sites where childs have already been created.
dt.HasChildren());
@@ -124,5 +128,11 @@ namespace Umbraco.Web.Trees
return menu;
}
public IEnumerable<SearchResultItem> Search(string query, int pageSize, long pageIndex, out long totalFound, string searchFrom = null)
{
var results = Services.EntityService.GetPagedDescendantsFromRoot(UmbracoObjectTypes.MediaType, pageIndex, pageSize, out totalFound, filter: query);
return Mapper.Map<IEnumerable<SearchResultItem>>(results);
}
}
}
+11 -1
View File
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
@@ -16,6 +17,8 @@ using Umbraco.Web.Mvc;
using Umbraco.Web.WebApi.Filters;
using umbraco;
using umbraco.BusinessLogic.Actions;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Search;
using Constants = Umbraco.Core.Constants;
namespace Umbraco.Web.Trees
@@ -30,7 +33,8 @@ namespace Umbraco.Web.Trees
[Tree(Constants.Applications.Members, Constants.Trees.Members, null, sortOrder: 0)]
[PluginController("UmbracoTrees")]
[CoreTree]
public class MemberTreeController : TreeController
[SearchableTree("searchResultFormatter", "configureMemberResult")]
public class MemberTreeController : TreeController, ISearchableTree
{
public MemberTreeController()
{
@@ -38,6 +42,7 @@ namespace Umbraco.Web.Trees
_isUmbracoProvider = _provider.IsUmbracoMembershipProvider();
}
private readonly UmbracoTreeSearcher _treeSearcher = new UmbracoTreeSearcher();
private readonly MembershipProvider _provider;
private readonly bool _isUmbracoProvider;
@@ -178,5 +183,10 @@ namespace Umbraco.Web.Trees
return menu;
}
public IEnumerable<SearchResultItem> Search(string query, int pageSize, long pageIndex, out long totalFound, string searchFrom = null)
{
return _treeSearcher.ExamineSearch(Umbraco, query, UmbracoEntityTypes.Member, pageSize, pageIndex, out totalFound, searchFrom);
}
}
}
@@ -6,14 +6,17 @@ using System.IO;
using System.Linq;
using System.Net.Http.Formatting;
using System.Web.Services.Description;
using AutoMapper;
using umbraco;
using umbraco.BusinessLogic.Actions;
using umbraco.cms.businesslogic.template;
using Umbraco.Core;
using Umbraco.Core.Configuration;
using Umbraco.Core.Models;
using Umbraco.Web.Models.ContentEditing;
using Umbraco.Web.Models.Trees;
using Umbraco.Web.Mvc;
using Umbraco.Web.Search;
using Umbraco.Web.WebApi.Filters;
using Constants = Umbraco.Core.Constants;
@@ -24,7 +27,7 @@ namespace Umbraco.Web.Trees
[Tree(Constants.Applications.Settings, Constants.Trees.Templates, null, sortOrder:1)]
[PluginController("UmbracoTrees")]
[CoreTree]
public class TemplatesTreeController : TreeController
public class TemplatesTreeController : TreeController, ISearchableTree
{
/// <summary>
/// The method called to render the contents of the tree structure
@@ -129,5 +132,11 @@ namespace Umbraco.Web.Trees
Uri.EscapeDataString("settings/editTemplate.aspx?templateID=" + template.Id)
: null;
}
public IEnumerable<SearchResultItem> Search(string query, int pageSize, long pageIndex, out long totalFound, string searchFrom = null)
{
var results = Services.EntityService.GetPagedDescendantsFromRoot(UmbracoObjectTypes.Template, pageIndex, pageSize, out totalFound, filter: query);
return Mapper.Map<IEnumerable<SearchResultItem>>(results);
}
}
}
+2 -29
View File
@@ -40,22 +40,7 @@ namespace Umbraco.Web.Trees
/// </summary>
public override string RootNodeDisplayName
{
get
{
//if title is defined, return that
if(string.IsNullOrEmpty(_attribute.Title) == false)
return _attribute.Title;
//try to look up a tree header matching the tree alias
var localizedLabel = Services.TextService.Localize("treeHeaders/" + _attribute.Alias);
if (string.IsNullOrEmpty(localizedLabel) == false)
return localizedLabel;
//is returned to signal that a label was not found
return "[" + _attribute.Alias + "]";
}
get { return _attribute.GetRootNodeDisplayName(Services.TextService); }
}
/// <summary>
@@ -68,19 +53,7 @@ namespace Umbraco.Web.Trees
private void Initialize()
{
//Locate the tree attribute
var treeAttributes = GetType()
.GetCustomAttributes(typeof(TreeAttribute), false)
.OfType<TreeAttribute>()
.ToArray();
if (treeAttributes.Any() == false)
{
throw new InvalidOperationException("The Tree controller is missing the " + typeof(TreeAttribute).FullName + " attribute");
}
//assign the properties of this object to those of the metadata attribute
_attribute = treeAttributes.First();
_attribute = GetType().GetTreeAttribute();
}
}
}
@@ -9,6 +9,7 @@ using Umbraco.Web.WebApi;
using Umbraco.Web.WebApi.Filters;
using Umbraco.Core.Models;
using Umbraco.Core.Models.EntityBase;
using Umbraco.Web.Search;
namespace Umbraco.Web.Trees
{
+8 -3
View File
@@ -324,7 +324,7 @@
<Compile Include="Editors\EditorValidator.cs" />
<Compile Include="Editors\FromJsonPathAttribute.cs" />
<Compile Include="Editors\IsCurrentUserModelFilterAttribute.cs" />
<Compile Include="Editors\PasswordChangeControllerHelper.cs" />
<Compile Include="Editors\PasswordChanger.cs" />
<Compile Include="Editors\TemplateController.cs" />
<Compile Include="Editors\ParameterSwapControllerActionSelector.cs" />
<Compile Include="Editors\CodeFileController.cs" />
@@ -458,6 +458,11 @@
<Compile Include="RouteDataExtensions.cs" />
<Compile Include="Routing\ContentFinderByRedirectUrl.cs" />
<Compile Include="Scheduling\LatchedBackgroundTaskBase.cs" />
<Compile Include="Search\SearchableApplicationTree.cs" />
<Compile Include="Search\SearchableTreeAttribute.cs" />
<Compile Include="Search\SearchableTreeCollection.cs" />
<Compile Include="Search\SearchableTreeResolver.cs" />
<Compile Include="Search\UmbracoTreeSearcher.cs" />
<Compile Include="Security\Identity\ExternalSignInAutoLinkOptions.cs" />
<Compile Include="Security\Identity\FixWindowsAuthMiddlware.cs" />
<Compile Include="Security\Identity\ForceRenewalCookieAuthenticationHandler.cs" />
@@ -583,7 +588,7 @@
<Compile Include="Install\Controllers\InstallApiController.cs" />
<Compile Include="Install\Controllers\InstallController.cs" />
<Compile Include="Install\InstallAuthorizeAttribute.cs" />
<Compile Include="Models\ContentEditing\EntityTypeSearchResult.cs" />
<Compile Include="Models\ContentEditing\TreeSearchResult.cs" />
<Compile Include="Models\ContentEditing\ListViewAwareContentItemDisplayBase.cs" />
<Compile Include="Models\ContentEditing\PropertyTypeValidation.cs" />
<Compile Include="Models\ImageCropData.cs" />
@@ -852,7 +857,7 @@
<Compile Include="Trees\ApplicationTreeRegistrar.cs" />
<Compile Include="Trees\ContentTreeControllerBase.cs" />
<Compile Include="Trees\LegacyTreeController.cs" />
<Compile Include="Trees\ISearchableTree.cs" />
<Compile Include="Search\ISearchableTree.cs" />
<Compile Include="Trees\LegacyTreeDataConverter.cs" />
<Compile Include="Trees\LegacyTreeJavascript.cs" />
<Compile Include="Trees\LegacyTreeParams.cs" />
+3
View File
@@ -51,6 +51,7 @@ using Umbraco.Core.Services;
using Umbraco.Web.Editors;
using Umbraco.Web.HealthCheck;
using Umbraco.Web.Profiling;
using Umbraco.Web.Search;
using GlobalSettings = Umbraco.Core.Configuration.GlobalSettings;
using ProfilingViewEngine = Umbraco.Core.Profiling.ProfilingViewEngine;
@@ -376,6 +377,8 @@ namespace Umbraco.Web
{
base.InitializeResolvers();
SearchableTreeResolver.Current = new SearchableTreeResolver(ServiceProvider, LoggerResolver.Current.Logger, ApplicationContext.Services.ApplicationTreeService, () => PluginManager.ResolveSearchableTrees());
XsltExtensionsResolver.Current = new XsltExtensionsResolver(ServiceProvider, LoggerResolver.Current.Logger, () => PluginManager.ResolveXsltExtensions());
EditorValidationResolver.Current= new EditorValidationResolver(ServiceProvider, LoggerResolver.Current.Logger, () => PluginManager.ResolveTypes<IEditorValidator>());