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...
+
+
+
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.
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.
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.
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.
+
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