diff --git a/src/Umbraco.Core/TypeFinder.cs b/src/Umbraco.Core/TypeFinder.cs index 6323227893..0b4e85e237 100644 --- a/src/Umbraco.Core/TypeFinder.cs +++ b/src/Umbraco.Core/TypeFinder.cs @@ -26,11 +26,8 @@ namespace Umbraco.Core /// public static class TypeFinder { - private static readonly HashSet LocalFilteredAssemblyCache = new HashSet(); - private static readonly ReaderWriterLockSlim LocalFilteredAssemblyCacheLocker = new ReaderWriterLockSlim(); - private static HashSet _allAssemblies = null; - private static HashSet _binFolderAssemblies = null; - private static readonly ReaderWriterLockSlim Locker = new ReaderWriterLockSlim(); + private static volatile HashSet _localFilteredAssemblyCache = null; + private static readonly object LocalFilteredAssemblyCacheLocker = new object(); /// /// lazily load a reference to all assemblies and only local assemblies. @@ -46,162 +43,97 @@ namespace Umbraco.Core /// internal static HashSet GetAllAssemblies() { - using (var lck = new UpgradeableReadLock(Locker)) + return AllAssemblies.Value; + } + + //Lazy access to the all assemblies list + private static readonly Lazy> AllAssemblies = new Lazy>(() => + { + HashSet assemblies = null; + try { - if (_allAssemblies == null) + var isHosted = HttpContext.Current != null; + + try { + if (isHosted) + { + assemblies = new HashSet(BuildManager.GetReferencedAssemblies().Cast()); + } + } + catch (InvalidOperationException e) + { + if (!(e.InnerException is SecurityException)) + throw; + } - lck.UpgradeToWriteLock(); + if (assemblies == null) + { + //NOTE: we cannot use AppDomain.CurrentDomain.GetAssemblies() because this only returns assemblies that have + // already been loaded in to the app domain, instead we will look directly into the bin folder and load each one. + var binFolder = IOHelper.GetRootDirectoryBinFolder(); + var binAssemblyFiles = Directory.GetFiles(binFolder, "*.dll", SearchOption.TopDirectoryOnly).ToList(); + //var binFolder = Assembly.GetExecutingAssembly().GetAssemblyFile().Directory; + //var binAssemblyFiles = Directory.GetFiles(binFolder.FullName, "*.dll", SearchOption.TopDirectoryOnly).ToList(); + assemblies = new HashSet(); + foreach (var a in binAssemblyFiles) + { + try + { + var assName = AssemblyName.GetAssemblyName(a); + var ass = Assembly.Load(assName); + assemblies.Add(ass); + } + catch (Exception e) + { + if (e is SecurityException || e is BadImageFormatException) + { + //swallow these exceptions + } + else + { + throw; + } + } + } + } - HashSet assemblies = null; + //if for some reason they are still no assemblies, then use the AppDomain to load in already loaded assemblies. + if (!assemblies.Any()) + { + foreach (var a in AppDomain.CurrentDomain.GetAssemblies()) + { + assemblies.Add(a); + } + } + + //here we are trying to get the App_Code assembly + var fileExtensions = new[] { ".cs", ".vb" }; //only vb and cs files are supported + var appCodeFolder = new DirectoryInfo(IOHelper.MapPath(IOHelper.ResolveUrl("~/App_code"))); + //check if the folder exists and if there are any files in it with the supported file extensions + if (appCodeFolder.Exists && (fileExtensions.Any(x => appCodeFolder.GetFiles("*" + x).Any()))) + { try { - var isHosted = HttpContext.Current != null; - - try - { - if (isHosted) - { - assemblies = new HashSet(BuildManager.GetReferencedAssemblies().Cast()); - } - } - catch (InvalidOperationException e) - { - if (!(e.InnerException is SecurityException)) - throw; - } - - - if (assemblies == null) - { - //NOTE: we cannot use AppDomain.CurrentDomain.GetAssemblies() because this only returns assemblies that have - // already been loaded in to the app domain, instead we will look directly into the bin folder and load each one. - var binFolder = IOHelper.GetRootDirectoryBinFolder(); - var binAssemblyFiles = Directory.GetFiles(binFolder, "*.dll", SearchOption.TopDirectoryOnly).ToList(); - //var binFolder = Assembly.GetExecutingAssembly().GetAssemblyFile().Directory; - //var binAssemblyFiles = Directory.GetFiles(binFolder.FullName, "*.dll", SearchOption.TopDirectoryOnly).ToList(); - assemblies = new HashSet(); - foreach (var a in binAssemblyFiles) - { - try - { - var assName = AssemblyName.GetAssemblyName(a); - var ass = Assembly.Load(assName); - assemblies.Add(ass); - } - catch (Exception e) - { - if (e is SecurityException || e is BadImageFormatException) - { - //swallow these exceptions - } - else - { - throw; - } - } - } - } - - //if for some reason they are still no assemblies, then use the AppDomain to load in already loaded assemblies. - if (!assemblies.Any()) - { - foreach (var a in AppDomain.CurrentDomain.GetAssemblies()) - { - assemblies.Add(a); - } - } - - //here we are trying to get the App_Code assembly - var fileExtensions = new[] { ".cs", ".vb" }; //only vb and cs files are supported - var appCodeFolder = new DirectoryInfo(IOHelper.MapPath(IOHelper.ResolveUrl("~/App_code"))); - //check if the folder exists and if there are any files in it with the supported file extensions - if (appCodeFolder.Exists && (fileExtensions.Any(x => appCodeFolder.GetFiles("*" + x).Any()))) - { - var appCodeAssembly = Assembly.Load("App_Code"); - if (!assemblies.Contains(appCodeAssembly)) // BuildManager will find App_Code already - assemblies.Add(appCodeAssembly); - } - - //now set the _allAssemblies - _allAssemblies = new HashSet(assemblies); - + var appCodeAssembly = Assembly.Load("App_Code"); + if (!assemblies.Contains(appCodeAssembly)) // BuildManager will find App_Code already + assemblies.Add(appCodeAssembly); } - catch (InvalidOperationException e) + catch (FileNotFoundException ex) { - if (!(e.InnerException is SecurityException)) - throw; - - _binFolderAssemblies = _allAssemblies; + //this will occur if it cannot load the assembly + LogHelper.Error(typeof(TypeFinder), "Could not load assembly App_Code", ex); } } - - return _allAssemblies; } - } - - /// - /// Returns only assemblies found in the bin folder that have been loaded into the app domain. - /// - /// - /// - /// This will be used if we implement App_Plugins from Umbraco v5 but currently it is not used. - /// - internal static HashSet GetBinAssemblies() - { - - if (_binFolderAssemblies == null) + catch (InvalidOperationException e) { - using (new WriteLock(Locker)) - { - var assemblies = GetAssembliesWithKnownExclusions().ToArray(); - var binFolder = Assembly.GetExecutingAssembly().GetAssemblyFile().Directory; - var binAssemblyFiles = Directory.GetFiles(binFolder.FullName, "*.dll", SearchOption.TopDirectoryOnly).ToList(); - var domainAssemblyNames = binAssemblyFiles.Select(AssemblyName.GetAssemblyName); - var safeDomainAssemblies = new HashSet(); - var binFolderAssemblies = new HashSet(); - - foreach (var a in assemblies) - { - try - { - //do a test to see if its queryable in med trust - var assemblyFile = a.GetAssemblyFile(); - safeDomainAssemblies.Add(a); - } - catch (SecurityException) - { - //we will just ignore this because this will fail - //in medium trust for system assemblies, we get an exception but we just want to continue until we get to - //an assembly that is ok. - } - } - - foreach (var assemblyName in domainAssemblyNames) - { - try - { - var foundAssembly = - safeDomainAssemblies.FirstOrDefault(a => a.GetAssemblyFile() == assemblyName.GetAssemblyFile()); - if (foundAssembly != null) - { - binFolderAssemblies.Add(foundAssembly); - } - } - catch (SecurityException) - { - //we will just ignore this because if we are trying to do a call to: - // AssemblyName.ReferenceMatchesDefinition(a.GetName(), assemblyName))) - //in medium trust for system assemblies, we get an exception but we just want to continue until we get to - //an assembly that is ok. - } - } - - _binFolderAssemblies = new HashSet(binFolderAssemblies); - } + if (!(e.InnerException is SecurityException)) + throw; } - return _binFolderAssemblies; - } + + return assemblies; + }); /// /// Return a list of found local Assemblies excluding the known assemblies we don't want to scan @@ -213,20 +145,23 @@ namespace Umbraco.Core internal static HashSet GetAssembliesWithKnownExclusions( IEnumerable excludeFromResults = null) { - using (var lck = new UpgradeableReadLock(LocalFilteredAssemblyCacheLocker)) + if (_localFilteredAssemblyCache == null) { - if (LocalFilteredAssemblyCache.Any()) return LocalFilteredAssemblyCache; - - lck.UpgradeToWriteLock(); - - var assemblies = GetFilteredAssemblies(excludeFromResults, KnownAssemblyExclusionFilter); - foreach (var a in assemblies) + lock (LocalFilteredAssemblyCacheLocker) { - LocalFilteredAssemblyCache.Add(a); + //double check + if (_localFilteredAssemblyCache == null) + { + _localFilteredAssemblyCache = new HashSet(); + var assemblies = GetFilteredAssemblies(excludeFromResults, KnownAssemblyExclusionFilter); + foreach (var a in assemblies) + { + _localFilteredAssemblyCache.Add(a); + } + } } - - return LocalFilteredAssemblyCache; } + return _localFilteredAssemblyCache; } /// diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js index 4576f3344b..ae4ca3cde8 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js @@ -2,13 +2,32 @@ /** * @ngdoc service * @name umbraco.services.contentEditingHelper -* @description A helper service for most editors, some methods are specific to content/media/member model types but most are used by +* @description A helper service for most editors, some methods are specific to content/media/member model types but most are used by * all editors to share logic and reduce the amount of replicated code among editors. **/ function contentEditingHelper(fileManager, $q, $location, $routeParams, notificationsService, serverValidationManager, dialogService, formHelper, appState, keyboardService) { + function isValidIdentifier(id){ + //empty id <= 0 + if(angular.isNumber(id) && id > 0){ + return true; + } + + //empty guid + if(id === "00000000-0000-0000-0000-000000000000"){ + return false; + } + + //empty string / alias + if(id === ""){ + return false; + } + + return true; + } + return { - + /** Used by the content editor and mini content editor to perform saving operations */ contentEditorPerformSave: function (args) { if (!angular.isObject(args)) { @@ -30,7 +49,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica var self = this; var deferred = $q.defer(); - + if (!args.scope.busy && formHelper.submitForm({ scope: args.scope, statusMessage: args.statusMessage })) { args.scope.busy = true; @@ -71,9 +90,9 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica return deferred.promise; }, - + /** Returns the action button definitions based on what permissions the user has. - The content.allowedActions parameter contains a list of chars, each represents a button by permission so + The content.allowedActions parameter contains a list of chars, each represents a button by permission so here we'll build the buttons according to the chars of the user. */ configureContentEditorButtons: function (args) { @@ -136,7 +155,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica handler: args.methods.unPublish }; default: - return null; + return null; } } @@ -160,8 +179,8 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica //Now we need to make the drop down button list, this is also slightly tricky because: //We cannot have any buttons if there's no default button above. - //We cannot have the unpublish button (Z) when there's no publish permission. - //We cannot have the unpublish button (Z) when the item is not published. + //We cannot have the unpublish button (Z) when there's no publish permission. + //We cannot have the unpublish button (Z) when the item is not published. if (buttons.defaultButton) { //get the last index of the button order @@ -174,7 +193,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica } - //if we are not creating, then we should add unpublish too, + //if we are not creating, then we should add unpublish too, // so long as it's already published and if the user has access to publish if (!args.create) { if (args.content.publishDate && _.contains(args.content.allowedActions, "U")) { @@ -235,13 +254,13 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica } } } - + actions.push(defaultAction); //Now we need to make the drop down button list, this is also slightly tricky because: //We cannot have any buttons if there's no default button above. - //We cannot have the unpublish button (Z) when there's no publish permission. - //We cannot have the unpublish button (Z) when the item is not published. + //We cannot have the unpublish button (Z) when there's no publish permission. + //We cannot have the unpublish button (Z) when the item is not published. if (defaultAction) { //get the last index of the button order var lastIndex = _.indexOf(actionOrder, defaultAction); @@ -253,7 +272,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica } } - //if we are not creating, then we should add unpublish too, + //if we are not creating, then we should add unpublish too, // so long as it's already published and if the user has access to publish if (!creating) { if (content.publishDate && _.contains(content.allowedActions,"U")) { @@ -329,7 +348,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica var allOrigProps = this.getAllProps(origContent); var allNewProps = this.getAllProps(savedContent); - function getNewProp(alias) { + function getNewProp(alias) { return _.find(allNewProps, function (item) { return item.alias === alias; }); @@ -343,12 +362,12 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica }; //check for changed built-in properties of the content for (var o in origContent) { - + //ignore the ones listed in the array if (shouldIgnore(o)) { continue; } - + if (!_.isEqual(origContent[o], savedContent[o])) { origContent[o] = savedContent[o]; } @@ -362,8 +381,8 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica //they have changed so set the origContent prop to the new one var origVal = allOrigProps[p].value; allOrigProps[p].value = newProp.value; - - //instead of having a property editor $watch their expression to check if it has + + //instead of having a property editor $watch their expression to check if it has // been updated, instead we'll check for the existence of a special method on their model // and just call it. if (angular.isFunction(allOrigProps[p].onValueChanged)) { @@ -388,7 +407,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica * A function to handle what happens when we have validation issues from the server side */ handleSaveError: function (args) { - + if (!args.err) { throw "args.err cannot be null"; } @@ -402,7 +421,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica if (args.err.status === 400) { //now we need to look through all the validation errors if (args.err.data && (args.err.data.ModelState)) { - + //wire up the server validation errs formHelper.handleServerValidation(args.err.data.ModelState); @@ -414,7 +433,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica if (args.rebindCallback && angular.isFunction(args.rebindCallback)) { args.rebindCallback(); } - + serverValidationManager.executeAndClearAllSubscriptions(); } @@ -453,7 +472,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica } if (!this.redirectToCreatedContent(args.redirectId ? args.redirectId : args.savedContent.id)) { - + //we are not redirecting because this is not new content, it is existing content. In this case // we need to detect what properties have changed and re-bind them with the server data. //call the callback @@ -471,14 +490,14 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica * * @description * Changes the location to be editing the newly created content after create was successful. - * We need to decide if we need to redirect to edito mode or if we will remain in create mode. + * We need to decide if we need to redirect to edito mode or if we will remain in create mode. * We will only need to maintain create mode if we have not fulfilled the basic requirements for creating an entity which is at least having a name and ID */ redirectToCreatedContent: function (id, modelState) { //only continue if we are currently in create mode and if there is no 'Name' modelstate errors // since we need at least a name to create content. - if ($routeParams.create && (id > 0 && (!modelState || !modelState["Name"]))) { + if ($routeParams.create && (isValidIdentifier(id) && (!modelState || !modelState["Name"]))) { //need to change the location to not be in 'create' mode. Currently the route will be something like: // /belle/#/content/edit/1234?doctype=newsArticle&create=true @@ -487,7 +506,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica //clear the query strings $location.search(""); - + //change to new path $location.path("/" + $routeParams.section + "/" + $routeParams.tree + "/" + $routeParams.method + "/" + id); //don't add a browser history for this @@ -498,4 +517,4 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica } }; } -angular.module('umbraco.services').factory('contentEditingHelper', contentEditingHelper); \ No newline at end of file +angular.module('umbraco.services').factory('contentEditingHelper', contentEditingHelper);