diff --git a/Rock.Update/App.config b/Rock.Update/App.config new file mode 100644 index 00000000000..f6baa235b28 --- /dev/null +++ b/Rock.Update/App.config @@ -0,0 +1,61 @@ + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Rock.Update/Enums/DotNetVersionCheckResult.cs b/Rock.Update/Enums/DotNetVersionCheckResult.cs new file mode 100644 index 00000000000..6eb90446d89 --- /dev/null +++ b/Rock.Update/Enums/DotNetVersionCheckResult.cs @@ -0,0 +1,39 @@ +// +// Copyright by the Spark Development Network +// +// Licensed under the Rock Community License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.rockrms.com/license +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Rock.Update.Enum +{ + /// + /// Represents the possible results of a version check. + /// + public enum DotNetVersionCheckResult + { + /// + /// The version check definitely fails + /// + Fail = 0, + + /// + /// This version check definitely passes + /// + Pass = 1, + + /// + /// The version check could not determine pass or fail so proceed at own risk. + /// + Unknown = 2 + } +} diff --git a/Rock.Update/Exceptions/PackageNotFoundException.cs b/Rock.Update/Exceptions/PackageNotFoundException.cs new file mode 100644 index 00000000000..8926ff979e6 --- /dev/null +++ b/Rock.Update/Exceptions/PackageNotFoundException.cs @@ -0,0 +1,35 @@ +// +// Copyright by the Spark Development Network +// +// Licensed under the Rock Community License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.rockrms.com/license +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; + +namespace Rock.Update.Exceptions +{ + /// + /// The exception that is thrown if the package for the requested version is not found. + /// + /// + public class PackageNotFoundException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public PackageNotFoundException( string message ) : base( message ) + { + } + } +} diff --git a/Rock.Update/Exceptions/VersionValidationException.cs b/Rock.Update/Exceptions/VersionValidationException.cs new file mode 100644 index 00000000000..c3ab8a673f9 --- /dev/null +++ b/Rock.Update/Exceptions/VersionValidationException.cs @@ -0,0 +1,35 @@ +// +// Copyright by the Spark Development Network +// +// Licensed under the Rock Community License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.rockrms.com/license +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; + +namespace Rock.Update.Exceptions +{ + /// + /// + /// + /// + public class VersionValidationException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// The message that describes the error. + public VersionValidationException( string message ) : base( message ) + { + } + } +} diff --git a/Rock.Update/Helpers/FileManagementHelper.cs b/Rock.Update/Helpers/FileManagementHelper.cs new file mode 100644 index 00000000000..a58e607aadb --- /dev/null +++ b/Rock.Update/Helpers/FileManagementHelper.cs @@ -0,0 +1,155 @@ +// +// Copyright by the Spark Development Network +// +// Licensed under the Rock Community License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.rockrms.com/license +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.IO; +using System.Web.Hosting; +using Rock.Model; + +namespace Rock.Update.Helpers +{ + /// + /// Helper methods for accessing the file system. + /// + public static class FileManagementHelper + { + /// + /// The root physical file system path of the web application. + /// + public static readonly string ROOT_PATH = HostingEnvironment.MapPath( "~/" ) ?? AppDomain.CurrentDomain.BaseDirectory; + private static readonly string DELETE_FILE_EXTENSION = "rdelete"; + + /// + /// Tries the delete. + /// + /// + /// This method will always log any exception that occurs even if the exception isn't thrown. + /// + /// The file path. + /// if set to true [should bubble exception]. + public static void TryDelete( string filePath, bool shouldBubbleException ) + { + TryDelete( filePath, ( ex ) => ExceptionLogService.LogException( ex ), shouldBubbleException ); + } + + /// + /// Tries the delete. + /// + /// + /// Will not log the exception by default. + /// + /// The file path. + /// The catch method. + /// if set to true [should bubble exception]. + public static void TryDelete( string filePath, Action catchMethod, bool shouldBubbleException ) + { + try + { + File.Delete( filePath ); + } + catch ( Exception ex ) + { + catchMethod( ex ); + if ( shouldBubbleException ) + { + throw; + } + } + } + + /// + /// Deletes the or rename. + /// + /// The file path. + public static void DeleteOrRename( string filepath ) + { + if ( File.Exists( filepath ) ) + { + TryDelete( filepath, ( ex ) => RenameFile( filepath ), false ); + } + } + + /// + /// Renames the file. + /// + /// The physical file. + public static void RenameFile( string physicalFile ) + { + if ( File.Exists( physicalFile ) ) + { + File.Move( physicalFile, GetRenameFileName( physicalFile ) ); + } + } + + /// + /// Renames the active file. + /// + /// The file path to rename. + public static void RenameActiveFile( string filepathToRename ) + { + bool dllFileNotInBin = filepathToRename.EndsWith( ".dll" ) && !filepathToRename.Contains( @"\bin\" ); + bool roslynAssembly = ( filepathToRename.EndsWith( ".dll" ) || filepathToRename.EndsWith( ".exe" ) ) && filepathToRename.Contains( @"\roslyn\" ); + + // If this a roslyn assembly or a dll file from the Content files, rename it so that we don't have problems with it being locks + if ( roslynAssembly || dllFileNotInBin ) + { + string physicalFile; + if ( roslynAssembly ) + { + physicalFile = filepathToRename; + } + else + { + physicalFile = Path.Combine( ROOT_PATH, filepathToRename ); + } + + RenameFile( physicalFile ); + } + } + + /// + /// Cleans up deleted files. + /// + public static void CleanUpDeletedFiles() + { + var filesToDelete = Directory.GetFiles( ROOT_PATH, $"*.{DELETE_FILE_EXTENSION}", SearchOption.AllDirectories ); + foreach ( var file in filesToDelete ) + { + FileManagementHelper.TryDelete( file, false ); + } + } + + /// + /// Gets the name of the rename file. + /// + /// The physical file. + /// + private static string GetRenameFileName( string physicalFile ) + { + var fileCount = 1; + var fileToDelete = $"{physicalFile}.{fileCount}.{DELETE_FILE_EXTENSION}"; + + // generate a unique *.#.rdelete filename + while ( File.Exists( fileToDelete ) ) + { + fileCount++; + fileToDelete = $"{physicalFile}.{fileCount}.{DELETE_FILE_EXTENSION}"; + } + + return fileToDelete; + } + } +} diff --git a/Rock.Update/Helpers/OfflinePageHelper.cs b/Rock.Update/Helpers/OfflinePageHelper.cs new file mode 100644 index 00000000000..212191fe6e2 --- /dev/null +++ b/Rock.Update/Helpers/OfflinePageHelper.cs @@ -0,0 +1,90 @@ +// +// Copyright by the Spark Development Network +// +// Licensed under the Rock Community License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.rockrms.com/license +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.IO; + +namespace Rock.Update.Helpers +{ + /// + /// A static class for managing the offline page used during the install process. + /// + public static class OfflinePageHelper + { + private static readonly string OFFLINE_TEMPLATE_NAME = "app_offline-template.htm"; + private static readonly string OFFLINE_FILE_NAME = "app_offline.htm"; + + /// + /// Removes the app_offline.htm file so the app can be used again. + /// + public static void RemoveOfflinePage() + { + var file = Path.Combine( FileManagementHelper.ROOT_PATH, OFFLINE_FILE_NAME ); + if ( File.Exists( file ) ) + { + File.Delete( file ); + } + } + + /// + /// Copies the app_offline-template.htm file to app_offline.htm so no one else can hit the app. + /// If the template file does not exist an app_offline.htm file will be created from scratch. + /// + public static void CreateOfflinePage() + { + var templateFile = Path.Combine( FileManagementHelper.ROOT_PATH, OFFLINE_TEMPLATE_NAME ); + var offlineFile = Path.Combine( FileManagementHelper.ROOT_PATH, OFFLINE_FILE_NAME ); + + try + { + if ( File.Exists( templateFile ) ) + { + File.Copy( templateFile, offlineFile, overwrite: true ); + } + else + { + CreateOfflinePageFromScratch( offlineFile ); + } + } + catch ( Exception ) + { + if ( !File.Exists( offlineFile ) ) + { + CreateOfflinePageFromScratch( offlineFile ); + } + } + } + + /// + /// Simply creates an app_offline.htm file so no one else can hit the app. + /// + private static void CreateOfflinePageFromScratch( string offlineFile ) + { + var offlinePage = @" + + + Application Updating... + + +

One Moment Please

+ This application is undergoing an essential update and is temporarily offline. Please give me a minute or two to wrap things up. + + + "; + File.WriteAllText( offlineFile, offlinePage ); + } + } +} diff --git a/Rock.Update/Helpers/RockUpdateHelper.cs b/Rock.Update/Helpers/RockUpdateHelper.cs new file mode 100644 index 00000000000..dee5c33dda4 --- /dev/null +++ b/Rock.Update/Helpers/RockUpdateHelper.cs @@ -0,0 +1,175 @@ +// +// Copyright by the Spark Development Network +// +// Licensed under the Rock Community License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.rockrms.com/license +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Data.Entity; +using System.Linq; +using Microsoft.Win32; +using Rock.Data; +using Rock.Model; +using Rock.Update.Enum; +using Rock.Web.Cache; + +namespace Rock.Update.Helpers +{ + /* + 02/18/2021 - MSB + Once the new Rock Updater is completely deployed and active this file should be obsoleted and + the RockUpdateHelper in the Rock.dll should be used. + + Reason: Easily Deployable Rock Updater. + */ + + /// + /// Helper class for collecting environmental information about Rock install that is saved during update + /// + public static class RockUpdateHelper + { + private const int DOT_NET_4_7_2_RELEASE_NUMBER = 461808; + + /// + /// Returns the environment data as json. + /// + /// a JSON formatted string + public static string GetEnvDataAsJson( System.Web.HttpRequest request, string rockUrl ) + { + var envData = new Dictionary(); + envData.Add( "AppRoot", rockUrl ); + envData.Add( "Architecture", ( IntPtr.Size == 4 ) ? "32bit" : "64bit" ); + envData.Add( "AspNetVersion", GetDotNetVersion() ); + envData.Add( "IisVersion", request.ServerVariables["SERVER_SOFTWARE"] ); + envData.Add( "ServerOs", Environment.OSVersion.ToString() ); + + try + { + envData.Add( "SqlVersion", DbService.ExecuteScaler( "SELECT SERVERPROPERTY('productversion')" ).ToString() ); + } + catch + { + // Intentionally ignored. + } + + try + { + using ( var rockContext = new RockContext() ) + { + var entityType = EntityTypeCache.Get( "Rock.Security.BackgroundCheck.ProtectMyMinistry", false, rockContext ); + if ( entityType != null ) + { + var pmmUserName = new AttributeValueService( rockContext ) + .Queryable().AsNoTracking() + .Where( v => + v.Attribute.EntityTypeId.HasValue && + v.Attribute.EntityTypeId.Value == entityType.Id && + v.Attribute.Key == "UserName" ) + .Select( v => v.Value ) + .FirstOrDefault(); + if ( !string.IsNullOrWhiteSpace( pmmUserName ) ) + { + envData.Add( "PMMUserName", pmmUserName ); + } + } + } + } + catch + { + // Intentionally ignored. + } + + return envData.ToJson(); + } + + /// + /// Suggested approach to check which version of the .Net framework is installed when using version 4.5 or later + /// as per https://msdn.microsoft.com/en-us/library/hh925568(v=vs.110).aspx. + /// + /// a string containing the human readable version of the .Net framework + public static DotNetVersionCheckResult CheckDotNetVersionFromRegistry() + { + // Check if Release is >= 461808 (4.7.2) + if ( GetDotNetReleaseNumber() >= DOT_NET_4_7_2_RELEASE_NUMBER ) + { + return DotNetVersionCheckResult.Pass; + } + else + { + return DotNetVersionCheckResult.Fail; + } + } + + /// + /// Gets the dot net release number from the registry. + /// + /// + public static int GetDotNetReleaseNumber() + { + const string SUB_KEY = @"SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full\"; + using ( RegistryKey ndpKey = RegistryKey.OpenBaseKey( RegistryHive.LocalMachine, RegistryView.Registry32 ).OpenSubKey( SUB_KEY ) ) + { + if ( ndpKey != null && ndpKey.GetValue( "Release" ) != null ) + { + return ( int ) ndpKey.GetValue( "Release" ); + } + else + { + return 0; + } + } + } + + /// + /// Gets the friendly string of the dot net version. + /// + /// + public static string GetDotNetVersion() + { + return GetDotNetVersion( GetDotNetReleaseNumber() ); + } + + /// + /// Gets the dot net version. + /// + /// The release number. + /// + public static string GetDotNetVersion( int releaseNumber ) + { + var dotNetReleaseNumberVersionMap = new Dictionary + { + { 528040, ".NET Framework 4.8" }, + { 461808, ".NET Framework 4.7.2" }, + { 461308, ".NET Framework 4.7.1" }, + { 460798, ".NET Framework 4.7" }, + { 394802, ".NET Framework 4.6.2" }, + { 394254, ".NET Framework 4.6.1" }, + { 393295, ".NET Framework 4.6" }, + { 379893, ".NET Framework 4.5.2" }, + { 378675, ".NET Framework 4.5.1" }, + { 378389, ".NET Framework 4.5" }, + }; + + foreach ( var key in dotNetReleaseNumberVersionMap.Keys ) + { + if ( releaseNumber >= key ) + { + return dotNetReleaseNumberVersionMap[key]; + } + } + + return "Unknown"; + } + } +} diff --git a/Rock.Update/Helpers/VersionValidationHelper.cs b/Rock.Update/Helpers/VersionValidationHelper.cs new file mode 100644 index 00000000000..95a0bff1ae8 --- /dev/null +++ b/Rock.Update/Helpers/VersionValidationHelper.cs @@ -0,0 +1,148 @@ +// +// Copyright by the Spark Development Network +// +// Licensed under the Rock Community License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.rockrms.com/license +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using Rock.Data; +using Rock.Update.Enum; +using Rock.Update.Exceptions; + +namespace Rock.Update.Helpers +{ + /// + /// This class is use to validate the a specific rock version can be installed. + /// + public static class VersionValidationHelper + { + /// + /// Checks the .NET Framework version and returns Pass, Fail, or Unknown which can be + /// used to determine if it's safe to proceed. + /// + /// One of the values of the VersionCheckResult enum. + public static DotNetVersionCheckResult CheckFrameworkVersion() + { + var result = DotNetVersionCheckResult.Fail; + try + { + // Once we get to 4.5 Microsoft recommends we test via the Registry... + result = RockUpdateHelper.CheckDotNetVersionFromRegistry(); + } + catch + { + // This would be pretty bad, but regardless we'll return + // the Unknown result and let the caller proceed with caution. + result = DotNetVersionCheckResult.Unknown; + } + + return result; + } + + /// + /// Determines whether this instance [can install version] the specified target version. + /// + /// The target version. + /// + /// true if this instance [can install version] the specified target version; otherwise, false. + /// + public static bool CanInstallVersion( Version targetVersion ) + { + try + { + ValidateVersionInstall( targetVersion ); + } + catch + { + return false; + } + + return true; + } + + /// + /// Checks the SQL server version and returns false if not at the needed + /// level to proceed. + /// + /// + public static bool CheckSqlServerVersionGreaterThenSqlServer2012() + { + var isOk = false; + var sqlVersion = string.Empty; + + try + { + sqlVersion = DbService.ExecuteScaler( "SELECT SERVERPROPERTY('productversion')" ).ToString(); + var versionParts = sqlVersion.Split( '.' ); + + int.TryParse( versionParts[0], out var majorVersion ); + + if ( majorVersion > 11 ) + { + isOk = true; + } + } + catch + { + // This would be pretty bad, but regardless we'll just + // return the isOk (not) and let the caller proceed. + } + + return isOk; + } + + /// + /// Validates the version install. + /// + /// The target version. + /// + /// Version {targetVersion} requires .Net 4.7.2 or greater. + /// or + /// Version {targetVersion} requires Microsoft Sql Server 2012 or greater. + /// + public static void ValidateVersionInstall( Version targetVersion ) + { + var requiresSqlServer14OrHigher = targetVersion.Major > 1 || targetVersion.Minor > 10; + var requiresNet472 = targetVersion.Major > 1 || targetVersion.Minor > 12; + + if ( !requiresSqlServer14OrHigher ) + { + return; + } + + var hasSqlServer2012OrGreater = CheckSqlServerVersionGreaterThenSqlServer2012(); + if ( requiresNet472 ) + { + var result = CheckFrameworkVersion(); + if ( result == DotNetVersionCheckResult.Fail ) + { + throw new VersionValidationException( $"Version {targetVersion} requires .Net 4.7.2 or greater." ); + } + } + + if ( !hasSqlServer2012OrGreater ) + { + throw new VersionValidationException( $"Version {targetVersion} requires Microsoft Sql Server 2012 or greater." ); + } + } + + /// + /// Gets the installed version. + /// + /// + public static Version GetInstalledVersion() + { + return new Version( VersionInfo.VersionInfo.GetRockSemanticVersionNumber() ); + } + } +} diff --git a/Rock.Update/Interfaces/IRockImpactService.cs b/Rock.Update/Interfaces/IRockImpactService.cs new file mode 100644 index 00000000000..a01d7f354bd --- /dev/null +++ b/Rock.Update/Interfaces/IRockImpactService.cs @@ -0,0 +1,32 @@ +// +// Copyright by the Spark Development Network +// +// Licensed under the Rock Community License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.rockrms.com/license +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using Rock.Update.Models; + +namespace Rock.Update.Interfaces +{ + /// + /// This interface is implemented by RockImpactService and is used so we can mock the service for testing. + /// + public interface IRockImpactService + { + /// + /// Sends the impact statistics to spark. + /// + /// The impact statistic. + void SendImpactStatisticsToSpark( ImpactStatistic impactStatistic ); + } +} \ No newline at end of file diff --git a/Rock.Update/Interfaces/IRockUpdateService.cs b/Rock.Update/Interfaces/IRockUpdateService.cs new file mode 100644 index 00000000000..76e188a4b98 --- /dev/null +++ b/Rock.Update/Interfaces/IRockUpdateService.cs @@ -0,0 +1,55 @@ +// +// Copyright by the Spark Development Network +// +// Licensed under the Rock Community License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.rockrms.com/license +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using Rock.Update.Models; + +namespace Rock.Update.Interfaces +{ + /// + /// This interface is implemented by RockUpdateService and is used so we can mock the service for testing. + /// + public interface IRockUpdateService + { + /// + /// Gets the releases list. + /// + /// The version. + /// + List GetReleasesList( Version version ); + + /// + /// Gets the rock early access request URL. + /// + /// + string GetRockEarlyAccessRequestUrl(); + + /// + /// Gets the rock release program. + /// + /// + RockReleaseProgram GetRockReleaseProgram(); + + /// + /// Determines whether [is early access instance]. + /// + /// + /// true if [is early access instance]; otherwise, false. + /// + bool IsEarlyAccessInstance(); + } +} \ No newline at end of file diff --git a/Rock.Update/Models/ImpactLocation.cs b/Rock.Update/Models/ImpactLocation.cs new file mode 100644 index 00000000000..37d8bbe85ae --- /dev/null +++ b/Rock.Update/Models/ImpactLocation.cs @@ -0,0 +1,89 @@ +// +// Copyright by the Spark Development Network +// +// Licensed under the Rock Community License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.rockrms.com/license +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; + +namespace Rock.Update.Models +{ + /// + /// The location of the organization which is passed to the spark site. + /// + [Serializable] + public class ImpactLocation + { + /// + /// Gets or sets the street1. + /// + /// + /// The street1. + /// + public string Street1 { get; set; } + + /// + /// Gets or sets the street2. + /// + /// + /// The street2. + /// + public string Street2 { get; set; } + + /// + /// Gets or sets the city. + /// + /// + /// The city. + /// + public string City { get; set; } + + /// + /// Gets or sets the state. + /// + /// + /// The state. + /// + public string State { get; set; } + + /// + /// Gets or sets the postal code. + /// + /// + /// The postal code. + /// + public string PostalCode { get; set; } + + /// + /// Gets or sets the country. + /// + /// + /// The country. + /// + public string Country { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The location. + public ImpactLocation( Rock.Model.Location location ) + { + Street1 = location.Street1; + Street2 = location.Street2; + City = location.City; + State = location.State; + PostalCode = location.PostalCode; + Country = location.Country; + } + } +} diff --git a/Rock.Update/Models/ImpactStatistic.cs b/Rock.Update/Models/ImpactStatistic.cs new file mode 100644 index 00000000000..eb857215f59 --- /dev/null +++ b/Rock.Update/Models/ImpactStatistic.cs @@ -0,0 +1,91 @@ +// +// Copyright by the Spark Development Network +// +// Licensed under the Rock Community License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.rockrms.com/license +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; + +namespace Rock.Update.Models +{ + /// + /// Class used to send statics to the Rock Site. + /// + [Serializable] + public class ImpactStatistic + { + /// + /// Gets or sets the rock instance identifier. + /// + /// + /// The rock instance identifier. + /// + public Guid RockInstanceId { get; set; } + + /// + /// Gets or sets the version. + /// + /// + /// The version. + /// + public string Version { get; set; } + + /// + /// Gets or sets the ip address. + /// + /// + /// The ip address. + /// + public string IpAddress { get; set; } + + /// + /// Gets or sets the public URL. + /// + /// + /// The public URL. + /// + public string PublicUrl { get; set; } + + /// + /// Gets or sets the name of the organization. + /// + /// + /// The name of the organization. + /// + public string OrganizationName { get; set; } + + /// + /// Gets or sets the organization location. + /// + /// + /// The organization location. + /// + public ImpactLocation OrganizationLocation { get; set; } + + /// + /// Gets or sets the number of active records. + /// + /// + /// The number of active records. + /// + public int NumberOfActiveRecords { get; set; } + + /// + /// Gets or sets the environment data. + /// + /// + /// The environment data. + /// + public string EnvironmentData { get; set; } + } +} diff --git a/Rock.Update/Models/RockRelease.cs b/Rock.Update/Models/RockRelease.cs new file mode 100644 index 00000000000..ca96a56cba6 --- /dev/null +++ b/Rock.Update/Models/RockRelease.cs @@ -0,0 +1,115 @@ +// +// Copyright by the Spark Development Network +// +// Licensed under the Rock Community License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.rockrms.com/license +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; + +namespace Rock.Update.Models +{ + /// + /// Represents the bits the Rock system stores regarding a particular release. + /// + [Serializable] + public class RockRelease + { + /// + /// Gets or sets the semantic version. + /// + /// + /// The semantic version. + /// + public string SemanticVersion { get; set; } + + /// + /// Gets or sets the version. + /// + /// + /// The version. + /// + public string Version { get; set; } + + /// + /// Gets or sets the release date. + /// + /// + /// The release date. + /// + public DateTime? ReleaseDate { get; set; } + + /// + /// Gets or sets the summary. + /// + /// + /// The summary. + /// + public string Summary { get; set; } + + /// + /// Gets or sets a value indicating whether [requires early access]. + /// + /// + /// true if [requires early access]; otherwise, false. + /// + public bool RequiresEarlyAccess { get; set; } + + /// + /// Gets or sets the requires version. + /// + /// + /// The requires version. + /// + public string RequiresVersion { get; set; } + + /// + /// Gets or sets a value indicating whether this instance is production. + /// + /// + /// true if this instance is production; otherwise, false. + /// + public bool IsProduction { get; set; } + + /// + /// Gets or sets a value indicating whether this instance is beta. + /// + /// + /// true if this instance is beta; otherwise, false. + /// + public bool IsBeta { get; set; } + + /// + /// Gets or sets a value indicating whether this instance is alpha. + /// + /// + /// true if this instance is alpha; otherwise, false. + /// + public bool IsAlpha { get; set; } + + /// + /// Gets or sets the package URI. + /// + /// + /// The package URI. + /// + public string PackageUri { get; set; } + + /// + /// Gets or sets the release notes. + /// + /// + /// The release notes. + /// + public string ReleaseNotes { get; set; } + } +} diff --git a/Rock.Update/Models/RockReleaseProgram.cs b/Rock.Update/Models/RockReleaseProgram.cs new file mode 100644 index 00000000000..746beb97e7a --- /dev/null +++ b/Rock.Update/Models/RockReleaseProgram.cs @@ -0,0 +1,39 @@ +// +// Copyright by the Spark Development Network +// +// Licensed under the Rock Community License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.rockrms.com/license +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +namespace Rock.Update.Models +{ + /// + /// One of three options that represent which release 'program' one can be on. + /// + public enum RockReleaseProgram + { + /// + /// The alpha + /// + Alpha = 1, + + /// + /// The beta + /// + Beta = 2, + + /// + /// The production + /// + Production = 3 + } +} diff --git a/Rock.Update/Properties/AssemblyInfo.cs b/Rock.Update/Properties/AssemblyInfo.cs new file mode 100644 index 00000000000..12137e8b30e --- /dev/null +++ b/Rock.Update/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Rock.Update")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Rock.Update")] +[assembly: AssemblyCopyright("Copyright © 2021")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("b62b27c8-1f77-43cf-8ee7-30de7f93facd")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/Rock.Update/Rock.Update.csproj b/Rock.Update/Rock.Update.csproj new file mode 100644 index 00000000000..58e85bf08d0 --- /dev/null +++ b/Rock.Update/Rock.Update.csproj @@ -0,0 +1,99 @@ + + + + + Debug + AnyCPU + {B62B27C8-1F77-43CF-8EE7-30DE7F93FACD} + Library + Properties + Rock.Update + Rock.Update + v4.5.2 + 512 + true + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.dll + + + ..\packages\EntityFramework.6.1.3\lib\net45\EntityFramework.SqlServer.dll + + + ..\packages\Microsoft.Web.Xdt.2.1.1\lib\net40\Microsoft.Web.XmlTransform.dll + + + ..\packages\Newtonsoft.Json.11.0.2\lib\net45\Newtonsoft.Json.dll + + + ..\packages\RestSharp.105.2.3\lib\net452\RestSharp.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {6fe0930c-6832-4c2f-8a76-d4e4a2d80ddf} + Rock.Version + + + {185a31d7-3037-4dae-8797-0459849a84bd} + Rock + + + + \ No newline at end of file diff --git a/Rock.Update/RockInstaller.cs b/Rock.Update/RockInstaller.cs new file mode 100644 index 00000000000..ff79b5cf936 --- /dev/null +++ b/Rock.Update/RockInstaller.cs @@ -0,0 +1,446 @@ +// +// Copyright by the Spark Development Network +// +// Licensed under the Rock Community License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.rockrms.com/license +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Linq; +using System.Net; +using Microsoft.Web.XmlTransform; +using Rock.Model; +using Rock.Update.Exceptions; +using Rock.Update.Helpers; +using Rock.Update.Interfaces; +using Rock.Update.Models; + +namespace Rock.Update +{ + /// + /// Class used to install Rock updates. + /// + public class RockInstaller + { + private const string LOCAL_ROCK_PACKAGE_FOLDER = "App_Data\\Packages"; + private const string BACKUP_FOLDER = "App_Data\\RockBackup"; + private const string TRANSFORM_FILE_SUFFIX = ".rock.xdt"; + private const string CONTENT_PATH = "content/"; + private const string CONTENT_PATH_ALT = "content\\"; + + private readonly string _backupPath = Path.Combine( FileManagementHelper.ROOT_PATH, BACKUP_FOLDER ); + private readonly IRockUpdateService _rockUpdateService; + private readonly string _versionBackupPath; + private readonly Version _targetVersion; + private readonly Version _installedVersion; + + /// + /// Initializes a new instance of the class. + /// + /// The rock update service. + /// The target version. + /// The installed version. + public RockInstaller( IRockUpdateService rockUpdateService, Version targetVersion, Version installedVersion ) + { + _rockUpdateService = rockUpdateService; + _targetVersion = targetVersion; + _installedVersion = installedVersion; + _versionBackupPath = Path.Combine( _backupPath, installedVersion.ToString() ); + } + + /// + /// Installs the version. + /// + /// + /// Target Release ${targetRelease} was not found. + public RockRelease InstallVersion() + { + VersionValidationHelper.ValidateVersionInstall( _targetVersion ); + + var releases = _rockUpdateService.GetReleasesList( _installedVersion ); + var targetRelease = releases?.Where( r => r.SemanticVersion == _targetVersion.ToString() ).FirstOrDefault(); + + if ( targetRelease == null ) + { + throw new PackageNotFoundException( $"Target Release ${targetRelease} was not found." ); + } + + var targetPackagePath = DownloadPackage( targetRelease ); + + InstallPackage( targetPackagePath ); + + // Record the current version to the database + Web.SystemSettings.SetValue( Rock.SystemKey.SystemSetting.ROCK_INSTANCE_ID, _targetVersion.ToString() ); + + // register any new REST controllers + try + { + RestControllerService.RegisterControllers(); + } + catch ( Exception ex ) + { + ExceptionLogService.LogException( ex ); + } + + return targetRelease; + } + + /// + /// Installs the package. + /// + /// The target package path. + private void InstallPackage( string targetPackagePath ) + { + OfflinePageHelper.CreateOfflinePage(); + try + { + using ( ZipArchive packageZip = ZipFile.OpenRead( targetPackagePath ) ) + { + ClearPreviousBackups(); + ProcessTransformFiles( packageZip ); + ProcessContentFiles( packageZip ); + ProcessDeleteFiles( packageZip ); + FileManagementHelper.CleanUpDeletedFiles(); + } + } + catch + { + RestoreOriginal(); + throw; + } + finally + { + OfflinePageHelper.RemoveOfflinePage(); + } + } + + /// + /// Clears the previous backups. + /// + private void ClearPreviousBackups() + { + if ( !Directory.Exists( _backupPath ) ) + { + return; + } + + try + { + Directory.Delete( _backupPath, true ); + } + catch ( Exception ex ) + { + // We're logging the exception but otherwise ignoring it because this will run again the next install. + ExceptionLogService.LogException( ex ); + } + } + + /// + /// Restores the original. + /// + private void RestoreOriginal() + { + if ( !Directory.Exists( _versionBackupPath ) ) + { + return; + } + + var filesToRestore = Directory.GetFiles( _versionBackupPath, "*.*", SearchOption.AllDirectories ); + + foreach ( var file in filesToRestore ) + { + try + { + var originalPath = Path.Combine( FileManagementHelper.ROOT_PATH, file.Replace( _versionBackupPath.EnsureTrailingBackslash(), string.Empty ) ); + + var backupDirectory = Path.GetDirectoryName( originalPath ); + if ( !Directory.Exists( backupDirectory ) ) + { + Directory.CreateDirectory( backupDirectory ); + } + + FileManagementHelper.RenameFile( originalPath ); + File.Move( file, originalPath ); + } + catch ( Exception ex ) + { + ExceptionLogService.LogException( ex ); + } + } + + FileManagementHelper.CleanUpDeletedFiles(); + } + + /// + /// Processes the delete files. + /// + /// The package zip. + private void ProcessDeleteFiles( ZipArchive packageZip ) + { + // process deletefile.lst + var deleteListEntry = packageZip + .Entries + .Where( e => e.FullName == "install\\deletefile.lst" || e.FullName == "install/deletefile.lst" ) + .FirstOrDefault(); + + if ( deleteListEntry != null ) + { + var deleteList = System.Text.Encoding.Default.GetString( deleteListEntry.Open().ReadBytesToEnd() ); + var itemsToDelete = deleteList.Split( new string[] { Environment.NewLine }, StringSplitOptions.None ); + + foreach ( var deleteItem in itemsToDelete ) + { + if ( !string.IsNullOrWhiteSpace( deleteItem ) ) + { + + var deleteItemFullPath = Path.Combine( FileManagementHelper.ROOT_PATH, deleteItem ); + + var rockWeb = "RockWeb\\"; + if ( deleteItem.StartsWith( rockWeb ) ) + { + deleteItemFullPath = Path.Combine( FileManagementHelper.ROOT_PATH, deleteItem.Substring( rockWeb.Length ) ); + } + + rockWeb = "RockWeb/"; + if ( deleteItem.StartsWith( rockWeb ) ) + { + deleteItemFullPath = Path.Combine( FileManagementHelper.ROOT_PATH, deleteItem.Substring( rockWeb.Length ) ); + } + + var backupFilePath = GetBackupFileLocation( deleteItemFullPath ); + + if ( Directory.Exists( deleteItemFullPath ) ) + { + // if the backup folder already exists we need to process the individual files so we can keep the true originals. + if ( Directory.Exists( backupFilePath ) ) + { + var directoryFiles = Directory.GetFiles( deleteItemFullPath, "*.*", SearchOption.AllDirectories ); + foreach ( var subFile in directoryFiles ) + { + HandleFileDeletes( subFile, GetBackupFileLocation( subFile ) ); + } + } + else + { + // Don't actually delete just move the directory to the backup folder. + Directory.Move( deleteItemFullPath, backupFilePath ); + } + } + + HandleFileDeletes( deleteItemFullPath, backupFilePath ); + } + } + } + } + + /// + /// Handles the file deletes. + /// + /// The delete item full path. + /// The backup file path. + private void HandleFileDeletes( string deleteItemFullPath, string backupFilePath ) + { + if ( File.Exists( deleteItemFullPath ) ) + { + var backupDirectory = Path.GetDirectoryName( backupFilePath ); + if ( !Directory.Exists( backupDirectory ) ) + { + Directory.CreateDirectory( backupDirectory ); + } + + // If the file already exists in the backup, we should just delete the file because true original has already been backed up. + if ( File.Exists( backupFilePath ) ) + { + File.Delete( deleteItemFullPath ); + } + else + { + File.Move( deleteItemFullPath, backupFilePath ); + } + } + } + + /// + /// Processes the content files. + /// + /// The package zip. + private void ProcessContentFiles( ZipArchive packageZip ) + { + var contentFilesToProcess = packageZip + .Entries + .Where( e => e.FullName.StartsWith( CONTENT_PATH, StringComparison.OrdinalIgnoreCase ) + || e.FullName.StartsWith( CONTENT_PATH_ALT, StringComparison.OrdinalIgnoreCase ) ) + .Where( e => !e.FullName.EndsWith( TRANSFORM_FILE_SUFFIX, StringComparison.OrdinalIgnoreCase ) ); + + // unzip content folder and process xdts + foreach ( ZipArchiveEntry entry in contentFilesToProcess ) + { + // process all content files + string fullpath = Path.Combine( FileManagementHelper.ROOT_PATH, entry.FullName.Replace( CONTENT_PATH, string.Empty ).Replace( CONTENT_PATH_ALT, string.Empty ) ); + fullpath = Path.Combine( FileManagementHelper.ROOT_PATH, entry.FullName.Replace( CONTENT_PATH, string.Empty ).Replace( CONTENT_PATH_ALT, string.Empty ) ); + + string directory = Path.GetDirectoryName( fullpath ).Replace( CONTENT_PATH, string.Empty ).Replace( CONTENT_PATH_ALT, string.Empty ); + + ThrowTestExceptions( Path.GetFileNameWithoutExtension( entry.FullName ) ); + + // if entry is a directory ignore it + if ( entry.Length != 0 ) + { + BackupFile( fullpath ); + + if ( !Directory.Exists( directory ) ) + { + Directory.CreateDirectory( directory ); + } + + FileManagementHelper.RenameActiveFile( fullpath ); + + entry.ExtractToFile( fullpath, true ); + } + } + } + + /// + /// Throws the test exceptions. + /// + /// The exception. + private void ThrowTestExceptions( string exception ) + { + var exceptionList = new Dictionary + { + { "exception", new Exception("Test Exception") }, + { "ioexception", new IOException("Test IO Exception") }, + { "outofmemoryexception", new OutOfMemoryException("Test Out of Memory Exception") }, + { "versionvalidationexception", new Exception("Test Version Validation Exception") }, + { "xmlexception", new Exception("XML Exception") }, + }; + + exception = exception.ToLower(); + if ( exceptionList.ContainsKey( exception ) ) + { + throw exceptionList[exception]; + } + } + + /// + /// Downloads the package. + /// + /// The release. + /// + /// Target Release ${release} doesn't have a Package URI specified. + private string DownloadPackage( RockRelease release ) + { + if ( release.PackageUri.IsNullOrWhiteSpace() ) + { + throw new Exception( $"Target Release ${release} doesn't have a Package URI specified." ); + } + + var localRockPackageDirectory = Path.Combine( FileManagementHelper.ROOT_PATH, LOCAL_ROCK_PACKAGE_FOLDER ); + + if ( !Directory.Exists( localRockPackageDirectory ) ) + { + Directory.CreateDirectory( localRockPackageDirectory ); + } + + var localRockPackagePath = Path.Combine( localRockPackageDirectory, $"{release.SemanticVersion}.rockpkg" ); + FileManagementHelper.DeleteOrRename( localRockPackagePath ); + + try + { + var wc = new WebClient(); + wc.DownloadFile( release.PackageUri, localRockPackagePath ); + } + catch + { + FileManagementHelper.DeleteOrRename( localRockPackagePath ); + throw; + } + + return localRockPackagePath; + } + + /// + /// Processes the transform files. + /// + /// The package zip. + private void ProcessTransformFiles( ZipArchive packageZip ) + { + var transformFilesToProcess = packageZip + .Entries + .Where( e => e.FullName.StartsWith( CONTENT_PATH, StringComparison.OrdinalIgnoreCase ) || e.FullName.StartsWith( CONTENT_PATH_ALT, StringComparison.OrdinalIgnoreCase ) ) + .Where( e => e.FullName.EndsWith( TRANSFORM_FILE_SUFFIX, StringComparison.OrdinalIgnoreCase ) ); + + foreach ( ZipArchiveEntry entry in transformFilesToProcess ) + { + // process xdt + string filename = entry.FullName.Replace( CONTENT_PATH, string.Empty ).Replace( CONTENT_PATH_ALT, string.Empty ); + string transformTargetFile = Path.Combine( FileManagementHelper.ROOT_PATH, filename.Substring( 0, filename.LastIndexOf( TRANSFORM_FILE_SUFFIX ) ) ); + + // process transform + using ( XmlTransformableDocument document = new XmlTransformableDocument() ) + { + document.PreserveWhitespace = true; + document.Load( transformTargetFile ); + + using ( XmlTransformation transform = new XmlTransformation( entry.Open(), null ) ) + { + if ( transform.Apply( document ) ) + { + BackupFile( transformTargetFile ); + document.Save( transformTargetFile ); + } + } + } + } + } + + /// + /// Gets the backup file location. + /// + /// The filepath to backup. + /// + private string GetBackupFileLocation( string filepathToBackup ) + { + var relativeTargetPath = filepathToBackup.Replace( FileManagementHelper.ROOT_PATH.EnsureTrailingBackslash(), string.Empty ); + + if ( !Directory.Exists( _versionBackupPath ) ) + { + Directory.CreateDirectory( _versionBackupPath ); + } + + return Path.Combine( _versionBackupPath, relativeTargetPath ); + } + + /// + /// Backups the file. + /// + /// The filepath to backup. + private void BackupFile( string filepathToBackup ) + { + if ( File.Exists( filepathToBackup ) ) + { + var backupFilePath = GetBackupFileLocation( filepathToBackup ); + var backupDirectory = Path.GetDirectoryName( backupFilePath ); + if ( !Directory.Exists( backupDirectory ) ) + { + Directory.CreateDirectory( backupDirectory ); + } + + FileManagementHelper.DeleteOrRename( backupFilePath ); + File.Copy( filepathToBackup, backupFilePath ); + } + } + } +} diff --git a/Rock.Update/RockInstanceImpactStatistics.cs b/Rock.Update/RockInstanceImpactStatistics.cs new file mode 100644 index 00000000000..265510bea42 --- /dev/null +++ b/Rock.Update/RockInstanceImpactStatistics.cs @@ -0,0 +1,105 @@ +// +// Copyright by the Spark Development Network +// +// Licensed under the Rock Community License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.rockrms.com/license +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Linq; +using Rock.Data; +using Rock.Model; +using Rock.Update.Interfaces; +using Rock.Update.Models; +using Rock.Web; +using Rock.Web.Cache; + +namespace Rock.Update +{ + /// + /// Class used to send statistics to the Rock site. + /// + public class RockInstanceImpactStatistics + { + private readonly IRockImpactService _rockImpactService; + private readonly RockContext _rockContext; + + /// + /// Initializes a new instance of the class. + /// + /// The rock impact service. + /// The rock context. + public RockInstanceImpactStatistics( IRockImpactService rockImpactService, RockContext rockContext ) + { + _rockImpactService = rockImpactService; + _rockContext = rockContext; + } + + /// + /// Sends the impact statistics to spark. + /// + /// if set to true [include organization data]. + /// The version. + /// The ip address. + /// The environment data. + public void SendImpactStatisticsToSpark( bool includeOrganizationData, string version, string ipAddress, string environmentData ) + { + ImpactLocation organizationLocation = null; + var publicUrl = string.Empty; + var organizationName = string.Empty; + + var numberOfActiveRecords = new PersonService( _rockContext ).Queryable( includeDeceased: false, includeBusinesses: false ).Count(); + + if ( numberOfActiveRecords <= 100 || SystemSettings.GetValue( SystemKey.SystemSetting.SAMPLEDATA_DATE ).AsDateTime().HasValue ) + { + return; + } + + if ( includeOrganizationData ) + { + var globalAttributes = GlobalAttributesCache.Get(); + + // Fetch the organization address + var organizationAddressLocationGuid = globalAttributes.GetValue( "OrganizationAddress" ).AsGuid(); + if ( !organizationAddressLocationGuid.Equals( Guid.Empty ) ) + { + var location = new LocationService( _rockContext ).Get( organizationAddressLocationGuid ); + if ( location != null ) + { + organizationLocation = new ImpactLocation( location ); + } + } + + organizationName = globalAttributes.GetValue( "OrganizationName" ); + publicUrl = globalAttributes.GetValue( "PublicApplicationRoot" ); + } + else + { + numberOfActiveRecords = 0; + } + + ImpactStatistic impactStatistic = new ImpactStatistic() + { + RockInstanceId = SystemSettings.GetRockInstanceId(), + Version = version, + IpAddress = ipAddress, + PublicUrl = publicUrl, + OrganizationName = organizationName, + OrganizationLocation = organizationLocation, + NumberOfActiveRecords = numberOfActiveRecords, + EnvironmentData = environmentData + }; + + _rockImpactService.SendImpactStatisticsToSpark( impactStatistic ); + } + } +} diff --git a/Rock.Update/Services/RockImpactService.cs b/Rock.Update/Services/RockImpactService.cs new file mode 100644 index 00000000000..6ae35eb1cb0 --- /dev/null +++ b/Rock.Update/Services/RockImpactService.cs @@ -0,0 +1,50 @@ +// +// Copyright by the Spark Development Network +// +// Licensed under the Rock Community License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.rockrms.com/license +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System.Configuration; +using RestSharp; +using Rock.Update.Interfaces; +using Rock.Update.Models; + +namespace Rock.Update.Services +{ + /// + /// Class used to send statistics to the Rock Site. + /// + /// + public class RockImpactService : IRockImpactService + { + private const string SEND_IMPACT_URL = "api/impacts/save"; + + private string BaseUrl + { + get => ConfigurationManager.AppSettings["RockStoreUrl"].EnsureTrailingForwardslash(); + } + + /// + /// Sends the impact statistics to spark. + /// + /// The impact statistic. + public void SendImpactStatisticsToSpark( ImpactStatistic impactStatistic ) + { + var client = new RestClient( BaseUrl + SEND_IMPACT_URL ); + var request = new RestRequest( Method.POST ); + request.RequestFormat = DataFormat.Json; + request.AddBody( impactStatistic ); + client.Execute( request ); + } + } +} diff --git a/Rock.Update/Services/RockUpdateService.cs b/Rock.Update/Services/RockUpdateService.cs new file mode 100644 index 00000000000..d0388e5eff6 --- /dev/null +++ b/Rock.Update/Services/RockUpdateService.cs @@ -0,0 +1,123 @@ +// +// Copyright by the Spark Development Network +// +// Licensed under the Rock Community License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.rockrms.com/license +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Net; +using Newtonsoft.Json; +using RestSharp; +using Rock.Update.Interfaces; +using Rock.Update.Models; +using Rock.Web.Cache; + +namespace Rock.Update.Services +{ + /// + /// Rock Update Service + /// + public class RockUpdateService : IRockUpdateService + { + private const string GET_RELEASE_LIST_URL = "api/RockUpdate/GetReleasesList"; + private const string GET_RELEASE_LIST_SINCE_URL = "api/RockUpdate/GetReleasesListSinceVersion"; + private const string EARLY_ACCESS_URL = "api/RockUpdate/GetEarlyAccessStatus"; + private const string EARLY_ACCESS_REQUEST_URL = "earlyaccessissues?RockInstanceId="; + + private string BaseUrl + { + get => ConfigurationManager.AppSettings["RockStoreUrl"].EnsureTrailingForwardslash(); + } + + /// + /// Gets the releases list from the rock server. + /// + /// + public List GetReleasesList( Version version ) + { + var request = new RestRequest( Method.GET ); + + request.RequestFormat = DataFormat.Json; + + request.AddParameter( "rockInstanceId", Web.SystemSettings.GetRockInstanceId() ); + request.AddParameter( "releaseProgram", GetRockReleaseProgram().ToString().ToLower() ); + + if ( version != null ) + { + request.AddParameter( "sinceVersion", version.ToString() ); + } + + var client = new RestClient( BaseUrl + ( version != null ? GET_RELEASE_LIST_SINCE_URL : GET_RELEASE_LIST_URL ) ); + var response = client.Execute( request ); + + if ( response.StatusCode == HttpStatusCode.OK || response.StatusCode == HttpStatusCode.Accepted ) + { + return JsonConvert.DeserializeObject>( response.Content ); + } + + return new List(); + } + + /// + /// Checks the early access status of this organization. + /// + /// + public bool IsEarlyAccessInstance() + { + var client = new RestClient( BaseUrl + EARLY_ACCESS_URL ); + var request = new RestRequest( Method.GET ); + request.RequestFormat = DataFormat.Json; + + request.AddParameter( "rockInstanceId", Web.SystemSettings.GetRockInstanceId() ); + IRestResponse response = client.Execute( request ); + if ( response.StatusCode == HttpStatusCode.OK || response.StatusCode == HttpStatusCode.Accepted ) + { + return response.Content.AsBoolean(); + } + + return false; + } + + /// + /// Gets the rock early access request URL. + /// + /// + public string GetRockEarlyAccessRequestUrl() + { + return $"{BaseUrl}{EARLY_ACCESS_REQUEST_URL}{Web.SystemSettings.GetRockInstanceId()}"; + } + + /// + /// Gets the rock release program. + /// + /// + public RockReleaseProgram GetRockReleaseProgram() + { + var releaseProgram = RockReleaseProgram.Production; + + var updateUrl = GlobalAttributesCache.Get().GetValue( "UpdateServerUrl" ); + if ( updateUrl.Contains( RockReleaseProgram.Alpha.ToString().ToLower() ) ) + { + releaseProgram = RockReleaseProgram.Alpha; + } + else if ( updateUrl.Contains( RockReleaseProgram.Beta.ToString().ToLower() ) ) + { + releaseProgram = RockReleaseProgram.Beta; + } + + return releaseProgram; + } + } +} diff --git a/Rock.Update/packages.config b/Rock.Update/packages.config new file mode 100644 index 00000000000..e10d0b701c8 --- /dev/null +++ b/Rock.Update/packages.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/Rock.sln b/Rock.sln index a3755518002..374068f58ec 100644 --- a/Rock.sln +++ b/Rock.sln @@ -6,7 +6,7 @@ MinimumVisualStudioVersion = 10.0.40219.1 Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "RockWeb", "RockWeb\", "{E43B5D55-BED0-4E74-BF97-9327F9A9D656}" ProjectSection(WebsiteProperties) = preProject TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.5.2" - ProjectReferences = "{00edcb8d-ef33-459c-ad62-02876bd24dff}|DotLiquid.dll;{185a31d7-3037-4dae-8797-0459849a84bd}|Rock.dll;{d6b19c0d-da5e-4f75-8001-04ded86b741f}|Rock.Mailgun.dll;{cf8c694a-55a0-434f-bc96-3450b96ec12a}|Rock.Mandrill.dll;{f3692909-952d-4c4a-b2d2-d90d0083cf53}|Rock.NMI.dll;{a005d091-140e-4ec4-bcdf-cf7d42bb702c}|Rock.PayFlowPro.dll;{add1edd0-a4cb-4e82-b6ad-6ad1d556deae}|Rock.Rest.dll;{6fe0930c-6832-4c2f-8a76-d4e4a2d80ddf}|Rock.Version.dll;{1f5956f2-2b0f-49b8-aaf1-2cc28f01426a}|Rock.SignNow.dll;{69AC175C-3997-4514-8C9E-5D24811928C2}|SignNowSDK.dll;{c1dd2402-5fb2-411e-bf8d-5d9cb3a58e8b}|Rock.Slingshot.dll;{962944DE-8BF4-4175-B55A-E75CF7918272}|Rock.Slingshot.Model.dll;{42edf2dd-5284-4b64-93c3-5186619d2b14}|Rock.StatementGenerator.dll;{5055f482-72c7-4cc9-8ed0-a77090c777db}|Rock.Security.Authentication.Auth0.dll;{515e22e4-4b9b-4886-8477-e5b312b75eb4}|Rock.WebStartup.dll;{94a5f880-31ae-4889-b9be-ba1fdc258e83}|Rock.Checkr.dll;{cadd9206-2c6b-42e4-b20b-2dfc3eb4d6d4}|Rock.DownhillCss.dll;{13711bed-69dd-4182-9bb5-b3c9a4de32df}|Rock.MyWell.dll;{205f439a-2ccc-42c1-b1ab-09e1b629d88c}|Rock.SendGrid.dll;{8ccb8e2a-073c-48cb-b31a-621ec5430a42}|Rock.Oidc.dll;{704740D8-B539-4560-9F8C-681670C9D6AD}|Rock.Migrations.dll;" + ProjectReferences = "{00edcb8d-ef33-459c-ad62-02876bd24dff}|DotLiquid.dll;{185a31d7-3037-4dae-8797-0459849a84bd}|Rock.dll;{d6b19c0d-da5e-4f75-8001-04ded86b741f}|Rock.Mailgun.dll;{cf8c694a-55a0-434f-bc96-3450b96ec12a}|Rock.Mandrill.dll;{f3692909-952d-4c4a-b2d2-d90d0083cf53}|Rock.NMI.dll;{a005d091-140e-4ec4-bcdf-cf7d42bb702c}|Rock.PayFlowPro.dll;{add1edd0-a4cb-4e82-b6ad-6ad1d556deae}|Rock.Rest.dll;{6fe0930c-6832-4c2f-8a76-d4e4a2d80ddf}|Rock.Version.dll;{1f5956f2-2b0f-49b8-aaf1-2cc28f01426a}|Rock.SignNow.dll;{69AC175C-3997-4514-8C9E-5D24811928C2}|SignNowSDK.dll;{c1dd2402-5fb2-411e-bf8d-5d9cb3a58e8b}|Rock.Slingshot.dll;{962944DE-8BF4-4175-B55A-E75CF7918272}|Rock.Slingshot.Model.dll;{42edf2dd-5284-4b64-93c3-5186619d2b14}|Rock.StatementGenerator.dll;{5055f482-72c7-4cc9-8ed0-a77090c777db}|Rock.Security.Authentication.Auth0.dll;{515e22e4-4b9b-4886-8477-e5b312b75eb4}|Rock.WebStartup.dll;{94a5f880-31ae-4889-b9be-ba1fdc258e83}|Rock.Checkr.dll;{cadd9206-2c6b-42e4-b20b-2dfc3eb4d6d4}|Rock.DownhillCss.dll;{13711bed-69dd-4182-9bb5-b3c9a4de32df}|Rock.MyWell.dll;{205f439a-2ccc-42c1-b1ab-09e1b629d88c}|Rock.SendGrid.dll;{8ccb8e2a-073c-48cb-b31a-621ec5430a42}|Rock.Oidc.dll;{704740D8-B539-4560-9F8C-681670C9D6AD}|Rock.Migrations.dll;{b62b27c8-1f77-43cf-8ee7-30de7f93facd}|Rock.Update.dll;" Debug.AspNetCompiler.VirtualPath = "/localhost_6229" Debug.AspNetCompiler.PhysicalPath = "RockWeb\" Debug.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_6229\" @@ -53,8 +53,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rock.Slingshot", "Rock.Slin EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rock.Slingshot.Model", "Rock.Slingshot.Model\Rock.Slingshot.Model.csproj", "{962944DE-8BF4-4175-B55A-E75CF7918272}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rock.Specs", "Rock.Specs\Rock.Specs.csproj", "{5BC16478-A45D-4F65-87B2-FFD61650D541}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rock.StatementGenerator", "Rock.StatementGenerator\Rock.StatementGenerator.csproj", "{42EDF2DD-5284-4B64-93C3-5186619D2B14}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rock.Security.Authentication.Auth0", "Rock.Security.Authentication.Auth0\Rock.Security.Authentication.Auth0.csproj", "{5055F482-72C7-4CC9-8ED0-A77090C777DB}" @@ -81,6 +79,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rock.SendGrid", "Rock.SendG EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rock.Oidc", "Rock.Oidc\Rock.Oidc.csproj", "{8CCB8E2A-073C-48CB-B31A-621EC5430A42}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rock.Update", "Rock.Update\Rock.Update.csproj", "{B62B27C8-1F77-43CF-8EE7-30DE7F93FACD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -147,10 +147,6 @@ Global {962944DE-8BF4-4175-B55A-E75CF7918272}.Debug|Any CPU.Build.0 = Debug|Any CPU {962944DE-8BF4-4175-B55A-E75CF7918272}.Release|Any CPU.ActiveCfg = Release|Any CPU {962944DE-8BF4-4175-B55A-E75CF7918272}.Release|Any CPU.Build.0 = Release|Any CPU - {5BC16478-A45D-4F65-87B2-FFD61650D541}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {5BC16478-A45D-4F65-87B2-FFD61650D541}.Debug|Any CPU.Build.0 = Debug|Any CPU - {5BC16478-A45D-4F65-87B2-FFD61650D541}.Release|Any CPU.ActiveCfg = Release|Any CPU - {5BC16478-A45D-4F65-87B2-FFD61650D541}.Release|Any CPU.Build.0 = Release|Any CPU {42EDF2DD-5284-4B64-93C3-5186619D2B14}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {42EDF2DD-5284-4B64-93C3-5186619D2B14}.Debug|Any CPU.Build.0 = Debug|Any CPU {42EDF2DD-5284-4B64-93C3-5186619D2B14}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -191,6 +187,10 @@ Global {8CCB8E2A-073C-48CB-B31A-621EC5430A42}.Debug|Any CPU.Build.0 = Debug|Any CPU {8CCB8E2A-073C-48CB-B31A-621EC5430A42}.Release|Any CPU.ActiveCfg = Release|Any CPU {8CCB8E2A-073C-48CB-B31A-621EC5430A42}.Release|Any CPU.Build.0 = Release|Any CPU + {B62B27C8-1F77-43CF-8EE7-30DE7F93FACD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B62B27C8-1F77-43CF-8EE7-30DE7F93FACD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B62B27C8-1F77-43CF-8EE7-30DE7F93FACD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B62B27C8-1F77-43CF-8EE7-30DE7F93FACD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Rock/Web/Utilities/Enums/DotNetVersionCheckResult.cs b/Rock/Web/Utilities/Enums/DotNetVersionCheckResult.cs index c935ec986be..a62cf1f3639 100644 --- a/Rock/Web/Utilities/Enums/DotNetVersionCheckResult.cs +++ b/Rock/Web/Utilities/Enums/DotNetVersionCheckResult.cs @@ -36,4 +36,4 @@ public enum DotNetVersionCheckResult /// Unknown = 2 } -} +} \ No newline at end of file diff --git a/RockWeb/Blocks/Core/RockUpdate.ascx b/RockWeb/Blocks/Core/RockUpdate.ascx index ae7a7bd22a0..9444a549ddf 100644 --- a/RockWeb/Blocks/Core/RockUpdate.ascx +++ b/RockWeb/Blocks/Core/RockUpdate.ascx @@ -1,38 +1,42 @@ <%@ Control Language="C#" AutoEventWireup="true" CodeFile="RockUpdate.ascx.cs" Inherits="RockWeb.Blocks.Core.RockUpdate" %> -<%@ Import namespace="Rock" %> +<%@ Import Namespace="Rock" %>
-

Rock Update

+

Rock Update

- + .NET Framework Update Required

As of Rock McKinley v13, Rock requires Microsoft .NET Framework 4.7.2 or greater on the hosting server. This framework version was released by Microsoft on April 30th, 2018.

-
- + +

Microsoft SQL Server Update Required

As of Rock McKinley v11, Rock requires SQL Server 2014 or greater. You will need to upgrade your database in order to proceed with that update.

-
- +
+

Everything Is Shipshape

-

You run a tight ship, there is nothing to update since . Check back soon as we're working hard on something amazing or - check out the release notes. +

+ You run a tight ship, there is nothing to update since + . Check back soon as we're working hard on something amazing or + check out the release notes.

@@ -58,21 +62,22 @@
- +

Community Contributors have early access to major releases of Rock. Find out - how to get early access to releases as a Community Contributor. + how to get early access to releases as a Community Contributor. If you are already a Community Contributor and are having trouble with your access, - let us know so we can resolve the problem. + let us know so we can resolve the problem.

- Thank you for being a Community Contributor! Learn more about the Early Access program. + + Thank you for being a Community Contributor! Learn more about the Early Access program.
@@ -91,47 +96,50 @@
- - -
-
-

-
-
-
-
- Install -
-
- + + +
+
+

+

+
+
+
+
+ Install +
+
+ -
-
Release Notes
+
+
Release Notes
- -
+
- +
+
- +

Eureka, Pay Dirt!

-

Update completed successfully... You're now running .

+

Update completed successfully... You're now running + + .

Below is a summary of the new toys you have to play with... - +
@@ -151,9 +159,18 @@ + + + + + + + + + Legacy Updater
+
- @@ -165,4 +182,4 @@ $top.find(".releasenotes-body").slideToggle(500); }); }); - \ No newline at end of file + diff --git a/RockWeb/Blocks/Core/RockUpdate.ascx.cs b/RockWeb/Blocks/Core/RockUpdate.ascx.cs index d8fec4354e8..738be39bc86 100644 --- a/RockWeb/Blocks/Core/RockUpdate.ascx.cs +++ b/RockWeb/Blocks/Core/RockUpdate.ascx.cs @@ -13,88 +13,44 @@ // See the License for the specific language governing permissions and // limitations under the License. // -// + using System; using System.Collections.Generic; using System.ComponentModel; -using System.Configuration; using System.IO; using System.Linq; -using System.Net; using System.Text; -using System.Text.RegularExpressions; using System.Web.UI; using System.Web.UI.HtmlControls; using System.Web.UI.WebControls; -using Newtonsoft.Json; -using NuGet; -using RestSharp; using Rock; using Rock.Data; using Rock.Model; -using Rock.Services.NuGet; +using Rock.Update; +using Rock.Update.Enum; +using Rock.Update.Exceptions; +using Rock.Update.Helpers; +using Rock.Update.Models; +using Rock.Update.Services; using Rock.VersionInfo; -using Rock.Web.Cache; -using Rock.Web.Utilities; namespace RockWeb.Blocks.Core { - [DisplayName( "RockUpdate" )] + [DisplayName( "Rock Update" )] [Category( "Core" )] [Description( "Handles checking for and performing upgrades to the Rock system." )] public partial class RockUpdate : Rock.Web.UI.RockBlock { #region Fields private bool _isEarlyAccessOrganization = false; - private List _releases = new List(); - WebProjectManager nuGetService = null; - private string _rockPackageId = "Rock"; - IEnumerable _availablePackages = null; - SemanticVersion _installedVersion = new SemanticVersion( "0.0.0" ); - private int _numberOfAvailablePackages = 0; + private List _releases = new List(); + Version _installedVersion = new Version( "0.0.0" ); private bool _isOkToProceed = false; private bool _hasSqlServer14OrHigher = false; #endregion #region Properties - - private string RockSiteUrl { get { return ConfigurationManager.AppSettings["RockStoreUrl"].EnsureTrailingForwardslash(); } } - - /// - /// Obtains a WebProjectManager from the Global "UpdateServerUrl" Attribute. - /// - /// - /// The NuGet service or null if no valid service could be found using the UpdateServerUrl. - /// - protected WebProjectManager NuGetService - { - get - { - if ( nuGetService == null ) - { - var globalAttributesCache = GlobalAttributesCache.Get(); - string packageSource = globalAttributesCache.GetValue( "UpdateServerUrl" ); - if ( packageSource.ToLowerInvariant().Contains( "rockalpha" ) || packageSource.ToLowerInvariant().Contains( "rockbeta" ) ) - { - nbRepoWarning.Visible = true; - } - - // Since you can use a URL or a local path, we can't just check for valid URI - try - { - string siteRoot = Request.MapPath( "~/" ); - nuGetService = new WebProjectManager( packageSource, siteRoot ); - } - catch - { - // if caught, we will return a null nuGetService - } - } - return nuGetService; - } - } - #endregion #region Base Control Methods @@ -108,12 +64,13 @@ protected override void OnInit( EventArgs e ) base.OnInit( e ); string script = @" - $('#btn-restart').on('click', function () { - var btn = $(this); - btn.button('loading'); - location = location.href; - }); -"; + $('#btn-restart').on('click', function () { + var btn = $(this); + btn.button('loading'); + location = location.href; + }); + "; + ScriptManager.RegisterStartupScript( pnlUpdateSuccess, pnlUpdateSuccess.GetType(), "restart-script", script, true ); } @@ -123,82 +80,87 @@ protected override void OnInit( EventArgs e ) /// An object that contains the event data. protected override void OnLoad( EventArgs e ) { + var rockUpdateService = new RockUpdateService(); + // Set timeout for up to 15 minutes (just like installer) Server.ScriptTimeout = 900; ScriptManager.GetCurrent( Page ).AsyncPostBackTimeout = 900; + _isEarlyAccessOrganization = rockUpdateService.IsEarlyAccessInstance(); + _installedVersion = new Version( VersionInfo.GetRockSemanticVersionNumber() ); + + if ( rockUpdateService.GetRockReleaseProgram() != RockReleaseProgram.Production ) + { + nbRepoWarning.Visible = true; + } + DisplayRockVersion(); if ( !IsPostBack ) { - if ( NuGetService == null ) - { - pnlNoUpdates.Visible = false; - pnlError.Visible = true; - nbErrors.Text = string.Format( "Your UpdateServerUrl is not valid. It is currently set to: {0}", GlobalAttributesCache.Get().GetValue( "UpdateServerUrl" ) ); - } - else - { - try - { - _isEarlyAccessOrganization = CheckEarlyAccess(); + btnIssues.NavigateUrl = rockUpdateService.GetRockEarlyAccessRequestUrl(); - btnIssues.NavigateUrl = string.Format( "{0}earlyaccessissues?RockInstanceId={1}", RockSiteUrl, Rock.Web.SystemSettings.GetRockInstanceId() ); + if ( _isEarlyAccessOrganization ) + { + hlblEarlyAccess.LabelType = Rock.Web.UI.Controls.LabelType.Success; + hlblEarlyAccess.Text = "Early Access: Enabled"; - if ( _isEarlyAccessOrganization ) - { - hlblEarlyAccess.LabelType = Rock.Web.UI.Controls.LabelType.Success; - hlblEarlyAccess.Text = "Early Access: Enabled"; + pnlEarlyAccessNotEnabled.Visible = false; + pnlEarlyAccessEnabled.Visible = true; + } - pnlEarlyAccessNotEnabled.Visible = false; - pnlEarlyAccessEnabled.Visible = true; - } + var result = VersionValidationHelper.CheckFrameworkVersion(); - var result = CheckFrameworkVersion(); + _isOkToProceed = true; - _isOkToProceed = true; + if ( result == DotNetVersionCheckResult.Fail ) + { - if ( result == DotNetVersionCheckResult.Fail ) - { + nbVersionIssue.Visible = true; + nbVersionIssue.Text += "

You will need to upgrade your hosting server in order to proceed with the v13 update.

"; + nbBackupMessage.Visible = false; + } + else if ( result == DotNetVersionCheckResult.Unknown ) + { + nbVersionIssue.Visible = true; + nbVersionIssue.Text += "

You may need to upgrade your hosting server in order to proceed with the v13 update. We were unable to determine which Framework version your server is using.

"; + nbVersionIssue.Details += "
We were unable to check your server to verify that the .Net 4.7.2 Framework is installed! You MUST verify this manually before you proceed with the update otherwise your Rock application will be broken until you update the server.
"; + nbBackupMessage.Visible = false; + } - nbVersionIssue.Visible = true; - nbVersionIssue.Text += "

You will need to upgrade your hosting server in order to proceed with the v13 update.

"; - nbBackupMessage.Visible = false; - } - else if ( result == DotNetVersionCheckResult.Unknown ) - { - nbVersionIssue.Visible = true; - nbVersionIssue.Text += "

You may need to upgrade your hosting server in order to proceed with the v13 update. We were unable to determine which Framework version your server is using.

"; - nbVersionIssue.Details += "
We were unable to check your server to verify that the .Net 4.7.2 Framework is installed! You MUST verify this manually before you proceed with the update otherwise your Rock application will be broken until you update the server.
"; - nbBackupMessage.Visible = false; - } + _hasSqlServer14OrHigher = VersionValidationHelper.CheckSqlServerVersionGreaterThenSqlServer2012(); - _hasSqlServer14OrHigher = CheckSqlServerVersionGreaterThenSqlServer2012(); - if ( !_hasSqlServer14OrHigher ) - { - nbSqlServerVersionIssue.Visible = true; - } + if ( !_hasSqlServer14OrHigher ) + { + nbSqlServerVersionIssue.Visible = true; + } - _releases = GetReleasesList(); - _availablePackages = NuGetService.SourceRepository.FindPackagesById( _rockPackageId ).OrderByDescending( p => p.Version ); - if ( IsUpdateAvailable() ) - { - pnlUpdatesAvailable.Visible = true; - pnlUpdates.Visible = true; - pnlNoUpdates.Visible = false; - cbIncludeStats.Visible = true; - BindGrid(); - } + _releases = GetOrderedReleaseList( rockUpdateService, _installedVersion ); - RemoveOldRDeleteFiles(); - } - catch ( System.InvalidOperationException ex ) + if ( _releases.Count > 0 ) + { + if ( new Version( _releases.Last().SemanticVersion ) >= new Version( "1.13.0" ) ) { - HandleNuGetException( ex ); + nbVersionIssue.NotificationBoxType = Rock.Web.UI.Controls.NotificationBoxType.Danger; } + pnlUpdatesAvailable.Visible = true; + pnlUpdates.Visible = true; + pnlNoUpdates.Visible = false; + cbIncludeStats.Visible = true; + BindGrid(); } + + FileManagementHelper.CleanUpDeletedFiles(); } } + private List GetOrderedReleaseList( RockUpdateService rockUpdateService, Version installedVersion ) + { + return rockUpdateService + .GetReleasesList( installedVersion ) + .OrderByDescending( p => new Version( p.SemanticVersion ) ) + .ToList(); + } + #endregion #region Events @@ -208,7 +170,7 @@ protected override void OnLoad( EventArgs e ) /// private void BindGrid() { - rptPackageVersions.DataSource = _availablePackages; + rptPackageVersions.DataSource = _releases; rptPackageVersions.DataBind(); } @@ -219,16 +181,8 @@ private void BindGrid() /// the semantic version number private void Update( string version ) { - var targetVersion = new SemanticVersion( version ); - if ( !CanInstallVersion( targetVersion ) ) - { - nbErrors.Text = "
  • This version cannot be installed because not all requirements have been met.
"; - pnlError.Visible = true; - pnlUpdateSuccess.Visible = false; - return; - } + var targetVersion = new Version( version ); - WriteAppOffline(); try { pnlUpdatesAvailable.Visible = false; @@ -253,30 +207,6 @@ private void Update( string version ) nbErrors.Text = string.Format( "Something went wrong. Although the errors were written to the error log, they are listed for your review:
{0}", ex.Message ); LogException( ex ); } - RemoveAppOffline(); - } - - private bool CanInstallVersion( SemanticVersion targetVersion ) - { - var requiresSqlServer14OrHigher = targetVersion.Version.Major > 1 || targetVersion.Version.Minor > 10; - var requiresNet472 = targetVersion.Version.Major > 1 || targetVersion.Version.Minor > 12; - - if ( !requiresSqlServer14OrHigher ) - { - return true; - } - - var hasSqlServer2012OrGreater = CheckSqlServerVersionGreaterThenSqlServer2012(); - if ( requiresNet472 ) - { - var result = CheckFrameworkVersion(); - if ( result == DotNetVersionCheckResult.Fail ) - { - return false; - } - } - - return hasSqlServer2012OrGreater; } /// @@ -288,16 +218,16 @@ protected void rptPackageVersions_ItemDataBound( object sender, RepeaterItemEven { if ( e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem ) { - IPackage package = e.Item.DataItem as IPackage; + var package = e.Item.DataItem as RockRelease; if ( package != null ) { LinkButton lbInstall = e.Item.FindControl( "lbInstall" ) as LinkButton; var divPanel = e.Item.FindControl( "divPanel" ) as HtmlGenericControl; - var requiredVersion = ExtractRequiredVersionFromTags( package ); - if ( requiredVersion <= _installedVersion ) + if ( ( package.RequiresVersion.IsNotNullOrWhiteSpace() && new Version( package.RequiresVersion ) <= _installedVersion ) + || ( package.RequiresVersion.IsNullOrWhiteSpace() && new Version( package.SemanticVersion ) > _installedVersion ) ) { - var release = _releases.Where( r => r.SemanticVersion == package.Version.ToString() ).FirstOrDefault(); + var release = _releases.Where( r => r.Version == package.Version.ToString() ).FirstOrDefault(); if ( !_isEarlyAccessOrganization && release != null && release.RequiresEarlyAccess ) { lbInstall.Enabled = false; @@ -320,7 +250,7 @@ protected void rptPackageVersions_ItemDataBound( object sender, RepeaterItemEven divPanel.AddCssClass( "panel-block" ); } - if ( !_isOkToProceed || !CanInstallVersion( package.Version ) ) + if ( !_isOkToProceed || !VersionValidationHelper.CanInstallVersion( new Version( package.SemanticVersion ) ) ) { lbInstall.Enabled = false; lbInstall.AddCssClass( "btn-danger" ); @@ -328,6 +258,15 @@ protected void rptPackageVersions_ItemDataBound( object sender, RepeaterItemEven lbInstall.AddCssClass( "btn-xs" ); lbInstall.Text = "Requirements not met"; } + else if ( package.PackageUri.IsNullOrWhiteSpace() ) + { + lbInstall.Enabled = false; + lbInstall.AddCssClass( "small" ); + lbInstall.AddCssClass( "btn-xs" ); + lbInstall.Text = "Package doesn't exists."; + } + // If any packages can be installed we need to show the backup message. + nbBackupMessage.Visible = nbBackupMessage.Visible || lbInstall.Enabled; } } } @@ -340,176 +279,50 @@ protected void rptPackageVersions_ItemDataBound( object sender, RepeaterItemEven protected void rptPackageVersions_ItemCommand( object source, RepeaterCommandEventArgs e ) { string version = e.CommandArgument.ToString(); - Update( version ); + + hdnInstallVersion.Value = version; + litConfirmationMessage.Text = string.Format( "Are you sure you want to upgrade to Rock {0}?", RockVersion( new Version( version ) ) ); + mdConfirmInstall.Show(); } #endregion #region Methods - - /// - /// Checks the early access status of this organization. - /// - /// - private bool CheckEarlyAccess() - { - var client = new RestClient( string.Format( "{0}api/RockUpdate/GetEarlyAccessStatus", RockSiteUrl ) ); - var request = new RestRequest( Method.GET ); - request.RequestFormat = DataFormat.Json; - - request.AddParameter( "rockInstanceId", Rock.Web.SystemSettings.GetRockInstanceId() ); - IRestResponse response = client.Execute( request ); - if ( response.StatusCode == HttpStatusCode.OK || response.StatusCode == HttpStatusCode.Accepted ) - { - return response.Content.AsBoolean(); - } - - return false; - } - - /// - /// Gets the releases list from the rock server. - /// - /// - private List GetReleasesList() - { - List releases = new List(); - - var releaseProgram = ReleaseProgram.PRODUCTION; - var updateUrl = GlobalAttributesCache.Get().GetValue( "UpdateServerUrl" ); - if ( updateUrl.Contains( ReleaseProgram.ALPHA ) ) - { - releaseProgram = ReleaseProgram.ALPHA; - } - else if ( updateUrl.Contains( ReleaseProgram.BETA ) ) - { - releaseProgram = ReleaseProgram.BETA; - } - - var client = new RestClient( string.Format( "{0}/api/RockUpdate/GetReleasesList", RockSiteUrl ) ); - var request = new RestRequest( Method.GET ); - request.RequestFormat = DataFormat.Json; - - request.AddParameter( "rockInstanceId", Rock.Web.SystemSettings.GetRockInstanceId() ); - request.AddParameter( "releaseProgram", releaseProgram ); - IRestResponse response = client.Execute( request ); - if ( response.StatusCode == HttpStatusCode.OK || response.StatusCode == HttpStatusCode.Accepted ) - { - foreach ( var release in JsonConvert.DeserializeObject>( response.Content ) ) - { - releases.Add( release ); - } - } - - return releases; - } - - /// - /// Checks the .NET Framework version and returns Pass, Fail, or Unknown which can be - /// used to determine if it's safe to proceed. - /// - /// One of the values of the VersionCheckResult enum. - private DotNetVersionCheckResult CheckFrameworkVersion() - { - var result = DotNetVersionCheckResult.Fail; - try - { - // Once we get to 4.5 Microsoft recommends we test via the Registry... - result = RockUpdateHelper.CheckDotNetVersionFromRegistry(); - } - catch - { - // This would be pretty bad, but regardless we'll return - // the Unknown result and let the caller proceed with caution. - result = DotNetVersionCheckResult.Unknown; - } - - return result; - } - - /// - /// Checks the SQL server version and returns false if not at the needed - /// level to proceed. - /// - /// - private bool CheckSqlServerVersionGreaterThenSqlServer2012() - { - bool isOk = false; - string sqlVersion = ""; - try - { - sqlVersion = Rock.Data.DbService.ExecuteScaler( "SELECT SERVERPROPERTY('productversion')" ).ToString(); - string[] versionParts = sqlVersion.Split( '.' ); - - int majorVersion = -1; - Int32.TryParse( versionParts[0], out majorVersion ); - - if ( majorVersion > 11 ) - { - isOk = true; - } - } - catch - { - // This would be pretty bad, but regardless we'll just - // return the isOk (not) and let the caller proceed. - } - - return isOk; - } - /// /// Updates an existing Rock package to the given version and returns true if successful. /// /// true if the update was successful; false if errors were encountered protected bool UpdateRockPackage( string version ) { - IEnumerable errors = Enumerable.Empty(); + var errors = Enumerable.Empty(); + var rockUpdateService = new RockUpdateService(); try { - var update = NuGetService.SourceRepository.FindPackage( _rockPackageId, ( version != null ) ? SemanticVersion.Parse( version ) : null, false, false ); - var installed = NuGetService.GetInstalledPackage( _rockPackageId ); - - if ( installed == null ) - { - errors = NuGetService.InstallPackage( update ); - } - else - { - errors = NuGetService.UpdatePackageAndBackup( update, installed ); - } + var rockInstaller = new RockInstaller( rockUpdateService, new Version( version ), _installedVersion ); - CheckForManualFileMoves( version ); + var installedRelease = rockInstaller.InstallVersion(); - nbSuccess.Text = ConvertToHtmlLiWrappedUl( update.ReleaseNotes ).ConvertCrLfToHtmlBr(); - lSuccessVersion.Text = GetRockVersion( update.Version ); - - // Record the current version to the database - Rock.Web.SystemSettings.SetValue( Rock.SystemKey.SystemSetting.ROCK_INSTANCE_ID, version ); - - // register any new REST controllers - try - { - RestControllerService.RegisterControllers(); - } - catch ( Exception ex ) - { - LogException( ex ); - } + nbSuccess.Text = ConvertToHtmlLiWrappedUl( installedRelease.ReleaseNotes ).ConvertCrLfToHtmlBr(); + lSuccessVersion.Text = GetRockVersion( installedRelease.SemanticVersion ); } catch ( OutOfMemoryException ex ) { - errors = errors.Concat( new[] { string.Format( "There is a problem installing v{0}. It looks like your website ran out of memory. Check out this page for some assistance", version ) } ); + errors = errors.Concat( new[] { string.Format( "There is a problem installing v{0}. It looks like your website ran out of memory. Check out this page for some assistance", version ) } ); LogException( ex ); } catch ( System.Xml.XmlException ex ) { - errors = errors.Concat( new[] { string.Format( "There is a problem installing v{0}. It looks one of the standard XML files ({1}) may have been customized which prevented us from updating it. Check out this page for some assistance", version, ex.Message ) } ); + errors = errors.Concat( new[] { string.Format( "There is a problem installing v{0}. It looks one of the standard XML files ({1}) may have been customized which prevented us from updating it. Check out this page for some assistance", version, ex.Message ) } ); LogException( ex ); } - catch ( System.IO.IOException ex ) + catch ( IOException ex ) { - errors = errors.Concat( new[] { string.Format( "There is a problem installing v{0}. We were not able to replace an important file ({1}) after the update. Check out this page for some assistance", version, ex.Message ) } ); + errors = errors.Concat( new[] { string.Format( "There is a problem installing v{0}. We were not able to replace an important file ({1}) after the update. Check out this page for some assistance", version, ex.Message ) } ); + LogException( ex ); + } + catch ( VersionValidationException ex ) + { + errors = errors.Concat( new[] { ex.Message } ); LogException( ex ); } catch ( Exception ex ) @@ -527,6 +340,7 @@ protected bool UpdateRockPackage( string version ) else { pnlUpdateSuccess.Visible = true; + nbMoreUpdatesAvailable.Visible = GetOrderedReleaseList( rockUpdateService, new Version( version ) ).Count > 0; rptPackageVersions.Visible = false; return true; } @@ -543,10 +357,10 @@ protected void DisplayRockVersion() protected string GetRockVersion( object version ) { - var semanticVersion = version as SemanticVersion; + var semanticVersion = version as Version; if ( semanticVersion == null ) { - semanticVersion = new SemanticVersion( version.ToString() ); + semanticVersion = new Version( version.ToString() ); } if ( semanticVersion != null ) @@ -559,226 +373,17 @@ protected string GetRockVersion( object version ) } } - protected string RockVersion( SemanticVersion version ) + protected string RockVersion( Version version ) { - switch ( version.Version.Major ) + switch ( version.Major ) { case 1: - return string.Format( "McKinley {0}.{1}", version.Version.Minor, version.Version.Build ); + return string.Format( "McKinley {0}.{1}", version.Minor, version.Build ); default: - return string.Format( "{0}.{1}.{2}", version.Version.Major, version.Version.Minor, version.Version.Build ); - } - } - - /// - /// Determines if there is an update available to install and - /// puts the valid ones (that is those that meet the requirements) - /// into the _availablePackages list. - /// - /// true if updates are available; false otherwise - private bool IsUpdateAvailable() - { - List verifiedPackages = new List(); - try - { - // Get the installed package so we can check its version... - var installedPackage = NuGetService.GetInstalledPackage( _rockPackageId ); - if ( installedPackage == null ) - { - pnlNoUpdates.Visible = false; - pnlError.Visible = true; - nbErrors.Text = "No packages were found under App_Data\\Packages\\, so it's not possible to perform an update using this block."; - return false; - } - else - { - _installedVersion = installedPackage.Version; - } - - // Now go though all versions to find the newest, installable package - // taking into consideration that a package may require that an earlier package - // must already be installed -- in which case *that* package would be the - // newest, most installable one. - foreach ( IPackage package in _availablePackages ) - { - if ( package.Version <= _installedVersion ) - break; - - verifiedPackages.Add( package ); - - //if ( package.Tags != null && package.Tags.Contains( "requires-" ) ) - //{ - // var requiredVersion = ExtractRequiredVersionFromTags( package ); - // // if that required version is greater than our currently installed version - // // then we can't have any of the prior packages in the verifiedPackages list - // // so we clear it out and keep processing. - // if ( requiredVersion > _installedVersion ) - // { - - // verifiedPackages.Clear(); - // } - //} - } - - _availablePackages = verifiedPackages; - _numberOfAvailablePackages = verifiedPackages.Count; - if ( _numberOfAvailablePackages > 1 ) - { - nbMoreUpdatesAvailable.Visible = true; - } - } - catch ( InvalidOperationException ex ) - { - pnlNoUpdates.Visible = false; - pnlError.Visible = true; - lMessage.Text = string.Format( "
There is a problem with the packaging system. {0}
", ex.Message ); - } - - if ( verifiedPackages.Count > 0 ) - { - return true; - } - else - { - return false; - } - } - - /// - /// Extracts the required SemanticVersion from the package's tags. - /// - /// a Rock nuget package - /// the SemanticVersion of the package that this particular package requires - protected SemanticVersion ExtractRequiredVersionFromTags( IPackage package ) - { - Regex regex = new Regex( @"requires-([\.\d]+)" ); - if ( package.Tags != null ) - { - Match match = regex.Match( package.Tags ); - if ( match.Success ) - { - return new SemanticVersion( match.Groups[1].Value ); - } - } - - throw new ArgumentException( string.Format( "There is a malformed 'requires-' tag in a Rock package ({0})", package.Version ) ); - } - - /// - /// Removes the app_offline.htm file so the app can be used again. - /// - private void RemoveAppOffline() - { - var root = this.Request.PhysicalApplicationPath; - var file = System.IO.Path.Combine( root, "app_offline.htm" ); - System.IO.File.Delete( file ); - } - - /// - /// Copies the app_offline-template.htm file to app_offline.htm so no one else can hit the app. - /// If the template file does not exist an app_offline.htm file will be created from scratch. - /// - private void WriteAppOffline() - { - var root = this.Request.PhysicalApplicationPath; - - var templateFile = System.IO.Path.Combine( root, "app_offline-template.htm" ); - var offlineFile = System.IO.Path.Combine( root, "app_offline.htm" ); - - try - { - if ( File.Exists( templateFile ) ) - { - System.IO.File.Copy( templateFile, offlineFile, overwrite: true ); - } - else - { - CreateOfflineFileFromScratch( offlineFile ); - } - } - catch ( Exception ) - { - if ( !File.Exists( offlineFile ) ) - { - CreateOfflineFileFromScratch( offlineFile ); - } - } - } - - /// - /// Simply creates an app_offline.htm file so no one else can hit the app. - /// - private void CreateOfflineFileFromScratch( string offlineFile ) - { - System.IO.File.WriteAllText( offlineFile, @" - - - Application Updating... - - -

One Moment Please

- This application is undergoing an essential update and is temporarily offline. Please give me a minute or two to wrap things up. - - -" ); - } - - /// - /// Removes the old *.rdelete (Rock delete) files that were created during an update. - /// - private void RemoveOldRDeleteFiles() - { - var rockDirectory = new DirectoryInfo( Server.MapPath( "~" ) ); - - foreach ( var file in rockDirectory.EnumerateFiles( "*.rdelete", SearchOption.AllDirectories ) ) - { - try - { - file.Delete(); - } - catch - { - //we'll try again later - } - } - } - - private void CheckForManualFileMoves( string version ) - { - var versionDirectory = new DirectoryInfo( Server.MapPath( "~/App_Data/" + version ) ); - if ( versionDirectory.Exists ) - { - foreach ( var file in versionDirectory.EnumerateFiles( "*", SearchOption.AllDirectories ) ) - { - ManuallyMoveFile( file, file.FullName.Replace( @"\App_Data\" + version, "" ) ); - } - - versionDirectory.Delete( true ); + return string.Format( "{0}.{1}.{2}", version.Major, version.Minor, version.Build ); } } - private void ManuallyMoveFile( FileInfo file, string newPath ) - { - if ( newPath.EndsWith( ".dll" ) && !newPath.Contains( @"\bin\" ) ) - { - int fileCount = 0; - if ( File.Exists( newPath ) ) - { - // generate a unique *.#.rdelete filename - do - { - fileCount++; - } - while ( File.Exists( string.Format( "{0}.{1}.rdelete", newPath, fileCount ) ) ); - - string fileToDelete = string.Format( "{0}.{1}.rdelete", newPath, fileCount ); - File.Move( newPath, fileToDelete ); - } - } - - file.CopyTo( newPath, true ); - } - /// /// Converts + and * to html line items (li) wrapped in unordered lists (ul). /// @@ -832,51 +437,20 @@ public string ConvertToHtmlLiWrappedUl( string str ) /// * Public Web Address /// * Number of Active Records /// - /// As per https://www.rockrms.com/Rock/Impact + /// As per http://www.rockrms.com/Rock/Impact ///
/// the semantic version number private void SendStatictics( string version ) { try { - var rockContext = new RockContext(); - int numberOfActiveRecords = new PersonService( rockContext ).Queryable( includeDeceased: false, includeBusinesses: false ).Count(); - - if ( numberOfActiveRecords > 100 || !Rock.Web.SystemSettings.GetValue( Rock.SystemKey.SystemSetting.SAMPLEDATA_DATE ).AsDateTime().HasValue ) + var ipAddress = Request.ServerVariables["LOCAL_ADDR"]; + var environmentData = RockUpdateHelper.GetEnvDataAsJson( Request, ResolveRockUrl( "~/" ) ); + using ( var rockContext = new RockContext() ) { - string organizationName = string.Empty; - ImpactLocation organizationLocation = null; - string publicUrl = string.Empty; + var instanceStatistics = new RockInstanceImpactStatistics( new RockImpactService(), rockContext ); - var rockInstanceId = Rock.Web.SystemSettings.GetRockInstanceId(); - var ipAddress = Request.ServerVariables["LOCAL_ADDR"]; - - if ( cbIncludeStats.Checked ) - { - var globalAttributes = GlobalAttributesCache.Get(); - organizationName = globalAttributes.GetValue( "OrganizationName" ); - publicUrl = globalAttributes.GetValue( "PublicApplicationRoot" ); - - // Fetch the organization address - var organizationAddressLocationGuid = globalAttributes.GetValue( "OrganizationAddress" ).AsGuid(); - if ( !organizationAddressLocationGuid.Equals( Guid.Empty ) ) - { - var location = new Rock.Model.LocationService( rockContext ).Get( organizationAddressLocationGuid ); - if ( location != null ) - { - organizationLocation = new ImpactLocation( location ); - } - } - } - else - { - numberOfActiveRecords = 0; - } - - var environmentData = Rock.Web.Utilities.RockUpdateHelper.GetEnvDataAsJson( Request, ResolveRockUrl( "~/" ) ); - - // now send them to SDN/Rock server - SendToSpark( rockInstanceId, version, ipAddress, publicUrl, organizationName, organizationLocation, numberOfActiveRecords, environmentData ); + instanceStatistics.SendImpactStatisticsToSpark( cbIncludeStats.Checked, version, ipAddress, environmentData ); } } catch ( Exception ex ) @@ -891,126 +465,12 @@ private void SendStatictics( string version ) } } - /// - /// Sends the public data and impact statistics to the Rock server. - /// - /// The rock instance identifier. - /// The version. - /// The ip address. - /// The public URL. - /// Name of the organization. - /// The organization location. - /// The number of active records. - /// The environment data (JSON). - private void SendToSpark( Guid rockInstanceId, string version, string ipAddress, string publicUrl, string organizationName, ImpactLocation organizationLocation, int numberOfActiveRecords, string environmentData ) - { - ImpactStatistic impactStatistic = new ImpactStatistic() - { - RockInstanceId = rockInstanceId, - Version = version, - IpAddress = ipAddress, - PublicUrl = publicUrl, - OrganizationName = organizationName, - OrganizationLocation = organizationLocation, - NumberOfActiveRecords = numberOfActiveRecords, - EnvironmentData = environmentData - }; - - var client = new RestClient( string.Format( "{0}/api/impacts/save", RockSiteUrl ) ); - var request = new RestRequest( Method.POST ); - request.RequestFormat = DataFormat.Json; - request.AddBody( impactStatistic ); - var response = client.Execute( request ); - } - - /// - /// Sets up the page to report the error in a nicer manner. - /// - /// - private void HandleNuGetException( Exception ex ) - { - pnlError.Visible = true; - pnlUpdateSuccess.Visible = false; - pnlNoUpdates.Visible = false; - nbErrors.NotificationBoxType = Rock.Web.UI.Controls.NotificationBoxType.Danger; - - if ( ex.Message.Contains( "404" ) ) - { - nbErrors.Text = string.Format( "It appears that someone configured your UpdateServerUrl setting incorrectly: {0}", GlobalAttributesCache.Get().GetValue( "UpdateServerUrl" ) ); - } - else if ( ex.Message.Contains( "could not be resolved" ) ) - { - nbErrors.Text = string.Format( "I think either the update server is down or your UpdateServerUrl setting is incorrect: {0}", GlobalAttributesCache.Get().GetValue( "UpdateServerUrl" ) ); - } - else if ( ex.Message.Contains( "Unable to connect" ) || ex.Message.Contains( "(503)" ) ) - { - nbErrors.NotificationBoxType = Rock.Web.UI.Controls.NotificationBoxType.Warning; - nbErrors.Text = "The update server is currently unavailable (possibly undergoing maintenance). Please try again later."; - } - else - { - nbErrors.Text = string.Format( "...actually, I'm not sure what happened here: {0}", ex.Message ); - } - } - #endregion - } - - [Serializable] - public class ImpactStatistic - { - public Guid RockInstanceId { get; set; } - public string Version { get; set; } - public string IpAddress { get; set; } - public string PublicUrl { get; set; } - public string OrganizationName { get; set; } - public ImpactLocation OrganizationLocation { get; set; } - public int NumberOfActiveRecords { get; set; } - public string EnvironmentData { get; set; } - } - [Serializable] - public class ImpactLocation - { - public string Street1 { get; set; } - public string Street2 { get; set; } - public string City { get; set; } - public string State { get; set; } - public string PostalCode { get; set; } - public string Country { get; set; } - - public ImpactLocation( Rock.Model.Location location ) + protected void mdConfirmInstall_SaveClick( object sender, EventArgs e ) { - Street1 = location.Street1; - Street2 = location.Street2; - City = location.City; - State = location.State; - PostalCode = location.PostalCode; - Country = location.Country; + mdConfirmInstall.Hide(); + Update( hdnInstallVersion.Value ); } } - - /// - /// Represents the bits the Rock system stores regarding a particular release. - /// - [Serializable] - public class Release - { - public string SemanticVersion { get; set; } - public string Version { get; set; } - public DateTime? ReleaseDate { get; set; } - public string Summary { get; set; } - public bool RequiresEarlyAccess { get; set; } - public string RequiresVersion { get; set; } - } - - /// - /// One of three options that represent which release 'program' one can be on. - /// - public static class ReleaseProgram - { - public static readonly string ALPHA = "alpha"; - public static readonly string BETA = "beta"; - public static readonly string PRODUCTION = "production"; - } } \ No newline at end of file diff --git a/RockWeb/Blocks/Core/RockUpdateLegacy.ascx b/RockWeb/Blocks/Core/RockUpdateLegacy.ascx new file mode 100644 index 00000000000..3a3aab18fcd --- /dev/null +++ b/RockWeb/Blocks/Core/RockUpdateLegacy.ascx @@ -0,0 +1,168 @@ +<%@ Control Language="C#" AutoEventWireup="true" CodeFile="RockUpdateLegacy.ascx.cs" Inherits="RockWeb.Blocks.Core.RockUpdateLegacy" %> +<%@ Import namespace="Rock" %> + + + + +
+
+

Rock Update

+
+
+ + .NET Framework Update Required +

As of Rock McKinley v13, Rock requires Microsoft .NET Framework 4.7.2 or greater on the hosting server. + This framework version was released by Microsoft on April 30th, 2018.

+
+ +

Microsoft SQL Server Update Required

+

+ As of Rock McKinley v11, Rock requires SQL Server 2014 or greater. + You will need to upgrade your database in order to proceed with that update. +

+
+ +
+

Everything Is Shipshape

+ +

You run a tight ship, there is nothing to update since . Check back soon as we're working hard on something amazing or + check out the release notes. +

+
+
+ + +
+

New Pieces Available

+ +

We've expanded the puzzle, let's get you up-to-date.

+
+ + + You're using a beta or alpha update repository. + + + We strongly urge you to backup your database and website before updating Rock. + The changes that are made during the update process can't be undone. + Also, be patient when updating. It takes anywhere from a few seconds + to several minutes depending on the update size and your download speed. +
+ + +
+
+
+ +
+
+ + +

+ Community Contributors have early access to major releases of Rock. Find out + how to get early access to releases as a Community Contributor. + + If you are already a Community Contributor and are having trouble with your access, + let us know so we can resolve the problem. +

+
+ + Thank you for being a Community Contributor! Learn more about the Early Access program. + +
+
+
+ + + +
+
+ +
+
+
+ +
+
+
+ + + +
+
+

+
+
+
+
+ Install +
+
+ + +
+
Release Notes
+ + +
+
+
+
+
+
+
+
+ + + + + +
+

Eureka, Pay Dirt!

+ +

Update completed successfully... You're now running .

+ +
+ Below is a summary of the new toys you have to play with... + +
+ + + +
+ +
+ + +
+

Whoa... That Wasn't Supposed To Happen

+ +

An error occurred during the update process.

+
+ + + + +
+
+
+ +
+
+ + \ No newline at end of file diff --git a/RockWeb/Blocks/Core/RockUpdateLegacy.ascx.cs b/RockWeb/Blocks/Core/RockUpdateLegacy.ascx.cs new file mode 100644 index 00000000000..450f52039dd --- /dev/null +++ b/RockWeb/Blocks/Core/RockUpdateLegacy.ascx.cs @@ -0,0 +1,1018 @@ +// +// Copyright by the Spark Development Network +// +// Licensed under the Rock Community License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.rockrms.com/license +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Configuration; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Text.RegularExpressions; +using System.Web.UI; +using System.Web.UI.HtmlControls; +using System.Web.UI.WebControls; +using Newtonsoft.Json; +using NuGet; +using RestSharp; + +using Rock; +using Rock.Data; +using Rock.Model; +using Rock.Update; +using Rock.Update.Enum; +using Rock.Services.NuGet; +using Rock.VersionInfo; +using Rock.Web.Cache; +using Rock.Update.Helpers; + +namespace RockWeb.Blocks.Core +{ + [DisplayName( "Rock Update Legacy" )] + [Category( "Core" )] + [Description( "Handles checking for and performing upgrades to the Rock system." )] + public partial class RockUpdateLegacy : Rock.Web.UI.RockBlock + { + #region Fields + private bool _isEarlyAccessOrganization = false; + private List _releases = new List(); + WebProjectManager nuGetService = null; + private string _rockPackageId = "Rock"; + IEnumerable _availablePackages = null; + SemanticVersion _installedVersion = new SemanticVersion( "0.0.0" ); + private int _numberOfAvailablePackages = 0; + private bool _isOkToProceed = false; + private bool _hasSqlServer14OrHigher = false; + #endregion + + #region Properties + + private string RockSiteUrl { get { return ConfigurationManager.AppSettings["RockStoreUrl"].EnsureTrailingForwardslash(); } } + + /// + /// Obtains a WebProjectManager from the Global "UpdateServerUrl" Attribute. + /// + /// + /// The NuGet service or null if no valid service could be found using the UpdateServerUrl. + /// + protected WebProjectManager NuGetService + { + get + { + if ( nuGetService == null ) + { + var globalAttributesCache = GlobalAttributesCache.Get(); + string packageSource = globalAttributesCache.GetValue( "UpdateServerUrl" ); + if ( packageSource.ToLowerInvariant().Contains( "rockalpha" ) || packageSource.ToLowerInvariant().Contains( "rockbeta" ) ) + { + nbRepoWarning.Visible = true; + } + + // Since you can use a URL or a local path, we can't just check for valid URI + try + { + string siteRoot = Request.MapPath( "~/" ); + nuGetService = new WebProjectManager( packageSource, siteRoot ); + } + catch + { + // if caught, we will return a null nuGetService + } + } + return nuGetService; + } + } + + #endregion + + #region Base Control Methods + + /// + /// Raises the event. + /// + /// An object that contains the event data. + protected override void OnInit( EventArgs e ) + { + base.OnInit( e ); + + string script = @" + $('#btn-restart').on('click', function () { + var btn = $(this); + btn.button('loading'); + location = location.href; + }); +"; + ScriptManager.RegisterStartupScript( pnlUpdateSuccess, pnlUpdateSuccess.GetType(), "restart-script", script, true ); + } + + /// + /// Invoked on page load. + /// + /// An object that contains the event data. + protected override void OnLoad( EventArgs e ) + { + // Set timeout for up to 15 minutes (just like installer) + Server.ScriptTimeout = 900; + ScriptManager.GetCurrent( Page ).AsyncPostBackTimeout = 900; + + DisplayRockVersion(); + if ( !IsPostBack ) + { + if ( NuGetService == null ) + { + pnlNoUpdates.Visible = false; + pnlError.Visible = true; + nbErrors.Text = string.Format( "Your UpdateServerUrl is not valid. It is currently set to: {0}", GlobalAttributesCache.Get().GetValue( "UpdateServerUrl" ) ); + } + else + { + try + { + _isEarlyAccessOrganization = CheckEarlyAccess(); + + btnIssues.NavigateUrl = string.Format( "{0}earlyaccessissues?RockInstanceId={1}", RockSiteUrl, Rock.Web.SystemSettings.GetRockInstanceId() ); + + if ( _isEarlyAccessOrganization ) + { + hlblEarlyAccess.LabelType = Rock.Web.UI.Controls.LabelType.Success; + hlblEarlyAccess.Text = "Early Access: Enabled"; + + pnlEarlyAccessNotEnabled.Visible = false; + pnlEarlyAccessEnabled.Visible = true; + } + + var result = CheckFrameworkVersion(); + + _isOkToProceed = true; + + if ( result == DotNetVersionCheckResult.Fail ) + { + + nbVersionIssue.Visible = true; + nbVersionIssue.Text += "

You will need to upgrade your hosting server in order to proceed with the v13 update.

"; + nbBackupMessage.Visible = false; + } + else if ( result == DotNetVersionCheckResult.Unknown ) + { + nbVersionIssue.Visible = true; + nbVersionIssue.Text += "

You may need to upgrade your hosting server in order to proceed with the v13 update. We were unable to determine which Framework version your server is using.

"; + nbVersionIssue.Details += "
We were unable to check your server to verify that the .Net 4.7.2 Framework is installed! You MUST verify this manually before you proceed with the update otherwise your Rock application will be broken until you update the server.
"; + nbBackupMessage.Visible = false; + } + + _hasSqlServer14OrHigher = CheckSqlServerVersionGreaterThenSqlServer2012(); + if ( !_hasSqlServer14OrHigher ) + { + nbSqlServerVersionIssue.Visible = true; + } + + _releases = GetReleasesList(); + _availablePackages = NuGetService.SourceRepository.FindPackagesById( _rockPackageId ).OrderByDescending( p => p.Version ); + if ( IsUpdateAvailable() ) + { + pnlUpdatesAvailable.Visible = true; + pnlUpdates.Visible = true; + pnlNoUpdates.Visible = false; + cbIncludeStats.Visible = true; + BindGrid(); + } + + RemoveOldRDeleteFiles(); + } + catch ( System.InvalidOperationException ex ) + { + HandleNuGetException( ex ); + } + } + } + } + + #endregion + + #region Events + + /// + /// Bind the available packages to the repeater. + /// + private void BindGrid() + { + rptPackageVersions.DataSource = _availablePackages; + rptPackageVersions.DataBind(); + } + + /// + /// Wraps the install or update process in some guarded code while putting the app in "offline" + /// mode and then back "online" when it's complete. + /// + /// the semantic version number + private void Update( string version ) + { + var targetVersion = new SemanticVersion( version ); + if ( !CanInstallVersion( targetVersion ) ) + { + nbErrors.Text = "
  • This version cannot be installed because not all requirements have been met.
"; + pnlError.Visible = true; + pnlUpdateSuccess.Visible = false; + return; + } + + WriteAppOffline(); + try + { + pnlUpdatesAvailable.Visible = false; + pnlUpdates.Visible = false; + + if ( !UpdateRockPackage( version ) ) + { + pnlError.Visible = true; + pnlUpdateSuccess.Visible = false; + } + else + { + SendStatictics( version ); + } + + lRockVersion.Text = ""; + } + catch ( Exception ex ) + { + pnlError.Visible = true; + pnlUpdateSuccess.Visible = false; + nbErrors.Text = string.Format( "Something went wrong. Although the errors were written to the error log, they are listed for your review:
{0}", ex.Message ); + LogException( ex ); + } + RemoveAppOffline(); + } + + private bool CanInstallVersion( SemanticVersion targetVersion ) + { + var requiresSqlServer14OrHigher = targetVersion.Version.Major > 1 || targetVersion.Version.Minor > 10; + var requiresNet472 = targetVersion.Version.Major > 1 || targetVersion.Version.Minor > 12; + + if ( !requiresSqlServer14OrHigher ) + { + return true; + } + + var hasSqlServer2012OrGreater = CheckSqlServerVersionGreaterThenSqlServer2012(); + if ( requiresNet472 ) + { + var result = CheckFrameworkVersion(); + if ( result == DotNetVersionCheckResult.Fail ) + { + return false; + } + } + + return hasSqlServer2012OrGreater; + } + + /// + /// Enables and sets the appropriate CSS class on the install buttons and each div panel. + /// + /// The source of the event. + /// The instance containing the event data. + protected void rptPackageVersions_ItemDataBound( object sender, RepeaterItemEventArgs e ) + { + if ( e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem ) + { + IPackage package = e.Item.DataItem as IPackage; + if ( package != null ) + { + LinkButton lbInstall = e.Item.FindControl( "lbInstall" ) as LinkButton; + var divPanel = e.Item.FindControl( "divPanel" ) as HtmlGenericControl; + + var requiredVersion = ExtractRequiredVersionFromTags( package ); + if ( requiredVersion <= _installedVersion ) + { + var release = _releases.Where( r => r.SemanticVersion == package.Version.ToString() ).FirstOrDefault(); + if ( !_isEarlyAccessOrganization && release != null && release.RequiresEarlyAccess ) + { + lbInstall.Enabled = false; + lbInstall.Text = "Available to Early
Access Organizations"; + lbInstall.AddCssClass( "btn-warning" ); + divPanel.AddCssClass( "panel-block" ); + } + else + { + lbInstall.Enabled = true; + lbInstall.AddCssClass( "btn-info" ); + divPanel.AddCssClass( "panel-info" ); + } + } + else + { + lbInstall.Enabled = false; + lbInstall.Text = "Pending"; + lbInstall.AddCssClass( "btn-default" ); + divPanel.AddCssClass( "panel-block" ); + } + + if ( !_isOkToProceed || !CanInstallVersion( package.Version ) ) + { + lbInstall.Enabled = false; + lbInstall.AddCssClass( "btn-danger" ); + lbInstall.AddCssClass( "small" ); + lbInstall.AddCssClass( "btn-xs" ); + lbInstall.Text = "Requirements not met"; + } + } + } + } + + /// + /// Handles the click event for the particular version button that was clicked. + /// + /// The source of the event. + /// The instance containing the event data. + protected void rptPackageVersions_ItemCommand( object source, RepeaterCommandEventArgs e ) + { + string version = e.CommandArgument.ToString(); + Update( version ); + } + + #endregion + + #region Methods + + /// + /// Checks the early access status of this organization. + /// + /// + private bool CheckEarlyAccess() + { + var client = new RestClient( string.Format( "{0}api/RockUpdate/GetEarlyAccessStatus", RockSiteUrl ) ); + var request = new RestRequest( Method.GET ); + request.RequestFormat = DataFormat.Json; + + request.AddParameter( "rockInstanceId", Rock.Web.SystemSettings.GetRockInstanceId() ); + IRestResponse response = client.Execute( request ); + if ( response.StatusCode == HttpStatusCode.OK || response.StatusCode == HttpStatusCode.Accepted ) + { + return response.Content.AsBoolean(); + } + + return false; + } + + /// + /// Gets the releases list from the rock server. + /// + /// + private List GetReleasesList() + { + List releases = new List(); + + var releaseProgram = ReleaseProgram.PRODUCTION; + var updateUrl = GlobalAttributesCache.Get().GetValue( "UpdateServerUrl" ); + if ( updateUrl.Contains( ReleaseProgram.ALPHA ) ) + { + releaseProgram = ReleaseProgram.ALPHA; + } + else if ( updateUrl.Contains( ReleaseProgram.BETA ) ) + { + releaseProgram = ReleaseProgram.BETA; + } + + var client = new RestClient( string.Format( "{0}/api/RockUpdate/GetReleasesList", RockSiteUrl ) ); + var request = new RestRequest( Method.GET ); + request.RequestFormat = DataFormat.Json; + + request.AddParameter( "rockInstanceId", Rock.Web.SystemSettings.GetRockInstanceId() ); + request.AddParameter( "releaseProgram", releaseProgram ); + IRestResponse response = client.Execute( request ); + if ( response.StatusCode == HttpStatusCode.OK || response.StatusCode == HttpStatusCode.Accepted ) + { + foreach ( var release in JsonConvert.DeserializeObject>( response.Content ) ) + { + releases.Add( release ); + } + } + + return releases; + } + + /// + /// Checks the .NET Framework version and returns Pass, Fail, or Unknown which can be + /// used to determine if it's safe to proceed. + /// + /// One of the values of the VersionCheckResult enum. + private DotNetVersionCheckResult CheckFrameworkVersion() + { + var result = DotNetVersionCheckResult.Fail; + try + { + // Once we get to 4.5 Microsoft recommends we test via the Registry... + result = RockUpdateHelper.CheckDotNetVersionFromRegistry(); + } + catch + { + // This would be pretty bad, but regardless we'll return + // the Unknown result and let the caller proceed with caution. + result = DotNetVersionCheckResult.Unknown; + } + + return result; + } + + /// + /// Checks the SQL server version and returns false if not at the needed + /// level to proceed. + /// + /// + private bool CheckSqlServerVersionGreaterThenSqlServer2012() + { + bool isOk = false; + string sqlVersion = ""; + try + { + sqlVersion = Rock.Data.DbService.ExecuteScaler( "SELECT SERVERPROPERTY('productversion')" ).ToString(); + string[] versionParts = sqlVersion.Split( '.' ); + + int majorVersion = -1; + Int32.TryParse( versionParts[0], out majorVersion ); + + if ( majorVersion > 11 ) + { + isOk = true; + } + } + catch + { + // This would be pretty bad, but regardless we'll just + // return the isOk (not) and let the caller proceed. + } + + return isOk; + } + + /// + /// Updates an existing Rock package to the given version and returns true if successful. + /// + /// true if the update was successful; false if errors were encountered + protected bool UpdateRockPackage( string version ) + { + IEnumerable errors = Enumerable.Empty(); + try + { + var update = NuGetService.SourceRepository.FindPackage( _rockPackageId, ( version != null ) ? SemanticVersion.Parse( version ) : null, false, false ); + var installed = NuGetService.GetInstalledPackage( _rockPackageId ); + + if ( installed == null ) + { + errors = NuGetService.InstallPackage( update ); + } + else + { + errors = NuGetService.UpdatePackageAndBackup( update, installed ); + } + + CheckForManualFileMoves( version ); + + nbSuccess.Text = ConvertToHtmlLiWrappedUl( update.ReleaseNotes ).ConvertCrLfToHtmlBr(); + lSuccessVersion.Text = GetRockVersion( update.Version ); + + // Record the current version to the database + Rock.Web.SystemSettings.SetValue( Rock.SystemKey.SystemSetting.ROCK_INSTANCE_ID, version ); + + // register any new REST controllers + try + { + RestControllerService.RegisterControllers(); + } + catch ( Exception ex ) + { + LogException( ex ); + } + } + catch ( OutOfMemoryException ex ) + { + errors = errors.Concat( new[] { string.Format( "There is a problem installing v{0}. It looks like your website ran out of memory. Check out this page for some assistance", version ) } ); + LogException( ex ); + } + catch ( System.Xml.XmlException ex ) + { + errors = errors.Concat( new[] { string.Format( "There is a problem installing v{0}. It looks one of the standard XML files ({1}) may have been customized which prevented us from updating it. Check out this page for some assistance", version, ex.Message ) } ); + LogException( ex ); + } + catch ( System.IO.IOException ex ) + { + errors = errors.Concat( new[] { string.Format( "There is a problem installing v{0}. We were not able to replace an important file ({1}) after the update. Check out this page for some assistance", version, ex.Message ) } ); + LogException( ex ); + } + catch ( Exception ex ) + { + errors = errors.Concat( new[] { string.Format( "There is a problem installing v{0}: {1}", version, ex.Message ) } ); + LogException( ex ); + } + + if ( errors != null && errors.Count() > 0 ) + { + pnlError.Visible = true; + nbErrors.Text = errors.Aggregate( new StringBuilder( "
    " ), ( sb, s ) => sb.AppendFormat( "
  • {0}
  • ", s ) ).Append( "
" ).ToString(); + return false; + } + else + { + pnlUpdateSuccess.Visible = true; + rptPackageVersions.Visible = false; + return true; + } + } + + /// + /// Fetches and displays the official Rock product version. + /// + protected void DisplayRockVersion() + { + lRockVersion.Text = string.Format( "Current Version: {0}", VersionInfo.GetRockProductVersionFullName() ); + lNoUpdateVersion.Text = VersionInfo.GetRockProductVersionFullName(); + } + + protected string GetRockVersion( object version ) + { + var semanticVersion = version as SemanticVersion; + if ( semanticVersion == null ) + { + semanticVersion = new SemanticVersion( version.ToString() ); + } + + if ( semanticVersion != null ) + { + return "Rock " + RockVersion( semanticVersion ); + } + else + { + return string.Empty; + } + } + + protected string RockVersion( SemanticVersion version ) + { + switch ( version.Version.Major ) + { + case 1: + return string.Format( "McKinley {0}.{1}", version.Version.Minor, version.Version.Build ); + default: + return string.Format( "{0}.{1}.{2}", version.Version.Major, version.Version.Minor, version.Version.Build ); + } + } + + /// + /// Determines if there is an update available to install and + /// puts the valid ones (that is those that meet the requirements) + /// into the _availablePackages list. + /// + /// true if updates are available; false otherwise + private bool IsUpdateAvailable() + { + List verifiedPackages = new List(); + try + { + // Get the installed package so we can check its version... + var installedPackage = NuGetService.GetInstalledPackage( _rockPackageId ); + if ( installedPackage == null ) + { + pnlNoUpdates.Visible = false; + pnlError.Visible = true; + nbErrors.Text = "No packages were found under App_Data\\Packages\\, so it's not possible to perform an update using this block."; + return false; + } + else + { + _installedVersion = installedPackage.Version; + } + + // Now go though all versions to find the newest, installable package + // taking into consideration that a package may require that an earlier package + // must already be installed -- in which case *that* package would be the + // newest, most installable one. + foreach ( IPackage package in _availablePackages ) + { + if ( package.Version <= _installedVersion ) + break; + + verifiedPackages.Add( package ); + + //if ( package.Tags != null && package.Tags.Contains( "requires-" ) ) + //{ + // var requiredVersion = ExtractRequiredVersionFromTags( package ); + // // if that required version is greater than our currently installed version + // // then we can't have any of the prior packages in the verifiedPackages list + // // so we clear it out and keep processing. + // if ( requiredVersion > _installedVersion ) + // { + + // verifiedPackages.Clear(); + // } + //} + } + + _availablePackages = verifiedPackages; + _numberOfAvailablePackages = verifiedPackages.Count; + if ( _numberOfAvailablePackages > 1 ) + { + nbMoreUpdatesAvailable.Visible = true; + } + } + catch ( InvalidOperationException ex ) + { + pnlNoUpdates.Visible = false; + pnlError.Visible = true; + lMessage.Text = string.Format( "
There is a problem with the packaging system. {0}
", ex.Message ); + } + + if ( verifiedPackages.Count > 0 ) + { + return true; + } + else + { + return false; + } + } + + /// + /// Extracts the required SemanticVersion from the package's tags. + /// + /// a Rock nuget package + /// the SemanticVersion of the package that this particular package requires + protected SemanticVersion ExtractRequiredVersionFromTags( IPackage package ) + { + Regex regex = new Regex( @"requires-([\.\d]+)" ); + if ( package.Tags != null ) + { + Match match = regex.Match( package.Tags ); + if ( match.Success ) + { + return new SemanticVersion( match.Groups[1].Value ); + } + } + + throw new ArgumentException( string.Format( "There is a malformed 'requires-' tag in a Rock package ({0})", package.Version ) ); + } + + /// + /// Removes the app_offline.htm file so the app can be used again. + /// + private void RemoveAppOffline() + { + var root = this.Request.PhysicalApplicationPath; + var file = System.IO.Path.Combine( root, "app_offline.htm" ); + System.IO.File.Delete( file ); + } + + /// + /// Copies the app_offline-template.htm file to app_offline.htm so no one else can hit the app. + /// If the template file does not exist an app_offline.htm file will be created from scratch. + /// + private void WriteAppOffline() + { + var root = this.Request.PhysicalApplicationPath; + + var templateFile = System.IO.Path.Combine( root, "app_offline-template.htm" ); + var offlineFile = System.IO.Path.Combine( root, "app_offline.htm" ); + + try + { + if ( File.Exists( templateFile ) ) + { + System.IO.File.Copy( templateFile, offlineFile, overwrite: true ); + } + else + { + CreateOfflineFileFromScratch( offlineFile ); + } + } + catch ( Exception ) + { + if ( !File.Exists( offlineFile ) ) + { + CreateOfflineFileFromScratch( offlineFile ); + } + } + } + + /// + /// Simply creates an app_offline.htm file so no one else can hit the app. + /// + private void CreateOfflineFileFromScratch( string offlineFile ) + { + System.IO.File.WriteAllText( offlineFile, @" + + + Application Updating... + + +

One Moment Please

+ This application is undergoing an essential update and is temporarily offline. Please give me a minute or two to wrap things up. + + +" ); + } + + /// + /// Removes the old *.rdelete (Rock delete) files that were created during an update. + /// + private void RemoveOldRDeleteFiles() + { + var rockDirectory = new DirectoryInfo( Server.MapPath( "~" ) ); + + foreach ( var file in rockDirectory.EnumerateFiles( "*.rdelete", SearchOption.AllDirectories ) ) + { + try + { + file.Delete(); + } + catch + { + //we'll try again later + } + } + } + + private void CheckForManualFileMoves( string version ) + { + var versionDirectory = new DirectoryInfo( Server.MapPath( "~/App_Data/" + version ) ); + if ( versionDirectory.Exists ) + { + foreach ( var file in versionDirectory.EnumerateFiles( "*", SearchOption.AllDirectories ) ) + { + ManuallyMoveFile( file, file.FullName.Replace( @"\App_Data\" + version, "" ) ); + } + + versionDirectory.Delete( true ); + } + } + + private void ManuallyMoveFile( FileInfo file, string newPath ) + { + if ( newPath.EndsWith( ".dll" ) && !newPath.Contains( @"\bin\" ) ) + { + int fileCount = 0; + if ( File.Exists( newPath ) ) + { + // generate a unique *.#.rdelete filename + do + { + fileCount++; + } + while ( File.Exists( string.Format( "{0}.{1}.rdelete", newPath, fileCount ) ) ); + + string fileToDelete = string.Format( "{0}.{1}.rdelete", newPath, fileCount ); + File.Move( newPath, fileToDelete ); + } + } + + file.CopyTo( newPath, true ); + } + + /// + /// Converts + and * to html line items (li) wrapped in unordered lists (ul). + /// + /// a string that contains lines that start with + or * + /// an html string of li wrapped in ul + public string ConvertToHtmlLiWrappedUl( string str ) + { + if ( str == null ) + { + return string.Empty; + } + + bool foundMatch = false; + + // Lines that start with "+ *" or "+" or "*" + var re = new System.Text.RegularExpressions.Regex( @"^\s*(\+ \* |[\+\*]+)(.*)" ); + var htmlBuilder = new StringBuilder(); + + // split the string on newlines... + string[] splits = str.Split( new[] { Environment.NewLine, "\x0A" }, StringSplitOptions.RemoveEmptyEntries ); + // look at each line to see if it starts with a + or * and then strip it and wrap it in
  • + for ( int i = 0; i < splits.Length; i++ ) + { + var match = re.Match( splits[i] ); + if ( match.Success ) + { + foundMatch = true; + htmlBuilder.AppendFormat( "
  • {0}
  • ", match.Groups[2] ); + } + else + { + htmlBuilder.Append( splits[i] ); + } + } + + // if we had a match then wrap it in
      markup + return foundMatch ? string.Format( "
        {0}
      ", htmlBuilder.ToString() ) : htmlBuilder.ToString(); + } + + /// + /// Sends statistics to the SDN server but only if there are more than 100 person records + /// or the sample data has not been loaded. + /// + /// The statistics are: + /// * Rock Instance Id + /// * Update Version + /// * IP Address - The IP address of your Rock server. + /// + /// ...and we only send these if they checked the "Include Impact Statistics": + /// * Organization Name and Address + /// * Public Web Address + /// * Number of Active Records + /// + /// As per https://www.rockrms.com/Rock/Impact + /// + /// the semantic version number + private void SendStatictics( string version ) + { + try + { + var rockContext = new RockContext(); + int numberOfActiveRecords = new PersonService( rockContext ).Queryable( includeDeceased: false, includeBusinesses: false ).Count(); + + if ( numberOfActiveRecords > 100 || !Rock.Web.SystemSettings.GetValue( Rock.SystemKey.SystemSetting.SAMPLEDATA_DATE ).AsDateTime().HasValue ) + { + string organizationName = string.Empty; + ImpactLocation organizationLocation = null; + string publicUrl = string.Empty; + + var rockInstanceId = Rock.Web.SystemSettings.GetRockInstanceId(); + var ipAddress = Request.ServerVariables["LOCAL_ADDR"]; + + if ( cbIncludeStats.Checked ) + { + var globalAttributes = GlobalAttributesCache.Get(); + organizationName = globalAttributes.GetValue( "OrganizationName" ); + publicUrl = globalAttributes.GetValue( "PublicApplicationRoot" ); + + // Fetch the organization address + var organizationAddressLocationGuid = globalAttributes.GetValue( "OrganizationAddress" ).AsGuid(); + if ( !organizationAddressLocationGuid.Equals( Guid.Empty ) ) + { + var location = new Rock.Model.LocationService( rockContext ).Get( organizationAddressLocationGuid ); + if ( location != null ) + { + organizationLocation = new ImpactLocation( location ); + } + } + } + else + { + numberOfActiveRecords = 0; + } + + var environmentData = RockUpdateHelper.GetEnvDataAsJson( Request, ResolveRockUrl( "~/" ) ); + + // now send them to SDN/Rock server + SendToSpark( rockInstanceId, version, ipAddress, publicUrl, organizationName, organizationLocation, numberOfActiveRecords, environmentData ); + } + } + catch ( Exception ex ) + { + // Just catch any exceptions, log it, and keep moving... We don't want to mess up the experience + // over a few statistics/metrics. + try + { + LogException( ex ); + } + catch { } + } + } + + /// + /// Sends the public data and impact statistics to the Rock server. + /// + /// The rock instance identifier. + /// The version. + /// The ip address. + /// The public URL. + /// Name of the organization. + /// The organization location. + /// The number of active records. + /// The environment data (JSON). + private void SendToSpark( Guid rockInstanceId, string version, string ipAddress, string publicUrl, string organizationName, ImpactLocation organizationLocation, int numberOfActiveRecords, string environmentData ) + { + ImpactStatistic impactStatistic = new ImpactStatistic() + { + RockInstanceId = rockInstanceId, + Version = version, + IpAddress = ipAddress, + PublicUrl = publicUrl, + OrganizationName = organizationName, + OrganizationLocation = organizationLocation, + NumberOfActiveRecords = numberOfActiveRecords, + EnvironmentData = environmentData + }; + + var client = new RestClient( string.Format( "{0}/api/impacts/save", RockSiteUrl ) ); + var request = new RestRequest( Method.POST ); + request.RequestFormat = DataFormat.Json; + request.AddBody( impactStatistic ); + var response = client.Execute( request ); + } + + /// + /// Sets up the page to report the error in a nicer manner. + /// + /// + private void HandleNuGetException( Exception ex ) + { + pnlError.Visible = true; + pnlUpdateSuccess.Visible = false; + pnlNoUpdates.Visible = false; + nbErrors.NotificationBoxType = Rock.Web.UI.Controls.NotificationBoxType.Danger; + + if ( ex.Message.Contains( "404" ) ) + { + nbErrors.Text = string.Format( "It appears that someone configured your UpdateServerUrl setting incorrectly: {0}", GlobalAttributesCache.Get().GetValue( "UpdateServerUrl" ) ); + } + else if ( ex.Message.Contains( "could not be resolved" ) ) + { + nbErrors.Text = string.Format( "I think either the update server is down or your UpdateServerUrl setting is incorrect: {0}", GlobalAttributesCache.Get().GetValue( "UpdateServerUrl" ) ); + } + else if ( ex.Message.Contains( "Unable to connect" ) || ex.Message.Contains( "(503)" ) ) + { + nbErrors.NotificationBoxType = Rock.Web.UI.Controls.NotificationBoxType.Warning; + nbErrors.Text = "The update server is currently unavailable (possibly undergoing maintenance). Please try again later."; + } + else + { + nbErrors.Text = string.Format( "...actually, I'm not sure what happened here: {0}", ex.Message ); + } + } + + #endregion + } + + [Serializable] + public class ImpactStatistic + { + public Guid RockInstanceId { get; set; } + public string Version { get; set; } + public string IpAddress { get; set; } + public string PublicUrl { get; set; } + public string OrganizationName { get; set; } + public ImpactLocation OrganizationLocation { get; set; } + public int NumberOfActiveRecords { get; set; } + public string EnvironmentData { get; set; } + } + + [Serializable] + public class ImpactLocation + { + public string Street1 { get; set; } + public string Street2 { get; set; } + public string City { get; set; } + public string State { get; set; } + public string PostalCode { get; set; } + public string Country { get; set; } + + public ImpactLocation( Rock.Model.Location location ) + { + Street1 = location.Street1; + Street2 = location.Street2; + City = location.City; + State = location.State; + PostalCode = location.PostalCode; + Country = location.Country; + } + } + + /// + /// Represents the bits the Rock system stores regarding a particular release. + /// + [Serializable] + public class Release + { + public string SemanticVersion { get; set; } + public string Version { get; set; } + public DateTime? ReleaseDate { get; set; } + public string Summary { get; set; } + public bool RequiresEarlyAccess { get; set; } + public string RequiresVersion { get; set; } + } + + /// + /// One of three options that represent which release 'program' one can be on. + /// + public static class ReleaseProgram + { + public static readonly string ALPHA = "alpha"; + public static readonly string BETA = "beta"; + public static readonly string PRODUCTION = "production"; + } +} \ No newline at end of file