From 73de12037eb4a38238c447799f4cc5608211ebac Mon Sep 17 00:00:00 2001 From: David Greasley Date: Thu, 25 Jun 2015 21:11:36 +0100 Subject: [PATCH 01/50] Fixes U4-6579: Checks if media item being deleted was at the root then redirects to the root, otherwise redirects to parent item. --- .../src/views/media/media.delete.controller.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js index eab7bbe4ad..3b25411bf9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js @@ -31,7 +31,13 @@ function MediaDeleteController($scope, mediaResource, treeService, navigationSer //if the current edited item is the same one as we're deleting, we need to navigate elsewhere if (editorState.current && editorState.current.id == $scope.currentNode.id) { - $location.path("/media/media/edit/" + $scope.currentNode.parentId); + + //If the deleted item lived at the root then just redirect back to the root, otherwise redirect to the item's parent + var location = "/media"; + if ($scope.currentNode.parentId != -1) + location = "/media/media/edit/" + $scope.currentNode.parentId; + + $location.path(location); } navigationService.hideMenu(); From 96199caa11e385667313c89c33048954c92bfdb5 Mon Sep 17 00:00:00 2001 From: craig Date: Thu, 25 Jun 2015 22:40:56 +0100 Subject: [PATCH 02/50] U4-6696 - UmbracoHelper.GetPreValueAsString fires exception with invalid integer --- .../Repositories/DataTypeDefinitionRepository.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs index 0ebbbeb6a5..3c5d5f2c38 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs @@ -283,15 +283,15 @@ AND umbracoNode.id <> @id", { //We need to see if we can find the cached PreValueCollection based on the cache key above - var regex = CacheKeys.DataTypePreValuesCacheKey + @"[\d]+-[,\d]*" + preValueId + @"[,\d$]*"; + var regex = CacheKeys.DataTypePreValuesCacheKey + @"[-\d]+-([\d]*,)*" + preValueId + @"(?!\d)[,\d$]*"; var cached = _cacheHelper.RuntimeCache.GetCacheItemsByKeyExpression(regex); if (cached != null && cached.Any()) { //return from the cache var collection = cached.First(); - var preVal = collection.FormatAsDictionary().Single(x => x.Value.Id == preValueId); - return preVal.Value.Value; + var preVal = collection.FormatAsDictionary().Single(x => x.Value.Id == preValueId); + return preVal.Value.Value; } //go and find the data type id for the pre val id passed in From aa9751afff5cbd6d710a2431511442c60bfb2fbc Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 26 Jun 2015 09:58:44 +0200 Subject: [PATCH 03/50] adds tests for U4-6696 --- .../DataTypeDefinitionRepository.cs | 10 +++++++--- .../DataTypeDefinitionRepositoryTest.cs | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs index 78b69f30b3..859c4e7ae7 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Data; using System.Globalization; using System.Linq; +using System.Text.RegularExpressions; using System.Threading; using Umbraco.Core.Cache; using Umbraco.Core.Logging; @@ -279,13 +280,16 @@ AND umbracoNode.id <> @id", return GetAndCachePreValueCollection(dataTypeId); } + internal static string GetCacheKeyRegex(int preValueId) + { + return CacheKeys.DataTypePreValuesCacheKey + @"[-\d]+-([\d]*,)*" + preValueId + @"(?!\d)[,\d$]*"; + } + public string GetPreValueAsString(int preValueId) { //We need to see if we can find the cached PreValueCollection based on the cache key above - var regex = CacheKeys.DataTypePreValuesCacheKey + @"[-\d]+-([\d]*,)*" + preValueId + @"(?!\d)[,\d$]*"; - - var cached = _cacheHelper.RuntimeCache.GetCacheItemsByKeyExpression(regex); + var cached = _cacheHelper.RuntimeCache.GetCacheItemsByKeyExpression(GetCacheKeyRegex(preValueId)); if (cached != null && cached.Any()) { //return from the cache diff --git a/src/Umbraco.Tests/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs index f3d7aaa0f1..289bd628ee 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DataTypeDefinitionRepositoryTest.cs @@ -1,6 +1,7 @@ using System; using System.Data; using System.Linq; +using System.Text.RegularExpressions; using Moq; using NUnit.Framework; using Umbraco.Core; @@ -40,6 +41,23 @@ namespace Umbraco.Tests.Persistence.Repositories return dataTypeDefinitionRepository; } + [TestCase("UmbracoPreVal87-21,3,48", 3, true)] + [TestCase("UmbracoPreVal87-21,33,48", 3, false)] + [TestCase("UmbracoPreVal87-21,33,48", 33, true)] + [TestCase("UmbracoPreVal87-21,3,48", 33, false)] + [TestCase("UmbracoPreVal87-21,3,48", 21, true)] + [TestCase("UmbracoPreVal87-21,3,48", 48, true)] + [TestCase("UmbracoPreVal87-22,33,48", 2, false)] + [TestCase("UmbracoPreVal87-22,33,48", 22, true)] + [TestCase("UmbracoPreVal87-22,33,44", 4, false)] + [TestCase("UmbracoPreVal87-22,33,44", 44, true)] + [TestCase("UmbracoPreVal87-22,333,44", 33, false)] + [TestCase("UmbracoPreVal87-22,333,44", 333, true)] + public void Pre_Value_Cache_Key_Tests(string cacheKey, int preValueId, bool outcome) + { + Assert.AreEqual(outcome, Regex.IsMatch(cacheKey, DataTypeDefinitionRepository.GetCacheKeyRegex(preValueId))); + } + [Test] public void Can_Create() { From acf7fa7907fb331927289f4e65b2e8f5b997b98b Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 26 Jun 2015 10:01:32 +0200 Subject: [PATCH 04/50] Updates code for U4-6579 with equals check and adds logic to the content editor too --- .../src/views/content/content.delete.controller.js | 8 +++++++- .../src/views/media/media.delete.controller.js | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.delete.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.delete.controller.js index be27f3a663..12da77e854 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.delete.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.delete.controller.js @@ -31,7 +31,13 @@ function ContentDeleteController($scope, contentResource, treeService, navigatio //if the current edited item is the same one as we're deleting, we need to navigate elsewhere if (editorState.current && editorState.current.id == $scope.currentNode.id) { - $location.path("/content/content/edit/" + $scope.currentNode.parentId); + + //If the deleted item lived at the root then just redirect back to the root, otherwise redirect to the item's parent + var location = "/content"; + if ($scope.currentNode.parentId !== -1) + location = "/content/content/edit/" + $scope.currentNode.parentId; + + $location.path(location); } navigationService.hideMenu(); diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js index 3b25411bf9..89a2920c49 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js @@ -34,7 +34,7 @@ function MediaDeleteController($scope, mediaResource, treeService, navigationSer //If the deleted item lived at the root then just redirect back to the root, otherwise redirect to the item's parent var location = "/media"; - if ($scope.currentNode.parentId != -1) + if ($scope.currentNode.parentId !== -1) location = "/media/media/edit/" + $scope.currentNode.parentId; $location.path(location); From f14989f7fd6708a42a95f55d4c989b8e24bc011e Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 26 Jun 2015 10:06:07 +0200 Subject: [PATCH 05/50] Updates code for U4-6579 with equals check and adds logic to the content editor too --- .../src/views/content/content.delete.controller.js | 2 +- .../src/views/media/media.delete.controller.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.delete.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.delete.controller.js index 12da77e854..d7ff1843c7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.delete.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.delete.controller.js @@ -34,7 +34,7 @@ function ContentDeleteController($scope, contentResource, treeService, navigatio //If the deleted item lived at the root then just redirect back to the root, otherwise redirect to the item's parent var location = "/content"; - if ($scope.currentNode.parentId !== -1) + if ($scope.currentNode.parentId.toString() !== "-1") location = "/content/content/edit/" + $scope.currentNode.parentId; $location.path(location); diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js index 89a2920c49..350821d975 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/media/media.delete.controller.js @@ -34,7 +34,7 @@ function MediaDeleteController($scope, mediaResource, treeService, navigationSer //If the deleted item lived at the root then just redirect back to the root, otherwise redirect to the item's parent var location = "/media"; - if ($scope.currentNode.parentId !== -1) + if ($scope.currentNode.parentId.toString() !== "-1") location = "/media/media/edit/" + $scope.currentNode.parentId; $location.path(location); From 42b2b16f0e3c4f4523688a503f1830b3dde970d0 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 26 Jun 2015 10:06:22 +0200 Subject: [PATCH 06/50] fixes js error with date picker --- .../propertyeditors/datepicker/datepicker.controller.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js index 4e540eb5fa..1fa210a19f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js @@ -135,18 +135,20 @@ function dateTimePickerController($scope, notificationsService, assetsService, a $scope.model.value = null; } }); + //unbind doc click event! + $scope.$on('$destroy', function () { + unsubscribe(); + }); }); }); }); - //unbind doc click event! $scope.$on('$destroy', function () { $(document).unbind("click", $scope.hidePicker); - unsubscribe(); }); } From 623e8e39791c4a8cea8c937e4aedc7fb7b6505b6 Mon Sep 17 00:00:00 2001 From: Stephan Date: Fri, 26 Jun 2015 12:31:53 +0200 Subject: [PATCH 07/50] U4-6642 - bugfix PR --- .../Persistence/Repositories/ContentRepository.cs | 9 ++++----- .../Persistence/Repositories/MediaRepository.cs | 9 +++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 1bec751c6c..315db64e01 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -351,11 +351,10 @@ namespace Umbraco.Core.Persistence.Repositories //NOTE Should the logic below have some kind of fallback for empty parent ids ? //Logic for setting Path, Level and SortOrder var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); - int level = parent.Level + 1; - var maxSortOrder = - Database.ExecuteScalar( - "SELECT coalesce(max(sortOrder),0) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", - new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); + var level = parent.Level + 1; + var maxSortOrder = Database.ExecuteScalar( + "SELECT coalesce(max(sortOrder),-1) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", + new { /*ParentId =*/ entity.ParentId, NodeObjectType = NodeObjectTypeId }); var sortOrder = maxSortOrder + 1; //Create the (base) node data - umbracoNode diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index 008e588311..452c229417 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -283,10 +283,11 @@ namespace Umbraco.Core.Persistence.Repositories //NOTE Should the logic below have some kind of fallback for empty parent ids ? //Logic for setting Path, Level and SortOrder var parent = Database.First("WHERE id = @ParentId", new { ParentId = entity.ParentId }); - int level = parent.Level + 1; - int sortOrder = - Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoNode WHERE parentID = @ParentId AND nodeObjectType = @NodeObjectType", - new { ParentId = entity.ParentId, NodeObjectType = NodeObjectTypeId }); + var level = parent.Level + 1; + var maxSortOrder = Database.ExecuteScalar( + "SELECT coalesce(max(sortOrder),-1) FROM umbracoNode WHERE parentid = @ParentId AND nodeObjectType = @NodeObjectType", + new { /*ParentId =*/ entity.ParentId, NodeObjectType = NodeObjectTypeId }); + var sortOrder = maxSortOrder + 1; //Create the (base) node data - umbracoNode var nodeDto = dto.ContentDto.NodeDto; From 30979f92cce358884aeb119702c16fc4e94d4c50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B6ren=20Deger?= Date: Fri, 26 Jun 2015 14:48:57 +0200 Subject: [PATCH 08/50] Added missing language areas/keys to de lang file Added missing language areas and keys to the german language file, i.e. area with alias: changeDocType, media, placeholders, grid and a lot of more. --- src/Umbraco.Web.UI/umbraco/config/lang/de.xml | 287 +++++++++++++----- 1 file changed, 205 insertions(+), 82 deletions(-) diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/de.xml b/src/Umbraco.Web.UI/umbraco/config/lang/de.xml index 92e76f26a7..c6f87964e7 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/de.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/de.xml @@ -8,6 +8,7 @@ Hostnamen verwalten Protokoll Durchsuchen + Dokumententyp ändern Kopieren Erstellen Paket erstellen @@ -23,6 +24,7 @@ Benachrichtigungen Öffentlicher Zugriff Veröffentlichen + Veröffentlichung zurück nehmen Aktualisieren Erneut veröffentlichen Berechtigungen @@ -33,33 +35,39 @@ Zur Veröffentlichung einreichen Übersetzen Aktualisieren - Veröffentlichung zurücknehmen + Standard Wert + Zugriff verweigert Neue Domain hinzufügen + Entfernen + Ungültiges Element + Format der Domain ungültig + Die Domain ist bereits zugeordnet + Sprache Domain Domain '%0%' hinzugefügt Domain '%0%' entfernt Die Domain '%0%' ist bereits zugeordnet - Beispiel: example.com, www.example.com Domain '%0%' aktualisiert Domains bearbeiten - Erlaubnis verweigert. - entfernen - Ungültiges Element. - Format der Domain ungültig. - Domain wurde bereits zugewiesen. - Sprache +
1-Level Pfade in Domains werden unterstützt, z.B. "example.com/en". Diese sollten aber nach Möglichkeit vermieden werden. Besser sollten + die Sprachkultur-Einstellungen verwendet werden.]]>
Vererben - Kultur - Domains - Definiert die Kultureinstellung für untergeordnete Elemente dieses Elements oder vererbt vom übergeordneten Element. Wird auch auf das aktuelle Element angewendet, sofern auf tieferer Ebene keine Domain zugeordnet ist. - + Sprachkultur + Definiert die Sprachkultureinstellung für untergeordnete Elemente dieses Elements oder vererbt vom übergeordneten Element. Wird auch auf das aktuelle Element angewendet, sofern auf tieferer Ebene keine Domain zugeordnet ist. + + Domains + Ansicht für - Fett + Auswählen + Verzeichnis wählen + Etwas anders machen + Fett Ausrücken Formularelement einfügen Graphische Überschrift einfügen @@ -74,34 +82,59 @@ Aufzählung Nummerierung Makro einfügen - Abbildung einfügen + Bild einfügen Datenbeziehungen bearbeiten + Zürück zur Übersicht Speichern Speichern und veröffentlichen Speichern und zur Abnahme übergeben Vorschau + Die Vorschaufunktion ist deaktiviert, da keine Vorlage zugewiesen ist Stil auswählen Stil anzeigen Tabelle einfügen - Die Vorschaufunktion ist deaktiviert, da keine Vorlage zugewiesen ist - Auswählen - Etwas anders machen + + + Um einen Dokumententyp zu ändern muss zunächst ein gültiger Dokumententyp in der Liste für diesen Inhalt ausgewählt werden. + Danach muss die Zuweisung der Eigenschaften vom aktuellen zum neuen Dokumententyp bestätigt und/oder geändert werden und auf Speichern geklickt werden. + Der Inhalt wurde neu veröffentlicht. + Aktuelle Eigenschaft + Aktueller Doumententyp + Der Dokumententyp kann nicht geändert werden, da es keine alternativen gültigen Eigenschaften für dieses Dokument gibt. Eine Alternative wird gültig sein, wenn es unter dem übergeordenten Knoten des ausgewählten Dokuments erlaubt ist und alle bestehenden untergeordneten Dokumente unter diesem erstellt werden dürfen. + Dokumententyp ändern + Eigenschaften zuweisen + Zu Eigenschaft zuweisen + Neue Vorlage + Neue Eigenschaft + keine + Inhalt + Neuen Dokumententyp auswählen + Der Dokumententyp des ausgewählten Dokuments wurde erfolgreich geändert zu [new type] und zu folgenden Eigenschaften zugewiesen: + zu + Zuweisung der Eigenschaft konnte nicht abgeschlossen werden, weil es eine oder mehr Eigenschaften mehr gibt als für eine Zuweisung definiert wurden. + Nur wecheselnde Eigenschaften sind gültig für das aktuell angezeigte Dokument. + Ist veröffentlicht Über dieses Dokument Alias (Wie würden Sie das Bild über das Telefon beschreiben?) Alternative Links Klicken, um das Dokument zu bearbeiten Erstellt von + Ursprünglicher Author + Geändert von Erstellt am + Erstellungszeitpunkt des Dokuments Dokumenttyp In Bearbeitung Veröffentlichung aufheben am Dieses Dokument wurde nach dem Veröffentlichen bearbeitet. Dieses Dokument ist nicht veröffentlicht. Zuletzt veröffentlicht + Es gibt keine anzeigbaren Elemente in dieser Liste. Medientyp + Link zu Medienobjekt(en) Mitgliedergruppe Mitgliederrolle Mitglieder-Typ @@ -109,29 +142,31 @@ Name des Dokument Eigenschaften Dieses Dokument ist veröffentlicht aber nicht sichtbar, da das übergeordnete Dokument '%0%' nicht publiziert ist + Ups! Dieses Dokument ist veröffentlicht, aber befindet sich nicht im internen Cache (Systemfehler) Veröffentlichen Publikationsstatus Veröffentlichen am + Veröffentlichung aufheben am Datum entfernen Sortierung abgeschlossen Um die Dokumente zu sortieren, ziehen Sie sie einfach an die gewünschte Position. Sie können mehrere Zeilen markieren indem Sie die Umschalttaste ("Shift") oder die Steuerungstaste ("Strg") gedrückt halten Statistiken Titel (optional) + Alternativer Text (optional) Typ - Ausblenden + Veröffentlichung zurück nehmen Zuletzt bearbeitet am + Letzter Änderungszeitpunkt des Dokuments Datei entfernen Link zum Dokument - Verweis auf Medienobjekt(e) - Ups! Dieses Dokument ist veröffentlicht aber nicht im internen Cache aufzufinden: Systemfehler. - Ursprünglicher Autor - Aktualisiert von - Erstellungszeitpunkt des Dokuments - Veröffentlichung widerrufen am - Letzter Änderungszeitpunkt des Dokuments Mitglied der Gruppe(n) Kein Mitglied der Gruppe(n) Untergeordnete Elemente + Ziel + + + Klicken zum Hochladen + Dateien hierher ziehen... An welcher Stellen wollen Sie das Element erstellen @@ -197,10 +232,24 @@ Bearbeiten Sie nachfolgend die verschiedenen Sprachversionen für den Wörterbucheintrag '<em>%0%</em>'. <br/>Unter dem links angezeigten Menüpunkt 'Sprachen' können Sie weitere hinzufügen. - Name der Kultur + Name der Sprachkultur + + + + +Benutzername eingeben + Passwort eingeben + Benenne %0%... + Name eingeben... + Suchbegriff eingeben... + Tippen zum Filtern... + Tippen für Schlagworte hinzuzufügen (drücke Enter nach jedem Schlagwort)... + Als Wurzel-Knoten erlauben + Nur Dokumententype mit dieser Markierung können auf Ebene 1 im Inhalts- und Medienverzeichnisbaum erstellt werden Dokumenttypen, die unterhalb dieses Typs erlaubt sind + Dokumententyp-Kompositionen Erstellen Registerkarte löschen Beschreibung @@ -208,6 +257,11 @@ Registerkarte Illustration Listenansicht aktivieren + Konfiguriert das Dokument zur Darstellung einer sortierbaren und durchsuchbaren Liste mit untergeordneten Dokumenten. Die untergeordneten Elemente werden im Verzeichnisbaum nicht mehr angezeigt. + Aktuelle Listenansicht + Datentyp der aktiven Listenansicht + Erstelle benutzerdefinierte Listenansicht + Entferne benutzerdefinierte Listenansicht Vorgabewert hinzufügen @@ -256,6 +310,7 @@ Info Aktion + Aktionen Hinzufügen Alias Sind Sie sicher? @@ -305,6 +360,7 @@ Abmelden Makro Verschieben + Mehr Name Neu Weiter @@ -324,6 +380,7 @@ Verbleibend Umbenennen Erneuern + Pflichtangaben Wiederholen Berechtigungen Suchen @@ -347,11 +404,11 @@ Breite Ja Ordner - Aktionen - Mehr - Pflichtangabe Suchergebnisse + + + Hintergrundfarbe Fett @@ -381,7 +438,7 @@ Keine Sorge - Dabei werden keine Inhalte gelöscht und alles wird weiterhin funktionieren! </p> - Die Datenbank wurde auf die Version %0% aktualisiert. Klicken Sie auf <strong>weiter</strong>, um fortzufahren. +Die Datenbank wurde auf die Version %0% aktualisiert. Klicken Sie auf <strong>weiter</strong>, um fortzufahren. Die Datenbank ist fertig eingerichtet. Klicken Sie auf <strong>"weiter"</strong>, um mit der Einrichtung fortzufahren. <strong>Das Kennwort des Standard-Benutzers muss geändert werden!</strong> <strong>Der Standard-Benutzer wurde deaktiviert oder hat keinen Zugriff auf Umbraco.</strong></p><p>Es sind keine weiteren Aktionen notwendig. Klicken Sie auf <b>Weiter</b> um fortzufahren. @@ -457,26 +514,27 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Dieser Assistent führt Sie durch die Einrichtung einer neuen Installation von <strong>Umbraco %0%</strong> oder einem Upgrade von Version 3.0.<br /><br />Klicken Sie auf <strong>weiter</strong>, um zu beginnen. - Code der Kultur - Name der Kultur + Code der Sprachkultur + Name der Sprachkultur Sie haben keine Tätigkeiten mehr durchgeführt und werden automatisch abgemeldet in Erneuern Sie, um Ihre Arbeit zu speichern ... - <p style="text-align:right;">&copy; 2001 - %0% <br /><a href="http://umbraco.com" style="text-decoration: none" target="_blank">umbraco.org</a></p> - Einen wunderbaren Sonntag - Frohen freundlichen Freitag - Donnerwetter Donnerstag - Schönen Montag - Einen großartigen Dienstag - Wunderbaren Mittwoch - Wunderbaren sonnigen Samstag + Einen wunderbaren Sonntag + Frohen freundlichen Freitag + Donnerwetter Donnerstag + Schönen Montag + Einen großartigen Dienstag + Wunderbaren Mittwoch + Wunderbaren sonnigen Samstag Hier anmelden: + Die Sitzung ist abgelaufen + <p style="text-align:right;">&copy; 2001 - %0% <br /><a href="http://umbraco.com" style="text-decoration: none" target="_blank">umbraco.org</a></p> - Dashboard + Armaturenbrett Bereiche Inhalt @@ -507,30 +565,33 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Ihr freundlicher Umbraco-Robot -<p>Hallo %0%,</p> +Hi %0%

-<p>die Aufgabe <strong>'%1%'</strong> (von Benutzer '%3%') an der Seite <a href="http://%4%/actions/preview.aspx?id=%5%"><strong>'%2%'</strong></a> wurde ausgeführt.</p> -<div style="margin: 8px 0; padding: 8px; display: block;"> - <br /> - <a style="color: white; font-weight: bold; background-color: #66cc66; text-decoration : none; margin-right: 20px; border: 8px solid #66cc66; width: 150px;" href="http://%4%/Umbraco/actions/publish.aspx?id=%5%">&nbsp;&nbsp;VERÖFFENTLICHEN&nbsp;&nbsp;</a> &nbsp; - <a style="color: white; font-weight: bold; background-color: #5372c3; text-decoration : none; margin-right: 20px; border: 8px solid #5372c3; width: 150px;" href="http://%4%/Umbraco/actions/editContent.aspx?id=%5%">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;BEARBEITEN&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</a> &nbsp; - <a style="color: white; font-weight: bold; background-color: #ca4a4a; text-decoration : none; margin-right: 20px; border: 8px solid #ca4a4a; width: 150px;" href="http://%4%/Umbraco/actions/delete.aspx?id=%5%">&nbsp;&nbsp;&nbsp;&nbsp;LÖSCHEN&nbsp;&nbsp;&nbsp;&nbsp;</a> - <br /> -</div> -<p> - <h3>Zusammenfassung der Aktualisierung:</h3> - <table style="width: 100%;"> - %6% - </table> - </p> -<div style="margin: 8px 0; padding: 8px; display: block;"> - <br /> - <a style="color: white; font-weight: bold; background-color: #66cc66; text-decoration : none; margin-right: 20px; border: 8px solid #66cc66; width: 150px;" href="http://%4%/Umbraco/actions/publish.aspx?id=%5%">&nbsp;&nbsp;VERÖFFENTLICHEN&nbsp;&nbsp;</a> &nbsp; - <a style="color: white; font-weight: bold; background-color: #5372c3; text-decoration : none; margin-right: 20px; border: 8px solid #5372c3; width: 150px;" href="http://%4%/Umbraco/actions/editContent.aspx?id=%5%">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;BEARBEITEN&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</a> &nbsp; - <a style="color: white; font-weight: bold; background-color: #ca4a4a; text-decoration : none; margin-right: 20px; border: 8px solid #ca4a4a; width: 150px;" href="http://%4%/Umbraco/actions/delete.aspx?id=%5%">&nbsp;&nbsp;&nbsp;&nbsp;LÖSCHEN&nbsp;&nbsp;&nbsp;&nbsp;</a> - <br /> -</div> -<p>Einen schönen Tag wünscht<br />Ihr freundlicher Umbraco-Robot</p> +

Dies ist eine automatisch E-Mail, welche Sie informiert, dass die Aufgabe '%1%' + an der Seite '%2%' + vom Benutzer '%3%' ausgeführt wurde. +

+ +

+

Zusammenfassung der Aktualisierung:

+ + %6% +
+

+ + + +

Einen schönen Tag wünscht

+ Ihr freundlicher Umbraco-Robot +

]]>
[%0%] Benachrichtigung: %1% ausgeführt an Seite '%2%' Benachrichtigungen @@ -599,17 +660,17 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die %0% kann nicht veröffentlicht werden, da die Veröffentlichung zeitlich geplant ist. - Neuer externer Link - Neuer interner Link - Link hinzufügen + Externer Link eingeben + Interne Seite auswählen + Link hinzufügen Beschriftung - interne Seite - URL - nach unten - nach oben - In neuem Fenster öffnen - Link entfernen - + In neuem Fenster öffnen + Neue Beschriftung eingeben + Link eingeben + + + Zurücksetzen + Aktuelle Version Zeigt die Unterschiede zwischen der aktuellen und der ausgewählten Version an.<br />Text in <del>rot</del> fehlen in der ausgewählten Version, <ins>grün</ins> markierter Text wurde hinzugefügt. @@ -635,8 +696,9 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Statistiken Übersetzung Benutzer - Umbraco Contour + Umbraco Forms Hilfe + Analytics Standardvorlage @@ -647,6 +709,7 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Typ Stylesheet Stylesheet-Eigenschaft + Skript Registerkarte Registerkartenbeschriftung Registerkarten @@ -654,6 +717,8 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Masterdokumenttyp aktiviert als Masterdokumenttyp. Register vom Masterdokumenttyp werden nicht angezeigt und können nur im Masterdokumenttyp selbst bearbeitet werden Für dieses Register sind keine Eigenschaften definiert. Klicken Sie oben auf "neue Eigenschaft hinzufügen", um eine neue Eigenschaft hinzuzufügen. + Master Dokumententyp + Erstelle zugehörige Vorlage Sortierung abgeschlossen. @@ -734,7 +799,63 @@ Wenn Sie sich für Runway entscheiden, können Sie optional Blöcke nutzen, die Schnellübersicht zu den verfügbaren Umbraco-Feldern Vorlage - + + Typ auswählen + Wählen Sie ein Layout für diese Seite + und fügen Sie ihr erstes Element hinzu.]]> + + Klicken zum Einfügen + Klicken zum Bild Einfügen + Bildbeschriftung... + Schreibe hier... + + Grid Layouts + Layouts sind der gesamte Arbeitsbereich des Grid Editors. Sie brauchen aber meistens nur ein oder zwei unterschiedliche Layouts. + Grid Layout hinzufügen + Layout einstellen (Spaltenbreite festlegen und zusätzliche Bereiche hinzufügen) + + Zeilenkonfiguration + Zeilen sind vordefinierte und horizontal angeordnete Zellen + Zeilenkonfiguration hinzufügen + Zeilen einstellen (Zellenbreite festlegen und zusätzliche Zellen hinzufügen) + + Spalten + Gesamtanzahl von allen kombinierten Spalten im Grid Layout + + Einstellungen + Konfiguriere, welche Einstellungen der Editor ändern kann + + + Stilangaben + Konfiguriere, welche Stilangaben Editoren ändern können + + Einstellungen werden nur mit eingetragener und gültiger JSON Konfiguration gespeichert + + Allen Editoren erlauben + Alle Zeilenkonfigurationen erlauben + + + + + + + + + + + + + + + + + + + + + + + Alternatives Feld Alternativer Text Groß- und Kleinschreibung @@ -820,7 +941,7 @@ Ihr freundlicher Umbraco-Robot Mitgliedergruppen Mitgliederrollen Mitglieder-Typen - Dokumenttypen + Dokumententypen Pakete Pakete Python-Dateien @@ -832,6 +953,7 @@ Ihr freundlicher Umbraco-Robot Stylesheets Vorlagen XSLT-Dateien + Analysen Neues Update verfügbar @@ -842,7 +964,9 @@ Ihr freundlicher Umbraco-Robot Administrator Feld für Kategorie - Kennwort ändern + Passwort ändern + Neues Passwort + Neues Kennwort (Bestätigung) Sie können Ihr Kennwort für den Zugriff auf den Umbraco-Verwaltungsbereich ändern, indem Sie das nachfolgende Formular ausfüllen und auf die 'Kennwort ändern'-Schaltfläche klicken Schnittstelle für externe Editoren Feld für Beschreibung @@ -856,7 +980,9 @@ Ihr freundlicher Umbraco-Robot Freigegebene Bereiche Zugang sperren Kennwort + Kennwort zurücksetzen Ihr Kennwort wurde geändert! + Bitte bestätigen Sie das neue Kennwort Aktuelle Kennwort Ungültig aktuelle Kennwort Geben Sie Ihr neues Kennwort ein @@ -873,10 +999,7 @@ Ihr freundlicher Umbraco-Robot Rolle Rollen Autor - Neues Kennwort - Neues Kennwort (Bestätigung) - Bitte bestätigen Sie das neue Kennwort - Kennwort zurücksetzen + Übersetzer Ihr Profil Ihr Verlauf Sitzung läuft ab in From b42959f663f4f017d2d08faf9d243fdc1999f47c Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 26 Jun 2015 16:59:40 +0200 Subject: [PATCH 09/50] Gets U4-6753 Identity support must have an option to enable auto-linked accounts working --- .../Models/Identity/BackOfficeIdentityUser.cs | 7 ++ .../Security/BackOfficeUserStore.cs | 16 +-- .../Editors/BackOfficeController.cs | 104 +++++++++++++++++- .../AuthenticationOptionsExtensions.cs | 25 +++++ .../Identity/ExternalSignInAutoLinkOptions.cs | 81 ++++++++++++++ .../Security/Identity/OwinExtensions.cs | 3 +- src/Umbraco.Web/Umbraco.Web.csproj | 1 + 7 files changed, 227 insertions(+), 10 deletions(-) create mode 100644 src/Umbraco.Web/Security/Identity/ExternalSignInAutoLinkOptions.cs diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs index 1523cf9040..9a53023a8d 100644 --- a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs @@ -12,6 +12,13 @@ namespace Umbraco.Core.Models.Identity public class BackOfficeIdentityUser : IdentityUser, IdentityUserClaim> { + public BackOfficeIdentityUser() + { + StartMediaId = -1; + StartContentId = -1; + Culture = Configuration.GlobalSettings.DefaultUILanguage; + } + public virtual async Task GenerateUserIdentityAsync(BackOfficeUserManager manager) { // NOTE the authenticationType must match the umbraco one diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index f6d8222c44..cf433b729c 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -75,12 +75,12 @@ namespace Umbraco.Core.Security { DefaultToLiveEditing = false, Email = user.Email, - Language = Configuration.GlobalSettings.DefaultUILanguage, + Language = user.Culture ?? Configuration.GlobalSettings.DefaultUILanguage, Name = user.Name, Username = user.UserName, - StartContentId = -1, - StartMediaId = -1, - IsLockedOut = false, + StartContentId = user.StartContentId == 0 ? -1 : user.StartContentId, + StartMediaId = user.StartMediaId == 0 ? -1 : user.StartMediaId, + IsLockedOut = user.LockoutEnabled, IsApproved = true }; @@ -168,7 +168,7 @@ namespace Umbraco.Core.Security /// /// /// - public Task FindByIdAsync(int userId) + public async Task FindByIdAsync(int userId) { ThrowIfDisposed(); var user = _userService.GetUserById(userId); @@ -176,7 +176,7 @@ namespace Umbraco.Core.Security { return null; } - return Task.FromResult(AssignLoginsCallback(Mapper.Map(user))); + return await Task.FromResult(AssignLoginsCallback(Mapper.Map(user))); } /// @@ -184,7 +184,7 @@ namespace Umbraco.Core.Security /// /// /// - public Task FindByNameAsync(string userName) + public async Task FindByNameAsync(string userName) { ThrowIfDisposed(); var user = _userService.GetByUsername(userName); @@ -195,7 +195,7 @@ namespace Umbraco.Core.Security var result = AssignLoginsCallback(Mapper.Map(user)); - return Task.FromResult(result); + return await Task.FromResult(result); } /// diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 8c1c23fbcb..ad1e4ea571 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -37,6 +37,7 @@ using System.Web; using AutoMapper; using Microsoft.AspNet.Identity.Owin; using Umbraco.Core.Models.Identity; +using Umbraco.Core.Models.Membership; using Umbraco.Core.Security; using Task = System.Threading.Tasks.Task; using Umbraco.Web.Security.Identity; @@ -478,7 +479,10 @@ namespace Umbraco.Web.Editors } else { - ViewBag.ExternalSignInError = new[] { "The requested provider (" + loginInfo.Login.LoginProvider + ") has not been linked to to an account" }; + if (await AutoLinkAndSignInExternalAccount(loginInfo) == false) + { + ViewBag.ExternalSignInError = new[] { "The requested provider (" + loginInfo.Login.LoginProvider + ") has not been linked to to an account" }; + } //Remove the cookie otherwise this message will keep appearing if (Response.Cookies[Core.Constants.Security.BackOfficeExternalCookieName] != null) @@ -490,6 +494,104 @@ namespace Umbraco.Web.Editors return response(); } + private async Task AutoLinkAndSignInExternalAccount(ExternalLoginInfo loginInfo) + { + //Here we can check if the provider associated with the request has been configured to allow + // new users (auto-linked external accounts). This would never be used with public providers such as + // Google, unless you for some reason wanted anybody to be able to access the backend if they have a Google account + // .... not likely! + + var authType = OwinContext.Authentication.GetExternalAuthenticationTypes().FirstOrDefault(x => x.AuthenticationType == loginInfo.Login.LoginProvider); + if (authType == null) + { + Logger.Warn("Could not find external authentication provider registered: " + loginInfo.Login.LoginProvider); + return false; + } + + var autoLinkOptions = authType.GetExternalAuthenticationOptions(); + if (autoLinkOptions != null) + { + if (autoLinkOptions.ShouldAutoLinkExternalAccount(UmbracoContext, loginInfo)) + { + //we are allowing auto-linking/creating of local accounts + if (loginInfo.Email.IsNullOrWhiteSpace()) + { + ViewBag.ExternalSignInError = new[] { "The requested provider (" + loginInfo.Login.LoginProvider + ") has not provided an email address, the account cannot be linked." }; + } + else + { + + //Now we need to perform the auto-link, so first we need to lookup/create a user with the email address + var foundByEmail = Services.UserService.GetByEmail(loginInfo.Email); + if (foundByEmail != null) + { + ViewBag.ExternalSignInError = new[] { "A user with this email address already exists locally. You will need to login locally to Umbraco and link this external provider: " + loginInfo.Login.LoginProvider }; + } + else + { + var defaultUserType = autoLinkOptions.GetDefaultUserType(UmbracoContext, loginInfo); + var userType = Services.UserService.GetUserTypeByAlias(defaultUserType); + if (userType == null) + { + ViewBag.ExternalSignInError = new[] { "Could not auto-link this account, the specified User Type does not exist: " + defaultUserType }; + } + else + { + //var userMembershipProvider = global::Umbraco.Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider(); + + var autoLinkUser = new BackOfficeIdentityUser() + { + Email = loginInfo.Email, + Name = loginInfo.ExternalIdentity.Name, + UserTypeAlias = userType.Alias, + AllowedSections = autoLinkOptions.GetDefaultAllowedSections(UmbracoContext, loginInfo), + Culture = autoLinkOptions.GetDefaultCulture(UmbracoContext, loginInfo), + UserName = loginInfo.Email + }; + var userCreationResult = await UserManager.CreateAsync(autoLinkUser); + + if (userCreationResult.Succeeded == false) + { + ViewBag.ExternalSignInError = userCreationResult.Errors; + } + else + { + var linkResult = await UserManager.AddLoginAsync(autoLinkUser.Id, loginInfo.Login); + if (linkResult.Succeeded == false) + { + ViewBag.ExternalSignInError = linkResult.Errors; + + //If this fails, we should really delete the user since it will be in an inconsistent state! + var deleteResult = await UserManager.DeleteAsync(autoLinkUser); + if (deleteResult.Succeeded == false) + { + //DOH! ... this isn't good, combine all errors to be shown + ViewBag.ExternalSignInError = linkResult.Errors.Concat(deleteResult.Errors); + } + } + else + { + + //Ok, we're all linked up! Assign the auto-link options to a ViewBag property, this can be used + // in the view to render a custom view (AutoLinkExternalAccountView) if required, which will allow + // a developer to display a custom angular view to prompt the user for more information if required. + ViewBag.ExternalSignInAutoLinkOptions = autoLinkOptions; + + //sign in + await SignInAsync(autoLinkUser, isPersistent: false); + } + } + } + } + + } + } + return true; + } + + return false; + } + private async Task SignInAsync(BackOfficeIdentityUser user, bool isPersistent) { OwinContext.Authentication.SignOut(Core.Constants.Security.BackOfficeExternalAuthenticationType); diff --git a/src/Umbraco.Web/Security/Identity/AuthenticationOptionsExtensions.cs b/src/Umbraco.Web/Security/Identity/AuthenticationOptionsExtensions.cs index 43b995bb06..2d4aef52fa 100644 --- a/src/Umbraco.Web/Security/Identity/AuthenticationOptionsExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AuthenticationOptionsExtensions.cs @@ -7,6 +7,31 @@ namespace Umbraco.Web.Security.Identity { public static class AuthenticationOptionsExtensions { + /// + /// Used during the External authentication process to assign external sign-in options + /// that are used by the Umbraco authentication process. + /// + /// + /// + public static void SetExternalAuthenticationOptions( + this AuthenticationOptions authOptions, + ExternalSignInAutoLinkOptions options) + { + authOptions.Description.Properties["ExternalSignInAutoLinkOptions"] = options; + } + + /// + /// Used during the External authentication process to retrieve external sign-in options + /// that have been set with SetExternalAuthenticationOptions + /// + /// + public static ExternalSignInAutoLinkOptions GetExternalAuthenticationOptions(this AuthenticationDescription authenticationDescription) + { + if (authenticationDescription.Properties.ContainsKey("ExternalSignInAutoLinkOptions") == false) return null; + var options = authenticationDescription.Properties["ExternalSignInAutoLinkOptions"] as ExternalSignInAutoLinkOptions; + return options; + } + /// /// Configures the properties of the authentication description instance for use with Umbraco back office /// diff --git a/src/Umbraco.Web/Security/Identity/ExternalSignInAutoLinkOptions.cs b/src/Umbraco.Web/Security/Identity/ExternalSignInAutoLinkOptions.cs new file mode 100644 index 0000000000..9fb9a88a45 --- /dev/null +++ b/src/Umbraco.Web/Security/Identity/ExternalSignInAutoLinkOptions.cs @@ -0,0 +1,81 @@ +using Microsoft.AspNet.Identity.Owin; +using Microsoft.Owin; +using Umbraco.Core; +using Umbraco.Core.Configuration; + +namespace Umbraco.Web.Security.Identity +{ + /// + /// Options used to configure auto-linking external OAuth providers + /// + public sealed class ExternalSignInAutoLinkOptions + { + + public ExternalSignInAutoLinkOptions( + bool autoLinkExternalAccount = false, + string defaultUserType = "editor", string[] defaultAllowedSections = null, string defaultCulture = null, string autoLinkExternalAccountView = null) + { + Mandate.ParameterNotNullOrEmpty(defaultUserType, "defaultUserType"); + + _defaultUserType = defaultUserType; + _defaultAllowedSections = defaultAllowedSections ?? new[] { "content", "media" }; + _autoLinkExternalAccount = autoLinkExternalAccount; + _autoLinkExternalAccountView = autoLinkExternalAccountView; + _defaultCulture = defaultCulture ?? GlobalSettings.DefaultUILanguage; + } + + private readonly string _defaultUserType; + + /// + /// The default User Type alias to use for auto-linking users + /// + public string GetDefaultUserType(UmbracoContext umbracoContext, ExternalLoginInfo loginInfo) + { + return _defaultUserType; + } + + private readonly string[] _defaultAllowedSections; + + /// + /// The default allowed sections to use for auto-linking users + /// + public string[] GetDefaultAllowedSections(UmbracoContext umbracoContext, ExternalLoginInfo loginInfo) + { + return _defaultAllowedSections; + } + + private readonly bool _autoLinkExternalAccount; + + /// + /// For private external auth providers such as Active Directory, which when set to true will automatically + /// create a local user if the external provider login was successful. + /// + /// For public auth providers this should always be false!!! + /// + public bool ShouldAutoLinkExternalAccount(UmbracoContext umbracoContext, ExternalLoginInfo loginInfo) + { + return _autoLinkExternalAccount; + } + + private readonly string _autoLinkExternalAccountView; + + /// + /// Generally this is empty which means auto-linking will be silent, however in some cases developers may want to + /// prompt the user to enter additional user information that they want to save with the user that has been created. + /// + public string GetAutoLinkExternalAccountView(UmbracoContext umbracoContext, ExternalLoginInfo loginInfo) + { + return _autoLinkExternalAccountView; + } + + private readonly string _defaultCulture; + + /// + /// The default Culture to use for auto-linking users + /// + public string GetDefaultCulture(UmbracoContext umbracoContext, ExternalLoginInfo loginInfo) + { + return _defaultCulture; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Security/Identity/OwinExtensions.cs b/src/Umbraco.Web/Security/Identity/OwinExtensions.cs index 4b83f97bd3..ceb0bdafb2 100644 --- a/src/Umbraco.Web/Security/Identity/OwinExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/OwinExtensions.cs @@ -5,12 +5,13 @@ namespace Umbraco.Web.Security.Identity { internal static class OwinExtensions { + /// /// Nasty little hack to get httpcontextbase from an owin context /// /// /// - public static HttpContextBase HttpContextFromOwinContext(this IOwinContext owinContext) + internal static HttpContextBase HttpContextFromOwinContext(this IOwinContext owinContext) { return owinContext.Get(typeof(HttpContextBase).FullName); } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index ea3f838076..b9df10254d 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -304,6 +304,7 @@ + From 56315768e9d4425498aa3946956519faef7c9978 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 26 Jun 2015 17:04:40 +0200 Subject: [PATCH 10/50] updates method name --- .../Security/Identity/AuthenticationOptionsExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Security/Identity/AuthenticationOptionsExtensions.cs b/src/Umbraco.Web/Security/Identity/AuthenticationOptionsExtensions.cs index 2d4aef52fa..97b7e85e37 100644 --- a/src/Umbraco.Web/Security/Identity/AuthenticationOptionsExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AuthenticationOptionsExtensions.cs @@ -13,7 +13,7 @@ namespace Umbraco.Web.Security.Identity /// /// /// - public static void SetExternalAuthenticationOptions( + public static void SetExternalSignInAutoLinkOptions( this AuthenticationOptions authOptions, ExternalSignInAutoLinkOptions options) { From 671026cfc98f38f7a9031eb8e10e10d2f91a612c Mon Sep 17 00:00:00 2001 From: Jesse Date: Mon, 29 Jun 2015 08:30:13 +0200 Subject: [PATCH 11/50] Edited swedish translations Added a few missing and removed unused translations. --- src/Umbraco.Web.UI/umbraco/config/lang/sv.xml | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml b/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml index b619025624..84b885a931 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/sv.xml @@ -554,6 +554,7 @@ Fyll i ditt lösenord Skriv för att söka... Fyll i ditt lösenord + Skriv för att lägga till taggar (och tryck enter efter varje tagg)... Rollbaserat lösenordsskydd @@ -587,16 +588,16 @@ ok för att publicera %0%. Därmed blir innehållet publikt.

Du kan publicera denna sida och alla dess undersidor genom att kryssa i publicera alla undersidor. ]]>
- Lägg till extern länk - Lägg till intern länk - Lägg till + ange en extern länk + ange en intern sida Rubrik - Intern sida - URL - Flytta ner - Flytta upp + Länk Öppna i nytt fönster - Ta bort länk + Ange visningstext + Ange adress + + + Återställ Nuvarande version @@ -644,6 +645,8 @@ Flik Fliknamn Flikar + Huvuddokumenttyp + Skapa matchande mall Sortering klar @@ -885,6 +888,7 @@ Användartyper Skribent Din nuvarande historik + Översättare Din profil \ No newline at end of file From bdd7d7280d90dc0a47fb513965885d7c8f315992 Mon Sep 17 00:00:00 2001 From: craig Date: Mon, 29 Jun 2015 22:26:08 +0100 Subject: [PATCH 12/50] U4-6773 create property - available properties are not sorted a-z --- .../umbraco/controls/ContentTypeControlNew.ascx.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/controls/ContentTypeControlNew.ascx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/controls/ContentTypeControlNew.ascx.cs index fff0ff6595..eed2947b23 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/controls/ContentTypeControlNew.ascx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/controls/ContentTypeControlNew.ascx.cs @@ -842,7 +842,7 @@ jQuery(document).ready(function() {{ refreshDropDowns(); }}); { var tabs = _contentType.getVirtualTabs; var propertyTypeGroups = _contentType.PropertyTypeGroups.ToList(); - var dtds = cms.businesslogic.datatype.DataTypeDefinition.GetAll(); + var dtds = cms.businesslogic.datatype.DataTypeDefinition.GetAll().OrderBy(d => d.Text).ToArray(); PropertyTypes.Controls.Clear(); From 048a8683dd1dfc623805c83096929f7a73ff164c Mon Sep 17 00:00:00 2001 From: Tom Fulton Date: Mon, 29 Jun 2015 17:48:01 -0600 Subject: [PATCH 13/50] Support virtual paths for PreValueField attributes (U4-6775) --- src/Umbraco.Core/PropertyEditors/PreValueEditor.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/PropertyEditors/PreValueEditor.cs b/src/Umbraco.Core/PropertyEditors/PreValueEditor.cs index 89e17438f3..d773eed2e9 100644 --- a/src/Umbraco.Core/PropertyEditors/PreValueEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/PreValueEditor.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using Newtonsoft.Json; +using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models; @@ -84,7 +85,7 @@ namespace Umbraco.Core.PropertyEditors Name = att.Name, Description = att.Description, HideLabel = att.HideLabel, - View = att.View + View = att.View.StartsWith("~/") ? IOHelper.ResolveUrl(att.View) : att.View }; } From 974f827e8bd948d3554ee61d6e9a657adffb3ed4 Mon Sep 17 00:00:00 2001 From: shwld Date: Wed, 1 Jul 2015 01:18:29 +0900 Subject: [PATCH 14/50] update language conf file (ja) --- src/Umbraco.Web.UI/umbraco/config/lang/ja.xml | 180 ++++++++++++++++-- 1 file changed, 163 insertions(+), 17 deletions(-) diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/ja.xml b/src/Umbraco.Web.UI/umbraco/config/lang/ja.xml index cd0bfe03d7..237f0cc4b3 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/ja.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/ja.xml @@ -8,6 +8,7 @@ ドメインの割り当て 動作記録 ノードの参照 + ドキュメントタイプの変更 コピー 新規作成 パッケージの作成 @@ -26,6 +27,7 @@ 公開を止める 最新の情報に更新 サイトのリフレッシュ + 復元 アクセス権 以前の版に戻る 公開に送る @@ -34,22 +36,35 @@ 公開する 翻訳 更新 + 初期値 + アクセスが拒否されました ドメインの割り当て + ドメインの削除 + 適当でないノード名 適当でないホスト名 + そのホスト名は既に利用されています + 言語コード ドメイン ドメイン '%0%' が新たに割り当てられました ドメイン '%0%' は削除されました ドメイン '%0%' は既に割り当てられています - 例: yourdomain.com, www.yourdomain.com ドメイン '%0%' は更新されました ドメインの編集 + + Inherit + カルチャの割り当て + + ドメインの割り当て これらを表示 + 選択 + 現在のフォルダを選択 + その他のアクション 太字 インデント解除 フィールドから挿入 @@ -67,6 +82,7 @@ マクロの挿入 画像の挿入 関係性の編集 + リストに戻る 保存 保存及び公開 保存して承認に送る @@ -76,20 +92,45 @@ スタイルの表示 表の挿入 + + ドキュメントタイプを変更するには、まず有効なドキュメントタイプのリストから選択します + 確認および現在のドキュメントタイプからのマッピングを割り当て、保存します。 + コンテントは再公開されています + 現在のプロパティ + 現在のドキュメントタイプ + 有効な代替タイプが存在しないため変更することができません。選択されたコンテントの親の下に許可されたドキュメントタイプへのみ変更ができます + ドキュメントタイプを変更しました + プロパティを割り当てる + 割り当てるプロパティ + 新しいテンプレート + 新しいドキュメントタイプ + None + コンテント + ドキュメントタイプを変更する + プロパティが以下のように割り当てられました + から + 1つ以上のプロパティを割り当てられませんでした。プロパティが定義が重複しています + 有効なドキュメントタイプのみが表示されます + + 公開されました このページについて エイリアス (画像を電話でわかるように言葉で説明) 別名のリンク クリックでアイテムを編集する 作成者 + 作成者 + 更新者 作成日時 + このドキュメントが作成された日時 ドキュメントタイプ 変種中 公開終了日時 このページは公開後変更されています このページは公開されていません 公開日時 + リストに表示するアイテムはありません メディアタイプ メディアの項目へのリンク メンバーグループ @@ -99,24 +140,38 @@ タイトル プロパティ このページは公開されましたが、親ページの '%0%' が非公開のため閲覧できません + このコンテントは公開されていますがキャッシュされていません(内部エラー) 公開 公開状態 公開開始日時 + 公開停止日時 日時の消去 並び順が更新されました ノードをドラッグ、クリック、または列のヘッダーをクリックする事でノードを簡単にソートできます。SHIFT、CONTROLキーを使い複数のノードを選択する事もできます。 統計 タイトル (オプション) + 代替テキスト (オプション) 非公開 最終更新日時 + このドキュメントが最後に更新された日時 ファイルの消去 ページへのリンク + グループのメンバー + グループのメンバーではありません + 子コンテンツ + ターゲット + + + クリックしてアップロードする + ファイルをここへドロップ.. どこに新しい %0% を作りますか ここに作成 型とタイトルを選んでください + "document types".]]> + "media types".]]> ウェブサイトを参照する @@ -181,14 +236,32 @@ ]]> カルチャ名 + + ユーザー名を入力... + パスワードを入力... + %0%と命名します... + ここに名称を入力してください... + 検索する... + 条件で絞り込む... + タグを追加します... + + ルートノードとして許可する + これを有効にするとコンテンツとメディアツリーのルートレベルに作成することができます 子ノードとして許可するタイプ + Document Type Compositions 新規作成 削除 説明 新規見出し 見出し サムネイル + リストビューを有効にする + 子ノードをツリーに表示せずにリストビューに表示します + 現在のリストビュー + 有効なリストビューデータタイプ + カスタムリストビューを作成する + カスタムリストビューを削除する 値の前に追加 @@ -217,7 +290,8 @@ %0% は正しい書式ではありません - 注意! CodeMirrorが設定で有効かされていますが、 Internet Explorerでは不安定なので無効化してください。 + 指定されたファイルタイプは管理者のみに許可されます + 注意! CodeMirrorが設定で有効化されていますが、 Internet Explorerでは不安定なので無効化してください。 新しいプロパティ型のエイリアスと名前の両方を設定してください! 特定のファイルまたはフォルタの読み込み/書き込みアクセスに問題があります タイトルを入力してください @@ -232,10 +306,12 @@ このセルは結合されたものではないので分離する事はできません。 XSLTソースにエラーがあります 1つ以上のエラーがあるのでこのXSLTは保存できませんでした + このプロパティに使用されているデータタイプにエラーがあります Umbracoについて アクション + アクション選択 追加 エイリアス 確かですか? @@ -285,6 +361,7 @@ ログアウト マクロ 移動 + もっと 名前 新規 次へ @@ -304,6 +381,7 @@ 残り 名前の変更 更新 + この項目は必須です 再試行 許可 検索 @@ -313,7 +391,7 @@ サイズ 並べ替え - 探す型... + 検索... 更新 アップグレード @@ -327,6 +405,7 @@ はい フォルダー + 検索結果 背景色 @@ -348,10 +427,10 @@ ]]> 次へを押して続行してください。]]> データベースを見つけられません!"web.config"ファイルの中の"接続文字列"を確認してください。

-

続行するには"web.config"ファイルを編集(Visual Studioないし使い慣れたテキストエディタで)し、下の方にスクロールし、"UmbracoDbDSN"という名前のキーでデータベースの接続文字列を追加して保存します。

+

続行するには"web.config"ファイルを編集(Visual Studioないし使い慣れたテキストエディタで)し、下の方にスクロールし、"umbracoDbDSN"という名前のキーでデータベースの接続文字列を追加して保存します。

再施行ボタンをクリックして - 続けます。
+ 続けます。
より詳細にはこちらの web.config を編集します。

]]>
必要ならISPに連絡するなどしてみてください。 @@ -380,7 +459,7 @@

]]>
始めに、ビデオによる解説を見ましょう - 次へボタンをクリック(またはweb.configのUmbracoConfigurationStatusを編集)すると、あなたはここに示されるこのソフトウェアのライセンスを承諾したと見做されます。注意として、UmbracoはMITライセンスをフレームワークへ、フリーウェアライセンスをUIへ、それぞれ異なる2つのライセンスを採用しています。 + 次へボタンをクリック(またはweb.configのumbracoConfigurationStatusを編集)すると、あなたはここに示されるこのソフトウェアのライセンスを承諾したと見做されます。注意として、UmbracoはMITライセンスをフレームワークへ、フリーウェアライセンスをUIへ、それぞれ異なる2つのライセンスを採用しています。 まだインストールは完了していません。 影響するファイルとフォルダ Umbracoに必要なアクセス権の設定についての詳細はこちらをどうぞ @@ -408,7 +487,7 @@ スクラッチから始めたい どうしたらいいの?) + (どうしたらいいの?) 後からRunwayをインストールする事もできます。そうしたくなった時は、Developerセクションのパッケージへどうぞ。 ]]> Umbracoプラットフォームのクリーンセットアップが完了しました。この後はどうしますか? @@ -444,7 +523,7 @@ Runwayをインストールして作られた新しいウェブサイトがど 我々の認めるコミュニティから手助けを得られるでしょう。どうしたら簡単なサイトを構築できるか、どうしたらパッケージを使えるかについてのビデオや文書、またUmbracoの用語のクイックガイドも見る事ができます。]]> Umbraco %0% のインストールは完了、準備が整いました /web.config fileを手作業で編集し、'%0%'の下にあるUmbracoConfigurationStatusキーを設定してください。]]> + /web.config fileを手作業で編集し、'%0%'の下にあるumbracoConfigurationStatusキーを設定してください。]]> 今すぐ開始できます。
もしUmbracoの初心者なら、 私たちの初心者向けのたくさんの情報を参考にしてください。]]>
Umbracoの開始 @@ -453,7 +532,7 @@ Runwayをインストールして作られた新しいウェブサイトがど Umbraco Version 3 Umbraco Version 4 見る - Umbraco %0% の新規インストールまたは3.0からの更新について設定方法を案内します。 + umbraco %0% の新規インストールまたは3.0からの更新について設定方法を案内します。

"次へ"を押してウィザードを開始します。]]>
@@ -466,7 +545,16 @@ Runwayをインストールして作られた新しいウェブサイトがど 作業を保存して今すぐ更新 - © 2001 - %0%
umbraco.com

]]>
+ Happy super sunday + Happy manic monday + Happy tubular tuesday + Happy wonderful wednesday + Happy thunder thursday + Happy funky friday + Happy caturday + ウェブサイトにログインします。 + セッションタイムアウトしました。 + © 2001 - %0%
umbraco.org

]]>
Umbraco にようこそ。ユーザー名とパスワードを入力してください: @@ -549,7 +637,7 @@ Runwayをインストールして作られた新しいウェブサイトがど 本当にアンインストールしますか パッケージのアンインストールが終了しました パッケージが正常にアンインストールされました - パッケージのアンンストール + パッケージのアンインストール 注意: 全ての、文書やメディアなどに依存したアイテムを削除する場合はそれらの作業を一端止めてからアンインストールしなければシステムが不安定になる恐れがあります。 疑問点などあればパッケージの作者へ連絡してください。]]> @@ -558,6 +646,7 @@ Runwayをインストールして作られた新しいウェブサイトがど 更新の手順 このパッケージの更新があります。Umbracoのパッケージリポジトリから直接ダウンロードできます。 パッケージのバージョン + パッケージのバージョン履歴 パッケージのウェブサイトを見る @@ -585,14 +674,15 @@ Runwayをインストールして作られた新しいウェブサイトがど 単一のログインとパスワードで単純に保護したい場合に適します + - - - + ]]>
非公開の子ページも含めます 公開を進めています - 少々お待ちください... %1% ページ中 %0% ページが公開されました... @@ -615,6 +705,9 @@ Runwayをインストールして作られた新しいウェブサイトがど 新規ウィンドウで開く リンクを削除 + + リセット + 現在の版 の文字列は以前の版にはない部分で、緑の文字列は以前の版にのみある部分です。]]> @@ -640,6 +733,9 @@ Runwayをインストールして作られた新しいウェブサイトがど 統計 翻訳 ユーザー + ヘルプ + フォーム + アナリティクス 既定のテンプレート @@ -649,6 +745,7 @@ Runwayをインストールして作られた新しいウェブサイトがど ノードのタイプ タイプ スタイルシート + スクリプト スタイルシートのプロパティ タブ タブの名前 @@ -656,6 +753,9 @@ Runwayをインストールして作られた新しいウェブサイトがど マスターコンテンツタイプが有効 このコンテンツタイプの使用 マスターコンテンツタイプについては、マスターコンテンツタイプからのタブは表示されず、マスターコンテンツタイプでのみ編集することができます。 + このタブにはプロパティが定義されていません、上部のリンクから新しいプロパティを作成してください + マスタードキュメントタイプ + テンプレートを作成する ソートが完了しました。 @@ -684,6 +784,8 @@ Runwayをインストールして作られた新しいウェブサイトがど 変更を適用する為に公開する事を忘れないでください 承認へ送りました 変更は承認へと送られます + メディアを保存しました + メディアをエラーなく保存しました メンバーを保存しました スタイルシートのプロパティを保存しました スタイルシートを保存しました @@ -729,15 +831,53 @@ Runwayをインストールして作られた新しいウェブサイトがど コンテンツ領域プレースホルダーの挿入 dictionary item の挿入 マクロの挿入 - Umbraco ページフィールドの挿入 + umbraco ページフィールドの挿入 マスターテンプレート - Umbraco テンプレートタグのクイックガイド + umbraco テンプレートタグのクイックガイド テンプレート + + + 挿入するアイテムを選択する + ここからレイアウトを選択します + 最初の要素を追加します]]> + + クリックして埋め込む + クリックして画像を挿入する + キャプション... + ここに記入する... + + レイアウト + レイアウトは通常1つまたは2つの異なるレイアウトを必要とする、グリッドエディタの全体的な作業エリアです + レイアウトを追加する + 追加のセクションの横幅を設定し、レイアウトを調整する + + 行の構成 + 定義された構成の行が水平に配置されます + 行の構成を追加 + 追加のセルのセル幅を設定することで調整します + + + グリッドレイアウトの列を合計した数 + + 設定 + 編集者が設定できる項目 + + + スタイル + 編集者が設定できるスタイル + + 入力されたJSONが正しい場合のみ設定が保存されます + + すべてのエディタを許可する + すべての行の構成を許可する + + 代替フィールド 代替テキスト 大文字小文字変換 + エンコーディング フィールドの選択 改行コードの変換 改行コードをhtmlタグ &lt;br&gt; に変換する @@ -838,6 +978,7 @@ Runwayをインストールして作られた新しいウェブサイトがど スタイルシート テンプレート XSLT ファイル + アナリティクス 新しい更新があります @@ -864,6 +1005,7 @@ Runwayをインストールして作られた新しいウェブサイトがど セクション Umbracoへのアクセスを無効にする パスワード + パスワードのリセット パスワードが変更されました! 新しいパスワードの確認 新しいパスワードの入力 @@ -882,5 +1024,9 @@ Runwayをインストールして作られた新しいウェブサイトがど ユーザーの種類 ユーザーの種類 投稿者 + 翻訳者 + あなたのプロフィール + あなたの最新の履歴 + セッションの期限 From 44943e367f76aa889a850e352dc93bc7d13712ea Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 1 Jul 2015 13:46:11 +0200 Subject: [PATCH 15/50] Updates async logging to a newer format --- ...ormatter.cs => AppDomainTokenConverter.cs} | 0 .../Logging/AsyncForwardingAppenderBase.cs | 105 ++++ .../AsynchronousRollingFileAppender.cs | 480 +++++++++--------- .../Logging/LoggingEventContext.cs | 17 + .../Logging/LoggingEventHelper.cs | 31 ++ .../Logging/ParallelForwardingAppender.cs | 307 +++++++++++ src/Umbraco.Core/Umbraco.Core.csproj | 6 +- .../AsynchronousRollingFileAppenderTests.cs | 173 ------- .../Logging/AsyncRollingFileAppenderTest.cs | 166 ++++++ src/Umbraco.Tests/Logging/DebugAppender.cs | 22 + .../Logging/ParallelForwarderTest.cs | 324 ++++++++++++ src/Umbraco.Tests/Logging/RingBufferTest.cs | 147 ++++++ src/Umbraco.Tests/Umbraco.Tests.csproj | 5 +- .../config/log4net.Release.config | 8 +- src/Umbraco.Web.UI/config/log4net.config | 19 +- 15 files changed, 1391 insertions(+), 419 deletions(-) rename src/Umbraco.Core/Logging/{AppDomainTokenFormatter.cs => AppDomainTokenConverter.cs} (100%) create mode 100644 src/Umbraco.Core/Logging/AsyncForwardingAppenderBase.cs create mode 100644 src/Umbraco.Core/Logging/LoggingEventContext.cs create mode 100644 src/Umbraco.Core/Logging/LoggingEventHelper.cs create mode 100644 src/Umbraco.Core/Logging/ParallelForwardingAppender.cs delete mode 100644 src/Umbraco.Tests/AsynchronousRollingFileAppenderTests.cs create mode 100644 src/Umbraco.Tests/Logging/AsyncRollingFileAppenderTest.cs create mode 100644 src/Umbraco.Tests/Logging/DebugAppender.cs create mode 100644 src/Umbraco.Tests/Logging/ParallelForwarderTest.cs create mode 100644 src/Umbraco.Tests/Logging/RingBufferTest.cs diff --git a/src/Umbraco.Core/Logging/AppDomainTokenFormatter.cs b/src/Umbraco.Core/Logging/AppDomainTokenConverter.cs similarity index 100% rename from src/Umbraco.Core/Logging/AppDomainTokenFormatter.cs rename to src/Umbraco.Core/Logging/AppDomainTokenConverter.cs diff --git a/src/Umbraco.Core/Logging/AsyncForwardingAppenderBase.cs b/src/Umbraco.Core/Logging/AsyncForwardingAppenderBase.cs new file mode 100644 index 0000000000..74a1de81f4 --- /dev/null +++ b/src/Umbraco.Core/Logging/AsyncForwardingAppenderBase.cs @@ -0,0 +1,105 @@ +using System; +using log4net.Appender; +using log4net.Core; +using log4net.Util; + +namespace Umbraco.Core.Logging +{ + /// + /// Based on https://github.com/cjbhaines/Log4Net.Async + /// + public abstract class AsyncForwardingAppenderBase : ForwardingAppender + { + #region Private Members + + private const FixFlags DefaultFixFlags = FixFlags.Partial; + private FixFlags _fixFlags = DefaultFixFlags; + private LoggingEventHelper _loggingEventHelper; + + #endregion Private Members + + #region Properties + + public FixFlags Fix + { + get { return _fixFlags; } + set { SetFixFlags(value); } + } + + /// + /// The logger name that will be used for logging internal errors. + /// + protected abstract string InternalLoggerName { get; } + + public abstract int? BufferSize { get; set; } + + #endregion Properties + + public override void ActivateOptions() + { + base.ActivateOptions(); + _loggingEventHelper = new LoggingEventHelper(InternalLoggerName, DefaultFixFlags); + InitializeAppenders(); + } + + #region Appender Management + + public override void AddAppender(IAppender newAppender) + { + base.AddAppender(newAppender); + SetAppenderFixFlags(newAppender); + } + + private void SetFixFlags(FixFlags newFixFlags) + { + if (newFixFlags != _fixFlags) + { + _loggingEventHelper.Fix = newFixFlags; + _fixFlags = newFixFlags; + InitializeAppenders(); + } + } + + private void InitializeAppenders() + { + foreach (var appender in Appenders) + { + SetAppenderFixFlags(appender); + } + } + + private void SetAppenderFixFlags(IAppender appender) + { + var bufferingAppender = appender as BufferingAppenderSkeleton; + if (bufferingAppender != null) + { + bufferingAppender.Fix = Fix; + } + } + + #endregion Appender Management + + #region Forwarding + + protected void ForwardInternalError(string message, Exception exception, Type thisType) + { + LogLog.Error(thisType, message, exception); + var loggingEvent = _loggingEventHelper.CreateLoggingEvent(Level.Error, message, exception); + ForwardLoggingEvent(loggingEvent, thisType); + } + + protected void ForwardLoggingEvent(LoggingEvent loggingEvent, Type thisType) + { + try + { + base.Append(loggingEvent); + } + catch (Exception exception) + { + LogLog.Error(thisType, "Unable to forward logging event", exception); + } + } + + #endregion Forwarding + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Logging/AsynchronousRollingFileAppender.cs b/src/Umbraco.Core/Logging/AsynchronousRollingFileAppender.cs index 56d04c8426..cb58ebbfaa 100644 --- a/src/Umbraco.Core/Logging/AsynchronousRollingFileAppender.cs +++ b/src/Umbraco.Core/Logging/AsynchronousRollingFileAppender.cs @@ -1,276 +1,276 @@ +using log4net.Core; +using log4net.Util; using System; +using System.Runtime.Remoting.Messaging; using System.Security.Principal; using System.Threading; using System.Threading.Tasks; using log4net.Appender; -using log4net.Core; -using log4net.Util; namespace Umbraco.Core.Logging { - /// - /// Based on code by Chris Haines http://cjbhaines.wordpress.com/2012/02/13/asynchronous-log4net-appenders/ + /// + /// Based on https://github.com/cjbhaines/Log4Net.Async + /// which is based on code by Chris Haines http://cjbhaines.wordpress.com/2012/02/13/asynchronous-log4net-appenders/ /// public class AsynchronousRollingFileAppender : RollingFileAppender { - private readonly ManualResetEvent _manualResetEvent; - private int _bufferOverflowCounter; - private bool _forceStop; - private bool _hasFinished; - private DateTime _lastLoggedBufferOverflow; - private bool _logBufferOverflow; - private RingBuffer _pendingAppends; - private int _queueSizeLimit = 1000; - private bool _shuttingDown; + private RingBuffer pendingAppends; + private readonly ManualResetEvent manualResetEvent; + private bool shuttingDown; + private bool hasFinished; + private bool forceStop; + private bool logBufferOverflow; + private int bufferOverflowCounter; + private DateTime lastLoggedBufferOverflow; + private int queueSizeLimit = 1000; + public int QueueSizeLimit + { + get + { + return queueSizeLimit; + } + set + { + queueSizeLimit = value; + } + } - public AsynchronousRollingFileAppender() - { - _manualResetEvent = new ManualResetEvent(false); - } + public AsynchronousRollingFileAppender() + { + manualResetEvent = new ManualResetEvent(false); + } - public int QueueSizeLimit - { - get { return _queueSizeLimit; } - set { _queueSizeLimit = value; } - } + public override void ActivateOptions() + { + base.ActivateOptions(); + pendingAppends = new RingBuffer(QueueSizeLimit); + pendingAppends.BufferOverflow += OnBufferOverflow; + StartAppendTask(); + } - public override void ActivateOptions() - { - base.ActivateOptions(); - _pendingAppends = new RingBuffer(QueueSizeLimit); - _pendingAppends.BufferOverflow += OnBufferOverflow; - StartAppendTask(); - } + protected override void Append(LoggingEvent[] loggingEvents) + { + Array.ForEach(loggingEvents, Append); + } - protected override void Append(LoggingEvent[] loggingEvents) - { - Array.ForEach(loggingEvents, Append); - } + protected override void Append(LoggingEvent loggingEvent) + { + if (FilterEvent(loggingEvent)) + { + pendingAppends.Enqueue(loggingEvent); + } + } - protected override void Append(LoggingEvent loggingEvent) - { - if (FilterEvent(loggingEvent)) - { - _pendingAppends.Enqueue(loggingEvent); - } - } + protected override void OnClose() + { + shuttingDown = true; + manualResetEvent.WaitOne(TimeSpan.FromSeconds(5)); - protected override void OnClose() - { - _shuttingDown = true; - _manualResetEvent.WaitOne(TimeSpan.FromSeconds(5)); + if (!hasFinished) + { + forceStop = true; + base.Append(new LoggingEvent(new LoggingEventData + { + Level = Level.Error, + Message = "Unable to clear out the AsyncRollingFileAppender buffer in the allotted time, forcing a shutdown", + TimeStamp = DateTime.UtcNow, + Identity = "", + ExceptionString = "", + UserName = WindowsIdentity.GetCurrent() != null ? WindowsIdentity.GetCurrent().Name : "", + Domain = AppDomain.CurrentDomain.FriendlyName, + ThreadName = Thread.CurrentThread.ManagedThreadId.ToString(), + LocationInfo = new LocationInfo(this.GetType().Name, "OnClose", "AsyncRollingFileAppender.cs", "75"), + LoggerName = this.GetType().FullName, + Properties = new PropertiesDictionary(), + }) + ); + } - if (!_hasFinished) - { - _forceStop = true; - var windowsIdentity = WindowsIdentity.GetCurrent(); + base.OnClose(); + } - var logEvent = new LoggingEvent(new LoggingEventData - { - Level = global::log4net.Core.Level.Error, - Message = - "Unable to clear out the AsynchronousRollingFileAppender buffer in the allotted time, forcing a shutdown", - TimeStamp = DateTime.UtcNow, - Identity = "", - ExceptionString = "", - UserName = windowsIdentity != null ? windowsIdentity.Name : "", - Domain = AppDomain.CurrentDomain.FriendlyName, - ThreadName = Thread.CurrentThread.ManagedThreadId.ToString(), - LocationInfo = - new LocationInfo(this.GetType().Name, "OnClose", "AsynchronousRollingFileAppender.cs", "59"), - LoggerName = this.GetType().FullName, - Properties = new PropertiesDictionary(), - }); + private void StartAppendTask() + { + if (!shuttingDown) + { + Task appendTask = new Task(AppendLoggingEvents, TaskCreationOptions.LongRunning); + appendTask.LogErrors(LogAppenderError).ContinueWith(x => StartAppendTask()).LogErrors(LogAppenderError); + appendTask.Start(); + } + } - if (this.DateTimeStrategy != null) - { - base.Append(logEvent); - } - } + private void LogAppenderError(string logMessage, Exception exception) + { + base.Append(new LoggingEvent(new LoggingEventData + { + Level = Level.Error, + Message = "Appender exception: " + logMessage, + TimeStamp = DateTime.UtcNow, + Identity = "", + ExceptionString = exception.ToString(), + UserName = WindowsIdentity.GetCurrent() != null ? WindowsIdentity.GetCurrent().Name : "", + Domain = AppDomain.CurrentDomain.FriendlyName, + ThreadName = Thread.CurrentThread.ManagedThreadId.ToString(), + LocationInfo = new LocationInfo(this.GetType().Name, "LogAppenderError", "AsyncRollingFileAppender.cs", "152"), + LoggerName = this.GetType().FullName, + Properties = new PropertiesDictionary(), + })); + } - base.OnClose(); - } + private void AppendLoggingEvents() + { + LoggingEvent loggingEventToAppend; + while (!shuttingDown) + { + if (logBufferOverflow) + { + LogBufferOverflowError(); + logBufferOverflow = false; + bufferOverflowCounter = 0; + lastLoggedBufferOverflow = DateTime.UtcNow; + } - private void StartAppendTask() - { - if (!_shuttingDown) - { - Task appendTask = new Task(AppendLoggingEvents, TaskCreationOptions.LongRunning); - appendTask.LogErrors(LogAppenderError).ContinueWith(x => StartAppendTask()).LogErrors(LogAppenderError); - appendTask.Start(); - } - } + while (!pendingAppends.TryDequeue(out loggingEventToAppend)) + { + Thread.Sleep(10); + if (shuttingDown) + { + break; + } + } + if (loggingEventToAppend == null) + { + continue; + } - private void LogAppenderError(string logMessage, Exception exception) - { - var windowsIdentity = WindowsIdentity.GetCurrent(); - base.Append(new LoggingEvent(new LoggingEventData - { - Level = Level.Error, - Message = "Appender exception: " + logMessage, - TimeStamp = DateTime.UtcNow, - Identity = "", - ExceptionString = exception.ToString(), - UserName = windowsIdentity != null ? windowsIdentity.Name : "", - Domain = AppDomain.CurrentDomain.FriendlyName, - ThreadName = Thread.CurrentThread.ManagedThreadId.ToString(), - LocationInfo = - new LocationInfo(this.GetType().Name, - "LogAppenderError", - "AsynchronousRollingFileAppender.cs", - "100"), - LoggerName = this.GetType().FullName, - Properties = new PropertiesDictionary(), - })); - } + try + { + base.Append(loggingEventToAppend); + } + catch + { + } + } - private void AppendLoggingEvents() - { - LoggingEvent loggingEventToAppend; - while (!_shuttingDown) - { - if (_logBufferOverflow) - { - LogBufferOverflowError(); - _logBufferOverflow = false; - _bufferOverflowCounter = 0; - _lastLoggedBufferOverflow = DateTime.UtcNow; - } + while (pendingAppends.TryDequeue(out loggingEventToAppend) && !forceStop) + { + try + { + base.Append(loggingEventToAppend); + } + catch + { + } + } + hasFinished = true; + manualResetEvent.Set(); + } - while (!_pendingAppends.TryDequeue(out loggingEventToAppend)) - { - Thread.Sleep(10); - if (_shuttingDown) - { - break; - } - } - if (loggingEventToAppend == null) - { - continue; - } + private void LogBufferOverflowError() + { + base.Append(new LoggingEvent(new LoggingEventData + { + Level = Level.Error, + Message = string.Format("Buffer overflow. {0} logging events have been lost in the last 30 seconds. [QueueSizeLimit: {1}]", bufferOverflowCounter, QueueSizeLimit), + TimeStamp = DateTime.UtcNow, + Identity = "", + ExceptionString = "", + UserName = WindowsIdentity.GetCurrent() != null ? WindowsIdentity.GetCurrent().Name : "", + Domain = AppDomain.CurrentDomain.FriendlyName, + ThreadName = Thread.CurrentThread.ManagedThreadId.ToString(), + LocationInfo = new LocationInfo(this.GetType().Name, "LogBufferOverflowError", "AsyncRollingFileAppender.cs", "152"), + LoggerName = this.GetType().FullName, + Properties = new PropertiesDictionary(), + })); + } - try - { - base.Append(loggingEventToAppend); - } - catch - { - } - } + private void OnBufferOverflow(object sender, EventArgs eventArgs) + { + bufferOverflowCounter++; + if (logBufferOverflow == false) + { + if (lastLoggedBufferOverflow < DateTime.UtcNow.AddSeconds(-30)) + { + logBufferOverflow = true; + } + } + } + } - while (_pendingAppends.TryDequeue(out loggingEventToAppend) && !_forceStop) - { - try - { - base.Append(loggingEventToAppend); - } - catch - { - } - } - _hasFinished = true; - _manualResetEvent.Set(); - } + internal interface IQueue + { + void Enqueue(T item); + bool TryDequeue(out T ret); + } - private void LogBufferOverflowError() - { - var windowsIdentity = WindowsIdentity.GetCurrent(); - base.Append(new LoggingEvent(new LoggingEventData - { - Level = Level.Error, - Message = - string.Format( - "Buffer overflow. {0} logging events have been lost in the last 30 seconds. [QueueSizeLimit: {1}]", - _bufferOverflowCounter, - QueueSizeLimit), - TimeStamp = DateTime.UtcNow, - Identity = "", - ExceptionString = "", - UserName = windowsIdentity != null ? windowsIdentity.Name : "", - Domain = AppDomain.CurrentDomain.FriendlyName, - ThreadName = Thread.CurrentThread.ManagedThreadId.ToString(), - LocationInfo = - new LocationInfo(this.GetType().Name, - "LogBufferOverflowError", - "AsynchronousRollingFileAppender.cs", - "172"), - LoggerName = this.GetType().FullName, - Properties = new PropertiesDictionary(), - })); - } + internal class RingBuffer : IQueue + { + private readonly object lockObject = new object(); + private readonly T[] buffer; + private readonly int size; + private int readIndex = 0; + private int writeIndex = 0; + private bool bufferFull = false; - private void OnBufferOverflow(object sender, EventArgs eventArgs) - { - _bufferOverflowCounter++; - if (_logBufferOverflow == false) - { - if (_lastLoggedBufferOverflow < DateTime.UtcNow.AddSeconds(-30)) - { - _logBufferOverflow = true; - } - } - } + public int Size { get { return size; } } - private class RingBuffer - { - private readonly object _lockObject = new object(); - private readonly T[] _buffer; - private readonly int _size; - private int _readIndex = 0; - private int _writeIndex = 0; - private bool _bufferFull = false; + public event Action BufferOverflow; - public event Action BufferOverflow; + public RingBuffer(int size) + { + this.size = size; + buffer = new T[size]; + } - public RingBuffer(int size) - { - this._size = size; - _buffer = new T[size]; - } + public void Enqueue(T item) + { + var bufferWasFull = false; + lock (lockObject) + { + buffer[writeIndex] = item; + writeIndex = (++writeIndex) % size; + if (bufferFull) + { + bufferWasFull = true; + readIndex = writeIndex; + } + else if (writeIndex == readIndex) + { + bufferFull = true; + } + } - public void Enqueue(T item) - { - lock (_lockObject) - { - _buffer[_writeIndex] = item; - _writeIndex = (++_writeIndex) % _size; - if (_bufferFull) - { - if (BufferOverflow != null) - { - BufferOverflow(this, EventArgs.Empty); - } - _readIndex = _writeIndex; - } - else if (_writeIndex == _readIndex) - { - _bufferFull = true; - } - } - } + if (bufferWasFull) + { + if (BufferOverflow != null) + { + BufferOverflow(this, EventArgs.Empty); + } + } + } - public bool TryDequeue(out T ret) - { - if (_readIndex == _writeIndex && !_bufferFull) - { - ret = default(T); - return false; - } - lock (_lockObject) - { - if (_readIndex == _writeIndex && !_bufferFull) - { - ret = default(T); - return false; - } + public bool TryDequeue(out T ret) + { + if (readIndex == writeIndex && !bufferFull) + { + ret = default(T); + return false; + } + lock (lockObject) + { + if (readIndex == writeIndex && !bufferFull) + { + ret = default(T); + return false; + } - ret = _buffer[_readIndex]; - _readIndex = (++_readIndex) % _size; - _bufferFull = false; - return true; - } - } - } - } + ret = buffer[readIndex]; + buffer[readIndex] = default(T); + readIndex = (++readIndex) % size; + bufferFull = false; + return true; + } + } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Logging/LoggingEventContext.cs b/src/Umbraco.Core/Logging/LoggingEventContext.cs new file mode 100644 index 0000000000..159af4266b --- /dev/null +++ b/src/Umbraco.Core/Logging/LoggingEventContext.cs @@ -0,0 +1,17 @@ +using log4net.Core; + +namespace Umbraco.Core.Logging +{ + /// + /// Based on https://github.com/cjbhaines/Log4Net.Async + /// + internal class LoggingEventContext + { + public LoggingEventContext(LoggingEvent loggingEvent) + { + LoggingEvent = loggingEvent; + } + + public LoggingEvent LoggingEvent { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Logging/LoggingEventHelper.cs b/src/Umbraco.Core/Logging/LoggingEventHelper.cs new file mode 100644 index 0000000000..c788e115f2 --- /dev/null +++ b/src/Umbraco.Core/Logging/LoggingEventHelper.cs @@ -0,0 +1,31 @@ +using System; +using log4net.Core; + +namespace Umbraco.Core.Logging +{ + /// + /// Based on https://github.com/cjbhaines/Log4Net.Async + /// + internal class LoggingEventHelper + { + // needs to be a seperate class so that location is determined correctly by log4net when required + + private static readonly Type HelperType = typeof(LoggingEventHelper); + private readonly string loggerName; + + public FixFlags Fix { get; set; } + + public LoggingEventHelper(string loggerName, FixFlags fix) + { + this.loggerName = loggerName; + Fix = fix; + } + + public LoggingEvent CreateLoggingEvent(Level level, string message, Exception exception) + { + var loggingEvent = new LoggingEvent(HelperType, null, loggerName, level, message, exception); + loggingEvent.Fix = Fix; + return loggingEvent; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Logging/ParallelForwardingAppender.cs b/src/Umbraco.Core/Logging/ParallelForwardingAppender.cs new file mode 100644 index 0000000000..92c4f7589b --- /dev/null +++ b/src/Umbraco.Core/Logging/ParallelForwardingAppender.cs @@ -0,0 +1,307 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using log4net.Core; +using log4net.Util; + +namespace Umbraco.Core.Logging +{ + /// + /// An asynchronous appender based on + /// + /// + /// Based on https://github.com/cjbhaines/Log4Net.Async + /// + public class ParallelForwardingAppender : AsyncForwardingAppenderBase, IDisposable + { + #region Private Members + + private const int DefaultBufferSize = 1000; + private BlockingCollection _loggingEvents; + private CancellationTokenSource _loggingCancelationTokenSource; + private CancellationToken _loggingCancelationToken; + private Task _loggingTask; + private Double _shutdownFlushTimeout = 5; + private TimeSpan _shutdownFlushTimespan = TimeSpan.FromSeconds(5); + private static readonly Type ThisType = typeof(ParallelForwardingAppender); + private volatile bool _shutDownRequested; + private int? _bufferSize = DefaultBufferSize; + + #endregion Private Members + + #region Properties + + /// + /// Gets or sets the number of LoggingEvents that will be buffered. Set to null for unlimited. + /// + public override int? BufferSize + { + get { return _bufferSize; } + set { _bufferSize = value; } + } + + public int BufferEntryCount + { + get + { + if (_loggingEvents == null) return 0; + return _loggingEvents.Count; + } + } + + /// + /// Gets or sets the time period in which the system will wait for appenders to flush before canceling the background task. + /// + public Double ShutdownFlushTimeout + { + get + { + return _shutdownFlushTimeout; + } + set + { + _shutdownFlushTimeout = value; + } + } + + protected override string InternalLoggerName + { + get { return "ParallelForwardingAppender"; } + } + + #endregion Properties + + #region Startup + + public override void ActivateOptions() + { + base.ActivateOptions(); + _shutdownFlushTimespan = TimeSpan.FromSeconds(_shutdownFlushTimeout); + StartForwarding(); + } + + private void StartForwarding() + { + if (_shutDownRequested) + { + return; + } + //Create a collection which will block the thread and wait for new entries + //if the collection is empty + if (BufferSize.HasValue && BufferSize > 0) + { + _loggingEvents = new BlockingCollection(BufferSize.Value); + } + else + { + //No limit on the number of events. + _loggingEvents = new BlockingCollection(); + } + //The cancellation token is used to cancel a running task gracefully. + _loggingCancelationTokenSource = new CancellationTokenSource(); + _loggingCancelationToken = _loggingCancelationTokenSource.Token; + _loggingTask = new Task(SubscriberLoop, _loggingCancelationToken); + _loggingTask.Start(); + } + + #endregion Startup + + #region Shutdown + + private void CompleteSubscriberTask() + { + _shutDownRequested = true; + if (_loggingEvents == null || _loggingEvents.IsAddingCompleted) + { + return; + } + //Don't allow more entries to be added. + _loggingEvents.CompleteAdding(); + //Allow some time to flush + Thread.Sleep(_shutdownFlushTimespan); + if (!_loggingTask.IsCompleted && !_loggingCancelationToken.IsCancellationRequested) + { + _loggingCancelationTokenSource.Cancel(); + //Wait here so that the error logging messages do not get into a random order. + //Don't pass the cancellation token because we are not interested + //in catching the OperationCanceledException that results. + _loggingTask.Wait(); + } + if (!_loggingEvents.IsCompleted) + { + ForwardInternalError("The buffer was not able to be flushed before timeout occurred.", null, ThisType); + } + } + + protected override void OnClose() + { + CompleteSubscriberTask(); + base.OnClose(); + } + + #endregion Shutdown + + #region Appending + + protected override void Append(LoggingEvent loggingEvent) + { + if (_loggingEvents == null || _loggingEvents.IsAddingCompleted || loggingEvent == null) + { + return; + } + + loggingEvent.Fix = Fix; + //In the case where blocking on a full collection, and the task is subsequently completed, the cancellation token + //will prevent the entry from attempting to add to the completed collection which would result in an exception. + _loggingEvents.Add(new LoggingEventContext(loggingEvent), _loggingCancelationToken); + } + + protected override void Append(LoggingEvent[] loggingEvents) + { + if (_loggingEvents == null || _loggingEvents.IsAddingCompleted || loggingEvents == null) + { + return; + } + + foreach (var loggingEvent in loggingEvents) + { + Append(loggingEvent); + } + } + + #endregion Appending + + #region Forwarding + + /// + /// Iterates over a BlockingCollection containing LoggingEvents. + /// + private void SubscriberLoop() + { + Thread.CurrentThread.Name = String.Format("{0} ParallelForwardingAppender Subscriber Task", Name); + //The task will continue in a blocking loop until + //the queue is marked as adding completed, or the task is canceled. + try + { + //This call blocks until an item is available or until adding is completed + foreach (var entry in _loggingEvents.GetConsumingEnumerable(_loggingCancelationToken)) + { + ForwardLoggingEvent(entry.LoggingEvent, ThisType); + } + } + catch (OperationCanceledException ex) + { + //The thread was canceled before all entries could be forwarded and the collection completed. + ForwardInternalError("Subscriber task was canceled before completion.", ex, ThisType); + //Cancellation is called in the CompleteSubscriberTask so don't call that again. + } + catch (ThreadAbortException ex) + { + //Thread abort may occur on domain unload. + ForwardInternalError("Subscriber task was aborted.", ex, ThisType); + //Cannot recover from a thread abort so complete the task. + CompleteSubscriberTask(); + //The exception is swallowed because we don't want the client application + //to halt due to a logging issue. + } + catch (Exception ex) + { + //On exception, try to log the exception + ForwardInternalError("Subscriber task error in forwarding loop.", ex, ThisType); + //Any error in the loop is going to be some sort of extenuating circumstance from which we + //probably cannot recover anyway. Complete subscribing. + CompleteSubscriberTask(); + } + } + + #endregion Forwarding + + #region IDisposable Implementation + + private bool _disposed = false; + + //Implement IDisposable. + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + if (_loggingTask != null) + { + if (!(_loggingTask.IsCanceled || _loggingTask.IsCompleted || _loggingTask.IsFaulted)) + { + try + { + CompleteSubscriberTask(); + } + catch (Exception ex) + { + LogLog.Error(ThisType, "Exception Completing Subscriber Task in Dispose Method", ex); + } + } + try + { + _loggingTask.Dispose(); + } + catch (Exception ex) + { + LogLog.Error(ThisType, "Exception Disposing Logging Task", ex); + } + finally + { + _loggingTask = null; + } + } + if (_loggingEvents != null) + { + try + { + _loggingEvents.Dispose(); + } + catch (Exception ex) + { + LogLog.Error(ThisType, "Exception Disposing BlockingCollection", ex); + } + finally + { + _loggingEvents = null; + } + } + if (_loggingCancelationTokenSource != null) + { + try + { + _loggingCancelationTokenSource.Dispose(); + } + catch (Exception ex) + { + LogLog.Error(ThisType, "Exception Disposing CancellationTokenSource", ex); + } + finally + { + _loggingCancelationTokenSource = null; + } + } + } + _disposed = true; + } + } + + // Use C# destructor syntax for finalization code. + ~ParallelForwardingAppender() + { + // Simply call Dispose(false). + Dispose(false); + } + + #endregion IDisposable Implementation + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 7c71015702..007d12500b 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -331,8 +331,12 @@ + + + + @@ -431,7 +435,7 @@ - + diff --git a/src/Umbraco.Tests/AsynchronousRollingFileAppenderTests.cs b/src/Umbraco.Tests/AsynchronousRollingFileAppenderTests.cs deleted file mode 100644 index 2f1a8d99d3..0000000000 --- a/src/Umbraco.Tests/AsynchronousRollingFileAppenderTests.cs +++ /dev/null @@ -1,173 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using NUnit.Framework; -using Umbraco.Core; -using Umbraco.Core.Logging; -using Umbraco.Tests.TestHelpers; -using log4net; -using log4net.Config; -using log4net.Core; -using log4net.Layout; -using log4net.Repository; - -namespace Umbraco.Tests -{ - //Ignore this test, it fails sometimes on the build server - pretty sure it's a threading issue with this test class - [Ignore] - [TestFixture] - public class AsynchronousRollingFileAppenderTests - { - private const string ErrorMessage = "TEST ERROR MESSAGE"; - private string _fileFolderPath = @"c:\LogTesting\"; - private readonly Level _errorLevel = Level.Error; - private AsynchronousRollingFileAppender _appender; - private ILoggerRepository _rep; - private Guid _fileGuid; - - - - private string GetFilePath() - { - return string.Format("{0}{1}.log", _fileFolderPath, _fileGuid); - } - - [SetUp] - public void SetUp() - { - _fileFolderPath = TestHelper.MapPathForTest("~/LogTesting/"); - - _fileGuid = Guid.NewGuid(); - if (File.Exists(GetFilePath())) - { - File.Delete(GetFilePath()); - } - - _appender = new AsynchronousRollingFileAppender(); - _appender.Threshold = _errorLevel; - _appender.File = GetFilePath(); - _appender.Layout = new PatternLayout("%d|%-5level|%logger| %message %exception%n"); - _appender.StaticLogFileName = true; - _appender.AppendToFile = true; - _appender.ActivateOptions(); - - _rep = LogManager.CreateRepository(Guid.NewGuid().ToString()); - BasicConfigurator.Configure(_rep, _appender); - } - - [TearDown] - public void TearDown() - { - _rep.Shutdown(); - if (File.Exists(GetFilePath())) - { - File.Delete(GetFilePath()); - } - } - - [TestFixtureTearDown] - public void FixtureTearDown() - { - foreach (string file in Directory.GetFiles(_fileFolderPath)) - { - try - { - File.Delete(file); - } - catch { } - } - } - - private void ReleaseFileLocks() - { - _rep.Shutdown(); - _appender.Close(); - } - - [Test] - public void CanWriteToFile() - { - // Arrange - ILog log = LogManager.GetLogger(_rep.Name, "CanWriteToDatabase"); - - // Act - log.Error(ErrorMessage); - Thread.Sleep(200); // let background thread finish - - // Assert - ReleaseFileLocks(); - Assert.That(File.Exists(GetFilePath()), Is.True); - IEnumerable readLines = File.ReadLines(GetFilePath()); - Assert.That(readLines.Count(), Is.GreaterThanOrEqualTo(1)); - } - - [Test] - public void ReturnsQuicklyAfterLogging100Messages() - { - // Arrange - ILog log = LogManager.GetLogger(_rep.Name, "ReturnsQuicklyAfterLogging100Messages"); - - // Act - DateTime startTime = DateTime.UtcNow; - 100.Times(i => log.Error(ErrorMessage)); - DateTime endTime = DateTime.UtcNow; - - // Give background thread time to finish - Thread.Sleep(500); - - // Assert - ReleaseFileLocks(); - Assert.That(endTime - startTime, Is.LessThan(TimeSpan.FromMilliseconds(100))); - Assert.That(File.Exists(GetFilePath()), Is.True); - IEnumerable readLines = File.ReadLines(GetFilePath()); - Assert.That(readLines.Count(), Is.GreaterThanOrEqualTo(100)); - } - - [Test] - public void CanLogAtleast1000MessagesASecond() - { - // Arrange - ILog log = LogManager.GetLogger(_rep.Name, "CanLogAtLeast1000MessagesASecond"); - - int logCount = 0; - bool logging = true; - bool logsCounted = false; - - var logTimer = new Timer(s => - { - logging = false; - - if (File.Exists(GetFilePath())) - { - ReleaseFileLocks(); - IEnumerable readLines = File.ReadLines(GetFilePath()); - logCount = readLines.Count(); - } - logsCounted = true; - }, null, TimeSpan.FromSeconds(3), TimeSpan.FromMilliseconds(-1)); - - // Act - DateTime startTime = DateTime.UtcNow; - while (logging) - { - log.Error(ErrorMessage); - } - TimeSpan testDuration = DateTime.UtcNow - startTime; - - while (!logsCounted) - { - Thread.Sleep(1); - } - - logTimer.Dispose(); - - // Assert - var logsPerSecond = logCount / testDuration.TotalSeconds; - - Console.WriteLine("{0} messages logged in {1}s => {2}/s", logCount, testDuration.TotalSeconds, logsPerSecond); - Assert.That(logsPerSecond, Is.GreaterThan(1000), "Must log at least 1000 messages per second"); - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Tests/Logging/AsyncRollingFileAppenderTest.cs b/src/Umbraco.Tests/Logging/AsyncRollingFileAppenderTest.cs new file mode 100644 index 0000000000..37323a67d5 --- /dev/null +++ b/src/Umbraco.Tests/Logging/AsyncRollingFileAppenderTest.cs @@ -0,0 +1,166 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using log4net; +using log4net.Config; +using log4net.Core; +using log4net.Layout; +using log4net.Repository; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.Logging; + +namespace Umbraco.Tests.Logging +{ + [TestFixture] + public class AsyncRollingFileAppenderTest + { + private const string ErrorMessage = "TEST ERROR MESSAGE"; + private const string FileFolderPath = @"c:\LogTesting\"; + private readonly Level ErrorLevel = Level.Error; + private AsynchronousRollingFileAppender appender; + private ILoggerRepository rep; + private Guid fileGuid; + + private string GetFilePath() + { + return string.Format("{0}{1}.log", FileFolderPath, fileGuid); + } + + [SetUp] + public void SetUp() + { + fileGuid = Guid.NewGuid(); + if (File.Exists(GetFilePath())) + { + File.Delete(GetFilePath()); + } + + appender = new AsynchronousRollingFileAppender(); + appender.Threshold = ErrorLevel; + appender.File = GetFilePath(); + appender.Layout = new PatternLayout("%d|%-5level|%logger| %message %exception%n"); + appender.StaticLogFileName = true; + appender.AppendToFile = true; + appender.ActivateOptions(); + + rep = LogManager.CreateRepository(Guid.NewGuid().ToString()); + BasicConfigurator.Configure(rep, appender); + } + + [TearDown] + public void TearDown() + { + rep.Shutdown(); + if (File.Exists(GetFilePath())) + { + File.Delete(GetFilePath()); + } + } + + [TestFixtureTearDown] + public void FixtureTearDown() + { + foreach (string file in Directory.GetFiles(FileFolderPath)) + { + try + { + File.Delete(file); + } + catch { } + } + } + + private void ReleaseFileLocks() + { + rep.Shutdown(); + appender.Close(); + } + + [Test] + public void CanWriteToFile() + { + // Arrange + ILog log = LogManager.GetLogger(rep.Name, "CanWriteToDatabase"); + + // Act + log.Error(ErrorMessage); + Thread.Sleep(200); // let background thread finish + + // Assert + ReleaseFileLocks(); + Assert.That(File.Exists(GetFilePath()), Is.True); + IEnumerable readLines = File.ReadLines(GetFilePath()); + Assert.That(readLines.Count(), Is.GreaterThanOrEqualTo(1)); + } + + [Test] + public void ReturnsQuicklyAfterLogging100Messages() + { + // Arrange + ILog log = LogManager.GetLogger(rep.Name, "ReturnsQuicklyAfterLogging100Messages"); + + // Act + DateTime startTime = DateTime.UtcNow; + 100.Times(i => log.Error(ErrorMessage)); + DateTime endTime = DateTime.UtcNow; + + // Give background thread time to finish + Thread.Sleep(500); + + // Assert + ReleaseFileLocks(); + Assert.That(endTime - startTime, Is.LessThan(TimeSpan.FromMilliseconds(100))); + Assert.That(File.Exists(GetFilePath()), Is.True); + IEnumerable readLines = File.ReadLines(GetFilePath()); + Assert.That(readLines.Count(), Is.GreaterThanOrEqualTo(100)); + } + + [Test] + public void CanLogAtleast1000MessagesASecond() + { + // Arrange + ILog log = LogManager.GetLogger(rep.Name, "CanLogAtLeast1000MessagesASecond"); + + int logCount = 0; + bool logging = true; + bool logsCounted = false; + + var logTimer = new Timer(s => + { + logging = false; + + if (File.Exists(GetFilePath())) + { + ReleaseFileLocks(); + IEnumerable readLines = File.ReadLines(GetFilePath()); + logCount = readLines.Count(); + } + logsCounted = true; + }, null, TimeSpan.FromSeconds(3), TimeSpan.FromMilliseconds(-1)); + + // Act + DateTime startTime = DateTime.UtcNow; + while (logging) + { + log.Error(ErrorMessage); + } + TimeSpan testDuration = DateTime.UtcNow - startTime; + + while (!logsCounted) + { + Thread.Sleep(1); + } + + logTimer.Dispose(); + + // Assert + var logsPerSecond = logCount / testDuration.TotalSeconds; + + Console.WriteLine("{0} messages logged in {1}s => {2}/s", logCount, testDuration.TotalSeconds, logsPerSecond); + Assert.That(logsPerSecond, Is.GreaterThan(1000), "Must log at least 1000 messages per second"); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/Logging/DebugAppender.cs b/src/Umbraco.Tests/Logging/DebugAppender.cs new file mode 100644 index 0000000000..c1a1523349 --- /dev/null +++ b/src/Umbraco.Tests/Logging/DebugAppender.cs @@ -0,0 +1,22 @@ +using System; +using System.Threading; +using log4net.Appender; +using log4net.Core; + +namespace Umbraco.Tests.Logging +{ + internal class DebugAppender : MemoryAppender + { + public TimeSpan AppendDelay { get; set; } + public int LoggedEventCount { get { return m_eventsList.Count; } } + + protected override void Append(LoggingEvent loggingEvent) + { + if (AppendDelay > TimeSpan.Zero) + { + Thread.Sleep(AppendDelay); + } + base.Append(loggingEvent); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/Logging/ParallelForwarderTest.cs b/src/Umbraco.Tests/Logging/ParallelForwarderTest.cs new file mode 100644 index 0000000000..fffc28fe1f --- /dev/null +++ b/src/Umbraco.Tests/Logging/ParallelForwarderTest.cs @@ -0,0 +1,324 @@ +using System; +using System.Diagnostics; +using System.Security.Principal; +using System.Threading; +using log4net; +using log4net.Appender; +using log4net.Config; +using log4net.Core; +using log4net.Filter; +using log4net.Repository; +using Moq; +using NUnit.Framework; +using Umbraco.Core.Logging; + +namespace Umbraco.Tests.Logging +{ + [TestFixture] + public class ParallelForwarderTest : IDisposable + { + private ParallelForwardingAppender asyncForwardingAppender; + private DebugAppender debugAppender; + private ILoggerRepository repository; + private ILog log; + + [SetUp] + public void TestFixtureSetUp() + { + debugAppender = new DebugAppender(); + debugAppender.ActivateOptions(); + + asyncForwardingAppender = new ParallelForwardingAppender(); + asyncForwardingAppender.AddAppender(debugAppender); + asyncForwardingAppender.ActivateOptions(); + + repository = LogManager.CreateRepository(Guid.NewGuid().ToString()); + BasicConfigurator.Configure(repository, asyncForwardingAppender); + + log = LogManager.GetLogger(repository.Name, "TestLogger"); + } + + [TearDown] + public void TearDown() + { + LogManager.Shutdown(); + } + + [Test] + public void CanHandleNullLoggingEvent() + { + // Arrange + + // Act + asyncForwardingAppender.DoAppend((LoggingEvent)null); + log.Info("SusequentMessage"); + asyncForwardingAppender.Close(); + + // Assert - should not have had an exception from previous call + Assert.That(debugAppender.LoggedEventCount, Is.EqualTo(1), "Expected subsequent message only"); + Assert.That(debugAppender.GetEvents()[0].MessageObject, Is.EqualTo("SusequentMessage")); + } + + [Test] + public void CanHandleNullLoggingEvents() + { + // Arrange + + // Act + asyncForwardingAppender.DoAppend((LoggingEvent[])null); + log.Info("SusequentMessage"); + asyncForwardingAppender.Close(); + + // Assert - should not have had an exception from previous call + Assert.That(debugAppender.LoggedEventCount, Is.EqualTo(1), "Expected subsequent message only"); + Assert.That(debugAppender.GetEvents()[0].MessageObject, Is.EqualTo("SusequentMessage")); + } + + [Test] + public void CanHandleAppenderThrowing() + { + // Arrange + var badAppender = new Mock(); + asyncForwardingAppender.AddAppender(badAppender.Object); + + badAppender + .Setup(ba => ba.DoAppend(It.IsAny())) + .Throws(new Exception("Bad Appender")); + //.Verifiable(); + + // Act + log.Info("InitialMessage"); + log.Info("SusequentMessage"); + asyncForwardingAppender.Close(); + + // Assert + Assert.That(debugAppender.LoggedEventCount, Is.EqualTo(2)); + Assert.That(debugAppender.GetEvents()[1].MessageObject, Is.EqualTo("SusequentMessage")); + badAppender.Verify(appender => appender.DoAppend(It.IsAny()), Times.Exactly(2)); + } + + [Test] + public void WillLogFastWhenThereIsASlowAppender() + { + const int testSize = 1000; + + // Arrange + debugAppender.AppendDelay = TimeSpan.FromSeconds(10); + var watch = new Stopwatch(); + + // Act + watch.Start(); + for (int i = 0; i < testSize; i++) + { + log.Error("Exception"); + } + watch.Stop(); + + // Assert + Assert.That(debugAppender.LoggedEventCount, Is.EqualTo(0)); + Assert.That(watch.ElapsedMilliseconds, Is.LessThan(testSize)); + Console.WriteLine("Logged {0} errors in {1}ms", testSize, watch.ElapsedMilliseconds); + } + + [Test] + public void WillNotOverflow() + { + const int testSize = 1000; + + // Arrange + debugAppender.AppendDelay = TimeSpan.FromMilliseconds(1); + asyncForwardingAppender.BufferSize = 100; + + // Act + for (int i = 0; i < testSize; i++) + { + log.Error("Exception"); + } + + while (asyncForwardingAppender.BufferEntryCount > 0) ; + asyncForwardingAppender.Close(); + + // Assert + Assert.That(debugAppender.LoggedEventCount, Is.EqualTo(testSize)); + } + + [Test] + public void WillTryToFlushBufferOnShutdown() + { + const int testSize = 250; + + // Arrange + debugAppender.AppendDelay = TimeSpan.FromMilliseconds(1); + + // Act + for (int i = 0; i < testSize; i++) + { + log.Error("Exception"); + } + + Thread.Sleep(50); + + var numberLoggedBeforeClose = debugAppender.LoggedEventCount; + asyncForwardingAppender.Close(); + var numberLoggedAfterClose = debugAppender.LoggedEventCount; + + // Assert + //We can't use specific numbers here because the timing and counts will be different on different systems. + Assert.That(numberLoggedBeforeClose, Is.GreaterThan(0), "Some number of Logging events should be logged prior to appender close."); + //On some systems, we may not be able to flush all events prior to close, but it is reasonable to assume in this test case + //that some events should be logged after close. + Assert.That(numberLoggedAfterClose, Is.GreaterThan(numberLoggedBeforeClose), "Some number of LoggingEvents should be logged after close."); + Console.WriteLine("Flushed {0} events during shutdown", numberLoggedAfterClose - numberLoggedBeforeClose); + } + + [Test, Explicit("Long-running")] + public void WillShutdownIfBufferCannotBeFlushedFastEnough() + { + const int testSize = 250; + + // Arrange + debugAppender.AppendDelay = TimeSpan.FromSeconds(1); + Stopwatch watch = new Stopwatch(); + + // Act + for (int i = 0; i < testSize; i++) + { + log.Error("Exception"); + } + + Thread.Sleep(TimeSpan.FromSeconds(2)); + var numberLoggedBeforeClose = debugAppender.LoggedEventCount; + + watch.Start(); + asyncForwardingAppender.Close(); + watch.Stop(); + + var numberLoggedAfterClose = debugAppender.LoggedEventCount; + + // Assert + Assert.That(numberLoggedBeforeClose, Is.GreaterThan(0)); + Assert.That(numberLoggedAfterClose, Is.GreaterThan(numberLoggedBeforeClose)); + Assert.That(numberLoggedAfterClose, Is.LessThan(testSize)); + //We can't assume what the shutdown time will be. It will vary from system to system. Don't test shutdown time. + var events = debugAppender.GetEvents(); + var evnt = events[events.Length - 1]; + Assert.That(evnt.MessageObject, Is.EqualTo("The buffer was not able to be flushed before timeout occurred.")); + Console.WriteLine("Flushed {0} events during shutdown which lasted {1}ms", numberLoggedAfterClose - numberLoggedBeforeClose, watch.ElapsedMilliseconds); + } + + [Test] + public void ThreadContextPropertiesArePreserved() + { + // Arrange + ThreadContext.Properties["TestProperty"] = "My Value"; + Assert.That(asyncForwardingAppender.Fix & FixFlags.Properties, Is.EqualTo(FixFlags.Properties), "Properties must be fixed if they are to be preserved"); + + // Act + log.Info("Information"); + asyncForwardingAppender.Close(); + + // Assert + var lastLoggedEvent = debugAppender.GetEvents()[0]; + Assert.That(lastLoggedEvent.Properties["TestProperty"], Is.EqualTo("My Value")); + } + + [Test] + public void MessagesExcludedByFilterShouldNotBeAppended() + { + // Arrange + var levelFilter = + new LevelRangeFilter + { + LevelMin = Level.Warn, + LevelMax = Level.Error, + }; + + asyncForwardingAppender.AddFilter(levelFilter); + + // Act + log.Info("Info"); + log.Warn("Warn"); + log.Error("Error"); + log.Fatal("Fatal"); + + asyncForwardingAppender.Close(); + + //Assert + Assert.That(debugAppender.LoggedEventCount, Is.EqualTo(2)); + } + + [Test] + public void HelperCanGenerateLoggingEventWithAllProperties() + { + // Arrange + var helper = new LoggingEventHelper("TestLoggerName", FixFlags.All); + ThreadContext.Properties["MyProperty"] = "MyValue"; + var exception = new Exception("SomeError"); + + var stackFrame = new StackFrame(0); + var currentUser = WindowsIdentity.GetCurrent(); + var loggingTime = DateTime.Now; // Log4Net does not seem to be using UtcNow + + // Act + var loggingEvent = helper.CreateLoggingEvent(Level.Emergency, "Who's on live support?", exception); + Thread.Sleep(50); // to make sure the time stamp is actually captured + + // Assert + Assert.That(loggingEvent.Domain, Is.EqualTo(AppDomain.CurrentDomain.FriendlyName), "Domain"); + //The identity assigned to new threads is dependent upon AppDomain principal policy. + //Background information here:http://www.neovolve.com/post/2010/10/21/Unit-testing-a-workflow-that-relies-on-ThreadCurrentPrincipalIdentityName.aspx + //VS2013 does have a principal assigned to new threads in the unit test. + //It's probably best not to test that the identity has been set. + //Assert.That(loggingEvent.Identity, Is.Empty, "Identity: always empty for some reason"); + Assert.That(loggingEvent.UserName, Is.EqualTo(currentUser == null ? String.Empty : currentUser.Name), "UserName"); + Assert.That(loggingEvent.ThreadName, Is.EqualTo(Thread.CurrentThread.Name), "ThreadName"); + + Assert.That(loggingEvent.Repository, Is.Null, "Repository: Helper does not have access to this"); + Assert.That(loggingEvent.LoggerName, Is.EqualTo("TestLoggerName"), "LoggerName"); + + Assert.That(loggingEvent.Level, Is.EqualTo(Level.Emergency), "Level"); + //Raised time to within 10 ms. However, this may not be a valid test. The time is going to vary from system to system. The + //tolerance setting here is arbitrary. + Assert.That(loggingEvent.TimeStamp, Is.EqualTo(loggingTime).Within(TimeSpan.FromMilliseconds(10)), "TimeStamp"); + Assert.That(loggingEvent.ExceptionObject, Is.EqualTo(exception), "ExceptionObject"); + Assert.That(loggingEvent.MessageObject, Is.EqualTo("Who's on live support?"), "MessageObject"); + + Assert.That(loggingEvent.LocationInformation.MethodName, Is.EqualTo(stackFrame.GetMethod().Name), "LocationInformation"); + Assert.That(loggingEvent.Properties["MyProperty"], Is.EqualTo("MyValue"), "Properties"); + } + + private bool _disposed = false; + + //Implement IDisposable. + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposed) + { + if (disposing) + { + if (asyncForwardingAppender != null) + { + asyncForwardingAppender.Dispose(); + asyncForwardingAppender = null; + } + } + // Free your own state (unmanaged objects). + // Set large fields to null. + _disposed = true; + } + } + + // Use C# destructor syntax for finalization code. + ~ParallelForwarderTest() + { + // Simply call Dispose(false). + Dispose(false); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/Logging/RingBufferTest.cs b/src/Umbraco.Tests/Logging/RingBufferTest.cs new file mode 100644 index 0000000000..8e80faf8ad --- /dev/null +++ b/src/Umbraco.Tests/Logging/RingBufferTest.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; +using Umbraco.Core.Logging; +using Umbraco.Tests.TestHelpers; + +namespace Umbraco.Tests.Logging +{ + [TestFixture] + public class RingBufferTest + { + [Test] + public void PerfTest() + { + RingBuffer ringBuffer = new RingBuffer(1000); + Stopwatch ringWatch = new Stopwatch(); + ringWatch.Start(); + for (int i = 0; i < 1000000; i++) + { + ringBuffer.Enqueue("StringOfFun"); + } + ringWatch.Stop(); + + Assert.That(ringWatch.ElapsedMilliseconds, Is.LessThan(150)); + } + + [Test] + public void PerfTestThreads() + { + RingBuffer ringBuffer = new RingBuffer(1000); + + Stopwatch ringWatch = new Stopwatch(); + List ringTasks = new List(); + CancellationTokenSource cancelationTokenSource = new CancellationTokenSource(); + CancellationToken cancelationToken = cancelationTokenSource.Token; + for (int t = 0; t < 10; t++) + { + ringTasks.Add(new Task(() => + { + for (int i = 0; i < 1000000; i++) + { + ringBuffer.Enqueue("StringOfFun"); + } + }, cancelationToken)); + } + ringWatch.Start(); + ringTasks.ForEach(t => t.Start()); + var allTasks = ringTasks.ToArray(); + Task.WaitAny(allTasks); + ringWatch.Stop(); + //Cancel tasks to avoid System.AppDominUnloadException which is caused when the domain is unloaded + //and threads created in the domain are not stopped. + //Do this before assertions because they may throw an exception causing the thread cancellation to not happen. + cancelationTokenSource.Cancel(); + try + { + Task.WaitAll(allTasks); + } + catch (AggregateException) + { + //Don't care about cancellation Exceptions. + } + //Tolerance at 500 was too low + ringTasks.ForEach(t => t.Dispose()); + Assert.That(ringWatch.ElapsedMilliseconds, Is.LessThan(1000)); + } + + [Test] + public void PerfTestThreadsWithDequeues() + { + RingBuffer ringBuffer = new RingBuffer(1000); + + Stopwatch ringWatch = new Stopwatch(); + List ringTasks = new List(); + CancellationTokenSource cancelationTokenSource = new CancellationTokenSource(); + CancellationToken cancelationToken = cancelationTokenSource.Token; + for (int t = 0; t < 10; t++) + { + ringTasks.Add(new Task(() => + { + for (int i = 0; i < 1000000; i++) + { + ringBuffer.Enqueue("StringOfFun"); + } + }, cancelationToken)); + } + for (int t = 0; t < 10; t++) + { + ringTasks.Add(new Task(() => + { + for (int i = 0; i < 1000000; i++) + { + string foo; + ringBuffer.TryDequeue(out foo); + } + })); + } + ringWatch.Start(); + ringTasks.ForEach(t => t.Start()); + var allTasks = ringTasks.ToArray(); + Task.WaitAny(allTasks); + ringWatch.Stop(); + //Cancel tasks to avoid System.AppDominUnloadException which is caused when the domain is unloaded + //and threads created in the domain are not stopped. + //Do this before assertions because they may throw an exception causing the thread cancellation to not happen. + cancelationTokenSource.Cancel(); + try + { + Task.WaitAll(allTasks); + } + catch (AggregateException) + { + //Don't care about cancellation Exceptions. + } + ringTasks.ForEach(t => t.Dispose()); + Assert.That(ringWatch.ElapsedMilliseconds, Is.LessThan(800)); + } + + [Test] + public void WhenRingSizeLimitIsHit_ItemsAreDequeued() + { + // Arrange + const int limit = 2; + object object1 = "one"; + object object2 = "two"; + object object3 = "three"; + RingBuffer queue = new RingBuffer(limit); + + // Act + queue.Enqueue(object1); + queue.Enqueue(object2); + queue.Enqueue(object3); + + // Assert + object value; + queue.TryDequeue(out value); + Assert.That(value, Is.EqualTo(object2)); + queue.TryDequeue(out value); + Assert.That(value, Is.EqualTo(object3)); + queue.TryDequeue(out value); + Assert.That(value, Is.Null); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 8ac904f175..a0c5a995cd 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -177,6 +177,9 @@ + + + @@ -445,7 +448,7 @@ - + diff --git a/src/Umbraco.Web.UI/config/log4net.Release.config b/src/Umbraco.Web.UI/config/log4net.Release.config index 13297c8a49..6b2df3e0bf 100644 --- a/src/Umbraco.Web.UI/config/log4net.Release.config +++ b/src/Umbraco.Web.UI/config/log4net.Release.config @@ -5,8 +5,8 @@ - - + + @@ -18,6 +18,10 @@ + + + + diff --git a/src/Umbraco.Web.UI/config/log4net.config b/src/Umbraco.Web.UI/config/log4net.config index 0a0e9e4ff7..814b310325 100644 --- a/src/Umbraco.Web.UI/config/log4net.config +++ b/src/Umbraco.Web.UI/config/log4net.config @@ -6,8 +6,7 @@ - - + @@ -19,6 +18,22 @@ + + + + + + From 53a0c55b14eb0dbfc11d114116ff385df4eb2ad4 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 1 Jul 2015 17:07:29 +0200 Subject: [PATCH 16/50] Implements SignInManager, implements lock out policy for user manager, allows for better implementation of 2 factor auth for developers. Updates to latest owin libs. --- src/Umbraco.Core/Constants-Web.cs | 1 + .../Models/Identity/IdentityModelMappings.cs | 1 + .../Security/BackOfficeSignInManager.cs | 72 ++++++++++ .../Security/BackOfficeUserManager.cs | 19 ++- .../Security/BackOfficeUserStore.cs | 126 +++++++++++++++++- src/Umbraco.Core/Umbraco.Core.csproj | 5 +- src/Umbraco.Core/packages.config | 4 +- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 12 +- src/Umbraco.Web.UI/packages.config | 4 +- .../Editors/AuthenticationController.cs | 126 ++++++++++++------ .../Editors/BackOfficeController.cs | 55 +++----- .../HtmlHelperBackOfficeExtensions.cs | 8 +- .../Security/Identity/AppBuilderExtensions.cs | 9 ++ .../Identity/ExternalSignInAutoLinkOptions.cs | 26 ++-- .../IUmbracoBackOfficeTwoFactorOptions.cs | 12 ++ src/Umbraco.Web/Umbraco.Web.csproj | 5 +- src/Umbraco.Web/packages.config | 4 +- 17 files changed, 358 insertions(+), 131 deletions(-) create mode 100644 src/Umbraco.Core/Security/BackOfficeSignInManager.cs create mode 100644 src/Umbraco.Web/Security/Identity/IUmbracoBackOfficeTwoFactorOptions.cs diff --git a/src/Umbraco.Core/Constants-Web.cs b/src/Umbraco.Core/Constants-Web.cs index 0d7c2f41da..13dee96b97 100644 --- a/src/Umbraco.Core/Constants-Web.cs +++ b/src/Umbraco.Core/Constants-Web.cs @@ -26,6 +26,7 @@ public const string BackOfficeExternalAuthenticationType = "UmbracoExternalCookie"; public const string BackOfficeExternalCookieName = "UMB_EXTLOGIN"; public const string BackOfficeTokenAuthenticationType = "UmbracoBackOfficeToken"; + public const string BackOfficeTwoFactorAuthenticationType = "UmbracoTwoFactorCookie"; /// /// The prefix used for external identity providers for their authentication type diff --git a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs index def71a8982..7e30d4a81e 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs @@ -15,6 +15,7 @@ namespace Umbraco.Core.Models.Identity .ForMember(user => user.Email, expression => expression.MapFrom(user => user.Email)) .ForMember(user => user.Id, expression => expression.MapFrom(user => user.Id)) .ForMember(user => user.LockoutEnabled, expression => expression.MapFrom(user => user.IsLockedOut)) + //Users currently are locked out for an infinite time, we do not support timed lock outs currently .ForMember(user => user.LockoutEndDateUtc, expression => expression.UseValue(DateTime.MaxValue.ToUniversalTime())) .ForMember(user => user.UserName, expression => expression.MapFrom(user => user.Username)) .ForMember(user => user.PasswordHash, expression => expression.MapFrom(user => GetPasswordHash(user.RawPasswordValue))) diff --git a/src/Umbraco.Core/Security/BackOfficeSignInManager.cs b/src/Umbraco.Core/Security/BackOfficeSignInManager.cs new file mode 100644 index 0000000000..fc7ecf5330 --- /dev/null +++ b/src/Umbraco.Core/Security/BackOfficeSignInManager.cs @@ -0,0 +1,72 @@ +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNet.Identity; +using Microsoft.AspNet.Identity.Owin; +using Microsoft.Owin; +using Microsoft.Owin.Security; +using Umbraco.Core.Configuration; +using Umbraco.Core.Models.Identity; + +namespace Umbraco.Core.Security +{ + public class BackOfficeSignInManager : SignInManager + { + public BackOfficeSignInManager(BackOfficeUserManager userManager, IAuthenticationManager authenticationManager) + : base(userManager, authenticationManager) + { + AuthenticationType = Constants.Security.BackOfficeAuthenticationType; + } + + public override Task CreateUserIdentityAsync(BackOfficeIdentityUser user) + { + return user.GenerateUserIdentityAsync((BackOfficeUserManager)UserManager); + } + + public static BackOfficeSignInManager Create(IdentityFactoryOptions options, IOwinContext context) + { + return new BackOfficeSignInManager(context.GetUserManager(), context.Authentication); + } + + /// + /// Creates a user identity and then signs the identity using the AuthenticationManager + /// + /// + /// + /// + /// + public override async Task SignInAsync(BackOfficeIdentityUser user, bool isPersistent, bool rememberBrowser) + { + var userIdentity = await CreateUserIdentityAsync(user); + + // Clear any partial cookies from external or two factor partial sign ins + AuthenticationManager.SignOut( + Constants.Security.BackOfficeExternalAuthenticationType, + Constants.Security.BackOfficeTwoFactorAuthenticationType); + + var nowUtc = DateTime.Now.ToUniversalTime(); + + if (rememberBrowser) + { + var rememberBrowserIdentity = AuthenticationManager.CreateTwoFactorRememberBrowserIdentity(ConvertIdToString(user.Id)); + AuthenticationManager.SignIn(new AuthenticationProperties() + { + IsPersistent = isPersistent, + AllowRefresh = true, + IssuedUtc = nowUtc, + ExpiresUtc = nowUtc.AddMinutes(GlobalSettings.TimeOutInMinutes) + }, userIdentity, rememberBrowserIdentity); + } + else + { + AuthenticationManager.SignIn(new AuthenticationProperties() + { + IsPersistent = isPersistent, + AllowRefresh = true, + IssuedUtc = nowUtc, + ExpiresUtc = nowUtc.AddMinutes(GlobalSettings.TimeOutInMinutes) + }, userIdentity); + } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs index def46b7556..798c124880 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -1,17 +1,16 @@ using System; using System.Linq; -using System.Security.Claims; using System.Text; -using System.Threading.Tasks; using System.Web.Security; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.Owin; -using Microsoft.Owin; using Umbraco.Core.Models.Identity; using Umbraco.Core.Services; namespace Umbraco.Core.Security { + + /// /// Default back office user manager /// @@ -89,6 +88,7 @@ namespace Umbraco.Core.Security RequireDigit = false, RequireLowercase = false, RequireUppercase = false + //TODO: Do we support the old regex match thing that membership providers used? }; //use a custom hasher based on our membership provider @@ -100,6 +100,9 @@ namespace Umbraco.Core.Security manager.UserTokenProvider = new DataProtectorTokenProvider(dataProtectionProvider.Create("ASP.NET Identity")); } + manager.UserLockoutEnabledByDefault = true; + manager.MaxFailedAccessAttemptsBeforeLockout = membershipProvider.MaxInvalidPasswordAttempts; + //custom identity factory for creating the identity object for which we auth against in the back office manager.ClaimsIdentityFactory = new BackOfficeClaimsIdentityFactory(); @@ -149,13 +152,9 @@ namespace Umbraco.Core.Security get { return false; } } - //TODO: Support this - public override bool SupportsUserLockout - { - get { return false; } - } - - //TODO: Support this + /// + /// Developers will need to override this to support custom 2 factor auth + /// public override bool SupportsUserTwoFactor { get { return false; } diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index cf433b729c..5745abb02e 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using System.Web.Security; using AutoMapper; using Microsoft.AspNet.Identity; +using Microsoft.Owin; using Umbraco.Core.Models.Identity; using Umbraco.Core.Models.Membership; using Umbraco.Core.Services; @@ -17,15 +18,12 @@ namespace Umbraco.Core.Security IUserEmailStore, IUserLoginStore, IUserRoleStore, - IUserSecurityStampStore - + IUserSecurityStampStore, + IUserLockoutStore, + IUserTwoFactorStore + //TODO: This would require additional columns/tables for now people will need to implement this on their own //IUserPhoneNumberStore, - //IUserTwoFactorStore, - - //TODO: This will require additional columns/tables - //IUserLockoutStore - //TODO: To do this we need to implement IQueryable - we'll have an IQuerable implementation soon with the UmbracoLinqPadDriver implementation //IQueryableUserStore { @@ -506,6 +504,117 @@ namespace Umbraco.Core.Security return user; } + /// + /// Sets whether two factor authentication is enabled for the user + /// + /// + /// + public virtual Task SetTwoFactorEnabledAsync(BackOfficeIdentityUser user, bool enabled) + { + user.TwoFactorEnabled = false; + return Task.FromResult(0); + } + + /// + /// Returns whether two factor authentication is enabled for the user + /// + /// + /// + public virtual Task GetTwoFactorEnabledAsync(BackOfficeIdentityUser user) + { + return Task.FromResult(false); + } + + #region IUserLockoutStore + + /// + /// Returns the DateTimeOffset that represents the end of a user's lockout, any time in the past should be considered not locked out. + /// + /// + /// + /// + /// Currently we do not suport a timed lock out, when they are locked out, an admin will have to reset the status + /// + public Task GetLockoutEndDateAsync(BackOfficeIdentityUser user) + { + if (user == null) throw new ArgumentNullException("user"); + + return user.LockoutEndDateUtc.HasValue + ? Task.FromResult(new DateTimeOffset(user.LockoutEndDateUtc.Value, TimeSpan.FromHours(0))) + : Task.FromResult(DateTimeOffset.MaxValue); + } + + /// + /// Locks a user out until the specified end date (set to a past date, to unlock a user) + /// + /// + /// + public Task SetLockoutEndDateAsync(BackOfficeIdentityUser user, DateTimeOffset lockoutEnd) + { + if (user == null) throw new ArgumentNullException("user"); + user.LockoutEndDateUtc = lockoutEnd.UtcDateTime; + return Task.FromResult(0); + } + + /// + /// Used to record when an attempt to access the user has failed + /// + /// + /// + public Task IncrementAccessFailedCountAsync(BackOfficeIdentityUser user) + { + if (user == null) throw new ArgumentNullException("user"); + user.AccessFailedCount++; + return Task.FromResult(user.AccessFailedCount); + } + + /// + /// Used to reset the access failed count, typically after the account is successfully accessed + /// + /// + /// + public Task ResetAccessFailedCountAsync(BackOfficeIdentityUser user) + { + if (user == null) throw new ArgumentNullException("user"); + throw new NotImplementedException(); + } + + /// + /// Returns the current number of failed access attempts. This number usually will be reset whenever the password is + /// verified or the account is locked out. + /// + /// + /// + public Task GetAccessFailedCountAsync(BackOfficeIdentityUser user) + { + if (user == null) throw new ArgumentNullException("user"); + return Task.FromResult(user.AccessFailedCount); + } + + /// + /// Returns whether the user can be locked out. + /// + /// + /// + public Task GetLockoutEnabledAsync(BackOfficeIdentityUser user) + { + if (user == null) throw new ArgumentNullException("user"); + return Task.FromResult(user.LockoutEnabled); + } + + /// + /// Sets whether the user can be locked out. + /// + /// + /// + public Task SetLockoutEnabledAsync(BackOfficeIdentityUser user, bool enabled) + { + if (user == null) throw new ArgumentNullException("user"); + user.LockoutEnabled = enabled; + return Task.FromResult(0); + } + #endregion + private bool UpdateMemberProperties(Models.Membership.IUser user, BackOfficeIdentityUser identityUser) { var anythingChanged = false; @@ -579,10 +688,13 @@ namespace Umbraco.Core.Security return anythingChanged; } + private void ThrowIfDisposed() { if (_disposed) throw new ObjectDisposedException(GetType().Name); } + + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 007d12500b..c6883c6a94 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -58,11 +58,11 @@ False - ..\packages\Microsoft.AspNet.Identity.Core.2.2.0\lib\net45\Microsoft.AspNet.Identity.Core.dll + ..\packages\Microsoft.AspNet.Identity.Core.2.2.1\lib\net45\Microsoft.AspNet.Identity.Core.dll False - ..\packages\Microsoft.AspNet.Identity.Owin.2.2.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll + ..\packages\Microsoft.AspNet.Identity.Owin.2.2.1\lib\net45\Microsoft.AspNet.Identity.Owin.dll False @@ -428,6 +428,7 @@ + diff --git a/src/Umbraco.Core/packages.config b/src/Umbraco.Core/packages.config index 6ffcf860e6..9edfbb097e 100644 --- a/src/Umbraco.Core/packages.config +++ b/src/Umbraco.Core/packages.config @@ -3,8 +3,8 @@ - - + + diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index e2b2d7b924..454250855b 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -151,14 +151,14 @@ ..\packages\Lucene.Net.2.9.4.1\lib\net40\Lucene.Net.dll - ..\packages\Microsoft.AspNet.Identity.Core.2.2.0\lib\net45\Microsoft.AspNet.Identity.Core.dll + ..\packages\Microsoft.AspNet.Identity.Core.2.2.1\lib\net45\Microsoft.AspNet.Identity.Core.dll True False - ..\packages\Microsoft.AspNet.Identity.Owin.2.2.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll + ..\packages\Microsoft.AspNet.Identity.Owin.2.2.1\lib\net45\Microsoft.AspNet.Identity.Owin.dll - + ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll True @@ -174,7 +174,7 @@ False ..\packages\Microsoft.Owin.Security.Cookies.3.0.1\lib\net45\Microsoft.Owin.Security.Cookies.dll - + False ..\packages\Microsoft.Owin.Security.OAuth.3.0.1\lib\net45\Microsoft.Owin.Security.OAuth.dll @@ -224,7 +224,7 @@ - + False @@ -343,7 +343,7 @@ Properties\SolutionInfo.cs - + loadStarterKits.ascx ASPXCodeBehind diff --git a/src/Umbraco.Web.UI/packages.config b/src/Umbraco.Web.UI/packages.config index 12afdc860d..a81a154970 100644 --- a/src/Umbraco.Web.UI/packages.config +++ b/src/Umbraco.Web.UI/packages.config @@ -9,8 +9,8 @@ - - + + diff --git a/src/Umbraco.Web/Editors/AuthenticationController.cs b/src/Umbraco.Web/Editors/AuthenticationController.cs index e3e2abf42f..95f985148c 100644 --- a/src/Umbraco.Web/Editors/AuthenticationController.cs +++ b/src/Umbraco.Web/Editors/AuthenticationController.cs @@ -31,6 +31,7 @@ using Microsoft.AspNet.Identity.Owin; using Umbraco.Core.Logging; using Newtonsoft.Json.Linq; using Umbraco.Core.Models.Identity; +using Umbraco.Web.Security.Identity; using IUser = Umbraco.Core.Models.Membership.IUser; namespace Umbraco.Web.Editors @@ -47,6 +48,7 @@ namespace Umbraco.Web.Editors { private BackOfficeUserManager _userManager; + private BackOfficeSignInManager _signInManager; protected BackOfficeUserManager UserManager { @@ -64,6 +66,24 @@ namespace Umbraco.Web.Editors return _userManager; } } + + protected BackOfficeSignInManager SignInManager + { + get + { + if (_signInManager == null) + { + var mgr = TryGetOwinContext().Result.Get(); + if (mgr == null) + { + throw new NullReferenceException("Could not resolve an instance of " + typeof(BackOfficeSignInManager) + " from the " + typeof(IOwinContext)); + } + _signInManager = mgr; + } + return _signInManager; + } + } + [WebApi.UmbracoAuthorize] [ValidateAngularAntiForgeryToken] @@ -76,7 +96,7 @@ namespace Umbraco.Web.Editors if (result.Succeeded) { var user = await UserManager.FindByIdAsync(User.Identity.GetUserId()); - await SignInAsync(user, isPersistent: false); + await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false); return Request.CreateResponse(HttpStatusCode.OK); } else @@ -146,36 +166,73 @@ namespace Umbraco.Web.Editors if (http.Success == false) throw new InvalidOperationException("This method requires that an HttpContext be active"); - if (UmbracoContext.Security.ValidateBackOfficeCredentials(loginModel.Username, loginModel.Password)) + var result = await SignInManager.PasswordSignInAsync( + loginModel.Username, loginModel.Password, isPersistent: true, shouldLockout: true); + + switch (result) { - //get the user - var user = Security.GetBackOfficeUser(loginModel.Username); - var userDetail = Mapper.Map(user); + case SignInStatus.Success: - //create a response with the userDetail object - var response = Request.CreateResponse(HttpStatusCode.OK, userDetail); + //get the user + var user = Security.GetBackOfficeUser(loginModel.Username); + var userDetail = Mapper.Map(user); - //set the response cookies with the ticket (NOTE: This needs to be done with the custom webapi extension because - // we cannot mix HttpContext.Response.Cookies and the way WebApi/Owin work) - var ticket = response.UmbracoLoginWebApi(user); + //create a response with the userDetail object + var response = Request.CreateResponse(HttpStatusCode.OK, userDetail); - //Identity does some of it's own checks as well so we need to use it's sign in process too... this will essentially re-create the - // ticket/cookie above but we need to create the ticket now so we can assign the Current Thread User/IPrinciple below - await SignInAsync(Mapper.Map(user), isPersistent: true); - //This ensure the current principal is set, otherwise any logic executing after this wouldn't actually be authenticated - http.Result.AuthenticateCurrentRequest(ticket, false); - - //update the userDetail and set their remaining seconds - userDetail.SecondsUntilTimeout = ticket.GetRemainingAuthSeconds(); + //set the response cookies with the ticket (NOTE: This needs to be done with the custom webapi extension because + // we cannot mix HttpContext.Response.Cookies and the way WebApi/Owin work) + var ticket = response.UmbracoLoginWebApi(user); - return response; + //This ensure the current principal is set, otherwise any logic executing after this wouldn't actually be authenticated + http.Result.AuthenticateCurrentRequest(ticket, false); + + //update the userDetail and set their remaining seconds + userDetail.SecondsUntilTimeout = ticket.GetRemainingAuthSeconds(); + + return response; + + case SignInStatus.RequiresVerification: + + var twofactorOptions = UserManager as IUmbracoBackOfficeTwoFactorOptions; + if (twofactorOptions == null) + { + throw new HttpResponseException( + Request.CreateErrorResponse( + HttpStatusCode.BadRequest, + "UserManager does not implement " + typeof(IUmbracoBackOfficeTwoFactorOptions))); + } + + var twofactorView = twofactorOptions.GetTwoFactorView( + TryGetOwinContext().Result, + UmbracoContext, + loginModel.Username); + + if (twofactorView.IsNullOrWhiteSpace()) + { + throw new HttpResponseException( + Request.CreateErrorResponse( + HttpStatusCode.BadRequest, + typeof(IUmbracoBackOfficeTwoFactorOptions) + ".GetTwoFactorView returned an empty string")); + } + + //create a with information to display a custom two factor send code view + var verifyResponse = Request.CreateResponse(HttpStatusCode.OK, new + { + twoFactorView = twofactorView + }); + + return verifyResponse; + + case SignInStatus.LockedOut: + case SignInStatus.Failure: + default: + //return BadRequest (400), we don't want to return a 401 because that get's intercepted + // by our angular helper because it thinks that we need to re-perform the request once we are + // authorized and we don't want to return a 403 because angular will show a warning msg indicating + // that the user doesn't have access to perform this function, we just want to return a normal invalid msg. + throw new HttpResponseException(HttpStatusCode.BadRequest); } - - //return BadRequest (400), we don't want to return a 401 because that get's intercepted - // by our angular helper because it thinks that we need to re-perform the request once we are - // authorized and we don't want to return a 403 because angular will show a warning msg indicating - // that the user doesn't have access to perform this function, we just want to return a normal invalid msg. - throw new HttpResponseException(HttpStatusCode.BadRequest); } @@ -199,24 +256,5 @@ namespace Umbraco.Web.Editors } } - private async Task SignInAsync(BackOfficeIdentityUser user, bool isPersistent) - { - var owinContext = TryGetOwinContext().Result; - - owinContext.Authentication.SignOut(Core.Constants.Security.BackOfficeExternalAuthenticationType); - - var nowUtc = DateTime.Now.ToUniversalTime(); - - owinContext.Authentication.SignIn( - new AuthenticationProperties() - { - IsPersistent = isPersistent, - AllowRefresh = true, - IssuedUtc = nowUtc, - ExpiresUtc = nowUtc.AddMinutes(GlobalSettings.TimeOutInMinutes) - }, - await user.GenerateUserIdentityAsync(UserManager)); - } - } } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index ad1e4ea571..46dc4df910 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -52,12 +52,23 @@ namespace Umbraco.Web.Editors public class BackOfficeController : UmbracoController { private BackOfficeUserManager _userManager; + private BackOfficeSignInManager _signInManager; + + protected BackOfficeSignInManager SignInManager + { + get { return _signInManager ?? (_signInManager = OwinContext.Get()); } + } protected BackOfficeUserManager UserManager { get { return _userManager ?? (_userManager = OwinContext.GetUserManager()); } } + protected IAuthenticationManager AuthenticationManager + { + get { return OwinContext.Authentication; } + } + /// /// Render the default view /// @@ -450,7 +461,7 @@ namespace Umbraco.Web.Editors var loginInfo = await OwinContext.Authentication.GetExternalLoginInfoAsync( Core.Constants.Security.BackOfficeExternalAuthenticationType); - if (loginInfo == null) + if (loginInfo == null || loginInfo.ExternalIdentity.IsAuthenticated == false) { return defaultResponse(); } @@ -475,7 +486,7 @@ namespace Umbraco.Web.Editors // that the ticket is created and stored and that the user is logged in. //sign in - await SignInAsync(user, isPersistent: false); + await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false); } else { @@ -537,8 +548,7 @@ namespace Umbraco.Web.Editors } else { - //var userMembershipProvider = global::Umbraco.Core.Security.MembershipProviderExtensions.GetUsersMembershipProvider(); - + var autoLinkUser = new BackOfficeIdentityUser() { Email = loginInfo.Email, @@ -548,6 +558,13 @@ namespace Umbraco.Web.Editors Culture = autoLinkOptions.GetDefaultCulture(UmbracoContext, loginInfo), UserName = loginInfo.Email }; + + //call the callback if one is assigned + if (autoLinkOptions.OnAutoLinking != null) + { + autoLinkOptions.OnAutoLinking(autoLinkUser, loginInfo); + } + var userCreationResult = await UserManager.CreateAsync(autoLinkUser); if (userCreationResult.Succeeded == false) @@ -571,14 +588,8 @@ namespace Umbraco.Web.Editors } else { - - //Ok, we're all linked up! Assign the auto-link options to a ViewBag property, this can be used - // in the view to render a custom view (AutoLinkExternalAccountView) if required, which will allow - // a developer to display a custom angular view to prompt the user for more information if required. - ViewBag.ExternalSignInAutoLinkOptions = autoLinkOptions; - //sign in - await SignInAsync(autoLinkUser, isPersistent: false); + await SignInManager.SignInAsync(autoLinkUser, isPersistent: false, rememberBrowser: false); } } } @@ -591,28 +602,6 @@ namespace Umbraco.Web.Editors return false; } - - private async Task SignInAsync(BackOfficeIdentityUser user, bool isPersistent) - { - OwinContext.Authentication.SignOut(Core.Constants.Security.BackOfficeExternalAuthenticationType); - - var nowUtc = DateTime.Now.ToUniversalTime(); - - OwinContext.Authentication.SignIn( - new AuthenticationProperties() - { - IsPersistent = isPersistent, - AllowRefresh = true, - IssuedUtc = nowUtc, - ExpiresUtc = nowUtc.AddMinutes(GlobalSettings.TimeOutInMinutes) - }, - await user.GenerateUserIdentityAsync(UserManager)); - } - - private IAuthenticationManager AuthenticationManager - { - get { return OwinContext.Authentication; } - } /// /// Returns the server variables regarding the application state diff --git a/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs b/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs index e3f58bb69e..75e628851e 100644 --- a/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs +++ b/src/Umbraco.Web/HtmlHelperBackOfficeExtensions.cs @@ -47,10 +47,10 @@ namespace Umbraco.Web "; return html.Raw(str); - } + } /// - /// Used to render the script tag that will pass in the angular externalLoginInfo service on page load + /// Used to render the script that will pass in the angular externalLoginInfo service on page load /// /// /// @@ -67,10 +67,6 @@ namespace Umbraco.Web }) .ToArray(); - - //define a callback that is executed when we bootstrap angular, this is used to inject angular values - //with server side info - var sb = new StringBuilder(); sb.AppendLine(); sb.AppendLine(@"var errors = [];"); diff --git a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs index a9a884a2e7..58a77d91cf 100644 --- a/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs +++ b/src/Umbraco.Web/Security/Identity/AppBuilderExtensions.cs @@ -55,6 +55,9 @@ namespace Umbraco.Web.Security.Identity appContext.Services.UserService, appContext.Services.ExternalLoginService, userMembershipProvider)); + + //Create a sign in manager per request + app.CreatePerOwinContext(BackOfficeSignInManager.Create); } /// @@ -82,6 +85,9 @@ namespace Umbraco.Web.Security.Identity options, customUserStore, userMembershipProvider)); + + //Create a sign in manager per request + app.CreatePerOwinContext(BackOfficeSignInManager.Create); } /// @@ -104,6 +110,9 @@ namespace Umbraco.Web.Security.Identity //Configure Umbraco user manager to be created per request app.CreatePerOwinContext(userManager); + + //Create a sign in manager per request + app.CreatePerOwinContext(BackOfficeSignInManager.Create); } /// diff --git a/src/Umbraco.Web/Security/Identity/ExternalSignInAutoLinkOptions.cs b/src/Umbraco.Web/Security/Identity/ExternalSignInAutoLinkOptions.cs index 9fb9a88a45..832f0b3a30 100644 --- a/src/Umbraco.Web/Security/Identity/ExternalSignInAutoLinkOptions.cs +++ b/src/Umbraco.Web/Security/Identity/ExternalSignInAutoLinkOptions.cs @@ -1,7 +1,9 @@ +using System; using Microsoft.AspNet.Identity.Owin; using Microsoft.Owin; using Umbraco.Core; using Umbraco.Core.Configuration; +using Umbraco.Core.Models.Identity; namespace Umbraco.Web.Security.Identity { @@ -10,22 +12,27 @@ namespace Umbraco.Web.Security.Identity /// public sealed class ExternalSignInAutoLinkOptions { - public ExternalSignInAutoLinkOptions( bool autoLinkExternalAccount = false, - string defaultUserType = "editor", string[] defaultAllowedSections = null, string defaultCulture = null, string autoLinkExternalAccountView = null) + string defaultUserType = "editor", + string[] defaultAllowedSections = null, + string defaultCulture = null) { Mandate.ParameterNotNullOrEmpty(defaultUserType, "defaultUserType"); _defaultUserType = defaultUserType; _defaultAllowedSections = defaultAllowedSections ?? new[] { "content", "media" }; _autoLinkExternalAccount = autoLinkExternalAccount; - _autoLinkExternalAccountView = autoLinkExternalAccountView; _defaultCulture = defaultCulture ?? GlobalSettings.DefaultUILanguage; } private readonly string _defaultUserType; + /// + /// A callback executed during account auto-linking and before the user is persisted + /// + public Action OnAutoLinking { get; set; } + /// /// The default User Type alias to use for auto-linking users /// @@ -56,18 +63,7 @@ namespace Umbraco.Web.Security.Identity { return _autoLinkExternalAccount; } - - private readonly string _autoLinkExternalAccountView; - - /// - /// Generally this is empty which means auto-linking will be silent, however in some cases developers may want to - /// prompt the user to enter additional user information that they want to save with the user that has been created. - /// - public string GetAutoLinkExternalAccountView(UmbracoContext umbracoContext, ExternalLoginInfo loginInfo) - { - return _autoLinkExternalAccountView; - } - + private readonly string _defaultCulture; /// diff --git a/src/Umbraco.Web/Security/Identity/IUmbracoBackOfficeTwoFactorOptions.cs b/src/Umbraco.Web/Security/Identity/IUmbracoBackOfficeTwoFactorOptions.cs new file mode 100644 index 0000000000..54d1d6bdce --- /dev/null +++ b/src/Umbraco.Web/Security/Identity/IUmbracoBackOfficeTwoFactorOptions.cs @@ -0,0 +1,12 @@ +using Microsoft.Owin; + +namespace Umbraco.Web.Security.Identity +{ + /// + /// Used to display a custom view in the back office if developers choose to implement their own custom 2 factor authentication + /// + public interface IUmbracoBackOfficeTwoFactorOptions + { + string GetTwoFactorView(IOwinContext owinContext, UmbracoContext umbracoContext, string username); + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index b9df10254d..f22db0c3b2 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -134,11 +134,11 @@ False - ..\packages\Microsoft.AspNet.Identity.Core.2.2.0\lib\net45\Microsoft.AspNet.Identity.Core.dll + ..\packages\Microsoft.AspNet.Identity.Core.2.2.1\lib\net45\Microsoft.AspNet.Identity.Core.dll False - ..\packages\Microsoft.AspNet.Identity.Owin.2.2.0\lib\net45\Microsoft.AspNet.Identity.Owin.dll + ..\packages\Microsoft.AspNet.Identity.Owin.2.2.1\lib\net45\Microsoft.AspNet.Identity.Owin.dll @@ -309,6 +309,7 @@ + diff --git a/src/Umbraco.Web/packages.config b/src/Umbraco.Web/packages.config index 33c259c885..eecd485e24 100644 --- a/src/Umbraco.Web/packages.config +++ b/src/Umbraco.Web/packages.config @@ -6,8 +6,8 @@ - - + + From d0c4b2ab7293f4ccacb6b9bcda237bd51be8a2b6 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 1 Jul 2015 18:02:58 +0200 Subject: [PATCH 17/50] Fixes user lockout with aspnet identity --- .../Models/Identity/BackOfficeIdentityUser.cs | 24 +++++++++++++++++++ .../Models/Identity/IdentityModelMappings.cs | 5 ++-- .../Security/BackOfficeUserManager.cs | 4 ++++ .../Security/BackOfficeUserStore.cs | 18 +++++++------- .../Security/MembershipProviderBase.cs | 2 +- 5 files changed, 41 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs index 9a53023a8d..0d803c26e5 100644 --- a/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs +++ b/src/Umbraco.Core/Models/Identity/BackOfficeIdentityUser.cs @@ -38,6 +38,30 @@ namespace Umbraco.Core.Models.Identity public string UserTypeAlias { get; set; } + /// + /// Lockout is always enabled + /// + public override bool LockoutEnabled + { + get { return true; } + set + { + //do nothing + } + } + + /// + /// Based on the user's lockout end date, this will determine if they are locked out + /// + internal bool IsLockedOut + { + get + { + var isLocked = (LockoutEndDateUtc.HasValue && LockoutEndDateUtc.Value.ToLocalTime() >= DateTime.Now); + return isLocked; + } + } + /// /// Overridden to make the retrieval lazy /// diff --git a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs index 7e30d4a81e..f57d6683a2 100644 --- a/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs +++ b/src/Umbraco.Core/Models/Identity/IdentityModelMappings.cs @@ -14,9 +14,7 @@ namespace Umbraco.Core.Models.Identity config.CreateMap() .ForMember(user => user.Email, expression => expression.MapFrom(user => user.Email)) .ForMember(user => user.Id, expression => expression.MapFrom(user => user.Id)) - .ForMember(user => user.LockoutEnabled, expression => expression.MapFrom(user => user.IsLockedOut)) - //Users currently are locked out for an infinite time, we do not support timed lock outs currently - .ForMember(user => user.LockoutEndDateUtc, expression => expression.UseValue(DateTime.MaxValue.ToUniversalTime())) + .ForMember(user => user.LockoutEndDateUtc, expression => expression.MapFrom(user => user.IsLockedOut ? DateTime.MaxValue.ToUniversalTime() : (DateTime?)null)) .ForMember(user => user.UserName, expression => expression.MapFrom(user => user.Username)) .ForMember(user => user.PasswordHash, expression => expression.MapFrom(user => GetPasswordHash(user.RawPasswordValue))) .ForMember(user => user.Culture, expression => expression.MapFrom(user => user.GetUserCulture(applicationContext.Services.TextService))) @@ -24,6 +22,7 @@ namespace Umbraco.Core.Models.Identity .ForMember(user => user.StartMediaId, expression => expression.MapFrom(user => user.StartMediaId)) .ForMember(user => user.StartContentId, expression => expression.MapFrom(user => user.StartContentId)) .ForMember(user => user.UserTypeAlias, expression => expression.MapFrom(user => user.UserType.Alias)) + .ForMember(user => user.AccessFailedCount, expression => expression.MapFrom(user => user.FailedPasswordAttempts)) .ForMember(user => user.AllowedSections, expression => expression.MapFrom(user => user.AllowedSections.ToArray())); } diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs index 798c124880..f86c06c39c 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -102,6 +102,10 @@ namespace Umbraco.Core.Security manager.UserLockoutEnabledByDefault = true; manager.MaxFailedAccessAttemptsBeforeLockout = membershipProvider.MaxInvalidPasswordAttempts; + //NOTE: This just needs to be in the future, we currently don't support a lockout timespan, it's either they are locked + // or they are not locked, but this determines what is set on the account lockout date which corresponds to whether they are + // locked out or not. + manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromDays(30); //custom identity factory for creating the identity object for which we auth against in the back office manager.ClaimsIdentityFactory = new BackOfficeClaimsIdentityFactory(); diff --git a/src/Umbraco.Core/Security/BackOfficeUserStore.cs b/src/Umbraco.Core/Security/BackOfficeUserStore.cs index 5745abb02e..e37f9d1a54 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserStore.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserStore.cs @@ -78,7 +78,7 @@ namespace Umbraco.Core.Security Username = user.UserName, StartContentId = user.StartContentId == 0 ? -1 : user.StartContentId, StartMediaId = user.StartMediaId == 0 ? -1 : user.StartMediaId, - IsLockedOut = user.LockoutEnabled, + IsLockedOut = user.IsLockedOut, IsApproved = true }; @@ -540,8 +540,8 @@ namespace Umbraco.Core.Security if (user == null) throw new ArgumentNullException("user"); return user.LockoutEndDateUtc.HasValue - ? Task.FromResult(new DateTimeOffset(user.LockoutEndDateUtc.Value, TimeSpan.FromHours(0))) - : Task.FromResult(DateTimeOffset.MaxValue); + ? Task.FromResult(DateTimeOffset.MaxValue) + : Task.FromResult(DateTimeOffset.MinValue); } /// @@ -576,7 +576,8 @@ namespace Umbraco.Core.Security public Task ResetAccessFailedCountAsync(BackOfficeIdentityUser user) { if (user == null) throw new ArgumentNullException("user"); - throw new NotImplementedException(); + user.AccessFailedCount = 0; + return Task.FromResult(0); } /// @@ -592,7 +593,7 @@ namespace Umbraco.Core.Security } /// - /// Returns whether the user can be locked out. + /// Returns true /// /// /// @@ -603,7 +604,7 @@ namespace Umbraco.Core.Security } /// - /// Sets whether the user can be locked out. + /// Doesn't actually perform any function, users can always be locked out /// /// /// @@ -635,10 +636,10 @@ namespace Umbraco.Core.Security anythingChanged = true; user.FailedPasswordAttempts = identityUser.AccessFailedCount; } - if (user.IsLockedOut != identityUser.LockoutEnabled) + if (user.IsLockedOut != identityUser.IsLockedOut) { anythingChanged = true; - user.IsLockedOut = identityUser.LockoutEnabled; + user.IsLockedOut = identityUser.IsLockedOut; } if (user.Username != identityUser.UserName && identityUser.UserName.IsNullOrWhiteSpace() == false) { @@ -671,6 +672,7 @@ namespace Umbraco.Core.Security anythingChanged = true; user.SecurityStamp = identityUser.SecurityStamp; } + if (user.AllowedSections.ContainsAll(identityUser.AllowedSections) == false || identityUser.AllowedSections.ContainsAll(user.AllowedSections) == false) { diff --git a/src/Umbraco.Core/Security/MembershipProviderBase.cs b/src/Umbraco.Core/Security/MembershipProviderBase.cs index 794c8fda2d..392c1545d1 100644 --- a/src/Umbraco.Core/Security/MembershipProviderBase.cs +++ b/src/Umbraco.Core/Security/MembershipProviderBase.cs @@ -225,7 +225,7 @@ namespace Umbraco.Core.Security _enablePasswordReset = config.GetValue("enablePasswordReset", false); _requiresQuestionAndAnswer = config.GetValue("requiresQuestionAndAnswer", false); _requiresUniqueEmail = config.GetValue("requiresUniqueEmail", true); - _maxInvalidPasswordAttempts = GetIntValue(config, "maxInvalidPasswordAttempts", 20, false, 0); + _maxInvalidPasswordAttempts = GetIntValue(config, "maxInvalidPasswordAttempts", 5, false, 0); _passwordAttemptWindow = GetIntValue(config, "passwordAttemptWindow", 10, false, 0); _minRequiredPasswordLength = GetIntValue(config, "minRequiredPasswordLength", DefaultMinPasswordLength, true, 0x80); _minRequiredNonAlphanumericCharacters = GetIntValue(config, "minRequiredNonalphanumericCharacters", DefaultMinNonAlphanumericChars, true, 0x80); From 9c50c67319fc1d27b59e887bea3d37f173358e9a Mon Sep 17 00:00:00 2001 From: craig Date: Mon, 29 Jun 2015 00:16:15 +0100 Subject: [PATCH 18/50] U4-6724 Moving content with JSON Tags add extra characters --- .../Persistence/Repositories/VersionableRepositoryBase.cs | 2 +- src/Umbraco.Core/PropertyEditors/TagPropertyDefinition.cs | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index 27fe5ef4ad..3610645241 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -395,7 +395,7 @@ ON cmsPropertyType.dataTypeId = cmsDataTypePreValues.datatypeNodeId", docSql.Arg // below if any property requires tag support var allPreValues = new Lazy>(() => { - var preValsSql = new Sql(@"SELECT a.id as preValId, a.value, a.sortorder, a.alias, a.datatypeNodeId + var preValsSql = new Sql(@"SELECT a.id, a.value, a.sortorder, a.alias, a.datatypeNodeId FROM cmsDataTypePreValues a WHERE EXISTS( SELECT DISTINCT b.id as preValIdInner diff --git a/src/Umbraco.Core/PropertyEditors/TagPropertyDefinition.cs b/src/Umbraco.Core/PropertyEditors/TagPropertyDefinition.cs index 978a7bf2ba..fd314ba9b7 100644 --- a/src/Umbraco.Core/PropertyEditors/TagPropertyDefinition.cs +++ b/src/Umbraco.Core/PropertyEditors/TagPropertyDefinition.cs @@ -31,7 +31,10 @@ namespace Umbraco.Core.PropertyEditors Delimiter = tagsAttribute.Delimiter; ReplaceTags = tagsAttribute.ReplaceTags; TagGroup = tagsAttribute.TagGroup; - StorageType = TagCacheStorageType.Csv; + + var preValues = propertySaving.PreValues.PreValuesAsDictionary; + StorageType = preValues.ContainsKey("storageType") && preValues["storageType"].Value == TagCacheStorageType.Json.ToString() ? + TagCacheStorageType.Json : TagCacheStorageType.Csv; } /// From 22c035c9b943aaa39580905566af1cb8cbb606a1 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 2 Jul 2015 11:21:46 +0200 Subject: [PATCH 19/50] Fixes test --- src/Umbraco.Tests/Membership/MembershipProviderBaseTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Tests/Membership/MembershipProviderBaseTests.cs b/src/Umbraco.Tests/Membership/MembershipProviderBaseTests.cs index 609452978c..c786a61996 100644 --- a/src/Umbraco.Tests/Membership/MembershipProviderBaseTests.cs +++ b/src/Umbraco.Tests/Membership/MembershipProviderBaseTests.cs @@ -148,7 +148,7 @@ namespace Umbraco.Tests.Membership Assert.AreEqual(false, provider.EnablePasswordReset); Assert.AreEqual(false, provider.RequiresQuestionAndAnswer); Assert.AreEqual(true, provider.RequiresUniqueEmail); - Assert.AreEqual(20, provider.MaxInvalidPasswordAttempts); + Assert.AreEqual(5, provider.MaxInvalidPasswordAttempts); Assert.AreEqual(10, provider.PasswordAttemptWindow); Assert.AreEqual(provider.DefaultMinPasswordLength, provider.MinRequiredPasswordLength); Assert.AreEqual(provider.DefaultMinNonAlphanumericChars, provider.MinRequiredNonAlphanumericCharacters); From 32e21b0314f40a0bd404b16e030681ac00083304 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 2 Jul 2015 14:30:21 +0200 Subject: [PATCH 20/50] Fixes: U4-5798 The database configuration failed with the following message: The incoming request has too many parameters. The server supports a maximum of 2100 parameters. --- .../TargetVersionSeven/AlterTagRelationsTable.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterTagRelationsTable.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterTagRelationsTable.cs index f6b7e75189..d822b7593a 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterTagRelationsTable.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSeven/AlterTagRelationsTable.cs @@ -38,15 +38,20 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSeven if (Context.CurrentDatabaseProvider == DatabaseProviders.MySql) { Delete.ForeignKey().FromTable("cmsTagRelationship").ForeignColumn("nodeId").ToTable("umbracoNode").PrimaryColumn("id"); + //check for another strange really old one that might have existed + if (constraints.Any(x => x.Item1 == "cmsTagRelationship" && x.Item2 == "tagId")) + { + Delete.ForeignKey().FromTable("cmsTagRelationship").ForeignColumn("tagId").ToTable("cmsTags").PrimaryColumn("id"); + } } else { //Before we try to delete this constraint, we'll see if it exists first, some older schemas never had it and some older schema's had this named // differently than the default. - var constraint = constraints - .SingleOrDefault(x => x.Item1 == "cmsTagRelationship" && x.Item2 == "nodeId" && x.Item3.InvariantStartsWith("PK_") == false); - if (constraint != null) + var constraintMatches = constraints.Where(x => x.Item1 == "cmsTagRelationship" && x.Item2 == "nodeId" && x.Item3.InvariantStartsWith("PK_") == false); + + foreach (var constraint in constraintMatches) { Delete.ForeignKey(constraint.Item3).OnTable("cmsTagRelationship"); } From f0742c9d7ca166468fa211ce2fe284e26594637b Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 2 Jul 2015 14:52:01 +0200 Subject: [PATCH 21/50] Fixes: U4-6733 Adding dictionary item (7.3): No parent dictionary item was found with id 41c7638d-f529-4bff-853e-59a0c2fb1bde --- src/umbraco.cms/businesslogic/Dictionary.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/umbraco.cms/businesslogic/Dictionary.cs b/src/umbraco.cms/businesslogic/Dictionary.cs index 0d34da4615..21d9bc516c 100644 --- a/src/umbraco.cms/businesslogic/Dictionary.cs +++ b/src/umbraco.cms/businesslogic/Dictionary.cs @@ -325,7 +325,9 @@ namespace umbraco.cms.businesslogic if (!hasKey(key)) { var item = ApplicationContext.Current.Services.LocalizationService.CreateDictionaryItemWithIdentity( - key, parentId, defaultValue); + key, + parentId == TopLevelParent ? (Guid?)null : parentId, + defaultValue); return item.Id; } From f68cdf43e1071d15c2964a3938e5fb4bfd006fbf Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 2 Jul 2015 17:19:42 +0200 Subject: [PATCH 22/50] Fixes: U4-6099 cmsLanguageText.languageId needs to be a foreign key to the umbracoLanguage table --- src/Umbraco.Core/Constants-Conventions.cs | 1 + src/Umbraco.Core/Models/DictionaryItem.cs | 16 ++--- src/Umbraco.Core/Models/IDictionaryItem.cs | 2 +- .../Models/Rdbms/DictionaryDto.cs | 4 +- .../Models/Rdbms/LanguageTextDto.cs | 1 + .../Persistence/DatabaseSchemaHelper.cs | 14 +++-- .../Initial/DatabaseSchemaCreation.cs | 4 +- .../Migrations/MigrationExpressionBase.cs | 27 +++++++++ .../Expressions/DeleteDataExpression.cs | 2 +- .../Expressions/InsertDataExpression.cs | 25 +------- .../Expressions/UpdateDataExpression.cs | 4 +- ...reignKeysForLanguageAndDictionaryTables.cs | 58 +++++++++++++++++++ .../Repositories/DictionaryRepository.cs | 5 +- .../Services/LocalizationService.cs | 5 +- src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../Persistence/BaseTableByTableTest.cs | 1 + .../Services/LocalizationServiceTests.cs | 4 +- src/umbraco.cms/businesslogic/Dictionary.cs | 8 +-- 18 files changed, 122 insertions(+), 60 deletions(-) create mode 100644 src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddForeignKeysForLanguageAndDictionaryTables.cs diff --git a/src/Umbraco.Core/Constants-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs index c1d5bd35e3..3456ff1cfa 100644 --- a/src/Umbraco.Core/Constants-Conventions.cs +++ b/src/Umbraco.Core/Constants-Conventions.cs @@ -27,6 +27,7 @@ namespace Umbraco.Core /// /// The root id for all top level dictionary items /// + [Obsolete("There is no dictionary root item id anymore, it is simply null")] public const string DictionaryItemRootId = "41c7638d-f529-4bff-853e-59a0c2fb1bde"; } diff --git a/src/Umbraco.Core/Models/DictionaryItem.cs b/src/Umbraco.Core/Models/DictionaryItem.cs index e09c370053..7e59726c80 100644 --- a/src/Umbraco.Core/Models/DictionaryItem.cs +++ b/src/Umbraco.Core/Models/DictionaryItem.cs @@ -14,22 +14,22 @@ namespace Umbraco.Core.Models [DataContract(IsReference = true)] public class DictionaryItem : Entity, IDictionaryItem { - private Guid _parentId; + private Guid? _parentId; private string _itemKey; private IEnumerable _translations; public DictionaryItem(string itemKey) - : this(new Guid(Constants.Conventions.Localization.DictionaryItemRootId), itemKey) + : this(null, itemKey) {} - public DictionaryItem(Guid parentId, string itemKey) + public DictionaryItem(Guid? parentId, string itemKey) { _parentId = parentId; _itemKey = itemKey; _translations = new List(); } - private static readonly PropertyInfo ParentIdSelector = ExpressionHelper.GetPropertyInfo(x => x.ParentId); + private static readonly PropertyInfo ParentIdSelector = ExpressionHelper.GetPropertyInfo(x => x.ParentId); private static readonly PropertyInfo ItemKeySelector = ExpressionHelper.GetPropertyInfo(x => x.ItemKey); private static readonly PropertyInfo TranslationsSelector = ExpressionHelper.GetPropertyInfo>(x => x.Translations); @@ -37,7 +37,7 @@ namespace Umbraco.Core.Models /// Gets or Sets the Parent Id of the Dictionary Item /// [DataMember] - public Guid ParentId + public Guid? ParentId { get { return _parentId; } set @@ -95,11 +95,7 @@ namespace Umbraco.Core.Models { base.AddingEntity(); - Key = Guid.NewGuid(); - - //If ParentId is not set we should default to the root parent id - if(ParentId == Guid.Empty) - _parentId = new Guid(Constants.Conventions.Localization.DictionaryItemRootId); + Key = Guid.NewGuid(); } } diff --git a/src/Umbraco.Core/Models/IDictionaryItem.cs b/src/Umbraco.Core/Models/IDictionaryItem.cs index e10a80e831..ed88acfbec 100644 --- a/src/Umbraco.Core/Models/IDictionaryItem.cs +++ b/src/Umbraco.Core/Models/IDictionaryItem.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Models /// Gets or Sets the Parent Id of the Dictionary Item /// [DataMember] - Guid ParentId { get; set; } + Guid? ParentId { get; set; } /// /// Gets or sets the Key for the Dictionary Item diff --git a/src/Umbraco.Core/Models/Rdbms/DictionaryDto.cs b/src/Umbraco.Core/Models/Rdbms/DictionaryDto.cs index 18fd16b658..712d1937a9 100644 --- a/src/Umbraco.Core/Models/Rdbms/DictionaryDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/DictionaryDto.cs @@ -19,7 +19,9 @@ namespace Umbraco.Core.Models.Rdbms public Guid UniqueId { get; set; } [Column("parent")] - public Guid Parent { get; set; } + [NullSetting(NullSetting = NullSettings.Null)] + [ForeignKey(typeof(DictionaryDto), Column = "id")] + public Guid? Parent { get; set; } [Column("key")] [Length(1000)] diff --git a/src/Umbraco.Core/Models/Rdbms/LanguageTextDto.cs b/src/Umbraco.Core/Models/Rdbms/LanguageTextDto.cs index d062df4eae..87329fbd4c 100644 --- a/src/Umbraco.Core/Models/Rdbms/LanguageTextDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/LanguageTextDto.cs @@ -14,6 +14,7 @@ namespace Umbraco.Core.Models.Rdbms public int PrimaryKey { get; set; } [Column("languageId")] + [ForeignKey(typeof(LanguageDto), Column = "id")] public int LanguageId { get; set; } [Column("UniqueId")] diff --git a/src/Umbraco.Core/Persistence/DatabaseSchemaHelper.cs b/src/Umbraco.Core/Persistence/DatabaseSchemaHelper.cs index ca64775880..55c1fe505f 100644 --- a/src/Umbraco.Core/Persistence/DatabaseSchemaHelper.cs +++ b/src/Umbraco.Core/Persistence/DatabaseSchemaHelper.cs @@ -151,6 +151,13 @@ namespace Umbraco.Core.Persistence _db.Update("SET id = @IdAfter WHERE id = @IdBefore AND userLogin = @Login", new { IdAfter = 0, IdBefore = 1, Login = "admin" }); } + //Loop through index statements and execute sql + foreach (var sql in indexSql) + { + int createdIndex = _db.Execute(new Sql(sql)); + _logger.Info(string.Format("Create Index sql {0}:\n {1}", createdIndex, sql)); + } + //Loop through foreignkey statements and execute sql foreach (var sql in foreignSql) { @@ -158,12 +165,7 @@ namespace Umbraco.Core.Persistence _logger.Info(string.Format("Create Foreign Key sql {0}:\n {1}", createdFk, sql)); } - //Loop through index statements and execute sql - foreach (var sql in indexSql) - { - int createdIndex = _db.Execute(new Sql(sql)); - _logger.Info(string.Format("Create Index sql {0}:\n {1}", createdIndex, sql)); - } + transaction.Complete(); } diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs index ce2a53ea95..a864bf21a4 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/DatabaseSchemaCreation.cs @@ -48,8 +48,8 @@ namespace Umbraco.Core.Persistence.Migrations.Initial {7, typeof (DataTypeDto)}, {8, typeof (DataTypePreValueDto)}, {9, typeof (DictionaryDto)}, - {10, typeof (LanguageTextDto)}, - {11, typeof (LanguageDto)}, + {10, typeof (LanguageDto)}, + {11, typeof (LanguageTextDto)}, {12, typeof (DomainDto)}, {13, typeof (LogDto)}, {14, typeof (MacroDto)}, diff --git a/src/Umbraco.Core/Persistence/Migrations/MigrationExpressionBase.cs b/src/Umbraco.Core/Persistence/Migrations/MigrationExpressionBase.cs index e7535e78d4..7dde31d3e3 100644 --- a/src/Umbraco.Core/Persistence/Migrations/MigrationExpressionBase.cs +++ b/src/Umbraco.Core/Persistence/Migrations/MigrationExpressionBase.cs @@ -53,5 +53,32 @@ namespace Umbraco.Core.Persistence.Migrations /// to ensure they are not executed twice. /// internal string Name { get; set; } + + protected string GetQuotedValue(object val) + { + if (val == null) return "NULL"; + + var type = val.GetType(); + + switch (Type.GetTypeCode(type)) + { + case TypeCode.Boolean: + return ((bool)val) ? "1" : "0"; + case TypeCode.Single: + case TypeCode.Double: + case TypeCode.Decimal: + case TypeCode.SByte: + case TypeCode.Int16: + case TypeCode.Int32: + case TypeCode.Int64: + case TypeCode.Byte: + case TypeCode.UInt16: + case TypeCode.UInt32: + case TypeCode.UInt64: + return val.ToString(); + default: + return SqlSyntaxContext.SqlSyntaxProvider.GetQuotedValue(val.ToString()); + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Migrations/Syntax/Delete/Expressions/DeleteDataExpression.cs b/src/Umbraco.Core/Persistence/Migrations/Syntax/Delete/Expressions/DeleteDataExpression.cs index 2131a7b106..73966f6e93 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Syntax/Delete/Expressions/DeleteDataExpression.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Syntax/Delete/Expressions/DeleteDataExpression.cs @@ -44,7 +44,7 @@ namespace Umbraco.Core.Persistence.Migrations.Syntax.Delete.Expressions whereClauses.Add(string.Format("{0} {1} {2}", SqlSyntaxContext.SqlSyntaxProvider.GetQuotedColumnName(item.Key), item.Value == null ? "IS" : "=", - SqlSyntaxContext.SqlSyntaxProvider.GetQuotedValue(item.Value.ToString()))); + GetQuotedValue(item.Value))); } deleteItems.Add(string.Format(SqlSyntaxContext.SqlSyntaxProvider.DeleteData, diff --git a/src/Umbraco.Core/Persistence/Migrations/Syntax/Insert/Expressions/InsertDataExpression.cs b/src/Umbraco.Core/Persistence/Migrations/Syntax/Insert/Expressions/InsertDataExpression.cs index 330e22bb93..568087733b 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Syntax/Insert/Expressions/InsertDataExpression.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Syntax/Insert/Expressions/InsertDataExpression.cs @@ -60,29 +60,6 @@ namespace Umbraco.Core.Persistence.Migrations.Syntax.Insert.Expressions return string.Join(",", insertItems); } - private string GetQuotedValue(object val) - { - var type = val.GetType(); - - switch (Type.GetTypeCode(type)) - { - case TypeCode.Boolean: - return ((bool) val) ? "1" : "0"; - case TypeCode.Single: - case TypeCode.Double: - case TypeCode.Decimal: - case TypeCode.SByte: - case TypeCode.Int16: - case TypeCode.Int32: - case TypeCode.Int64: - case TypeCode.Byte: - case TypeCode.UInt16: - case TypeCode.UInt32: - case TypeCode.UInt64: - return val.ToString(); - default: - return SqlSyntaxContext.SqlSyntaxProvider.GetQuotedValue(val.ToString()); - } - } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Migrations/Syntax/Update/Expressions/UpdateDataExpression.cs b/src/Umbraco.Core/Persistence/Migrations/Syntax/Update/Expressions/UpdateDataExpression.cs index a5a3204974..fd470dd7f2 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Syntax/Update/Expressions/UpdateDataExpression.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Syntax/Update/Expressions/UpdateDataExpression.cs @@ -32,7 +32,7 @@ namespace Umbraco.Core.Persistence.Migrations.Syntax.Update.Expressions { updateItems.Add(string.Format("{0} = {1}", SqlSyntaxContext.SqlSyntaxProvider.GetQuotedColumnName(item.Key), - SqlSyntaxContext.SqlSyntaxProvider.GetQuotedValue(item.Value.ToString()))); + GetQuotedValue(item.Value))); } if (IsAllRows) @@ -46,7 +46,7 @@ namespace Umbraco.Core.Persistence.Migrations.Syntax.Update.Expressions whereClauses.Add(string.Format("{0} {1} {2}", SqlSyntaxContext.SqlSyntaxProvider.GetQuotedColumnName(item.Key), item.Value == null ? "IS" : "=", - SqlSyntaxContext.SqlSyntaxProvider.GetQuotedValue(item.Value.ToString()))); + GetQuotedValue(item.Value))); } } return string.Format(SqlSyntaxContext.SqlSyntaxProvider.UpdateData, diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddForeignKeysForLanguageAndDictionaryTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddForeignKeysForLanguageAndDictionaryTables.cs new file mode 100644 index 0000000000..1657cacc8b --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/AddForeignKeysForLanguageAndDictionaryTables.cs @@ -0,0 +1,58 @@ +using System; +using System.Data; +using System.Linq; +using Umbraco.Core.Configuration; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZero +{ + [Migration("7.3.0", 14, GlobalSettings.UmbracoMigrationName)] + public class AddForeignKeysForLanguageAndDictionaryTables : MigrationBase + { + public AddForeignKeysForLanguageAndDictionaryTables(ISqlSyntaxProvider sqlSyntax, ILogger logger) + : base(sqlSyntax, logger) + { + } + + public override void Up() + { + var constraints = SqlSyntax.GetConstraintsPerColumn(Context.Database).Distinct().ToArray(); + + //if the FK doesn't exist + if (constraints.Any(x => x.Item1.InvariantEquals("cmsLanguageText") && x.Item2.InvariantEquals("umbracoLanguage") && x.Item3.InvariantEquals("FK_cmsLanguageText_umbracoLanguage_id")) == false) + { + //Somehow, a language text item might end up with a language Id of zero or one that no longer exists + //before we add the foreign key + foreach (var pk in Context.Database.Query( + "SELECT cmsLanguageText.pk FROM cmsLanguageText WHERE cmsLanguageText.languageId NOT IN (SELECT umbracoLanguage.id FROM umbracoLanguage)")) + { + Delete.FromTable("cmsLanguageText").Row(new { pk = pk }); + } + + //now we need to create a foreign key + Create.ForeignKey("FK_cmsLanguageText_umbracoLanguage_id").FromTable("cmsLanguageText").ForeignColumn("languageId") + .ToTable("umbracoLanguage").PrimaryColumn("id").OnDeleteOrUpdate(Rule.None); + + Alter.Table("cmsDictionary").AlterColumn("parent").AsGuid().Nullable(); + + //set the parent to null if it equals the default dictionary item root id + foreach (var pk in Context.Database.Query("SELECT pk FROM cmsDictionary WHERE parent NOT IN (SELECT id FROM cmsDictionary)")) + { + Update.Table("cmsDictionary").Set(new { parent = (Guid?)null }).Where(new { pk = pk }); + } + + Create.ForeignKey("FK_cmsDictionary_cmsDictionary_id").FromTable("cmsDictionary").ForeignColumn("parent") + .ToTable("cmsDictionary").PrimaryColumn("id").OnDeleteOrUpdate(Rule.None); + } + + + + } + + public override void Down() + { + throw new System.NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs index cf3193943b..1e84814d1b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DictionaryRepository.cs @@ -103,12 +103,9 @@ namespace Umbraco.Core.Persistence.Repositories return new List(); } - /// - /// Returns the Top Level Parent Guid Id - /// protected override Guid NodeObjectTypeId { - get { return new Guid(Constants.Conventions.Localization.DictionaryItemRootId); } + get { throw new NotImplementedException(); } } #endregion diff --git a/src/Umbraco.Core/Services/LocalizationService.cs b/src/Umbraco.Core/Services/LocalizationService.cs index bd695b65e5..59b77085af 100644 --- a/src/Umbraco.Core/Services/LocalizationService.cs +++ b/src/Umbraco.Core/Services/LocalizationService.cs @@ -18,7 +18,6 @@ namespace Umbraco.Core.Services /// public class LocalizationService : RepositoryService, ILocalizationService { - private static readonly Guid RootParentId = new Guid(Constants.Conventions.Localization.DictionaryItemRootId); [Obsolete("Use the constructors that specify all dependencies instead")] public LocalizationService() @@ -93,7 +92,7 @@ namespace Umbraco.Core.Services } } - var item = new DictionaryItem(parentId.HasValue ? parentId.Value : RootParentId, key); + var item = new DictionaryItem(parentId, key); if (defaultValue.IsNullOrWhiteSpace() == false) { @@ -188,7 +187,7 @@ namespace Umbraco.Core.Services { using (var repository = RepositoryFactory.CreateDictionaryRepository(UowProvider.GetUnitOfWork())) { - var query = Query.Builder.Where(x => x.ParentId == RootParentId); + var query = Query.Builder.Where(x => x.ParentId == null); var items = repository.GetByQuery(query); return items; diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index c6883c6a94..b70653a7d3 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -381,6 +381,7 @@ + diff --git a/src/Umbraco.Tests/Persistence/BaseTableByTableTest.cs b/src/Umbraco.Tests/Persistence/BaseTableByTableTest.cs index 2467bf8a54..a05956e109 100644 --- a/src/Umbraco.Tests/Persistence/BaseTableByTableTest.cs +++ b/src/Umbraco.Tests/Persistence/BaseTableByTableTest.cs @@ -258,6 +258,7 @@ namespace Umbraco.Tests.Persistence using (Transaction transaction = Database.GetTransaction()) { DatabaseSchemaHelper.CreateTable(); + DatabaseSchemaHelper.CreateTable(); DatabaseSchemaHelper.CreateTable(); //transaction.Complete(); diff --git a/src/Umbraco.Tests/Services/LocalizationServiceTests.cs b/src/Umbraco.Tests/Services/LocalizationServiceTests.cs index 40597dc285..fd0fd1437d 100644 --- a/src/Umbraco.Tests/Services/LocalizationServiceTests.cs +++ b/src/Umbraco.Tests/Services/LocalizationServiceTests.cs @@ -179,7 +179,7 @@ namespace Umbraco.Tests.Services Assert.Greater(item.Id, 0); Assert.IsTrue(item.HasIdentity); - Assert.AreEqual(new Guid(Constants.Conventions.Localization.DictionaryItemRootId), item.ParentId); + Assert.IsFalse(item.ParentId.HasValue); Assert.AreEqual("Testing123", item.ItemKey); Assert.AreEqual(1, item.Translations.Count()); } @@ -197,7 +197,7 @@ namespace Umbraco.Tests.Services Assert.IsNotNull(item); Assert.Greater(item.Id, 0); Assert.IsTrue(item.HasIdentity); - Assert.AreEqual(new Guid(Constants.Conventions.Localization.DictionaryItemRootId), item.ParentId); + Assert.IsFalse(item.ParentId.HasValue); Assert.AreEqual("Testing12345", item.ItemKey); var allLangs = ServiceContext.LocalizationService.GetAllLanguages(); Assert.Greater(allLangs.Count(), 0); diff --git a/src/umbraco.cms/businesslogic/Dictionary.cs b/src/umbraco.cms/businesslogic/Dictionary.cs index 21d9bc516c..8e76122175 100644 --- a/src/umbraco.cms/businesslogic/Dictionary.cs +++ b/src/umbraco.cms/businesslogic/Dictionary.cs @@ -93,8 +93,8 @@ namespace umbraco.cms.businesslogic [Obsolete("This is no longer used and will be removed from the codebase in future versions")] public bool IsTopMostItem() - { - return _dictionaryItem.ParentId == new Guid(Constants.Conventions.Localization.DictionaryItemRootId); + { + return _dictionaryItem.ParentId.HasValue == false; } /// @@ -105,9 +105,9 @@ namespace umbraco.cms.businesslogic get { //EnsureCache(); - if (_parent == null) + if (_parent == null && _dictionaryItem.ParentId.HasValue) { - var p = ApplicationContext.Current.Services.LocalizationService.GetDictionaryItemById(_dictionaryItem.ParentId); + var p = ApplicationContext.Current.Services.LocalizationService.GetDictionaryItemById(_dictionaryItem.ParentId.Value); if (p == null) { From c917fc37ae8a3569a813d3588f0cb71bc05898ad Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 2 Jul 2015 17:20:27 +0200 Subject: [PATCH 23/50] Fixes: U4-6113 Cannot delete member with upload field --- .../Repositories/ContentRepository.cs | 5 +- .../Interfaces/IContentRepository.cs | 2 +- .../Interfaces/IDeleteMediaFilesRepository.cs | 14 +++++ .../Interfaces/IMediaRepository.cs | 2 +- .../Interfaces/IMemberRepository.cs | 2 +- .../Interfaces/IRecycleBinRepository.cs | 8 +-- .../Repositories/MediaRepository.cs | 9 ++-- .../Repositories/MemberRepository.cs | 36 ++----------- .../Repositories/RecycleBinRepository.cs | 50 ++--------------- .../Repositories/VersionableRepositoryBase.cs | 53 ++++++++++++++++++- .../Persistence/RepositoryFactory.cs | 9 ++-- src/Umbraco.Core/Services/ContentService.cs | 4 +- src/Umbraco.Core/Services/MediaService.cs | 4 +- src/Umbraco.Core/Services/MemberService.cs | 22 ++++---- src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../Repositories/ContentRepositoryTest.cs | 2 +- .../Repositories/ContentTypeRepositoryTest.cs | 2 +- .../Repositories/DomainRepositoryTest.cs | 2 +- .../Repositories/MediaRepositoryTest.cs | 3 +- .../Repositories/MemberRepositoryTest.cs | 3 +- .../PublicAccessRepositoryTest.cs | 2 +- .../Repositories/TagRepositoryTest.cs | 4 +- .../Repositories/TemplateRepositoryTest.cs | 2 +- .../Services/ContentServicePerformanceTest.cs | 8 +-- .../Services/ContentServiceTests.cs | 2 +- .../FileUploadPropertyEditor.cs | 2 + .../ImageCropperPropertyEditor.cs | 2 + 27 files changed, 129 insertions(+), 126 deletions(-) create mode 100644 src/Umbraco.Core/Persistence/Repositories/Interfaces/IDeleteMediaFilesRepository.cs diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 315db64e01..1e0d8884bd 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -20,6 +20,7 @@ using Umbraco.Core.Persistence.DatabaseModelDefinitions; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Cache; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; @@ -37,8 +38,8 @@ namespace Umbraco.Core.Persistence.Repositories private readonly ContentPreviewRepository _contentPreviewRepository; private readonly ContentXmlRepository _contentXmlRepository; - public ContentRepository(IDatabaseUnitOfWork work, CacheHelper cacheHelper, ILogger logger, ISqlSyntaxProvider syntaxProvider, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository) - : base(work, cacheHelper, logger, syntaxProvider) + public ContentRepository(IDatabaseUnitOfWork work, CacheHelper cacheHelper, ILogger logger, ISqlSyntaxProvider syntaxProvider, IContentTypeRepository contentTypeRepository, ITemplateRepository templateRepository, ITagRepository tagRepository, IContentSection contentSection) + : base(work, cacheHelper, logger, syntaxProvider, contentSection) { if (contentTypeRepository == null) throw new ArgumentNullException("contentTypeRepository"); if (templateRepository == null) throw new ArgumentNullException("templateRepository"); diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs index c27dd96d46..9d3fcbb40b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentRepository.cs @@ -9,7 +9,7 @@ using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories { - public interface IContentRepository : IRepositoryVersionable, IRecycleBinRepository + public interface IContentRepository : IRepositoryVersionable, IRecycleBinRepository, IDeleteMediaFilesRepository { /// /// Get the count of published items diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IDeleteMediaFilesRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IDeleteMediaFilesRepository.cs new file mode 100644 index 0000000000..005c1d62ba --- /dev/null +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IDeleteMediaFilesRepository.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Persistence.Repositories +{ + public interface IDeleteMediaFilesRepository + { + /// + /// Called to remove all files associated with entities when an entity is permanently deleted + /// + /// + /// + bool DeleteMediaFiles(IEnumerable files); + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs index 1149949e31..46bce6a03c 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaRepository.cs @@ -7,7 +7,7 @@ using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories { - public interface IMediaRepository : IRepositoryVersionable, IRecycleBinRepository + public interface IMediaRepository : IRepositoryVersionable, IRecycleBinRepository, IDeleteMediaFilesRepository { /// diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs index 2436ffbfb6..9cb74d1806 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberRepository.cs @@ -9,7 +9,7 @@ using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories { - public interface IMemberRepository : IRepositoryVersionable + public interface IMemberRepository : IRepositoryVersionable, IDeleteMediaFilesRepository { /// /// Finds members in a given role diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRecycleBinRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRecycleBinRepository.cs index e1a464af66..a6ed95711e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRecycleBinRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRecycleBinRepository.cs @@ -18,12 +18,6 @@ namespace Umbraco.Core.Persistence.Repositories /// /// bool EmptyRecycleBin(); - - /// - /// Called to remove all files associated with entities when recycle bin is emptied - /// - /// - /// - bool DeleteFiles(IEnumerable files); + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index 452c229417..d2bf19448b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Text; using System.Xml.Linq; using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Dynamics; using Umbraco.Core.IO; using Umbraco.Core.Logging; @@ -31,8 +32,8 @@ namespace Umbraco.Core.Persistence.Repositories private readonly ContentXmlRepository _contentXmlRepository; private readonly ContentPreviewRepository _contentPreviewRepository; - public MediaRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IMediaTypeRepository mediaTypeRepository, ITagRepository tagRepository) - : base(work, cache, logger, sqlSyntax) + public MediaRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IMediaTypeRepository mediaTypeRepository, ITagRepository tagRepository, IContentSection contentSection) + : base(work, cache, logger, sqlSyntax, contentSection) { if (mediaTypeRepository == null) throw new ArgumentNullException("mediaTypeRepository"); if (tagRepository == null) throw new ArgumentNullException("tagRepository"); @@ -40,10 +41,10 @@ namespace Umbraco.Core.Persistence.Repositories _tagRepository = tagRepository; _contentXmlRepository = new ContentXmlRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, sqlSyntax); _contentPreviewRepository = new ContentPreviewRepository(work, CacheHelper.CreateDisabledCacheHelper(), logger, sqlSyntax); - EnsureUniqueNaming = true; + EnsureUniqueNaming = contentSection.EnsureUniqueNaming; } - public bool EnsureUniqueNaming { get; set; } + public bool EnsureUniqueNaming { get; private set; } #region Overrides of RepositoryBase diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs index 8c6edbd27e..18d23d335d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs @@ -6,6 +6,7 @@ using System.Linq.Expressions; using System.Text; using System.Xml.Linq; using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models.EntityBase; @@ -33,8 +34,8 @@ namespace Umbraco.Core.Persistence.Repositories private readonly ContentXmlRepository _contentXmlRepository; private readonly ContentPreviewRepository _contentPreviewRepository; - public MemberRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, ITagRepository tagRepository) - : base(work, cache, logger, sqlSyntax) + public MemberRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IMemberTypeRepository memberTypeRepository, IMemberGroupRepository memberGroupRepository, ITagRepository tagRepository, IContentSection contentSection) + : base(work, cache, logger, sqlSyntax, contentSection) { if (memberTypeRepository == null) throw new ArgumentNullException("memberTypeRepository"); if (tagRepository == null) throw new ArgumentNullException("tagRepository"); @@ -373,36 +374,6 @@ namespace Umbraco.Core.Persistence.Repositories dirtyEntity.ResetDirtyProperties(); } - protected override void PersistDeletedItem(IMember entity) - { - var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); - var uploadFieldAlias = Constants.PropertyEditors.UploadFieldAlias; - //Loop through properties to check if the media item contains images/file that should be deleted - foreach (var property in ((Member)entity).Properties) - { - if (property.PropertyType.PropertyEditorAlias == uploadFieldAlias && - string.IsNullOrEmpty(property.Value.ToString()) == false - && fs.FileExists(fs.GetRelativePath(property.Value.ToString()))) - { - var relativeFilePath = fs.GetRelativePath(property.Value.ToString()); - var parentDirectory = System.IO.Path.GetDirectoryName(relativeFilePath); - - // don't want to delete the media folder if not using directories. - if (UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories && parentDirectory != fs.GetRelativePath("/")) - { - //issue U4-771: if there is a parent directory the recursive parameter should be true - fs.DeleteDirectory(parentDirectory, String.IsNullOrEmpty(parentDirectory) == false); - } - else - { - fs.DeleteFile(relativeFilePath, true); - } - } - } - - base.PersistDeletedItem(entity); - } - #endregion #region Overrides of VersionableRepositoryBase @@ -791,5 +762,6 @@ namespace Umbraco.Core.Persistence.Repositories _contentXmlRepository.Dispose(); _contentPreviewRepository.Dispose(); } + } } diff --git a/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs b/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs index 0599afce82..977c2a54cd 100644 --- a/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/RecycleBinRepository.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Models.EntityBase; @@ -17,8 +18,8 @@ namespace Umbraco.Core.Persistence.Repositories internal abstract class RecycleBinRepository : VersionableRepositoryBase, IRecycleBinRepository where TEntity : class, IUmbracoEntity { - protected RecycleBinRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) - : base(work, cache, logger, sqlSyntax) + protected RecycleBinRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IContentSection contentSection) + : base(work, cache, logger, sqlSyntax, contentSection) { } @@ -81,51 +82,6 @@ namespace Umbraco.Core.Persistence.Repositories } } - /// - /// Deletes all files passed in. - /// - /// - /// - public virtual bool DeleteFiles(IEnumerable files) - { - //ensure duplicates are removed - files = files.Distinct(); - - var allsuccess = true; - - var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); - Parallel.ForEach(files, file => - { - try - { - if (file.IsNullOrWhiteSpace()) return; - - var relativeFilePath = fs.GetRelativePath(file); - if (fs.FileExists(relativeFilePath) == false) return; - - var parentDirectory = System.IO.Path.GetDirectoryName(relativeFilePath); - - // don't want to delete the media folder if not using directories. - if (UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories && parentDirectory != fs.GetRelativePath("/")) - { - //issue U4-771: if there is a parent directory the recursive parameter should be true - fs.DeleteDirectory(parentDirectory, String.IsNullOrEmpty(parentDirectory) == false); - } - else - { - fs.DeleteFile(file, true); - } - } - catch (Exception e) - { - Logger.Error>("An error occurred while deleting file attached to nodes: " + file, e); - allsuccess = false; - } - }); - - return allsuccess; - } - private string FormatDeleteStatement(string tableName, string keyName) { //This query works with sql ce and sql server: diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index 3610645241..207cb1d04e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -3,6 +3,9 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Configuration; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Editors; @@ -17,16 +20,19 @@ using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; using Umbraco.Core.Dynamics; +using Umbraco.Core.IO; namespace Umbraco.Core.Persistence.Repositories { internal abstract class VersionableRepositoryBase : PetaPocoRepositoryBase where TEntity : class, IAggregateRoot { + private readonly IContentSection _contentSection; - protected VersionableRepositoryBase(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + protected VersionableRepositoryBase(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax, IContentSection contentSection) : base(work, cache, logger, sqlSyntax) { + _contentSection = contentSection; } #region IRepositoryVersionable Implementation @@ -535,5 +541,50 @@ WHERE EXISTS( return orderBy; } } + + /// + /// Deletes all media files passed in. + /// + /// + /// + public virtual bool DeleteMediaFiles(IEnumerable files) + { + //ensure duplicates are removed + files = files.Distinct(); + + var allsuccess = true; + + var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); + Parallel.ForEach(files, file => + { + try + { + if (file.IsNullOrWhiteSpace()) return; + + var relativeFilePath = fs.GetRelativePath(file); + if (fs.FileExists(relativeFilePath) == false) return; + + var parentDirectory = System.IO.Path.GetDirectoryName(relativeFilePath); + + // don't want to delete the media folder if not using directories. + if (_contentSection.UploadAllowDirectories && parentDirectory != fs.GetRelativePath("/")) + { + //issue U4-771: if there is a parent directory the recursive parameter should be true + fs.DeleteDirectory(parentDirectory, String.IsNullOrEmpty(parentDirectory) == false); + } + else + { + fs.DeleteFile(file, true); + } + } + catch (Exception e) + { + Logger.Error>("An error occurred while deleting file attached to nodes: " + file, e); + allsuccess = false; + } + }); + + return allsuccess; + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/RepositoryFactory.cs b/src/Umbraco.Core/Persistence/RepositoryFactory.cs index 787a06f005..19556b1f31 100644 --- a/src/Umbraco.Core/Persistence/RepositoryFactory.cs +++ b/src/Umbraco.Core/Persistence/RepositoryFactory.cs @@ -108,7 +108,8 @@ namespace Umbraco.Core.Persistence _sqlSyntax, CreateContentTypeRepository(uow), CreateTemplateRepository(uow), - CreateTagRepository(uow)) + CreateTagRepository(uow), + _settings.Content) { EnsureUniqueNaming = _settings.Content.EnsureUniqueNaming }; @@ -158,7 +159,8 @@ namespace Umbraco.Core.Persistence _cacheHelper, _logger, _sqlSyntax, CreateMediaTypeRepository(uow), - CreateTagRepository(uow)) { EnsureUniqueNaming = _settings.Content.EnsureUniqueNaming }; + CreateTagRepository(uow), + _settings.Content); } public virtual IMediaTypeRepository CreateMediaTypeRepository(IDatabaseUnitOfWork uow) @@ -266,7 +268,8 @@ namespace Umbraco.Core.Persistence _logger, _sqlSyntax, CreateMemberTypeRepository(uow), CreateMemberGroupRepository(uow), - CreateTagRepository(uow)); + CreateTagRepository(uow), + _settings.Content); } public virtual IMemberTypeRepository CreateMemberTypeRepository(IDatabaseUnitOfWork uow) diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 57da3bd8b3..1059f0e420 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -1111,7 +1111,7 @@ namespace Umbraco.Core.Services Deleted.RaiseEvent(args, this); //remove any flagged media files - repository.DeleteFiles(args.MediaFilesToDelete); + repository.DeleteMediaFiles(args.MediaFilesToDelete); } Audit(AuditType.Delete, "Delete Content performed by user", userId, content.Id); @@ -1310,7 +1310,7 @@ namespace Umbraco.Core.Services EmptiedRecycleBin.RaiseEvent(new RecycleBinEventArgs(nodeObjectType, entities, files, success), this); if (success) - repository.DeleteFiles(files); + repository.DeleteMediaFiles(files); } diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index 1c7355ef42..dcccf4d5d0 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -825,7 +825,7 @@ namespace Umbraco.Core.Services EmptiedRecycleBin.RaiseEvent(new RecycleBinEventArgs(nodeObjectType, entities, files, success), this); if (success) - repository.DeleteFiles(files); + repository.DeleteMediaFiles(files); } } Audit(AuditType.Delete, "Empty Media Recycle Bin performed by user", 0, -21); @@ -910,7 +910,7 @@ namespace Umbraco.Core.Services Deleted.RaiseEvent(args, this); //remove any flagged media files - repository.DeleteFiles(args.MediaFilesToDelete); + repository.DeleteMediaFiles(args.MediaFilesToDelete); } Audit(AuditType.Delete, "Delete Media performed by user", userId, media.Id); diff --git a/src/Umbraco.Core/Services/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs index 9b444de296..33f2ae5306 100644 --- a/src/Umbraco.Core/Services/MemberService.cs +++ b/src/Umbraco.Core/Services/MemberService.cs @@ -132,11 +132,11 @@ namespace Umbraco.Core.Services //go re-fetch the member and update the properties that may have changed var result = GetByUsername(member.Username); - + //should never be null but it could have been deleted by another thread. - if (result == null) + if (result == null) return; - + member.RawPasswordValue = result.RawPasswordValue; member.LastPasswordChangeDate = result.LastPasswordChangeDate; member.UpdateDate = member.UpdateDate; @@ -975,9 +975,13 @@ namespace Umbraco.Core.Services { repository.Delete(member); uow.Commit(); - } - Deleted.RaiseEvent(new DeleteEventArgs(member, false), this); + var args = new DeleteEventArgs(member, false); + Deleted.RaiseEvent(args, this); + + //remove any flagged media files + repository.DeleteMediaFiles(args.MediaFilesToDelete); + } } /// @@ -1169,7 +1173,7 @@ namespace Umbraco.Core.Services repository.DissociateRoles(usernames, roleNames); } } - + public void AssignRole(int memberId, string roleName) { AssignRoles(new[] { memberId }, new[] { roleName }); @@ -1198,7 +1202,7 @@ namespace Umbraco.Core.Services } } - + #endregion @@ -1233,7 +1237,7 @@ namespace Umbraco.Core.Services uow.Commit(); } } - + #region Event Handlers /// @@ -1250,7 +1254,7 @@ namespace Umbraco.Core.Services /// Occurs before Save /// public static event TypedEventHandler> Saving; - + /// /// Occurs after Create /// diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index b70653a7d3..23e8f1d612 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -420,6 +420,7 @@ + diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs index 8b7d87a0a9..089e9ebaf9 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentRepositoryTest.cs @@ -47,7 +47,7 @@ namespace Umbraco.Tests.Persistence.Repositories var templateRepository = new TemplateRepository(unitOfWork, CacheHelper, Logger, SqlSyntax, Mock.Of(), Mock.Of(), Mock.Of()); var tagRepository = new TagRepository(unitOfWork, CacheHelper, Logger, SqlSyntax); contentTypeRepository = new ContentTypeRepository(unitOfWork, CacheHelper, Logger, SqlSyntax, templateRepository); - var repository = new ContentRepository(unitOfWork, CacheHelper, Logger, SqlSyntax, contentTypeRepository, templateRepository, tagRepository); + var repository = new ContentRepository(unitOfWork, CacheHelper, Logger, SqlSyntax, contentTypeRepository, templateRepository, tagRepository, Mock.Of()); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs index 72aa195cb4..63ef153e32 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -42,7 +42,7 @@ namespace Umbraco.Tests.Persistence.Repositories var templateRepository = new TemplateRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax, Mock.Of(), Mock.Of(), Mock.Of()); var tagRepository = new TagRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax); contentTypeRepository = new ContentTypeRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax, templateRepository); - var repository = new ContentRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax, contentTypeRepository, templateRepository, tagRepository); + var repository = new ContentRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax, contentTypeRepository, templateRepository, tagRepository, Mock.Of()); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs index f394bafb1f..08fae6d3ef 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DomainRepositoryTest.cs @@ -25,7 +25,7 @@ namespace Umbraco.Tests.Persistence.Repositories var templateRepository = new TemplateRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Logger, SqlSyntax, Mock.Of(), Mock.Of(), Mock.Of()); var tagRepository = new TagRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Logger, SqlSyntax); contentTypeRepository = new ContentTypeRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Logger, SqlSyntax, templateRepository); - contentRepository = new ContentRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Logger, SqlSyntax, contentTypeRepository, templateRepository, tagRepository); + contentRepository = new ContentRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Logger, SqlSyntax, contentTypeRepository, templateRepository, tagRepository, Mock.Of()); languageRepository = new LanguageRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Logger, SqlSyntax); var domainRepository = new DomainRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Logger, SqlSyntax, contentRepository, languageRepository); return domainRepository; diff --git a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs index a26c9ead5d..60c49f89cd 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MediaRepositoryTest.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Xml.Linq; using Moq; using NUnit.Framework; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; @@ -36,7 +37,7 @@ namespace Umbraco.Tests.Persistence.Repositories { mediaTypeRepository = new MediaTypeRepository(unitOfWork, CacheHelper, Mock.Of(), SqlSyntax); var tagRepository = new TagRepository(unitOfWork, CacheHelper, Mock.Of(), SqlSyntax); - var repository = new MediaRepository(unitOfWork, CacheHelper, Mock.Of(), SqlSyntax, mediaTypeRepository, tagRepository); + var repository = new MediaRepository(unitOfWork, CacheHelper, Mock.Of(), SqlSyntax, mediaTypeRepository, tagRepository, Mock.Of()); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs index d03eeaa475..64e8587e00 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MemberRepositoryTest.cs @@ -4,6 +4,7 @@ using System.Xml.Linq; using Moq; using NUnit.Framework; using Umbraco.Core; +using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.Rdbms; @@ -38,7 +39,7 @@ namespace Umbraco.Tests.Persistence.Repositories memberTypeRepository = new MemberTypeRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax); memberGroupRepository = new MemberGroupRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax, CacheHelper.CreateDisabledCacheHelper()); var tagRepo = new TagRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax); - var repository = new MemberRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax, memberTypeRepository, memberGroupRepository, tagRepo); + var repository = new MemberRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax, memberTypeRepository, memberGroupRepository, tagRepo, Mock.Of()); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs index 77a252a129..4fa212f9bc 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/PublicAccessRepositoryTest.cs @@ -230,7 +230,7 @@ namespace Umbraco.Tests.Persistence.Repositories var templateRepository = new TemplateRepository(unitOfWork, CacheHelper, Logger, SqlSyntax, Mock.Of(), Mock.Of(), Mock.Of()); var tagRepository = new TagRepository(unitOfWork, CacheHelper, Logger, SqlSyntax); contentTypeRepository = new ContentTypeRepository(unitOfWork, CacheHelper, Logger, SqlSyntax, templateRepository); - var repository = new ContentRepository(unitOfWork, CacheHelper, Logger, SqlSyntax, contentTypeRepository, templateRepository, tagRepository); + var repository = new ContentRepository(unitOfWork, CacheHelper, Logger, SqlSyntax, contentTypeRepository, templateRepository, tagRepository, Mock.Of()); return repository; } diff --git a/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs index 93089b6e93..d8232d9aaf 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TagRepositoryTest.cs @@ -942,7 +942,7 @@ namespace Umbraco.Tests.Persistence.Repositories var templateRepository = new TemplateRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax, Mock.Of(), Mock.Of(), Mock.Of()); var tagRepository = new TagRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax); contentTypeRepository = new ContentTypeRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax, templateRepository); - var repository = new ContentRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax, contentTypeRepository, templateRepository, tagRepository); + var repository = new ContentRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax, contentTypeRepository, templateRepository, tagRepository, Mock.Of()); return repository; } @@ -950,7 +950,7 @@ namespace Umbraco.Tests.Persistence.Repositories { var tagRepository = new TagRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax); mediaTypeRepository = new MediaTypeRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax); - var repository = new MediaRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax, mediaTypeRepository, tagRepository); + var repository = new MediaRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax, mediaTypeRepository, tagRepository, Mock.Of()); return repository; } } diff --git a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs index 7630e0aaf7..fb76f4402b 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/TemplateRepositoryTest.cs @@ -406,7 +406,7 @@ namespace Umbraco.Tests.Persistence.Repositories { var tagRepository = new TagRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax); var contentTypeRepository = new ContentTypeRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax, templateRepository); - var contentRepo = new ContentRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax, contentTypeRepository, templateRepository, tagRepository); + var contentRepo = new ContentRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax, contentTypeRepository, templateRepository, tagRepository, Mock.Of()); using (contentRepo) { diff --git a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs index 4a26d917e1..316d957a0c 100644 --- a/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs +++ b/src/Umbraco.Tests/Services/ContentServicePerformanceTest.cs @@ -151,7 +151,7 @@ namespace Umbraco.Tests.Services using (var tRepository = new TemplateRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Logger, SqlSyntax, Mock.Of(), Mock.Of(), Mock.Of())) using (var tagRepo = new TagRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Logger, SqlSyntax)) using (var ctRepository = new ContentTypeRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Logger, SqlSyntax, tRepository)) - using (var repository = new ContentRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Logger, SqlSyntax, ctRepository, tRepository, tagRepo)) + using (var repository = new ContentRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Logger, SqlSyntax, ctRepository, tRepository, tagRepo, Mock.Of())) { // Act Stopwatch watch = Stopwatch.StartNew(); @@ -182,7 +182,7 @@ namespace Umbraco.Tests.Services using (var tRepository = new TemplateRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Logger, SqlSyntax, Mock.Of(), Mock.Of(), Mock.Of())) using (var tagRepo = new TagRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Logger, SqlSyntax)) using (var ctRepository = new ContentTypeRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Logger, SqlSyntax, tRepository)) - using (var repository = new ContentRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Logger, SqlSyntax, ctRepository, tRepository, tagRepo)) + using (var repository = new ContentRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Logger, SqlSyntax, ctRepository, tRepository, tagRepo, Mock.Of())) { // Act Stopwatch watch = Stopwatch.StartNew(); @@ -212,7 +212,7 @@ namespace Umbraco.Tests.Services using (var tRepository = new TemplateRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Logger, SqlSyntax, Mock.Of(), Mock.Of(), Mock.Of())) using (var tagRepo = new TagRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Logger, SqlSyntax)) using (var ctRepository = new ContentTypeRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Logger, SqlSyntax, tRepository)) - using (var repository = new ContentRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Logger, SqlSyntax, ctRepository, tRepository, tagRepo)) + using (var repository = new ContentRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Logger, SqlSyntax, ctRepository, tRepository, tagRepo, Mock.Of())) { // Act @@ -245,7 +245,7 @@ namespace Umbraco.Tests.Services using (var tRepository = new TemplateRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Logger, SqlSyntax, Mock.Of(), Mock.Of(), Mock.Of())) using (var tagRepo = new TagRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Logger, SqlSyntax)) using (var ctRepository = new ContentTypeRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Logger, SqlSyntax, tRepository)) - using (var repository = new ContentRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Logger, SqlSyntax, ctRepository, tRepository, tagRepo)) + using (var repository = new ContentRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Logger, SqlSyntax, ctRepository, tRepository, tagRepo, Mock.Of())) { // Act diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index f2f858ec26..176db015f8 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -1550,7 +1550,7 @@ namespace Umbraco.Tests.Services var templateRepository = new TemplateRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax, Mock.Of(), Mock.Of(), Mock.Of()); var tagRepository = new TagRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax); contentTypeRepository = new ContentTypeRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax, templateRepository); - var repository = new ContentRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax, contentTypeRepository, templateRepository, tagRepository); + var repository = new ContentRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax, contentTypeRepository, templateRepository, tagRepository, Mock.Of()); return repository; } } diff --git a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs index 9eff763dda..89af8b8f66 100644 --- a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs @@ -44,6 +44,8 @@ namespace Umbraco.Web.PropertyEditors args.MediaFilesToDelete.AddRange(ServiceDeleted(args.DeletedEntities.Cast())); ContentService.EmptiedRecycleBin += (sender, args) => args.Files.AddRange(ServiceEmptiedRecycleBin(args.AllPropertyData)); + MemberService.Deleted += (sender, args) => + args.MediaFilesToDelete.AddRange(ServiceDeleted(args.DeletedEntities.Cast())); } /// diff --git a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs index ea1fd8dfe9..4acb783da6 100644 --- a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs @@ -40,6 +40,8 @@ namespace Umbraco.Web.PropertyEditors args.MediaFilesToDelete.AddRange(ServiceDeleted(args.DeletedEntities.Cast())); ContentService.EmptiedRecycleBin += (sender, args) => args.Files.AddRange(ServiceEmptiedRecycleBin(args.AllPropertyData)); + MemberService.Deleted += (sender, args) => + args.MediaFilesToDelete.AddRange(ServiceDeleted(args.DeletedEntities.Cast())); } /// From 85158abb5f0e21d4f00c290533cd23a9dac9d6a4 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 2 Jul 2015 18:27:55 +0200 Subject: [PATCH 24/50] removes unit test that no longer makes any sense. --- .../Repositories/DictionaryRepositoryTest.cs | 31 ------------------- 1 file changed, 31 deletions(-) diff --git a/src/Umbraco.Tests/Persistence/Repositories/DictionaryRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/DictionaryRepositoryTest.cs index 25fcc5e13a..d21da8c6e2 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/DictionaryRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/DictionaryRepositoryTest.cs @@ -161,37 +161,6 @@ namespace Umbraco.Tests.Persistence.Repositories } - [Test] - public void Get_Ignores_Item_WhenLanguageMissing() - { - // Arrange - var language = ServiceContext.LocalizationService.GetLanguageByCultureCode("en-US"); - var itemMissingLanguage = new DictionaryItem("I have invalid language"); - var translations = new List - { - new DictionaryTranslation(new Language("") { Id = 0 }, ""), - new DictionaryTranslation(language, "I have language") - }; - itemMissingLanguage.Translations = translations; - ServiceContext.LocalizationService.Save(itemMissingLanguage);//Id 3? - - var provider = new PetaPocoUnitOfWorkProvider(Logger); - var unitOfWork = provider.GetUnitOfWork(); - LanguageRepository languageRepository; - using (var repository = CreateRepository(unitOfWork, out languageRepository)) - { - // Act - var dictionaryItem = repository.Get(3); - - // Assert - Assert.That(dictionaryItem, Is.Not.Null); - Assert.That(dictionaryItem.ItemKey, Is.EqualTo("I have invalid language")); - Assert.That(dictionaryItem.Translations.Any(), Is.True); - Assert.That(dictionaryItem.Translations.Any(x => x == null), Is.False); - Assert.That(dictionaryItem.Translations.First().Value, Is.EqualTo("I have language")); - Assert.That(dictionaryItem.Translations.Count(), Is.EqualTo(1)); - } - } [Test] public void Can_Perform_GetAll_On_DictionaryRepository() From d63f83f7c723be56c264c6e712620e3484841807 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 3 Jul 2015 11:40:41 +0200 Subject: [PATCH 25/50] Fixes: U4-6786 Update to CDF 1.8.4 --- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 12 ++++++------ src/Umbraco.Web.UI/config/ClientDependency.config | 2 +- src/Umbraco.Web.UI/packages.config | 2 +- src/Umbraco.Web/Umbraco.Web.csproj | 4 ++-- src/Umbraco.Web/packages.config | 2 +- src/umbraco.cms/packages.config | 2 +- src/umbraco.cms/umbraco.cms.csproj | 4 ++-- src/umbraco.controls/packages.config | 2 +- src/umbraco.controls/umbraco.controls.csproj | 4 ++-- src/umbraco.editorControls/packages.config | 2 +- .../umbraco.editorControls.csproj | 4 ++-- 11 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 454250855b..ed269cddde 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -114,9 +114,9 @@ False ..\packages\AutoMapper.3.3.1\lib\net40\AutoMapper.Net4.dll - + False - ..\packages\ClientDependency.1.8.3.1\lib\net45\ClientDependency.Core.dll + ..\packages\ClientDependency.1.8.4\lib\net45\ClientDependency.Core.dll False @@ -158,7 +158,7 @@ False ..\packages\Microsoft.AspNet.Identity.Owin.2.2.1\lib\net45\Microsoft.AspNet.Identity.Owin.dll - + ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll True @@ -174,7 +174,7 @@ False ..\packages\Microsoft.Owin.Security.Cookies.3.0.1\lib\net45\Microsoft.Owin.Security.Cookies.dll - + False ..\packages\Microsoft.Owin.Security.OAuth.3.0.1\lib\net45\Microsoft.Owin.Security.OAuth.dll @@ -224,7 +224,7 @@ - + False @@ -343,7 +343,7 @@ Properties\SolutionInfo.cs - + loadStarterKits.ascx ASPXCodeBehind diff --git a/src/Umbraco.Web.UI/config/ClientDependency.config b/src/Umbraco.Web.UI/config/ClientDependency.config index 7269594985..456473e07d 100644 --- a/src/Umbraco.Web.UI/config/ClientDependency.config +++ b/src/Umbraco.Web.UI/config/ClientDependency.config @@ -10,7 +10,7 @@ NOTES: * Compression/Combination/Minification is not enabled unless debug="false" is specified on the 'compiliation' element in the web.config * A new version will invalidate both client and server cache and create new persisted files --> - + - From bf19d61d141548cff74bc23c94287b17724bee7b Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 6 Jul 2015 18:02:25 +0200 Subject: [PATCH 33/50] bumps txt version --- build/UmbracoVersion.txt | 2 +- src/SolutionInfo.cs | 2 +- src/Umbraco.Core/Configuration/UmbracoVersion.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build/UmbracoVersion.txt b/build/UmbracoVersion.txt index 6ce8416610..f767faaa60 100644 --- a/build/UmbracoVersion.txt +++ b/build/UmbracoVersion.txt @@ -1,3 +1,3 @@ # Usage: on line 2 put the release version, on line 3 put the version comment (example: beta) 7.3.0 -beta \ No newline at end of file +beta2 \ No newline at end of file diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 29ba870d90..e4258e943d 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -12,4 +12,4 @@ using System.Resources; [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyFileVersion("7.3.0")] -[assembly: AssemblyInformationalVersion("7.3.0-beta")] \ No newline at end of file +[assembly: AssemblyInformationalVersion("7.3.0-beta2")] \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index d96b9f146b..49bea68719 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -24,7 +24,7 @@ namespace Umbraco.Core.Configuration /// Gets the version comment (like beta or RC). /// /// The version comment. - public static string CurrentComment { get { return "beta.2"; } } + public static string CurrentComment { get { return "beta2"; } } // Get the version of the umbraco.dll by looking at a class in that dll // Had to do it like this due to medium trust issues, see: http://haacked.com/archive/2010/11/04/assembly-location-and-medium-trust.aspx From 7c451411cb8ce064e084969771e697ad2cec6076 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 7 Jul 2015 11:24:28 +0200 Subject: [PATCH 34/50] Fixes issue with upgrading - potential file lock issues and also file check for access.config --- .../MovePublicAccessXmlDataToDb.cs | 3 +++ .../Install/FilePermissionHelper.cs | 2 +- .../umbraco.presentation/content.cs | 25 +++++++++++-------- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MovePublicAccessXmlDataToDb.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MovePublicAccessXmlDataToDb.cs index 7dd5f81b3a..d60385926b 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MovePublicAccessXmlDataToDb.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenThreeZero/MovePublicAccessXmlDataToDb.cs @@ -30,6 +30,9 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenThreeZe } var xmlFile = IOHelper.MapPath(SystemFiles.AccessXml); + + if (File.Exists(xmlFile) == false) return; + using (var fileStream = File.OpenRead(xmlFile)) { var xml = XDocument.Load(fileStream); diff --git a/src/Umbraco.Web/Install/FilePermissionHelper.cs b/src/Umbraco.Web/Install/FilePermissionHelper.cs index 013f40fc0e..b5bc908fde 100644 --- a/src/Umbraco.Web/Install/FilePermissionHelper.cs +++ b/src/Umbraco.Web/Install/FilePermissionHelper.cs @@ -98,7 +98,7 @@ namespace Umbraco.Web.Install // that and we might get lock issues. try { - var xmlFile = content.Instance.UmbracoXmlDiskCacheFileName + ".tmp"; + var xmlFile = content.GetUmbracoXmlDiskFileName() + ".tmp"; SaveAndDeleteFile(xmlFile); return true; } diff --git a/src/Umbraco.Web/umbraco.presentation/content.cs b/src/Umbraco.Web/umbraco.presentation/content.cs index 7b9b726223..ecb21ba5b7 100644 --- a/src/Umbraco.Web/umbraco.presentation/content.cs +++ b/src/Umbraco.Web/umbraco.presentation/content.cs @@ -132,23 +132,26 @@ namespace umbraco private static readonly object DbReadSyncLock = new object(); private const string XmlContextContentItemKey = "UmbracoXmlContextContent"; - private string _umbracoXmlDiskCacheFileName = string.Empty; + private static string _umbracoXmlDiskCacheFileName = string.Empty; private volatile XmlDocument _xmlContent; /// /// Gets the path of the umbraco XML disk cache file. /// /// The name of the umbraco XML disk cache file. + public static string GetUmbracoXmlDiskFileName() + { + if (string.IsNullOrEmpty(_umbracoXmlDiskCacheFileName)) + { + _umbracoXmlDiskCacheFileName = IOHelper.MapPath(SystemFiles.ContentCacheXml); + } + return _umbracoXmlDiskCacheFileName; + } + + [Obsolete("Use the safer static GetUmbracoXmlDiskFileName() method instead to retrieve this value")] public string UmbracoXmlDiskCacheFileName { - get - { - if (string.IsNullOrEmpty(_umbracoXmlDiskCacheFileName)) - { - _umbracoXmlDiskCacheFileName = IOHelper.MapPath(SystemFiles.ContentCacheXml); - } - return _umbracoXmlDiskCacheFileName; - } + get { return GetUmbracoXmlDiskFileName(); } set { _umbracoXmlDiskCacheFileName = value; } } @@ -674,9 +677,9 @@ order by umbracoNode.level, umbracoNode.sortOrder"; { //TODO: Should there be a try/catch here in case the file is being written to while this is trying to be executed? - if (File.Exists(UmbracoXmlDiskCacheFileName)) + if (File.Exists(GetUmbracoXmlDiskFileName())) { - return new FileInfo(UmbracoXmlDiskCacheFileName).LastWriteTimeUtc; + return new FileInfo(GetUmbracoXmlDiskFileName()).LastWriteTimeUtc; } return DateTime.MinValue; From cf90e9a6b8258628965d19d5d9ecf74798108ae8 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 7 Jul 2015 12:05:51 +0200 Subject: [PATCH 35/50] Fixes: U4-6796 BackOfficeIdentity gets assigned inadvertently for front-end requests if using ui.Text to localize --- .../BasePages/BasePage.cs | 28 +++++++++++++++++-- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/src/umbraco.businesslogic/BasePages/BasePage.cs b/src/umbraco.businesslogic/BasePages/BasePage.cs index 51f059dbb5..8e7f1f47e1 100644 --- a/src/umbraco.businesslogic/BasePages/BasePage.cs +++ b/src/umbraco.businesslogic/BasePages/BasePage.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Web.Mvc; using System.Web.Routing; using System.Web.Security; +using System.Web.UI; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.IO; @@ -181,7 +182,14 @@ namespace umbraco.BasePages /// public static int GetUserId() { - var identity = HttpContext.Current.GetCurrentIdentity(true); + var identity = HttpContext.Current.GetCurrentIdentity( + //DO NOT AUTO-AUTH UNLESS THE CURRENT HANDLER IS WEBFORMS! + // Without this check, anything that is using this legacy API, like ui.Text will + // automatically log the back office user in even if it is a front-end request (if there is + // a back office user logged in. This can cause problems becaues the identity is changing mid + // request. For example: http://issues.umbraco.org/issue/U4-4010 + HttpContext.Current.CurrentHandler is Page); + if (identity == null) return -1; return Convert.ToInt32(identity.Id); @@ -205,7 +213,14 @@ namespace umbraco.BasePages /// public static bool ValidateCurrentUser() { - var identity = HttpContext.Current.GetCurrentIdentity(true); + var identity = HttpContext.Current.GetCurrentIdentity( + //DO NOT AUTO-AUTH UNLESS THE CURRENT HANDLER IS WEBFORMS! + // Without this check, anything that is using this legacy API, like ui.Text will + // automatically log the back office user in even if it is a front-end request (if there is + // a back office user logged in. This can cause problems becaues the identity is changing mid + // request. For example: http://issues.umbraco.org/issue/U4-4010 + HttpContext.Current.CurrentHandler is Page); + if (identity != null) { return true; @@ -232,7 +247,14 @@ namespace umbraco.BasePages { get { - var identity = HttpContext.Current.GetCurrentIdentity(true); + var identity = HttpContext.Current.GetCurrentIdentity( + //DO NOT AUTO-AUTH UNLESS THE CURRENT HANDLER IS WEBFORMS! + // Without this check, anything that is using this legacy API, like ui.Text will + // automatically log the back office user in even if it is a front-end request (if there is + // a back office user logged in. This can cause problems becaues the identity is changing mid + // request. For example: http://issues.umbraco.org/issue/U4-4010 + HttpContext.Current.CurrentHandler is Page); + return identity == null ? "" : identity.SessionId; } set From db030ffa311168c0264d92c266a6ecda965331ff Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Mon, 6 Jul 2015 11:59:03 +0200 Subject: [PATCH 36/50] Adds the ability to pass in a nightly version to the version comment --- build/Build.proj | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/build/Build.proj b/build/Build.proj index bdaa5b9c0c..95c2543dd0 100644 --- a/build/Build.proj +++ b/build/Build.proj @@ -68,6 +68,12 @@ .$(BUILD_RELEASE)-$(BUILD_COMMENT) + + .$(BUILD_RELEASE)+$(BUILD_NIGHTLY) + + + .$(BUILD_RELEASE)-$(BUILD_COMMENT)+$(BUILD_NIGHTLY) + Release @@ -271,9 +277,11 @@ $(BUILD_RELEASE) $(BUILD_RELEASE)-$(BUILD_COMMENT) + $(BUILD_RELEASE)+$(BUILD_NIGHTLY) + $(BUILD_RELEASE)-$(BUILD_COMMENT)+$(BUILD_NIGHTLY) - + + + + + + + + From b0ccb040a16a4065cf39c1c1b4fb878f8850f8ce Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 7 Jul 2015 10:12:34 +0200 Subject: [PATCH 37/50] Makes sure we can have nightlies for non-pre-releases as well --- .gitignore | 1 + src/Umbraco.Core/ApplicationContext.cs | 4 ++-- src/Umbraco.Core/Configuration/GlobalSettings.cs | 4 ++-- src/Umbraco.Core/CoreBootManager.cs | 2 +- src/Umbraco.Core/SemVersionExtensions.cs | 12 ++++++++++++ src/Umbraco.Core/Umbraco.Core.csproj | 1 + src/Umbraco.Tests/ApplicationContextTests.cs | 4 ++-- src/Umbraco.Tests/Routing/UmbracoModuleTests.cs | 2 +- .../TestHelpers/BaseDatabaseFactoryTest.cs | 2 +- src/Umbraco.Web/Editors/BackOfficeController.cs | 2 +- .../Install/InstallSteps/SetUmbracoVersionStep.cs | 2 +- .../umbraco/dialogs/about.aspx.cs | 3 ++- 12 files changed, 27 insertions(+), 12 deletions(-) create mode 100644 src/Umbraco.Core/SemVersionExtensions.cs diff --git a/.gitignore b/.gitignore index 0073675d82..ebee9f6ae9 100644 --- a/.gitignore +++ b/.gitignore @@ -129,3 +129,4 @@ src/*.boltdata/ /src/Umbraco.Web.UI/Umbraco/Js/canvasdesigner.front.js src/umbraco.sln.ide/* build/UmbracoCms.*/ +src/.vs/ diff --git a/src/Umbraco.Core/ApplicationContext.cs b/src/Umbraco.Core/ApplicationContext.cs index 7b9a6a6b2b..b48d4eb5e9 100644 --- a/src/Umbraco.Core/ApplicationContext.cs +++ b/src/Umbraco.Core/ApplicationContext.cs @@ -286,14 +286,14 @@ namespace Umbraco.Core { //we haven't executed this migration in this environment, so even though the config versions match, // this db has not been updated. - ProfilingLogger.Logger.Debug("The migration for version: '" + currentVersion + " has not been executed, there is no record in the database"); + ProfilingLogger.Logger.Debug(string.Format("The migration for version: '{0} has not been executed, there is no record in the database", currentVersion.ToSemanticString())); ok = false; } } } else { - ProfilingLogger.Logger.Debug("CurrentVersion different from configStatus: '" + currentVersion + "','" + configStatus + "'"); + ProfilingLogger.Logger.Debug(string.Format("CurrentVersion different from configStatus: '{0}','{1}'", currentVersion.ToSemanticString(), configStatus)); } return ok; diff --git a/src/Umbraco.Core/Configuration/GlobalSettings.cs b/src/Umbraco.Core/Configuration/GlobalSettings.cs index 329fa598c1..2c3855727b 100644 --- a/src/Umbraco.Core/Configuration/GlobalSettings.cs +++ b/src/Umbraco.Core/Configuration/GlobalSettings.cs @@ -429,7 +429,7 @@ namespace Umbraco.Core.Configuration try { string configStatus = ConfigurationStatus; - string currentVersion = UmbracoVersion.GetSemanticVersion().ToString(); + string currentVersion = UmbracoVersion.GetSemanticVersion().ToSemanticString(); if (currentVersion != configStatus) @@ -596,7 +596,7 @@ namespace Umbraco.Core.Configuration [Obsolete("Use Umbraco.Core.Configuration.UmbracoVersion.Current instead", false)] public static string CurrentVersion { - get { return UmbracoVersion.GetSemanticVersion().ToString(); } + get { return UmbracoVersion.GetSemanticVersion().ToSemanticString(); } } /// diff --git a/src/Umbraco.Core/CoreBootManager.cs b/src/Umbraco.Core/CoreBootManager.cs index b188bf2232..998bda32f2 100644 --- a/src/Umbraco.Core/CoreBootManager.cs +++ b/src/Umbraco.Core/CoreBootManager.cs @@ -77,7 +77,7 @@ namespace Umbraco.Core _profilingLogger = new ProfilingLogger(LoggerResolver.Current.Logger, ProfilerResolver.Current.Profiler); _timer = _profilingLogger.TraceDuration( - "Umbraco application (" + UmbracoVersion.GetSemanticVersion() + ") starting", + string.Format("Umbraco application ({0}) starting", UmbracoVersion.GetSemanticVersion().ToSemanticString()), "Umbraco application startup complete"); CreateApplicationCache(); diff --git a/src/Umbraco.Core/SemVersionExtensions.cs b/src/Umbraco.Core/SemVersionExtensions.cs new file mode 100644 index 0000000000..f45aacdfc2 --- /dev/null +++ b/src/Umbraco.Core/SemVersionExtensions.cs @@ -0,0 +1,12 @@ +using Semver; + +namespace Umbraco.Core +{ + public static class SemVersionExtensions + { + public static string ToSemanticString(this SemVersion semVersion) + { + return semVersion.ToString().Replace("--", "-").Replace("-+", "+"); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 4058970401..e73ff5bd15 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -435,6 +435,7 @@ + diff --git a/src/Umbraco.Tests/ApplicationContextTests.cs b/src/Umbraco.Tests/ApplicationContextTests.cs index 60eecce81d..61dabf6e3b 100644 --- a/src/Umbraco.Tests/ApplicationContextTests.cs +++ b/src/Umbraco.Tests/ApplicationContextTests.cs @@ -20,7 +20,7 @@ namespace Umbraco.Tests [Test] public void Is_Configured() { - ConfigurationManager.AppSettings.Set("umbracoConfigurationStatus", UmbracoVersion.GetSemanticVersion().ToString()); + ConfigurationManager.AppSettings.Set("umbracoConfigurationStatus", UmbracoVersion.GetSemanticVersion().ToSemanticString()); var migrationEntryService = new Mock(); migrationEntryService.Setup(x => x.FindEntry(It.IsAny(), It.IsAny())) @@ -42,7 +42,7 @@ namespace Umbraco.Tests [Test] public void Is_Not_Configured_By_Migration_Not_Found() { - ConfigurationManager.AppSettings.Set("umbracoConfigurationStatus", UmbracoVersion.GetSemanticVersion().ToString()); + ConfigurationManager.AppSettings.Set("umbracoConfigurationStatus", UmbracoVersion.GetSemanticVersion().ToSemanticString()); var migrationEntryService = new Mock(); migrationEntryService.Setup(x => x.FindEntry(It.IsAny(), It.IsAny())) diff --git a/src/Umbraco.Tests/Routing/UmbracoModuleTests.cs b/src/Umbraco.Tests/Routing/UmbracoModuleTests.cs index 2eb59aba64..3c3686aded 100644 --- a/src/Umbraco.Tests/Routing/UmbracoModuleTests.cs +++ b/src/Umbraco.Tests/Routing/UmbracoModuleTests.cs @@ -28,7 +28,7 @@ namespace Umbraco.Tests.Routing //create the module _module = new UmbracoModule(); - SettingsForTests.ConfigurationStatus = UmbracoVersion.GetSemanticVersion().ToString(); + SettingsForTests.ConfigurationStatus = UmbracoVersion.GetSemanticVersion().ToSemanticString(); //SettingsForTests.ReservedPaths = "~/umbraco,~/install/"; //SettingsForTests.ReservedUrls = "~/config/splashes/booting.aspx,~/install/default.aspx,~/config/splashes/noNodes.aspx,~/VSEnterpriseHelper.axd"; diff --git a/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs b/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs index 4c64c5eaf2..621837ee36 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs @@ -97,7 +97,7 @@ namespace Umbraco.Tests.TestHelpers InitializeDatabase(); //ensure the configuration matches the current version for tests - SettingsForTests.ConfigurationStatus = UmbracoVersion.GetSemanticVersion().ToString(); + SettingsForTests.ConfigurationStatus = UmbracoVersion.GetSemanticVersion().ToSemanticString(); } } diff --git a/src/Umbraco.Web/Editors/BackOfficeController.cs b/src/Umbraco.Web/Editors/BackOfficeController.cs index 46dc4df910..0be884f5b4 100644 --- a/src/Umbraco.Web/Editors/BackOfficeController.cs +++ b/src/Umbraco.Web/Editors/BackOfficeController.cs @@ -617,7 +617,7 @@ namespace Umbraco.Web.Editors {"assemblyVersion", UmbracoVersion.AssemblyVersion} }; - var version = UmbracoVersion.GetSemanticVersion().ToString(); + var version = UmbracoVersion.GetSemanticVersion().ToSemanticString(); app.Add("version", version); app.Add("cdf", ClientDependency.Core.Config.ClientDependencySettings.Instance.Version); diff --git a/src/Umbraco.Web/Install/InstallSteps/SetUmbracoVersionStep.cs b/src/Umbraco.Web/Install/InstallSteps/SetUmbracoVersionStep.cs index d52579772c..b8be5b7a22 100644 --- a/src/Umbraco.Web/Install/InstallSteps/SetUmbracoVersionStep.cs +++ b/src/Umbraco.Web/Install/InstallSteps/SetUmbracoVersionStep.cs @@ -34,7 +34,7 @@ namespace Umbraco.Web.Install.InstallSteps DistributedCache.Instance.RefreshAllPageCache(); // Update configurationStatus - GlobalSettings.ConfigurationStatus = UmbracoVersion.GetSemanticVersion().ToString(); + GlobalSettings.ConfigurationStatus = UmbracoVersion.GetSemanticVersion().ToSemanticString(); // Update ClientDependency version var clientDependencyConfig = new ClientDependencyConfiguration(_applicationContext.ProfilingLogger.Logger); diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/about.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/about.aspx.cs index 37e1f6eca9..a30cb1eb7b 100644 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/about.aspx.cs +++ b/src/Umbraco.Web/umbraco.presentation/umbraco/dialogs/about.aspx.cs @@ -1,5 +1,6 @@ using System; using System.Globalization; +using Umbraco.Core; using Umbraco.Core.Configuration; namespace umbraco.dialogs @@ -14,7 +15,7 @@ namespace umbraco.dialogs { // Put user code to initialize the page here thisYear.Text = DateTime.Now.Year.ToString(CultureInfo.InvariantCulture); - version.Text = UmbracoVersion.GetSemanticVersion().ToString(); + version.Text = UmbracoVersion.GetSemanticVersion().ToSemanticString(); } #region Web Form Designer generated code From b903e0503f5c253a5c445deb787b532b9723d498 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 7 Jul 2015 15:11:40 +0200 Subject: [PATCH 38/50] "+" in the version doesn't work with the current NuGet --- build/Build.proj | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/build/Build.proj b/build/Build.proj index 95c2543dd0..b0ea447de6 100644 --- a/build/Build.proj +++ b/build/Build.proj @@ -72,7 +72,7 @@ .$(BUILD_RELEASE)+$(BUILD_NIGHTLY) - .$(BUILD_RELEASE)-$(BUILD_COMMENT)+$(BUILD_NIGHTLY) + .$(BUILD_RELEASE)-$(BUILD_COMMENT)-$(BUILD_NIGHTLY) @@ -277,8 +277,8 @@ $(BUILD_RELEASE) $(BUILD_RELEASE)-$(BUILD_COMMENT) - $(BUILD_RELEASE)+$(BUILD_NIGHTLY) - $(BUILD_RELEASE)-$(BUILD_COMMENT)+$(BUILD_NIGHTLY) + $(BUILD_RELEASE)-$(BUILD_NIGHTLY) + $(BUILD_RELEASE)-$(BUILD_COMMENT)-$(BUILD_NIGHTLY) @@ -294,12 +294,12 @@ + ReplacementText="CurrentComment { get { return "$(BUILD_NIGHTLY)""/> + ReplacementText="CurrentComment { get { return "$(BUILD_COMMENT)-$(BUILD_NIGHTLY)""/> + ReplacementText="AssemblyInformationalVersion("$(BUILD_RELEASE)-$(BUILD_NIGHTLY)")"/> + ReplacementText="AssemblyInformationalVersion("$(BUILD_RELEASE)-$(BUILD_COMMENT)-$(BUILD_NIGHTLY)")"/> Date: Tue, 7 Jul 2015 19:17:56 +0600 Subject: [PATCH 39/50] All 7.2 and actual at the moment 7.3 keys --- src/Umbraco.Web.UI/umbraco/config/lang/ru.xml | 85 ++++++++++++++----- 1 file changed, 66 insertions(+), 19 deletions(-) diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml b/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml index 41dd15fbf2..df1273ae99 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/ru.xml @@ -27,6 +27,7 @@ Опубликовать Обновить узлы Опубликовать весь сайт + Восстановить Разрешения Откатить Направить на публикацию @@ -81,6 +82,7 @@ Вставить макрос Вставить изображение Править связи + Вернуться к списку Сохранить Сохранить и опубликовать Сохранить и направить на публикацию @@ -99,7 +101,8 @@ Узел переопубликован. Текущее свойство Текущий тип - Тип документа не может быть изменен, так как для данного расположения нет разрешенных альтернатив. + Тип документа не может быть изменен, так как для данного расположения нет разрешенных альтернатив. + Альтернативный тип станет доступным, если его разрешить как тип, пригодный для создания дочерних узлов внутри родительского узла данного документа. Тип документа изменен Сопоставление свойств Сопоставлено свойству @@ -121,7 +124,8 @@ Алиас (как бы Вы описали изображение по телефону) Альтернативные ссылки - Дочерние узлы + Альтернативный текст (необязательно) + Элементы списка Нажмите для правки этого элемента Создано пользователем Исходный автор @@ -130,9 +134,11 @@ Тип документа Редактирование Скрыть + Опубликовано Этот документ был изменен после публикации Этот документ не опубликован Документ опубликован + В этом списке пока нет элементов. Ссылка на медиа-элементы Тип медиа-контента Группа участников @@ -165,9 +171,9 @@ Где вы хотите создать новый %0% - Создать в - "document types".]]> - "media types".]]> + Создать в узле + "Типы документов".]]> + "Типы медиа-материалов".]]> Выберите тип и заголовок @@ -234,12 +240,20 @@ Название языка (культуры) + Допустим как корневой + Только узлы таких типов (с установленным флагом) могут быть созданы в корневом уровне дерева содержимого или медиа-библиотеки Допустимые типы дочерних узлов + Составные типы документов Создать + Создать пользовательский список + Текущий список + Текущий тип данных в виде списка Удалить вкладку Описание Включить представление в виде списка + Включает представление узлов, дочерних к узлу данного типа, в виде сортируемого списка с функцией фильтра и поиска. Такие дочерние узлы при этом перестают быть видимыми в дереве. Новая вкладка + Удалить пользовательский список Вкладка Миниатюра @@ -266,7 +280,7 @@ %0% должно быть целочисленным значением %0% в %1% является обязательным полем %0% является обязательным полем - %0% в %1%: данные введены в некорректном формате + %0% в %1%: данные в некорректном формате %0% - данные в некорректном формате @@ -394,9 +408,38 @@ Шрифт Текст + + снизу и добавьте Ваш первый элемент]]> + Добавить шаблон сетки + Настройте шаблон, задавая ширину колонок или добавляя дополнительные секции + Добавить конфигурацию строки + Настройте строку, задавая ширину ячеек или добавляя дополнительные ячейки + Добавить новые строки + Доступны все редакторы + Доступны все конфигурации строк + Кликните для встраивания + Кликните для вставки изображения + Колонки + Суммарное число колонок в шаблоне сетки + Шаблоны сетки + Шаблоны являются рабочим пространством для редактора сетки, обычно Вам понадобится не более одного или двух шаблонов + Вставить элемент + Заголовок для изображения... + Напишите... + Конфигурации строк + Строки - это последовательности ячеек с горизонтальным расположением + Установки будут сохранены только если они указаны в корректном формате json + Установки + Задайте установки, доступные редакторам для изменения + Стили + Задайте стили, доступные редакторам для изменения + Страница + + Сбросить + Программа установки не может установить подключение к базе данных. Невозможно сохранить изменения в файл web.config. Пожалуйста, вручную измените настройки строки подключения к базе данных. @@ -533,7 +576,7 @@ Рыбный день Слава Богу, сегодня пятница Понедельник начинается в субботу - Введите имя пользователя и пароль + Укажите имя пользователя и пароль Время сессии истекло @@ -643,11 +686,12 @@ Вставить с очисткой форматирования (рекомендуется) - Введите имя... - Введите фильтр... + Укажите имя... + Укажите теги (нажимайте Enter после каждого тега)... + Укажите фильтр... Назовите %0%... Пароль - Введите для поиска... + Что искать... Имя пользователя @@ -692,16 +736,13 @@ ]]> - Добавить внешнюю ссылку - Добавить внутреннюю ссылку - Добавить ссылку Заголовок - Внутренняя страница - Внешний URL - Переместить вниз - Переместить вверх - Открывать в новом окне - Удалить ссылку + Укажите заголовок + выбрать страницу сайта + указать внешнюю ссылку + Укажите ссылку + Ссылка + В новом окне Текущая версия @@ -716,10 +757,12 @@ Править файл скрипта + Аналитика Смотритель Содержимое Курьер Для Разработчиков + Формы Помощь Мастер конфигурирования Umbraco Медиа-материалы @@ -734,9 +777,11 @@ в качестве родительского типа. Вкладки родительского типа не показаны и могут быть изменены непосредственно в родительском типе Родительский тип контента разрешен Данный тип контента использует + Создать шаблон для документов этого типа Шаблон по-умолчанию Словарная статья Чтобы импортировать тип документа, найдите файл ".udt" на своем компьютере, нажав на кнопку "Обзор", затем нажмите "Импортировать" (на следующем экране будет запрошено подтверждение для этой операции). + Родительский тип документа Заголовок новой вкладки Тип узла (документа) Для данной вкладки не определены свойства. Кликните по ссылке "Click here to add a new property" сверху, чтобы создать новое свойство. @@ -906,6 +951,7 @@ Загрузить переведенный xml + Аналитика Обзор кэша Корзина Созданные пакеты @@ -975,6 +1021,7 @@ Поиск всех дочерних документов Сессия истекает через Начальный узел содержимого + Переводчик Имя пользователя Разрешения для пользователя Тип пользователя From 30dfd2772b2f49f05c13c5081229742ceab064b5 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 7 Jul 2015 15:20:35 +0200 Subject: [PATCH 40/50] "+" in the version doesn't work with the current NuGet --- build/Build.proj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Build.proj b/build/Build.proj index b0ea447de6..051e02de5c 100644 --- a/build/Build.proj +++ b/build/Build.proj @@ -69,7 +69,7 @@ .$(BUILD_RELEASE)-$(BUILD_COMMENT) - .$(BUILD_RELEASE)+$(BUILD_NIGHTLY) + .$(BUILD_RELEASE)-$(BUILD_NIGHTLY) .$(BUILD_RELEASE)-$(BUILD_COMMENT)-$(BUILD_NIGHTLY) From f6e0dfe9dc2fd9eef3d77dd057188a3ab0b2cb3e Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 7 Jul 2015 16:11:33 +0200 Subject: [PATCH 41/50] Changes published media cache log output to warnings (when indexes are not found) --- .../PublishedCache/XmlPublishedCache/PublishedMediaCache.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs index a10d110255..2e437ccc97 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs @@ -218,7 +218,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache } } - LogHelper.Debug( + LogHelper.Warn( "Could not retrieve media {0} from Examine index, reverting to looking up media via legacy library.GetMedia method", () => id); @@ -236,7 +236,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache : ConvertFromXPathNavigator(media.Current); } - LogHelper.Debug( + LogHelper.Warn( "Could not retrieve media {0} from Examine index or from legacy library.GetMedia method", () => id); From 1ffde1e9a6eb4a34b14d70359bc4b6aedfdf4869 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 7 Jul 2015 16:11:49 +0200 Subject: [PATCH 42/50] makes log tests go a little quicker --- src/Umbraco.Tests/Logging/DebugAppender.cs | 4 ++++ src/Umbraco.Tests/Logging/ParallelForwarderTest.cs | 2 ++ 2 files changed, 6 insertions(+) diff --git a/src/Umbraco.Tests/Logging/DebugAppender.cs b/src/Umbraco.Tests/Logging/DebugAppender.cs index c1a1523349..a483103992 100644 --- a/src/Umbraco.Tests/Logging/DebugAppender.cs +++ b/src/Umbraco.Tests/Logging/DebugAppender.cs @@ -10,8 +10,12 @@ namespace Umbraco.Tests.Logging public TimeSpan AppendDelay { get; set; } public int LoggedEventCount { get { return m_eventsList.Count; } } + public bool Cancel { get; set; } + protected override void Append(LoggingEvent loggingEvent) { + if (Cancel) return; + if (AppendDelay > TimeSpan.Zero) { Thread.Sleep(AppendDelay); diff --git a/src/Umbraco.Tests/Logging/ParallelForwarderTest.cs b/src/Umbraco.Tests/Logging/ParallelForwarderTest.cs index fffc28fe1f..03df34c420 100644 --- a/src/Umbraco.Tests/Logging/ParallelForwarderTest.cs +++ b/src/Umbraco.Tests/Logging/ParallelForwarderTest.cs @@ -118,6 +118,8 @@ namespace Umbraco.Tests.Logging Assert.That(debugAppender.LoggedEventCount, Is.EqualTo(0)); Assert.That(watch.ElapsedMilliseconds, Is.LessThan(testSize)); Console.WriteLine("Logged {0} errors in {1}ms", testSize, watch.ElapsedMilliseconds); + + debugAppender.Cancel = true; } [Test] From 253b9dab270824c98d3e956ce24ceb4631b1c1d2 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 7 Jul 2015 16:12:05 +0200 Subject: [PATCH 43/50] Ensures log4net is shutdown on app shutdown --- src/Umbraco.Core/UmbracoApplicationBase.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/UmbracoApplicationBase.cs b/src/Umbraco.Core/UmbracoApplicationBase.cs index a53dd09ca5..b98e3935ee 100644 --- a/src/Umbraco.Core/UmbracoApplicationBase.cs +++ b/src/Umbraco.Core/UmbracoApplicationBase.cs @@ -2,6 +2,7 @@ using System.Linq; using System.Web; using System.Web.Hosting; +using log4net; using Umbraco.Core.Logging; using Umbraco.Core.ObjectResolution; @@ -31,7 +32,6 @@ namespace Umbraco.Core /// internal void StartApplication(object sender, EventArgs e) { - //boot up the application GetBootManager() .Initialize() @@ -139,6 +139,9 @@ namespace Umbraco.Core Logger.Info("Application shutdown. Reason: " + HostingEnvironment.ShutdownReason); } OnApplicationEnd(sender, e); + + //Last thing to do is shutdown log4net + LogManager.Shutdown(); } protected abstract IBootManager GetBootManager(); From a712726065dc28e87fafdfefea5ea8aeab34cf3d Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 7 Jul 2015 16:18:57 +0200 Subject: [PATCH 44/50] Updates the logger and how to output process id and app domain id with the default log4net patterns. --- src/Umbraco.Core/Logging/Logger.cs | 47 ++++++------------- .../config/log4net.Release.config | 2 +- src/Umbraco.Web.UI/config/log4net.config | 2 +- 3 files changed, 16 insertions(+), 35 deletions(-) diff --git a/src/Umbraco.Core/Logging/Logger.cs b/src/Umbraco.Core/Logging/Logger.cs index a362ae0a02..ae8bb60fcd 100644 --- a/src/Umbraco.Core/Logging/Logger.cs +++ b/src/Umbraco.Core/Logging/Logger.cs @@ -14,23 +14,19 @@ namespace Umbraco.Core.Logging /// public class Logger : ILogger { - private static string _processAndDomain; - - static Logger() - { - // these won't change and can go in a static variable - _processAndDomain = "P" + Process.GetCurrentProcess().Id - + "/D" + AppDomain.CurrentDomain.Id; - } public Logger(FileInfo log4NetConfigFile) + :this() { XmlConfigurator.Configure(log4NetConfigFile); } private Logger() { - + //Add custom global properties to the log4net context that we can use in our logging output + + log4net.GlobalContext.Properties["processId"] = Process.GetCurrentProcess().Id; + log4net.GlobalContext.Properties["appDomainId"] = AppDomain.CurrentDomain.Id; } /// @@ -63,34 +59,19 @@ namespace Umbraco.Core.Logging return LogManager.GetLogger(getTypeFromInstance.GetType()); } - - /// - /// Useful if the logger itself is running on another thread - /// - /// - /// - private string PrefixThreadId(string generateMessageFormat) - { - return "[" + _processAndDomain - + "/T" + Thread.CurrentThread.ManagedThreadId - + "] " - + generateMessageFormat; - } - + public void Error(Type callingType, string message, Exception exception) { var logger = LogManager.GetLogger(callingType); if (logger != null) - logger.Error(PrefixThreadId(message), exception); + logger.Error((message), exception); } - - public void Warn(Type callingType, string message, params Func[] formatItems) { var logger = LogManager.GetLogger(callingType); if (logger == null || logger.IsWarnEnabled == false) return; - logger.WarnFormat(PrefixThreadId(message), formatItems.Select(x => x.Invoke()).ToArray()); + logger.WarnFormat((message), formatItems.Select(x => x.Invoke()).ToArray()); } public void Warn(Type callingType, string message, bool showHttpTrace, params Func[] formatItems) @@ -105,7 +86,7 @@ namespace Umbraco.Core.Logging var logger = LogManager.GetLogger(callingType); if (logger == null || logger.IsWarnEnabled == false) return; - logger.WarnFormat(PrefixThreadId(message), formatItems.Select(x => x.Invoke()).ToArray()); + logger.WarnFormat((message), formatItems.Select(x => x.Invoke()).ToArray()); } @@ -118,7 +99,7 @@ namespace Umbraco.Core.Logging var logger = LogManager.GetLogger(callingType); if (logger == null || logger.IsWarnEnabled == false) return; var executedParams = formatItems.Select(x => x.Invoke()).ToArray(); - logger.WarnFormat(PrefixThreadId(message) + ". Exception: " + e, executedParams); + logger.WarnFormat((message) + ". Exception: " + e, executedParams); } /// @@ -130,7 +111,7 @@ namespace Umbraco.Core.Logging { var logger = LogManager.GetLogger(callingType); if (logger == null || logger.IsInfoEnabled == false) return; - logger.Info(PrefixThreadId(generateMessage.Invoke())); + logger.Info((generateMessage.Invoke())); } /// @@ -144,7 +125,7 @@ namespace Umbraco.Core.Logging var logger = LogManager.GetLogger(type); if (logger == null || logger.IsInfoEnabled == false) return; var executedParams = formatItems.Select(x => x.Invoke()).ToArray(); - logger.InfoFormat(PrefixThreadId(generateMessageFormat), executedParams); + logger.InfoFormat((generateMessageFormat), executedParams); } @@ -157,7 +138,7 @@ namespace Umbraco.Core.Logging { var logger = LogManager.GetLogger(callingType); if (logger == null || logger.IsDebugEnabled == false) return; - logger.Debug(PrefixThreadId(generateMessage.Invoke())); + logger.Debug((generateMessage.Invoke())); } /// @@ -171,7 +152,7 @@ namespace Umbraco.Core.Logging var logger = LogManager.GetLogger(type); if (logger == null || logger.IsDebugEnabled == false) return; var executedParams = formatItems.Select(x => x.Invoke()).ToArray(); - logger.DebugFormat(PrefixThreadId(generateMessageFormat), executedParams); + logger.DebugFormat((generateMessageFormat), executedParams); } diff --git a/src/Umbraco.Web.UI/config/log4net.Release.config b/src/Umbraco.Web.UI/config/log4net.Release.config index 6b2df3e0bf..bbca2a8277 100644 --- a/src/Umbraco.Web.UI/config/log4net.Release.config +++ b/src/Umbraco.Web.UI/config/log4net.Release.config @@ -13,7 +13,7 @@ - + diff --git a/src/Umbraco.Web.UI/config/log4net.config b/src/Umbraco.Web.UI/config/log4net.config index 2ff4924560..497fd4471f 100644 --- a/src/Umbraco.Web.UI/config/log4net.config +++ b/src/Umbraco.Web.UI/config/log4net.config @@ -13,7 +13,7 @@ - + From 0fe96e80c90f38cbfffd08f3311136daf1d54f50 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 7 Jul 2015 18:26:53 +0200 Subject: [PATCH 45/50] Fixes: U4-6782 ContentTypeService should support getByKey(Guid) and refactors a little bit of the RepositoryBase stuff with the addition of IReadRepository, adds some tests to support. --- .../Repositories/ContentTypeBaseRepository.cs | 333 ++++++++++++++---- .../Repositories/ContentTypeRepository.cs | 23 +- .../Interfaces/IContentTypeRepository.cs | 5 +- .../Interfaces/IMediaTypeRepository.cs | 5 +- .../Interfaces/IMemberTypeRepository.cs | 5 +- .../Repositories/Interfaces/IRepository.cs | 41 ++- .../Interfaces/IRepositoryQueryable.cs | 2 + .../Repositories/MediaTypeRepository.cs | 65 ++-- .../Repositories/MemberTypeRepository.cs | 36 ++ .../Services/ContentTypeService.cs | 138 +++++++- .../Services/IContentTypeService.cs | 56 +++ src/Umbraco.Core/Services/MemberService.cs | 2 +- .../Repositories/ContentTypeRepositoryTest.cs | 42 +++ .../Repositories/MediaTypeRepositoryTest.cs | 45 +++ .../Repositories/MemberTypeRepositoryTest.cs | 40 +++ 15 files changed, 692 insertions(+), 146 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs index cdd502944c..fb3e9f4871 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs @@ -24,15 +24,18 @@ namespace Umbraco.Core.Persistence.Repositories /// /// Exposes shared functionality /// - internal abstract class ContentTypeBaseRepository : PetaPocoRepositoryBase + internal abstract class ContentTypeBaseRepository : PetaPocoRepositoryBase, IReadRepository where TEntity : class, IContentTypeComposition { protected ContentTypeBaseRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) { + _guidRepo = new GuidReadOnlyContentTypeBaseRepository(this, work, cache, logger, sqlSyntax); } + private readonly GuidReadOnlyContentTypeBaseRepository _guidRepo; + /// /// Returns the content type ids that match the query /// @@ -59,7 +62,7 @@ namespace Umbraco.Core.Persistence.Repositories yield return dto.ContentTypeNodeId; } } - + protected virtual PropertyType CreatePropertyType(string propertyEditorAlias, DataTypeDatabaseType dbType, string propertyTypeAlias) { return new PropertyType(propertyEditorAlias, dbType, propertyTypeAlias); @@ -118,7 +121,7 @@ AND umbracoNode.nodeObjectType = @objectType", else { //Fallback for ContentTypes with no identity - var contentTypeDto = Database.FirstOrDefault("WHERE alias = @Alias", new {Alias = composition.Alias}); + var contentTypeDto = Database.FirstOrDefault("WHERE alias = @Alias", new { Alias = composition.Alias }); if (contentTypeDto != null) { Database.Insert(new ContentType2ContentTypeDto { ParentId = contentTypeDto.NodeId, ChildId = entity.Id }); @@ -138,7 +141,7 @@ AND umbracoNode.nodeObjectType = @objectType", } var propertyFactory = new PropertyGroupFactory(nodeDto.NodeId); - + //Insert Tabs foreach (var propertyGroup in entity.PropertyGroups) { @@ -199,16 +202,16 @@ AND umbracoNode.id <> @id", var o = Database.Update(nodeDto); //Look up ContentType entry to get PrimaryKey for updating the DTO - var dtoPk = Database.First("WHERE nodeId = @Id", new {Id = entity.Id}); + var dtoPk = Database.First("WHERE nodeId = @Id", new { Id = entity.Id }); dto.PrimaryKey = dtoPk.PrimaryKey; Database.Update(dto); //Delete the ContentType composition entries before adding the updated collection - Database.Delete("WHERE childContentTypeId = @Id", new {Id = entity.Id}); + Database.Delete("WHERE childContentTypeId = @Id", new { Id = entity.Id }); //Update ContentType composition in new table foreach (var composition in entity.ContentTypeComposition) { - Database.Insert(new ContentType2ContentTypeDto {ParentId = composition.Id, ChildId = entity.Id}); + Database.Insert(new ContentType2ContentTypeDto { ParentId = composition.Id, ChildId = entity.Id }); } //Removing a ContentType from a composition (U4-1690) @@ -233,7 +236,7 @@ AND umbracoNode.id <> @id", foreach (var key in compositionBase.RemovedContentTypeKeyTracker) { //Find PropertyTypes for the removed ContentType - var propertyTypes = Database.Fetch("WHERE contentTypeId = @Id", new {Id = key}); + var propertyTypes = Database.Fetch("WHERE contentTypeId = @Id", new { Id = key }); //Loop through the Content that is based on the current ContentType in order to remove the Properties that are //based on the PropertyTypes that belong to the removed ContentType. foreach (var contentDto in contentDtos) @@ -258,7 +261,7 @@ AND umbracoNode.id <> @id", } //Delete the allowed content type entries before adding the updated collection - Database.Delete("WHERE Id = @Id", new {Id = entity.Id}); + Database.Delete("WHERE Id = @Id", new { Id = entity.Id }); //Insert collection of allowed content types foreach (var allowedContentType in entity.AllowedContentTypes) { @@ -271,10 +274,10 @@ AND umbracoNode.id <> @id", } - if (((ICanBeDirty) entity).IsPropertyDirty("PropertyTypes") || entity.PropertyTypes.Any(x => x.IsDirty())) + if (((ICanBeDirty)entity).IsPropertyDirty("PropertyTypes") || entity.PropertyTypes.Any(x => x.IsDirty())) { //Delete PropertyTypes by excepting entries from db with entries from collections - var dbPropertyTypes = Database.Fetch("WHERE contentTypeId = @Id", new {Id = entity.Id}); + var dbPropertyTypes = Database.Fetch("WHERE contentTypeId = @Id", new { Id = entity.Id }); var dbPropertyTypeAlias = dbPropertyTypes.Select(x => x.Id); var entityPropertyTypes = entity.PropertyTypes.Where(x => x.HasIdentity).Select(x => x.Id); var items = dbPropertyTypeAlias.Except(entityPropertyTypes); @@ -282,18 +285,18 @@ AND umbracoNode.id <> @id", { //Before a PropertyType can be deleted, all Properties based on that PropertyType should be deleted. Database.Delete("WHERE propertyTypeId = @Id", new { Id = item }); - Database.Delete("WHERE propertytypeid = @Id", new {Id = item}); + Database.Delete("WHERE propertytypeid = @Id", new { Id = item }); Database.Delete("WHERE contentTypeId = @Id AND id = @PropertyTypeId", - new {Id = entity.Id, PropertyTypeId = item}); + new { Id = entity.Id, PropertyTypeId = item }); } } - if (entity.IsPropertyDirty("PropertyGroups") || + if (entity.IsPropertyDirty("PropertyGroups") || entity.PropertyGroups.Any(x => x.IsDirty())) { //Delete Tabs/Groups by excepting entries from db with entries from collections var dbPropertyGroups = - Database.Fetch("WHERE contenttypeNodeId = @Id", new {Id = entity.Id}) + Database.Fetch("WHERE contenttypeNodeId = @Id", new { Id = entity.Id }) .Select(x => new Tuple(x.Id, x.Text)) .ToList(); var entityPropertyGroups = entity.PropertyGroups.Select(x => new Tuple(x.Id, x.Name)).ToList(); @@ -329,11 +332,11 @@ AND umbracoNode.id <> @id", foreach (var tab in tabs) { Database.Update("SET propertyTypeGroupId = NULL WHERE propertyTypeGroupId = @PropertyGroupId", - new {PropertyGroupId = tab.Item1}); + new { PropertyGroupId = tab.Item1 }); Database.Update("SET parentGroupId = NULL WHERE parentGroupId = @TabId", - new {TabId = tab.Item1}); + new { TabId = tab.Item1 }); Database.Delete("WHERE contenttypeNodeId = @Id AND text = @Name", - new {Id = entity.Id, Name = tab.Item2}); + new { Id = entity.Id, Name = tab.Item2 }); } } @@ -447,7 +450,7 @@ AND umbracoNode.id <> @id", var list = new List(); foreach (var dto in dtos.Where(x => (x.PropertyTypeGroupId > 0) == false)) { - var propType = CreatePropertyType(dto.DataTypeDto.PropertyEditorAlias, dto.DataTypeDto.DbType.EnumParse(true), dto.Alias); + var propType = CreatePropertyType(dto.DataTypeDto.PropertyEditorAlias, dto.DataTypeDto.DbType.EnumParse(true), dto.Alias); propType.DataTypeDefinitionId = dto.DataTypeId; propType.Description = dto.Description; propType.Id = dto.Id; @@ -498,7 +501,7 @@ AND umbracoNode.id <> @id", throw exception; }); } - + /// /// Try to set the data type id based on its ControlId /// @@ -561,28 +564,30 @@ AND umbracoNode.id <> @id", } } - public static IEnumerable GetMediaTypes( - int[] mediaTypeIds, Database db, ISqlSyntaxProvider sqlSyntax, + public static IEnumerable GetMediaTypes( + TId[] mediaTypeIds, Database db, ISqlSyntaxProvider sqlSyntax, TRepo contentTypeRepository) - where TRepo : IRepositoryQueryable + where TRepo : IReadRepository + where TId: struct { - IDictionary> allParentMediaTypeIds; + IDictionary> allParentMediaTypeIds; var mediaTypes = MapMediaTypes(mediaTypeIds, db, sqlSyntax, out allParentMediaTypeIds) .ToArray(); MapContentTypeChildren(mediaTypes, db, sqlSyntax, contentTypeRepository, allParentMediaTypeIds); - + return mediaTypes; } - public static IEnumerable GetContentTypes( - int[] contentTypeIds, Database db, ISqlSyntaxProvider sqlSyntax, + public static IEnumerable GetContentTypes( + TId[] contentTypeIds, Database db, ISqlSyntaxProvider sqlSyntax, TRepo contentTypeRepository, ITemplateRepository templateRepository) - where TRepo : IRepositoryQueryable + where TRepo : IReadRepository + where TId : struct { - IDictionary> allAssociatedTemplates; - IDictionary> allParentContentTypeIds; + IDictionary> allAssociatedTemplates; + IDictionary> allParentContentTypeIds; var contentTypes = MapContentTypes(contentTypeIds, db, sqlSyntax, out allAssociatedTemplates, out allParentContentTypeIds) .ToArray(); @@ -592,17 +597,18 @@ AND umbracoNode.id <> @id", contentTypes, db, contentTypeRepository, templateRepository, allAssociatedTemplates); MapContentTypeChildren( - contentTypes, db, sqlSyntax, contentTypeRepository, allParentContentTypeIds); + contentTypes, db, sqlSyntax, contentTypeRepository, allParentContentTypeIds); } return contentTypes; } - internal static void MapContentTypeChildren(IContentTypeComposition[] contentTypes, + internal static void MapContentTypeChildren(IContentTypeComposition[] contentTypes, Database db, ISqlSyntaxProvider sqlSyntax, TRepo contentTypeRepository, - IDictionary> allParentContentTypeIds) - where TRepo : IRepositoryQueryable + IDictionary> allParentContentTypeIds) + where TRepo : IReadRepository + where TId : struct { //NOTE: SQL call #2 @@ -614,9 +620,9 @@ AND umbracoNode.id <> @id", foreach (var contentType in contentTypes) { contentType.PropertyGroups = allPropGroups[contentType.Id]; - ((ContentTypeBase) contentType).PropertyTypes = allPropTypes[contentType.Id]; + ((ContentTypeBase)contentType).PropertyTypes = allPropTypes[contentType.Id]; } - + //NOTE: SQL call #3++ if (allParentContentTypeIds != null) @@ -627,7 +633,18 @@ AND umbracoNode.id <> @id", var allParentContentTypes = contentTypeRepository.GetAll(allParentIdsAsArray).ToArray(); foreach (var contentType in contentTypes) { - var parentContentTypes = allParentContentTypes.Where(x => allParentContentTypeIds[contentType.Id].Contains(x.Id)); + //TODO: this is pretty hacky right now but i don't have time to refactor/fix running queries based on ints and Guids + // (i.e. for v8) but we need queries by GUIDs now so this is how it's gonna have to be + var entityId = typeof(TId) == typeof(int) ? contentType.Id : (object)contentType.Key; + + var parentContentTypes = allParentContentTypes.Where(x => + { + //TODO: this is pretty hacky right now but i don't have time to refactor/fix running queries based on ints and Guids + // (i.e. for v8) but we need queries by GUIDs now so this is how it's gonna have to be + var parentEntityId = typeof(TId) == typeof(int) ? x.Id : (object)x.Key; + + return allParentContentTypeIds[(TId)entityId].Contains((TId)parentEntityId); + }); foreach (var parentContentType in parentContentTypes) { var result = contentType.AddContentType(parentContentType); @@ -641,15 +658,16 @@ AND umbracoNode.id <> @id", } } - + } - internal static void MapContentTypeTemplates(IContentType[] contentTypes, + internal static void MapContentTypeTemplates(IContentType[] contentTypes, Database db, TRepo contentTypeRepository, ITemplateRepository templateRepository, - IDictionary> associatedTemplates) - where TRepo : IRepositoryQueryable + IDictionary> associatedTemplates) + where TRepo : IReadRepository + where TId: struct { if (associatedTemplates == null || associatedTemplates.Any() == false) return; @@ -666,7 +684,11 @@ AND umbracoNode.id <> @id", foreach (var contentType in contentTypes) { - var associatedTemplateIds = associatedTemplates[contentType.Id].Select(x => x.TemplateId) + //TODO: this is pretty hacky right now but i don't have time to refactor/fix running queries based on ints and Guids + // (i.e. for v8) but we need queries by GUIDs now so this is how it's gonna have to be + var entityId = typeof(TId) == typeof(int) ? contentType.Id : (object)contentType.Key; + + var associatedTemplateIds = associatedTemplates[(TId)entityId].Select(x => x.TemplateId) .Distinct() .ToArray(); @@ -675,11 +697,12 @@ AND umbracoNode.id <> @id", : Enumerable.Empty()).ToArray(); } - + } - internal static IEnumerable MapMediaTypes(int[] mediaTypeIds, Database db, ISqlSyntaxProvider sqlSyntax, - out IDictionary> parentMediaTypeIds) + internal static IEnumerable MapMediaTypes(TId[] mediaTypeIds, Database db, ISqlSyntaxProvider sqlSyntax, + out IDictionary> parentMediaTypeIds) + where TId : struct { Mandate.That(mediaTypeIds.Any(), () => new InvalidOperationException("must be at least one content type id specified")); Mandate.ParameterNotNull(db, "db"); @@ -706,8 +729,21 @@ AND umbracoNode.id <> @id", ON AllowedTypes.Id = cmsContentType.nodeId LEFT JOIN cmsContentType2ContentType as ParentTypes ON ParentTypes.childContentTypeId = cmsContentType.nodeId - WHERE (umbracoNode.nodeObjectType = @nodeObjectType) - AND (umbracoNode.id IN (@contentTypeIds))"; + WHERE (umbracoNode.nodeObjectType = @nodeObjectType)"; + + if (mediaTypeIds.Any()) + { + //TODO: This is all sorts of hacky but i don't have time to refactor a lot to get both ints and guids working nicely... this will + // work for the time being. + if (typeof(TId) == typeof(int)) + { + sql = sql + " AND (umbracoNode.id IN (@contentTypeIds))"; + } + else if (typeof(TId) == typeof(Guid)) + { + sql = sql + " AND (umbracoNode.uniqueID IN (@contentTypeIds))"; + } + } //NOTE: we are going to assume there's not going to be more than 2100 content type ids since that is the max SQL param count! if ((mediaTypeIds.Length - 1) > 2000) @@ -721,7 +757,7 @@ AND umbracoNode.id <> @id", return Enumerable.Empty(); } - parentMediaTypeIds = new Dictionary>(); + parentMediaTypeIds = new Dictionary>(); var mappedMediaTypes = new List(); foreach (var contentTypeId in mediaTypeIds) @@ -733,7 +769,14 @@ AND umbracoNode.id <> @id", //first we want to get the main content type data this is 1 : 1 with umbraco node data var ct = result - .Where(x => x.ctId == currentCtId) + .Where(x => + { + //TODO: This is a bit hacky right now but don't have time to do a nice refactor to support both GUID and Int queries, so this is + // how it is for now. + return (typeof (TId) == typeof (int)) + ? x.ctId == currentCtId + : x.nUniqueId == currentCtId; + }) .Select(x => new { x.ctPk, x.ctId, x.ctAlias, x.ctAllowAtRoot, x.ctDesc, x.ctIcon, x.ctIsContainer, x.ctThumb, x.nName, x.nCreateDate, x.nLevel, x.nObjectType, x.nUser, x.nParentId, x.nPath, x.nSortOrder, x.nTrashed, x.nUniqueId }) .DistinctBy(x => (int)x.ctId) .FirstOrDefault(); @@ -757,7 +800,7 @@ AND umbracoNode.id <> @id", NodeDto = new NodeDto { CreateDate = ct.nCreateDate, - Level = (short) ct.nLevel, + Level = (short)ct.nLevel, NodeId = ct.ctId, NodeObjectType = ct.nObjectType, ParentId = ct.nParentId, @@ -769,7 +812,7 @@ AND umbracoNode.id <> @id", UserId = ct.nUser } }; - + //now create the media type object var factory = new MediaTypeFactory(new Guid(Constants.ObjectTypes.MediaType)); @@ -785,9 +828,10 @@ AND umbracoNode.id <> @id", return mappedMediaTypes; } - internal static IEnumerable MapContentTypes(int[] contentTypeIds, Database db, ISqlSyntaxProvider sqlSyntax, - out IDictionary> associatedTemplates, - out IDictionary> parentContentTypeIds) + internal static IEnumerable MapContentTypes(TId[] contentTypeIds, Database db, ISqlSyntaxProvider sqlSyntax, + out IDictionary> associatedTemplates, + out IDictionary> parentContentTypeIds) + where TId : struct { Mandate.ParameterNotNull(db, "db"); @@ -824,8 +868,21 @@ AND umbracoNode.id <> @id", LEFT JOIN cmsContentType2ContentType as ParentTypes ON ParentTypes.childContentTypeId = cmsContentType.nodeId WHERE (umbracoNode.nodeObjectType = @nodeObjectType)"; - if(contentTypeIds.Any()) - sql = sql + " AND (umbracoNode.id IN (@contentTypeIds))"; + + if (contentTypeIds.Any()) + { + //TODO: This is all sorts of hacky but i don't have time to refactor a lot to get both ints and guids working nicely... this will + // work for the time being. + if (typeof(TId) == typeof(int)) + { + sql = sql + " AND (umbracoNode.id IN (@contentTypeIds))"; + } + else if (typeof(TId) == typeof(Guid)) + { + sql = sql + " AND (umbracoNode.uniqueID IN (@contentTypeIds))"; + } + } + //NOTE: we are going to assume there's not going to be more than 2100 content type ids since that is the max SQL param count! if ((contentTypeIds.Length - 1) > 2000) @@ -840,8 +897,8 @@ AND umbracoNode.id <> @id", return Enumerable.Empty(); } - parentContentTypeIds = new Dictionary>(); - associatedTemplates = new Dictionary>(); + parentContentTypeIds = new Dictionary>(); + associatedTemplates = new Dictionary>(); var mappedContentTypes = new List(); foreach (var contentTypeId in contentTypeIds) @@ -853,7 +910,14 @@ AND umbracoNode.id <> @id", //first we want to get the main content type data this is 1 : 1 with umbraco node data var ct = result - .Where(x => x.ctId == currentCtId) + .Where(x => + { + //TODO: This is a bit hacky right now but don't have time to do a nice refactor to support both GUID and Int queries, so this is + // how it is for now. + return (typeof(TId) == typeof(int)) + ? x.ctId == currentCtId + : x.nUniqueId == currentCtId; + }) .Select(x => new { x.ctPk, x.ctId, x.ctAlias, x.ctAllowAtRoot, x.ctDesc, x.ctIcon, x.ctIsContainer, x.ctThumb, x.nName, x.nCreateDate, x.nLevel, x.nObjectType, x.nUser, x.nParentId, x.nPath, x.nSortOrder, x.nTrashed, x.nUniqueId }) .DistinctBy(x => (int)x.ctId) .FirstOrDefault(); @@ -865,7 +929,14 @@ AND umbracoNode.id <> @id", //get the unique list of associated templates var defaultTemplates = result - .Where(x => x.ctId == currentCtId) + .Where(x => + { + //TODO: This is a bit hacky right now but don't have time to do a nice refactor to support both GUID and Int queries, so this is + // how it is for now. + return (typeof(TId) == typeof(int)) + ? x.ctId == currentCtId + : x.nUniqueId == currentCtId; + }) //use a tuple so that distinct checks both values (in some rare cases the dtIsDefault will not compute as bool?, so we force it with Convert.ToBoolean) .Select(x => new Tuple(Convert.ToBoolean(x.dtIsDefault), x.dtTemplateId)) .Where(x => x.Item1.HasValue && x.Item2.HasValue) @@ -913,7 +984,14 @@ AND umbracoNode.id <> @id", // We will map a subset of the associated template - alias, id, name associatedTemplates.Add(currentCtId, result - .Where(x => x.ctId == currentCtId) + .Where(x => + { + //TODO: This is a bit hacky right now but don't have time to do a nice refactor to support both GUID and Int queries, so this is + // how it is for now. + return (typeof(TId) == typeof(int)) + ? x.ctId == currentCtId + : x.nUniqueId == currentCtId; + }) .Where(x => x.tId != null) .Select(x => new AssociatedTemplate(x.tId, x.tAlias, x.tText)) .Distinct() @@ -927,19 +1005,27 @@ AND umbracoNode.id <> @id", //map the allowed content types //map the child content type ids MapCommonContentTypeObjects(contentType, currentCtId, result, parentContentTypeIds); - + mappedContentTypes.Add(contentType); } return mappedContentTypes; } - private static void MapCommonContentTypeObjects(T contentType, int currentCtId, List result, IDictionary> parentContentTypeIds) - where T: IContentTypeBase + private static void MapCommonContentTypeObjects(T contentType, TId currentCtId, List result, IDictionary> parentContentTypeIds) + where T : IContentTypeBase + where TId : struct { //map the allowed content types contentType.AllowedContentTypes = result - .Where(x => x.ctId == currentCtId) + .Where(x => + { + //TODO: This is a bit hacky right now but don't have time to do a nice refactor to support both GUID and Int queries, so this is + // how it is for now. + return (typeof(TId) == typeof(int)) + ? x.ctId == currentCtId + : x.nUniqueId == currentCtId; + }) //use tuple so we can use distinct on all vals .Select(x => new Tuple(x.ctaAllowedId, x.ctaSortOrder, x.ctaAlias)) .Where(x => x.Item1.HasValue && x.Item2.HasValue && x.Item3 != null) @@ -949,8 +1035,15 @@ AND umbracoNode.id <> @id", //map the child content type ids parentContentTypeIds.Add(currentCtId, result - .Where(x => x.ctId == currentCtId) - .Select(x => (int?)x.chtParentId) + .Where(x => + { + //TODO: This is a bit hacky right now but don't have time to do a nice refactor to support both GUID and Int queries, so this is + // how it is for now. + return (typeof(TId) == typeof(int)) + ? x.ctId == currentCtId + : x.nUniqueId == currentCtId; + }) + .Select(x => (TId?)x.chtParentId) .Where(x => x.HasValue) .Distinct() .Select(x => x.Value).ToList()); @@ -959,7 +1052,7 @@ AND umbracoNode.id <> @id", internal static void MapGroupsAndProperties(int[] contentTypeIds, Database db, ISqlSyntaxProvider sqlSyntax, out IDictionary allPropertyTypeCollection, out IDictionary allPropertyGroupCollection) - { + { // first part Gets all property groups including property type data even when no property type exists on the group // second part Gets all property types including ones that are not on a group @@ -997,7 +1090,7 @@ AND umbracoNode.id <> @id", LEFT JOIN cmsPropertyTypeGroup as PG ON PG.id = PT.propertyTypeGroupId"); - if(contentTypeIds.Any()) + if (contentTypeIds.Any()) sqlBuilder.AppendLine(" WHERE (PT.contentTypeId in (@contentTypeIds))"); sqlBuilder.AppendLine(" ORDER BY (pgId)"); @@ -1022,7 +1115,7 @@ AND umbracoNode.id <> @id", int currId = contentTypeId; - var propertyGroupCollection = new PropertyGroupCollection(result + var propertyGroupCollection = new PropertyGroupCollection(result //get all rows that have a group id .Where(x => x.pgId != null) //filter based on the current content type @@ -1060,7 +1153,7 @@ AND umbracoNode.id <> @id", //Create the property type collection now (that don't have groups) - var propertyTypeCollection = new PropertyTypeCollection(result + var propertyTypeCollection = new PropertyTypeCollection(result .Where(x => x.pgId == null) //filter based on the current content type .Where(x => x.contentTypeId == currId) @@ -1081,9 +1174,105 @@ AND umbracoNode.id <> @id", allPropertyTypeCollection[currId] = propertyTypeCollection; } - + } } + + /// + /// Inner repository to support the GUID lookups and keep the caching consistent + /// + internal class GuidReadOnlyContentTypeBaseRepository : PetaPocoRepositoryBase + { + private readonly ContentTypeBaseRepository _parentRepo; + + public GuidReadOnlyContentTypeBaseRepository( + ContentTypeBaseRepository parentRepo, + IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) + : base(work, cache, logger, sqlSyntax) + { + _parentRepo = parentRepo; + } + + protected override TEntity PerformGet(Guid id) + { + return _parentRepo.PerformGet(id); + } + + protected override IEnumerable PerformGetAll(params Guid[] ids) + { + return _parentRepo.PerformGetAll(ids); + } + + protected override Sql GetBaseQuery(bool isCount) + { + return _parentRepo.GetBaseQuery(isCount); + } + + protected override string GetBaseWhereClause() + { + return "umbracoNode.uniqueID = @Id"; + } + + #region No implementation required + protected override IEnumerable PerformGetByQuery(IQuery query) + { + throw new NotImplementedException(); + } + + protected override IEnumerable GetDeleteClauses() + { + throw new NotImplementedException(); + } + + protected override Guid NodeObjectTypeId + { + get { throw new NotImplementedException(); } + } + + protected override void PersistNewItem(TEntity entity) + { + throw new NotImplementedException(); + } + + protected override void PersistUpdatedItem(TEntity entity) + { + throw new NotImplementedException(); + } + #endregion + } + + protected abstract TEntity PerformGet(Guid id); + protected abstract IEnumerable PerformGetAll(params Guid[] ids); + + /// + /// Gets an Entity by Id + /// + /// + /// + public TEntity Get(Guid id) + { + return _guidRepo.Get(id); + } + + /// + /// Gets all entities of the spefified type + /// + /// + /// + public IEnumerable GetAll(params Guid[] ids) + { + return _guidRepo.GetAll(ids); + } + + /// + /// Boolean indicating whether an Entity with the specified Id exists + /// + /// + /// + public bool Exists(Guid id) + { + return _guidRepo.Exists(id); + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs index 40679af854..95601011fa 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs @@ -242,6 +242,27 @@ namespace Umbraco.Core.Persistence.Repositories #endregion - + protected override IContentType PerformGet(Guid id) + { + var contentTypes = ContentTypeQueryMapper.GetContentTypes( + new[] { id }, Database, SqlSyntax, this, _templateRepository); + + var contentType = contentTypes.SingleOrDefault(); + return contentType; + } + + protected override IEnumerable PerformGetAll(params Guid[] ids) + { + if (ids.Any()) + { + return ContentTypeQueryMapper.GetContentTypes(ids, Database, SqlSyntax, this, _templateRepository); + } + else + { + var sql = new Sql().Select("id").From(SqlSyntax).Where(dto => dto.NodeObjectType == NodeObjectTypeId); + var allIds = Database.Fetch(sql).ToArray(); + return ContentTypeQueryMapper.GetContentTypes(allIds, Database, SqlSyntax, this, _templateRepository); + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs index e3ce084cf7..ca7e0e3c32 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs @@ -1,10 +1,11 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories { - public interface IContentTypeRepository : IRepositoryQueryable + public interface IContentTypeRepository : IRepositoryQueryable, IReadRepository { /// /// Gets all entities of the specified query diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaTypeRepository.cs index 54220e0b59..d28c59ac5b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaTypeRepository.cs @@ -1,10 +1,11 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories { - public interface IMediaTypeRepository : IRepositoryQueryable + public interface IMediaTypeRepository : IRepositoryQueryable, IReadRepository { /// /// Gets all entities of the specified query diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberTypeRepository.cs index 9e91b1c87c..ae7739b28b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMemberTypeRepository.cs @@ -1,8 +1,9 @@ -using Umbraco.Core.Models; +using System; +using Umbraco.Core.Models; namespace Umbraco.Core.Persistence.Repositories { - public interface IMemberTypeRepository : IRepositoryQueryable + public interface IMemberTypeRepository : IRepositoryQueryable, IReadRepository { } diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepository.cs index 9cb4d938c7..ecee9af138 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepository.cs @@ -15,23 +15,8 @@ namespace Umbraco.Core.Persistence.Repositories } - /// - /// Defines the implementation of a Repository - /// - public interface IRepository : IRepository - { - /// - /// Adds or Updates an Entity - /// - /// - void AddOrUpdate(TEntity entity); - - /// - /// Deletes an Entity - /// - /// - void Delete(TEntity entity); - + public interface IReadRepository : IRepository + { /// /// Gets an Entity by Id /// @@ -52,5 +37,25 @@ namespace Umbraco.Core.Persistence.Repositories /// /// bool Exists(TId id); - } + } + + //TODO: This should be decoupled! Shouldn't inherit from the IReadRepository and should be named IWriteRepository + + /// + /// Defines the implementation of a Repository + /// + public interface IRepository : IReadRepository + { + /// + /// Adds or Updates an Entity + /// + /// + void AddOrUpdate(TEntity entity); + + /// + /// Deletes an Entity + /// + /// + void Delete(TEntity entity); + } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepositoryQueryable.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepositoryQueryable.cs index c32f1b33a6..c0682f94f3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepositoryQueryable.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IRepositoryQueryable.cs @@ -4,6 +4,8 @@ using Umbraco.Core.Persistence.Querying; namespace Umbraco.Core.Persistence.Repositories { + //TODO: This should be decoupled! Shouldn't inherit from the IRepository + /// /// Defines the implementation of a Repository, which allows queries against the /// diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs index cc51fd6e7c..bf963492d2 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs @@ -28,31 +28,10 @@ namespace Umbraco.Core.Persistence.Repositories protected override IMediaType PerformGet(int id) { - var contentTypeSql = GetBaseQuery(false); - contentTypeSql.Where(GetBaseWhereClause(), new { Id = id}); + var contentTypes = ContentTypeQueryMapper.GetMediaTypes( + new[] { id }, Database, SqlSyntax, this); - var dto = Database.Fetch(contentTypeSql).FirstOrDefault(); - - if (dto == null) - return null; - - var factory = new MediaTypeFactory(NodeObjectTypeId); - var contentType = factory.BuildEntity(dto); - - contentType.AllowedContentTypes = GetAllowedContentTypeIds(id); - contentType.PropertyGroups = GetPropertyGroupCollection(id, contentType.CreateDate, contentType.UpdateDate); - ((MediaType)contentType).PropertyTypes = GetPropertyTypeCollection(id, contentType.CreateDate, contentType.UpdateDate); - - var list = Database.Fetch("WHERE childContentTypeId = @Id", new{ Id = id}); - foreach (var contentTypeDto in list) - { - bool result = contentType.AddContentType(Get(contentTypeDto.ParentId)); - //Do something if adding fails? (Should hopefully not be possible unless someone create a circular reference) - } - - //on initial construction we don't want to have dirty properties tracked - // http://issues.umbraco.org/issue/U4-1946 - ((Entity)contentType).ResetDirtyProperties(false); + var contentType = contentTypes.SingleOrDefault(); return contentType; } @@ -85,13 +64,18 @@ namespace Umbraco.Core.Persistence.Repositories #endregion + + /// + /// Gets all entities of the specified query + /// + /// + /// An enumerable list of objects public IEnumerable GetByQuery(IQuery query) { - var ints = PerformGetByQuery(query); - foreach (var i in ints) - { - yield return Get(i); - } + var ints = PerformGetByQuery(query).ToArray(); + return ints.Any() + ? GetAll(ints) + : Enumerable.Empty(); } #region Overrides of PetaPocoRepositoryBase @@ -181,5 +165,28 @@ namespace Umbraco.Core.Persistence.Repositories } #endregion + + protected override IMediaType PerformGet(Guid id) + { + var contentTypes = ContentTypeQueryMapper.GetMediaTypes( + new[] { id }, Database, SqlSyntax, this); + + var contentType = contentTypes.SingleOrDefault(); + return contentType; + } + + protected override IEnumerable PerformGetAll(params Guid[] ids) + { + if (ids.Any()) + { + return ContentTypeQueryMapper.GetMediaTypes(ids, Database, SqlSyntax, this); + } + else + { + var sql = new Sql().Select("id").From(SqlSyntax).Where(dto => dto.NodeObjectType == NodeObjectTypeId); + var allIds = Database.Fetch(sql).ToArray(); + return ContentTypeQueryMapper.GetMediaTypes(allIds, Database, SqlSyntax, this); + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs index bb6099a206..39f2a93082 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberTypeRepository.cs @@ -260,6 +260,42 @@ namespace Umbraco.Core.Persistence.Repositories propertyTypeAlias); } + protected override IMemberType PerformGet(Guid id) + { + var sql = GetBaseQuery(false); + sql.Where("umbracoNode.uniqueID = @Id", new { Id = id }); + sql.OrderByDescending(x => x.NodeId); + + var dtos = + Database.Fetch( + new PropertyTypePropertyGroupRelator().Map, sql); + + if (dtos == null || dtos.Any() == false) + return null; + + var factory = new MemberTypeReadOnlyFactory(); + var member = factory.BuildEntity(dtos.First()); + + return member; + } + + protected override IEnumerable PerformGetAll(params Guid[] ids) + { + var sql = GetBaseQuery(false); + if (ids.Any()) + { + var statement = string.Join(" OR ", ids.Select(x => string.Format("umbracoNode.uniqueID='{0}'", x))); + sql.Where(statement); + } + sql.OrderByDescending(x => x.NodeId, SqlSyntax); + + var dtos = + Database.Fetch( + new PropertyTypePropertyGroupRelator().Map, sql); + + return BuildFromDtos(dtos); + } + /// /// Ensure that all the built-in membership provider properties have their correct data type /// and property editors assigned. This occurs prior to saving so that the correct values are persisted. diff --git a/src/Umbraco.Core/Services/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs index 9bff23a99b..194871f252 100644 --- a/src/Umbraco.Core/Services/ContentTypeService.cs +++ b/src/Umbraco.Core/Services/ContentTypeService.cs @@ -185,6 +185,19 @@ namespace Umbraco.Core.Services } } + /// + /// Gets an object by its Key + /// + /// Alias of the to retrieve + /// + public IContentType GetContentType(Guid id) + { + using (var repository = RepositoryFactory.CreateContentTypeRepository(UowProvider.GetUnitOfWork())) + { + return repository.Get(id); + } + } + /// /// Gets a list of all available objects /// @@ -198,6 +211,19 @@ namespace Umbraco.Core.Services } } + /// + /// Gets a list of all available objects + /// + /// Optional list of ids + /// An Enumerable list of objects + public IEnumerable GetAllContentTypes(IEnumerable ids) + { + using (var repository = RepositoryFactory.CreateContentTypeRepository(UowProvider.GetUnitOfWork())) + { + return repository.GetAll(ids.ToArray()); + } + } + /// /// Gets a list of children for a object /// @@ -213,25 +239,22 @@ namespace Umbraco.Core.Services } } - ///// - ///// Returns the content type descendant Ids for the content type specified - ///// - ///// - ///// - //internal IEnumerable GetDescendantContentTypeIds(int contentTypeId) - //{ - // using (var uow = UowProvider.GetUnitOfWork()) - // { - // //method to return the child content type ids for the id specified - // Func getChildIds = - // parentId => - // uow.Database.Fetch("WHERE parentContentTypeId = @Id", new {Id = parentId}) - // .Select(x => x.ChildId).ToArray(); - - // //recursively get all descendant ids - // return getChildIds(contentTypeId).FlattenList(getChildIds); - // } - //} + /// + /// Gets a list of children for a object + /// + /// Id of the Parent + /// An Enumerable list of objects + public IEnumerable GetContentTypeChildren(Guid id) + { + using (var repository = RepositoryFactory.CreateContentTypeRepository(UowProvider.GetUnitOfWork())) + { + var found = GetContentType(id); + if (found == null) return Enumerable.Empty(); + var query = Query.Builder.Where(x => x.ParentId == found.Id); + var contentTypes = repository.GetByQuery(query); + return contentTypes; + } + } /// /// Checks whether an item has any children @@ -248,6 +271,23 @@ namespace Umbraco.Core.Services } } + /// + /// Checks whether an item has any children + /// + /// Id of the + /// True if the content type has any children otherwise False + public bool HasChildren(Guid id) + { + using (var repository = RepositoryFactory.CreateContentTypeRepository(UowProvider.GetUnitOfWork())) + { + var found = GetContentType(id); + if (found == null) return false; + var query = Query.Builder.Where(x => x.ParentId == found.Id); + int count = repository.Count(query); + return count > 0; + } + } + /// /// This is called after an IContentType is saved and is used to update the content xml structures in the database /// if they are required to be updated. @@ -518,6 +558,19 @@ namespace Umbraco.Core.Services } } + /// + /// Gets an object by its Id + /// + /// Id of the to retrieve + /// + public IMediaType GetMediaType(Guid id) + { + using (var repository = RepositoryFactory.CreateMediaTypeRepository(UowProvider.GetUnitOfWork())) + { + return repository.Get(id); + } + } + /// /// Gets a list of all available objects /// @@ -531,6 +584,19 @@ namespace Umbraco.Core.Services } } + /// + /// Gets a list of all available objects + /// + /// Optional list of ids + /// An Enumerable list of objects + public IEnumerable GetAllMediaTypes(IEnumerable ids) + { + using (var repository = RepositoryFactory.CreateMediaTypeRepository(UowProvider.GetUnitOfWork())) + { + return repository.GetAll(ids.ToArray()); + } + } + /// /// Gets a list of children for a object /// @@ -546,6 +612,23 @@ namespace Umbraco.Core.Services } } + /// + /// Gets a list of children for a object + /// + /// Id of the Parent + /// An Enumerable list of objects + public IEnumerable GetMediaTypeChildren(Guid id) + { + using (var repository = RepositoryFactory.CreateMediaTypeRepository(UowProvider.GetUnitOfWork())) + { + var found = GetMediaType(id); + if (found == null) return Enumerable.Empty(); + var query = Query.Builder.Where(x => x.ParentId == found.Id); + var contentTypes = repository.GetByQuery(query); + return contentTypes; + } + } + /// /// Checks whether an item has any children /// @@ -561,6 +644,23 @@ namespace Umbraco.Core.Services } } + /// + /// Checks whether an item has any children + /// + /// Id of the + /// True if the media type has any children otherwise False + public bool MediaTypeHasChildren(Guid id) + { + using (var repository = RepositoryFactory.CreateMediaTypeRepository(UowProvider.GetUnitOfWork())) + { + var found = GetMediaType(id); + if (found == null) return false; + var query = Query.Builder.Where(x => x.ParentId == found.Id); + int count = repository.Count(query); + return count > 0; + } + } + /// /// Saves a single object /// diff --git a/src/Umbraco.Core/Services/IContentTypeService.cs b/src/Umbraco.Core/Services/IContentTypeService.cs index 9e2d179bb0..241f82ec83 100644 --- a/src/Umbraco.Core/Services/IContentTypeService.cs +++ b/src/Umbraco.Core/Services/IContentTypeService.cs @@ -66,6 +66,13 @@ namespace Umbraco.Core.Services /// IContentType GetContentType(string alias); + /// + /// Gets an object by its Key + /// + /// Alias of the to retrieve + /// + IContentType GetContentType(Guid id); + /// /// Gets a list of all available objects /// @@ -73,6 +80,13 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects IEnumerable GetAllContentTypes(params int[] ids); + /// + /// Gets a list of all available objects + /// + /// Optional list of ids + /// An Enumerable list of objects + IEnumerable GetAllContentTypes(IEnumerable ids); + /// /// Gets a list of children for a object /// @@ -80,6 +94,13 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects IEnumerable GetContentTypeChildren(int id); + /// + /// Gets a list of children for a object + /// + /// Id of the Parent + /// An Enumerable list of objects + IEnumerable GetContentTypeChildren(Guid id); + /// /// Saves a single object /// @@ -124,6 +145,13 @@ namespace Umbraco.Core.Services /// IMediaType GetMediaType(string alias); + /// + /// Gets an object by its Id + /// + /// Id of the to retrieve + /// + IMediaType GetMediaType(Guid id); + /// /// Gets a list of all available objects /// @@ -131,6 +159,13 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects IEnumerable GetAllMediaTypes(params int[] ids); + /// + /// Gets a list of all available objects + /// + /// Optional list of ids + /// An Enumerable list of objects + IEnumerable GetAllMediaTypes(IEnumerable ids); + /// /// Gets a list of children for a object /// @@ -138,6 +173,13 @@ namespace Umbraco.Core.Services /// An Enumerable list of objects IEnumerable GetMediaTypeChildren(int id); + /// + /// Gets a list of children for a object + /// + /// Id of the Parent + /// An Enumerable list of objects + IEnumerable GetMediaTypeChildren(Guid id); + /// /// Saves a single object /// @@ -187,11 +229,25 @@ namespace Umbraco.Core.Services /// True if the content type has any children otherwise False bool HasChildren(int id); + /// + /// Checks whether an item has any children + /// + /// Id of the + /// True if the content type has any children otherwise False + bool HasChildren(Guid id); + /// /// Checks whether an item has any children /// /// Id of the /// True if the media type has any children otherwise False bool MediaTypeHasChildren(int id); + + /// + /// Checks whether an item has any children + /// + /// Id of the + /// True if the media type has any children otherwise False + bool MediaTypeHasChildren(Guid id); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/MemberService.cs b/src/Umbraco.Core/Services/MemberService.cs index 33f2ae5306..c6c49da7e9 100644 --- a/src/Umbraco.Core/Services/MemberService.cs +++ b/src/Umbraco.Core/Services/MemberService.cs @@ -80,7 +80,7 @@ namespace Umbraco.Core.Services { using (var repository = RepositoryFactory.CreateMemberTypeRepository(UowProvider.GetUnitOfWork())) { - var types = repository.GetAll().Select(x => x.Alias).ToArray(); + var types = repository.GetAll(new int[]{}).Select(x => x.Alias).ToArray(); if (types.Any() == false) { diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs index 63ef153e32..61364dd629 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -226,6 +226,25 @@ namespace Umbraco.Tests.Persistence.Repositories } } + [Test] + public void Can_Perform_Get_By_Guid_On_ContentTypeRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + using (var repository = CreateRepository(unitOfWork)) + { + var contentType = repository.Get(NodeDto.NodeIdSeed + 1); + + // Act + contentType = repository.Get(contentType.Key); + + // Assert + Assert.That(contentType, Is.Not.Null); + Assert.That(contentType.Id, Is.EqualTo(NodeDto.NodeIdSeed + 1)); + } + } + [Test] public void Can_Perform_GetAll_On_ContentTypeRepository() { @@ -248,6 +267,29 @@ namespace Umbraco.Tests.Persistence.Repositories } } + [Test] + public void Can_Perform_GetAll_By_Guid_On_ContentTypeRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + using (var repository = CreateRepository(unitOfWork)) + { + var allGuidIds = repository.GetAll().Select(x => x.Key).ToArray(); + + // Act + var contentTypes = repository.GetAll(allGuidIds); + int count = + DatabaseContext.Database.ExecuteScalar( + "SELECT COUNT(*) FROM umbracoNode WHERE nodeObjectType = @NodeObjectType", + new { NodeObjectType = new Guid(Constants.ObjectTypes.DocumentType) }); + + // Assert + Assert.That(contentTypes.Any(), Is.True); + Assert.That(contentTypes.Count(), Is.EqualTo(count)); + } + } + [Test] public void Can_Perform_Exists_On_ContentTypeRepository() { diff --git a/src/Umbraco.Tests/Persistence/Repositories/MediaTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MediaTypeRepositoryTest.cs index 55a4b38ba4..d0d66c5479 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MediaTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MediaTypeRepositoryTest.cs @@ -135,6 +135,26 @@ namespace Umbraco.Tests.Persistence.Repositories } } + [Test] + public void Can_Perform_Get_By_Guid_On_MediaTypeRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + using (var repository = CreateRepository(unitOfWork)) + { + var mediaType = repository.Get(1033); //File + + // Act + mediaType = repository.Get(mediaType.Key); + + // Assert + Assert.That(mediaType, Is.Not.Null); + Assert.That(mediaType.Id, Is.EqualTo(1033)); + Assert.That(mediaType.Name, Is.EqualTo(Constants.Conventions.MediaTypes.File)); + } + } + [Test] public void Can_Perform_GetAll_On_MediaTypeRepository() { @@ -156,6 +176,31 @@ namespace Umbraco.Tests.Persistence.Repositories } } + [Test] + public void Can_Perform_GetAll_By_Guid_On_MediaTypeRepository() + { + // Arrange + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + using (var repository = CreateRepository(unitOfWork)) + { + var allGuidIds = repository.GetAll().Select(x => x.Key).ToArray(); + + // Act + + var mediaTypes = repository.GetAll(allGuidIds); + + int count = + DatabaseContext.Database.ExecuteScalar( + "SELECT COUNT(*) FROM umbracoNode WHERE nodeObjectType = @NodeObjectType", + new { NodeObjectType = new Guid(Constants.ObjectTypes.MediaType) }); + + // Assert + Assert.That(mediaTypes.Any(), Is.True); + Assert.That(mediaTypes.Count(), Is.EqualTo(count)); + } + } + [Test] public void Can_Perform_Exists_On_MediaTypeRepository() { diff --git a/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs index 1360500d96..1c4e5e4c2b 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MemberTypeRepositoryTest.cs @@ -98,6 +98,30 @@ namespace Umbraco.Tests.Persistence.Repositories } } + [Test] + public void Can_Get_All_Member_Types_By_Guid_Ids() + { + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + using (var repository = CreateRepository(unitOfWork)) + { + var memberType1 = MockedContentTypes.CreateSimpleMemberType(); + repository.AddOrUpdate(memberType1); + unitOfWork.Commit(); + + var memberType2 = MockedContentTypes.CreateSimpleMemberType(); + memberType2.Name = "AnotherType"; + memberType2.Alias = "anotherType"; + repository.AddOrUpdate(memberType2); + unitOfWork.Commit(); + + var result = repository.GetAll(memberType1.Key, memberType2.Key); + + //there are 3 because of the Member type created for init data + Assert.AreEqual(2, result.Count()); + } + } + //NOTE: This tests for left join logic (rev 7b14e8eacc65f82d4f184ef46c23340c09569052) [Test] public void Can_Get_All_Members_When_No_Properties_Assigned() @@ -125,6 +149,7 @@ namespace Umbraco.Tests.Persistence.Repositories } } + [Test] public void Can_Get_Member_Type_By_Id() { @@ -140,6 +165,21 @@ namespace Umbraco.Tests.Persistence.Repositories } } + [Test] + public void Can_Get_Member_Type_By_Guid_Id() + { + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + using (var repository = CreateRepository(unitOfWork)) + { + IMemberType memberType = MockedContentTypes.CreateSimpleMemberType(); + repository.AddOrUpdate(memberType); + unitOfWork.Commit(); + memberType = repository.Get(memberType.Key); + Assert.That(memberType, Is.Not.Null); + } + } + [Test] public void Built_In_Member_Type_Properties_Are_Automatically_Added_When_Creating() { From 6833c195c1faf28851fed949687cce513b54e565 Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 6 Jul 2015 16:56:01 +0200 Subject: [PATCH 46/50] BackgroundTaskRunner - refactor recurring tasks, improve shutdown --- .../Scheduling/BackgroundTaskRunnerTests.cs | 67 +++++------ .../XmlCacheFilePersister.cs | 26 +---- .../Scheduling/BackgroundTaskRunner.cs | 9 +- .../Scheduling/DelayedRecurringTaskBase.cs | 74 ------------- src/Umbraco.Web/Scheduling/KeepAlive.cs | 79 +++++++++---- .../Scheduling/LatchedBackgroundTaskBase.cs | 77 +++++++++++++ src/Umbraco.Web/Scheduling/LogScrubber.cs | 37 +++---- .../Scheduling/RecurringTaskBase.cs | 104 ++++++++---------- .../Scheduling/ScheduledPublishing.cs | 71 ++++-------- src/Umbraco.Web/Scheduling/ScheduledTasks.cs | 35 ++---- src/Umbraco.Web/Scheduling/Scheduler.cs | 24 ++-- src/Umbraco.Web/Umbraco.Web.csproj | 2 +- 12 files changed, 278 insertions(+), 327 deletions(-) delete mode 100644 src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs create mode 100644 src/Umbraco.Web/Scheduling/LatchedBackgroundTaskBase.cs diff --git a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs index b541182de0..e6fc492350 100644 --- a/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs +++ b/src/Umbraco.Tests/Scheduling/BackgroundTaskRunnerTests.cs @@ -525,15 +525,11 @@ namespace Umbraco.Tests.Scheduling var waitHandle = new ManualResetEvent(false); using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions(), _logger)) { - runner.TaskCompleted += (sender, args) => runCount++; - runner.TaskStarting += async (sender, args) => + runner.TaskCompleted += (sender, args) => { - //wait for each task to finish once it's started - await sender.CurrentThreadingTask; + runCount++; if (runCount > 3) - { waitHandle.Set(); - } }; var task = new MyRecurringTask(runner, 200, 500); @@ -548,7 +544,10 @@ namespace Umbraco.Tests.Scheduling Assert.GreaterOrEqual(runCount, 4); // stops recurring - runner.Shutdown(false, false); + runner.Shutdown(false, true); + + // check that task has been disposed (timer has been killed, etc) + Assert.IsTrue(task.Disposed); } } @@ -595,15 +594,12 @@ namespace Umbraco.Tests.Scheduling var waitHandle = new ManualResetEvent(false); using (var runner = new BackgroundTaskRunner(new BackgroundTaskRunnerOptions(), _logger)) { - runner.TaskCompleted += (sender, args) => runCount++; - runner.TaskStarting += async (sender, args) => + runner.TaskCompleted += (sender, args) => { - //wait for each task to finish once it's started - await sender.CurrentThreadingTask; + runCount++; if (runCount > 3) - { waitHandle.Set(); - } + }; var task = new MyDelayedRecurringTask(runner, 2000, 1000); @@ -727,37 +723,31 @@ namespace Umbraco.Tests.Scheduling } } - private class MyDelayedRecurringTask : DelayedRecurringTaskBase + private class MyDelayedRecurringTask : RecurringTaskBase { public bool HasRun { get; private set; } - public MyDelayedRecurringTask(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds) + public MyDelayedRecurringTask(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds) : base(runner, delayMilliseconds, periodMilliseconds) { } - private MyDelayedRecurringTask(MyDelayedRecurringTask source) - : base(source) - { } - public override bool IsAsync { get { return false; } } - public override void PerformRun() + public override bool PerformRun() { HasRun = true; + return true; // repeat } - public override Task PerformRunAsync(CancellationToken token) + public override Task PerformRunAsync(CancellationToken token) { throw new NotImplementedException(); } - protected override MyDelayedRecurringTask GetRecurring() - { - return new MyDelayedRecurringTask(this); - } + public override bool RunsOnShutdown { get { return true; } } } private class MyDelayedTask : ILatchedBackgroundTask @@ -811,29 +801,23 @@ namespace Umbraco.Tests.Scheduling { } } - private class MyRecurringTask : RecurringTaskBase + private class MyRecurringTask : RecurringTaskBase { private readonly int _runMilliseconds; - - public MyRecurringTask(IBackgroundTaskRunner runner, int runMilliseconds, int periodMilliseconds) - : base(runner, periodMilliseconds) + public MyRecurringTask(IBackgroundTaskRunner runner, int runMilliseconds, int periodMilliseconds) + : base(runner, 0, periodMilliseconds) { _runMilliseconds = runMilliseconds; } - private MyRecurringTask(MyRecurringTask source, int runMilliseconds) - : base(source) - { - _runMilliseconds = runMilliseconds; - } - - public override void PerformRun() + public override bool PerformRun() { Thread.Sleep(_runMilliseconds); + return true; // repeat } - public override Task PerformRunAsync(CancellationToken token) + public override Task PerformRunAsync(CancellationToken token) { throw new NotImplementedException(); } @@ -843,10 +827,15 @@ namespace Umbraco.Tests.Scheduling get { return false; } } - protected override MyRecurringTask GetRecurring() + public override bool RunsOnShutdown { get { return false; } } + + protected override void Dispose(bool disposing) { - return new MyRecurringTask(this, _runMilliseconds); + Disposed = true; + base.Dispose(disposing); } + + public bool Disposed { get; private set; } } private class MyTask : BaseTask diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs index 137ed11482..ef84f31473 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/XmlCacheFilePersister.cs @@ -17,12 +17,11 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache /// if multiple threads are performing publishing tasks that the file will be persisted in accordance with the final resulting /// xml structure since the file writes are queued. /// - internal class XmlCacheFilePersister : ILatchedBackgroundTask + internal class XmlCacheFilePersister : LatchedBackgroundTaskBase { private readonly IBackgroundTaskRunner _runner; private readonly content _content; private readonly ProfilingLogger _logger; - private readonly ManualResetEventSlim _latch = new ManualResetEventSlim(false); private readonly object _locko = new object(); private bool _released; private Timer _timer; @@ -39,7 +38,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache private const int MaxWaitMilliseconds = 30000; // save the cache after some time (ie no more than 30s of changes) // save the cache when the app goes down - public bool RunsOnShutdown { get { return true; } } + public override bool RunsOnShutdown { get { return true; } } // initialize the first instance, which is inactive (not touched yet) public XmlCacheFilePersister(IBackgroundTaskRunner runner, content content, ProfilingLogger logger) @@ -150,21 +149,11 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache // if running (because of shutdown) this will have no effect // else it tells the runner it is time to run the task - _latch.Set(); + Release(); } } - public WaitHandle Latch - { - get { return _latch.WaitHandle; } - } - - public bool IsLatched - { - get { return true; } - } - - public async Task RunAsync(CancellationToken token) + public override async Task RunAsync(CancellationToken token) { lock (_locko) { @@ -189,15 +178,12 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache } } - public bool IsAsync + public override bool IsAsync { get { return true; } } - public void Dispose() - { } - - public void Run() + public override void Run() { lock (_locko) { diff --git a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs index 3cac89c8de..388012930b 100644 --- a/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs +++ b/src/Umbraco.Web/Scheduling/BackgroundTaskRunner.cs @@ -391,6 +391,7 @@ namespace Umbraco.Web.Scheduling // still latched & not running on shutdown = stop here if (dbgTask.IsLatched && dbgTask.RunsOnShutdown == false) { + dbgTask.Dispose(); // will not run TaskSourceCompleted(taskSource, token); return; } @@ -448,7 +449,7 @@ namespace Umbraco.Web.Scheduling try { - using (bgTask) // ensure it's disposed + try { if (bgTask.IsAsync) //configure await = false since we don't care about the context, we're on a background thread. @@ -456,6 +457,12 @@ namespace Umbraco.Web.Scheduling else bgTask.Run(); } + finally // ensure we disposed - unless latched (again) + { + var lbgTask = bgTask as ILatchedBackgroundTask; + if (lbgTask == null || lbgTask.IsLatched == false) + bgTask.Dispose(); + } } catch (Exception e) { diff --git a/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs b/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs deleted file mode 100644 index af3dedbe70..0000000000 --- a/src/Umbraco.Web/Scheduling/DelayedRecurringTaskBase.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System; -using System.Threading; -using System.Threading.Tasks; - -namespace Umbraco.Web.Scheduling -{ - /// - /// Provides a base class for recurring background tasks. - /// - /// The type of the managed tasks. - internal abstract class DelayedRecurringTaskBase : RecurringTaskBase, ILatchedBackgroundTask - where T : class, IBackgroundTask - { - private readonly ManualResetEventSlim _latch; - private Timer _timer; - - protected DelayedRecurringTaskBase(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds) - : base(runner, periodMilliseconds) - { - if (delayMilliseconds > 0) - { - _latch = new ManualResetEventSlim(false); - _timer = new Timer(_ => - { - _timer.Dispose(); - _timer = null; - _latch.Set(); - }); - _timer.Change(delayMilliseconds, 0); - } - } - - protected DelayedRecurringTaskBase(DelayedRecurringTaskBase source) - : base(source) - { - // no latch on recurring instances - _latch = null; - } - - public override void Run() - { - if (_latch != null) - _latch.Dispose(); - base.Run(); - } - - public override async Task RunAsync(CancellationToken token) - { - if (_latch != null) - _latch.Dispose(); - await base.RunAsync(token); - } - - public WaitHandle Latch - { - get - { - if (_latch == null) - throw new InvalidOperationException("The task is not latched."); - return _latch.WaitHandle; - } - } - - public bool IsLatched - { - get { return _latch != null && _latch.IsSet == false; } - } - - public virtual bool RunsOnShutdown - { - get { return true; } - } - } -} diff --git a/src/Umbraco.Web/Scheduling/KeepAlive.cs b/src/Umbraco.Web/Scheduling/KeepAlive.cs index a47f8aa72c..d5ffbc811d 100644 --- a/src/Umbraco.Web/Scheduling/KeepAlive.cs +++ b/src/Umbraco.Web/Scheduling/KeepAlive.cs @@ -1,41 +1,76 @@ using System; using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; using Umbraco.Core; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; namespace Umbraco.Web.Scheduling { - internal class KeepAlive + internal class KeepAlive : RecurringTaskBase { - public static void Start(ApplicationContext appContext, IUmbracoSettingsSection settings) + private readonly ApplicationContext _appContext; + + public KeepAlive(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, + ApplicationContext appContext) + : base(runner, delayMilliseconds, periodMilliseconds) { - using (DisposableTimer.DebugDuration(() => "Keep alive executing", () => "Keep alive complete")) + _appContext = appContext; + } + + public override bool PerformRun() + { + throw new NotImplementedException(); + } + + public override async Task PerformRunAsync(CancellationToken token) + { + if (_appContext == null) return true; // repeat... + + string umbracoAppUrl = null; + + try { - var umbracoAppUrl = appContext.UmbracoApplicationUrl; - if (umbracoAppUrl.IsNullOrWhiteSpace()) + using (DisposableTimer.DebugDuration(() => "Keep alive executing", () => "Keep alive complete")) { - LogHelper.Warn("No url for service (yet), skip."); - return; - } - - var url = umbracoAppUrl + "/ping.aspx"; - - try - { - using (var wc = new WebClient()) + umbracoAppUrl = _appContext.UmbracoApplicationUrl; + if (umbracoAppUrl.IsNullOrWhiteSpace()) { - wc.DownloadString(url); + LogHelper.Warn("No url for service (yet), skip."); + return true; // repeat + } + + var url = umbracoAppUrl + "/ping.aspx"; + using (var wc = new HttpClient()) + { + var request = new HttpRequestMessage() + { + RequestUri = new Uri(url), + Method = HttpMethod.Get + }; + + var result = await wc.SendAsync(request, token); } } - catch (Exception ee) - { - LogHelper.Error( - string.Format("Error in ping. The base url used in the request was: {0}, see http://our.umbraco.org/documentation/Using-Umbraco/Config-files/umbracoSettings/#ScheduledTasks documentation for details on setting a baseUrl if this is in error", umbracoAppUrl) - , ee); - } } - + catch (Exception e) + { + LogHelper.Error(string.Format("Failed (at \"{0}\").", umbracoAppUrl), e); + } + + return true; // repeat + } + + public override bool IsAsync + { + get { return true; } + } + + public override bool RunsOnShutdown + { + get { return false; } } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/LatchedBackgroundTaskBase.cs b/src/Umbraco.Web/Scheduling/LatchedBackgroundTaskBase.cs new file mode 100644 index 0000000000..c024382ee8 --- /dev/null +++ b/src/Umbraco.Web/Scheduling/LatchedBackgroundTaskBase.cs @@ -0,0 +1,77 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Umbraco.Web.Scheduling +{ + internal abstract class LatchedBackgroundTaskBase : ILatchedBackgroundTask + { + private readonly ManualResetEventSlim _latch; + private bool _disposed; + + protected LatchedBackgroundTaskBase() + { + _latch = new ManualResetEventSlim(false); + } + + /// + /// Implements IBackgroundTask.Run(). + /// + public abstract void Run(); + + /// + /// Implements IBackgroundTask.RunAsync(). + /// + public abstract Task RunAsync(CancellationToken token); + + /// + /// Indicates whether the background task can run asynchronously. + /// + public abstract bool IsAsync { get; } + + public WaitHandle Latch + { + get { return _latch.WaitHandle; } + } + + public bool IsLatched + { + get { return _latch.IsSet == false; } + } + + protected void Release() + { + _latch.Set(); + } + + protected void Reset() + { + _latch.Reset(); + } + + public abstract bool RunsOnShutdown { get; } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + // the task is going to be disposed again after execution, + // unless it is latched again, thus indicating it wants to + // remain active + + protected virtual void Dispose(bool disposing) + { + // lock on _latch instead of creating a new object as _timer is + // private, non-null, readonly - so safe here + lock (_latch) + { + if (_disposed) return; + _disposed = true; + + _latch.Dispose(); + } + } + } +} diff --git a/src/Umbraco.Web/Scheduling/LogScrubber.cs b/src/Umbraco.Web/Scheduling/LogScrubber.cs index 14f534653d..f920b5bb9d 100644 --- a/src/Umbraco.Web/Scheduling/LogScrubber.cs +++ b/src/Umbraco.Web/Scheduling/LogScrubber.cs @@ -7,15 +7,16 @@ using umbraco.BusinessLogic; using Umbraco.Core; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; +using Umbraco.Core.Sync; namespace Umbraco.Web.Scheduling { - internal class LogScrubber : DelayedRecurringTaskBase + internal class LogScrubber : RecurringTaskBase { private readonly ApplicationContext _appContext; private readonly IUmbracoSettingsSection _settings; - public LogScrubber(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, + public LogScrubber(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, ApplicationContext appContext, IUmbracoSettingsSection settings) : base(runner, delayMilliseconds, periodMilliseconds) { @@ -23,20 +24,8 @@ namespace Umbraco.Web.Scheduling _settings = settings; } - public LogScrubber(LogScrubber source) - : base(source) - { - _appContext = source._appContext; - _settings = source._settings; - } - - protected override LogScrubber GetRecurring() - { - return new LogScrubber(this); - } - // maximum age, in minutes - private int GetLogScrubbingMaximumAge(IUmbracoSettingsSection settings) + private static int GetLogScrubbingMaximumAge(IUmbracoSettingsSection settings) { var maximumAge = 24 * 60; // 24 hours, in minutes try @@ -46,7 +35,7 @@ namespace Umbraco.Web.Scheduling } catch (Exception e) { - LogHelper.Error("Unable to locate a log scrubbing maximum age. Defaulting to 24 hours.", e); + LogHelper.Error("Unable to locate a log scrubbing maximum age. Defaulting to 24 hours.", e); } return maximumAge; @@ -67,15 +56,25 @@ namespace Umbraco.Web.Scheduling return interval; } - public override void PerformRun() + public override bool PerformRun() { + if (_appContext == null) return true; // repeat... + + if (ServerEnvironmentHelper.GetStatus(_settings) == CurrentServerEnvironmentStatus.Slave) + { + LogHelper.Debug("Does not run on slave servers."); + return false; // do NOT repeat, server status comes from config and will NOT change + } + using (DisposableTimer.DebugDuration("Log scrubbing executing", "Log scrubbing complete")) { Log.CleanLogs(GetLogScrubbingMaximumAge(_settings)); - } + } + + return true; // repeat } - public override Task PerformRunAsync(CancellationToken token) + public override Task PerformRunAsync(CancellationToken token) { throw new NotImplementedException(); } diff --git a/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs b/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs index dc82795852..3fea70a2b8 100644 --- a/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs +++ b/src/Umbraco.Web/Scheduling/RecurringTaskBase.cs @@ -6,57 +6,51 @@ namespace Umbraco.Web.Scheduling /// /// Provides a base class for recurring background tasks. /// - /// The type of the managed tasks. - internal abstract class RecurringTaskBase : IBackgroundTask - where T : class, IBackgroundTask + internal abstract class RecurringTaskBase : LatchedBackgroundTaskBase { - private readonly IBackgroundTaskRunner _runner; + private readonly IBackgroundTaskRunner _runner; private readonly int _periodMilliseconds; - private Timer _timer; - private T _recurrent; + private readonly Timer _timer; + private bool _disposed; /// - /// Initializes a new instance of the class with a tasks runner and a period. + /// Initializes a new instance of the class. /// /// The task runner. + /// The delay. /// The period. /// The task will repeat itself periodically. Use this constructor to create a new task. - protected RecurringTaskBase(IBackgroundTaskRunner runner, int periodMilliseconds) + protected RecurringTaskBase(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds) { _runner = runner; _periodMilliseconds = periodMilliseconds; - } - /// - /// Initializes a new instance of the class with a source task. - /// - /// The source task. - /// Use this constructor to create a new task from a source task in GetRecurring. - protected RecurringTaskBase(RecurringTaskBase source) - { - _runner = source._runner; - _timer = source._timer; - _periodMilliseconds = source._periodMilliseconds; + // note + // must use the single-parameter constructor on Timer to avoid it from being GC'd + // read http://stackoverflow.com/questions/4962172/why-does-a-system-timers-timer-survive-gc-but-not-system-threading-timer + + _timer = new Timer(_ => Release()); + _timer.Change(delayMilliseconds, 0); } /// /// Implements IBackgroundTask.Run(). /// /// Classes inheriting from RecurringTaskBase must implement PerformRun. - public virtual void Run() + public override void Run() { - PerformRun(); - Repeat(); + var shouldRepeat = PerformRun(); + if (shouldRepeat) Repeat(); } /// /// Implements IBackgroundTask.RunAsync(). /// /// Classes inheriting from RecurringTaskBase must implement PerformRun. - public virtual async Task RunAsync(CancellationToken token) + public override async Task RunAsync(CancellationToken token) { - await PerformRunAsync(token); - Repeat(); + var shouldRepeat = await PerformRunAsync(token); + if (shouldRepeat) Repeat(); } private void Repeat() @@ -64,53 +58,45 @@ namespace Umbraco.Web.Scheduling // again? if (_runner.IsCompleted) return; // fail fast - if (_periodMilliseconds == 0) return; + if (_periodMilliseconds == 0) return; // safe - _recurrent = GetRecurring(); - if (_recurrent == null) - { - _timer.Dispose(); - _timer = null; - return; // done - } + Reset(); // re-latch - // note - // must use the single-parameter constructor on Timer to avoid it from being GC'd - // read http://stackoverflow.com/questions/4962172/why-does-a-system-timers-timer-survive-gc-but-not-system-threading-timer - - _timer = _timer ?? new Timer(_ => _runner.TryAdd(_recurrent)); - _timer.Change(_periodMilliseconds, 0); + // try to add again (may fail if runner has completed) + // if added, re-start the timer, else kill it + if (_runner.TryAdd(this)) + _timer.Change(_periodMilliseconds, 0); + else + Dispose(true); } - /// - /// Indicates whether the background task can run asynchronously. - /// - public abstract bool IsAsync { get; } - /// /// Runs the background task. /// - public abstract void PerformRun(); + /// A value indicating whether to repeat the task. + public abstract bool PerformRun(); /// /// Runs the task asynchronously. /// /// A cancellation token. - /// A instance representing the execution of the background task. - public abstract Task PerformRunAsync(CancellationToken token); + /// A instance representing the execution of the background task, + /// and returning a value indicating whether to repeat the task. + public abstract Task PerformRunAsync(CancellationToken token); - /// - /// Gets a new occurence of the recurring task. - /// - /// A new task instance to be queued, or null to terminate the recurring task. - /// The new task instance must be created via the RecurringTaskBase(RecurringTaskBase{T} source) constructor, - /// where source is the current task, eg: return new MyTask(this); - protected abstract T GetRecurring(); + protected override void Dispose(bool disposing) + { + // lock on _timer instead of creating a new object as _timer is + // private, non-null, readonly - so safe here + lock (_timer) + { + if (_disposed) return; + _disposed = true; - /// - /// Dispose the task. - /// - public virtual void Dispose() - { } + // stop the timer + _timer.Change(Timeout.Infinite, Timeout.Infinite); + _timer.Dispose(); + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs index d1dc8d1935..78a91f6341 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -10,14 +10,12 @@ using Umbraco.Web.Mvc; namespace Umbraco.Web.Scheduling { - internal class ScheduledPublishing : DelayedRecurringTaskBase + internal class ScheduledPublishing : RecurringTaskBase { private readonly ApplicationContext _appContext; private readonly IUmbracoSettingsSection _settings; - private static bool _isPublishingRunning; - - public ScheduledPublishing(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, + public ScheduledPublishing(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, ApplicationContext appContext, IUmbracoSettingsSection settings) : base(runner, delayMilliseconds, periodMilliseconds) { @@ -25,48 +23,34 @@ namespace Umbraco.Web.Scheduling _settings = settings; } - private ScheduledPublishing(ScheduledPublishing source) - : base(source) - { - _appContext = source._appContext; - _settings = source._settings; - } - - protected override ScheduledPublishing GetRecurring() - { - return new ScheduledPublishing(this); - } - - public override void PerformRun() + public override bool PerformRun() { throw new NotImplementedException(); } - public override async Task PerformRunAsync(CancellationToken token) - { - - if (_appContext == null) return; + public override async Task PerformRunAsync(CancellationToken token) + { + if (_appContext == null) return true; // repeat... + if (ServerEnvironmentHelper.GetStatus(_settings) == CurrentServerEnvironmentStatus.Slave) { LogHelper.Debug("Does not run on slave servers."); - return; + return false; // do NOT repeat, server status comes from config and will NOT change } using (DisposableTimer.DebugDuration(() => "Scheduled publishing executing", () => "Scheduled publishing complete")) { - if (_isPublishingRunning) return; - - _isPublishingRunning = true; - - var umbracoAppUrl = _appContext.UmbracoApplicationUrl; - if (umbracoAppUrl.IsNullOrWhiteSpace()) - { - LogHelper.Warn("No url for service (yet), skip."); - return; - } + string umbracoAppUrl = null; try { + umbracoAppUrl = _appContext.UmbracoApplicationUrl; + if (umbracoAppUrl.IsNullOrWhiteSpace()) + { + LogHelper.Warn("No url for service (yet), skip."); + return true; // repeat + } + var url = umbracoAppUrl + "/RestServices/ScheduledPublish/Index"; using (var wc = new HttpClient()) { @@ -79,27 +63,16 @@ namespace Umbraco.Web.Scheduling //pass custom the authorization header request.Headers.Authorization = AdminTokenAuthorizeAttribute.GetAuthenticationHeaderValue(_appContext); - try - { - var result = await wc.SendAsync(request, token); - } - catch (Exception ex) - { - LogHelper.Error("An error occurred calling scheduled publish url", ex); - } + var result = await wc.SendAsync(request, token); } } - catch (Exception ee) + catch (Exception e) { - LogHelper.Error( - string.Format("An error occurred with the scheduled publishing. The base url used in the request was: {0}, see http://our.umbraco.org/documentation/Using-Umbraco/Config-files/umbracoSettings/#ScheduledTasks documentation for details on setting a baseUrl if this is in error", umbracoAppUrl) - , ee); + LogHelper.Error(string.Format("Failed (at \"{0}\").", umbracoAppUrl), e); } - finally - { - _isPublishingRunning = false; - } - } + } + + return true; // repeat } public override bool IsAsync diff --git a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs index 1015b2d4f6..92214e5199 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledTasks.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledTasks.cs @@ -15,14 +15,13 @@ namespace Umbraco.Web.Scheduling // would need to be a publicly available task (URL) which isn't really very good :( // We should really be using the AdminTokenAuthorizeAttribute for this stuff - internal class ScheduledTasks : DelayedRecurringTaskBase + internal class ScheduledTasks : RecurringTaskBase { private readonly ApplicationContext _appContext; private readonly IUmbracoSettingsSection _settings; private static readonly Hashtable ScheduledTaskTimes = new Hashtable(); - private static bool _isPublishingRunning = false; - public ScheduledTasks(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, + public ScheduledTasks(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, ApplicationContext appContext, IUmbracoSettingsSection settings) : base(runner, delayMilliseconds, periodMilliseconds) { @@ -30,18 +29,6 @@ namespace Umbraco.Web.Scheduling _settings = settings; } - public ScheduledTasks(ScheduledTasks source) - : base(source) - { - _appContext = source._appContext; - _settings = source._settings; - } - - protected override ScheduledTasks GetRecurring() - { - return new ScheduledTasks(this); - } - private async Task ProcessTasksAsync(CancellationToken token) { var scheduledTasks = _settings.ScheduledTasks.Tasks; @@ -99,25 +86,23 @@ namespace Umbraco.Web.Scheduling } } - public override void PerformRun() + public override bool PerformRun() { throw new NotImplementedException(); } - public override async Task PerformRunAsync(CancellationToken token) + public override async Task PerformRunAsync(CancellationToken token) { + if (_appContext == null) return true; // repeat... + if (ServerEnvironmentHelper.GetStatus(_settings) == CurrentServerEnvironmentStatus.Slave) { LogHelper.Debug("Does not run on slave servers."); - return; + return false; // do NOT repeat, server status comes from config and will NOT change } using (DisposableTimer.DebugDuration(() => "Scheduled tasks executing", () => "Scheduled tasks complete")) { - if (_isPublishingRunning) return; - - _isPublishingRunning = true; - try { await ProcessTasksAsync(token); @@ -126,11 +111,9 @@ namespace Umbraco.Web.Scheduling { LogHelper.Error("Error executing scheduled task", ee); } - finally - { - _isPublishingRunning = false; - } } + + return true; // repeat } public override bool IsAsync diff --git a/src/Umbraco.Web/Scheduling/Scheduler.cs b/src/Umbraco.Web/Scheduling/Scheduler.cs index fff6261401..f1f48f141a 100644 --- a/src/Umbraco.Web/Scheduling/Scheduler.cs +++ b/src/Umbraco.Web/Scheduling/Scheduler.cs @@ -1,11 +1,7 @@ -using System; -using System.Threading; -using System.Web; +using System.Web; using Umbraco.Core; using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Logging; -using Umbraco.Core.Sync; namespace Umbraco.Web.Scheduling { @@ -18,7 +14,7 @@ namespace Umbraco.Web.Scheduling /// internal sealed class Scheduler : ApplicationEventHandler { - private static Timer _pingTimer; + private static BackgroundTaskRunner _keepAliveRunner; private static BackgroundTaskRunner _publishingRunner; private static BackgroundTaskRunner _tasksRunner; private static BackgroundTaskRunner _scrubberRunner; @@ -48,30 +44,24 @@ namespace Umbraco.Web.Scheduling LogHelper.Debug(() => "Initializing the scheduler"); // backgrounds runners are web aware, if the app domain dies, these tasks will wind down correctly + _keepAliveRunner = new BackgroundTaskRunner("KeepAlive", applicationContext.ProfilingLogger.Logger); _publishingRunner = new BackgroundTaskRunner("ScheduledPublishing", applicationContext.ProfilingLogger.Logger); _tasksRunner = new BackgroundTaskRunner("ScheduledTasks", applicationContext.ProfilingLogger.Logger); _scrubberRunner = new BackgroundTaskRunner("LogScrubber", applicationContext.ProfilingLogger.Logger); var settings = UmbracoConfig.For.UmbracoSettings(); - // note - // must use the single-parameter constructor on Timer to avoid it from being GC'd - // also make the timer static to ensure further GC safety - // read http://stackoverflow.com/questions/4962172/why-does-a-system-timers-timer-survive-gc-but-not-system-threading-timer - - // ping/keepalive - no need for a background runner - does not need to be web aware, ok if the app domain dies - _pingTimer = new Timer(state => KeepAlive.Start(applicationContext, UmbracoConfig.For.UmbracoSettings())); - _pingTimer.Change(60000, 300000); + // ping/keepalive + // on all servers + _keepAliveRunner.Add(new KeepAlive(_keepAliveRunner, 60000, 300000, applicationContext)); // scheduled publishing/unpublishing // install on all, will only run on non-slaves servers - // both are delayed recurring tasks _publishingRunner.Add(new ScheduledPublishing(_publishingRunner, 60000, 60000, applicationContext, settings)); _tasksRunner.Add(new ScheduledTasks(_tasksRunner, 60000, 60000, applicationContext, settings)); // log scrubbing - // install & run on all servers - // LogScrubber is a delayed recurring task + // install on all, will only run on non-slaves servers _scrubberRunner.Add(new LogScrubber(_scrubberRunner, 60000, LogScrubber.GetLogScrubbingInterval(settings), applicationContext, settings)); } } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index f4ff5df589..c4adb2ab23 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -304,6 +304,7 @@ + @@ -558,7 +559,6 @@ - From fce688f47f58844dd186b73026f81282a9eea348 Mon Sep 17 00:00:00 2001 From: Stephan Date: Tue, 7 Jul 2015 19:36:17 +0200 Subject: [PATCH 47/50] DatabaseServerMessenger - local identity is unique appdomain --- .../Sync/BatchedDatabaseServerMessenger.cs | 2 +- .../Sync/DatabaseServerMessenger.cs | 28 +++++++++++-------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Core/Sync/BatchedDatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/BatchedDatabaseServerMessenger.cs index 9eaf557170..b16caa8779 100644 --- a/src/Umbraco.Core/Sync/BatchedDatabaseServerMessenger.cs +++ b/src/Umbraco.Core/Sync/BatchedDatabaseServerMessenger.cs @@ -40,7 +40,7 @@ namespace Umbraco.Core.Sync { UtcStamp = DateTime.UtcNow, Instructions = JsonConvert.SerializeObject(instructions, Formatting.None), - OriginIdentity = GetLocalIdentity() + OriginIdentity = LocalIdentity }; ApplicationContext.DatabaseContext.Database.Insert(dto); diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs index e6ce565280..e9be30ab09 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerMessenger.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; @@ -78,7 +79,7 @@ namespace Umbraco.Core.Sync { UtcStamp = DateTime.UtcNow, Instructions = JsonConvert.SerializeObject(instructions, Formatting.None), - OriginIdentity = GetLocalIdentity() + OriginIdentity = LocalIdentity }; ApplicationContext.DatabaseContext.Database.Insert(dto); @@ -186,7 +187,7 @@ namespace Umbraco.Core.Sync // only process instructions coming from a remote server, and ignore instructions coming from // the local server as they've already been processed. We should NOT assume that the sequence of // instructions in the database makes any sense whatsoever, because it's all async. - var localIdentity = GetLocalIdentity(); + var localIdentity = LocalIdentity; var lastId = 0; foreach (var dto in dtos) @@ -269,17 +270,20 @@ namespace Umbraco.Core.Sync } /// - /// Gets the local server unique identity. + /// Gets the unique local identity of the executing AppDomain. /// - /// The unique identity of the local server. - protected string GetLocalIdentity() - { - return JsonConvert.SerializeObject(new - { - machineName = NetworkHelper.MachineName, - appDomainAppId = HttpRuntime.AppDomainAppId - }); - } + /// + /// It is not only about the "server" (machine name and appDomainappId), but also about + /// an AppDomain, within a Process, on that server - because two AppDomains running at the same + /// time on the same server (eg during a restart) are, practically, a LB setup. + /// Practically, all we really need is the guid, the other infos are here for information + /// and debugging purposes. + /// + protected readonly static string LocalIdentity = NetworkHelper.MachineName // eg DOMAIN\SERVER + + "/" + HttpRuntime.AppDomainAppId // eg /LM/S3SVC/11/ROOT + + " [P" + Process.GetCurrentProcess().Id // eg 1234 + + "/D" + AppDomain.CurrentDomain.Id // eg 22 + + "] " + Guid.NewGuid().ToString("N").ToUpper(); // make it truly unique /// /// Gets the sync file path for the local server. From c9489bb912550a80a9f9deb8978b33654584240c Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 8 Jul 2015 14:41:59 +0200 Subject: [PATCH 48/50] DatabaseServerMessenger - default instructions retention is 2 days --- src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs b/src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs index 66b845f4ec..f1bebce10b 100644 --- a/src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs +++ b/src/Umbraco.Core/Sync/DatabaseServerMessengerOptions.cs @@ -13,7 +13,7 @@ namespace Umbraco.Core.Sync /// public DatabaseServerMessengerOptions() { - DaysToRetainInstructions = 100; // 100 days + DaysToRetainInstructions = 2; // 2 days ThrottleSeconds = 5; // 5 seconds } From 993a5bbc9293d57308e7b91b400c5a8f71091a8e Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 8 Jul 2015 16:28:35 +0200 Subject: [PATCH 49/50] Resolution - better logging when freezing --- .../ObjectResolution/Resolution.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/ObjectResolution/Resolution.cs b/src/Umbraco.Core/ObjectResolution/Resolution.cs index 87eb06e295..0b478d54cf 100644 --- a/src/Umbraco.Core/ObjectResolution/Resolution.cs +++ b/src/Umbraco.Core/ObjectResolution/Resolution.cs @@ -112,7 +112,7 @@ namespace Umbraco.Core.ObjectResolution /// resolution is already frozen. public static void Freeze() { - LogHelper.Debug(typeof(Resolution), "Freezing resolution"); + LogHelper.Debug(typeof (Resolution), "Freezing resolution"); using (new WriteLock(ConfigurationLock)) { @@ -121,9 +121,20 @@ namespace Umbraco.Core.ObjectResolution _isFrozen = true; } - - if (Frozen != null) - Frozen(null, null); + + LogHelper.Debug(typeof(Resolution), "Resolution is frozen"); + + if (Frozen == null) return; + + try + { + Frozen(null, null); + } + catch (Exception e) + { + LogHelper.Error(typeof (Resolution), "Exception in Frozen event handler.", e); + throw; + } } /// From 97533e7c3d91b7c579616962106faa0ec3ff9258 Mon Sep 17 00:00:00 2001 From: Stephan Date: Wed, 8 Jul 2015 21:28:59 +0200 Subject: [PATCH 50/50] UmbracoApplication - log unhandled exceptions --- src/Umbraco.Core/UmbracoApplicationBase.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Umbraco.Core/UmbracoApplicationBase.cs b/src/Umbraco.Core/UmbracoApplicationBase.cs index b98e3935ee..33c3d58689 100644 --- a/src/Umbraco.Core/UmbracoApplicationBase.cs +++ b/src/Umbraco.Core/UmbracoApplicationBase.cs @@ -32,6 +32,19 @@ namespace Umbraco.Core /// internal void StartApplication(object sender, EventArgs e) { + //take care of unhandled exceptions - there is nothing we can do to + // prevent the entire w3wp process to go down but at least we can try + // and log the exception + AppDomain.CurrentDomain.UnhandledException += (_, args) => + { + var exception = (Exception) args.ExceptionObject; + var isTerminating = args.IsTerminating; // always true? + + var msg = "Unhandled exception in AppDomain"; + if (isTerminating) msg += " (terminating)"; + Logger.Error(typeof(UmbracoApplicationBase), msg, exception); + }; + //boot up the application GetBootManager() .Initialize() @@ -150,6 +163,7 @@ namespace Umbraco.Core { get { + // LoggerResolver can resolve before resolution is frozen if (LoggerResolver.HasCurrent && LoggerResolver.Current.HasValue) { return LoggerResolver.Current.Logger;