diff --git a/Disco.BI/BI/AttachmentBI/Utilities.cs b/Disco.BI/BI/AttachmentBI/Utilities.cs new file mode 100644 index 00000000..54ce4815 --- /dev/null +++ b/Disco.BI/BI/AttachmentBI/Utilities.cs @@ -0,0 +1,96 @@ +using System; +using System.Drawing; +using System.IO; +using System.Linq; +using Disco.BI.Extensions; +using iTextSharp.text.pdf; + +namespace Disco.BI.AttachmentBI +{ + public static class Utilities + { + + public static bool GenerateThumbnail(Stream Source, string SourceMimeType, Stream OutStream) + { + if (Source != null) + { + // GDI+ (jpg, png, gif, bmp) + if (SourceMimeType.Equals("image/jpeg", StringComparison.InvariantCultureIgnoreCase) || SourceMimeType.Contains("jpg") || + SourceMimeType.Equals("image/png", StringComparison.InvariantCultureIgnoreCase) || SourceMimeType.Contains("png") || + SourceMimeType.Equals("image/gif", StringComparison.InvariantCultureIgnoreCase) || SourceMimeType.Contains("gif") || + SourceMimeType.Equals("image/bmp", StringComparison.InvariantCultureIgnoreCase) || SourceMimeType.Contains("bmp")) + { + try + { + using (Image sourceImage = Image.FromStream(Source)) + { + using (Image thumbImage = sourceImage.ResizeImage(48, 48)) + { + using (Image mimeTypeIcon = Disco.Properties.Resources.MimeType_img16) + thumbImage.EmbedIconOverlay(mimeTypeIcon); + thumbImage.SaveJpg(90, OutStream); + return true; + } + } + } + catch (Exception) + { + // Ignore Thumbnail Generation exceptions for images + //throw; + } + + } + + // PDF + if (SourceMimeType.Equals("application/pdf", StringComparison.InvariantCultureIgnoreCase) || SourceMimeType.Contains("pdf")) + { + PdfReader pdfReader = new PdfReader(Source); + try + { + using (DisposableImageCollection pdfPageImages = pdfReader.PdfPageImages(1)) + { + if (pdfPageImages.Count() > 0) + { + // Find Biggest Image on Page + Image biggestImage = pdfPageImages.OrderByDescending(i => i.Height * i.Width).First(); + using (Image thumbImage = biggestImage.ResizeImage(48, 48, Brushes.White)) + { + using (Image mimeTypeIcon = Disco.Properties.Resources.MimeType_pdf16) + thumbImage.EmbedIconOverlay(mimeTypeIcon); + thumbImage.SaveJpg(90, OutStream); + return true; + } + } + } + } + finally + { + if (pdfReader != null) + pdfReader.Close(); + } + } + } + return false; + } + public static bool GenerateThumbnail(string SourceFilename, string SourceMimeType, string DestinationFilename) + { + using (FileStream sourceStream = new FileStream(SourceFilename, FileMode.Open, FileAccess.Read)) + { + return GenerateThumbnail(sourceStream, SourceMimeType, DestinationFilename); + } + } + public static bool GenerateThumbnail(Stream Source, string SourceMimeType, string DestinationFilename) + { + bool result; + using (FileStream destinationStream = new FileStream(DestinationFilename, FileMode.Create, FileAccess.Write, FileShare.None)) + { + result = GenerateThumbnail(Source, SourceMimeType, destinationStream); + } + if (!result && File.Exists(DestinationFilename)) + File.Delete(DestinationFilename); + + return result; + } + + } +} diff --git a/Disco.BI/BI/DataStore.cs b/Disco.BI/BI/DataStore.cs new file mode 100644 index 00000000..66ccbd90 --- /dev/null +++ b/Disco.BI/BI/DataStore.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Data.Repository; +using Disco.Data.Configuration; + +namespace Disco.BI +{ + public static class DataStore + { + + public static string CreateLocation(DiscoDataContext dbContext, string SubLocation, DateTime? SubSubLocationTimestamp = null) + { + return CreateLocation(dbContext.DiscoConfiguration, SubLocation, SubSubLocationTimestamp); + } + public static string CreateLocation(ConfigurationContext DiscoConfiguration, string SubLocation, DateTime? SubSubLocationTimestamp = null) + { + string SubSubLocation = string.Empty; + if (SubSubLocationTimestamp.HasValue) + SubSubLocation = SubSubLocationTimestamp.Value.ToString(@"yyyy\\MM"); + + string storeDirectory = System.IO.Path.Combine(DiscoConfiguration.DataStoreLocation, SubLocation, SubSubLocation); + if (!System.IO.Directory.Exists(storeDirectory)) + System.IO.Directory.CreateDirectory(storeDirectory); + + return storeDirectory; + } + + } +} diff --git a/Disco.BI/BI/DeviceBI/BatchUtilities.cs b/Disco.BI/BI/DeviceBI/BatchUtilities.cs new file mode 100644 index 00000000..c2b91d9d --- /dev/null +++ b/Disco.BI/BI/DeviceBI/BatchUtilities.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Models.Repository; +using Disco.Data.Repository; + +namespace Disco.BI.DeviceBI +{ + public static class BatchUtilities + { + public static DeviceBatch DefaultNewDeviceBatch(DiscoDataContext dbContext) + { + return new DeviceBatch() + { + PurchaseDate = DateTime.Today + }; + } + + } +} diff --git a/Disco.BI/BI/DeviceBI/Enrol.cs b/Disco.BI/BI/DeviceBI/Enrol.cs new file mode 100644 index 00000000..c5166870 --- /dev/null +++ b/Disco.BI/BI/DeviceBI/Enrol.cs @@ -0,0 +1,621 @@ +using System; +using System.Linq; +using System.Text.RegularExpressions; +using Disco.BI.Interop.ActiveDirectory; +using Disco.BI.Extensions; +using Disco.Data.Configuration.Modules; +using Disco.Data.Repository; +using Disco.Models.ClientServices; +using Disco.Models.Interop.ActiveDirectory; +using Disco.Models.Repository; +using Tamir.SharpSsh; +using Disco.Services.Plugins; +using Disco.Services.Plugins.Features.CertificateProvider; + +namespace Disco.BI.DeviceBI +{ + public class DeviceEnrol + { + public enum EnrolmentTypes + { + Normal, + Mac = 5, + MacSecure + } + + private static Regex SshPromptRegEx = new Regex("[\\$,\\#]", RegexOptions.Multiline); + public static MacSecureEnrolResponse MacSecureEnrol(DiscoDataContext dbContext, string Host) + { + MacEnrol trustedRequest = new MacEnrol(); + string sessionId = System.Guid.NewGuid().ToString("B"); + MacSecureEnrolResponse MacSecureEnrol; + try + { + EnrolmentLog.LogSessionStarting(sessionId, Host, EnrolmentTypes.MacSecure); + EnrolmentLog.LogSessionProgress(sessionId, 0, string.Format("Connecting to '{0}' as '{1}'", Host, dbContext.DiscoConfiguration.Bootstrapper.MacSshUsername)); + SshShell shell = new SshShell(Host, dbContext.DiscoConfiguration.Bootstrapper.MacSshUsername, dbContext.DiscoConfiguration.Bootstrapper.MacSshPassword); + try + { + shell.ExpectPattern = "#"; + shell.Connect(); + EnrolmentLog.LogSessionProgress(sessionId, 10, "Connected, Authenticating"); + var output = shell.Expect(SshPromptRegEx); + bool sessionElevated = false; + EnrolmentLog.LogSessionDiagnosticInformation(sessionId, output); + if (!output.TrimEnd(new char[0]).EndsWith("#")) + { + EnrolmentLog.LogSessionProgress(sessionId, 22, "Connected, Elevating Credentials"); + shell.WriteLine("sudo -k"); + System.Threading.Thread.Sleep(250); + output = shell.Expect(SshPromptRegEx); + EnrolmentLog.LogSessionProgress(sessionId, 25, "Connected, Elevating Credentials"); + EnrolmentLog.LogSessionDiagnosticInformation(sessionId, output); + shell.WriteLine("sudo -s -S"); + System.Threading.Thread.Sleep(250); + output = shell.Expect(":"); + EnrolmentLog.LogSessionProgress(sessionId, 27, "Connected, Elevating Credentials"); + EnrolmentLog.LogSessionDiagnosticInformation(sessionId, output); + shell.WriteLine(dbContext.DiscoConfiguration.Bootstrapper.MacSshPassword); + System.Threading.Thread.Sleep(250); + output = shell.Expect(SshPromptRegEx); + sessionElevated = true; + EnrolmentLog.LogSessionDiagnosticInformation(sessionId, output); + } + EnrolmentLog.LogSessionProgress(sessionId, 20, "Retrieving Serial Number"); + trustedRequest.DeviceSerialNumber = ParseMacShellCommand(shell, "system_profiler SPHardwareDataType | grep \"Serial Number\" | cut -d \":\" -f 2-", sessionId); + EnrolmentLog.LogSessionDevice(sessionId, trustedRequest.DeviceSerialNumber, null); + EnrolmentLog.LogSessionProgress(sessionId, 30, "Retrieving Hardware UUID"); + trustedRequest.DeviceUUID = ParseMacShellCommand(shell, "system_profiler SPHardwareDataType | grep \"Hardware UUID:\" | cut -d \":\" -f 2-", sessionId); + EnrolmentLog.LogSessionProgress(sessionId, 40, "Retrieving Computer Name"); + trustedRequest.DeviceComputerName = ParseMacShellCommand(shell, "scutil --get ComputerName", sessionId); + EnrolmentLog.LogSessionProgress(sessionId, 50, "Retrieving Ethernet MAC Address"); + string lanNicId = ParseMacShellCommand(shell, "system_profiler SPEthernetDataType | egrep -o \"en0|en1|en2|en3|en4|en5|en6\"", sessionId); + if (!string.IsNullOrWhiteSpace(lanNicId)) + { + trustedRequest.DeviceLanMacAddress = ParseMacShellCommand(shell, string.Format("ifconfig {0} | grep ether | cut -d \" \" -f 2-", lanNicId), sessionId); + } + EnrolmentLog.LogSessionProgress(sessionId, 65, "Retrieving Wireless MAC Address"); + string wlanNicId = ParseMacShellCommand(shell, "system_profiler SPAirPortDataType | egrep -o \"en0|en1|en2|en3|en4|en5|en6\"", sessionId); + if (!string.IsNullOrWhiteSpace(wlanNicId)) + { + trustedRequest.DeviceWlanMacAddress = ParseMacShellCommand(shell, string.Format("ifconfig {0} | grep ether | cut -d \" \" -f 2-", wlanNicId), sessionId); + } + trustedRequest.DeviceManufacturer = "Apple Inc."; + EnrolmentLog.LogSessionProgress(sessionId, 80, "Retrieving Model"); + trustedRequest.DeviceModel = ParseMacShellCommand(shell, "system_profiler SPHardwareDataType | grep \"Model Identifier:\" | cut -d \":\" -f 2-", sessionId); + EnrolmentLog.LogSessionProgress(sessionId, 90, "Retrieving Model Type"); + trustedRequest.DeviceModelType = ParseMacModelType(ParseMacShellCommand(shell, "system_profiler SPHardwareDataType | grep \"Model Name:\" | cut -d \":\" -f 2-", sessionId)); + EnrolmentLog.LogSessionProgress(sessionId, 99, "Disconnecting"); + output = ParseMacModelType(ParseMacShellCommand(shell, "exit", sessionId)); + if (sessionElevated) + { + output = ParseMacModelType(ParseMacShellCommand(shell, "exit", sessionId)); + } + if (shell.Connected) + { + shell.Close(); + } + EnrolmentLog.LogSessionProgress(sessionId, 100, "Disconnected, Starting Disco Enrolment"); + MacSecureEnrolResponse response = MacSecureEnrolResponse.FromMacEnrolResponse(MacEnrol(dbContext, trustedRequest, true, sessionId)); + EnrolmentLog.LogSessionFinished(sessionId); + MacSecureEnrol = response; + } + catch (System.Exception ex) + { + throw ex; + } + finally + { + if (shell != null) + { + bool connected = shell.Connected; + if (connected) + { + shell.Close(); + } + } + } + } + catch (System.Exception ex) + { + EnrolmentLog.LogSessionError(sessionId, ex); + throw ex; + } + return MacSecureEnrol; + } + + #region "Mac Enrol Helpers" + + private static string ParseMacModelType(string ModelName) + { + string ParseMacModelType; + if (!string.IsNullOrWhiteSpace(ModelName)) + { + string mn = ModelName.ToLower(); + if (mn.Contains("imac") || mn.Contains("mini")) + { + ParseMacModelType = "Desktop"; + return ParseMacModelType; + } + if (mn.Contains("macbook")) + { + ParseMacModelType = "Mobile"; + return ParseMacModelType; + } + if (mn.Contains("xserve")) + { + ParseMacModelType = "Server"; + return ParseMacModelType; + } + } + ParseMacModelType = "Unknown"; + return ParseMacModelType; + } + + private static string ParseMacShellCommand(SshShell Shell, string Command, string LogSessionId) + { + Shell.WriteLine(Command); + System.Threading.Thread.Sleep(250); + string Response = Shell.Expect(SshPromptRegEx); + Response = Response.Replace("\r", string.Empty); + EnrolmentLog.LogSessionDiagnosticInformation(LogSessionId, Response); + bool flag = Response.Contains("\n"); + string ParseMacShellCommand; + if (flag) + { + string[] ResponseLines = Response.Split(new char[] + { + '\n' + }); + switch (ResponseLines.Length) + { + case 0: + case 1: + { + ParseMacShellCommand = string.Empty; + break; + } + case 2: + case 3: + { + ParseMacShellCommand = ResponseLines[1].Trim(); + break; + } + default: + { + System.Text.StringBuilder ResponseBuilder = new System.Text.StringBuilder(); + int num = ResponseLines.Length - 2; + int lineIndex = 1; + while (true) + { + int arg_111_0 = lineIndex; + int num2 = num; + if (arg_111_0 > num2) + { + break; + } + ResponseBuilder.AppendLine(ResponseLines[lineIndex]); + lineIndex++; + } + ParseMacShellCommand = ResponseBuilder.ToString().Trim(); + break; + } + } + } + else + { + ParseMacShellCommand = Response; + } + return ParseMacShellCommand; + } + + #endregion + + public static MacEnrolResponse MacEnrol(DiscoDataContext dbContext, MacEnrol Request, bool Trusted, string OpenSessionId = null) + { + string sessionId; + if (OpenSessionId == null) + { + sessionId = System.Guid.NewGuid().ToString("B"); + EnrolmentLog.LogSessionStarting(sessionId, Request.DeviceSerialNumber, EnrolmentTypes.Mac); + } + else + { + sessionId = OpenSessionId; + } + EnrolmentLog.LogSessionDeviceInfo(sessionId, Request); + MacEnrolResponse response = new MacEnrolResponse(); + try + { + EnrolmentLog.LogSessionProgress(sessionId, 10, "Querying Database"); + Device RepoDevice = dbContext.Devices.Include("AssignedUser").Include("DeviceProfile").Include("DeviceProfile").Where(d => d.SerialNumber == Request.DeviceSerialNumber).FirstOrDefault(); + if (!Trusted) + { + if (RepoDevice == null) + throw new EnrolSafeException(string.Format("Unknown Device Serial Number (SN: '{0}')", Request.DeviceSerialNumber)); + if (!RepoDevice.AllowUnauthenticatedEnrol) + throw new EnrolSafeException(string.Format("Device isn't allowed an Unauthenticated Enrolment (SN: '{0}')", Request.DeviceSerialNumber)); + } + if (RepoDevice == null) + { + EnrolmentLog.LogSessionProgress(sessionId, 50, "New Device, Building Disco Instance"); + EnrolmentLog.LogSessionTaskAddedDevice(sessionId, Request.DeviceSerialNumber); + DeviceProfile deviceProfile = dbContext.DeviceProfiles.Find(dbContext.DiscoConfiguration.DeviceProfiles.DefaultDeviceProfileId); + DeviceModel deviceModel = dbContext.DeviceModels.Where(dm => dm.Manufacturer == Request.DeviceManufacturer.Trim() && dm.Model == Request.DeviceModel.Trim()).FirstOrDefault(); + if (deviceModel == null) + { + deviceModel = new DeviceModel + { + Manufacturer = Request.DeviceManufacturer.Trim(), + Model = Request.DeviceModel.Trim(), + ModelType = Request.DeviceModelType.Trim(), + Description = string.Format("{0} {1}", Request.DeviceManufacturer.Trim(), Request.DeviceModel) + }; + dbContext.DeviceModels.Add(deviceModel); + EnrolmentLog.LogSessionTaskCreatedDeviceModel(sessionId, Request.DeviceSerialNumber, Request.DeviceManufacturer.Trim(), Request.DeviceModel.Trim()); + } + else + { + EnrolmentLog.LogSessionDevice(sessionId, Request.DeviceSerialNumber, deviceModel.Id); + } + RepoDevice = new Device + { + SerialNumber = Request.DeviceSerialNumber, + ComputerName = Request.DeviceComputerName, + DeviceProfile = deviceProfile, + DeviceModel = deviceModel, + AllowUnauthenticatedEnrol = false, + Active = true, + CreatedDate = DateTime.Now, + EnrolledDate = DateTime.Now + }; + dbContext.Devices.Add(RepoDevice); + } + else + { + EnrolmentLog.LogSessionProgress(sessionId, 50, "Existing Device, Updating Disco Instance"); + EnrolmentLog.LogSessionTaskUpdatingDevice(sessionId, Request.DeviceSerialNumber); + if (!RepoDevice.DeviceModelId.HasValue || RepoDevice.DeviceModelId.Value == 1) + { + DeviceModel deviceModel = dbContext.DeviceModels.Where(dm => dm.Manufacturer == Request.DeviceManufacturer.Trim() && dm.Model == Request.DeviceModel.Trim()).FirstOrDefault(); + if (deviceModel == null) + { + deviceModel = new DeviceModel + { + Manufacturer = Request.DeviceManufacturer.Trim(), + Model = Request.DeviceModel.Trim(), + ModelType = Request.DeviceModelType.Trim(), + Description = string.Format("{0} {1}", Request.DeviceManufacturer.Trim(), Request.DeviceModel.Trim()) + }; + dbContext.DeviceModels.Add(deviceModel); + EnrolmentLog.LogSessionTaskCreatedDeviceModel(sessionId, Request.DeviceSerialNumber, Request.DeviceManufacturer.Trim(), Request.DeviceModel.Trim()); + } + else + { + EnrolmentLog.LogSessionDevice(sessionId, Request.DeviceSerialNumber, deviceModel.Id); + } + RepoDevice.DeviceModel = deviceModel; + } + else + { + EnrolmentLog.LogSessionDevice(sessionId, Request.DeviceSerialNumber, RepoDevice.DeviceModelId); + } + RepoDevice.ComputerName = Request.DeviceComputerName; + if (!RepoDevice.EnrolledDate.HasValue) + { + RepoDevice.EnrolledDate = DateTime.Now; + } + } + RepoDevice.LastEnrolDate = DateTime.Now; + RepoDevice.AllowUnauthenticatedEnrol = false; + // Removed 2012-06-14 G# - Properties moved to DeviceProfile model & DB Migrated in DBv3. + //DeviceProfileConfiguration RepoDeviceProfileContext = RepoDevice.DeviceProfile.Configuration(Context); + EnrolmentLog.LogSessionProgress(sessionId, 90, "Building Response"); + //if (RepoDeviceProfileContext.DistributionType == DeviceProfileConfiguration.DeviceProfileDistributionTypes.OneToOne && RepoDevice.AssignedUser != null) + if (RepoDevice.DeviceProfile.DistributionType == DeviceProfile.DistributionTypes.OneToOne && RepoDevice.AssignedUser != null) + { + ActiveDirectoryUserAccount AssignedUserInfo = ActiveDirectory.GetUserAccount(RepoDevice.AssignedUser.Id); + EnrolmentLog.LogSessionTaskAssigningUser(sessionId, RepoDevice.SerialNumber, AssignedUserInfo.DisplayName, AssignedUserInfo.sAMAccountName, AssignedUserInfo.Domain, AssignedUserInfo.ObjectSid); + response.DeviceAssignedUserUsername = AssignedUserInfo.sAMAccountName; + response.DeviceAssignedUserDomain = AssignedUserInfo.Domain; + response.DeviceAssignedUserName = AssignedUserInfo.DisplayName; + response.DeviceAssignedUserSID = AssignedUserInfo.ObjectSid; + } + response.DeviceComputerName = RepoDevice.ComputerName; + EnrolmentLog.LogSessionProgress(sessionId, 100, "Completed Successfully"); + } + catch (EnrolSafeException ex) + { + EnrolmentLog.LogSessionError(sessionId, ex); + return new MacEnrolResponse { ErrorMessage = ex.Message }; + } + catch (System.Exception ex2) + { + EnrolmentLog.LogSessionError(sessionId, ex2); + throw ex2; + } + finally + { + if (OpenSessionId == null) + EnrolmentLog.LogSessionFinished(sessionId); + } + return response; + } + public static EnrolResponse Enrol(DiscoDataContext dbContext, string Username, Models.ClientServices.Enrol Request) + { + ActiveDirectoryMachineAccount MachineInfo = null; + EnrolResponse response = new EnrolResponse(); + User authenticatedUser = null; + bool isAuthenticated = false; + string sessionId = System.Guid.NewGuid().ToString("B"); + response.SessionId = sessionId; + EnrolmentLog.LogSessionStarting(sessionId, Request.DeviceSerialNumber, EnrolmentTypes.Normal); + EnrolmentLog.LogSessionDeviceInfo(sessionId, Request); + try + { + EnrolmentLog.LogSessionProgress(sessionId, 10, "Loading User Data"); + if (!string.IsNullOrWhiteSpace(Username)) + { + authenticatedUser = UserBI.UserCache.GetUser(Username, dbContext); + isAuthenticated = (authenticatedUser != null); + } + EnrolmentLog.LogSessionProgress(sessionId, 13, "Loading Device Data"); + + Device RepoDevice = dbContext.Devices.Include("AssignedUser").Include("DeviceModel").Include("DeviceProfile").Where(d => d.SerialNumber == Request.DeviceSerialNumber).FirstOrDefault(); + EnrolmentLog.LogSessionProgress(sessionId, 15, "Discovering User/Device Disco Permissions"); + if (isAuthenticated) + { + if (authenticatedUser.Type != "Admin") + { + if (authenticatedUser.Type != "Computer") + throw new EnrolSafeException(string.Format("Connection not correctly authenticated (SN: {0}; Auth User: {1}; User Type: {2})", Request.DeviceSerialNumber, authenticatedUser.Id, authenticatedUser.Type)); + if (!authenticatedUser.Id.Equals(string.Format("{0}$", Request.DeviceComputerName), System.StringComparison.InvariantCultureIgnoreCase)) + throw new EnrolSafeException(string.Format("Connection not correctly authenticated (SN: {0}; Auth User: {1}; User Type: {2})", Request.DeviceSerialNumber, authenticatedUser.Id, authenticatedUser.Type)); + } + } + else + { + if (RepoDevice == null) + throw new EnrolSafeException(string.Format("Unknown Device Serial Number (SN: '{0}')", Request.DeviceSerialNumber)); + if (!RepoDevice.AllowUnauthenticatedEnrol) + throw new EnrolSafeException(string.Format("Device isn't allowed an Unauthenticated Enrolment (SN: '{0}')", Request.DeviceSerialNumber)); + } + if (Request.DeviceIsPartOfDomain && !string.IsNullOrWhiteSpace(Request.DeviceComputerName)) + { + EnrolmentLog.LogSessionProgress(sessionId, 20, "Loading Active Directory Computer Account"); + System.Guid? uuidGuid = null; + System.Guid? macAddressGuid = null; + if (!string.IsNullOrEmpty(Request.DeviceUUID)) + uuidGuid = ActiveDirectoryMachineAccountExtensions.NetbootGUIDFromUUID(Request.DeviceUUID); + if (!string.IsNullOrEmpty(Request.DeviceLanMacAddress)) + macAddressGuid = ActiveDirectoryMachineAccountExtensions.NetbootGUIDFromMACAddress(Request.DeviceLanMacAddress); + MachineInfo = ActiveDirectory.GetMachineAccount(Request.DeviceComputerName, uuidGuid, macAddressGuid); + } + if (RepoDevice == null) + { + EnrolmentLog.LogSessionProgress(sessionId, 30, "New Device, Creating Disco Instance"); + EnrolmentLog.LogSessionTaskAddedDevice(sessionId, Request.DeviceSerialNumber); + DeviceProfile deviceProfile = dbContext.DeviceProfiles.Find(dbContext.DiscoConfiguration.DeviceProfiles.DefaultDeviceProfileId); + DeviceModel deviceModel = dbContext.DeviceModels.Where(dm => dm.Manufacturer == Request.DeviceManufacturer.Trim() && dm.Model == Request.DeviceModel.Trim()).FirstOrDefault(); + if (deviceModel == null) + { + deviceModel = new DeviceModel + { + Manufacturer = Request.DeviceManufacturer.Trim(), + Model = Request.DeviceModel.Trim(), + ModelType = Request.DeviceModelType.Trim(), + Description = string.Format("{0} {1}", Request.DeviceManufacturer.Trim(), Request.DeviceModel.Trim()) + }; + dbContext.DeviceModels.Add(deviceModel); + EnrolmentLog.LogSessionTaskCreatedDeviceModel(sessionId, Request.DeviceSerialNumber, Request.DeviceManufacturer.Trim(), Request.DeviceModel.Trim()); + } + else + { + EnrolmentLog.LogSessionDevice(sessionId, Request.DeviceSerialNumber, deviceModel.Id); + } + RepoDevice = new Device + { + SerialNumber = Request.DeviceSerialNumber, + ComputerName = Request.DeviceComputerName, + DeviceProfile = deviceProfile, + DeviceModel = deviceModel, + AllowUnauthenticatedEnrol = false, + Active = true, + CreatedDate = DateTime.Now, + EnrolledDate = DateTime.Now, + LastEnrolDate = DateTime.Now + }; + dbContext.Devices.Add(RepoDevice); + } + else + { + EnrolmentLog.LogSessionProgress(sessionId, 30, "Existing Device, Updating Disco Instance"); + EnrolmentLog.LogSessionTaskUpdatingDevice(sessionId, Request.DeviceSerialNumber); + + DeviceModel deviceModel = dbContext.DeviceModels.Where(dm => dm.Manufacturer == Request.DeviceManufacturer.Trim() && dm.Model == Request.DeviceModel.Trim()).FirstOrDefault(); + if (deviceModel == null) + { + deviceModel = new DeviceModel + { + Manufacturer = Request.DeviceManufacturer.Trim(), + Model = Request.DeviceModel.Trim(), + ModelType = Request.DeviceModelType.Trim(), + Description = string.Format("{0} {1}", Request.DeviceManufacturer.Trim(), Request.DeviceModel) + }; + dbContext.DeviceModels.Add(deviceModel); + RepoDevice.DeviceModel = deviceModel; + EnrolmentLog.LogSessionTaskCreatedDeviceModel(sessionId, Request.DeviceSerialNumber, Request.DeviceManufacturer.Trim(), Request.DeviceModel.Trim()); + } + else + { + if (!RepoDevice.DeviceModelId.HasValue || RepoDevice.DeviceModelId.Value != deviceModel.Id) + { + RepoDevice.DeviceModel = deviceModel; + } + EnrolmentLog.LogSessionDevice(sessionId, Request.DeviceSerialNumber, RepoDevice.DeviceModelId); + } + + if (!RepoDevice.EnrolledDate.HasValue) + RepoDevice.EnrolledDate = DateTime.Now; + RepoDevice.LastEnrolDate = DateTime.Now; + } + + if (MachineInfo == null) + { + if (isAuthenticated || RepoDevice.AllowUnauthenticatedEnrol) + { + if (RepoDevice.DeviceProfile.ProvisionADAccount) + { + EnrolmentLog.LogSessionProgress(sessionId, 50, "Provisioning an Active Directory Computer Account"); + if (string.IsNullOrEmpty(RepoDevice.ComputerName) || RepoDevice.DeviceProfile.EnforceComputerNameConvention) + RepoDevice.ComputerName = RepoDevice.ComputerNameRender(dbContext); + EnrolmentLog.LogSessionTaskProvisioningADAccount(sessionId, RepoDevice.SerialNumber, RepoDevice.ComputerName); + MachineInfo = ActiveDirectory.GetMachineAccount(RepoDevice.ComputerName); + response.OfflineDomainJoin = ActiveDirectory.OfflineDomainJoinProvision(ref MachineInfo, RepoDevice.ComputerName, RepoDevice.DeviceProfile.OrganisationalUnit, sessionId); + response.RequireReboot = true; + } + if (MachineInfo != null) + { + response.DeviceComputerName = MachineInfo.Name; + response.DeviceDomainName = MachineInfo.Domain; + } + else + { + response.DeviceComputerName = RepoDevice.ComputerName; + response.DeviceDomainName = RepoDevice.ComputerName; + } + } + else + { + RepoDevice.ComputerName = Request.DeviceComputerName; + response.DeviceComputerName = Request.DeviceComputerName; + response.DeviceDomainName = RepoDevice.ComputerName; + } + } + else + { + RepoDevice.ComputerName = MachineInfo.Name; + response.DeviceComputerName = MachineInfo.Name; + response.DeviceDomainName = MachineInfo.Domain; + + // Enforce Computer Name Convention + if (RepoDevice.DeviceProfile.EnforceComputerNameConvention) + { + var calculatedComputerName = RepoDevice.ComputerNameRender(dbContext); + if (!Request.DeviceComputerName.Equals(calculatedComputerName, StringComparison.InvariantCultureIgnoreCase)) + { + EnrolmentLog.LogSessionProgress(sessionId, 50, string.Format("Renaming Device: {0} -> {1}", Request.DeviceComputerName, calculatedComputerName)); + EnrolmentLog.LogSessionTaskRenamingDevice(sessionId, Request.DeviceComputerName, calculatedComputerName); + + RepoDevice.ComputerName = calculatedComputerName; + response.DeviceComputerName = calculatedComputerName; + + // Create New Account + response.OfflineDomainJoin = ActiveDirectory.OfflineDomainJoinProvision(ref MachineInfo, RepoDevice.ComputerName, RepoDevice.DeviceProfile.OrganisationalUnit, sessionId); + response.RequireReboot = true; + } + } + + // Enforce Organisation Unit + if (response.OfflineDomainJoin == null && RepoDevice.DeviceProfile.EnforceOrganisationalUnit) + { + if ((RepoDevice.DeviceProfile.OrganisationalUnit == null && MachineInfo.ParentDistinguishedName.Equals("CN=Computers", StringComparison.InvariantCultureIgnoreCase)) // Null (Default) OU + || !MachineInfo.ParentDistinguishedName.Equals(RepoDevice.DeviceProfile.OrganisationalUnit, StringComparison.InvariantCultureIgnoreCase)) // Custom OU + { + string newOU = RepoDevice.DeviceProfile.OrganisationalUnit ?? "CN=Computers"; + + EnrolmentLog.LogSessionProgress(sessionId, 65, string.Format("Moving Device Organisation Unit: {0} -> {1}", MachineInfo.ParentDistinguishedName, newOU)); + EnrolmentLog.LogSessionTaskMovingDeviceOrganisationUnit(sessionId, MachineInfo.ParentDistinguishedName, newOU); + MachineInfo.MoveOrganisationUnit(RepoDevice.DeviceProfile.OrganisationalUnit); + MachineInfo = ActiveDirectory.GetMachineAccount(MachineInfo.sAMAccountName); + response.RequireReboot = true; + } + } + + } + if (MachineInfo != null) + { + EnrolmentLog.LogSessionProgress(sessionId, 75, "Updating Active Directory Computer Account Properties"); + MachineInfo.UpdateNetbootGUID(Request.DeviceUUID, Request.DeviceLanMacAddress); + if (RepoDevice.AssignedUser != null) + MachineInfo.SetDescription(RepoDevice); + } + if (RepoDevice.DeviceProfile.DistributionType == DeviceProfile.DistributionTypes.OneToOne) + { + if (RepoDevice.AssignedUser == null) + { + response.AllowBootstrapperUninstall = false; + } + else + { + EnrolmentLog.LogSessionProgress(sessionId, 80, "Retrieving Active Directory Assigned User Account"); + ActiveDirectoryUserAccount AssignedUserInfo = ActiveDirectory.GetUserAccount(RepoDevice.AssignedUser.Id); + EnrolmentLog.LogSessionTaskAssigningUser(sessionId, RepoDevice.SerialNumber, AssignedUserInfo.DisplayName, AssignedUserInfo.sAMAccountName, AssignedUserInfo.Domain, AssignedUserInfo.ObjectSid); + response.AllowBootstrapperUninstall = true; + response.DeviceAssignedUserUsername = AssignedUserInfo.sAMAccountName; + response.DeviceAssignedUserDomain = AssignedUserInfo.Domain; + response.DeviceAssignedUserName = AssignedUserInfo.DisplayName; + response.DeviceAssignedUserSID = AssignedUserInfo.ObjectSid; + } + } + else + { + response.AllowBootstrapperUninstall = true; + } + if (!string.IsNullOrEmpty(Request.DeviceWlanMacAddress) && !string.IsNullOrEmpty(RepoDevice.DeviceProfile.CertificateProviderId)) + { + EnrolmentLog.LogSessionProgress(sessionId, 90, "Provisioning a Wireless Certificate"); + + var allocationResult = RepoDevice.AllocateCertificate(dbContext); + var deviceCertificate = allocationResult.Item1; + if (deviceCertificate != null) + { + bool certAlreadyInstalled = false; + if (Request.DeviceCertificates != null && Request.DeviceCertificates.Count > 0) + { + foreach (string existingCertName in Request.DeviceCertificates) + { + if (existingCertName.Contains(deviceCertificate.Name)) + { + certAlreadyInstalled = true; + break; + } + } + } + if (!certAlreadyInstalled) + { + EnrolmentLog.LogSessionTaskProvisioningWirelessCertificate(sessionId, RepoDevice.SerialNumber, deviceCertificate.Name); + response.DeviceCertificate = System.Convert.ToBase64String(deviceCertificate.Content); + } + } + response.DeviceCertificateRemoveExisting = allocationResult.Item2; + } + + // Reset 'AllowUnauthenticatedEnrol' + if (RepoDevice.AllowUnauthenticatedEnrol) + RepoDevice.AllowUnauthenticatedEnrol = false; + + EnrolmentLog.LogSessionProgress(sessionId, 100, "Completed Successfully"); + } + catch (EnrolSafeException ex) + { + EnrolmentLog.LogSessionError(sessionId, ex); + return new EnrolResponse + { + SessionId = sessionId, + ErrorMessage = ex.Message + }; + } + catch (System.Exception ex2) + { + EnrolmentLog.LogSessionError(sessionId, ex2); + throw ex2; + } + finally + { + EnrolmentLog.LogSessionFinished(sessionId); + } + return response; + } + } +} diff --git a/Disco.BI/BI/DeviceBI/EnrolSafeException.cs b/Disco.BI/BI/DeviceBI/EnrolSafeException.cs new file mode 100644 index 00000000..85244d8b --- /dev/null +++ b/Disco.BI/BI/DeviceBI/EnrolSafeException.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading; +namespace Disco.BI +{ + public class EnrolSafeException : System.Exception + { + public EnrolSafeException(string Message) : base(Message) + { + } + } +} diff --git a/Disco.BI/BI/DeviceBI/EnrolmentLog.cs b/Disco.BI/BI/DeviceBI/EnrolmentLog.cs new file mode 100644 index 00000000..de68bea6 --- /dev/null +++ b/Disco.BI/BI/DeviceBI/EnrolmentLog.cs @@ -0,0 +1,482 @@ +using Disco.Services.Logging; +using Disco.Services.Logging.Models; +using Disco.Models.ClientServices; +using System; +using System.Collections.Generic; +using System.Diagnostics; +namespace Disco.BI.DeviceBI +{ + public class EnrolmentLog : LogBase + { + public enum EventTypeIds + { + SessionStarting = 10, + SessionProgress, + SessionDevice, + SessionDeviceInfo, + SessionFinished = 20, + SessionDiagnosticInformation, + SessionWarning, + SessionError, + SessionErrorWithInner, + SessionClientError, + SessionTaskAddedDevice = 50, + SessionTaskUpdatingDevice, + SessionTaskCreatedDeviceModel = 56, + SessionTaskProvisioningADAccount = 58, + SessionTaskAssigningUser = 60, + SessionTaskProvisioningWirelessCertificate = 62, + SessionTaskRenamingDevice = 64, + SessionTaskMovingDeviceOrganisationUnit = 66, + ClientError = 400 + } + private const int _ModuleId = 50; + public static EnrolmentLog Current + { + get + { + return (EnrolmentLog)LogContext.LogModules[50]; + } + } + public override string ModuleDescription + { + get + { + return "Device Enrolment"; + } + } + public override int ModuleId + { + get + { + return 50; + } + } + public override string ModuleName + { + get + { + return "DeviceEnrolment"; + } + } + [System.Diagnostics.DebuggerNonUserCode] + public EnrolmentLog() + { + } + private static void Log(EnrolmentLog.EventTypeIds EventTypeId, params object[] Args) + { + EnrolmentLog.Current.Log((int)EventTypeId, Args); + } + public static void LogSessionStarting(string SessionId, string HostId, DeviceEnrol.EnrolmentTypes EnrolmentType) + { + EnrolmentLog.Log(EnrolmentLog.EventTypeIds.SessionStarting, new object[] + { + SessionId, + HostId, + System.Enum.GetName(EnrolmentType.GetType(), EnrolmentType) + }); + } + public static void LogSessionDevice(string SessionId, string DeviceSerialNumber, int? DeviceModelId) + { + EnrolmentLog.Log(EnrolmentLog.EventTypeIds.SessionDevice, new object[] + { + SessionId, + DeviceSerialNumber, + DeviceModelId + }); + } + public static void LogSessionDeviceInfo(string SessionId, string SerialNumber, string UUID, string ComputerName, string LanMacAddress, string WlanMacAddress, string Manufacturer, string Model, string ModelType) + { + EnrolmentLog.Log(EnrolmentLog.EventTypeIds.SessionDeviceInfo, new object[] + { + SessionId, + SerialNumber, + UUID, + ComputerName, + LanMacAddress, + WlanMacAddress, + Manufacturer, + Model, + ModelType + }); + } + public static void LogSessionDeviceInfo(string SessionId, MacEnrol Request) + { + EnrolmentLog.LogSessionDeviceInfo(SessionId, Request.DeviceSerialNumber, Request.DeviceUUID, Request.DeviceComputerName, Request.DeviceLanMacAddress, Request.DeviceWlanMacAddress, Request.DeviceManufacturer, Request.DeviceModel, Request.DeviceModelType); + } + public static void LogSessionDeviceInfo(string SessionId, Models.ClientServices.Enrol Request) + { + EnrolmentLog.LogSessionDeviceInfo(SessionId, Request.DeviceSerialNumber, Request.DeviceUUID, Request.DeviceComputerName, Request.DeviceLanMacAddress, Request.DeviceWlanMacAddress, Request.DeviceManufacturer, Request.DeviceModel, Request.DeviceModelType); + } + public static void LogSessionProgress(string SessionId, int Progress, string Status) + { + EnrolmentLog.Log(EnrolmentLog.EventTypeIds.SessionProgress, new object[] + { + SessionId, + Progress, + Status + }); + } + public static void LogSessionFinished(string SessionId) + { + EnrolmentLog.Log(EnrolmentLog.EventTypeIds.SessionFinished, new object[] + { + SessionId + }); + } + public static void LogSessionDiagnosticInformation(string SessionId, string Message) + { + EnrolmentLog.Log(EnrolmentLog.EventTypeIds.SessionDiagnosticInformation, new object[] + { + SessionId, + Message + }); + } + public static void LogSessionWarning(string SessionId, string Message) + { + EnrolmentLog.Log(EnrolmentLog.EventTypeIds.SessionWarning, new object[] + { + SessionId, + Message + }); + } + public static void LogSessionError(string SessionId, System.Exception Ex) + { + if (Ex.InnerException == null) + { + EnrolmentLog.Log(EnrolmentLog.EventTypeIds.SessionError, new object[] + { + SessionId, + Ex.GetType().Name, + Ex.Message, + Ex.StackTrace + }); + } + else + { + EnrolmentLog.Log(EnrolmentLog.EventTypeIds.SessionErrorWithInner, new object[] + { + SessionId, + Ex.GetType().Name, + Ex.Message, + Ex.InnerException.GetType().Name, + Ex.InnerException.Message, + Ex.StackTrace, + Ex.InnerException.StackTrace + }); + } + } + public static void LogSessionClientError(string SessionId, string ClientIP, string ClientIdentifier, string ClientVersion, string Error, string RawError) + { + EnrolmentLog.Log(EnrolmentLog.EventTypeIds.SessionClientError, new object[] + { + SessionId, + ClientIP, + ClientIdentifier, + ClientVersion, + Error, + RawError + }); + } + public static void LogSessionTaskAddedDevice(string SessionId, string DeviceSerialNumber) + { + EnrolmentLog.Log(EnrolmentLog.EventTypeIds.SessionTaskAddedDevice, new object[] + { + SessionId, + DeviceSerialNumber + }); + } + public static void LogSessionTaskUpdatingDevice(string SessionId, string DeviceSerialNumber) + { + EnrolmentLog.Log(EnrolmentLog.EventTypeIds.SessionTaskUpdatingDevice, new object[] + { + SessionId, + DeviceSerialNumber + }); + } + public static void LogSessionTaskCreatedDeviceModel(string SessionId, string DeviceSerialNumber, string Manufacturer, string Model) + { + EnrolmentLog.Log(EnrolmentLog.EventTypeIds.SessionTaskCreatedDeviceModel, new object[] + { + SessionId, + DeviceSerialNumber, + Manufacturer, + Model + }); + } + public static void LogSessionTaskProvisioningADAccount(string SessionId, string DeviceSerialNumber, string ADAccountName) + { + EnrolmentLog.Log(EnrolmentLog.EventTypeIds.SessionTaskProvisioningADAccount, new object[] + { + SessionId, + DeviceSerialNumber, + ADAccountName + }); + } + public static void LogSessionTaskAssigningUser(string SessionId, string DeviceSerialNumber, string UserDisplayName, string UserUsername, string UserDomain, string UserSID) + { + EnrolmentLog.Log(EnrolmentLog.EventTypeIds.SessionTaskAssigningUser, new object[] + { + SessionId, + DeviceSerialNumber, + UserDisplayName, + UserUsername, + UserDomain, + UserSID + }); + } + public static void LogSessionTaskProvisioningWirelessCertificate(string SessionId, string DeviceSerialNumber, string CertificateName) + { + EnrolmentLog.Log(EnrolmentLog.EventTypeIds.SessionTaskProvisioningWirelessCertificate, new object[] + { + SessionId, + DeviceSerialNumber, + CertificateName + }); + } + public static void LogSessionTaskRenamingDevice(string SessionId, string OldComputerName, string NewComputerName) + { + EnrolmentLog.Log(EnrolmentLog.EventTypeIds.SessionTaskRenamingDevice, new object[] + { + SessionId, + OldComputerName, + NewComputerName + }); + } + public static void LogSessionTaskMovingDeviceOrganisationUnit(string SessionId, string OldOrganisationUnit, string NewOrganisationUnit) + { + EnrolmentLog.Log(EnrolmentLog.EventTypeIds.SessionTaskMovingDeviceOrganisationUnit, new object[] + { + SessionId, + OldOrganisationUnit, + NewOrganisationUnit + }); + } + public static void LogClientError(string ClientIP, string ClientIdentifier, string ClientVersion, string Error, string RawError) + { + EnrolmentLog.Log(EnrolmentLog.EventTypeIds.ClientError, new object[] + { + ClientIP, + ClientIdentifier, + ClientVersion, + Error, + RawError + }); + } + protected override System.Collections.Generic.List LoadEventTypes() + { + return new System.Collections.Generic.List + { + new LogEventType + { + Id = 10, + ModuleId = 50, + Name = "Session Starting", + Format = "Starting '{2}' Enrolment for {1} (Session# {0})", + Severity = 0, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 12, + ModuleId = 50, + Name = "Session Device", + Format = null, + Severity = 0, + UseLive = true, + UsePersist = true, + UseDisplay = false + }, + new LogEventType + { + Id = 11, + ModuleId = 50, + Name = "Session Progress", + Format = "Processing Session# {0}; {1}% Complete; Status: {2}", + Severity = 0, + UseLive = true, + UsePersist = false, + UseDisplay = false + }, + new LogEventType + { + Id = 13, + ModuleId = 50, + Name = "Session Device Info", + Format = null, + Severity = 0, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 20, + ModuleId = 50, + Name = "Session Finished", + Format = "Finished Session# {0}", + Severity = 0, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 21, + ModuleId = 50, + Name = "Session Diagnostic Information", + Format = null, + Severity = 0, + UseLive = true, + UsePersist = false, + UseDisplay = false + }, + new LogEventType + { + Id = 22, + ModuleId = 50, + Name = "Session Warning", + Format = null, + Severity = 1, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 23, + ModuleId = 50, + Name = "Session Error", + Format = "An Error Occurred: [{1}] {2}", + Severity = 2, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 24, + ModuleId = 50, + Name = "Session Error with Internal", + Format = "An Error Occurred: [{1}] {2}; Internal Error: [{3}] {4}", + Severity = 2, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.SessionClientError, + ModuleId = _ModuleId, + Name = "Client Error", + Format = "IP: {1}; Device ID: {2}; Version: {3} Error: {4}; Session# {0}", + Severity = (int)LogEventType.Severities.Error, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 50, + ModuleId = 50, + Name = "Task - Added Device", + Format = "Creating Disco Device {1}", + Severity = 0, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 51, + ModuleId = 50, + Name = "Task - Updating Device", + Format = "Updating Disco Device {1}", + Severity = 0, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 56, + ModuleId = 50, + Name = "Task - Creating Device Model", + Format = "Creating Device Model '{2} {3}' for Device {1}", + Severity = 0, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 58, + ModuleId = 50, + Name = "Task - Provisioning Active Directory Account", + Format = "Provisioning Active Directory Account '{2}' for Device {1}", + Severity = 0, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 60, + ModuleId = 50, + Name = "Task - Assigning User", + Format = "Assigning User '{2}' ({4}\\{3} {{{5}}}) for Device {1}", + Severity = 0, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 62, + ModuleId = 50, + Name = "Task - Provisioning Wireless Certificate", + Format = "Provisioning Wireless Certificate '{2}' for Device {1}", + Severity = 0, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 64, + ModuleId = 50, + Name = "Task - Renaming Device", + Format = "Renaming Device '{1}' to '{2}'", + Severity = 0, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 66, + ModuleId = 50, + Name = "Task - Moving Device Organisation Unit", + Format = "Moving Device Organisation Unit '{1}' to '{2}'", + Severity = 0, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.ClientError, + ModuleId = _ModuleId, + Name = "Client Error", + Format = "IP: {0}; Device ID: {1}; Version: {2} Error: {3}", + Severity = (int)LogEventType.Severities.Error, + UseLive = true, + UsePersist = true, + UseDisplay = true + } + }; + } + } +} diff --git a/Disco.BI/BI/DeviceBI/Searching.cs b/Disco.BI/BI/DeviceBI/Searching.cs new file mode 100644 index 00000000..e4fb1f11 --- /dev/null +++ b/Disco.BI/BI/DeviceBI/Searching.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Models.BI.Search; +using Disco.Models.Repository; +using Disco.Data.Repository; + +namespace Disco.BI.DeviceBI +{ + public static class Searching + { + private static List Search_SelectDeviceSearchResultItem(IQueryable Query, int? LimitCount = null){ + if (LimitCount.HasValue) + Query = Query.Take(LimitCount.Value); + + return Query.Select(d => new DeviceSearchResultItem() + { + SerialNumber = d.SerialNumber, + AssetNumber = d.AssetNumber, + ComputerName = d.ComputerName, + DeviceModelDescription = d.DeviceModel.Description, + DeviceProfileDescription = d.DeviceProfile.Description, + DecommissionedDate = d.DecommissionedDate, + AssignedUserId = d.AssignedUserId, + AssignedUserDisplayName = d.AssignedUser.DisplayName, + JobCount = d.Jobs.Count() + }).ToList(); + } + + public static List Search(DiscoDataContext dbContext, string Term, int? LimitCount = null) + { + return Search_SelectDeviceSearchResultItem(dbContext.Devices.Where(d => + d.AssetNumber.Contains(Term) || + d.ComputerName.Contains(Term) || + d.SerialNumber.Contains(Term) || + d.Location.Contains(Term) || + Term.Contains(d.SerialNumber) + ), LimitCount); + } + + public static List SearchDeviceModel(DiscoDataContext dbContext, int DeviceModelId, int? LimitCount = null) + { + return Search_SelectDeviceSearchResultItem(dbContext.Devices.Where(d => d.DeviceModelId == DeviceModelId), LimitCount); + } + public static List SearchDeviceProfile(DiscoDataContext dbContext, int DeviceProfileId, int? LimitCount = null) + { + return Search_SelectDeviceSearchResultItem(dbContext.Devices.Where(d => d.DeviceProfileId == DeviceProfileId), LimitCount); + } + public static List SearchDeviceBatch(DiscoDataContext dbContext, int DeviceBatchId, int? LimitCount = null) + { + return Search_SelectDeviceSearchResultItem(dbContext.Devices.Where(d => d.DeviceBatchId == DeviceBatchId), LimitCount); + } + + } +} diff --git a/Disco.BI/BI/DisposableImageCollection.cs b/Disco.BI/BI/DisposableImageCollection.cs new file mode 100644 index 00000000..8283e740 --- /dev/null +++ b/Disco.BI/BI/DisposableImageCollection.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Drawing; + +namespace Disco.BI +{ + public class DisposableImageCollection : List, IDisposable + { + public void Dispose() + { + foreach (Image i in this) + { + if (i != null) + i.Dispose(); + } + } + + } +} diff --git a/Disco.BI/BI/DocumentTemplateBI/DocumentTemplateQRCodeLocationCache.cs b/Disco.BI/BI/DocumentTemplateBI/DocumentTemplateQRCodeLocationCache.cs new file mode 100644 index 00000000..d48cb80f --- /dev/null +++ b/Disco.BI/BI/DocumentTemplateBI/DocumentTemplateQRCodeLocationCache.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Collections.Concurrent; +using Disco.Models.Repository; +using Disco.Data.Repository; +using Disco.BI.Extensions; +using System.Web; +using System.Drawing; +using iTextSharp.text.pdf; + +namespace Disco.BI.DocumentTemplateBI +{ + class DocumentTemplateQRCodeLocationCache + { + private static ConcurrentDictionary> _Cache = new ConcurrentDictionary>(); + + public static List GetLocations(DocumentTemplate dt, DiscoDataContext dbContext) + { + // Check Cache + List locations; + if (_Cache.TryGetValue(dt.Id, out locations)) + { + return locations; + } + // Generate Cache + return GenerateLocations(dt, dbContext); + } + + public static bool InvalidateLocations(DocumentTemplate dt) + { + List locations; + return _Cache.TryRemove(dt.Id, out locations); + } + + private static bool SetValue(string DocumentTemplateId, List Locations) + { + if (_Cache.ContainsKey(DocumentTemplateId)) + { + List oldLocations; + if (_Cache.TryGetValue(DocumentTemplateId, out oldLocations)) + { + return _Cache.TryUpdate(DocumentTemplateId, Locations, oldLocations); + } + } + return _Cache.TryAdd(DocumentTemplateId, Locations); + } + + internal static List GenerateLocations(DocumentTemplate dt, DiscoDataContext dbContext) + { + string templateFilename = dt.RepositoryFilename(dbContext); + PdfReader pdfReader = new PdfReader(templateFilename); + List locations = new List(); + + if (pdfReader.AcroFields.Fields.ContainsKey("DiscoAttachmentId")) + { + foreach (var pdfFieldPosition in pdfReader.AcroFields.GetFieldPositions("DiscoAttachmentId")) + { + var pdfPageSize = pdfReader.GetPageSize(pdfFieldPosition.page); + locations.Add(new RectangleF((float)System.Math.Min(1.0, System.Math.Max(0.0, (double)(pdfFieldPosition.position.Left / pdfPageSize.Width) - 0.1)), (float)System.Math.Min(1.0, System.Math.Max(0.0, (double)((pdfPageSize.Height - pdfFieldPosition.position.Top) / pdfPageSize.Height) - 0.1)), (float)System.Math.Min(1.0, System.Math.Max(0.0, (double)(pdfFieldPosition.position.Width / pdfPageSize.Width) + 0.2)), (float)System.Math.Min(1.0, System.Math.Max(0.0, (double)(pdfFieldPosition.position.Height / pdfPageSize.Height) + 0.2)))); + } + } + pdfReader.Close(); + + // Update Cache + SetValue(dt.Id, locations); + return locations; + } + + } +} diff --git a/Disco.BI/BI/DocumentTemplateBI/DocumentUniqueIdentifier.cs b/Disco.BI/BI/DocumentTemplateBI/DocumentUniqueIdentifier.cs new file mode 100644 index 00000000..08eaca39 --- /dev/null +++ b/Disco.BI/BI/DocumentTemplateBI/DocumentUniqueIdentifier.cs @@ -0,0 +1,203 @@ +using Disco.Data.Repository; +using Disco.Models.Repository; +using System; +namespace Disco.BI.DocumentTemplateBI +{ + public class DocumentUniqueIdentifier + { + private bool? _loadedComponentsOk; + private DocumentTemplate _documentTemplate; + private object _data; + private string _dataDescription; + public string TemplateTypeId { get; private set; } + public string DataId { get; private set; } + public string DocumentUniqueId + { + get + { + return string.Format("{0}|{1}", this.TemplateTypeId, this.DataId); + } + } + public string CreatorId { get; private set; } + public System.DateTime TimeStamp { get; private set; } + public int Page { get; private set; } + public string Tag { get; private set; } + public DocumentTemplate DocumentTemplate + { + get + { + bool flag = this._loadedComponentsOk.HasValue && this._loadedComponentsOk.Value; + if (flag) + { + return this._documentTemplate; + } + throw new System.Exception("Document Unique Identifier Components not loaded or invalid"); + } + } + public object Data + { + get + { + bool flag = this._loadedComponentsOk.HasValue && this._loadedComponentsOk.Value; + if (flag) + { + return this._data; + } + throw new System.Exception("Document Unique Identifier Components not loaded or invalid"); + } + } + public string DataDescription + { + get + { + bool flag = this._loadedComponentsOk.HasValue && this._loadedComponentsOk.Value; + if (flag) + { + return this._dataDescription; + } + throw new System.Exception("Document Unique Identifier Components not loaded or invalid"); + } + } + public string DataScope { get; private set; } + public static bool IsDocumentUniqueIdentifier(string UniqueIdentifier) + { + return UniqueIdentifier.StartsWith("Disco|", System.StringComparison.InvariantCultureIgnoreCase); + } + public DocumentUniqueIdentifier(string TemplateTypeId, string DataId, string CreatorId, DateTime TimeStamp, int? Page = null, string Tag = null) + { + this.Tag = Tag; + this.TemplateTypeId = TemplateTypeId; + this.DataId = DataId; + this.CreatorId = CreatorId; + this.TimeStamp = TimeStamp; + this.Page = Page ?? 0; + } + public DocumentUniqueIdentifier(string UniqueIdentifier, string Tag) + { + if (!DocumentUniqueIdentifier.IsDocumentUniqueIdentifier(UniqueIdentifier)) + { + throw new System.ArgumentException("Invalid Document Unique Identifier", "UniqueIdentifier"); + } + this.Tag = Tag; + string[] s = UniqueIdentifier.Split(new char[] { '|' }); + string left = s[1].ToUpper(); + if (left == "AT" || left == "1") + { + if (s.Length >= 3) + { + this.TemplateTypeId = s[2]; + } + if (s.Length >= 4) + { + this.DataId = s[3]; + } + if (s.Length >= 5) + { + this.CreatorId = s[4]; + } + if (s.Length >= 6) + { + System.DateTime timeStamp; + if (System.DateTime.TryParse(s[5], out timeStamp)) + { + this.TimeStamp = timeStamp; + } + } + if (s.Length >= 7) + { + int page = 0; + if (int.TryParse(s[6], out page)) + { + this.Page = page; + } + } + return; + } + throw new System.ArgumentException(string.Format("Invalid Document Unique Identifier Version ({0})", s[1]), "UniqueIdentifier"); + } + public bool LoadComponents(DiscoDataContext Context) + { + bool LoadComponents; + if (!this._loadedComponentsOk.HasValue) + { + string scopeType; + if (this.TemplateTypeId.StartsWith("--")) + { + string templateTypeId = this.TemplateTypeId; + switch (this.TemplateTypeId) + { + case "--DEVICE": + scopeType = DocumentTemplate.DocumentTemplateScopes.Device; + break; + case "--JOB": + scopeType = DocumentTemplate.DocumentTemplateScopes.Job; + break; + case "--USER": + scopeType = DocumentTemplate.DocumentTemplateScopes.User; + break; + default: + scopeType = null; + break; + } + } + else + { + this._documentTemplate = Context.DocumentTemplates.Find(this.TemplateTypeId); + if (this._documentTemplate != null) + { + scopeType = this._documentTemplate.Scope; + } + else + { + scopeType = null; + } + } + if (scopeType != null) + { + this.DataScope = scopeType; + switch (scopeType) + { + case DocumentTemplate.DocumentTemplateScopes.Device: + Device d = Context.Devices.Find(this.DataId); + if (d != null) + { + this._data = d; + this._dataDescription = d.SerialNumber; + this._loadedComponentsOk = true; + LoadComponents = true; + return LoadComponents; + } + break; + case DocumentTemplate.DocumentTemplateScopes.Job: + Job i = Context.Jobs.Find(int.Parse(this.DataId)); + if (i != null) + { + this._data = i; + this._dataDescription = i.Id.ToString(); + this._loadedComponentsOk = true; + LoadComponents = true; + return LoadComponents; + } + break; + case DocumentTemplate.DocumentTemplateScopes.User: + User u = Context.Users.Find(this.DataId); + if (u != null) + { + this._data = u; + this._dataDescription = u.DisplayName; + this._loadedComponentsOk = true; + LoadComponents = true; + return LoadComponents; + } + break; + default: + break; + } + } + this._loadedComponentsOk = false; + } + LoadComponents = this._loadedComponentsOk.Value; + return LoadComponents; + } + } +} diff --git a/Disco.BI/BI/DocumentTemplateBI/Importer/DocumentDropBoxMonitor.cs b/Disco.BI/BI/DocumentTemplateBI/Importer/DocumentDropBoxMonitor.cs new file mode 100644 index 00000000..635f1871 --- /dev/null +++ b/Disco.BI/BI/DocumentTemplateBI/Importer/DocumentDropBoxMonitor.cs @@ -0,0 +1,85 @@ +using System; +using System.IO; +using System.Web.Caching; +using Disco.Data.Repository; +using Quartz; +using Quartz.Impl; +using Quartz.Impl.Triggers; + +namespace Disco.BI.DocumentTemplateBI.Importer +{ + public class DocumentDropBoxMonitor : System.IDisposable + { + private IScheduler _scheduler; + private FileSystemWatcher _fsw; + private Cache _httpCache; + + public const string WatcherFilter = "*.pdf"; + public string DropBoxLocation { get; private set; } + + public DocumentDropBoxMonitor(DiscoDataContext Context, ISchedulerFactory SchedulerFactory, Cache HttpCache) + { + if (Context == null) + throw new System.ArgumentNullException("Context"); + + this._httpCache = HttpCache; + + var location = DataStore.CreateLocation(Context, "DocumentDropBox"); + this.DropBoxLocation = location.EndsWith(@"\") ? location : string.Concat(location, @"\"); + + this._scheduler = SchedulerFactory.GetScheduler(); + this._scheduler.Start(); + } + public void ScheduleCurrentFiles(int Delay) + { + foreach (var filename in System.IO.Directory.GetFiles(this.DropBoxLocation, "*.pdf")) + { + this.ScheduleFile(filename, Delay); + } + } + public void StartWatching() + { + if (this._fsw == null) + { + this._fsw = new FileSystemWatcher(this.DropBoxLocation, "*.pdf"); + this._fsw.Created += new FileSystemEventHandler(this.FSW_Created); + } + this._fsw.EnableRaisingEvents = true; + } + public void StopWatching() + { + if (this._fsw != null) + { + this._fsw.EnableRaisingEvents = false; + } + } + public void ScheduleFile(string Filename, int Delay) + { + System.Guid guid = System.Guid.NewGuid(); + JobDetailImpl jd = new JobDetailImpl(guid.ToString(), typeof(DocumentImporterJob)); + jd.JobDataMap.Add("Filename", Filename); + jd.JobDataMap.Add("RetryCount", 0); + jd.JobDataMap.Add("HttpCache", this._httpCache); + guid = System.Guid.NewGuid(); + + System.DateTimeOffset startTimeUtc = new System.DateTimeOffset(DateTime.Now.AddSeconds((double)Delay)); + SimpleTriggerImpl trig = new SimpleTriggerImpl(guid.ToString(), startTimeUtc); + + this._scheduler.ScheduleJob(jd, trig); + } + private void FSW_Created(object sender, FileSystemEventArgs e) + { + if ((e.ChangeType & WatcherChangeTypes.Deleted) != WatcherChangeTypes.Deleted) + this.ScheduleFile(e.FullPath, 5); + } + + public void Dispose() + { + this.StopWatching(); + if (this._fsw != null) + this._fsw.Dispose(); + if (this._scheduler != null) + this._scheduler.Shutdown(false); + } + } +} diff --git a/Disco.BI/BI/DocumentTemplateBI/Importer/DocumentImporterCleanCacheJob.cs b/Disco.BI/BI/DocumentTemplateBI/Importer/DocumentImporterCleanCacheJob.cs new file mode 100644 index 00000000..2b5dc099 --- /dev/null +++ b/Disco.BI/BI/DocumentTemplateBI/Importer/DocumentImporterCleanCacheJob.cs @@ -0,0 +1,63 @@ +using Disco.Data.Repository; +using Disco.Services.Logging; +using Quartz; +using Quartz.Impl; +using Disco.Services.Tasks; + +namespace Disco.BI.DocumentTemplateBI.Importer +{ + public class DocumentImporterCleanCacheJob : ScheduledTask + { + public override string TaskName { get { return "Document Importer - Clean Cache Task"; } } + + public override bool SingleInstanceTask { get { return true; } } + public override bool CancelInitiallySupported { get { return false; } } + + public override void InitalizeScheduledTask(DiscoDataContext dbContext) + { + // Trigger Daily @ 12:30am + TriggerBuilder triggerBuilder = TriggerBuilder.Create().WithSchedule(CronScheduleBuilder.DailyAtHourAndMinute(0, 30)); + + this.ScheduleTask(triggerBuilder); + } + + protected override void ExecuteTask() + { + string dataStoreLocation; + using (DiscoDataContext dbContext = new DiscoDataContext()) + { + dataStoreLocation = DataStore.CreateLocation(dbContext, "Cache\\DocumentDropBox_SessionPages"); + } + + int deleteCount = 0; + int errorCount = 0; + + System.IO.DirectoryInfo dataStoreInfo = new System.IO.DirectoryInfo(dataStoreLocation); + + System.DateTime today = System.DateTime.Today; + + foreach (System.IO.FileInfo file in dataStoreInfo.GetFiles()) + { + try + { + if (file.CreationTime < today) + { + file.Delete(); + deleteCount++; + } + } + catch + { + errorCount++; + } + } + + SystemLog.LogInformation( + string.Format("Cleared DocumentDropBox_SessionPages Cache, Deleted {0} File/s, with {1} Error/s", deleteCount, errorCount), + deleteCount, + errorCount + ); + } + + } +} diff --git a/Disco.BI/BI/DocumentTemplateBI/Importer/DocumentImporterJob.cs b/Disco.BI/BI/DocumentTemplateBI/Importer/DocumentImporterJob.cs new file mode 100644 index 00000000..f06297fc --- /dev/null +++ b/Disco.BI/BI/DocumentTemplateBI/Importer/DocumentImporterJob.cs @@ -0,0 +1,120 @@ +using System; +using System.IO; +using System.Web.Caching; +using Disco.Data.Repository; +using Quartz; +using Quartz.Impl.Triggers; + +namespace Disco.BI.DocumentTemplateBI.Importer +{ + [PersistJobDataAfterExecution] + public class DocumentImporterJob : IJob + { + + void IJob.Execute(IJobExecutionContext context) + { + string sessionId = context.JobDetail.JobDataMap["SessionId"] as string; + if (string.IsNullOrEmpty(sessionId)) + { + sessionId = Guid.NewGuid().ToString(); + context.JobDetail.JobDataMap["SessionId"] = sessionId; + } + + string filename = context.JobDetail.JobDataMap["Filename"] as string; + int retryCount = (int)context.JobDetail.JobDataMap["RetryCount"]; + Cache httpCache = context.JobDetail.JobDataMap["HttpCache"] as Cache; + + var friendlyFilename = filename; + if (!string.IsNullOrEmpty(friendlyFilename)) + friendlyFilename = System.IO.Path.GetFileName(friendlyFilename); + + DocumentImporterLog.LogImportStarting(sessionId, friendlyFilename); + + if (!File.Exists(filename)) + { + DocumentImporterLog.LogImportWarning(sessionId, string.Format("File not found: {0}", filename)); + DocumentImporterLog.LogImportFinished(sessionId); + context.Scheduler.DeleteJob(context.JobDetail.Key); + return; + } + + try + { + using (DiscoDataContext dbContext = new DiscoDataContext()) + { + if (retryCount < 18) + { + context.JobDetail.JobDataMap["RetryCount"] = (++retryCount); + bool processResult = Interop.Pdf.PdfImporter.ProcessPdfAttachment(filename, dbContext, sessionId, httpCache); + + if (processResult) + { + // Import Successful - Delete + if (File.Exists(filename)) + File.Delete(filename); + } + else + { + // Import Failed - Move to Errors Folder + if (File.Exists(filename)) + { + try + { + string folderError = DataStore.CreateLocation(dbContext, "DocumentDropBox_Errors"); + string filenameError = Path.Combine(folderError, Path.GetFileName(filename)); + int filenameErrorCount = 0; + while (File.Exists(filenameError)) + { + filenameError = Path.Combine(folderError, string.Format("{0} ({1}){2}", Path.GetFileNameWithoutExtension(filename), ++filenameErrorCount, Path.GetExtension(filename))); + } + File.Move(filename, filenameError); + } + catch + { + // Ignore Errors + } + } + } + } + else + { + // To Many Errors + DocumentImporterLog.LogImportError(sessionId, string.Format("To many errors occurred trying to import '{1}' (SessionId: {0})", sessionId, friendlyFilename)); + // Move to Errors Folder + if (File.Exists(filename)) + { + try + { + string folderError = DataStore.CreateLocation(dbContext, "DocumentDropBox_Errors"); + string filenameError = Path.Combine(folderError, Path.GetFileName(filename)); + int filenameErrorCount = 0; + while (File.Exists(filenameError)) + { + filenameError = Path.Combine(folderError, string.Format("{0} ({1}){2}", Path.GetFileNameWithoutExtension(filename), ++filenameErrorCount, Path.GetExtension(filename))); + } + File.Move(filename, filenameError); + } + catch + { + // Ignore Errors + } + } + } + } + DocumentImporterLog.LogImportFinished(sessionId); + + // All Done + context.Scheduler.DeleteJob(context.JobDetail.Key); + } + catch (Exception ex) + { + DocumentImporterLog.LogImportWarning(sessionId, string.Format("{0}; Will try again in 10 Seconds", ex.Message)); + // Reschedule Job for 10 seconds + SimpleTriggerImpl trig = new SimpleTriggerImpl(Guid.NewGuid().ToString(), new DateTimeOffset(DateTime.Now.AddSeconds(10))); + context.Scheduler.RescheduleJob(context.Trigger.Key, trig); + } + + } + + } +} diff --git a/Disco.BI/BI/DocumentTemplateBI/Importer/DocumentImporterLog.cs b/Disco.BI/BI/DocumentTemplateBI/Importer/DocumentImporterLog.cs new file mode 100644 index 00000000..cb8f4b1b --- /dev/null +++ b/Disco.BI/BI/DocumentTemplateBI/Importer/DocumentImporterLog.cs @@ -0,0 +1,304 @@ +using Disco.Services.Logging; +using Disco.Services.Logging.Models; +using System; +using System.Collections.Generic; +using System.Diagnostics; +namespace Disco.BI.DocumentTemplateBI.Importer +{ + public class DocumentImporterLog : LogBase + { + public enum EventTypeIds + { + ImportStarting = 10, + ImportProgress, + ImportFinished, + ImportWarning = 15, + ImportError, + ImportPageStarting = 100, + ImportPageImageUpdate = 104, + ImportPageProgress, + ImportPageDetected = 110, + ImportPageUndetected = 115, + ImportPageError = 120, + ImportPageUndetectedStored = 150 + } + + private const int _ModuleId = 40; + + public static DocumentImporterLog Current + { + get + { + return (DocumentImporterLog)LogContext.LogModules[_ModuleId]; + } + } + + public override string ModuleDescription + { + get + { + return "Document Importer"; + } + } + public override int ModuleId + { + get + { + return _ModuleId; + } + } + public override string ModuleName + { + get + { + return "DocumentImporter"; + } + } + private static void Log(DocumentImporterLog.EventTypeIds EventTypeId, params object[] Args) + { + DocumentImporterLog.Current.Log((int)EventTypeId, Args); + } + public static void LogImportStarting(string SessionId, string DocumentName) + { + DocumentImporterLog.Log(DocumentImporterLog.EventTypeIds.ImportStarting, new object[] + { + SessionId, + DocumentName + }); + } + public static void LogImportProgress(string SessionId, int? Progress, string Status) + { + DocumentImporterLog.Log(DocumentImporterLog.EventTypeIds.ImportProgress, new object[] + { + SessionId, + Progress, + Status + }); + } + public static void LogImportFinished(string SessionId) + { + DocumentImporterLog.Log(DocumentImporterLog.EventTypeIds.ImportFinished, new object[] + { + SessionId + }); + } + public static void LogImportWarning(string SessionId, string Message) + { + DocumentImporterLog.Log(DocumentImporterLog.EventTypeIds.ImportWarning, new object[] + { + SessionId, + Message + }); + } + public static void LogImportError(string SessionId, string Message) + { + DocumentImporterLog.Log(DocumentImporterLog.EventTypeIds.ImportError, new object[] + { + SessionId, + Message + }); + } + public static void LogImportPageStarting(string SessionId, int PageNumber) + { + DocumentImporterLog.Log(DocumentImporterLog.EventTypeIds.ImportPageStarting, new object[] + { + SessionId, + PageNumber + }); + } + public static void LogImportPageImageUpdate(string SessionId, int PageNumber) + { + DocumentImporterLog.Log(DocumentImporterLog.EventTypeIds.ImportPageImageUpdate, new object[] + { + SessionId, + PageNumber + }); + } + public static void LogImportPageProgress(string SessionId, int PageNumber, int? Progress, string Status) + { + DocumentImporterLog.Log(DocumentImporterLog.EventTypeIds.ImportPageProgress, new object[] + { + SessionId, + PageNumber, + Progress, + Status + }); + } + public static void LogImportPageDetected(string SessionId, int PageNumber, string DocumentTypeId, string DocumentTypeName, string TargetType, string AssignedId, string AssignedName) + { + DocumentImporterLog.Log(DocumentImporterLog.EventTypeIds.ImportPageDetected, new object[] + { + SessionId, + PageNumber, + DocumentTypeId, + DocumentTypeName, + TargetType, + AssignedId, + AssignedName + }); + } + public static void LogImportPageUndetected(string SessionId, int PageNumber) + { + DocumentImporterLog.Log(DocumentImporterLog.EventTypeIds.ImportPageUndetected, new object[] + { + SessionId, + PageNumber + }); + } + public static void LogImportPageError(string SessionId, int PageNumber, string Message) + { + DocumentImporterLog.Log(DocumentImporterLog.EventTypeIds.ImportPageError, new object[] + { + SessionId, + PageNumber, + Message + }); + } + public static void LogImportPageUndetectedStored(string SessionId, int PageNumber) + { + DocumentImporterLog.Log(DocumentImporterLog.EventTypeIds.ImportPageUndetectedStored, new object[] + { + SessionId, + PageNumber + }); + } + protected override System.Collections.Generic.List LoadEventTypes() + { + return new System.Collections.Generic.List + { + new LogEventType + { + Id = 10, + ModuleId = 40, + Name = "Import Starting", + Format = "Starting import of document: {1} (SessionId: {0})", + Severity = 0, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 11, + ModuleId = 40, + Name = "Import Progress", + Format = "Processing: {1}% Complete; Status: {2}", + Severity = 0, + UseLive = true, + UsePersist = false, + UseDisplay = false + }, + new LogEventType + { + Id = 12, + ModuleId = 40, + Name = "Import Finished", + Format = "Import of document complete (SessionId: {0})", + Severity = 0, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 15, + ModuleId = 40, + Name = "Import Warning", + Format = "Import Warning: {1} (SessionId: {0})", + Severity = 1, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 16, + ModuleId = 40, + Name = "Import Error", + Format = "Import Error: {1} (SessionId: {0})", + Severity = 2, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 100, + ModuleId = 40, + Name = "Import Page Starting", + Format = "Starting import of page: {1} (SessionId: {0})", + Severity = 0, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 104, + ModuleId = 40, + Name = "Import Page Image Update", + Format = null, + Severity = 0, + UseLive = true, + UsePersist = false, + UseDisplay = false + }, + new LogEventType + { + Id = 105, + ModuleId = 40, + Name = "Import Page Progress", + Format = "Processing: Page {1}; {2}% Complete; Status: {3}", + Severity = 0, + UseLive = true, + UsePersist = false, + UseDisplay = false + }, + new LogEventType + { + Id = 110, + ModuleId = 40, + Name = "Import Page Assigned", + Format = "Page {1} of type '{3}' assigned to {4}: '{6}'", + Severity = 0, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 115, + ModuleId = 40, + Name = "Import Page Undetected", + Format = "Page {1} not detected", + Severity = 1, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 120, + ModuleId = 40, + Name = "Import Page Error", + Format = "Page {1}, Import Error: {2}", + Severity = 2, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 150, + ModuleId = 40, + Name = "Import Page Undetected Stored", + Format = null, + Severity = 0, + UseLive = true, + UsePersist = false, + UseDisplay = false + } + }; + } + } +} diff --git a/Disco.BI/BI/DocumentTemplateBI/Utilities.cs b/Disco.BI/BI/DocumentTemplateBI/Utilities.cs new file mode 100644 index 00000000..0d54bc45 --- /dev/null +++ b/Disco.BI/BI/DocumentTemplateBI/Utilities.cs @@ -0,0 +1,49 @@ +using iTextSharp.text; +using iTextSharp.text.pdf; + +namespace Disco.BI.DocumentTemplateBI +{ + public static class Utilities + { + public static System.IO.Stream JoinPdfs(params System.IO.Stream[] Pdfs) + { + if (Pdfs.Length == 0) + throw new System.ArgumentNullException("Pdfs"); + + // Only One PDF - Possible Reference Bug v's Memory/Speed (Returning Param Memory Stream) + if (Pdfs.Length == 1) + return Pdfs[0]; + + // Join Pdfs + System.IO.MemoryStream msBuilder = new System.IO.MemoryStream(); + + Document pdfDoc = new Document(); + PdfCopy pdfCopy = new PdfCopy(pdfDoc, msBuilder); + pdfDoc.Open(); + pdfCopy.CloseStream = false; + + for (int i = 0; i < Pdfs.Length; i++) + { + System.IO.Stream pdf = Pdfs[i]; + PdfReader pdfReader = new PdfReader(pdf); + + for (int indexPage = 1; indexPage <= pdfReader.NumberOfPages; indexPage++) + { + iTextSharp.text.Rectangle pageSize = pdfReader.GetPageSizeWithRotation(indexPage); + PdfImportedPage page = pdfCopy.GetImportedPage(pdfReader, indexPage); + pdfDoc.SetPageSize(pageSize); + pdfDoc.NewPage(); + pdfCopy.AddPage(page); + } + + pdfReader.Close(); + } + + pdfDoc.Close(); + pdfCopy.Close(); + msBuilder.Position = 0; + + return msBuilder; + } + } +} diff --git a/Disco.BI/BI/Expressions/EvaluateExpressionParseException.cs b/Disco.BI/BI/Expressions/EvaluateExpressionParseException.cs new file mode 100644 index 00000000..88d90554 --- /dev/null +++ b/Disco.BI/BI/Expressions/EvaluateExpressionParseException.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Spring.Expressions.Parser.antlr; + +namespace Disco.BI.Expressions +{ + public class EvaluateExpressionParseException + { + public string Expression { get; set; } + public int PositionRow { get; set; } + public int PositionColumn { get; set; } + public string Message { get; set; } + + internal static EvaluateExpressionParseException FromRecognitionException(RecognitionException e, string Expression) + { + return new EvaluateExpressionParseException() + { + Expression = Expression, + Message = e.Message, + PositionRow = e.getLine(), + PositionColumn = e.getColumn() + }; + } + } +} diff --git a/Disco.BI/BI/Expressions/EvaluateExpressionPart.cs b/Disco.BI/BI/Expressions/EvaluateExpressionPart.cs new file mode 100644 index 00000000..f258c286 --- /dev/null +++ b/Disco.BI/BI/Expressions/EvaluateExpressionPart.cs @@ -0,0 +1,84 @@ +using System; +using System.Collections; +using System.Runtime.CompilerServices; +using Spring.Expressions.Parser.antlr; + +namespace Disco.BI.Expressions +{ + public class EvaluateExpressionPart : IExpressionPart + { + private Spring.Expressions.IExpression _Expression; + private RecognitionException _ExpressionParseException; + private EvaluateExpressionParseException _EvaluateParseException; + + public string RawSource { get; set; } + public string Source { get; set; } + public bool ErrorsAllowed { get; set; } + public bool IsDynamic { get { return true; } set { return; } } + + public EvaluateExpressionParseException ParseException + { + get + { + if (_ExpressionParseException == null) + return null; + else + if (_EvaluateParseException == null) + _EvaluateParseException = EvaluateExpressionParseException.FromRecognitionException(_ExpressionParseException, this.Source); + return _EvaluateParseException; + } + } + + public bool ParseError + { + get { return (_ExpressionParseException != null); } + } + public string ParseErrorMessage + { + get + { + if (ParseError) + return ParseException.Message; + else + return null; + } + } + + public EvaluateExpressionPart(string Source) + { + this.RawSource = Source; + + if (Source.StartsWith("{") && Source.EndsWith("}")) + Source = Source.Substring(1, Source.Length - 2); + + if (Source[0] == '~') + { + this.ErrorsAllowed = true; + this.Source = Source.Substring(1); + } + else + { + this.ErrorsAllowed = false; + this.Source = Source; + } + try + { + this._Expression = Spring.Expressions.Expression.Parse(this.Source); + + } + catch (RecognitionException ex) + { + this._ExpressionParseException = ex; + } + } + object IExpressionPart.Evaluate(object ExpressionContext, System.Collections.IDictionary Variables) + { + if (this._ExpressionParseException == null) + { + return this._Expression.GetValue(ExpressionContext, Variables); + } + throw this._ExpressionParseException; + } + + } +} diff --git a/Disco.BI/BI/Expressions/Expression.cs b/Disco.BI/BI/Expressions/Expression.cs new file mode 100644 index 00000000..67960977 --- /dev/null +++ b/Disco.BI/BI/Expressions/Expression.cs @@ -0,0 +1,259 @@ +using Disco.Data.Repository; +using Disco.Models.BI.DocumentTemplates; +using Disco.Models.Repository; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Text; +using Disco.Models.BI.Expressions; + +namespace Disco.BI.Expressions +{ + public sealed class Expression : System.Collections.Generic.List + { + public string Name { get; private set; } + public string Source { get; private set; } + public bool IsDynamic { get; private set; } + public int Ordinal { get; private set; } + + private Expression(string Name, string Source, int Ordinal) + { + this.Name = Name; + this.Source = Source; + this.Ordinal = Ordinal; + } + + public static void InitializeExpressions() + { + Spring.Core.TypeResolution.TypeRegistry.RegisterType("DataExt", typeof(Extensions.DataExt)); + Spring.Core.TypeResolution.TypeRegistry.RegisterType("UserExt", typeof(Extensions.UserExt)); + Spring.Core.TypeResolution.TypeRegistry.RegisterType("DeviceExt", typeof(Extensions.DeviceExt)); + Spring.Core.TypeResolution.TypeRegistry.RegisterType("ImageExt", typeof(Extensions.ImageExt)); + } + + public T EvaluateFirst(object ExpressionContext, System.Collections.IDictionary Variables) + { + T result = default(T); + if (this.Count > 0) + { + try + { + object expressionResult = this[0].Evaluate(ExpressionContext, Variables); + if (expressionResult != null) + { + if (expressionResult is T) + { + result = (T)expressionResult; + } + else + { + throw new InvalidOperationException("Expression returned an invalid type"); + } + } + } + catch (System.Exception ex) + { + throw new InvalidOperationException("Expression evaluation resulted in an error", ex); + } + } + + return result; + } + + public Tuple Evaluate(object ExpressionContext, System.Collections.IDictionary Variables) + { + System.Text.StringBuilder resultValue = new System.Text.StringBuilder(); + object resultObject = null; + bool resultError = false; + foreach (var expressionPart in this) + { + try + { + object partValue = expressionPart.Evaluate(ExpressionContext, Variables); + if (partValue != null) + { + // Check for Result Objects + if (partValue is IImageExpressionResult) + resultObject = partValue; + else + resultValue.Append(partValue.ToString()); + } + } + catch (System.Exception ex) + { + if (!expressionPart.ErrorsAllowed) + { + resultValue.Append("## ERROR # "); + resultValue.Append(ex.Message); + resultValue.Append(" ##"); + resultError = true; + } + } + } + return new Tuple(resultValue.ToString(), resultError, resultObject); + } + public static Expression TokenizeSingleDynamic(string Name, string ExpressionSource, int Ordinal) + { + Expression e = new Expression(Name, ExpressionSource, Ordinal); + if (ExpressionSource != null && !string.IsNullOrWhiteSpace(ExpressionSource)) + e.Add(new EvaluateExpressionPart(ExpressionSource)); + e.IsDynamic = true; + return e; + } + public static Expression Tokenize(string Name, string ExpressionSource, int Ordinal) + { + Expression e = new Expression(Name, ExpressionSource, Ordinal); + if (!ExpressionSource.Contains("{") || !ExpressionSource.Contains("}")) + { + e.Add(new TextExpressionPart(ExpressionSource)); + } + else + { + System.Text.StringBuilder token = new System.Text.StringBuilder(); + bool tokenEval = false; + int tokenEvalDepth = 0; + foreach (char c in ExpressionSource) + { + switch (c) + { + case '{': + { + if (!tokenEval) + { + if (token.Length > 0) + { + e.Add(new TextExpressionPart(token.ToString())); + token = new System.Text.StringBuilder(); + } + tokenEval = true; + tokenEvalDepth = 0; + } + tokenEvalDepth++; + token.Append(c); + break; + } + case '}': + { + token.Append(c); + if (tokenEval) + { + tokenEvalDepth--; + if (tokenEvalDepth <= 0) + { + if (token.Length != 2 && (token.Length != 3 || token[1] != '@')) + { + e.Add(new EvaluateExpressionPart(token.ToString())); + e.IsDynamic = true; + token = new System.Text.StringBuilder(); + } + tokenEval = false; + } + } + break; + } + default: + { + token.Append(c); + break; + } + } + } + if (token.Length > 0) + { + e.Add(new TextExpressionPart(token.ToString())); + } + } + return e; + } + + public static IDictionary StandardVariables(DocumentTemplate AttachmentType, DiscoDataContext DataContext, User User, System.DateTime TimeStamp, DocumentState DocumentState) + { + return new Hashtable + { + + { + "DataContext", + DataContext + }, + + { + "User", + User + }, + + { + "TimeStamp", + TimeStamp + }, + + { + "AttachmentType", + AttachmentType + }, + + { + "State", + DocumentState + } + }; + } + public static Dictionary StandardVariableTypes() + { + return new Dictionary + { + + { + "#DataContext", + typeof(DiscoDataContext).AssemblyQualifiedName + }, + + { + "#User", + typeof(User).AssemblyQualifiedName + }, + + { + "#TimeStamp", + typeof(System.DateTime).AssemblyQualifiedName + }, + + { + "#AttachmentType", + typeof(DocumentTemplate).AssemblyQualifiedName + }, + + { + "#State", + typeof(DocumentState).AssemblyQualifiedName + } + }; + } + public static Dictionary ExtensionLibraryTypes() + { + return new Dictionary + { + { + "DataExt", + typeof(Extensions.DataExt).AssemblyQualifiedName + }, + + { + "DeviceExt", + typeof(Extensions.DeviceExt).AssemblyQualifiedName + }, + + { + "ImageExt", + typeof(Extensions.ImageExt).AssemblyQualifiedName + }, + + { + "UserExt", + typeof(Extensions.UserExt).AssemblyQualifiedName + } + }; + } + + } +} diff --git a/Disco.BI/BI/Expressions/ExpressionCache.cs b/Disco.BI/BI/Expressions/ExpressionCache.cs new file mode 100644 index 00000000..8f433689 --- /dev/null +++ b/Disco.BI/BI/Expressions/ExpressionCache.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Collections.Concurrent; + +namespace Disco.BI.Expressions +{ + public static class ExpressionCache + { + private static ConcurrentDictionary> _Cache = new ConcurrentDictionary>(); + + public delegate Expression CreateValueDelegate(); + + public static ConcurrentDictionary GetModule(string Module, bool Create = false) + { + ConcurrentDictionary moduleCache; + if (_Cache.TryGetValue(Module, out moduleCache)) + return moduleCache; + else + { + if (Create) + { + moduleCache = new ConcurrentDictionary(); + _Cache.TryAdd(Module, moduleCache); + return moduleCache; + } + else + return null; + } + } + private static Expression GetModuleValue(string Module, string Key, CreateValueDelegate CreateValue) + { + ConcurrentDictionary moduleCache = GetModule(Module, (CreateValue != null)); + if (moduleCache != null) + { + Expression expression; + if (moduleCache.TryGetValue(Key, out expression)) + { + return expression; + } + if (CreateValue != null) + { + expression = CreateValue(); + Expression oldExpression; + if (moduleCache.TryGetValue(Key, out oldExpression)) + moduleCache.TryUpdate(Key, expression, oldExpression); + else + moduleCache.TryAdd(Key, expression); + return expression; + } + } + return null; + } + + public static Expression GetValue(string Module, string Key, CreateValueDelegate CreateValue) + { + return GetModuleValue(Module, Key, CreateValue); + } + + public static Expression GetValue(string Module, string Key) + { + return GetModuleValue(Module, Key, null); + } + + public static bool InvalidModule(string Module) + { + ConcurrentDictionary moduleCache; + return _Cache.TryRemove(Module, out moduleCache); + } + + public static bool InvalidateKey(string Module, string Key) + { + Expression expression; + ConcurrentDictionary moduleCache = GetModule(Module, false); + if (moduleCache != null) + { + bool removeResult = moduleCache.TryRemove(Key, out expression); + if (moduleCache.Count == 0) + InvalidModule(Module); + return removeResult; + } + else + return false; + } + + public static bool SetValue(string Module, string Key, Expression Expression) + { + ConcurrentDictionary moduleCache = GetModule(Module, true); + + if (moduleCache.ContainsKey(Key)) + { + Expression oldExpression; + if (moduleCache.TryGetValue(Key, out oldExpression)) + { + return moduleCache.TryUpdate(Key, Expression, oldExpression); + } + } + return moduleCache.TryAdd(Key, Expression); + } + + } +} diff --git a/Disco.BI/BI/Expressions/ExpressionCachePreloadTask.cs b/Disco.BI/BI/Expressions/ExpressionCachePreloadTask.cs new file mode 100644 index 00000000..c9291837 --- /dev/null +++ b/Disco.BI/BI/Expressions/ExpressionCachePreloadTask.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Services.Tasks; +using Disco.Data.Repository; +using Quartz; +using Disco.BI.Extensions; +using System.Diagnostics; + +namespace Disco.BI.Expressions +{ + public class ExpressionCachePreloadTask : ScheduledTask + { + + public override string TaskName { get { return "Expression Cache - Preload Task"; } } + public override bool SingleInstanceTask { get { return true; } } + public override bool CancelInitiallySupported { get { return false; } } + + public override void InitalizeScheduledTask(DiscoDataContext dbContext) + { + // Run in Background 1 Second after Scheduled (on App Startup) + TriggerBuilder triggerBuilder = TriggerBuilder.Create().StartAt(new DateTimeOffset(DateTime.Now).AddSeconds(5)); + + this.ScheduleTask(triggerBuilder); + } + + protected override void ExecuteTask() + { + // Cache Document Template Filter Expressions + using (DiscoDataContext dbContext = new DiscoDataContext()) + { + foreach (var documentTemplate in dbContext.DocumentTemplates.Where(dt => dt.FilterExpression != null && dt.FilterExpression != string.Empty)) + { + if (!string.IsNullOrWhiteSpace(documentTemplate.FilterExpression)) + documentTemplate.FilterExpressionFromCache(); + } + } + + + } + } +} diff --git a/Disco.BI/BI/Expressions/ExpressionTypeDescriptor.cs b/Disco.BI/BI/Expressions/ExpressionTypeDescriptor.cs new file mode 100644 index 00000000..bec6dc14 --- /dev/null +++ b/Disco.BI/BI/Expressions/ExpressionTypeDescriptor.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Disco.BI.Expressions +{ + public class ExpressionTypeDescriptor + { + public string ExpressionType { get; set; } + public string Name { get; set; } + public List Members { get; set; } + + public static ExpressionTypeDescriptor Build(System.Type t, bool StaticDeclaredMembersOnly = true) + { + ExpressionTypeDescriptor i = new ExpressionTypeDescriptor + { + ExpressionType = t.AssemblyQualifiedName, + Name = t.Name + }; + i.Members = new System.Collections.Generic.List(); + + System.Reflection.MemberInfo[] members; + if (StaticDeclaredMembersOnly) + members = t.GetMembers(BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly); + else + members = t.GetMembers(BindingFlags.Public | BindingFlags.Instance); + + for (int j = 0; j < members.Length; j++) + { + System.Reflection.MemberInfo member = members[j]; + if (member is System.Reflection.PropertyInfo) + { + System.Reflection.PropertyInfo pi = (System.Reflection.PropertyInfo)member; + if (!pi.IsSpecialName && pi.CanRead) + { + i.Members.Add(ExpressionTypeMemberDescriptor.Build(pi)); + } + } + if (member is System.Reflection.MethodInfo) + { + System.Reflection.MethodInfo mi2 = (System.Reflection.MethodInfo)member; + if (!mi2.IsSpecialName) + { + i.Members.Add(ExpressionTypeMemberDescriptor.Build(mi2)); + } + } + } + i.Members = ( + from mi in i.Members + orderby mi.Name + select mi).ToList(); + return i; + } + } +} diff --git a/Disco.BI/BI/Expressions/ExpressionTypeMemberDescriptor.cs b/Disco.BI/BI/Expressions/ExpressionTypeMemberDescriptor.cs new file mode 100644 index 00000000..433c7312 --- /dev/null +++ b/Disco.BI/BI/Expressions/ExpressionTypeMemberDescriptor.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Runtime.CompilerServices; + +namespace Disco.BI.Expressions +{ + public class ExpressionTypeMemberDescriptor + { + public const string FunctionKind = "function"; + public const string PropertyKind = "property"; + public const string ParameterKind = "parameter"; + + public string Kind {get;set;} + public string Name {get;set;} + public string ReturnType {get;set;} + public string ReturnExpressionType{get;set;} + public List Parameters{get;set;} + + public static ExpressionTypeMemberDescriptor Build(System.Reflection.MethodInfo m) + { + ExpressionTypeMemberDescriptor md = new ExpressionTypeMemberDescriptor + { + Kind = "function", + Name = m.Name, + ReturnType = m.ReturnType.Name, + ReturnExpressionType = m.ReturnType.AssemblyQualifiedName + }; + md.Parameters = ( + from mdp in m.GetParameters() + select ExpressionTypeMemberDescriptor.Build(mdp)).ToList(); + return md; + } + public static ExpressionTypeMemberDescriptor Build(System.Reflection.PropertyInfo p) + { + ExpressionTypeMemberDescriptor md = new ExpressionTypeMemberDescriptor + { + Kind = "property", + Name = p.Name, + ReturnType = p.PropertyType.Name, + ReturnExpressionType = p.PropertyType.AssemblyQualifiedName + }; + md.Parameters = ( + from mdp in p.GetIndexParameters() + select ExpressionTypeMemberDescriptor.Build(mdp)).ToList(); + return md; + } + public static ExpressionTypeMemberDescriptor Build(System.Reflection.ParameterInfo pi) + { + return new ExpressionTypeMemberDescriptor + { + Kind = "parameter", + Name = pi.Name, + ReturnType = pi.ParameterType.Name, + ReturnExpressionType = pi.ParameterType.AssemblyQualifiedName + }; + } + } +} diff --git a/Disco.BI/BI/Expressions/Extensions/DataExt.cs b/Disco.BI/BI/Expressions/Extensions/DataExt.cs new file mode 100644 index 00000000..d7efbd1d --- /dev/null +++ b/Disco.BI/BI/Expressions/Extensions/DataExt.cs @@ -0,0 +1,176 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Data; +using System.Data.SqlClient; +using System.Collections; +using System.Data.Odbc; + +namespace Disco.BI.Expressions.Extensions +{ + public static class DataExt + { + #region SqlClient + + private static SqlConnection BuildSqlConnection(string Server, string Database, string Username, string Password) + { + SqlConnectionStringBuilder dbConnectionStringBuilder = new SqlConnectionStringBuilder(); + dbConnectionStringBuilder.ApplicationName = "Disco"; + dbConnectionStringBuilder.DataSource = Server; + dbConnectionStringBuilder.InitialCatalog = Database; + dbConnectionStringBuilder.MultipleActiveResultSets = true; + dbConnectionStringBuilder.PersistSecurityInfo = true; + if (Username == null || Password == null) + dbConnectionStringBuilder.IntegratedSecurity = true; + else + { + dbConnectionStringBuilder.UserID = Username; + dbConnectionStringBuilder.Password = Password; + } + + return new SqlConnection(dbConnectionStringBuilder.ConnectionString); + } + private static void BuildSqlParameters(SqlCommand dbCommand, Hashtable SqlParameters) + { + if (SqlParameters != null) + { + foreach (var sqlParameterKey in SqlParameters.Keys) + { + string key = sqlParameterKey.ToString(); + if (!key.StartsWith("@")) + key = string.Concat("@", key); + dbCommand.Parameters.AddWithValue(key, SqlParameters[sqlParameterKey]); + } + } + } + + public static DataTable QuerySqlDatabase(string Server, string Database, string Username, string Password, string SqlQuery, Hashtable SqlParameters) + { + using (SqlConnection dbConnection = BuildSqlConnection(Server, Database, Username, Password)) + { + using (SqlCommand dbCommand = new SqlCommand(SqlQuery, dbConnection)) + { + BuildSqlParameters(dbCommand, SqlParameters); + using (SqlDataAdapter dbAdapter = new SqlDataAdapter(dbCommand)) + { + var dbTable = new DataTable(); + dbAdapter.Fill(dbTable); + return dbTable; + } + } + } + } + public static DataTable QuerySqlDatabase(string Server, string Database, string SqlQuery, Hashtable SqlParameters) + { + return QuerySqlDatabase(Server, Database, null, null, SqlQuery, SqlParameters); + } + public static DataTable QuerySqlDatabase(string Server, string Database, string SqlQuery) + { + return QuerySqlDatabase(Server, Database, null, null, SqlQuery, null); + } + + public static object QuerySqlDatabaseScalar(string Server, string Database, string Username, string Password, string SqlQuery, Hashtable SqlParameters) + { + using (SqlConnection dbConnection = BuildSqlConnection(Server, Database, Username, Password)) + { + using (SqlCommand dbCommand = new SqlCommand(SqlQuery, dbConnection)) + { + BuildSqlParameters(dbCommand, SqlParameters); + try + { + dbConnection.Open(); + return dbCommand.ExecuteScalar(); + } + catch (Exception) + { + throw; + } + finally + { + dbConnection.Close(); + } + } + } + } + public static object QuerySqlDatabaseScalar(string Server, string Database, string SqlQuery, Hashtable SqlParameters) + { + return QuerySqlDatabaseScalar(Server, Database, null, null, SqlQuery, SqlParameters); + } + public static object QuerySqlDatabaseScalar(string Server, string Database, string SqlQuery) + { + return QuerySqlDatabaseScalar(Server, Database, null, null, SqlQuery, null); + } + + #endregion + + #region ODBC + + private static OdbcConnection BuildOdbcConnection(string ConnectionString) + { + return new OdbcConnection(ConnectionString); + } + private static void BuildOdbcParameters(OdbcCommand dbCommand, Hashtable OdbcParameters) + { + if (OdbcParameters != null) + { + foreach (var odbcParameterKey in OdbcParameters.Keys) + { + string key = odbcParameterKey.ToString(); + dbCommand.Parameters.AddWithValue(key, OdbcParameters[odbcParameterKey]); + } + } + } + + public static DataTable QueryOdbcDatabase(string ConnectionString, string OdbcQuery, Hashtable OdbcParameters) + { + using (OdbcConnection dbConnection = BuildOdbcConnection(ConnectionString)) + { + using (OdbcCommand dbCommand = new OdbcCommand(OdbcQuery, dbConnection)) + { + BuildOdbcParameters(dbCommand, OdbcParameters); + using (OdbcDataAdapter dbAdapter = new OdbcDataAdapter(dbCommand)) + { + var dbTable = new DataTable(); + dbAdapter.Fill(dbTable); + return dbTable; + } + } + } + } + public static DataTable QueryOdbcDatabase(string ConnectionString, string OdbcQuery) + { + return QueryOdbcDatabase(ConnectionString, OdbcQuery, null); + } + + public static object QueryOdbcDatabaseScalar(string ConnectionString, string OdbcQuery, Hashtable OdbcParameters) + { + using (OdbcConnection dbConnection = BuildOdbcConnection(ConnectionString)) + { + using (OdbcCommand dbCommand = new OdbcCommand(OdbcQuery, dbConnection)) + { + BuildOdbcParameters(dbCommand, OdbcParameters); + try + { + dbConnection.Open(); + return dbCommand.ExecuteScalar(); + } + catch (Exception) + { + throw; + } + finally + { + dbConnection.Close(); + } + } + } + } + public static object QueryOdbcDatabaseScalar(string ConnectionString, string OdbcQuery) + { + return QueryOdbcDatabaseScalar(ConnectionString, OdbcQuery, null); + } + + #endregion + } +} diff --git a/Disco.BI/BI/Expressions/Extensions/DeviceExt.cs b/Disco.BI/BI/Expressions/Extensions/DeviceExt.cs new file mode 100644 index 00000000..9d603aa8 --- /dev/null +++ b/Disco.BI/BI/Expressions/Extensions/DeviceExt.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Models.Repository; +using Disco.BI.Extensions; +using Disco.BI.Interop.ActiveDirectory; + +namespace Disco.BI.Expressions.Extensions +{ + public static class DeviceExt + { + public static object GetActiveDirectoryObjectValue(Device Device, string PropertyName, int Index = 0) + { + var adMachineAccount = Device.ActiveDirectoryAccount(PropertyName); + if (adMachineAccount != null) + return adMachineAccount.GetPropertyValue(PropertyName, Index); + else + return null; + } + + public static string GetActiveDirectoryStringValue(Device Device, string PropertyName, int Index = 0) + { + var objectValue = GetActiveDirectoryObjectValue(Device, PropertyName, Index); + string stringValue = objectValue as string; + if (stringValue == null && objectValue != null) + stringValue = objectValue.ToString(); + return stringValue; + } + + public static int GetActiveDirectoryIntegerValue(Device Device, string PropertyName, int Index = 0) + { + var objectValue = GetActiveDirectoryObjectValue(Device, PropertyName, Index); + if (objectValue == null) + return default(int); + else + { + int intValue; + try + { + intValue = (int)Convert.ChangeType(objectValue, typeof(int)); + } + catch (Exception) + { + throw; + } + return intValue; + } + } + } +} diff --git a/Disco.BI/BI/Expressions/Extensions/ImageExt.cs b/Disco.BI/BI/Expressions/Extensions/ImageExt.cs new file mode 100644 index 00000000..85914ad0 --- /dev/null +++ b/Disco.BI/BI/Expressions/Extensions/ImageExt.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Models.BI.Expressions; +using Disco.BI.Expressions.Extensions.ImageResultImplementations; +using Disco.Models.Repository; +using Disco.BI.Extensions; +using Disco.Data.Repository; +using System.Collections; +using System.IO; +using System.Drawing; + +namespace Disco.BI.Expressions.Extensions +{ + public static class ImageExt + { + public static FileImageExpressionResult ImageFromFile(string AbsoluteFilePath) + { + return new FileImageExpressionResult(AbsoluteFilePath); + } + public static FileImageExpressionResult ImageFromDataStoreFile(string RelativeFilePath) + { + var configCache = new Disco.Data.Configuration.ConfigurationContext(null); + string DataStoreLocation = configCache.DataStoreLocation; + string AbsoluteFilePath = System.IO.Path.Combine(DataStoreLocation, RelativeFilePath); + return new FileImageExpressionResult(AbsoluteFilePath); + } + public static FileImageExpressionResult JobAttachmentFirstImage(Job Job, DiscoDataContext dbContext) + { + var attachment = Job.JobAttachments.FirstOrDefault(ja => ja.MimeType.StartsWith("image/", StringComparison.InvariantCultureIgnoreCase)); + if (attachment != null) + { + var filename = attachment.RepositoryFilename(dbContext); + return new FileImageExpressionResult(filename); + } + else + return null; + } + public static FileImageExpressionResult JobAttachmentLastImage(Job Job, DiscoDataContext dbContext) + { + var attachment = Job.JobAttachments.LastOrDefault(ja => ja.MimeType.StartsWith("image/", StringComparison.InvariantCultureIgnoreCase)); + if (attachment != null) + { + var filename = attachment.RepositoryFilename(dbContext); + return new FileImageExpressionResult(filename); + } + else + return null; + } + public static FileImageExpressionResult JobAttachmentImage(JobAttachment JobAttachment, DiscoDataContext dbContext) + { + if (JobAttachment == null) + throw new ArgumentNullException("JobAttachment"); + if (!JobAttachment.MimeType.StartsWith("image/", StringComparison.InvariantCultureIgnoreCase)) + throw new ArgumentException("Invalid Image MimeType for Attachment"); + + var filename = JobAttachment.RepositoryFilename(dbContext); + return new FileImageExpressionResult(filename); + } + public static FileMontageImageExpressionResult JobAttachmentImageMontage(Job Job, DiscoDataContext dbContext) + { + if (Job == null) + throw new ArgumentNullException("Job"); + if (Job.JobAttachments == null) + throw new ArgumentException("Job.JobAttachments is null", "Job"); + + var attachments = Job.JobAttachments.Where(a => a.MimeType.StartsWith("image/", StringComparison.InvariantCultureIgnoreCase)).ToList(); + + if (attachments.Count > 0) + { + var attachmentFilepaths = attachments.Select(a => a.RepositoryFilename(dbContext)).ToList(); + + return new FileMontageImageExpressionResult(attachmentFilepaths); + } + else + return null; + } + public static FileMontageImageExpressionResult JobAttachmentsImageMontage(ArrayList JobAttachments, DiscoDataContext dbContext) + { + if (JobAttachments == null) + throw new ArgumentNullException("JobAttachments"); + + var attachments = JobAttachments.Cast().Where(a => a.MimeType.StartsWith("image/", StringComparison.InvariantCultureIgnoreCase)).ToList(); + + if (attachments.Count > 0) + { + var attachmentFilepaths = attachments.Select(a => a.RepositoryFilename(dbContext)).ToList(); + + return new FileMontageImageExpressionResult(attachmentFilepaths); + } + else + return null; + } + + public static BitmapImageExpressionResult ImageFromStream(Stream ImageStream) + { + if (ImageStream == null) + throw new ArgumentNullException("ImageStream"); + + return new BitmapImageExpressionResult(Bitmap.FromStream(ImageStream)); + } + public static BitmapImageExpressionResult ImageFromByteArray(byte[] ImageByteArray) + { + if (ImageByteArray == null) + throw new ArgumentNullException("ImageByteArray"); + + return new BitmapImageExpressionResult(Bitmap.FromStream(new MemoryStream(ImageByteArray))); + } + public static BitmapImageExpressionResult DeviceModelImage(DeviceModel DeviceModel) + { + if (DeviceModel == null) + throw new ArgumentNullException("DeviceModel"); + + using (Stream deviceModelImage = DeviceModel.Image()) + { + if (deviceModelImage == null) + return null; + else + return ImageFromStream(deviceModelImage); + } + //if (DeviceModel.Image == null || DeviceModel.Image.Length == 0) + // return null; + + //return ImageFromByteArray(DeviceModel.Image); + } + public static BitmapImageExpressionResult OrganisationLogo() + { + var configCache = new Disco.Data.Configuration.ConfigurationContext(null); + BitmapImageExpressionResult result; + using (var orgLogo = configCache.OrganisationLogo) + { + result = ImageFromStream(orgLogo); + } + result.LosslessFormat = true; + return result; + } + + } +} diff --git a/Disco.BI/BI/Expressions/Extensions/ImageResultImplementations/BaseImageExpressionResult.cs b/Disco.BI/BI/Expressions/Extensions/ImageResultImplementations/BaseImageExpressionResult.cs new file mode 100644 index 00000000..bd703af5 --- /dev/null +++ b/Disco.BI/BI/Expressions/Extensions/ImageResultImplementations/BaseImageExpressionResult.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Models.BI.Expressions; +using System.IO; +using System.Drawing; +using Disco.BI.Extensions; + +namespace Disco.BI.Expressions.Extensions.ImageResultImplementations +{ + public abstract class BaseImageExpressionResult : IImageExpressionResult + { + public byte Quality { get; set; } + public bool LosslessFormat { get; set; } + public bool ShowField { get; set; } + public string BackgroundColour { get; set; } + public bool BackgroundPreferTransparent { get; set; } + + public BaseImageExpressionResult() + { + this.LosslessFormat = false; + this.Quality = 90; + this.ShowField = false; + this.BackgroundPreferTransparent = true; + } + + public abstract Stream GetImage(int Width, int Height); + + protected Stream RenderImage(Image SourceImage, int Width, int Height) + { + if (SourceImage == null) + throw new ArgumentNullException("SourceImage"); + if (Width <= 0) + throw new ArgumentOutOfRangeException("Width", "Width must be > 0"); + if (Height <= 0) + throw new ArgumentOutOfRangeException("Height", "Height must be > 0"); + + Brush backgroundBrush = null; + if (!LosslessFormat || !BackgroundPreferTransparent) + { + if (string.IsNullOrEmpty(this.BackgroundColour)) + backgroundBrush = Brushes.White; + else + backgroundBrush = new SolidBrush(ColorTranslator.FromHtml(this.BackgroundColour)); + } + + using (Image resizedImage = SourceImage.ResizeImage(Width, Height, backgroundBrush)) + { + return OutputImage(resizedImage); + } + } + + protected Stream OutputImage(Image SourceImage) + { + MemoryStream imageStream = new MemoryStream(); + if (LosslessFormat) + { // Lossless Format - PNG + SourceImage.SavePng(imageStream); + } + else + { // Lossy Format - JPG + byte quality = Math.Min((byte)100, Math.Max((byte)1, this.Quality)); + SourceImage.SaveJpg(quality, imageStream); + } + imageStream.Position = 0; + return imageStream; + } + } +} diff --git a/Disco.BI/BI/Expressions/Extensions/ImageResultImplementations/BitmapImageExpressionResult.cs b/Disco.BI/BI/Expressions/Extensions/ImageResultImplementations/BitmapImageExpressionResult.cs new file mode 100644 index 00000000..938e3c41 --- /dev/null +++ b/Disco.BI/BI/Expressions/Extensions/ImageResultImplementations/BitmapImageExpressionResult.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; +using System.Drawing; + +namespace Disco.BI.Expressions.Extensions.ImageResultImplementations +{ + public class BitmapImageExpressionResult : BaseImageExpressionResult + { + public Image Image { get; set; } + + public BitmapImageExpressionResult(Image Image) + { + if (Image == null) + throw new ArgumentNullException("Image"); + + this.Image = Image; + } + + public override Stream GetImage(int Width, int Height) + { + return this.RenderImage(this.Image, Width, Height); + } + } +} diff --git a/Disco.BI/BI/Expressions/Extensions/ImageResultImplementations/FileImageExpressionResult.cs b/Disco.BI/BI/Expressions/Extensions/ImageResultImplementations/FileImageExpressionResult.cs new file mode 100644 index 00000000..9d9be068 --- /dev/null +++ b/Disco.BI/BI/Expressions/Extensions/ImageResultImplementations/FileImageExpressionResult.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; +using System.Drawing; + +namespace Disco.BI.Expressions.Extensions.ImageResultImplementations +{ + public class FileImageExpressionResult : BaseImageExpressionResult + { + public string AbsoluteFilePath { get; set; } + + public FileImageExpressionResult(string AbsoluteFilePath) + { + if (string.IsNullOrWhiteSpace(AbsoluteFilePath)) + throw new ArgumentNullException("AbsoluteFilePath"); + if (!File.Exists(AbsoluteFilePath)) + throw new FileNotFoundException("Image not found", AbsoluteFilePath); + + this.AbsoluteFilePath = AbsoluteFilePath; + } + + public override Stream GetImage(int Width, int Height) + { + using (Image SourceImage = Bitmap.FromFile(this.AbsoluteFilePath)) + { + return this.RenderImage(SourceImage, Width, Height); + } + } + } +} diff --git a/Disco.BI/BI/Expressions/Extensions/ImageResultImplementations/FileMontageImageExpressionResult.cs b/Disco.BI/BI/Expressions/Extensions/ImageResultImplementations/FileMontageImageExpressionResult.cs new file mode 100644 index 00000000..7500b6d3 --- /dev/null +++ b/Disco.BI/BI/Expressions/Extensions/ImageResultImplementations/FileMontageImageExpressionResult.cs @@ -0,0 +1,181 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; +using System.Drawing; +using System.Drawing.Drawing2D; +using Disco.BI.Extensions; + +namespace Disco.BI.Expressions.Extensions.ImageResultImplementations +{ + public class FileMontageImageExpressionResult : BaseImageExpressionResult + { + public List AbsoluteFilePaths { get; set; } + public bool MontageHorizontalLayout { get; set; } + public bool MontageVerticalLayout { get; set; } + public bool MontageTableLayout { get; set; } + public int Padding { get; set; } + + public FileMontageImageExpressionResult(List AbsoluteFilePaths) + { + if (AbsoluteFilePaths == null) + throw new ArgumentNullException("AbsoluteFilePaths"); + if (AbsoluteFilePaths.Count == 0) + throw new ArgumentException("AbsoluteFilePaths is empty", "AbsoluteFilePaths"); + + this.AbsoluteFilePaths = AbsoluteFilePaths; + this.MontageTableLayout = true; + this.Padding = 4; + } + + public override Stream GetImage(int Width, int Height) + { + List Images = new List(); + try + { + // Load Images + foreach (string imageFilePath in this.AbsoluteFilePaths) + Images.Add(Bitmap.FromFile(imageFilePath)); + + // Build Montage + using (Bitmap montageImage = new Bitmap(Width, Height)) + { + using (Graphics montageGraphics = Graphics.FromImage(montageImage)) + { + montageGraphics.CompositingQuality = CompositingQuality.HighQuality; + montageGraphics.InterpolationMode = InterpolationMode.HighQualityBicubic; + montageGraphics.SmoothingMode = SmoothingMode.HighQuality; + + // Draw Background + if (!LosslessFormat || !BackgroundPreferTransparent) + { + Brush backgroundBrush = Brushes.White; + if (!string.IsNullOrEmpty(this.BackgroundColour)) + backgroundBrush = new SolidBrush(ColorTranslator.FromHtml(this.BackgroundColour)); + montageGraphics.FillRectangle(backgroundBrush, montageGraphics.VisibleClipBounds); + } + + if (this.MontageHorizontalLayout) + DoHorizontalLayout(Images, montageGraphics); + else + if (this.MontageVerticalLayout) + DoVirticalLayout(Images, montageGraphics); + else + DoTableLayout(Images, montageGraphics); + } + + return this.OutputImage(montageImage); + } + } + catch (Exception) { throw; } + finally + { + // Dispose of any Images + if (Images != null) + foreach (Image i in Images) + i.Dispose(); + } + } + + private void DoHorizontalLayout(List Images, Graphics MontageGraphics) + { + + float imageScale; + float imagePosition = 0; + int imagesWidthTotal = Images.Sum(i => i.Width); + int imagesHeightMax = Images.Max(i => i.Height); + int imagesPadding = ((Images.Count - 1) * this.Padding); + + imageScale = (float)(MontageGraphics.VisibleClipBounds.Width - imagesPadding) / (float)imagesWidthTotal; + if ((MontageGraphics.VisibleClipBounds.Height / (float)imagesHeightMax) < imageScale) + imageScale = (float)MontageGraphics.VisibleClipBounds.Height / (float)imagesHeightMax; + foreach (Image image in Images) + { + MontageGraphics.DrawImageResized(image, imageScale, imagePosition, 0); + imagePosition += (imageScale * image.Width) + this.Padding; + } + } + private void DoVirticalLayout(List Images, Graphics MontageGraphics) + { + float imageScale; + float imagePosition = 0; + int imagesWidthMax = Images.Max(i => i.Width); + int imagesHeightTotal = Images.Sum(i => i.Height); + int imagesPadding = ((Images.Count - 1) * this.Padding); + + imageScale = (float)(MontageGraphics.VisibleClipBounds.Height - imagesPadding) / (float)imagesHeightTotal; + if ((MontageGraphics.VisibleClipBounds.Width / (float)imagesWidthMax) < imageScale) + imageScale = (float)MontageGraphics.VisibleClipBounds.Width / (float)imagesWidthMax; + foreach (Image image in Images) + { + MontageGraphics.DrawImageResized(image, imageScale, 0, imagePosition); + imagePosition += (imageScale * image.Height) + this.Padding; + } + } + private void DoTableLayout(List Images, Graphics MontageGraphics) + { + var stageSize = MontageGraphics.VisibleClipBounds.Size.ToSize(); + var itemAverageSize = new SizeF(Images.Average(i => (float)i.Size.Width), Images.Average(i => (float)i.Size.Height)); + + var calculatedLayout = CalculateColumnCount(stageSize, itemAverageSize, Images.Count); + + SizeF cellSize = new SizeF((MontageGraphics.VisibleClipBounds.Width - ((calculatedLayout.Item1 - 1) * this.Padding)) / calculatedLayout.Item1, + (MontageGraphics.VisibleClipBounds.Height - ((calculatedLayout.Item2 - 1) * this.Padding)) / calculatedLayout.Item2); + + int imageIndex = 0; + for (int rowIndex = 0; rowIndex < calculatedLayout.Item2; rowIndex++) + { + for (int columnIndex = 0; columnIndex < calculatedLayout.Item1; columnIndex++) + { + if (imageIndex < Images.Count) + { + var image = Images[imageIndex]; + var cellPoint = new PointF((cellSize.Width * columnIndex) + (this.Padding * columnIndex), (cellSize.Height * rowIndex) + (this.Padding * rowIndex)); + MontageGraphics.Clip = new Region(new RectangleF(cellPoint, cellSize)); + MontageGraphics.DrawImageResized(image); + imageIndex++; + } + else + break; + } + } + } + + private Tuple CalculateColumnCount(Size StageSize, SizeF ItemAverageSize, int ItemCount) + { + double? bestUsedSpace = null; + int bestColumnCount = 1; + int bestRowCount = 1; + double bestItemRatio = 1; + + for (int columnCount = 1; columnCount <= ItemCount; columnCount++) + { + int rowCount = (int)Math.Ceiling((double)ItemCount / (double)columnCount); + + int requiredWidthPadding = (columnCount - 1) * this.Padding; + int requiredHeightPadding = (rowCount - 1) * this.Padding; + Size usableStageSize = new Size(StageSize.Width - requiredWidthPadding, StageSize.Height - requiredHeightPadding); + double stageWidthRatio = (float)usableStageSize.Width / (float)usableStageSize.Height; + double stageHeightRatio = (float)usableStageSize.Height / (float)usableStageSize.Width; + + int requiredWidth = (int)Math.Ceiling(ItemAverageSize.Width * columnCount); + int requiredHeight = (int)Math.Ceiling(ItemAverageSize.Height * rowCount); + + int usedSpace = requiredWidth * requiredHeight; + int stageArea = Math.Max((requiredWidth * (int)Math.Ceiling(requiredWidth * stageHeightRatio)), + (requiredHeight * (int)Math.Ceiling(requiredHeight * stageWidthRatio))); + double usedStageSpace = (double)usedSpace / stageArea; + if (bestUsedSpace == null || bestUsedSpace < usedStageSpace) + { + bestUsedSpace = usedStageSpace; + bestColumnCount = columnCount; + bestRowCount = rowCount; + bestItemRatio = Math.Min((double)usableStageSize.Width / (double)requiredWidth, (double)usableStageSize.Height / (double)requiredHeight); + } + } + + return new Tuple(bestColumnCount, bestRowCount, bestItemRatio); + } + } +} diff --git a/Disco.BI/BI/Expressions/Extensions/UserExt.cs b/Disco.BI/BI/Expressions/Extensions/UserExt.cs new file mode 100644 index 00000000..97492b4f --- /dev/null +++ b/Disco.BI/BI/Expressions/Extensions/UserExt.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Models.Repository; +using Disco.BI.Extensions; +using Disco.BI.Interop.ActiveDirectory; + +namespace Disco.BI.Expressions.Extensions +{ + public static class UserExt + { + public static object GetActiveDirectoryObjectValue(User User, string PropertyName, int Index = 0) + { + var adUserAccount = User.ActiveDirectoryAccount(PropertyName); + if (adUserAccount != null) + return adUserAccount.GetPropertyValue(PropertyName, Index); + else + return null; + } + + public static string GetActiveDirectoryStringValue(User User, string PropertyName, int Index = 0) + { + var objectValue = GetActiveDirectoryObjectValue(User, PropertyName, Index); + string stringValue = objectValue as string; + if (stringValue == null && objectValue != null) + stringValue = objectValue.ToString(); + return stringValue; + } + + public static int GetActiveDirectoryIntegerValue(User User, string PropertyName, int Index = 0) + { + var objectValue = GetActiveDirectoryObjectValue(User, PropertyName, Index); + if (objectValue == null) + return default(int); + else + { + int intValue; + try + { + intValue = (int)Convert.ChangeType(objectValue, typeof(int)); + } + catch (Exception) + { + throw; + } + return intValue; + } + } + } +} diff --git a/Disco.BI/BI/Expressions/IExpressionPart.cs b/Disco.BI/BI/Expressions/IExpressionPart.cs new file mode 100644 index 00000000..dbe6304b --- /dev/null +++ b/Disco.BI/BI/Expressions/IExpressionPart.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections; + +namespace Disco.BI.Expressions +{ + public interface IExpressionPart + { + string RawSource { get; set; } + string Source { get; set; } + bool ErrorsAllowed { get; set; } + bool ParseError { get; } + string ParseErrorMessage { get; } + bool IsDynamic { get; set; } + object Evaluate(object ExpressionContext, System.Collections.IDictionary Variables); + } +} diff --git a/Disco.BI/BI/Expressions/TextExpressionPart.cs b/Disco.BI/BI/Expressions/TextExpressionPart.cs new file mode 100644 index 00000000..980e80a1 --- /dev/null +++ b/Disco.BI/BI/Expressions/TextExpressionPart.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections; + +namespace Disco.BI.Expressions +{ + public class TextExpressionPart : IExpressionPart + { + private string _Source; + + bool IExpressionPart.ErrorsAllowed + { + get + { + return false; + } + set + { + return; + } + } + string IExpressionPart.Source + { + get + { + return this._Source; + } + set + { + return; + } + } + string IExpressionPart.RawSource + { + get + { + return this._Source; + } + set + { + return; + } + } + bool IExpressionPart.IsDynamic + { + get + { + return false; + } + set + { + return; + } + } + public bool ParseError + { + get { return false; } + } + + public string ParseErrorMessage + { + get { return null; } + } + + public TextExpressionPart(string Source) + { + this._Source = Source; + } + object IExpressionPart.Evaluate(object ExpressionContext, System.Collections.IDictionary Variables) + { + return this._Source; + } + + } +} diff --git a/Disco.BI/BI/Extensions/AttachmentActionExtensions.cs b/Disco.BI/BI/Extensions/AttachmentActionExtensions.cs new file mode 100644 index 00000000..cb500598 --- /dev/null +++ b/Disco.BI/BI/Extensions/AttachmentActionExtensions.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Models.Repository; +using Disco.Data.Repository; + +namespace Disco.BI.Extensions +{ + public static class AttachmentActionExtensions + { + + #region Delete + public static bool CanDelete(this DeviceAttachment da) + { + return true; // Placeholder - Currently Can Always Delete; + } + public static void OnDelete(this DeviceAttachment da, DiscoDataContext dbContext) + { + if (!da.CanDelete()) + throw new InvalidOperationException("Deletion of Attachment is Denied"); + + da.RepositoryDelete(dbContext); + dbContext.DeviceAttachments.Remove(da); + } + public static bool CanDelete(this JobAttachment ja) + { + return true; // Placeholder - Currently Can Always Delete; + } + public static void OnDelete(this JobAttachment ja, DiscoDataContext dbContext) + { + if (!ja.CanDelete()) + throw new InvalidOperationException("Deletion of Attachment is Denied"); + + ja.RepositoryDelete(dbContext); + dbContext.JobAttachments.Remove(ja); + } + public static bool CanDelete(this UserAttachment ua) + { + return true; // Placeholder - Currently Can Always Delete; + } + public static void OnDelete(this UserAttachment ua, DiscoDataContext dbContext) + { + if (!ua.CanDelete()) + throw new InvalidOperationException("Deletion of Attachment is Denied"); + + ua.RepositoryDelete(dbContext); + dbContext.UserAttachments.Remove(ua); + } + #endregion + + } +} diff --git a/Disco.BI/BI/Extensions/AttachmentExtensions.cs b/Disco.BI/BI/Extensions/AttachmentExtensions.cs new file mode 100644 index 00000000..4c11ee82 --- /dev/null +++ b/Disco.BI/BI/Extensions/AttachmentExtensions.cs @@ -0,0 +1,196 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Models.Repository; +using Disco.Data.Repository; +using System.IO; +using Disco.BI.DocumentTemplateBI; + +namespace Disco.BI.Extensions +{ + public static class AttachmentExtensions + { + + public static bool ImportPdfAttachment(this DocumentUniqueIdentifier UniqueIdentifier, DiscoDataContext dbContext, System.IO.Stream PdfContent, byte[] PdfThumbnail) + { + + UniqueIdentifier.LoadComponents(dbContext); + DocumentTemplate documentTemplate = UniqueIdentifier.DocumentTemplate; + string filename; + string comments; + + if (documentTemplate == null) + { + filename = string.Format("{0}_{1:yyyyMMdd-HHmmss}.pdf", UniqueIdentifier.DataId, UniqueIdentifier.TimeStamp); + comments = string.Format("Uploaded: {0:s}", UniqueIdentifier.TimeStamp); + } + else + { + filename = string.Format("{0}_{1:yyyyMMdd-HHmmss}.pdf", UniqueIdentifier.TemplateTypeId, UniqueIdentifier.TimeStamp); + comments = string.Format("Generated: {0:s}", UniqueIdentifier.TimeStamp); + } + + User creatorUser = UserBI.UserCache.GetUser(UniqueIdentifier.CreatorId, dbContext); + if (creatorUser == null) + { + // No Creator User (or Username invalid) + creatorUser = UserBI.UserCache.CurrentUser; + } + switch (UniqueIdentifier.DataScope) + { + case DocumentTemplate.DocumentTemplateScopes.Device: + Device d = (Device)UniqueIdentifier.Data; + d.CreateAttachment(dbContext, creatorUser, filename, DocumentTemplate.PdfMimeType, comments, PdfContent, documentTemplate, PdfThumbnail); + return true; + case DocumentTemplate.DocumentTemplateScopes.Job: + Job j = (Job)UniqueIdentifier.Data; + j.CreateAttachment(dbContext, creatorUser, filename, DocumentTemplate.PdfMimeType, comments, PdfContent, documentTemplate, PdfThumbnail); + return true; + case DocumentTemplate.DocumentTemplateScopes.User: + User u = (User)UniqueIdentifier.Data; + u.CreateAttachment(dbContext, creatorUser, filename, DocumentTemplate.PdfMimeType, comments, PdfContent, documentTemplate, PdfThumbnail); + return true; + default: + return false; + } + + } + + public static string RepositoryFilename(this DeviceAttachment da, DiscoDataContext dbContext) + { + return Path.Combine(DataStore.CreateLocation(dbContext, "DeviceAttachments", da.Timestamp), string.Format("{0}_{1}_file", da.DeviceSerialNumber, da.Id)); + } + public static string RepositoryFilename(this JobAttachment ja, DiscoDataContext dbContext) + { + return Path.Combine(DataStore.CreateLocation(dbContext, "JobAttachments", ja.Timestamp), string.Format("{0}_{1}_file", ja.JobId, ja.Id)); + } + public static string RepositoryFilename(this UserAttachment ua, DiscoDataContext dbContext) + { + return Path.Combine(DataStore.CreateLocation(dbContext, "UserAttachments", ua.Timestamp), string.Format("{0}_{1}_file", ua.UserId, ua.Id)); + } + + private static string RepositoryThumbnailFilenameInternal(string DirectoryPath, string Filename) + { + return Path.Combine(DirectoryPath, Filename); + } + public static string RepositoryThumbnailFilename(this DeviceAttachment da, DiscoDataContext dbContext) + { + return RepositoryThumbnailFilenameInternal(DataStore.CreateLocation(dbContext, "DeviceAttachments", da.Timestamp), string.Format("{0}_{1}_thumb.jpg", da.DeviceSerialNumber, da.Id)); + } + public static string RepositoryThumbnailFilename(this JobAttachment ja, DiscoDataContext dbContext) + { + return RepositoryThumbnailFilenameInternal(DataStore.CreateLocation(dbContext, "JobAttachments", ja.Timestamp), string.Format("{0}_{1}_thumb.jpg", ja.JobId, ja.Id)); + } + public static string RepositoryThumbnailFilename(this UserAttachment ua, DiscoDataContext dbContext) + { + return RepositoryThumbnailFilenameInternal(DataStore.CreateLocation(dbContext, "UserAttachments", ua.Timestamp), string.Format("{0}_{1}_thumb.jpg", ua.UserId, ua.Id)); + } + + public static void RepositoryDelete(this DeviceAttachment da, DiscoDataContext dbContext) + { + RepositoryDelete(da.RepositoryFilename(dbContext), da.RepositoryThumbnailFilename(dbContext)); + } + public static void RepositoryDelete(this JobAttachment ja, DiscoDataContext dbContext) + { + RepositoryDelete(ja.RepositoryFilename(dbContext), ja.RepositoryThumbnailFilename(dbContext)); + } + public static void RepositoryDelete(this UserAttachment ua, DiscoDataContext dbContext) + { + RepositoryDelete(ua.RepositoryFilename(dbContext), ua.RepositoryThumbnailFilename(dbContext)); + } + private static void RepositoryDelete(params string[] filePaths) + { + foreach (string filePath in filePaths) + { + if (File.Exists(filePath)) + File.Delete(filePath); + } + } + + public static string SaveAttachment(this DeviceAttachment da, DiscoDataContext dbContext, Stream FileContent) + { + string filePath = da.RepositoryFilename(dbContext); + SaveAttachment(filePath, FileContent); + return filePath; + } + public static string SaveAttachment(this JobAttachment ja, DiscoDataContext dbContext, Stream FileContent) + { + string filePath = ja.RepositoryFilename(dbContext); + SaveAttachment(filePath, FileContent); + return filePath; + } + public static string SaveAttachment(this UserAttachment ua, DiscoDataContext dbContext, Stream FileContent) + { + string filePath = ua.RepositoryFilename(dbContext); + SaveAttachment(filePath, FileContent); + return filePath; + } + public static string SaveThumbnailAttachment(this DeviceAttachment da, DiscoDataContext dbContext, byte[] FileContent) + { + string filePath = da.RepositoryThumbnailFilename(dbContext); + File.WriteAllBytes(filePath, FileContent); + return filePath; + } + public static string SaveThumbnailAttachment(this JobAttachment ja, DiscoDataContext dbContext, byte[] FileContent) + { + string filePath = ja.RepositoryThumbnailFilename(dbContext); + File.WriteAllBytes(filePath, FileContent); + return filePath; + } + public static string SaveThumbnailAttachment(this UserAttachment ua, DiscoDataContext dbContext, byte[] FileContent) + { + string filePath = ua.RepositoryThumbnailFilename(dbContext); + File.WriteAllBytes(filePath, FileContent); + return filePath; + } + private static void SaveAttachment(string FilePath, Stream FileContent) + { + using (FileStream sw = new FileStream(FilePath, FileMode.Create, FileAccess.Write, FileShare.None)) + { + FileContent.CopyTo(sw); + sw.Flush(); + sw.Close(); + } + } + + public static string GenerateThumbnail(this DeviceAttachment da, DiscoDataContext dbContext) + { + string filePath = da.RepositoryThumbnailFilename(dbContext); + AttachmentBI.Utilities.GenerateThumbnail(da.RepositoryFilename(dbContext), da.MimeType, filePath); + return filePath; + } + public static string GenerateThumbnail(this JobAttachment ja, DiscoDataContext dbContext) + { + string filePath = ja.RepositoryThumbnailFilename(dbContext); + AttachmentBI.Utilities.GenerateThumbnail(ja.RepositoryFilename(dbContext), ja.MimeType, filePath); + return filePath; + } + public static string GenerateThumbnail(this UserAttachment ua, DiscoDataContext dbContext) + { + string filePath = ua.RepositoryThumbnailFilename(dbContext); + AttachmentBI.Utilities.GenerateThumbnail(ua.RepositoryFilename(dbContext), ua.MimeType, filePath); + return filePath; + } + public static string GenerateThumbnail(this DeviceAttachment da, DiscoDataContext dbContext, Stream SourceFile) + { + string filePath = da.RepositoryThumbnailFilename(dbContext); + AttachmentBI.Utilities.GenerateThumbnail(SourceFile, da.MimeType, filePath); + return filePath; + } + public static string GenerateThumbnail(this JobAttachment ja, DiscoDataContext dbContext, Stream SourceFile) + { + string filePath = ja.RepositoryThumbnailFilename(dbContext); + AttachmentBI.Utilities.GenerateThumbnail(SourceFile, ja.MimeType, filePath); + return filePath; + } + public static string GenerateThumbnail(this UserAttachment ua, DiscoDataContext dbContext, Stream SourceFile) + { + string filePath = ua.RepositoryThumbnailFilename(dbContext); + AttachmentBI.Utilities.GenerateThumbnail(SourceFile, ua.MimeType, filePath); + return filePath; + } + + + } +} diff --git a/Disco.BI/BI/Extensions/ClientServicesExtensions.cs b/Disco.BI/BI/Extensions/ClientServicesExtensions.cs new file mode 100644 index 00000000..8c66fe36 --- /dev/null +++ b/Disco.BI/BI/Extensions/ClientServicesExtensions.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Models.ClientServices; +using System.Web; +using Disco.Data.Repository; +using Disco.Models.Repository; + +namespace Disco.BI.Extensions +{ + public static class ClientServicesExtensions + { + public static EnrolResponse BuildResponse(this Enrol request) + { + if (HttpContext.Current == null) + throw new PlatformNotSupportedException("This function can only be accessed from within ASP.NET"); + + string username = null; + if (HttpContext.Current.Request.IsAuthenticated) + username = HttpContext.Current.User.Identity.Name; + + using (DiscoDataContext dbContext = new DiscoDataContext()) + { + EnrolResponse response = DeviceBI.DeviceEnrol.Enrol(dbContext, username, request); + dbContext.SaveChanges(); + return response; + } + } + + public static WhoAmIResponse BuildResponse(this WhoAmI request) + { + if (HttpContext.Current == null) + throw new PlatformNotSupportedException("This function can only be accessed from within ASP.NET"); + + string username = null; + if (HttpContext.Current.Request.IsAuthenticated) + username = HttpContext.Current.User.Identity.Name; + + if (username == null) + throw new InvalidOperationException("Unauthenticated Http Context"); + + using (DiscoDataContext dbContext = new DiscoDataContext()) + { + User user = UserBI.UserCache.GetUser(username, dbContext, true); + WhoAmIResponse response = new WhoAmIResponse() + { + Username = user.Id, + DisplayName = user.DisplayName, + Type = user.Type + }; + return response; + } + } + + public static MacEnrolResponse BuildResponse(this MacEnrol request) + { + if (HttpContext.Current == null) + throw new PlatformNotSupportedException("This function can only be accessed from within ASP.NET"); + + using (DiscoDataContext dbContext = new DiscoDataContext()) + { + MacEnrolResponse response = DeviceBI.DeviceEnrol.MacEnrol(dbContext, request, false); + dbContext.SaveChanges(); + return response; + } + } + + } +} diff --git a/Disco.BI/BI/Extensions/DeviceActionExtensions.cs b/Disco.BI/BI/Extensions/DeviceActionExtensions.cs new file mode 100644 index 00000000..3db5a5db --- /dev/null +++ b/Disco.BI/BI/Extensions/DeviceActionExtensions.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Models.Repository; +using Disco.Data.Repository; +using Disco.BI.Interop.ActiveDirectory; + +namespace Disco.BI.Extensions +{ + public static class DeviceActionExtensions + { + + public static bool CanCreateJob(this Device d) + { + return !d.DecommissionedDate.HasValue; + } + + #region Decommission + public static bool CanDecommission(this Device d) + { + if (d.DecommissionedDate.HasValue) + return false; // Already Decommissioned + + if (d.AssignedUserId != null) + return false; // User Assigned to Device + + if (d.Jobs.Count(j => !j.ClosedDate.HasValue) > 0) + return false; // Device linked to > 0 Open Jobs + + return true; + } + public static void OnDecommission(this Device d) + { + if (!d.CanDecommission()) + throw new InvalidOperationException("Decommission of Device is Denied"); + + d.DecommissionedDate = DateTime.Now; + + // Disable AD Account + if (d.ComputerName != null) + { + var adAccount = d.ActiveDirectoryAccount(); + if (adAccount != null) + { + adAccount.DisableAccount(); + } + } + } + #endregion + #region Recommission + public static bool CanRecommission(this Device d) + { + return d.DecommissionedDate.HasValue; + } + public static void OnRecommission(this Device d) + { + if (!d.CanRecommission()) + throw new InvalidOperationException("Recommission of Device is Denied"); + + d.DecommissionedDate = null; + + // Enable AD Account + if (d.ComputerName != null) + { + var adAccount = d.ActiveDirectoryAccount(); + if (adAccount != null) + { + adAccount.EnableAccount(); + } + } + } + #endregion + + #region Delete + public static bool CanDelete(this Device d) + { + return d.DecommissionedDate.HasValue; + } + public static void OnDelete(this Device d, DiscoDataContext dbContext) + { + // Delete Jobs + foreach (Job j in dbContext.Jobs.Where(i => i.DeviceSerialNumber == d.SerialNumber)) + { + if (j.UserId == null) + { // No User associated, thus must Delete whole Job + if (j.CanDelete()) + j.OnDelete(dbContext); + else + throw new InvalidOperationException(string.Format("Deletion of Device is Denied (See Job# {0})", j.Id)); + } + else + { + // User associated to Job, thus just remove Devices' association + j.DeviceSerialNumber = null; + + // Write Job Log + JobLog jobLog = new JobLog() + { + JobId = j.Id, + TechUserId = UserBI.UserCache.CurrentUser.Id, + Timestamp = DateTime.Now, + Comments = string.Format("Device Deleted{0}{0}Serial Number: {1}{0}Computer Name: {2}{0}Model: {3}{0}Profile: {4}", + Environment.NewLine, d.SerialNumber, d.ComputerName, d.DeviceModel, d.DeviceProfile) + }; + dbContext.JobLogs.Add(jobLog); + } + } + + // Disable Wireless Certificates + foreach (var wc in dbContext.DeviceCertificates.Where(i => i.DeviceSerialNumber == d.SerialNumber)) + { + wc.DeviceSerialNumber = null; + wc.Enabled = false; + } + // Delete Device Details + foreach (var dd in dbContext.DeviceDetails.Where(i => i.DeviceSerialNumber == d.SerialNumber)) + dbContext.DeviceDetails.Remove(dd); + // Delete Device Attachments + foreach (var da in dbContext.DeviceAttachments.Where(i => i.DeviceSerialNumber == d.SerialNumber)) + { + da.RepositoryDelete(dbContext); + dbContext.DeviceAttachments.Remove(da); + } + // Delete Device User Assignments + foreach (var dua in dbContext.DeviceUserAssignments.Where(i => i.DeviceSerialNumber == d.SerialNumber)) + dbContext.DeviceUserAssignments.Remove(dua); + + dbContext.Devices.Remove(d); + } + #endregion + } +} diff --git a/Disco.BI/BI/Extensions/DeviceBatchExtensions.cs b/Disco.BI/BI/Extensions/DeviceBatchExtensions.cs new file mode 100644 index 00000000..9c71b67d --- /dev/null +++ b/Disco.BI/BI/Extensions/DeviceBatchExtensions.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Models.Repository; +using Disco.Data.Repository; + +namespace Disco.BI.Extensions +{ + public static class DeviceBatchExtensions + { + public static bool CanDelete(this DeviceBatch db, DiscoDataContext dbContext) + { + // Can't Delete if Contains Devices + var deviceCount = dbContext.Devices.Count(d => d.DeviceBatchId == db.Id); + if (deviceCount > 0) + return false; + + return true; + } + + public static void Delete(this DeviceBatch db, DiscoDataContext dbContext) + { + if (!db.CanDelete(dbContext)) + throw new InvalidOperationException("The state of this Device Batch doesn't allow it to be deleted"); + + // Delete Batch + dbContext.DeviceBatches.Remove(db); + } + } +} diff --git a/Disco.BI/BI/Extensions/DeviceCertificateExtensions.cs b/Disco.BI/BI/Extensions/DeviceCertificateExtensions.cs new file mode 100644 index 00000000..0d6cac42 --- /dev/null +++ b/Disco.BI/BI/Extensions/DeviceCertificateExtensions.cs @@ -0,0 +1,44 @@ +using System.Linq; +using Disco.Data.Repository; +using Disco.Models.Repository; +using Disco.Services.Plugins; +using Disco.Services.Plugins.Features.CertificateProvider; +using System; +using System.Collections.Generic; + +namespace Disco.BI.Extensions +{ + public static class DeviceCertificateExtensions + { + + public static Tuple> AllocateCertificate(this Device device, DiscoDataContext dbContext) + { + if (!string.IsNullOrEmpty(device.DeviceProfile.CertificateProviderId)) + { + // REMOVED 2012-07-18 G# - Plugin is responsible for checking + //var deviceCertificates = dbContext.DeviceCertificates.Where(c => + // c.DeviceSerialNumber == device.SerialNumber && + // c.ProviderId == device.DeviceProfile.CertificateProviderId && + // c.Enabled == true).ToList(); + + // Load Plugin + PluginFeatureManifest featureManifest = Plugins.GetPluginFeature(device.DeviceProfile.CertificateProviderId, typeof(CertificateProviderFeature)); + + using (CertificateProviderFeature providerFeature = featureManifest.CreateInstance()) + { + // REMOVED 2012-07-18 G# - Plugin is responsible for checking + // Already Allocated Certificate + //if (deviceCertificates.Count > 0) + // return new Tuple>(deviceCertificates[0], providerPlugin.RemoveExistingCertificateNames()); + //else + + return providerFeature.AllocateCertificate(dbContext, device); + } + } + + // Device Profile does not allow certificate allocation + return null; + } + + } +} diff --git a/Disco.BI/BI/Extensions/DeviceExtensions.cs b/Disco.BI/BI/Extensions/DeviceExtensions.cs new file mode 100644 index 00000000..7b9a11b6 --- /dev/null +++ b/Disco.BI/BI/Extensions/DeviceExtensions.cs @@ -0,0 +1,186 @@ +using System.Linq; +using Disco.BI.Interop.ActiveDirectory; +using Disco.Data.Configuration; +using Disco.Data.Repository; +using Disco.Models.BI.DocumentTemplates; +using Disco.Models.Repository; +using System.Collections.Generic; +using System; +using System.IO; +using Disco.Models.Interop.ActiveDirectory; + +namespace Disco.BI.Extensions +{ + public static class DeviceExtensions + { + + public static string ComputerNameRender(this Device device, DiscoDataContext context) + { + DeviceProfile deviceProfile = device.DeviceProfile; + Expressions.Expression computerNameTemplateExpression = null; + computerNameTemplateExpression = Expressions.ExpressionCache.GetValue(DeviceProfileExtensions.ComputerNameExpressionCacheModule, deviceProfile.Id.ToString(), () => + { + // Removed 2012-06-14 G# - Properties moved to DeviceProfile model & DB Migrated in DBv3. + //return Expressions.Expression.TokenizeSingleDynamic(null, deviceProfile.Configuration(context).ComputerNameTemplate, 0); + return Expressions.Expression.TokenizeSingleDynamic(null, deviceProfile.ComputerNameTemplate, 0); + }); + System.Collections.IDictionary evaluatorVariables = Expressions.Expression.StandardVariables(null, context, UserBI.UserCache.CurrentUser, System.DateTime.Now, null); + string rendered; + try + { + rendered = computerNameTemplateExpression.EvaluateFirst(device, evaluatorVariables); + } + catch (Exception ex) + { + throw new InvalidOperationException(string.Format("An error occurred rendering the computer name: [{0}] {1}", ex.GetType().Name, ex.Message), ex.InnerException); + } + if (rendered == null || rendered.Length > 24) + { + throw new System.InvalidOperationException("The rendered computer name would be invalid or longer than 24 characters"); + } + return rendered.ToString(); + } + public static System.Collections.Generic.List AvailableDocumentTemplates(this Device d, DiscoDataContext Context, User User, System.DateTime TimeStamp) + { + List ats = Context.DocumentTemplates + .Where(at => at.Scope == Disco.Models.Repository.DocumentTemplate.DocumentTemplateScopes.Device).ToList(); + + return ats.Where(at => at.FilterExpressionMatches(d, Context, User, TimeStamp, DocumentState.DefaultState())).ToList(); + } + + public static bool UpdateLastNetworkLogonDate(this Device Device) + { + return ActiveDirectoryUpdateLastNetworkLogonDateJob.UpdateLastNetworkLogonDate(Device); + } + + public static DeviceAttachment CreateAttachment(this Device Device, DiscoDataContext dbContext, User CreatorUser, string Filename, string MimeType, string Comments, Stream Content, DocumentTemplate DocumentTemplate = null, byte[] PdfThumbnail = null) + { + if (string.IsNullOrEmpty(MimeType) || MimeType.Equals("unknown/unknown", StringComparison.InvariantCultureIgnoreCase)) + MimeType = Interop.MimeTypes.ResolveMimeType(Filename); + + DeviceAttachment da = new DeviceAttachment() + { + DeviceSerialNumber = Device.SerialNumber, + TechUserId = CreatorUser.Id, + Filename = Filename, + MimeType = MimeType, + Timestamp = DateTime.Now, + Comments = Comments + }; + + if (DocumentTemplate != null) + da.DocumentTemplateId = DocumentTemplate.Id; + + dbContext.DeviceAttachments.Add(da); + dbContext.SaveChanges(); + + da.SaveAttachment(dbContext, Content); + Content.Position = 0; + if (PdfThumbnail == null) + da.GenerateThumbnail(dbContext, Content); + else + da.SaveThumbnailAttachment(dbContext, PdfThumbnail); + + return da; + } + + public static Device AddOffline(this Device d, DiscoDataContext dbContext) + { + // Just Include: + // - Serial Number + // - Asset Number + // - Profile Id + // - Assigned User Id + // - Batch + + // Batch + DeviceBatch db = default(DeviceBatch); + if (d.DeviceBatchId.HasValue) + db = dbContext.DeviceBatches.Find(d.DeviceBatchId.Value); + + // Default Device Model + DeviceModel dm = default(DeviceModel); + if (db != null && db.DefaultDeviceModelId.HasValue) + dm = dbContext.DeviceModels.Find(db.DefaultDeviceModelId); // From Batch + else + dm = dbContext.DeviceModels.Find(1); // Default + + Device d2 = new Device() + { + SerialNumber = d.SerialNumber.ToUpper(), + AssetNumber = d.AssetNumber, + Location = d.Location, + CreatedDate = DateTime.Now, + DeviceProfileId = d.DeviceProfileId, + DeviceProfile = dbContext.DeviceProfiles.Find(d.DeviceProfileId), + AllowUnauthenticatedEnrol = true, + Active = true, + DeviceModelId = dm.Id, + DeviceModel = dm, + DeviceBatchId = d.DeviceBatchId, + DeviceBatch = db + }; + + dbContext.Devices.Add(d2); + if (!string.IsNullOrEmpty(d.AssignedUserId)) + { + User u = UserBI.UserCache.GetUser(d.AssignedUserId, dbContext); + d2.AssignDevice(dbContext, u); + } + + return d2; + } + + public static DeviceUserAssignment AssignDevice(this Device d, DiscoDataContext dbContext, User u) + { + DeviceUserAssignment newDua = default(DeviceUserAssignment); + + // Mark existing assignments as Unassigned + foreach (var dua in dbContext.DeviceUserAssignments.Where(m => m.DeviceSerialNumber == d.SerialNumber && !m.UnassignedDate.HasValue)) + dua.UnassignedDate = DateTime.Now; + + if (u != null) + { + // Add new Assignment + newDua = new DeviceUserAssignment() + { + DeviceSerialNumber = d.SerialNumber, + AssignedUserId = u.Id, + AssignedDate = DateTime.Now + }; + dbContext.DeviceUserAssignments.Add(newDua); + + d.AssignedUserId = u.Id; + d.AssignedUser = u; + } + else + { + d.AssignedUserId = null; + } + + // Update AD Account + if (!string.IsNullOrEmpty(d.ComputerName) && d.ComputerName.Length <= 24) + { + var adMachineAccount = Interop.ActiveDirectory.ActiveDirectory.GetMachineAccount(d.ComputerName); + if (adMachineAccount != null) + { + if (newDua == null) + adMachineAccount.SetDescription(string.Empty); + else + adMachineAccount.SetDescription(d); + } + } + + return newDua; + } + + public static ActiveDirectoryMachineAccount ActiveDirectoryAccount(this Device Device, params string[] AdditionalProperties) + { + if (!string.IsNullOrEmpty(Device.ComputerName)) + return Interop.ActiveDirectory.ActiveDirectory.GetMachineAccount(Device.ComputerName, AdditionalProperties: AdditionalProperties); + else + return null; + } + + } +} diff --git a/Disco.BI/BI/Extensions/DeviceModelExtensions.cs b/Disco.BI/BI/Extensions/DeviceModelExtensions.cs new file mode 100644 index 00000000..0f6c7729 --- /dev/null +++ b/Disco.BI/BI/Extensions/DeviceModelExtensions.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Models.Repository; +using System.IO; +using System.Drawing; +using Disco.Data.Repository; + +namespace Disco.BI.Extensions +{ + public static class DeviceModelExtensions + { + public static bool ImageImport(this DeviceModel deviceModel, Stream ImageStream) + { + try + { + using (Bitmap inputBitmap = new Bitmap(ImageStream)) + { + using (Image outputBitmap = inputBitmap.ResizeImage(255, 255)) + { + using (MemoryStream ms = new MemoryStream()) + { + outputBitmap.SavePng(ms); + ms.Position = 0; + + var deviceModelImagePath = deviceModel.ImageFilePath(); + + + using (var storeStream = new FileStream(deviceModelImagePath, FileMode.Create, FileAccess.Write, FileShare.None)) + { + ms.CopyTo(storeStream); + } + //deviceModel.Image = ms.ToArray(); + } + } + } + return true; + } + catch (Exception) + { + return false; + } + } + + public static FileStream Image(this DeviceModel deviceModel) + { + var deviceModelImagePath = deviceModel.ImageFilePath(); + + if (File.Exists(deviceModelImagePath)) + return new FileStream(deviceModelImagePath, FileMode.Open, FileAccess.Read, FileShare.Read); + else + return null; + } + + public static string ImageFilePath(this DeviceModel deviceModel) + { + var configCache = new Disco.Data.Configuration.ConfigurationContext(null); + + var deviceModelImagesDataStore = DataStore.CreateLocation(configCache, "DeviceModelImages"); + + return Path.Combine(deviceModelImagesDataStore, string.Format("{0}.png", deviceModel.Id)); + } + + public static string ImageHash(this DeviceModel deviceModel) + { + var deviceModelImagePath = deviceModel.ImageFilePath(); + + if (File.Exists(deviceModelImagePath)) + return File.GetLastWriteTimeUtc(deviceModelImagePath).ToBinary().ToString(); + else + return "-1"; + } + + #region Actions + // Added 2012-11-26 G# - Need ability to delete Device Models + public static bool CanDelete(this DeviceModel dm, DiscoDataContext dbContext) + { + // Can't Delete Default Model (Id: 1) + if (dm.Id == 1) + return false; + + // Can't Delete if Contains Devices + if (dbContext.Devices.Count(d => d.DeviceModelId == dm.Id) > 0) + return false; + + return true; + } + public static void Delete(this DeviceModel dm, DiscoDataContext dbContext) + { + if (!dm.CanDelete(dbContext)) + throw new InvalidOperationException("The state of this Device Model doesn't allow it to be deleted"); + + // Delete Model + dbContext.DeviceModels.Remove(dm); + } + // End Added 2012-11-26 G# + #endregion + + } +} diff --git a/Disco.BI/BI/Extensions/DeviceProfileExtensions.cs b/Disco.BI/BI/Extensions/DeviceProfileExtensions.cs new file mode 100644 index 00000000..e3019bff --- /dev/null +++ b/Disco.BI/BI/Extensions/DeviceProfileExtensions.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Models.Repository; +using Disco.Data.Repository; +using Disco.Data.Configuration.Modules; + +namespace Disco.BI.Extensions +{ + public static class DeviceProfileExtensions + { + public const string ComputerNameExpressionCacheModule = "ComputerNameTemplate"; + + public static void ComputerNameInvalidateCache(this DeviceProfile deviceProfile) + { + Expressions.ExpressionCache.InvalidateKey(ComputerNameExpressionCacheModule, deviceProfile.Id.ToString()); + } + + public static bool CanDelete(this DeviceProfile dp, DiscoDataContext dbContext) + { + // Can't Delete Default Profile (Id: 1) + if (dp.Id == 1) + return false; + + // Can't Delete if Contains Devices + if (dbContext.Devices.Count(d => d.DeviceProfileId == dp.Id) > 0) + return false; + + return true; + } + public static void Delete(this DeviceProfile dp, DiscoDataContext dbContext) + { + if (!dp.CanDelete(dbContext)) + throw new InvalidOperationException("The state of this Device Profile doesn't allow it to be deleted"); + + // Update Defaults + if (dbContext.DiscoConfiguration.DeviceProfiles.DefaultDeviceProfileId == dp.Id) + dbContext.DiscoConfiguration.DeviceProfiles.DefaultDeviceProfileId = 1; + if (dbContext.DiscoConfiguration.DeviceProfiles.DefaultAddDeviceOfflineDeviceProfileId == dp.Id) + dbContext.DiscoConfiguration.DeviceProfiles.DefaultAddDeviceOfflineDeviceProfileId = 1; + + // Delete Profile + dbContext.DeviceProfiles.Remove(dp); + } + + // Removed 2012-06-14 G# - Properties moved to DeviceProfile model & DB Migrated in DBv3. + //public static DeviceProfileConfiguration Configuration(this DeviceProfile dp, DiscoDataContext dbContext) + //{ + // return dbContext.DiscoConfiguration.DeviceProfiles.DeviceProfile(dp); + //} + + } +} diff --git a/Disco.BI/BI/Extensions/DocumentTemplateExtensions.cs b/Disco.BI/BI/Extensions/DocumentTemplateExtensions.cs new file mode 100644 index 00000000..be4c8a52 --- /dev/null +++ b/Disco.BI/BI/Extensions/DocumentTemplateExtensions.cs @@ -0,0 +1,220 @@ +using System; +using System.Collections.Concurrent; +using System.Linq; +using System.Web; +using Disco.Data.Repository; +using Disco.Models.BI.DocumentTemplates; +using Disco.Models.Repository; +using System.Collections; +using System.Collections.Generic; +using iTextSharp.text.pdf; +using Disco.BI.Expressions; +using System.Drawing; +using System.IO; +using Disco.BI.DocumentTemplateBI; + +namespace Disco.BI.Extensions +{ + public static class DocumentTemplateExtensions + { + private const string DocumentTemplateExpressionCacheTemplate = "DocumentTemplate_{0}"; + + public static string RepositoryFilename(this DocumentTemplate dt, DiscoDataContext dbContext) + { + return System.IO.Path.Combine(DataStore.CreateLocation(dbContext, "DocumentTemplates"), string.Format("{0}.pdf", dt.Id)); + } + public static string SavePdfTemplate(this DocumentTemplate dt, DiscoDataContext dbContext, Stream TemplateFile) + { + string filePath = dt.RepositoryFilename(dbContext); + using (FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write)) + { + TemplateFile.CopyTo(fs); + } + Expressions.ExpressionCache.InvalidModule(string.Format(DocumentTemplateExpressionCacheTemplate, dt.Id)); + return filePath; + } + + public static DisposableImageCollection PdfPageImages(this PdfReader pdfReader, int PageNumber) + { + return Interop.Pdf.PdfImporter.GetPageImages(pdfReader, PageNumber); + } + + public static ConcurrentDictionary PdfExpressionsFromCache(this DocumentTemplate dt, DiscoDataContext dbContext) + { + string cacheModuleKey = string.Format(DocumentTemplateExpressionCacheTemplate, dt.Id); + var module = Expressions.ExpressionCache.GetModule(cacheModuleKey); + if (module == null) + { + // Cache + string templateFilename = dt.RepositoryFilename(dbContext); + PdfReader pdfReader = new PdfReader(templateFilename); + int pdfFieldOrdinal = 0; + foreach (string pdfFieldKey in pdfReader.AcroFields.Fields.Keys) + { + var pdfFieldValue = pdfReader.AcroFields.GetField(pdfFieldKey); + Expressions.ExpressionCache.SetValue(cacheModuleKey, pdfFieldKey, Expressions.Expression.Tokenize(pdfFieldKey, pdfFieldValue, pdfFieldOrdinal)); + pdfFieldOrdinal++; + } + pdfReader.Close(); + module = Expressions.ExpressionCache.GetModule(cacheModuleKey, true); + } + return module; + } + + public static List ExtractPdfExpressions(this DocumentTemplate dt, DiscoDataContext dbContext) + { + return dt.PdfExpressionsFromCache(dbContext).Values.OrderBy(e => e.Ordinal).ToList(); + } + public static System.IO.Stream GeneratePdfBulk(this DocumentTemplate dt, DiscoDataContext dbContext, User CreatorUser, System.DateTime Timestamp, params string[] DataObjectsIds) + { + return Interop.Pdf.PdfGenerator.GenerateBulkFromTemplate(dt, dbContext, CreatorUser, Timestamp, DataObjectsIds); + } + public static System.IO.Stream GeneratePdfBulk(this DocumentTemplate dt, DiscoDataContext dbContext, User CreatorUser, System.DateTime Timestamp, params object[] DataObjects) + { + return Interop.Pdf.PdfGenerator.GenerateBulkFromTemplate(dt, dbContext, CreatorUser, Timestamp, DataObjects); + } + public static System.IO.Stream GeneratePdf(this DocumentTemplate dt, DiscoDataContext dbContext, object Data, User CreatorUser, System.DateTime TimeStamp, DocumentState State, bool FlattenFields = false) + { + return Interop.Pdf.PdfGenerator.GenerateFromTemplate(dt, dbContext, Data, CreatorUser, TimeStamp, State, FlattenFields); + } + + public static Expression FilterExpressionFromCache(this DocumentTemplate dt) + { + return ExpressionCache.GetValue("DocumentTemplateFilterExpression", dt.Id, () => { return Expression.TokenizeSingleDynamic(null, dt.FilterExpression, 0); }); + } + public static void FilterExpressionInvalidateCache(this DocumentTemplate dt) + { + ExpressionCache.InvalidateKey("DocumentTemplateFilterExpression", dt.Id); + } + public static bool FilterExpressionMatches(this DocumentTemplate dt, object Data, DiscoDataContext DataContext, User User, System.DateTime TimeStamp, DocumentState State) + { + if (!string.IsNullOrEmpty(dt.FilterExpression)) + { + Expression compiledExpression = dt.FilterExpressionFromCache(); + System.Collections.IDictionary evaluatorVariables = Expression.StandardVariables(dt, DataContext, User, TimeStamp, State); + try + { + object er = compiledExpression.EvaluateFirst(Data, evaluatorVariables); + if (er is bool) + { + return (bool)er; + } + bool erBool; + if (bool.TryParse(er.ToString(), out erBool)) + { + return erBool; + } + } + catch + { + return false; + } + } + return true; + } + public static string GetDataId(this DocumentTemplate dt, object Data) + { + if (Data is string) + { + return (string)Data; + } + else + { + switch (dt.Scope) + { + case Models.Repository.DocumentTemplate.DocumentTemplateScopes.Device: + if (!(Data is Device)) + throw new ArgumentException("This Document Template is configured for Devices only", "Data"); + Device d = (Device)Data; + return d.SerialNumber; + case Models.Repository.DocumentTemplate.DocumentTemplateScopes.Job: + if (!(Data is Job)) + throw new ArgumentException("This Document Template is configured for Jobs only", "Data"); + Job d2 = (Job)Data; + return d2.Id.ToString(); + case Models.Repository.DocumentTemplate.DocumentTemplateScopes.User: + if (!(Data is User)) + throw new ArgumentException("This Document Template is configured for Users only", "Data"); + User d3 = (User)Data; + return d3.Id; + default: + throw new InvalidOperationException("Invalid Document Template Scope"); + } + } + } + public static string UniqueIdentifier(string DocumentTemplateId, string DataId, string CreatorId, System.DateTime Timestamp) + { + return string.Format("Disco|1|{0}|{1}|{2}|{3:s}", + DocumentTemplateId, + DataId, + CreatorId, + Timestamp + ); + } + public static string UniqueIdentifier(this DocumentTemplate dt, object Data, string CreatorId, System.DateTime Timestamp) + { + return string.Format("Disco|1|{0}|{1}|{2}|{3:s}", + dt.Id, + dt.GetDataId(System.Runtime.CompilerServices.RuntimeHelpers.GetObjectValue(Data)), + CreatorId, + Timestamp + ); + } + public static string UniquePageIdentifier(this DocumentTemplate dt, object Data, string CreatorId, System.DateTime Timestamp, int Page) + { + return string.Format("Disco|1|{0}|{1}|{2}|{3:s}|{4}", + dt.Id, + dt.GetDataId(System.Runtime.CompilerServices.RuntimeHelpers.GetObjectValue(Data)), + CreatorId, + Timestamp, + Page + ); + } + public static List QRCodeLocations(this DocumentTemplate dt, DiscoDataContext dbContext) + { + return DocumentTemplateBI.DocumentTemplateQRCodeLocationCache.GetLocations(dt, dbContext); + } + public static void Delete(this DocumentTemplate dt, DiscoDataContext Context) + { + // Find & Rename all references + foreach (DeviceAttachment a in Context.DeviceAttachments.Where(a => a.DocumentTemplateId == dt.Id)) + { + a.Comments = string.Format("{0} - {1}", dt.Description, a.Comments); + if (a.Comments.Length > 500) + a.Comments = a.Comments.Substring(0, 500); + a.DocumentTemplateId = null; + a.DocumentTemplate = null; + } + foreach (JobAttachment a in Context.JobAttachments.Where(a => a.DocumentTemplateId == dt.Id)) + { + a.Comments = string.Format("{0} - {1}", dt.Description, a.Comments); + if (a.Comments.Length > 500) + a.Comments = a.Comments.Substring(0, 500); + a.DocumentTemplateId = null; + a.DocumentTemplate = null; + } + foreach (UserAttachment a in Context.UserAttachments.Where(a => a.DocumentTemplateId == dt.Id)) + { + a.Comments = string.Format("{0} - {1}", dt.Description, a.Comments); + if (a.Comments.Length > 500) + a.Comments = a.Comments.Substring(0, 500); + a.DocumentTemplateId = null; + a.DocumentTemplate = null; + } + + // Delete SubTypes + dt.JobSubTypes.Clear(); + + // Delete Template + string templateRepositoryFilename = dt.RepositoryFilename(Context); + if (System.IO.File.Exists(templateRepositoryFilename)) + System.IO.File.Delete(templateRepositoryFilename); + + // Remove from Cache + dt.FilterExpressionInvalidateCache(); + + // Delete Document Template from Repository + Context.DocumentTemplates.Remove(dt); + } + } +} diff --git a/Disco.BI/BI/Extensions/JobActionExtensions.cs b/Disco.BI/BI/Extensions/JobActionExtensions.cs new file mode 100644 index 00000000..937d351b --- /dev/null +++ b/Disco.BI/BI/Extensions/JobActionExtensions.cs @@ -0,0 +1,415 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Disco.Data.Repository; +using Disco.Models.BI.Config; +using Disco.Models.Repository; +using Disco.Services.Plugins; +using Disco.Services.Plugins.Features.WarrantyProvider; + +namespace Disco.BI.Extensions +{ + public static class JobActionExtensions + { + + #region Device Held + public static bool CanDeviceHeld(this Job j) + { + return (!j.ClosedDate.HasValue) && (j.DeviceSerialNumber != null) && + (!j.DeviceHeld.HasValue || j.DeviceReturnedDate.HasValue); + } + public static void OnDeviceHeld(this Job j, User Technician) + { + if (!j.CanDeviceHeld()) + throw new InvalidOperationException("Holding Device was Denied"); + + j.DeviceHeld = DateTime.Now; + j.DeviceHeldTechUserId = Technician.Id; + j.DeviceReadyForReturn = null; + j.DeviceReadyForReturnTechUserId = null; + j.DeviceReturnedDate = null; + j.DeviceReturnedTechUserId = null; + } + #endregion + + #region Device Ready for Return + public static bool CanDeviceReadyForReturn(this Job j) + { + return (!j.ClosedDate.HasValue) && j.DeviceHeld.HasValue && + !j.DeviceReadyForReturn.HasValue && !j.DeviceReturnedDate.HasValue; + } + public static void OnDeviceReadyForReturn(this Job j, User Technician) + { + if (!j.CanDeviceReadyForReturn()) + throw new InvalidOperationException("Device Ready for Return was Denied"); + + j.DeviceReadyForReturn = DateTime.Now; + j.DeviceReadyForReturnTechUserId = Technician.Id; + } + #endregion + + #region Device Returned + public static bool CanDeviceReturned(this Job j) + { + return (!j.ClosedDate.HasValue) && j.DeviceHeld.HasValue && + !j.DeviceReturnedDate.HasValue; + } + public static void OnDeviceReturned(this Job j, User Technician) + { + if (!j.CanDeviceReturned()) + throw new InvalidOperationException("Device Return was Denied"); + + j.DeviceReturnedDate = DateTime.Now; + j.DeviceReturnedTechUserId = Technician.Id; + } + #endregion + + #region Waiting For User Action + public static bool CanWaitingForUserAction(this Job j) + { + return !j.ClosedDate.HasValue && (j.UserId != null) && !j.WaitingForUserAction.HasValue; + } + public static void OnWaitingForUserAction(this Job j, DiscoDataContext dbContext, User Technician, string Reason) + { + if (!j.CanWaitingForUserAction()) + throw new InvalidOperationException("Waiting for User Action was Denied"); + + j.WaitingForUserAction = DateTime.Now; + + // Write Log + JobLog jobLog = new JobLog() + { + JobId = j.Id, + TechUserId = Technician.Id, + Timestamp = DateTime.Now, + Comments = string.Format("Waiting on User Action{0}Reason: {1}", Environment.NewLine, Reason) + }; + dbContext.JobLogs.Add(jobLog); + } + #endregion + + #region Not Waiting For User Action + public static bool CanNotWaitingForUserAction(this Job j) + { + return j.WaitingForUserAction.HasValue; + } + public static void OnNotWaitingForUserAction(this Job j, DiscoDataContext dbContext, User Technician, string Resolution) + { + if (!j.CanNotWaitingForUserAction()) + throw new InvalidOperationException("Not Waiting for User Action was Denied"); + + j.WaitingForUserAction = null; + + // Write Log + JobLog jobLog = new JobLog() + { + JobId = j.Id, + TechUserId = Technician.Id, + Timestamp = DateTime.Now, + Comments = string.Format("User Action Resolved{0}Resolution: {1}", Environment.NewLine, Resolution) + }; + dbContext.JobLogs.Add(jobLog); + } + #endregion + + #region Log Warranty + public static bool CanLogWarranty(this Job j) + { + return !j.ClosedDate.HasValue && + (j.DeviceSerialNumber != null) && + j.JobTypeId == JobType.JobTypeIds.HWar && + string.IsNullOrEmpty(j.JobMetaWarranty.ExternalReference); + } + public static void OnLogWarranty(this Job j, DiscoDataContext dbContext, string FaultDescription, PluginFeatureManifest WarrantyProviderDefinition, OrganisationAddress Address, User TechUser, Dictionary WarrantyProviderProperties) + { + if (!j.CanLogWarranty()) + throw new InvalidOperationException("Log Warranty was Denied"); + + if (string.IsNullOrWhiteSpace(FaultDescription)) + FaultDescription = j.GenerateFaultDescriptionFooter(dbContext, WarrantyProviderDefinition); + else + FaultDescription = string.Concat(FaultDescription, Environment.NewLine, Environment.NewLine, j.GenerateFaultDescriptionFooter(dbContext, WarrantyProviderDefinition)); + + using (WarrantyProviderFeature WarrantyProvider = WarrantyProviderDefinition.CreateInstance()) + { + string providerRef = WarrantyProvider.SubmitJob(dbContext, j, Address, TechUser, FaultDescription, WarrantyProviderProperties); + + j.JobMetaWarranty.ExternalLoggedDate = DateTime.Now; + j.JobMetaWarranty.ExternalName = WarrantyProvider.WarrantyProviderId; + + if (providerRef.Length > 100) + j.JobMetaWarranty.ExternalReference = providerRef.Substring(0, 100); + else + j.JobMetaWarranty.ExternalReference = providerRef; + + // Write Log + JobLog jobLog = new JobLog() + { + JobId = j.Id, + TechUserId = TechUser.Id, + Timestamp = DateTime.Now, + Comments = string.Format("Warranty Claim Submitted{0}{0}Provider: {1}{0}Repair Address: {2}{0}Provider Reference: {3}{0}{0}{4}", Environment.NewLine, WarrantyProvider.Manifest.Name, Address.Name, providerRef, FaultDescription) + }; + dbContext.JobLogs.Add(jobLog); + } + } + #endregion + + #region Convert HWar to HNWar + public static bool CanConvertHWarToHNWar(this Job j) + { + return !j.ClosedDate.HasValue && (j.DeviceSerialNumber != null) && + j.JobTypeId == JobType.JobTypeIds.HWar && string.IsNullOrEmpty(j.JobMetaWarranty.ExternalReference); + } + public static void OnConvertHWarToHNWar(this Job j, DiscoDataContext dbContext) + { + if (!j.CanConvertHWarToHNWar()) + throw new InvalidOperationException("Convert HWar to HNWar was Denied"); + + var techUser = UserBI.UserCache.CurrentUser; + + // Remove JobMetaWarranty + if (j.JobMetaWarranty != null) + dbContext.JobMetaWarranties.Remove(j.JobMetaWarranty); + + // Add JobMetaNonWarranty + var metaHNWar = new JobMetaNonWarranty() { Job = j }; + dbContext.JobMetaNonWarranties.Add(metaHNWar); + + // Swap Job Sub Types + List jobSubTypes = j.JobSubTypes.Select(jst => jst.Id).ToList(); + j.JobSubTypes.Clear(); + foreach (var jst in dbContext.JobSubTypes.Where(i => i.JobTypeId == JobType.JobTypeIds.HNWar && jobSubTypes.Contains(i.Id))) + j.JobSubTypes.Add(jst); + + // Add Components + var components = dbContext.DeviceComponents.Include("JobSubTypes").Where(c => !c.DeviceModelId.HasValue || c.DeviceModelId == j.Device.DeviceModelId); + var jobComponents = new List(); + foreach (var component in components) + { + if (!component.DeviceModelId.HasValue) + { + jobComponents.Add(component); + } + else + { + foreach (var st in component.JobSubTypes) + { + foreach (var jst in j.JobSubTypes) + { + if (st.JobTypeId == jst.JobTypeId && st.Id == jst.Id) + { + jobComponents.Add(component); + break; + } + } + if (jobComponents.Contains(component)) + break; + } + } + } + foreach (var component in jobComponents) + { + dbContext.JobComponents.Add(new JobComponent() + { + Job = j, + TechUserId = techUser.Id, + Cost = component.Cost, + Description = component.Description + }); + } + + // Write Log + JobLog jobLog = new JobLog() + { + JobId = j.Id, + TechUserId = techUser.Id, + Timestamp = DateTime.Now, + Comments = string.Format("Job Type Converted{0}From: {1}{0}To: {2}", Environment.NewLine, dbContext.JobTypes.Find(JobType.JobTypeIds.HWar), dbContext.JobTypes.Find(JobType.JobTypeIds.HNWar)) + }; + dbContext.JobLogs.Add(jobLog); + + j.JobTypeId = JobType.JobTypeIds.HNWar; + } + #endregion + + #region Warranty Completed + public static bool CanWarrantyCompleted(this Job j) + { + return (j.JobTypeId == JobType.JobTypeIds.HWar) && + j.JobMetaWarranty.ExternalLoggedDate.HasValue && + !j.JobMetaWarranty.ExternalCompletedDate.HasValue; + } + public static void OnWarrantyCompleted(this Job j) + { + if (!j.CanWarrantyCompleted()) + throw new InvalidOperationException("Warranty Completed was Denied"); + + j.JobMetaWarranty.ExternalCompletedDate = DateTime.Now; + } + #endregion + + #region Insurance Claim Form Sent + public static bool CanInsuranceClaimFormSent(this Job j) + { + return (j.JobTypeId == JobType.JobTypeIds.HNWar) && + j.JobMetaNonWarranty.IsInsuranceClaim && + !j.JobMetaInsurance.ClaimFormSentDate.HasValue; + } + public static void OnInsuranceClaimFormSent(this Job j) + { + if (!j.CanInsuranceClaimFormSent()) + throw new InvalidOperationException("Insurance Claim Form Sent was Denied"); + + var techUser = UserBI.UserCache.CurrentUser; + + j.JobMetaInsurance.ClaimFormSentDate = DateTime.Now; + j.JobMetaInsurance.ClaimFormSentUserId = techUser.Id; + } + #endregion + + #region Log Repair + public static bool CanLogRepair(this Job j) + { + return (j.JobTypeId == JobType.JobTypeIds.HNWar) && + (j.DeviceSerialNumber != null) && + !j.JobMetaNonWarranty.RepairerLoggedDate.HasValue && + !j.JobMetaNonWarranty.RepairerCompletedDate.HasValue; + } + public static void OnLogRepair(this Job j, string RepairerName, string RepairerReference) + { + if (!j.CanLogRepair()) + throw new InvalidOperationException("Log Repair was Denied"); + + if (j.JobMetaNonWarranty.RepairerName != RepairerName) + j.JobMetaNonWarranty.RepairerName = RepairerName; + if (j.JobMetaNonWarranty.RepairerReference != RepairerReference) + j.JobMetaNonWarranty.RepairerReference = RepairerReference; + j.JobMetaNonWarranty.RepairerLoggedDate = DateTime.Now; + } + #endregion + + #region Repair Complete + public static bool CanRepairComplete(this Job j) + { + return (j.JobTypeId == JobType.JobTypeIds.HNWar) && + j.JobMetaNonWarranty.RepairerLoggedDate.HasValue && + !j.JobMetaNonWarranty.RepairerCompletedDate.HasValue; + } + public static void OnRepairComplete(this Job j) + { + if (!j.CanRepairComplete()) + throw new InvalidOperationException("Repair Complete was Denied"); + + j.JobMetaNonWarranty.RepairerCompletedDate = DateTime.Now; + } + #endregion + + #region Close + public static bool CanClose(this Job j) + { + if (j.ClosedDate.HasValue) + return false; // Job already Closed + + if (j.DeviceHeld.HasValue && !j.DeviceReturnedDate.HasValue) + return false; // Device not returned to User + + if (j.WaitingForUserAction.HasValue) + return false; // Job waiting on User Action + + switch (j.JobTypeId) + { + case JobType.JobTypeIds.HWar: + if (!string.IsNullOrEmpty(j.JobMetaWarranty.ExternalReference) && !j.JobMetaWarranty.ExternalCompletedDate.HasValue) + return false; // Job Logged (Warranty) but not completed + break; + case JobType.JobTypeIds.HNWar: + if (j.JobMetaNonWarranty.RepairerLoggedDate.HasValue && !j.JobMetaNonWarranty.RepairerCompletedDate.HasValue) + return false; // Job Logged (Repair) but not completed + if (j.JobMetaNonWarranty.AccountingChargeRequiredDate.HasValue && (!j.JobMetaNonWarranty.AccountingChargePaidDate.HasValue || !j.JobMetaNonWarranty.AccountingChargeAddedDate.HasValue)) + return false; // Accounting Charge Required, but not added or paid + + // Removed Rule: 2012-05-31 - A Job can be closed if the decision has been made for the user not to pay... + //if (j.JobMetaNonWarranty.AccountingChargeAddedDate.HasValue && !j.JobMetaNonWarranty.AccountingChargePaidDate.HasValue) + // return false; // Accounting Charge Added, but not paid + + if (j.JobMetaNonWarranty.IsInsuranceClaim && !j.JobMetaInsurance.ClaimFormSentDate.HasValue) + return false; // Is Insurance Claim, but claim form not sent + break; + } + + return true; + } + public static void OnClose(this Job j, User Technician) + { + if (!j.CanClose()) + throw new InvalidOperationException("Close was Denied"); + + j.ClosedDate = DateTime.Now; + j.ClosedTechUserId = Technician.Id; + } + #endregion + + #region Reopen + public static bool CanReopen(this Job j) + { + return j.ClosedDate.HasValue; + } + public static void OnReopen(this Job j) + { + if (!j.CanReopen()) + throw new InvalidOperationException("Reopen was Denied"); + + j.ClosedDate = null; + j.ClosedTechUserId = null; + } + #endregion + + #region Delete + public static bool CanDelete(this Job j) + { + return j.ClosedDate.HasValue; + } + public static void OnDelete(this Job j, DiscoDataContext dbContext) + { + // Job Sub Types + j.JobSubTypes.Clear(); + + // Job Attachments + foreach (var ja in j.JobAttachments.ToArray()) + ja.OnDelete(dbContext); + j.JobAttachments.Clear(); + + // Job Components + foreach (var jc in j.JobComponents.ToArray()) + dbContext.JobComponents.Remove(jc); + j.JobComponents.Clear(); + + // Job Logs + foreach (var jl in j.JobLogs.ToArray()) + dbContext.JobLogs.Remove(jl); + j.JobLogs.Clear(); + + // Job Meta + if (j.JobMetaInsurance != null) + { + dbContext.JobMetaInsurances.Remove(j.JobMetaInsurance); + j.JobMetaInsurance = null; + } + if (j.JobMetaNonWarranty != null) + { + dbContext.JobMetaNonWarranties.Remove(j.JobMetaNonWarranty); + j.JobMetaNonWarranty = null; + } + if (j.JobMetaWarranty != null) + { + dbContext.JobMetaWarranties.Remove(j.JobMetaWarranty); + j.JobMetaWarranty = null; + } + + // Job + dbContext.Jobs.Remove(j); + } + #endregion + } +} diff --git a/Disco.BI/BI/Extensions/JobExtensions.cs b/Disco.BI/BI/Extensions/JobExtensions.cs new file mode 100644 index 00000000..5101ddc1 --- /dev/null +++ b/Disco.BI/BI/Extensions/JobExtensions.cs @@ -0,0 +1,303 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Models.Repository; +using Disco.Data.Repository; +using System.IO; +using Disco.Models.BI.DocumentTemplates; +using Disco.Services.Plugins; +using Disco.Models.BI.Job; + +namespace Disco.BI.Extensions +{ + public static class JobExtensions + { + + public static void BroadcastUpdate(this Job j) + { + if (j.UserId != null) + Interop.SignalRHandlers.UserHeldDevices.UserJobUpdated(j.UserId); + } + + public static JobAttachment CreateAttachment(this Job Job, DiscoDataContext dbContext, User CreatorUser, string Filename, string MimeType, string Comments, Stream Content, DocumentTemplate DocumentTemplate = null, byte[] PdfThumbnail = null) + { + if (string.IsNullOrEmpty(MimeType) || MimeType.Equals("unknown/unknown", StringComparison.InvariantCultureIgnoreCase)) + MimeType = Interop.MimeTypes.ResolveMimeType(Filename); + + JobAttachment ja = new JobAttachment() + { + JobId = Job.Id, + TechUserId = CreatorUser.Id, + Filename = Filename, + MimeType = MimeType, + Timestamp = DateTime.Now, + Comments = Comments + }; + + if (DocumentTemplate != null) + ja.DocumentTemplateId = DocumentTemplate.Id; + + dbContext.JobAttachments.Add(ja); + dbContext.SaveChanges(); + + ja.SaveAttachment(dbContext, Content); + Content.Position = 0; + if (PdfThumbnail == null) + ja.GenerateThumbnail(dbContext, Content); + else + ja.SaveThumbnailAttachment(dbContext, PdfThumbnail); + + return ja; + } + + public static Tuple Status(this Job j) + { + var statusId = j.CalculateStatusId(); + return new Tuple(statusId, JobBI.Utilities.JobStatusDescription(statusId, j)); + } + + public static JobTableModel.JobTableItemModelIncludeStatus ToJobTableItemModelIncludeStatus(this Job j) + { + var i = new JobTableModel.JobTableItemModelIncludeStatus() + { + Id = j.Id, + OpenedDate = j.OpenedDate, + ClosedDate = j.ClosedDate, + TypeId = j.JobTypeId, + TypeDescription = j.JobType.Description, + Location = j.DeviceHeldLocation, + + WaitingForUserAction = j.WaitingForUserAction, + DeviceReadyForReturn = j.DeviceReadyForReturn, + DeviceHeld = j.DeviceHeld, + DeviceReturnedDate = j.DeviceReturnedDate + }; + + if (j.Device != null) + { + i.DeviceSerialNumber = j.DeviceSerialNumber; + i.DeviceModelDescription = j.Device.DeviceModel.Description; + i.DeviceAddressId = j.Device.DeviceProfile.DefaultOrganisationAddress; + + if (j.JobMetaWarranty != null) + { + i.JobMetaWarranty_ExternalReference = j.JobMetaWarranty.ExternalReference; + i.JobMetaWarranty_ExternalCompletedDate = j.JobMetaWarranty.ExternalCompletedDate; + i.JobMetaWarranty_ExternalName = j.JobMetaWarranty.ExternalName; + } + if (j.JobMetaNonWarranty != null) + { + i.JobMetaNonWarranty_RepairerLoggedDate = j.JobMetaNonWarranty.RepairerLoggedDate; + i.JobMetaNonWarranty_RepairerCompletedDate = j.JobMetaNonWarranty.RepairerCompletedDate; + i.JobMetaNonWarranty_AccountingChargeAddedDate = j.JobMetaNonWarranty.AccountingChargeAddedDate; + i.JobMetaNonWarranty_AccountingChargePaidDate = j.JobMetaNonWarranty.AccountingChargePaidDate; + i.JobMetaNonWarranty_AccountingChargeRequiredDate = j.JobMetaNonWarranty.AccountingChargeRequiredDate; + i.JobMetaNonWarranty_IsInsuranceClaim = j.JobMetaNonWarranty.IsInsuranceClaim; + i.JobMetaNonWarranty_RepairerName = j.JobMetaNonWarranty.RepairerName; + if (j.JobMetaInsurance != null) + { + i.JobMetaInsurance_ClaimFormSentDate = j.JobMetaInsurance.ClaimFormSentDate; + } + } + + } + if (j.User != null) + { + i.UserId = j.UserId; + i.UserDisplayName = j.User.DisplayName; + } + if (j.OpenedTechUser != null) + { + i.OpenedTechUserId = j.OpenedTechUserId; + i.OpenedTechUserDisplayName = j.OpenedTechUser.DisplayName; + } + + return i; + } + + public static string CalculateStatusId(this Job j) + { + return j.ToJobTableItemModelIncludeStatus().CalculateStatusId(); + } + + public static string CalculateStatusId(this JobTableModel.JobTableItemModelIncludeStatus j) + { + if (j.ClosedDate.HasValue) + return Job.JobStatusIds.Closed; + + if (j.TypeId == JobType.JobTypeIds.HWar) + { + if (!string.IsNullOrEmpty(j.JobMetaWarranty_ExternalReference) && !j.JobMetaWarranty_ExternalCompletedDate.HasValue) + return Job.JobStatusIds.AwaitingWarrantyRepair; // Job Logged - but not marked as completed + } + + if (j.TypeId == JobType.JobTypeIds.HNWar) + { + if (j.JobMetaNonWarranty_RepairerLoggedDate.HasValue && !j.JobMetaNonWarranty_RepairerCompletedDate.HasValue) + return Job.JobStatusIds.AwaitingRepairs; // Repairs logged - but not complete + if (j.JobMetaNonWarranty_AccountingChargeAddedDate.HasValue && !j.JobMetaNonWarranty_AccountingChargePaidDate.HasValue) + return Job.JobStatusIds.AwaitingAccountingPayment; // Accounting Charge Added, but not paid + if (j.JobMetaNonWarranty_AccountingChargeRequiredDate.HasValue && (!j.JobMetaNonWarranty_AccountingChargePaidDate.HasValue || !j.JobMetaNonWarranty_AccountingChargeAddedDate.HasValue)) + return Job.JobStatusIds.AwaitingAccountingCharge; // Accounting Charge Required, but not added or paid + if (j.JobMetaNonWarranty_RepairerLoggedDate.HasValue && j.JobMetaNonWarranty_IsInsuranceClaim.Value && !j.JobMetaInsurance_ClaimFormSentDate.HasValue) + return Job.JobStatusIds.AwaitingInsuranceProcessing; // Is insurance claim, but no Claim Form Sent + } + + if (j.WaitingForUserAction.HasValue) + return Job.JobStatusIds.AwaitingUserAction; // Awaiting for User + + if (j.DeviceReadyForReturn.HasValue && !j.DeviceReturnedDate.HasValue) + return Job.JobStatusIds.AwaitingDeviceReturn; // Device not returned to User + + return Job.JobStatusIds.Open; + } + + public static List AvailableDocumentTemplates(this Job j, DiscoDataContext dbContext, User User, DateTime TimeStamp) + { + var dts = dbContext.DocumentTemplates.Include("JobSubTypes") + .Where(dt => dt.Scope == DocumentTemplate.DocumentTemplateScopes.Job) + .ToList(); + + foreach (var dt in dts.ToArray()) + { + if (dt.JobSubTypes.Count != 0) + { // Filter Applied + bool match = false; + foreach (var st in j.JobSubTypes) + { + if (dt.JobSubTypes.Contains(st)) + { + match = true; + break; + } + } + if (!match) + dts.Remove(dt); + } + } + + // Evaluate Filters + dts = dts.Where(dt => dt.FilterExpressionMatches(j, dbContext, User, TimeStamp, DocumentState.DefaultState())).ToList(); + + return dts; + } + + public static DateTime ValidateDateAfterOpened(this Job j, DateTime d) + { + if (d < j.OpenedDate) + { + if (d > j.OpenedDate.AddMinutes(-1)) + return j.OpenedDate; + else + throw new ArgumentException("The Date must be >= the Open Date.", "d"); + } + return d; + } + + public static string GenerateFaultDescription(this Job j, DiscoDataContext dbContext) + { + StringBuilder sb = new StringBuilder(); + + sb.AppendLine("Faulty Components:"); + foreach (var jst in j.JobSubTypes) + sb.Append("- ").AppendLine(jst.Description).AppendLine(" - "); + + return sb.ToString(); + } + + public static string GenerateFaultDescriptionFooter(this Job j, DiscoDataContext dbContext, PluginFeatureManifest WarrantyProviderDefinition) + { + var versionDisco = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version; + return string.Format("Automation by Disco v{0}.{1:0000}.{2:0000} (Provider: {3} v{4})", + versionDisco.Major, versionDisco.Minor, versionDisco.Build, WarrantyProviderDefinition.Id, WarrantyProviderDefinition.PluginManifest.Version.ToString(3)); + } + + public static void UpdateSubTypes(this Job j, DiscoDataContext dbContext, List SubTypes, bool AddComponents, User TechUser) + { + if (SubTypes == null || SubTypes.Count == 0) + throw new ArgumentException("The Job must contain at least one Sub Type"); + + List addedSubTypes = new List(); + List removedSubTypes = new List(); + + // Removed Sub Types + foreach (var t in j.JobSubTypes.ToArray()) + if (!SubTypes.Contains(t)) + { + removedSubTypes.Add(t); + j.JobSubTypes.Remove(t); + } + // Added Sub Types + foreach (var t in SubTypes) + if (!j.JobSubTypes.Contains(t)) + { + addedSubTypes.Add(t); + j.JobSubTypes.Add(t); + } + + // Write Log + if (addedSubTypes.Count > 0 || removedSubTypes.Count > 0) + { + StringBuilder logBuilder = new StringBuilder(); + logBuilder.AppendLine("Updated Job Sub Types"); + if (removedSubTypes.Count > 0) + { + logBuilder.AppendLine("Removed:"); + foreach (var t in removedSubTypes) + logBuilder.Append("- ").AppendLine(t.ToString()); + } + if (addedSubTypes.Count > 0) + { + logBuilder.AppendLine("Added:"); + foreach (var t in addedSubTypes) + logBuilder.Append("- ").AppendLine(t.ToString()); + } + dbContext.JobLogs.Add(new JobLog() + { + JobId = j.Id, + TechUserId = TechUser.Id, + Timestamp = DateTime.Now, + Comments = logBuilder.ToString() + }); + } + + // Add Components + if (AddComponents && addedSubTypes.Count > 0 && j.DeviceSerialNumber != null) + { + var components = dbContext.DeviceComponents.Include("JobSubTypes").Where(c => !c.DeviceModelId.HasValue || c.DeviceModelId == j.Device.DeviceModelId); + var addedComponents = new List(); + foreach (var c in components) + { + foreach (var st in c.JobSubTypes) + { + foreach (var jst in addedSubTypes) + { + if (st.JobTypeId == jst.JobTypeId && st.Id == jst.Id) + { + addedComponents.Add(c); + break; + } + } + if (addedComponents.Contains(c)) + break; + } + } + foreach (var c in addedComponents) + { + if (!j.JobComponents.Any(jc => jc.Description.Equals(c.Description, StringComparison.InvariantCultureIgnoreCase))) + { // Job Component with matching Description doesn't exist. + dbContext.JobComponents.Add(new JobComponent() + { + Job = j, + TechUserId = TechUser.Id, + Cost = c.Cost, + Description = c.Description + }); + } + } + } + } + + } +} diff --git a/Disco.BI/BI/Extensions/JobFlagExtensions.cs b/Disco.BI/BI/Extensions/JobFlagExtensions.cs new file mode 100644 index 00000000..ad47df74 --- /dev/null +++ b/Disco.BI/BI/Extensions/JobFlagExtensions.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Models.Repository; +using System.ComponentModel.DataAnnotations; + +namespace Disco.BI.Extensions +{ + public static class JobFlagExtensions + { + + private static Dictionary> allFlags; + private static void CacheAllFlags() + { + if (allFlags == null) + { + var fType = typeof(Job.UserManagementFlags); + var fMembers = fType.GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static); + + var flags = new Dictionary>(); + foreach (var f in fMembers) + { + DisplayAttribute display = (DisplayAttribute)(f.GetCustomAttributes(typeof(DisplayAttribute), false)[0]); + string gn = display.GroupName; + Dictionary g; + if (!flags.TryGetValue(gn, out g)) + { + g = new Dictionary(); + flags.Add(gn, g); + } + g[(long)f.GetRawConstantValue()] = display.Name; + } + allFlags = flags; + } + } + + public static Dictionary>> ValidFlagsGrouped(this Job j) + { + Dictionary>> validFlags = new Dictionary>>(); + + CacheAllFlags(); + + var currentFlags = j.Flags ?? 0; + + foreach (var jt in j.JobSubTypes) + { + Dictionary g; + if (allFlags.TryGetValue(jt.Id, out g)) + { + validFlags[jt.Id] = g.Select(f => new Tuple(f.Key, f.Value, ((currentFlags & f.Key) == f.Key))).ToList(); + } + else + { + validFlags[jt.Id] = null; + } + } + return validFlags; + } + public static Dictionary> ValidFlags(this Job j) + { + Dictionary> validFlags = new Dictionary>(); + + CacheAllFlags(); + + var currentFlags = j.Flags ?? 0; + + foreach (var jt in j.JobSubTypes) + { + Dictionary g; + if (allFlags.TryGetValue(jt.Id, out g)) + { + foreach (var f in g) + validFlags[f.Key] = new Tuple(string.Format("{0}: {1}", jt.Description, f.Value), ((currentFlags & f.Key) == f.Key)); + } + } + return validFlags; + } + } +} diff --git a/Disco.BI/BI/Extensions/JobTableExtensions.cs b/Disco.BI/BI/Extensions/JobTableExtensions.cs new file mode 100644 index 00000000..0fdccdd2 --- /dev/null +++ b/Disco.BI/BI/Extensions/JobTableExtensions.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Models.BI.Job; +using Disco.Models.Repository; +using Disco.Data.Repository; + +namespace Disco.BI.Extensions +{ + public static class JobTableExtensions + { + + public static void Fill(this JobTableModel model, DiscoDataContext dbContext, IQueryable Jobs) + { + if (model.ShowStatus) + { + + var jobItems = Jobs.Select(j => new JobTableModel.JobTableItemModelIncludeStatus() + { + Id = j.Id, + DeviceAddressId = j.Device.DeviceProfile.DefaultOrganisationAddress, + OpenedDate = j.OpenedDate, + ClosedDate = j.ClosedDate, + TypeId = j.JobTypeId, + TypeDescription = j.JobType.Description, + DeviceSerialNumber = j.Device.SerialNumber, + DeviceModelDescription = j.Device.DeviceModel.Description, + UserId = j.UserId, + UserDisplayName = j.User.DisplayName, + OpenedTechUserId = j.OpenedTechUserId, + OpenedTechUserDisplayName = j.OpenedTechUser.DisplayName, + Location = j.DeviceHeldLocation, + + JobMetaWarranty_ExternalReference = j.JobMetaWarranty.ExternalReference, + JobMetaWarranty_ExternalCompletedDate = j.JobMetaWarranty.ExternalCompletedDate, + JobMetaNonWarranty_RepairerLoggedDate = j.JobMetaNonWarranty.RepairerLoggedDate, + JobMetaNonWarranty_RepairerCompletedDate = j.JobMetaNonWarranty.RepairerCompletedDate, + JobMetaNonWarranty_AccountingChargeAddedDate = j.JobMetaNonWarranty.AccountingChargeAddedDate, + JobMetaNonWarranty_AccountingChargePaidDate = j.JobMetaNonWarranty.AccountingChargePaidDate, + JobMetaNonWarranty_AccountingChargeRequiredDate = j.JobMetaNonWarranty.AccountingChargeRequiredDate, + JobMetaNonWarranty_IsInsuranceClaim = j.JobMetaNonWarranty.IsInsuranceClaim, + JobMetaInsurance_ClaimFormSentDate = j.JobMetaInsurance.ClaimFormSentDate, + + WaitingForUserAction = j.WaitingForUserAction, + DeviceReadyForReturn = j.DeviceReadyForReturn, + DeviceHeld = j.DeviceHeld, + DeviceReturnedDate = j.DeviceReturnedDate, + JobMetaWarranty_ExternalName = j.JobMetaWarranty.ExternalName, + JobMetaNonWarranty_RepairerName = j.JobMetaNonWarranty.RepairerName + }); + + model.Items = new List(); + foreach (var j in jobItems) + { + j.StatusId = j.CalculateStatusId(); + j.StatusDescription = JobBI.Utilities.JobStatusDescription(j.StatusId, j); + + model.Items.Add(j); + } + } + else + { + model.Items = Jobs.Select(j => new JobTableModel.JobTableItemModel() + { + Id = j.Id, + DeviceAddressId = j.Device.DeviceProfile.DefaultOrganisationAddress, + OpenedDate = j.OpenedDate, + ClosedDate = j.ClosedDate, + TypeId = j.JobTypeId, + TypeDescription = j.JobType.Description, + DeviceSerialNumber = j.Device.SerialNumber, + DeviceModelDescription = j.Device.DeviceModel.Description, + UserId = j.UserId, + UserDisplayName = j.User.DisplayName, + OpenedTechUserId = j.OpenedTechUserId, + OpenedTechUserDisplayName = j.OpenedTechUser.DisplayName, + Location = j.DeviceHeldLocation + }).ToList(); + } + + if (!model.ShowDeviceAddress.HasValue) + model.ShowDeviceAddress = dbContext.DiscoConfiguration.MultiSiteMode; + + if (model.ShowDeviceAddress.Value) + { + foreach (var j in model.Items) + if (j.DeviceAddressId.HasValue) + j.DeviceAddress = dbContext.DiscoConfiguration.OrganisationAddresses.GetAddress(j.DeviceAddressId.Value).Name; + } + } + + } +} diff --git a/Disco.BI/BI/Extensions/UserExtensions.cs b/Disco.BI/BI/Extensions/UserExtensions.cs new file mode 100644 index 00000000..5e2e5254 --- /dev/null +++ b/Disco.BI/BI/Extensions/UserExtensions.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Models.Repository; +using Disco.Data.Repository; +using System.IO; +using Disco.Models.BI.DocumentTemplates; +using Disco.Models.Interop.ActiveDirectory; + +namespace Disco.BI.Extensions +{ + public static class UserExtensions + { + public static UserAttachment CreateAttachment(this User User, DiscoDataContext dbContext, User CreatorUser, string Filename, string MimeType, string Comments, Stream Content, DocumentTemplate DocumentTemplate = null, byte[] PdfThumbnail = null) + { + if (string.IsNullOrEmpty(MimeType) || MimeType.Equals("unknown/unknown", StringComparison.InvariantCultureIgnoreCase)) + MimeType = Interop.MimeTypes.ResolveMimeType(Filename); + + UserAttachment ua = new UserAttachment() + { + UserId = User.Id, + TechUserId = CreatorUser.Id, + Filename = Filename, + MimeType = MimeType, + Timestamp = DateTime.Now, + Comments = Comments + }; + + if (DocumentTemplate != null) + ua.DocumentTemplateId = DocumentTemplate.Id; + + dbContext.UserAttachments.Add(ua); + dbContext.SaveChanges(); + + ua.SaveAttachment(dbContext, Content); + Content.Position = 0; + if (PdfThumbnail == null) + ua.GenerateThumbnail(dbContext, Content); + else + ua.SaveThumbnailAttachment(dbContext, PdfThumbnail); + + return ua; + } + + public static List AvailableDocumentTemplates(this User u, DiscoDataContext dbContext, User User, DateTime TimeStamp) + { + var dts = dbContext.DocumentTemplates.Include("JobSubTypes") + .Where(dt => dt.Scope == DocumentTemplate.DocumentTemplateScopes.User) + .ToArray() + .Where(dt => dt.FilterExpressionMatches(u, dbContext, User, TimeStamp, DocumentState.DefaultState())).ToList(); + + return dts; + } + + public static List CurrentDeviceUserAssignments(this User u) + { + return u.DeviceUserAssignments.Where(dua => !dua.UnassignedDate.HasValue).ToList(); + } + public static ActiveDirectoryUserAccount ActiveDirectoryAccount(this User User, params string[] AdditionalProperties) + { + return Interop.ActiveDirectory.ActiveDirectory.GetUserAccount(User.Id, AdditionalProperties); + } + } +} diff --git a/Disco.BI/BI/Extensions/UtilityExtensions.cs b/Disco.BI/BI/Extensions/UtilityExtensions.cs new file mode 100644 index 00000000..9a901b77 --- /dev/null +++ b/Disco.BI/BI/Extensions/UtilityExtensions.cs @@ -0,0 +1,313 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Drawing; +using System.Drawing.Drawing2D; +using System.IO; +using System.Drawing.Imaging; + +namespace Disco.BI.Extensions +{ + public static class UtilityExtensions + { + + public static string StreamToString(this System.IO.Stream stream) + { + if (stream.Position != 0 && stream.CanSeek) + { + stream.Position = 0; + } + using (System.IO.StreamReader sr = new System.IO.StreamReader(stream)) + { + return sr.ReadToEnd(); + } + } + + #region Date/Time Extensions + public static string ToFuzzy(this DateTime d) + { + var n = DateTime.Now; + + // Today + if (d.Date == n.Date) + { + if (d < n) + { + // Earlier + if (d > n.AddMinutes(-1)) + return "A moment ago"; + + if (d > n.AddMinutes(-10)) + return "A few minutes ago"; + } + else + { + // Later + if (d < n.AddMinutes(1)) + return "In a moment"; + + if (d < n.AddMinutes(10)) + return "In a few minutes"; + } + + return string.Format("Today at {0:h:mm tt}", d); + } + + if (d.Date < n.Date) + { + // PAST + var dif = n.Subtract(d); + + // Yesterday + if (d.Date == n.Date.AddDays(-1)) + return string.Format("Yesterday at {0:h:mm tt}", d); + // Last Week + if (dif.TotalDays <= 7) + return string.Format("Last {0:dddd} at {0:h:mm tt}", d); + // Within 8 Weeks + if (d > n.Date.AddMonths(-2)) + return string.Format("{0} Weeks ago, {1:ddd, d MMM}", (int)(dif.TotalDays / 7), d); + // Same Year + if (d.Year == n.Year) + return string.Format("{0} Months ago, {1:ddd, d MMM}", (int)(dif.TotalDays / 30), d); + } + else + { + // Future + var dif = d.Subtract(n); + + // Tomorrow + if (d.Date == n.Date.AddDays(1)) + return string.Format("Tomorrow at {0:h:mm tt}", d); + // Next Week + if (dif.TotalDays <= 7) + return string.Format("Next {0:dddd} at {0:h:mm tt}", d); + // < 2 Month + if (d < n.Date.AddMonths(2)) + return string.Format("In {0} Weeks, {1:ddd, d MMM}", (int)(dif.TotalDays / 7), d); + // Same Year + if (d.Year == n.Year) + return string.Format("In {0} Months, {1:ddd, d MMM}", (int)(dif.TotalDays / 30), d); + } + + return d.ToString("ddd, d MMM yyyy"); + } + public static string ToFuzzy(this DateTime? d, string NullValue = "N/A") + { + if (d.HasValue) + return ToFuzzy(d.Value); + else + return NullValue; + } + public static string ToFullDateTime(this DateTime d) + { + return d.ToString("ddd, d MMM yyyy @ h:mm:sstt"); + } + public static string ToFullDateTime(this DateTime? d, string NullValue = "N/A") + { + if (d.HasValue) + return ToFullDateTime(d.Value); + else + return NullValue; + } + public static long ToSortableDateTime(this DateTime? d) + { + if (d.HasValue) + return d.Value.ToBinary(); + else + return -1; + } + public static long ToSortableDateTime(this DateTime d) + { + return d.ToBinary(); + } + #endregion + + #region Image Extensions + public static Bitmap ResizeImage(this Image Source, int Width, int Height, Brush BackgroundColor = null) + { + Bitmap destination = new Bitmap(Width, Height); + destination.SetResolution(72, 72); + using (Graphics destinationGraphics = Graphics.FromImage(destination)) + { + destinationGraphics.CompositingQuality = CompositingQuality.HighQuality; + destinationGraphics.InterpolationMode = InterpolationMode.HighQualityBicubic; + destinationGraphics.SmoothingMode = SmoothingMode.HighQuality; + + if (BackgroundColor != null) + destinationGraphics.FillRectangle(BackgroundColor, destinationGraphics.VisibleClipBounds); + + float ratio = Math.Min((float)(destination.Width) / (float)(Source.Width), (float)(destination.Height) / (float)(Source.Height)); + + destinationGraphics.DrawImageResized(Source, ratio); + } + + return destination; + } + public static void DrawImageResized(this Graphics graphics, Image SourceImage, float? Scale = null, float LocationX = -1, float LocationY = -1) + { + RectangleF clipBounds = graphics.VisibleClipBounds; + if (Scale == null) // Calculate Scale + Scale = Math.Min(clipBounds.Width / SourceImage.Width, clipBounds.Height / SourceImage.Height); + float newWidth = SourceImage.Width * Scale.Value; + float newHeight = SourceImage.Height * Scale.Value; + float newLeft = LocationX; + float newTop = LocationY; + + if (newLeft < 0 || newTop < 0) + { + if (newWidth < clipBounds.Width) + newLeft = (clipBounds.Width - newWidth) / 2; + else + newLeft = 0; + if (newHeight < clipBounds.Height) + newTop = (clipBounds.Height - newHeight) / 2; + else + newTop = 0; + } + newLeft += clipBounds.Left; + newTop += clipBounds.Top; + + graphics.DrawImage(SourceImage, new RectangleF(newLeft, newTop, newWidth, newHeight), new RectangleF(0, 0, SourceImage.Width, SourceImage.Height), GraphicsUnit.Pixel); + } + public static void EmbedIconOverlay(this Image Source, Image Icon) + { + int top = Math.Max(0, Source.Height - Icon.Height); + int left = Math.Max(0, Source.Width - Icon.Width); + + using (Graphics sourceGraphics = Graphics.FromImage(Source)) + { + sourceGraphics.DrawImage(Icon, left, top); + } + } + public static void SavePng(this Image Source, string Filename) + { + using (FileStream outStream = new FileStream(Filename, FileMode.Create, FileAccess.Write, FileShare.None)) + { + SavePng(Source, outStream); + outStream.Flush(); + outStream.Close(); + } + } + public static void SavePng(this Image Source, Stream OutStream) + { + Source.Save(OutStream, ImageFormat.Png); + } + public static Stream SavePng(this Image Source) + { + MemoryStream outStream = new MemoryStream(); + Source.SavePng(outStream); + outStream.Position = 0; + return outStream; + } + public static Stream SaveJpg(this Image Source, int Quality) + { + MemoryStream outStream = new MemoryStream(); + Source.SaveJpg(Quality, outStream); + outStream.Position = 0; + return outStream; + } + public static void SaveJpg(this Image Source, int Quality, string Filename) + { + using (FileStream outStream = new FileStream(Filename, FileMode.Create, FileAccess.Write, FileShare.None)) + { + SaveJpg(Source, Quality, outStream); + outStream.Flush(); + outStream.Close(); + } + } + public static void SaveJpg(this Image Source, int Quality, Stream OutStream) + { + ImageCodecInfo jpgCodec = ImageCodecInfo.GetImageEncoders().Where(c => c.MimeType.Equals("image/jpeg", StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault(); + if (jpgCodec != null) + { + if (Quality < 0 || Quality > 100) + throw new ArgumentOutOfRangeException("Quality", "Quality must be a positive integer <= 100"); + using (EncoderParameters ep = new EncoderParameters(1)) + { + ep.Param[0] = new EncoderParameter(Encoder.Quality, Quality); + Source.Save(OutStream, jpgCodec, ep); + } + } + else + { + // Fallback + Source.Save(OutStream, ImageFormat.Jpeg); + } + } + public static ImageMontage BuildImageMontage(this IEnumerable Images, int MaxHeight = -1, int MaxWidth = -1, bool EnforceDimensions = false) + { + if (EnforceDimensions && (MaxHeight < 0 || MaxWidth < 0)) + throw new ArgumentOutOfRangeException("EnforceDimensions", "Dimensions can only be enforced when a MaxHeight and MaxWidth is supplied"); + + Dictionary imageLocations = new Dictionary(); + double imageScale = 1.0; + int totalHeight = Images.Max(i => i.Height); + int totalWidth = Images.Sum(i => i.Width); + if (MaxHeight > 0 && totalHeight > MaxHeight) + { + imageScale = (double)MaxHeight / (double)totalHeight; + } + if (MaxWidth > 0 && totalWidth > MaxWidth) + { + imageScale = System.Math.Min(imageScale, (double)MaxWidth / (double)totalWidth); + } + int scaledHeight = EnforceDimensions ? MaxHeight : (int)System.Math.Round((double)totalHeight * imageScale); + int scaledWidth = EnforceDimensions ? MaxWidth : (int)System.Math.Round((double)totalWidth * imageScale); + System.Drawing.Bitmap imageResult = new System.Drawing.Bitmap(scaledWidth, scaledHeight); + imageResult.SetResolution(72f, 72f); + + using (Graphics g = Graphics.FromImage(imageResult)) + { + g.FillRectangle(Brushes.White, new Rectangle(Point.Empty, imageResult.Size)); + + int imageResultNextOffset = 0; + foreach (Image i in Images) + { + Rectangle imagePosition = new Rectangle(imageResultNextOffset, 0, (int)System.Math.Round((double)i.Width * imageScale), (int)System.Math.Round((double)i.Height * imageScale)); + System.Drawing.Rectangle imageSize = new System.Drawing.Rectangle(0, 0, i.Width, i.Height); + g.DrawImage(i, imagePosition, imageSize, System.Drawing.GraphicsUnit.Pixel); + imageLocations[i] = imageResultNextOffset; + imageResultNextOffset += imagePosition.Width; + } + } + + return new ImageMontage() { Montage = imageResult, MontageScale = imageScale, MontageSourceImageOffsets = imageLocations }; + } + public class ImageMontage : IDisposable + { + + public Image Montage { get; set; } + public double MontageScale { get; set; } + public Dictionary MontageSourceImageOffsets { get; set; } + + public void Dispose() + { + if (Montage != null) + { + Montage.Dispose(); + Montage = null; + } + if (MontageSourceImageOffsets != null) + { + MontageSourceImageOffsets.Clear(); + MontageSourceImageOffsets = null; + } + } + } + + public static Color InterpolateColours(this Color Start, Color End, double Progress) + { + if (Progress > 1 || Progress < 0) + throw new ArgumentOutOfRangeException("Progress", "Progress must be >= 0 && <= 1"); + + return Color.FromArgb( + (byte)(Start.A * (1 - Progress) + (End.A * Progress)), + (byte)(Start.R * (1 - Progress) + (End.R * Progress)), + (byte)(Start.G * (1 - Progress) + (End.G * Progress)), + (byte)(Start.B * (1 - Progress) + (End.B * Progress)) + ); + } + #endregion + } +} diff --git a/Disco.BI/BI/Extensions/WirelessCertificateExtensions.cs b/Disco.BI/BI/Extensions/WirelessCertificateExtensions.cs new file mode 100644 index 00000000..d6cb268b --- /dev/null +++ b/Disco.BI/BI/Extensions/WirelessCertificateExtensions.cs @@ -0,0 +1,18 @@ +using Disco.Models.Repository; +using System; +using System.Security.Cryptography.X509Certificates; +namespace Disco.BI.Extensions +{ + public static class WirelessCertificateExtensions + { + public static System.DateTime? CertificateExpirationDate(this DeviceCertificate wc) + { + if (wc.Content == null || wc.Content.Length == 0) + { + return null; + } + X509Certificate2 c = new X509Certificate2(wc.Content, "password"); + return c.NotAfter; + } + } +} diff --git a/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectory.cs b/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectory.cs new file mode 100644 index 00000000..15eb38db --- /dev/null +++ b/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectory.cs @@ -0,0 +1,397 @@ +using Disco.Models.Interop.ActiveDirectory; +using Disco.BI.DeviceBI; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.DirectoryServices; +using System.Linq; +using System.IO; + +namespace Disco.BI.Interop.ActiveDirectory +{ + public static class ActiveDirectory + { + public static ActiveDirectoryMachineAccount GetMachineAccount(string ComputerName, System.Guid? UUIDNetbootGUID = null, System.Guid? MacAddressNetbootGUID = null, params string[] AdditionalProperties) + { + if (string.IsNullOrWhiteSpace(ComputerName)) + throw new System.ArgumentException("Invalid Computer Name - Empty", "ComputerName"); + if (ComputerName.Contains("\\")) + ComputerName = ComputerName.Substring(checked(ComputerName.IndexOf("\\") + 1)); + if (ComputerName.Length > 24) + throw new System.ArgumentException("Invalid Computer Name - Length > 24", "ComputerName"); + string sAMAccountName = ComputerName; + if (!sAMAccountName.EndsWith("$")) + sAMAccountName = string.Format("{0}$", sAMAccountName); + + using (DirectoryEntry dRootEntry = ActiveDirectoryHelpers.DefaultLdapRoot) + { + var loadProperties = new List { "name", "distinguishedName", "sAMAccountName", "objectSid", "dNSHostName", "netbootGUID", "isCriticalSystemObject" }; + loadProperties.AddRange(AdditionalProperties); + using (DirectorySearcher dSearcher = new DirectorySearcher(dRootEntry, string.Format("(&(objectClass=computer)(sAMAccountName={0}))", ActiveDirectoryHelpers.EscapeLdapQuery(sAMAccountName)), loadProperties.ToArray(), SearchScope.Subtree)) + { + SearchResult dResult = dSearcher.FindOne(); + if (dResult != null) + { + return ActiveDirectory.DirectorySearchResultToMachineAccount(dResult, AdditionalProperties); + } + } + + if (UUIDNetbootGUID.HasValue) + { + using (DirectorySearcher dSearcher = new DirectorySearcher(dRootEntry, string.Format("(&(objectClass=computer)(netbootGUID={0}))", ActiveDirectoryHelpers.FormatGuidForLdapQuery(UUIDNetbootGUID.Value)), loadProperties.ToArray(), SearchScope.Subtree)) + { + SearchResult dResult = dSearcher.FindOne(); + if (dResult != null) + { + return ActiveDirectory.DirectorySearchResultToMachineAccount(dResult, AdditionalProperties); + } + } + } + if (MacAddressNetbootGUID.HasValue) + { + using (DirectorySearcher dSearcher = new DirectorySearcher(dRootEntry, string.Format("(&(objectClass=computer)(netbootGUID={0}))", ActiveDirectoryHelpers.FormatGuidForLdapQuery(MacAddressNetbootGUID.Value)), loadProperties.ToArray(), SearchScope.Subtree)) + { + SearchResult dResult = dSearcher.FindOne(); + if (dResult != null) + { + return ActiveDirectory.DirectorySearchResultToMachineAccount(dResult, AdditionalProperties); + } + } + } + + } + + return null; + } + private static ActiveDirectoryMachineAccount DirectorySearchResultToMachineAccount(SearchResult result, params string[] AdditionalProperties) + { + string name = result.Properties["name"][0].ToString(); + string sAMAccountName = result.Properties["sAMAccountName"][0].ToString(); + string distinguishedName = result.Properties["distinguishedName"][0].ToString(); + string objectSid = ActiveDirectoryHelpers.ConvertBytesToSIDString((byte[])result.Properties["objectSid"][0]); + + var dNSNameProperty = result.Properties["dNSHostName"]; + string dNSName = null; + if (dNSNameProperty.Count > 0) + dNSName = dNSNameProperty[0].ToString(); + else + dNSName = string.Format("{0}.{1}", sAMAccountName.TrimEnd('$'), ActiveDirectoryHelpers.DefaultDomainQualifiedName); + + bool isCriticalSystemObject = (bool)result.Properties["isCriticalSystemObject"][0]; + + System.Guid netbootGUIDResult = default(System.Guid); + ResultPropertyValueCollection netbootGUIDProp = result.Properties["netbootGUID"]; + if (netbootGUIDProp.Count > 0) + { + netbootGUIDResult = new System.Guid((byte[])netbootGUIDProp[0]); + } + + // Additional Properties + Dictionary additionalProperties = new Dictionary(); + foreach (string propertyName in AdditionalProperties) + { + var property = result.Properties[propertyName]; + var propertyValues = new List(); + for (int index = 0; index < property.Count; index++) + propertyValues.Add(property[index]); + additionalProperties.Add(propertyName, propertyValues.ToArray()); + } + + return new ActiveDirectoryMachineAccount + { + Name = name, + DistinguishedName = distinguishedName, + sAMAccountName = sAMAccountName, + ObjectSid = objectSid, + NetbootGUID = netbootGUIDResult, + Path = result.Path, + Domain = ActiveDirectoryHelpers.DefaultDomainNetBiosName, + DnsName = dNSName, + IsCriticalSystemObject = isCriticalSystemObject, + LoadedProperties = additionalProperties + }; + } + private static ActiveDirectoryUserAccount SearchResultToActiveDirectoryUserAccount(SearchResult result, params string[] AdditionalProperties) + { + string name = result.Properties["name"][0].ToString(); + string username = result.Properties["sAMAccountName"][0].ToString(); + string distinguishedName = result.Properties["distinguishedName"][0].ToString(); + string objectSid = ActiveDirectoryHelpers.ConvertBytesToSIDString((byte[])result.Properties["objectSid"][0]); + + ResultPropertyValueCollection displayNameProp = result.Properties["displayName"]; + string displayName = username; + if (displayNameProp.Count > 0) + displayName = displayNameProp[0].ToString(); + string surname = null; + ResultPropertyValueCollection surnameProp = result.Properties["sn"]; + if (surnameProp.Count > 0) + surname = surnameProp[0].ToString(); + string givenName = null; + ResultPropertyValueCollection givenNameProp = result.Properties["givenName"]; + if (givenNameProp.Count > 0) + givenName = givenNameProp[0].ToString(); + string email = null; + ResultPropertyValueCollection emailProp = result.Properties["mail"]; + if (emailProp.Count > 0) + email = emailProp[0].ToString(); + string phone = null; + ResultPropertyValueCollection phoneProp = result.Properties["telephoneNumber"]; + if (phoneProp.Count > 0) + phone = phoneProp[0].ToString(); + + IEnumerable groupCNs = result.Properties["memberOf"].Cast(); + List groups = ActiveDirectoryCachedGroups.GetGroups(groupCNs).Select(g => g.ToLower()).ToList(); + + //foreach (string groupCN in result.Properties["memberOf"]) + //{ + // Removed 2012-11-30 G# - Moved to Recursive Cache + //var groupCNlower = groupCN.ToLower(); + //if (groupCNlower.StartsWith("cn=")) + // groups.Add(groupCNlower.Substring(3, groupCNlower.IndexOf(",") - 3)); + // End Removed 2012-11-30 G# + //} + + string type = null; + if (groups.Contains("domain admins") || groups.Contains("disco admins")) + { + type = "Admin"; + } + else + { + if (groups.Contains("staff")) + { + type = "Staff"; + } + else + { + if (groups.Contains("students")) + { + type = "Student"; + } + } + } + + // Additional Properties + Dictionary additionalProperties = new Dictionary(); + foreach (string propertyName in AdditionalProperties) + { + var property = result.Properties[propertyName]; + var propertyValues = new List(); + for (int index = 0; index < property.Count; index++) + propertyValues.Add(property[index]); + additionalProperties.Add(propertyName, propertyValues.ToArray()); + } + + return new ActiveDirectoryUserAccount + { + Domain = ActiveDirectoryHelpers.DefaultDomainNetBiosName, + Name = name, + Surname = surname, + GivenName = givenName, + Email = email, + Phone = phone, + DistinguishedName = distinguishedName, + sAMAccountName = username, + DisplayName = displayName, + ObjectSid = objectSid, + Type = type, + Path = result.Path, + LoadedProperties = additionalProperties + }; + } + public static ActiveDirectoryUserAccount GetUserAccount(string Username, params string[] AdditionalProperties) + { + if (string.IsNullOrWhiteSpace(Username)) + throw new System.ArgumentException("Invalid User Account", "Username"); + string sAMAccountName = Username; + if (sAMAccountName.Contains("\\")) + sAMAccountName = sAMAccountName.Substring(checked(sAMAccountName.IndexOf("\\") + 1)); + + using (DirectoryEntry dRootEntry = ActiveDirectoryHelpers.DefaultLdapRoot) + { + var loadProperties = new List { + "name", + "distinguishedName", + "sAMAccountName", + "objectSid", + "displayName", + "sn", + "givenName", + "memberOf", + "mail", + "telephoneNumber" + }; + loadProperties.AddRange(AdditionalProperties); + using (DirectorySearcher dSearcher = new DirectorySearcher(dRootEntry, string.Format("(&(objectClass=user)(sAMAccountName={0}))", ActiveDirectoryHelpers.EscapeLdapQuery(sAMAccountName)), loadProperties.ToArray(), SearchScope.Subtree)) + { + SearchResult dResult = dSearcher.FindOne(); + if (dResult != null) + return ActiveDirectory.SearchResultToActiveDirectoryUserAccount(dResult, AdditionalProperties); + else + return null; + } + } + } + public static string OfflineDomainJoinProvision(ref ActiveDirectoryMachineAccount ExistingAccount, string ComputerName, string OrganisationalUnit = null, string EnrolSessionId = null) + { + if (ExistingAccount != null && ExistingAccount.IsCriticalSystemObject) + throw new InvalidOperationException(string.Format("This account {0} is a Critical System Active Directory Object and Disco refuses to modify it", ExistingAccount.DistinguishedName)); + + string DJoinResult = null; + if (string.IsNullOrWhiteSpace(ComputerName) || ComputerName.Length > 24) + throw new System.ArgumentException("Invalid Computer Name; > 0 and <= 24", "ComputerName"); + + // Added 2012-10-25 G# + // Ensure Specified OU Exists + if (!string.IsNullOrEmpty(OrganisationalUnit)) + { + var ouPath = string.Format("{0}{1},{2}", ActiveDirectoryHelpers.DefaultLdapPath, OrganisationalUnit, ActiveDirectoryHelpers.DefaultDomainQualifiedName); + try + { + using (DirectoryEntry ou = new DirectoryEntry(ouPath)) + { + if (ou == null) + { + throw new Exception("OU's Directory Entry couldn't be found"); + } + } + } + catch (Exception ex) + { + throw new ArgumentException(string.Format("An error occurred while trying to locate the specified OU: {0}", ouPath), "OrganisationalUnit", ex); + } + } + // End Added 2012-10-25 G# + + // Delete Existing + if (ExistingAccount != null) + ExistingAccount.DeleteAccount(); + + string tempFileName = System.IO.Path.GetTempFileName(); + string argumentOU = (!string.IsNullOrWhiteSpace(OrganisationalUnit)) ? string.Format(" /MACHINEOU \"{0},{1}\"", OrganisationalUnit, ActiveDirectoryHelpers.DefaultDomainQualifiedName) : string.Empty; + string arguments = string.Format("/PROVISION /DOMAIN \"{0}\" /DCNAME \"{1}\" /MACHINE \"{2}\"{3} /REUSE /SAVEFILE \"{4}\"", + ActiveDirectoryHelpers.DefaultDomainName, + ActiveDirectoryHelpers.DefaultDomainPDCName, + ComputerName, + argumentOU, + tempFileName + ); + ProcessStartInfo commandStarter = new ProcessStartInfo("DJOIN.EXE", arguments) + { + CreateNoWindow = true, + ErrorDialog = false, + LoadUserProfile = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false + }; + if (EnrolSessionId != null) + { + EnrolmentLog.LogSessionDiagnosticInformation(EnrolSessionId, string.Format("{0} {1}{2}", "DJOIN.EXE", arguments, System.Environment.NewLine)); + } + + string stdOutput; + string stdError; + using (Process commandProc = Process.Start(commandStarter)) + { + commandProc.WaitForExit(20000); + stdOutput = commandProc.StandardOutput.ReadToEnd(); + stdError = commandProc.StandardError.ReadToEnd(); + } + if (EnrolSessionId != null) + { + if (!string.IsNullOrWhiteSpace(stdOutput)) + EnrolmentLog.LogSessionDiagnosticInformation(EnrolSessionId, stdOutput + System.Environment.NewLine); + if (!string.IsNullOrWhiteSpace(stdError)) + EnrolmentLog.LogSessionDiagnosticInformation(EnrolSessionId, stdError + System.Environment.NewLine); + } + + if (System.IO.File.Exists(tempFileName)) + { + DJoinResult = System.Convert.ToBase64String(System.IO.File.ReadAllBytes(tempFileName)); + System.IO.File.Delete(tempFileName); + } + if (string.IsNullOrWhiteSpace(DJoinResult)) + throw new System.InvalidOperationException(string.Format("Domain Join Unsuccessful{0}Error: {1}{0}Output: {2}", System.Environment.NewLine, stdError, stdOutput)); + ExistingAccount = ActiveDirectory.GetMachineAccount(ComputerName); + return DJoinResult; + } + + public static List SearchUsers(string term) + { + List users = new List(); + string defaultQualifiedDomainName = ActiveDirectoryHelpers.DefaultDomainQualifiedName; + string defaultNetBiosDomainName = ActiveDirectoryHelpers.DefaultDomainNetBiosName; + term = ActiveDirectoryHelpers.EscapeLdapQuery(term); + using (DirectoryEntry entry = new DirectoryEntry(string.Format("LDAP://{0}", defaultQualifiedDomainName))) + { + using (DirectorySearcher searcher = new DirectorySearcher(entry, string.Format("(&(objectClass=User)(objectCategory=Person)(|(sAMAccountName=*{0}*)(displayName=*{0}*)))", term), new string[] + { + "name", + "distinguishedName", + "sAMAccountName", + "objectSid", + "displayName", + "sn", + "givenName", + "memberOf", + "mail", + "telephoneNumber" + }, SearchScope.Subtree)) + { + searcher.SizeLimit = 30; + SearchResultCollection results = searcher.FindAll(); + foreach (SearchResult result in results) + { + users.Add(ActiveDirectory.SearchResultToActiveDirectoryUserAccount(result)); + } + } + } + return users; + } + public static List GetOrganisationalUnitStructure() + { + ActiveDirectoryOrganisationalUnit DomainOUs = new ActiveDirectoryOrganisationalUnit + { + Children = new System.Collections.Generic.List() + }; + string defaultQualifiedDomainName = ActiveDirectoryHelpers.DefaultDomainQualifiedName; + + using (DirectoryEntry entry = new DirectoryEntry(string.Format("LDAP://{0}", defaultQualifiedDomainName))) + { + ActiveDirectory.GetOrganisationalUnitStructure_Recursive(ref DomainOUs, entry); + } + return DomainOUs.Children; + } + private static void GetOrganisationalUnitStructure_Recursive(ref ActiveDirectoryOrganisationalUnit ParentOU, DirectoryEntry Container) + { + using (DirectorySearcher searcher = new DirectorySearcher(Container, "(objectCategory=organizationalUnit)", new string[] + { + "name", + "distinguishedName" + }, SearchScope.OneLevel)) + { + using (SearchResultCollection results = searcher.FindAll()) + { + foreach (SearchResult result in results) + { + string i = result.Properties["name"][0].ToString(); + string dn = result.Properties["distinguishedName"][0].ToString(); + ActiveDirectoryOrganisationalUnit ChildOU = new ActiveDirectoryOrganisationalUnit + { + Name = i, + Path = dn.Substring(0, dn.IndexOf(",DC=")), + Children = new List() + }; + ActiveDirectory.GetOrganisationalUnitStructure_Recursive(ref ChildOU, result.GetDirectoryEntry()); + if (ChildOU.Children.Count == 0) + ChildOU.Children = null; + ParentOU.Children.Add(ChildOU); + } + } + } + + } + } +} diff --git a/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectoryCachedGroups.cs b/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectoryCachedGroups.cs new file mode 100644 index 00000000..9fa8bc46 --- /dev/null +++ b/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectoryCachedGroups.cs @@ -0,0 +1,186 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.DirectoryServices; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Disco.Data.Repository; +using Disco.Services.Tasks; +using Quartz; + +namespace Disco.BI.Interop.ActiveDirectory +{ + public class ActiveDirectoryCachedGroups : ScheduledTask + { + private static ConcurrentDictionary> _Cache = new ConcurrentDictionary>(); + private const long CacheTimeoutTicks = 6000000000; // 10 Minutes + + public static IEnumerable GetGroups(IEnumerable GroupCNs) + { + List groups = new List(); + + foreach (var groupCN in GroupCNs) + foreach (var group in GetGroupsRecursive(groupCN, new Stack())) + if (!groups.Contains(group)) + { + groups.Add(group); + yield return group.FriendlyName; + } + } + public static IEnumerable GetGroups(string GroupCN) + { + foreach (var group in GetGroupsRecursive(GroupCN, new Stack())) + yield return group.FriendlyName; + } + private static IEnumerable GetGroupsRecursive(string GroupCN, Stack RecursiveTree) + { + var group = GetGroup(GroupCN); + + if (group != null && !RecursiveTree.Contains(group)) + { + yield return group; + + if (group.MemberOf != null) + { + RecursiveTree.Push(group); + + foreach (var memberOfGroupCN in group.MemberOf) + foreach (var memberOfGroup in GetGroupsRecursive(memberOfGroupCN, RecursiveTree)) + yield return memberOfGroup; + + RecursiveTree.Pop(); + } + } + } + + private static ADCachedGroup GetGroup(string GroupCN) + { + // Check Cache + Tuple groupRecord = TryCache(GroupCN); + + if (groupRecord == null) + { + // Load from AD + var group = ADCachedGroup.LoadFromAD(GroupCN); + SetValue(GroupCN, group); + + return group; + } + else + { + // Return from Cache + return groupRecord.Item1; + } + } + + private static Tuple TryCache(string GroupCN) + { + string groupCN = GroupCN.ToLower(); + Tuple groupRecord; + if (_Cache.TryGetValue(groupCN, out groupRecord)) + { + if (groupRecord.Item2 > DateTime.Now) + return groupRecord; + else + _Cache.TryRemove(groupCN, out groupRecord); + } + return null; + } + private static bool SetValue(string GroupCN, ADCachedGroup GroupRecord) + { + string key = GroupCN.ToLower(); + Tuple groupRecord = new Tuple(GroupRecord, DateTime.Now.AddTicks(CacheTimeoutTicks)); + if (_Cache.ContainsKey(key)) + { + Tuple oldGroupRecord; + if (_Cache.TryGetValue(key, out oldGroupRecord)) + { + return _Cache.TryUpdate(key, groupRecord, oldGroupRecord); + } + } + return _Cache.TryAdd(key, groupRecord); + } + + private class ADCachedGroup + { + public string CN { get; private set; } + public string FriendlyName { get; private set; } + + public List MemberOf { get; private set; } + + public static ADCachedGroup LoadFromAD(string CN) + { + ADCachedGroup group = null; + + using (DirectoryEntry groupDE = new DirectoryEntry(string.Concat(ActiveDirectoryHelpers.DefaultLdapPath, CN))) + { + if (groupDE != null) + { + group = new ADCachedGroup() + { + CN = CN + }; + + group.FriendlyName = (string)groupDE.Properties["sAMAccountName"].Value; + + var groupMemberOf = groupDE.Properties["memberOf"]; + if (groupMemberOf != null && groupMemberOf.Count > 0) + { + group.MemberOf = groupMemberOf.Cast().ToList(); + } + } + } + + return group; + } + + private ADCachedGroup() + { + // Private Constructor + } + } + + private static void CleanStaleCache() + { + var groupKeys = _Cache.Keys.ToArray(); + foreach (string groupKey in groupKeys) + { + Tuple groupRecord; + if (_Cache.TryGetValue(groupKey, out groupRecord)) + { + if (groupRecord.Item2 <= DateTime.Now) + _Cache.TryRemove(groupKey, out groupRecord); + } + } + } + + public override string TaskName { get { return "AD Group Cache - Clean Stale Cache"; } } + + public override bool SingleInstanceTask { get { return true; } } + public override bool CancelInitiallySupported { get { return false; } } + public override bool LogExceptionsOnly { get { return true; } } + + public override void InitalizeScheduledTask(DiscoDataContext dbContext) + { + // Run @ every 15mins + + // Next 15min interval + DateTime now = DateTime.Now; + int mins = (15 - (now.Minute % 15)); + if (mins < 10) + mins += 15; + DateTimeOffset startAt = new DateTimeOffset(now).AddMinutes(mins).AddSeconds(now.Second * -1).AddMilliseconds(now.Millisecond * -1); + + TriggerBuilder triggerBuilder = TriggerBuilder.Create().StartAt(startAt). + WithSchedule(SimpleScheduleBuilder.RepeatMinutelyForever(15)); + + this.ScheduleTask(triggerBuilder); + } + + protected override void ExecuteTask() + { + CleanStaleCache(); + } + } +} diff --git a/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectoryHelpers.cs b/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectoryHelpers.cs new file mode 100644 index 00000000..293f47a4 --- /dev/null +++ b/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectoryHelpers.cs @@ -0,0 +1,169 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.DirectoryServices; +using System.DirectoryServices.ActiveDirectory; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; + +namespace Disco.BI.Interop.ActiveDirectory +{ + internal static class ActiveDirectoryHelpers + { + #region Static Cached Properties + private static string _DefaultDomainName; + private static string _DefaultDomainPDCName; + private static System.Collections.Generic.List _DefaultDomainDCNames; + private static string _DefaultDomainNetBiosName; + private static string _DefaultDomainQualifiedName; + private static string _DefaultLdapPath; + private static bool _DetermineDomainProperties_Loaded = false; + private static object _DetermineDomainProperties_Lock = new object(); + internal static string DefaultDomainName + { + get + { + ActiveDirectoryHelpers.DetermineDomainProperties(); + return ActiveDirectoryHelpers._DefaultDomainName; + } + } + internal static string DefaultDomainPDCName + { + get + { + ActiveDirectoryHelpers.DetermineDomainProperties(); + return ActiveDirectoryHelpers._DefaultDomainPDCName; + } + } + internal static System.Collections.Generic.List DefaultDomainDCNames + { + get + { + ActiveDirectoryHelpers.DetermineDomainProperties(); + return ActiveDirectoryHelpers._DefaultDomainDCNames; + } + } + internal static string DefaultDomainNetBiosName + { + get + { + ActiveDirectoryHelpers.DetermineDomainProperties(); + return ActiveDirectoryHelpers._DefaultDomainNetBiosName; + } + } + internal static string DefaultDomainQualifiedName + { + get + { + ActiveDirectoryHelpers.DetermineDomainProperties(); + return ActiveDirectoryHelpers._DefaultDomainQualifiedName; + } + } + internal static string DefaultLdapPath + { + get + { + ActiveDirectoryHelpers.DetermineDomainProperties(); + return ActiveDirectoryHelpers._DefaultLdapPath; + } + } + internal static string DefaultDCLdapPath(string DC) + { + return string.Format("LDAP://{0}/", DC); + } + internal static DirectoryEntry DefaultLdapRoot + { + get + { + return new DirectoryEntry(string.Concat(ActiveDirectoryHelpers.DefaultLdapPath, ActiveDirectoryHelpers.DefaultDomainQualifiedName)); + } + } + internal static DirectoryEntry DefaultDCLdapRoot(string DC) + { + return new DirectoryEntry(string.Concat(ActiveDirectoryHelpers.DefaultDCLdapPath(DC), ActiveDirectoryHelpers.DefaultDomainQualifiedName)); + } + + private static void DetermineDomainProperties() + { + if (!ActiveDirectoryHelpers._DetermineDomainProperties_Loaded) + { + lock (ActiveDirectoryHelpers._DetermineDomainProperties_Lock) + { + + if (!ActiveDirectoryHelpers._DetermineDomainProperties_Loaded) + { + using (Domain domain = Domain.GetDomain(new DirectoryContext(DirectoryContextType.Domain))) + { + ActiveDirectoryHelpers._DefaultDomainName = domain.Name; + ActiveDirectoryHelpers._DefaultDomainPDCName = domain.PdcRoleOwner.Name; + ActiveDirectoryHelpers._DefaultDomainDCNames = new System.Collections.Generic.List(domain.DomainControllers.Count); + foreach (DomainController dc in domain.DomainControllers) + { + ActiveDirectoryHelpers._DefaultDomainDCNames.Add(dc.Name); + } + } + ActiveDirectoryHelpers._DefaultDomainQualifiedName = string.Format("DC={0}", ActiveDirectoryHelpers._DefaultDomainName.Replace(".", ",DC=")); + ActiveDirectoryHelpers._DefaultLdapPath = string.Format("LDAP://{0}/", ActiveDirectoryHelpers._DefaultDomainPDCName); + using (DirectoryEntry entry = new DirectoryEntry(string.Format("{0}CN=Partitions,CN=Configuration,{1}", ActiveDirectoryHelpers._DefaultLdapPath, ActiveDirectoryHelpers._DefaultDomainQualifiedName))) + { + using (DirectorySearcher searcher = new DirectorySearcher(entry, "(&(objectClass=crossRef)(nETBIOSName=*))", new string[] { "nETBIOSName" })) + { + SearchResult result = searcher.FindOne(); + if (result != null) + { + ActiveDirectoryHelpers._DefaultDomainNetBiosName = result.Properties["nETBIOSName"][0].ToString(); + } + else + { + ActiveDirectoryHelpers._DefaultDomainNetBiosName = ActiveDirectoryHelpers._DefaultDomainQualifiedName; + } + } + } + } + ActiveDirectoryHelpers._DetermineDomainProperties_Loaded = true; + } + } + } + #endregion + + [System.Runtime.InteropServices.DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern bool ConvertSidToStringSid(byte[] pSID, ref System.Text.StringBuilder ptrSid); + internal static string ConvertBytesToSIDString(byte[] SID) + { + System.Text.StringBuilder sidString = new System.Text.StringBuilder(); + bool flag = ActiveDirectoryHelpers.ConvertSidToStringSid(SID, ref sidString); + string ConvertBytesToSIDString; + if (flag) + { + ConvertBytesToSIDString = sidString.ToString(); + } + else + { + ConvertBytesToSIDString = null; + } + return ConvertBytesToSIDString; + } + + internal static string EscapeLdapQuery(string query) + { + return query.Replace("*", "\\2a").Replace("(", "\\28").Replace(")", "\\29").Replace("\\", "\\5c").Replace("NUL", "\\00").Replace("/", "\\2f"); + } + internal static string FormatGuidForLdapQuery(System.Guid g) + { + checked + { + System.Text.StringBuilder sb = new System.Text.StringBuilder(); + byte[] array = g.ToByteArray(); + for (int i = 0; i < array.Length; i++) + { + byte b = array[i]; + sb.Append("\\"); + sb.Append(b.ToString("X2")); + } + return sb.ToString(); + } + } + } +} diff --git a/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectoryMachineAccountExtensions.cs b/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectoryMachineAccountExtensions.cs new file mode 100644 index 00000000..73e75938 --- /dev/null +++ b/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectoryMachineAccountExtensions.cs @@ -0,0 +1,296 @@ +using Disco.Models.Interop.ActiveDirectory; +using Disco.Models.Repository; +using System; +using System.Collections; +using System.Collections.Generic; +using System.DirectoryServices; +using System.Text; +using System.Net.NetworkInformation; +using System.Management; + +namespace Disco.BI.Interop.ActiveDirectory +{ + public static class ActiveDirectoryMachineAccountExtensions + { + public static void DeleteAccount(this ActiveDirectoryMachineAccount account) + { + if (account.IsCriticalSystemObject) + throw new InvalidOperationException(string.Format("This account {0} is a Critical System Active Directory Object and Disco refuses to modify it", account.DistinguishedName)); + + using (DirectoryEntry machineDE = new DirectoryEntry(account.Path)) + { + DeleteAccountRecursive(machineDE); + + using (var machineDEParent = machineDE.Parent) + { + machineDEParent.Children.Remove(machineDE); + } + } + } + private static void DeleteAccountRecursive(DirectoryEntry parent) + { + List children = new List(); + foreach (DirectoryEntry child in parent.Children) + children.Add(child); + + foreach (var child in children) + { + DeleteAccountRecursive(child); + parent.Children.Remove(child); + child.Dispose(); + } + } + private static void SetNetbootGUID(this ActiveDirectoryMachineAccount account, System.Guid updatedNetbootGUID) + { + if (account.IsCriticalSystemObject) + throw new InvalidOperationException(string.Format("This account {0} is a Critical System Active Directory Object and Disco refuses to modify it", account.DistinguishedName)); + + using (DirectoryEntry machineDE = new DirectoryEntry(account.Path)) + { + PropertyValueCollection netbootGUIDProp = machineDE.Properties["netbootGUID"]; + bool flag = netbootGUIDProp.Count > 0; + if (flag) + { + netbootGUIDProp.Clear(); + } + netbootGUIDProp.Add(updatedNetbootGUID.ToByteArray()); + machineDE.CommitChanges(); + } + } + public static void SetDescription(this ActiveDirectoryMachineAccount account, string Description) + { + using (DirectoryEntry machineDE = new DirectoryEntry(account.Path)) + { + PropertyValueCollection descriptionProp = machineDE.Properties["description"]; + bool flag = descriptionProp.Count > 0; + if (flag) + { + descriptionProp.Clear(); + } + flag = !string.IsNullOrEmpty(Description); + if (flag) + { + descriptionProp.Add(Description); + } + machineDE.CommitChanges(); + } + } + public static void SetDescription(this ActiveDirectoryMachineAccount account, Device Device) + { + System.Text.StringBuilder descriptionBuilder = new System.Text.StringBuilder(); + bool flag = Device.AssignedUserId != null; + if (flag) + { + descriptionBuilder.Append(Device.AssignedUser.Id); + descriptionBuilder.Append(" ("); + descriptionBuilder.Append(Device.AssignedUser.DisplayName); + descriptionBuilder.Append("); "); + } + flag = Device.DeviceModelId.HasValue; + if (flag) + { + descriptionBuilder.Append(Device.DeviceModel.Description); + descriptionBuilder.Append("; "); + } + descriptionBuilder.Append(Device.DeviceProfile.Description); + descriptionBuilder.Append("; "); + string description = descriptionBuilder.ToString().Trim(); + flag = (description.Length > 1024); + if (flag) + { + description = description.Substring(0, 1024); + } + account.SetDescription(description); + } + + public static void DisableAccount(this ActiveDirectoryMachineAccount account) + { + if (account.IsCriticalSystemObject) + throw new InvalidOperationException(string.Format("This account {0} is a Critical System Active Directory Object and Disco refuses to modify it", account.DistinguishedName)); + + using (DirectoryEntry machineDE = new DirectoryEntry(account.Path)) + { + int accountControl = (int)machineDE.Properties["userAccountControl"][0]; + int updatedAccountControl = (accountControl | 2); + if (accountControl != updatedAccountControl) + { + machineDE.Properties["userAccountControl"][0] = updatedAccountControl; + machineDE.CommitChanges(); + } + } + } + public static void EnableAccount(this ActiveDirectoryMachineAccount account) + { + if (account.IsCriticalSystemObject) + throw new InvalidOperationException(string.Format("This account {0} is a Critical System Active Directory Object and Disco refuses to modify it", account.DistinguishedName)); + + using (DirectoryEntry machineDE = new DirectoryEntry(account.Path)) + { + int accountControl = (int)machineDE.Properties["userAccountControl"][0]; + if ((accountControl & 2) == 2) + { + int updatedAccountControl = (accountControl ^ 2); + machineDE.Properties["userAccountControl"][0] = updatedAccountControl; + machineDE.CommitChanges(); + } + } + } + + public static bool UpdateNetbootGUID(this ActiveDirectoryMachineAccount account, string UUID, string MACAddress) + { + if (account.IsCriticalSystemObject) + throw new InvalidOperationException(string.Format("This account {0} is a Critical System Active Directory Object and Disco refuses to modify it", account.DistinguishedName)); + + System.Guid netbootGUID = System.Guid.Empty; + bool flag = !string.IsNullOrWhiteSpace(UUID); + if (flag) + { + netbootGUID = ActiveDirectoryMachineAccountExtensions.NetbootGUIDFromUUID(UUID); + } + else + { + flag = !string.IsNullOrWhiteSpace(MACAddress); + if (flag) + { + netbootGUID = ActiveDirectoryMachineAccountExtensions.NetbootGUIDFromMACAddress(MACAddress); + } + } + flag = (netbootGUID != System.Guid.Empty && netbootGUID != account.NetbootGUID); + bool UpdateNetbootGUID; + if (flag) + { + account.SetNetbootGUID(netbootGUID); + UpdateNetbootGUID = true; + } + else + { + UpdateNetbootGUID = false; + } + return UpdateNetbootGUID; + } + internal static System.Guid NetbootGUIDFromMACAddress(string MACAddress) + { + string strippedMACAddress = MACAddress.Trim().Replace(":", string.Empty).Replace("-", string.Empty); + bool flag = strippedMACAddress.Length == 12; + System.Guid NetbootGUIDFromMACAddress; + if (flag) + { + System.Guid guid = new System.Guid(string.Format("00000000-0000-0000-0000-{0}", strippedMACAddress)); + NetbootGUIDFromMACAddress = guid; + } + else + { + NetbootGUIDFromMACAddress = System.Guid.Empty; + } + return NetbootGUIDFromMACAddress; + } + internal static System.Guid NetbootGUIDFromUUID(string UUID) + { + System.Guid result = new System.Guid(UUID); + return result; + } + + public static object GetPropertyValue(this ActiveDirectoryMachineAccount account, string PropertyName, int Index = 0) + { + switch (PropertyName.ToLower()) + { + case "name": + return account.Name; + case "samaccountname": + return account.sAMAccountName; + case "distinguishedname": + return account.DistinguishedName; + case "objectsid": + return account.ObjectSid; + case "netbootguid": + return account.NetbootGUID; + default: + object[] adProperty; + if (account.LoadedProperties.TryGetValue(PropertyName, out adProperty) && Index <= adProperty.Length) + return adProperty[Index]; + else + return null; + } + } + + public static IPStatus PingComputer(this ActiveDirectoryMachineAccount account, int Timeout = 2000) + { + using (var p = new Ping()) + { + PingReply reply = p.Send(account.DnsName, Timeout); + return reply.Status; + } + } + + // Didn't Work - WMI Limitation? + // G# - 2012-06-18 + //public static void OnlineRenameComputer(this ActiveDirectoryMachineAccount account, string NewComputerName) + //{ + // if (account.IsCriticalSystemObject) + // throw new InvalidOperationException(string.Format("This account {0} is a Critical System Active Directory Object and Disco refuses to modify it", account.DistinguishedName)); + + // try + // { + // IPStatus pingResult = account.PingComputer(); + // if (pingResult != IPStatus.Success) + // throw new Exception(string.Format("Ping Error Result: {0}", pingResult.ToString())); + // } + // catch (Exception ex) + // { + // throw new Exception(string.Format("Error trying to Ping the Device: {0}; {1}", account.DnsName, ex.Message), ex); + // } + + // ConnectionOptions wmiConnectionOptions = new ConnectionOptions() + // { + // Authentication = AuthenticationLevel.PacketPrivacy, + // Impersonation = ImpersonationLevel.Impersonate, + // EnablePrivileges = true, + // Timeout = new TimeSpan(0, 0, 6) + // }; + // ManagementPath wmiPath = new ManagementPath() + // { + // Server = account.DnsName, + // NamespacePath = @"root\cimv2", + // ClassName = "Win32_ComputerSystem" + // }; + + // ManagementScope wmiScope = new ManagementScope(wmiPath, wmiConnectionOptions); + + // ObjectGetOptions wmiGetOptions = new ObjectGetOptions() { Timeout = new TimeSpan(0, 1, 0) }; + + // using (ManagementClass wmiClass = new ManagementClass(wmiScope, wmiPath, wmiGetOptions)) + // { + // foreach (ManagementObject wmiWin32ComputerSystem in wmiClass.GetInstances()) + // { + // UInt32 result = (UInt32)wmiWin32ComputerSystem.InvokeMethod("Rename", new object[] { NewComputerName }); + // if (result != 0) + // throw new Exception(string.Format("Error Renaming Computer; WMI Remote Method 'Rename' returned: {0}", result)); + // } + // } + //} + + public static void MoveOrganisationUnit(this ActiveDirectoryMachineAccount account, string NewOrganisationUnit) + { + if (account.IsCriticalSystemObject) + throw new InvalidOperationException(string.Format("This account {0} is a Critical System Active Directory Object and Disco refuses to modify it", account.DistinguishedName)); + + if (!account.ParentDistinguishedName.Equals(NewOrganisationUnit, StringComparison.InvariantCultureIgnoreCase)) + { + string ouPath; + if (string.IsNullOrWhiteSpace(NewOrganisationUnit)) + ouPath = string.Format("{0}CN=Computers,{1}", ActiveDirectoryHelpers.DefaultLdapPath, ActiveDirectoryHelpers.DefaultDomainQualifiedName); + else + ouPath = string.Format("{0}{1},{2}", ActiveDirectoryHelpers.DefaultLdapPath, NewOrganisationUnit, ActiveDirectoryHelpers.DefaultDomainQualifiedName); + + using (DirectoryEntry ou = new DirectoryEntry(ouPath)) + { + using (DirectoryEntry i = new DirectoryEntry(account.Path) { UsePropertyCache = false }) + { + i.MoveTo(ou); + } + } + } + } + + } +} diff --git a/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectoryOrganisationalUnit.cs b/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectoryOrganisationalUnit.cs new file mode 100644 index 00000000..289d4d8a --- /dev/null +++ b/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectoryOrganisationalUnit.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Disco.BI.Interop.ActiveDirectory +{ + public class ActiveDirectoryOrganisationalUnit + { + public string Name { get; set; } + public string Path { get; set; } + public List Children { get; set; } + } +} diff --git a/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectoryUpdateLastNetworkLogonDateJob.cs b/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectoryUpdateLastNetworkLogonDateJob.cs new file mode 100644 index 00000000..c1eddb4d --- /dev/null +++ b/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectoryUpdateLastNetworkLogonDateJob.cs @@ -0,0 +1,293 @@ +using Disco.Data.Repository; +using Disco.Services.Logging; +using Disco.Models.Repository; +using Quartz; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.DirectoryServices; +using System.Linq; +using System.Linq.Expressions; +using System.Net.NetworkInformation; +using System.Reflection; +using Disco.Services.Tasks; +namespace Disco.BI.Interop.ActiveDirectory +{ + public class ActiveDirectoryUpdateLastNetworkLogonDateJob : ScheduledTask + { + + public override string TaskName { get { return "Active Directory - Update Last Network Logon Dates Task"; } } + public override bool SingleInstanceTask { get { return true; } } + public override bool CancelInitiallySupported { get { return false; } } + + public override void InitalizeScheduledTask(DiscoDataContext dbContext) + { + // ActiveDirectoryUpdateLastNetworkLogonDateJob @ 11:30pm + TriggerBuilder triggerBuilder = TriggerBuilder.Create(). + WithSchedule(CronScheduleBuilder.DailyAtHourAndMinute(23, 30)); + + this.ScheduleTask(triggerBuilder); + } + + protected override void ExecuteTask() + { + int changeCount; + + this.Status.UpdateStatus(1, "Starting", "Connecting to the Database and initializing the environment"); + using (DiscoDataContext dbContext = new DiscoDataContext()) + { + UpdateLastNetworkLogonDates(dbContext, this.Status); + this.Status.UpdateStatus(95, "Updating Database", "Writing last network logon dates to the Database"); + changeCount = dbContext.SaveChanges(); + this.Status.UpdateStatus(100, "Finished", string.Format("{0} Device last network logon dates updated", changeCount)); + } + + SystemLog.LogInformation(new string[] + { + "Updated LastNetworkLogon Device Property for Device/s", + changeCount.ToString() + }); + } + + //public void InitalizeScheduledTask(DiscoDataContext dbContext, IScheduler Scheduler) + //{ + // // UpdateLastNetworkLogonDates @ 11:30pm + // IJobDetail jobDetail = new JobDetailImpl("UpdateLastNetworkLogonDates", typeof(ActiveDirectoryUpdateLastNetworkLogonDateJob)); + // ITrigger trigger = TriggerBuilder.Create(). + // WithIdentity("UpdateLastNetworkLogonDatesTrigger"). + // StartNow(). + // WithSchedule(CronScheduleBuilder.DailyAtHourAndMinute(23, 30)). + // Build(); + // Scheduler.ScheduleJob(jobDetail, trigger); + //} + + //void IJob.Execute(IJobExecutionContext context) + //{ + // DiscoDataContext dbContext = new DiscoDataContext(); + // try + // { + // ActiveDirectoryUpdateLastNetworkLogonDateJob.UpdateLastNetworkLogonDates(dbContext); + // int changeCount = dbContext.SaveChanges(); + // SystemLog.LogInformation(new string[] + // { + // "Updated LastNetworkLogon Device Property for Device/s", + // changeCount.ToString() + // }); + // } + // catch (System.Exception ex) + // { + // SystemLog.LogException("ActiveDirectoryUpdateLastNetworkLogonDateJob", ex); + // } + // finally + // { + // bool flag = dbContext != null; + // if (flag) + // { + // ((System.IDisposable)dbContext).Dispose(); + // } + // } + //} + public static bool UpdateLastNetworkLogonDate(Device Device) + { + System.DateTime? computerLastLogonDate = Device.LastNetworkLogonDate; + if (!string.IsNullOrEmpty(Device.ComputerName)) + { + foreach (var dcName in ActiveDirectoryHelpers.DefaultDomainDCNames) + { + try + { + Ping p = new Ping(); + PingReply pr; + try + { + pr = p.Send(dcName, 500); + } + finally + { + if (p != null) + { + ((System.IDisposable)p).Dispose(); + } + } + if (pr.Status == IPStatus.Success) + { + using (DirectoryEntry dRootEntry = ActiveDirectoryHelpers.DefaultDCLdapRoot(dcName)) + { + DirectorySearcher dSearcher = new DirectorySearcher(dRootEntry, string.Format("(&(objectClass=computer)(sAMAccountName={0}$))", ActiveDirectoryHelpers.EscapeLdapQuery(Device.ComputerName)), new string[] + { + "lastLogon" + }, SearchScope.Subtree); + SearchResult dResult = dSearcher.FindOne(); + if (dResult != null) + { + ResultPropertyValueCollection dProp = dResult.Properties["lastLogon"]; + if (dProp != null && dProp.Count > 0) + { + long lastLogonInt = (long)dProp[0]; + if (lastLogonInt > 0L) + { + System.DateTime computerNameDate = System.DateTime.FromFileTime(lastLogonInt); + if (computerLastLogonDate.HasValue) + { + if (System.DateTime.Compare(computerLastLogonDate.Value, computerNameDate) < 0) + { + computerLastLogonDate = computerNameDate; + } + } + else + { + computerLastLogonDate = computerNameDate; + } + } + } + } + + } + } + else + { + SystemLog.LogError(new string[] + { + string.Format("Unable to ping Domain Controller: '{0}' (ref: Disco.BI.Interop.ActiveDirectory.ActiveDirectoryUpdateLastNetworkLogonDateJob.UpdateDeviceLastNetworkLogonDate)", dcName) + }); + } + } + catch (System.Exception ex) + { + SystemLog.LogException("UpdateDeviceLastNetworkLogonDate", ex); + } + } + } + bool UpdateLastNetworkLogonDate; + if (computerLastLogonDate.HasValue) + { + if (!Device.LastNetworkLogonDate.HasValue) + { + Device.LastNetworkLogonDate = computerLastLogonDate; + UpdateLastNetworkLogonDate = true; + return UpdateLastNetworkLogonDate; + } + if (System.DateTime.Compare(computerLastLogonDate.Value, Device.LastNetworkLogonDate.Value) > 0) + { + Device.LastNetworkLogonDate = computerLastLogonDate; + UpdateLastNetworkLogonDate = true; + return UpdateLastNetworkLogonDate; + } + } + UpdateLastNetworkLogonDate = false; + return UpdateLastNetworkLogonDate; + } + public static void UpdateLastNetworkLogonDates(DiscoDataContext context, ScheduledTaskStatus status = null) + { + System.Collections.Generic.Dictionary computerLastLogonDates = new System.Collections.Generic.Dictionary(); + + int progressDCCountTotal = ActiveDirectoryHelpers.DefaultDomainDCNames.Count; + int progressDCCount = 0; + double progressDCProgress = 0; + if (progressDCCountTotal > 0) + progressDCProgress = 90 / progressDCCountTotal; + + foreach (var dcName in ActiveDirectoryHelpers.DefaultDomainDCNames) + { + try + { + PingReply pr; + using (Ping p = new Ping()) + { + pr = p.Send(dcName, 2000); + } + if (pr.Status == IPStatus.Success) + { + using (DirectoryEntry dRootEntry = ActiveDirectoryHelpers.DefaultDCLdapRoot(dcName)) + { + double progressDCStart = 5 + (progressDCCount * progressDCProgress); + if (status != null) + { + status.UpdateStatus(progressDCStart, string.Format("Querying Domain Controller: {0}", dcName), "Searching..."); + } + + using (DirectorySearcher dSearcher = new DirectorySearcher(dRootEntry, "(objectClass=computer)", new string[] { "sAMAccountName", "lastLogon" }, SearchScope.Subtree)) + { + using (SearchResultCollection dResults = dSearcher.FindAll()) + { + + int progressItemCount = 0; + double progressItemProgress = dResults.Count == 0 ? 0 : (progressDCProgress / dResults.Count); + + foreach (SearchResult dResult in dResults) + { + ResultPropertyValueCollection dProp = dResult.Properties["sAMAccountName"]; + if (dProp != null && dProp.Count > 0) + { + string computerName = ((string)dProp[0]).TrimEnd(new char[] { '$' }).ToUpper(); + + if (status != null) + if (progressItemCount % 150 == 0) // Only Update Status every 150 devices + status.UpdateStatus(progressDCStart + (progressItemProgress * progressItemCount), string.Format("Analysing Device: {0}", computerName)); + + dProp = dResult.Properties["lastLogon"]; + if (dProp != null && dProp.Count > 0) + { + long lastLogonInt = (long)dProp[0]; + if (lastLogonInt > 0L) + { + System.DateTime computerNameDate = System.DateTime.FromFileTime(lastLogonInt); + System.DateTime existingDate; + if (computerLastLogonDates.TryGetValue(computerName, out existingDate)) + { + if (System.DateTime.Compare(existingDate, computerNameDate) < 0) + { + computerLastLogonDates[computerName] = computerNameDate; + } + } + else + { + computerLastLogonDates[computerName] = computerNameDate; + } + } + } + } + progressItemCount++; + } + } + } + } + } + else + { + SystemLog.LogError(new string[] + { + string.Format("Unable to ping Domain Controller: '{0}' (ref: Disco.BI.Interop.ActiveDirectory.ActiveDirectoryUpdateLastNetworkLogonDateJob.UpdateLastNetworkLogonDates)", dcName) + }); + } + } + catch (System.Exception ex) + { + SystemLog.LogException("UpdateLastNetworkLogonDates", ex); + } + progressDCCount++; + } + + + foreach (Device d in context.Devices.Where(device => device.ComputerName != null)) + { + DateTime computerLastLogonDate; + if (computerLastLogonDates.TryGetValue(d.ComputerName.ToUpper(), out computerLastLogonDate)) + { + if (d.LastNetworkLogonDate.HasValue) + { + if (System.DateTime.Compare(d.LastNetworkLogonDate.Value, computerLastLogonDate) < 0) + { + d.LastNetworkLogonDate = computerLastLogonDate; + } + } + else + { + d.LastNetworkLogonDate = computerLastLogonDate; + } + } + } + } + } +} diff --git a/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectoryUserAccountExtensions.cs b/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectoryUserAccountExtensions.cs new file mode 100644 index 00000000..630bb462 --- /dev/null +++ b/Disco.BI/BI/Interop/ActiveDirectory/ActiveDirectoryUserAccountExtensions.cs @@ -0,0 +1,42 @@ +using Disco.Models.Interop.ActiveDirectory; +using System; +using Disco.Models.Repository; +namespace Disco.BI.Interop.ActiveDirectory +{ + internal static class ActiveDirectoryUserAccountExtensions + { + public static bool HasRole(this ActiveDirectoryUserAccount account, string Role) + { + return account.Groups != null && account.Groups.Contains(Role.ToLower()); + } + + public static object GetPropertyValue(this ActiveDirectoryUserAccount account, string PropertyName, int Index = 0) + { + switch (PropertyName.ToLower()) + { + case "name": + return account.Name; + case "samaccountname": + return account.sAMAccountName; + case "distinguishedname": + return account.DistinguishedName; + case "objectsid": + return account.ObjectSid; + case "sn": + return account.Surname; + case "givenname": + return account.GivenName; + case "mail": + return account.Email; + case "telephonenumber": + return account.Phone; + default: + object[] adProperty; + if (account.LoadedProperties.TryGetValue(PropertyName, out adProperty) && Index <= adProperty.Length) + return adProperty[Index]; + else + return null; + } + } + } +} diff --git a/Disco.BI/BI/Interop/Community/UpdateCheck.cs b/Disco.BI/BI/Interop/Community/UpdateCheck.cs new file mode 100644 index 00000000..ce76b880 --- /dev/null +++ b/Disco.BI/BI/Interop/Community/UpdateCheck.cs @@ -0,0 +1,185 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; +using System.Xml.Serialization; +using Disco.Data.Repository; +using Disco.Models.BI.Interop.Community; +using Disco.Services.Tasks; +using Newtonsoft.Json; + +namespace Disco.BI.Interop.Community +{ + public static class UpdateCheck + { + private static string UpdateUrl(DiscoDataContext db) + { + // Special case for DiscoCommunity Hosting Network + try + { + var ip = (from addr in Dns.GetHostEntry(Dns.GetHostName()).AddressList + where addr.AddressFamily == System.Net.Sockets.AddressFamily.InterNetwork + && addr.ToString().StartsWith("10.131.200.") + select addr).FirstOrDefault(); + if (ip != null) + { + return "http://hades3:9393/base/DiscoUpdate/V1"; + } + } + catch (Exception) + {} // Ignore Errors + + return "http://discoict.com.au/base/DiscoUpdate/V1"; + } + + public static string CurrentDiscoVersion() + { + var AssemblyVersion = typeof(UpdateCheck).Assembly.GetName().Version; + return string.Format("{0}.{1}.{2:0000}.{3:0000}", AssemblyVersion.Major, AssemblyVersion.Minor, AssemblyVersion.Build, AssemblyVersion.Revision); + } + + public static UpdateResponse Check(DiscoDataContext db, bool UseProxy, ScheduledTaskStatus status = null) + { + if (status != null) + status.UpdateStatus(10, "Building Update Request"); + + var request = BuildRequest(db); + //var requestJson = JsonConvert.SerializeObject(request); + + if (status != null) + status.UpdateStatus(40, "Sending Request"); + + var DiscoBIVersion = CurrentDiscoVersion(); + + HttpWebRequest webRequest = (HttpWebRequest)HttpWebRequest.Create(UpdateUrl(db)); + webRequest.ContentType = "application/json"; + webRequest.Method = WebRequestMethods.Http.Post; + webRequest.UserAgent = string.Format("Disco/{0} (Update)", DiscoBIVersion); + + using (var wrStream = webRequest.GetRequestStream()) + { + XmlSerializer xml = new XmlSerializer(typeof(UpdateRequestV1)); + xml.Serialize(wrStream, request); + } + if (status != null) + status.UpdateStatus(50, "Waiting for Response"); + using (HttpWebResponse webResponse = (HttpWebResponse)webRequest.GetResponse()) + { + if (webResponse.StatusCode == HttpStatusCode.OK) + { + if (status != null) + status.UpdateStatus(90, "Reading Response"); + UpdateResponse result; + using (var wResStream = webResponse.GetResponseStream()) + { + XmlSerializer xml = new XmlSerializer(typeof(UpdateResponse)); + result = (UpdateResponse)xml.Deserialize(wResStream); + } + //var result = JsonConvert.DeserializeObject(responseContent); + db.DiscoConfiguration.UpdateLastCheck = result; + db.SaveChanges(); + + status.SetFinishedMessage(string.Format("The update server reported Version {0} is the latest.", result.Version)); + + return result; + } + else + { + if (status != null) + status.SetTaskException(new WebException(string.Format("Server responded with: [{0}] {1}", webResponse.StatusCode, webResponse.StatusDescription))); + return null; + } + } + } + + private static UpdateRequestV1 BuildRequest(DiscoDataContext db) + { + var m = new UpdateRequestV1(); + + m.DeploymentId = db.DiscoConfiguration.DeploymentId; + + m.CurrentDiscoVersion = CurrentDiscoVersion(); + + m.OrganisationName = db.DiscoConfiguration.OrganisationName; + m.BroadbandDoeWanId = GetBroadbandDoeWanId(); + m.BetaDeployment = db.DiscoConfiguration.UpdateBetaDeployment; + + m.Stat_JobCounts = db.Jobs.GroupBy(j => j.JobTypeId).Select(g => new Disco.Models.BI.Interop.Community.UpdateRequestV1.Stat { Key = g.Key, Count = g.Count() }).ToList(); + m.Stat_OpenJobCounts = db.Jobs.Where(j => j.ClosedDate == null).GroupBy(j => j.JobTypeId).Select(g => new Disco.Models.BI.Interop.Community.UpdateRequestV1.Stat { Key = g.Key, Count = g.Count() }).ToList(); + var activeThreshold = DateTime.Now.AddDays(-60); + m.Stat_ActiveDeviceModelCounts = db.DeviceModels.Select(dm => new Disco.Models.BI.Interop.Community.UpdateRequestV1.Stat { Key = dm.Manufacturer + ";" + dm.Model, Count = dm.Devices.Count(d => d.DecommissionedDate == null && (d.LastNetworkLogonDate == null || d.LastNetworkLogonDate > activeThreshold)) }).ToList(); + m.Stat_UserCounts = db.Users.GroupBy(u => u.Type).Select(g => new Disco.Models.BI.Interop.Community.UpdateRequestV1.Stat { Key = g.Key, Count = g.Count() }).ToList(); + + return m; + } + + #region DoE Query + public static string GetBroadbandDoeWanId() + { + // DnsQuery for broadband.doe.wan + IPHostEntry doeWanDnsEntry; + try + { + doeWanDnsEntry = Dns.GetHostEntry("broadband.doe.wan"); + } + catch (Exception) + { return null; } // Fail on error + + // Try using IPSearch feature + XDocument doeWanIPSearchResult = TryDownloadDoeIPSearch(false); + if (doeWanIPSearchResult == null) + doeWanIPSearchResult = TryDownloadDoeIPSearch(true); + + if (doeWanIPSearchResult == null) + return null; + + try + { + return doeWanIPSearchResult.Element("resultset").Element("site").Element("number").Value.ToLower(); + } + catch (Exception) + { return null; } // Fail on error + } + private static XDocument TryDownloadDoeIPSearch(bool useProxy) + { + try + { + var DiscoBIVersion = CurrentDiscoVersion(); + + HttpWebRequest wReq = (HttpWebRequest)HttpWebRequest.Create("http://broadband.doe.wan/ipsearch/showresult.php"); + if (!useProxy) + wReq.Proxy = new WebProxy(); // Empty Proxy Config + wReq.Method = WebRequestMethods.Http.Post; + wReq.ContentType = "application/x-www-form-urlencoded"; + wReq.UserAgent = string.Format("Disco/{0}", DiscoBIVersion); + using (var wrStream = wReq.GetRequestStream()) + { + using (var wrStreamWriter = new StreamWriter(wrStream)) + { + wrStreamWriter.Write("mode=whoami"); + } + } + using (HttpWebResponse wRes = (HttpWebResponse)wReq.GetResponse()) + { + if (wRes.StatusCode == HttpStatusCode.OK) + { + using (var wResStream = wRes.GetResponseStream()) + { + return XDocument.Load(wResStream); + } + } + else + return null; + } + } + catch (Exception) + { return null; } // Fail on error + } + #endregion + + } +} diff --git a/Disco.BI/BI/Interop/Community/UpdateCheckTask.cs b/Disco.BI/BI/Interop/Community/UpdateCheckTask.cs new file mode 100644 index 00000000..9cfbca9c --- /dev/null +++ b/Disco.BI/BI/Interop/Community/UpdateCheckTask.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Disco.Data.Repository; +using Disco.Services.Logging; +using Disco.Services.Tasks; +using Quartz; + +namespace Disco.BI.Interop.Community +{ + public class UpdateCheckTask : ScheduledTask + { + public override string TaskName { get { return "Disco Community - Check for Update"; } } + public override bool SingleInstanceTask { get { return true; } } + public override bool CancelInitiallySupported { get { return false; } } + + public static ScheduledTaskStatus ScheduleNow() + { + + var runningTasks = ScheduledTasks.GetTaskStatuses(typeof(UpdateCheckTask)).Where(ts => ts.IsRunning).ToList(); + if (runningTasks.Count > 0) + return runningTasks.First(); + else + { + var t = new UpdateCheckTask(); + return t.ScheduleTask(); + } + } + public static ScheduledTaskStatus RunningStatus + { + get + { + return ScheduledTasks.GetTaskStatuses(typeof(UpdateCheckTask)).Where(ts => ts.IsRunning).FirstOrDefault(); + } + } + public static DateTime? NextScheduled + { + get + { + var runningTasks = ScheduledTasks.GetTaskStatuses(typeof(UpdateCheckTask)).ToList(); + DateTime timestamp = DateTime.MaxValue; + foreach (var t in runningTasks) + { + if (t.NextScheduledTimestamp != null && t.NextScheduledTimestamp.Value < timestamp) + timestamp = t.NextScheduledTimestamp.Value; + } + if (timestamp == DateTime.MaxValue) + return null; + else + return timestamp; + } + } + + public override void InitalizeScheduledTask(Data.Repository.DiscoDataContext dbContext) + { + // ActiveDirectoryUpdateLastNetworkLogonDateJob @ 11:30pm + var rnd = new Random(); + + var rndHour = rnd.Next(12, 23); + var rndMinute = rnd.Next(0, 59); + + TriggerBuilder triggerBuilder = TriggerBuilder.Create(). + WithSchedule(CronScheduleBuilder.DailyAtHourAndMinute(rndHour, rndMinute)); + + this.ScheduleTask(triggerBuilder); + } + + protected override void ExecuteTask() + { + using (DiscoDataContext db = new DiscoDataContext()) + { + try + { + UpdateCheck.Check(db, true, this.Status); + } + catch (Exception) + { + // Could be proxy error - try again without proxy: + UpdateCheck.Check(db, false, this.Status); + } + } + } + } +} diff --git a/Disco.BI/BI/Interop/MimeTypes.cs b/Disco.BI/BI/Interop/MimeTypes.cs new file mode 100644 index 00000000..38580f2d --- /dev/null +++ b/Disco.BI/BI/Interop/MimeTypes.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Win32; + +namespace Disco.BI.Interop +{ + public static class MimeTypes + { + public static string ResolveMimeType(string Filename) + { + string fileExtension; + if (Filename.Contains(".")) + fileExtension = Filename.Substring(Filename.LastIndexOf(".") + 1).ToLower(); + else + fileExtension = Filename.ToLower(); + + // Try Known Mime Types + switch (fileExtension) + { + case "pdf": + return "application/pdf"; + case "doc": + return "application/msword"; + case "docx": + return "application/vnd.openxmlformats-officedocument.wordprocessingml.document"; + case "docm": + return "application/vnd.ms-word.document.macroEnabled.12"; + case "xml": + return "text/xml"; + case "xls": + return "application/vnd.ms-excel"; + case "xlsx": + return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; + case "xlsm": + return "application/vnd.ms-excel.sheet.macroEnabled.12"; + case "csv": + return "application/vnd.ms-excel"; + case "jpg": + return "image/jpeg"; + case "gif": + return "image/gif"; + case "png": + return "image/png"; + case "bmp": + return "image/bmp"; + case "avi": + return "video/avi"; + case "mpeg": + case "mpg": + return "video/mpeg"; + case "mp3": + return "audio/mpeg"; + case "mp4": + return "video/mp4"; + case "wmv": + return "video/x-ms-wmv"; + case "mov": + return "video/quicktime"; + } + + // Check System Registry + try + { + RegistryKey regExtensionKey = Registry.ClassesRoot.OpenSubKey("." + fileExtension); + if (regExtensionKey != null) + { + string regExtensionContentType = regExtensionKey.GetValue("Content Type") as string; + if (regExtensionContentType != null) + { + return regExtensionContentType; + } + } + } + catch + { + // Ignore Errors + } + + // Return Default + return "unknown/unknown"; + } + } +} diff --git a/Disco.BI/BI/Interop/Pdf/PdfGenerator.cs b/Disco.BI/BI/Interop/Pdf/PdfGenerator.cs new file mode 100644 index 00000000..eaebd62b --- /dev/null +++ b/Disco.BI/BI/Interop/Pdf/PdfGenerator.cs @@ -0,0 +1,251 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Models.Repository; +using Disco.Data.Repository; +using Disco.Models.BI.DocumentTemplates; +using System.IO; +using iTextSharp.text.pdf; +using System.Collections.Concurrent; +using Disco.BI.Expressions; +using System.Collections; +using Disco.BI.Extensions; +using Disco.Models.BI.Expressions; + +namespace Disco.BI.Interop.Pdf +{ + public static class PdfGenerator + { + + public static System.IO.Stream GenerateBulkFromTemplate(DocumentTemplate dt, DiscoDataContext dbContext, User CreatorUser, System.DateTime Timestamp, params object[] DataObjects) + { + if (DataObjects.Length > 0) + { + List generatedPdfs = new List(DataObjects.Length); + using (Models.BI.DocumentTemplates.DocumentState state = Models.BI.DocumentTemplates.DocumentState.DefaultState()) + { + foreach (object d in DataObjects) + { + generatedPdfs.Add(dt.GeneratePdf(dbContext, d, CreatorUser, Timestamp, state, true)); + state.SequenceNumber++; + state.FlushScopeCache(); + } + } + if (generatedPdfs.Count == 1) + { + return generatedPdfs[0]; + } + else + { + Stream bulkPdf = DocumentTemplateBI.Utilities.JoinPdfs(generatedPdfs.ToArray()); + foreach (Stream singlePdf in generatedPdfs) + singlePdf.Dispose(); + return bulkPdf; + } + } + return null; + } + + public static System.IO.Stream GenerateBulkFromTemplate(DocumentTemplate dt, DiscoDataContext dbContext, User CreatorUser, System.DateTime Timestamp, params string[] DataObjectsIds) + { + object[] DataObjects; + + switch (dt.Scope) + { + case DocumentTemplate.DocumentTemplateScopes.Device: + DataObjects = dbContext.Devices.Where(d => DataObjectsIds.Contains(d.SerialNumber)).ToArray(); + break; + case DocumentTemplate.DocumentTemplateScopes.Job: + int[] intDataObjectsIds = DataObjectsIds.Select(i => int.Parse(i)).ToArray(); + DataObjects = dbContext.Jobs.Where(j => intDataObjectsIds.Contains(j.Id)).ToArray(); + break; + case DocumentTemplate.DocumentTemplateScopes.User: + DataObjects = new object[DataObjectsIds.Length]; + for (int idIndex = 0; idIndex < DataObjectsIds.Length; idIndex++) + { + DataObjects[idIndex] = UserBI.UserCache.GetUser(DataObjectsIds[idIndex], dbContext, true); + if (DataObjects[idIndex] == null) + throw new Exception(string.Format("Unknown Username specified: {0}", DataObjectsIds[idIndex])); + } + break; + default: + throw new InvalidOperationException("Invalid DocumentType Scope"); + } + + return GenerateBulkFromTemplate(dt, dbContext, CreatorUser, Timestamp, DataObjects); + } + + public static System.IO.Stream GenerateFromTemplate(DocumentTemplate dt, DiscoDataContext dbContext, object Data, User CreatorUser, System.DateTime TimeStamp, DocumentState State, bool FlattenFields = false) + { + // Validate Data + switch (dt.Scope) + { + case DocumentTemplate.DocumentTemplateScopes.Device: + if (!(Data is Device)) + throw new ArgumentException("This AttachmentType is configured for Devices only", "Data"); + break; + case DocumentTemplate.DocumentTemplateScopes.Job: + if (!(Data is Job)) + throw new ArgumentException("This AttachmentType is configured for Jobs only", "Data"); + break; + case DocumentTemplate.DocumentTemplateScopes.User: + if (!(Data is User)) + throw new ArgumentException("This AttachmentType is configured for Users only", "Data"); + break; + default: + throw new InvalidOperationException("Invalid AttachmentType Scope"); + } + + dbContext.Configuration.LazyLoadingEnabled = true; + + // Override FlattenFields if Document Template instructs. + if (dt.FlattenForm) + FlattenFields = true; + + ConcurrentDictionary expressionCache = dt.PdfExpressionsFromCache(dbContext); + + string templateFilename = dt.RepositoryFilename(dbContext); + PdfReader pdfReader = new PdfReader(templateFilename); + + MemoryStream pdfGeneratedStream = new MemoryStream(); + PdfStamper pdfStamper = new PdfStamper(pdfReader, pdfGeneratedStream); + + pdfStamper.FormFlattening = FlattenFields; + pdfStamper.Writer.CloseStream = false; + + IDictionary expressionVariables = Expression.StandardVariables(dt, dbContext, CreatorUser, TimeStamp, State); + + foreach (string pdfFieldKey in pdfStamper.AcroFields.Fields.Keys) + { + if (pdfFieldKey.Equals("DiscoAttachmentId", StringComparison.InvariantCultureIgnoreCase)) + { + AcroFields.Item fields = pdfStamper.AcroFields.Fields[pdfFieldKey]; + string fieldValue = dt.UniqueIdentifier(Data, CreatorUser.Id, TimeStamp); + if (FlattenFields) + pdfStamper.AcroFields.SetField(pdfFieldKey, String.Empty); + else + pdfStamper.AcroFields.SetField(pdfFieldKey, fieldValue); + + IList pdfFieldPositions = pdfStamper.AcroFields.GetFieldPositions(pdfFieldKey); + for (int pdfFieldOrdinal = 0; pdfFieldOrdinal < fields.Size; pdfFieldOrdinal++) + { + AcroFields.FieldPosition pdfFieldPosition = pdfFieldPositions[pdfFieldOrdinal]; + string pdfBarcodeContent = dt.UniquePageIdentifier(Data, CreatorUser.Id, TimeStamp, pdfFieldPosition.page); + BarcodeQRCode pdfBarcode = new BarcodeQRCode(pdfBarcodeContent, (int)pdfFieldPosition.position.Width, (int)pdfFieldPosition.position.Height, null); + iTextSharp.text.Image pdfBarcodeImage = pdfBarcode.GetImage(); + pdfBarcodeImage.SetAbsolutePosition(pdfFieldPosition.position.Left, pdfFieldPosition.position.Bottom); + pdfStamper.GetOverContent(pdfFieldPosition.page).AddImage(pdfBarcodeImage); + } + // Hide Fields + PdfDictionary field = fields.GetValue(0); + if ((PdfName)field.Get(PdfName.TYPE) == PdfName.ANNOT) + { + field.Put(PdfName.F, new PdfNumber(6)); + } + else + { + PdfArray fieldKids = (PdfArray)field.Get(PdfName.KIDS); + foreach (PdfIndirectReference fieldKidRef in fieldKids) + { + ((PdfDictionary)pdfReader.GetPdfObject(fieldKidRef.Number)).Put(PdfName.F, new PdfNumber(6)); + } + } + } + else + { + Expression fieldExpression = null; + if (expressionCache.TryGetValue(pdfFieldKey, out fieldExpression)) + { + if (fieldExpression.IsDynamic) + { + Tuple fieldExpressionResult = fieldExpression.Evaluate(Data, expressionVariables); + + if (fieldExpressionResult.Item3 != null) + { + IImageExpressionResult imageResult = (fieldExpressionResult.Item3 as IImageExpressionResult); + if (imageResult != null) + { + // Output Image + AcroFields.Item fields = pdfStamper.AcroFields.Fields[pdfFieldKey]; + IList pdfFieldPositions = pdfStamper.AcroFields.GetFieldPositions(pdfFieldKey); + for (int pdfFieldOrdinal = 0; pdfFieldOrdinal < fields.Size; pdfFieldOrdinal++) + { + AcroFields.FieldPosition pdfFieldPosition = pdfFieldPositions[pdfFieldOrdinal]; + iTextSharp.text.Image pdfImage = iTextSharp.text.Image.GetInstance(imageResult.GetImage((int)pdfFieldPosition.position.Width, (int)pdfFieldPosition.position.Height)); + pdfImage.SetAbsolutePosition(pdfFieldPosition.position.Left, pdfFieldPosition.position.Bottom); + pdfStamper.GetOverContent(pdfFieldPosition.page).AddImage(pdfImage); + } + if (!fieldExpressionResult.Item2 && !imageResult.ShowField) + { + // Hide Fields + PdfDictionary field = fields.GetValue(0); + if ((PdfName)field.Get(PdfName.TYPE) == PdfName.ANNOT) + { + field.Put(PdfName.F, new PdfNumber(6)); + } + else + { + PdfArray fieldKids = (PdfArray)field.Get(PdfName.KIDS); + foreach (PdfIndirectReference fieldKidRef in fieldKids) + { + ((PdfDictionary)pdfReader.GetPdfObject(fieldKidRef.Number)).Put(PdfName.F, new PdfNumber(6)); + } + } + } + } + } + + pdfStamper.AcroFields.SetField(pdfFieldKey, fieldExpressionResult.Item1); + + if (fieldExpressionResult.Item2) // Expression Error + { + AcroFields.Item fields = pdfStamper.AcroFields.Fields[pdfFieldKey]; + for (int pdfFieldOrdinal = 0; pdfFieldOrdinal < fields.Size; pdfFieldOrdinal++) + { + PdfDictionary field = fields.GetValue(pdfFieldOrdinal); + PdfDictionary fieldMK; + if (field.Contains(PdfName.MK)) + fieldMK = field.GetAsDict(PdfName.MK); + else + { + fieldMK = new PdfDictionary(PdfName.MK); + field.Put(PdfName.MK, fieldMK); + } + fieldMK.Put(PdfName.BC, new PdfArray(new float[] { 1, 0, 0 })); + } + } + } + } + else + { + throw new InvalidOperationException("Pdf template field expressions are out of sync with the expression cache"); + } + } + State.FlushFieldCache(); + } + + pdfStamper.Close(); + pdfReader.Close(); + + if (dt.Scope == DocumentTemplate.DocumentTemplateScopes.Job) + { + // Write Job Log + + Job j = (Job)Data; + JobLog jl = new JobLog() + { + JobId = j.Id, + TechUserId = CreatorUser.Id, + Timestamp = DateTime.Now + }; + jl.Comments = string.Format("Document Generated{0}{1} [{2}]", Environment.NewLine, dt.Description, dt.Id); + dbContext.JobLogs.Add(jl); + } + + pdfGeneratedStream.Position = 0; + return pdfGeneratedStream; + } + + } +} diff --git a/Disco.BI/BI/Interop/Pdf/PdfImporter.cs b/Disco.BI/BI/Interop/Pdf/PdfImporter.cs new file mode 100644 index 00000000..48a00a2f --- /dev/null +++ b/Disco.BI/BI/Interop/Pdf/PdfImporter.cs @@ -0,0 +1,471 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using iTextSharp.text.pdf; +using System.IO; +using System.Drawing; +using Disco.BI.DocumentTemplateBI.Importer; +using Disco.BI.DocumentTemplateBI; +using System.Drawing.Drawing2D; +using com.google.zxing; +using com.google.zxing.multi.qrcode; +using Disco.Data.Repository; +using System.Web.Caching; +using Disco.BI.Extensions; +using Disco.Models.Repository; +using System.Collections; +using com.google.zxing.common; +using BitMiracle.LibTiff.Classic; + +namespace Disco.BI.Interop.Pdf +{ + public static class PdfImporter + { + + private class DetectImageResult : IDisposable + { + public Result Result { get; set; } + public Point ResultOffset { get; set; } + public double ResultScale { get; set; } + + public void Dispose() + { + // Do Nothing; yet... + } + } + + private class DetectPageResult : IDisposable + { + public int PageNumber { get; set; } + public DocumentUniqueIdentifier DetectedIdentifier { get; set; } + public Disco.BI.Extensions.UtilityExtensions.ImageMontage ThumbnailImage { get; set; } + public MemoryStream AttachmentThumbnailImage { get; set; } + public Disco.BI.Extensions.UtilityExtensions.ImageMontage UndetectedPageImage { get; set; } + + public void DrawThumbnailImageResult(DetectImageResult Result, Image DetectedImage) + { + if (Result.Result.ResultPoints.Length == 4) + { // Draw Square on Thumbnail + using (Graphics thumbnailGraphics = Graphics.FromImage(ThumbnailImage.Montage)) + { + var thumbnailOffset = ThumbnailImage.MontageSourceImageOffsets[DetectedImage]; + + var linePoints = Result.Result.ResultPoints.Select(p => new Point((int)(thumbnailOffset + ((Result.ResultOffset.X + p.X) * Result.ResultScale * ThumbnailImage.MontageScale)), (int)((p.Y + Result.ResultOffset.Y) * Result.ResultScale * ThumbnailImage.MontageScale))).ToArray(); + using (GraphicsPath graphicsPath = new GraphicsPath()) + { + for (int linePointIndex = 0; linePointIndex < (linePoints.Length - 1); linePointIndex++) + graphicsPath.AddLine(linePoints[linePointIndex], linePoints[linePointIndex + 1]); + graphicsPath.AddLine(linePoints[linePoints.Length - 1], linePoints[0]); + using (SolidBrush graphicsBrush = new SolidBrush(Color.FromArgb(128, 255, 0, 0))) + thumbnailGraphics.FillPath(graphicsBrush, graphicsPath); + using (Pen graphicsPen = new Pen(Color.FromArgb(200, 255, 0, 0), 2)) + thumbnailGraphics.DrawPath(graphicsPen, graphicsPath); + } + } + } + } + + public void Dispose() + { + if (ThumbnailImage != null) + { + ThumbnailImage.Dispose(); + ThumbnailImage = null; + } + if (AttachmentThumbnailImage != null) + { + AttachmentThumbnailImage.Dispose(); + AttachmentThumbnailImage = null; + } + if (UndetectedPageImage != null) + { + UndetectedPageImage.Dispose(); + UndetectedPageImage = null; + } + } + } + + private static DetectImageResult DetectImage(DiscoDataContext dbContext, Bitmap pageImageOriginal, string SessionId, IEnumerable detectDocumentTemplates) + { + Bitmap pageImage = pageImageOriginal; + double pageImageModifiedScale = 1; + + try + { + // Resize if Resolution > 80; Set to 72 Dpi + if (pageImage.HorizontalResolution > 80 || pageImage.VerticalResolution > 80) + { + pageImageModifiedScale = pageImage.HorizontalResolution / 72; + int newWidth = (int)((72 / pageImage.HorizontalResolution) * pageImage.Width); + int newHeight = (int)((72 / pageImage.VerticalResolution) * pageImage.Height); + pageImage = pageImage.ResizeImage(newWidth, newHeight); + } + + Result zxingResult = default(Result); + Point zxingResultOffset = Point.Empty; + QRCodeMultiReader zxingMfr = new QRCodeMultiReader(); + Hashtable zxingMfrHints = new Hashtable(); + zxingMfrHints.Add(DecodeHintType.TRY_HARDER, true); + // Look in 'Known' locations + foreach (DocumentTemplate dt in detectDocumentTemplates) + { + var locationBag = dt.QRCodeLocations(dbContext); + foreach (var location in locationBag) + { + System.Drawing.Rectangle region = new Rectangle( + (int)(pageImage.Width * location.Left), + (int)(pageImage.Width * location.Top), + (int)(pageImage.Width * location.Width), + (int)(pageImage.Height * location.Height)); + RGBLuminanceSource zxingSource; + using (Bitmap pageImageRegion = new Bitmap(region.Width, region.Height)) + { + using (Graphics pageImageRegionGraphics = Graphics.FromImage(pageImageRegion)) + { + pageImageRegionGraphics.DrawImage(pageImage, 0, 0, region, GraphicsUnit.Pixel); + } + zxingSource = new RGBLuminanceSource(pageImageRegion, region.Width, region.Height); + } + var zxingHB = new HybridBinarizer(zxingSource); + var zxingBB = new BinaryBitmap(zxingHB); + try + { + zxingResult = zxingMfr.decode(zxingBB, zxingMfrHints); + zxingResultOffset = region.Location; + break; + } + catch (ReaderException) + { + // Ignore Location Errors + } + } + if (zxingResult != null) + break; + } + if (zxingResult == null) + { + // Not found with 'known' locations + // Try whole image + var zxingSource = new RGBLuminanceSource(pageImage, pageImage.Width, pageImage.Height); + var zxingHB = new HybridBinarizer(zxingSource); + var zxingBB = new BinaryBitmap(zxingHB); + try + { + zxingResult = zxingMfr.decode(zxingBB, zxingMfrHints); + } + catch (ReaderException) + { + // Ignore Errors + } + } + + if (zxingResult != null) + return new DetectImageResult() { Result = zxingResult, ResultOffset = zxingResultOffset, ResultScale = pageImageModifiedScale }; + else + return null; + } + catch (Exception ex) + { + throw ex; + } + finally + { + if (pageImageOriginal != pageImage) + pageImage.Dispose(); + } + } + + private static DetectPageResult DetectPage(DiscoDataContext dbContext, PdfReader pdfReader, int PageNumber, string SessionId, string DataStoreSessionCacheLocation, IEnumerable detectDocumentTemplates) + { + DetectPageResult result = new DetectPageResult() { PageNumber = PageNumber }; + + DocumentImporterLog.LogImportPageProgress(SessionId, PageNumber, 10, "Loading Page Images"); + + using (DisposableImageCollection pageImages = pdfReader.PdfPageImages(PageNumber)) + { + if (pageImages.Count > 0) + { + result.ThumbnailImage = pageImages.BuildImageMontage(256, 256); + var pageThumbnailFilename = Path.Combine(DataStoreSessionCacheLocation, string.Format("{0}-{1}", SessionId, PageNumber)); + + result.ThumbnailImage.Montage.SavePng(pageThumbnailFilename); + DocumentImporterLog.LogImportPageImageUpdate(SessionId, PageNumber); + + double pageProgressInterval = 90 / pageImages.Count; + + foreach (var pageImageOriginal in pageImages) + { + DocumentImporterLog.LogImportPageProgress(SessionId, PageNumber, (int)(10 + (pageProgressInterval * pageImages.IndexOf(pageImageOriginal))), String.Format("Processing Page Image {0} of {1}", pageImages.IndexOf(pageImageOriginal) + 1, pageImages.Count)); + + using (var zxingResult = DetectImage(dbContext, pageImageOriginal, SessionId, detectDocumentTemplates)) + { + if (zxingResult != null) + { + if (DocumentUniqueIdentifier.IsDocumentUniqueIdentifier(zxingResult.Result.Text)) + { + result.DrawThumbnailImageResult(zxingResult, pageImageOriginal); + result.ThumbnailImage.Montage.SavePng(pageThumbnailFilename); + DocumentImporterLog.LogImportPageImageUpdate(SessionId, PageNumber); + + result.AttachmentThumbnailImage = new MemoryStream(); + using (var attachmentThumbImage = pageImages.BuildImageMontage(48, 48, true)) + { + using (Image mimeTypeIcon = Disco.Properties.Resources.MimeType_pdf16) + attachmentThumbImage.Montage.EmbedIconOverlay(mimeTypeIcon); + attachmentThumbImage.Montage.SaveJpg(95, result.AttachmentThumbnailImage); + } + + result.DetectedIdentifier = new DocumentUniqueIdentifier(zxingResult.Result.Text, PageNumber.ToString()); + + return result; + } + } + } + } + + // Page Unassigned + result.UndetectedPageImage = pageImages.BuildImageMontage(700, 700); + } + + return result; + } + } + + public static bool ProcessPdfAttachment(string Filename, DiscoDataContext dbContext, string SessionId, Cache HttpCache) + { + var dataStoreUnassignedLocation = DataStore.CreateLocation(dbContext, "DocumentDropBox_Unassigned"); + + DocumentImporterLog.LogImportProgress(SessionId, 0, "Reading File"); + + using (FileStream fs = new FileStream(Filename, FileMode.Open, FileAccess.ReadWrite, FileShare.None)) + { + var pdfReader = new PdfReader(fs); + + var pdfPagesAssigned = new Dictionary>(); + + var dataStoreSessionPagesCacheLocation = DataStore.CreateLocation(dbContext, "Cache\\DocumentDropBox_SessionPages"); + var detectDocumentTemplates = dbContext.DocumentTemplates.ToArray(); + + double progressInterval = 70 / pdfReader.NumberOfPages; + + for (int PageNumber = 1; PageNumber <= pdfReader.NumberOfPages; PageNumber++) + { + DocumentImporterLog.LogImportProgress(SessionId, (int)(PageNumber * progressInterval), string.Format("Processing Page {0} of {1}", PageNumber, pdfReader.NumberOfPages)); + DocumentImporterLog.LogImportPageStarting(SessionId, PageNumber); + + using (var pageResult = DetectPage(dbContext, pdfReader, PageNumber, SessionId, dataStoreSessionPagesCacheLocation, detectDocumentTemplates)) + { + if (pageResult.DetectedIdentifier != null) + { + var docId = pageResult.DetectedIdentifier; + pdfPagesAssigned.Add(PageNumber, new Tuple(docId, pageResult.AttachmentThumbnailImage.ToArray())); + + docId.LoadComponents(dbContext); + DocumentImporterLog.LogImportPageDetected(SessionId, PageNumber, docId.DocumentUniqueId, docId.DocumentTemplate.Description, docId.DocumentTemplate.Scope, docId.DataId, docId.DataDescription); + } + else + { + // Undetected Page - Write Preview-Images while still in Memory + DocumentImporterLog.LogImportPageUndetected(SessionId, PageNumber); + + // Thumbnail: + string unassignedImageThumbnailFilename = Path.Combine(dataStoreUnassignedLocation, string.Format("{0}_{1}_thumbnail.png", SessionId, PageNumber)); + pageResult.ThumbnailImage.Montage.SavePng(unassignedImageThumbnailFilename); + // Large Preview + string unassignedImageFilename = Path.Combine(dataStoreUnassignedLocation, string.Format("{0}_{1}.jpg", SessionId, PageNumber)); + pageResult.UndetectedPageImage.Montage.SaveJpg(90, unassignedImageFilename); + } + } + + } + + // Write out Assigned Documents + var assignedDocuments = pdfPagesAssigned.GroupBy(u => u.Value.Item1.DocumentUniqueId).ToList(); + if (assignedDocuments.Count > 0) + { + progressInterval = 20 / assignedDocuments.Count; + + foreach (var documentPortion in assignedDocuments) + { + DocumentImporterLog.LogImportProgress(SessionId, (int)(70 + (assignedDocuments.IndexOf(documentPortion) * progressInterval)), string.Format("Importing Documents {0} of {1}", assignedDocuments.IndexOf(documentPortion) + 1, assignedDocuments.Count)); + + var documentPortionInfo = documentPortion.First().Value; + var documentPortionIdentifier = documentPortionInfo.Item1; + var documentPortionThumbnail = documentPortionInfo.Item2; + + if (!documentPortionIdentifier.LoadComponents(dbContext)) + { + // Unknown Document Unique Id + foreach (var dp in documentPortion) + { + var tag = int.Parse(dp.Value.Item1.Tag); + if (pdfPagesAssigned.ContainsKey(tag)) + pdfPagesAssigned.Remove(tag); + } + } + else + { + using (MemoryStream msBuilder = new MemoryStream()) + { + var pdfDoc = new iTextSharp.text.Document(); + var pdfCopy = new PdfCopy(pdfDoc, msBuilder); + + pdfDoc.Open(); + pdfCopy.CloseStream = false; + + foreach (var dp in documentPortion.OrderBy(dg => dg.Value.Item1.Page)) + { + var pageSize = pdfReader.GetPageSizeWithRotation(dp.Key); + var page = pdfCopy.GetImportedPage(pdfReader, dp.Key); + + pdfDoc.SetPageSize(pageSize); + pdfDoc.NewPage(); + + pdfCopy.AddPage(page); + } + + pdfDoc.Close(); + pdfCopy.Close(); + + msBuilder.Position = 0; + + var attachmentSuccess = documentPortionIdentifier.ImportPdfAttachment(dbContext, msBuilder, documentPortionThumbnail); + + if (!attachmentSuccess) + { // Unable to add Attachment + foreach (var dp in documentPortion) + { + var tag = int.Parse(dp.Value.Item1.Tag); + if (pdfPagesAssigned.ContainsKey(tag)) + pdfPagesAssigned.Remove(tag); + } + } + } + } + + + } + } + + // Write out Unassigned Pages + List pdfPagesUnassigned = new List(); + for (int PageNumber = 1; PageNumber <= pdfReader.NumberOfPages; PageNumber++) + if (!pdfPagesAssigned.ContainsKey(PageNumber)) + pdfPagesUnassigned.Add(PageNumber); + if (pdfPagesUnassigned.Count > 0) + { + progressInterval = 10 / pdfPagesUnassigned.Count; + //dataStoreUnassignedLocation + foreach (var PageNumber in pdfPagesUnassigned) + { + DocumentImporterLog.LogImportProgress(SessionId, (int)(90 + (pdfPagesUnassigned.IndexOf(PageNumber) * progressInterval)), string.Format("Processing Undetected Documents {0} of {1}", pdfPagesUnassigned.IndexOf(PageNumber) + 1, pdfPagesUnassigned.Count)); + + using (MemoryStream msBuilder = new MemoryStream()) + { + var pdfDoc = new iTextSharp.text.Document(); + var pdfCopy = new PdfCopy(pdfDoc, msBuilder); + + pdfDoc.Open(); + pdfCopy.CloseStream = false; + + var pageSize = pdfReader.GetPageSizeWithRotation(PageNumber); + var page = pdfCopy.GetImportedPage(pdfReader, PageNumber); + pdfDoc.SetPageSize(pageSize); + pdfDoc.NewPage(); + + pdfCopy.AddPage(page); + pdfDoc.Close(); + pdfCopy.Close(); + + File.WriteAllBytes(Path.Combine(dataStoreUnassignedLocation, string.Format("{0}_{1}.pdf", SessionId, PageNumber)), msBuilder.ToArray()); + + DocumentImporterLog.LogImportPageUndetectedStored(SessionId, PageNumber); + } + } + } + + } + + DocumentImporterLog.LogImportProgress(SessionId, 100, "Finished Importing Document"); + + return true; + } + public static bool ProcessPdfAttachment(string Filename, DiscoDataContext dbContext, string DocumentTemplateId, string DataId, string UserId, DateTime Timestamp) + { + using (FileStream fs = new FileStream(Filename, FileMode.Open, FileAccess.ReadWrite, FileShare.None)) + { + DocumentUniqueIdentifier identifier = new DocumentUniqueIdentifier(DocumentTemplateId, DataId, UserId, Timestamp); + identifier.LoadComponents(dbContext); + return identifier.ImportPdfAttachment(dbContext, fs, null); + } + } + + public static DisposableImageCollection GetPageImages(PdfReader pdfReader, int PageNumber) + { + var pageImages = new DisposableImageCollection(); + + var pdfPage = pdfReader.GetPageN(PageNumber); + PdfDictionary pdfPageResouces = (PdfDictionary)((PdfDictionary)pdfPage.GetDirectObject(PdfName.RESOURCES)).GetDirectObject(PdfName.XOBJECT); + + foreach (var pdfResKey in pdfPageResouces.Keys) + { + var pdfRes = pdfPageResouces.GetDirectObject(pdfResKey); + if (pdfRes.IsStream()) + { + var pdfResStream = (PdfStream)pdfRes; + var pdfResSubType = pdfResStream.Get(PdfName.SUBTYPE); + if (pdfResSubType != null && pdfResSubType == PdfName.IMAGE) + { + if (pdfResStream.Get(PdfName.FILTER) == PdfName.CCITTFAXDECODE) + { // TIFF + // Try Using GDI+ for TIFF... + var width = ((PdfNumber)(pdfResStream.Get(PdfName.WIDTH))).IntValue; + var height = ((PdfNumber)(pdfResStream.Get(PdfName.HEIGHT))).IntValue; + var bpc = ((PdfNumber)(pdfResStream.Get(PdfName.BITSPERCOMPONENT))).IntValue; + + var compressionMethod = Compression.CCITTFAX3; + + var decodeParams = pdfResStream.GetAsDict(PdfName.DECODEPARMS); + if (decodeParams != null && decodeParams.Contains(PdfName.K) && decodeParams.GetAsNumber(PdfName.K).IntValue < 0) + compressionMethod = Compression.CCITTFAX4; + + using (MemoryStream tiffStream = PdfToTiffStream(PdfReader.GetStreamBytesRaw((PRStream)pdfResStream), width, height, bpc, compressionMethod)) + { + pageImages.Add((Bitmap)Bitmap.FromStream(tiffStream)); + } + continue; + } + if (pdfResStream.Get(PdfName.FILTER) == PdfName.DCTDECODE) + { // JPG + using (MemoryStream jpgStream = new MemoryStream(PdfReader.GetStreamBytesRaw((PRStream)pdfResStream))) + { + pageImages.Add((Bitmap)Bitmap.FromStream(jpgStream, true, true)); + } + continue; + } + } + } + } + + return pageImages; + } + + private static MemoryStream PdfToTiffStream(byte[] PdfStream, int Width, int Height, int BitsPerComponent, Compression CompressionMethod) + { + var ms = new MemoryStream(); + + Tiff tif = Tiff.ClientOpen("in-memory", "w", ms, new TiffStream()); + tif.SetField(TiffTag.IMAGEWIDTH, Width); + tif.SetField(TiffTag.IMAGELENGTH, Height); + tif.SetField(TiffTag.COMPRESSION, CompressionMethod); + tif.SetField(TiffTag.BITSPERSAMPLE, BitsPerComponent); + tif.SetField(TiffTag.SAMPLESPERPIXEL, 1); + tif.WriteRawStrip(0, PdfStream, PdfStream.Length); + tif.Flush(); + + return ms; + } + + } +} diff --git a/Disco.BI/BI/Interop/PluginServices/IDiscoScheduledTask.cs b/Disco.BI/BI/Interop/PluginServices/IDiscoScheduledTask.cs new file mode 100644 index 00000000..5c817728 --- /dev/null +++ b/Disco.BI/BI/Interop/PluginServices/IDiscoScheduledTask.cs @@ -0,0 +1,14 @@ +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using System.Text; +//using Disco.Data.Repository; +//using Quartz; + +//namespace Disco.BI.Interop.PluginServices +//{ +// interface IDiscoScheduledTask +// { +// void InitalizeScheduledTask(DiscoDataContext dbContext, IScheduler Scheduler); +// } +//} diff --git a/Disco.BI/BI/Interop/PluginServices/Utilities.cs b/Disco.BI/BI/Interop/PluginServices/Utilities.cs new file mode 100644 index 00000000..73568295 --- /dev/null +++ b/Disco.BI/BI/Interop/PluginServices/Utilities.cs @@ -0,0 +1,45 @@ +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using System.Text; +//using Disco.Data.Repository; +//using Quartz; + +//namespace Disco.BI.Interop.PluginServices +//{ +// public static class Utilities +// { + +// public static void InitalizeScheduledTasks(DiscoDataContext dbContext, ISchedulerFactory SchedulerFactory) +// { + +// var scheduler = SchedulerFactory.GetScheduler(); + +// // Discover IDiscoScheduledTasks (Only from Disco Assemblies) +// var appDomain = AppDomain.CurrentDomain; + +// var scheduledTaskTypes = (from a in appDomain.GetAssemblies() +// where !a.GlobalAssemblyCache && !a.IsDynamic && a.FullName.StartsWith("Disco.", StringComparison.InvariantCultureIgnoreCase) +// from type in a.GetTypes() +// where typeof(IDiscoScheduledTask).IsAssignableFrom(type) && !type.IsAbstract +// select type); +// foreach (Type scheduledTaskType in scheduledTaskTypes) +// { +// IDiscoScheduledTask instance = (IDiscoScheduledTask)Activator.CreateInstance(scheduledTaskType); +// try +// { +// instance.InitalizeScheduledTask(dbContext, scheduler); +// } +// catch (Exception ex) +// { +// if (instance == null) +// Logging.SystemLog.LogException("Initializing Scheduled Task; Disco.BI.Interop.Plugins.Utilities.InitalizeScheduledTasks()", ex); +// else +// Logging.SystemLog.LogException(string.Format("Initializing Scheduled Task: '{0}'; Disco.BI.Interop.Plugins.Utilities.InitalizeScheduledTasks()", instance.GetType().Name), ex); +// } +// } + +// } + +// } +//} diff --git a/Disco.BI/BI/Interop/SignalRHandlers/UserHeldDevices.cs b/Disco.BI/BI/Interop/SignalRHandlers/UserHeldDevices.cs new file mode 100644 index 00000000..c14ccb24 --- /dev/null +++ b/Disco.BI/BI/Interop/SignalRHandlers/UserHeldDevices.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using SignalR; +using SignalR.Hosting.AspNet; +using SignalR.Infrastructure; + +namespace Disco.BI.Interop.SignalRHandlers +{ + public class UserHeldDevices : PersistentConnection + { + + internal static void UserJobUpdated(string JobUserId) + { + var connectionManager = GlobalHost.ConnectionManager; + var connectionContext = connectionManager.GetConnectionContext(); + if (connectionContext != null) + connectionContext.Connection.Broadcast(JobUserId); + } + + } +} diff --git a/Disco.BI/BI/JobBI/Searching.cs b/Disco.BI/BI/JobBI/Searching.cs new file mode 100644 index 00000000..072951b4 --- /dev/null +++ b/Disco.BI/BI/JobBI/Searching.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Models.BI.Job; +using Disco.Data.Repository; +using Disco.Models.Repository; +using Disco.BI.Extensions; + +namespace Disco.BI.JobBI +{ + public static class Searching + { + public static JobTableModel Search(DiscoDataContext dbContext, string Term, int? LimitCount = null, bool IncludeJobStatus = true, bool SearchDetails = false) + { + int termInt = default(int); + + IQueryable query = default(IQueryable); + + if (int.TryParse(Term, out termInt)) + { + // Term is a Number (int) + if (SearchDetails) + { + query = BuildJobTableModel(dbContext).Where(j => + j.Id == termInt || + j.DeviceHeldLocation.Contains(Term) || + j.Device.SerialNumber.Contains(Term) || + j.Device.AssetNumber.Contains(Term) || + j.User.Id == Term || + j.User.Surname.Contains(Term) || + j.User.GivenName.Contains(Term) || + j.User.DisplayName.Contains(Term) || + j.JobLogs.Any(jl => jl.Comments.Contains(Term)) || + j.JobAttachments.Any(ja => ja.Comments.Contains(Term))); + } + else + { + query = BuildJobTableModel(dbContext).Where(j => + j.Id == termInt || + j.DeviceHeldLocation.Contains(Term) || + j.Device.SerialNumber.Contains(Term) || + j.Device.AssetNumber.Contains(Term) || + j.User.Id == Term || + j.User.Surname.Contains(Term) || + j.User.GivenName.Contains(Term) || + j.User.DisplayName.Contains(Term)); + } + } + else + { + if (SearchDetails) + { + query = BuildJobTableModel(dbContext).Where(j => + j.DeviceHeldLocation.Contains(Term) || + j.Device.SerialNumber.Contains(Term) || + j.Device.AssetNumber.Contains(Term) || + j.User.Id == Term || + j.User.Surname.Contains(Term) || + j.User.GivenName.Contains(Term) || + j.User.DisplayName.Contains(Term) || + j.JobLogs.Any(jl => jl.Comments.Contains(Term)) || + j.JobAttachments.Any(ja => ja.Comments.Contains(Term))); + } + else + { + query = BuildJobTableModel(dbContext).Where(j => + j.DeviceHeldLocation.Contains(Term) || + j.Device.SerialNumber.Contains(Term) || + j.Device.AssetNumber.Contains(Term) || + j.User.Id == Term || + j.User.Surname.Contains(Term) || + j.User.GivenName.Contains(Term) || + j.User.DisplayName.Contains(Term)); + } + } + + if (LimitCount.HasValue) + query = query.Take(LimitCount.Value); + + JobTableModel model = new JobTableModel() { ShowStatus = IncludeJobStatus }; + model.Fill(dbContext, query); + + return model; + } + + public static IQueryable BuildJobTableModel(DiscoDataContext dbContext) + { + return dbContext.Jobs.Include("JobType").Include("Device").Include("User").Include("OpenedTechUser"); + } + + } +} diff --git a/Disco.BI/BI/JobBI/Statistics/DailyOpenedClosed.cs b/Disco.BI/BI/JobBI/Statistics/DailyOpenedClosed.cs new file mode 100644 index 00000000..41766bc2 --- /dev/null +++ b/Disco.BI/BI/JobBI/Statistics/DailyOpenedClosed.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Quartz; +using Disco.Models.BI.Job.Statistics; +using Disco.Data.Repository; +using Quartz.Impl; +using Disco.Services.Tasks; + +namespace Disco.BI.JobBI.Statistics +{ + public class DailyOpenedClosed : ScheduledTask + { + + private static List _data; + private static object _dataLock = new object(); + + + public override string TaskName { get { return "Job Statistics - Daily Opened/Closed Task"; } } + public override bool SingleInstanceTask { get { return true; } } + public override bool CancelInitiallySupported { get { return false; } } + + public override void InitalizeScheduledTask(DiscoDataContext dbContext) + { + // Trigger Daily @ 12:29am + TriggerBuilder triggerBuilder = TriggerBuilder.Create().WithSchedule(CronScheduleBuilder.DailyAtHourAndMinute(0, 29)); + + this.ScheduleTask(triggerBuilder); + } + protected override void ExecuteTask() + { + using (var dbContext = new DiscoDataContext()) + { + UpdateDataHistory(dbContext, true); + } + } + + //public void InitalizeScheduledTask(DiscoDataContext dbContext, IScheduler Scheduler) + //{ + // // Run @ 12:29am + // IJobDetail jobDetail = new JobDetailImpl("JobStatisticsDailyOpenedClosed", typeof(DailyOpenedClosed)); + // ITrigger trigger = TriggerBuilder.Create(). + // WithIdentity("JobStatisticsDailyOpenedClosedTrigger"). + // StartNow(). + // WithSchedule(CronScheduleBuilder.DailyAtHourAndMinute(0, 29)). + // Build(); + // Scheduler.ScheduleJob(jobDetail, trigger); + //} + + //public void Execute(IJobExecutionContext context) + //{ + // try + // { + // using (var dbContext = new DiscoDataContext()) + // { + // UpdateDataHistory(dbContext, true); + // } + // } + // catch (Exception ex) + // { + // Logging.SystemLog.LogException("Disco.BI.JobBI.Statistics.DailyOpenedClosed", ex); + // } + //} + + private static void UpdateDataHistory(DiscoDataContext dbContext, bool Refresh = false) + { + DateTime historyEnd = DateTime.Now.AddDays(-1).Date; + + if (Refresh || _data == null || _data.Count == 0 || _data.Last().Timestamp < historyEnd) + { + lock (_dataLock) + { + if (Refresh || _data == null || _data.Count == 0 || _data.Last().Timestamp < historyEnd) + { + DateTime historyStart = DateTime.Now.AddDays(-28).Date; + + // Initialize Memory Store + List resultData; + if (Refresh || _data == null) + resultData = new List(); + else + resultData = _data; + + // Remove Old Data + while (resultData.Count > 0 && resultData[0].Timestamp < historyStart) + resultData.RemoveAt(0); + + // Calculate Update Scope + DateTime processDate = historyStart; + if (resultData.Count > 0) + processDate = resultData.Last().Timestamp.AddDays(-1); + + // Cache Data + while (processDate <= historyEnd) + { + resultData.Add(Data(dbContext, processDate)); + processDate = processDate.AddDays(1); + } + + _data = resultData; + } + } + } + } + + private static DailyOpenedClosedItem Data(DiscoDataContext dbContext, DateTime ProcessDate) + { + DateTime processDateStart = ProcessDate; + DateTime processDateEnd = ProcessDate.AddDays(1); + + int totalJobs = dbContext.Jobs.Where(j => j.OpenedDate < processDateEnd && (!j.ClosedDate.HasValue || j.ClosedDate > processDateEnd)).Count(); + int openedJobs = dbContext.Jobs.Where(j => j.OpenedDate > processDateStart && j.OpenedDate < processDateEnd).Count(); + int closedJobs = dbContext.Jobs.Where(j => j.ClosedDate > processDateStart && j.ClosedDate < processDateEnd).Count(); + + return new DailyOpenedClosedItem() + { + Timestamp = ProcessDate, + TotalJobs = totalJobs, + OpenedJobs = openedJobs, + ClosedJobs = closedJobs + }; + } + + public static List Data(DiscoDataContext dbContext, bool FilterUnimportantWeekends = false) + { + List resultData; + + UpdateDataHistory(dbContext); + + if (FilterUnimportantWeekends) + resultData = _data.Where(i => (i.Timestamp.DayOfWeek != DayOfWeek.Saturday && i.Timestamp.DayOfWeek != DayOfWeek.Sunday) || + (i.OpenedJobs > 0 || i.ClosedJobs > 0)).ToList(); + else + resultData = _data.ToList(); + + resultData.Add(Data(dbContext, DateTime.Today)); + + return resultData; + } + } +} diff --git a/Disco.BI/BI/JobBI/Utilities.cs b/Disco.BI/BI/JobBI/Utilities.cs new file mode 100644 index 00000000..a7fb228a --- /dev/null +++ b/Disco.BI/BI/JobBI/Utilities.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Models.Repository; +using Disco.Data.Repository; +using Disco.Models.BI.Job; + +namespace Disco.BI.JobBI +{ + public static class Utilities + { + public static Job Create(DiscoDataContext dbContext, Device device, User user, JobType type, List subTypes, User initialTech) + { + Job j = new Job() + { + JobType = type, + OpenedTechUserId = initialTech.Id, + OpenedTechUser = initialTech, + OpenedDate = DateTime.Now + }; + + // Device + if (device != null) + { + j.Device = device; + j.DeviceSerialNumber = device.SerialNumber; + } + + // User + if (user != null) + { + j.User = user; + j.UserId = user.Id; + } + + // Sub Types + List jobSubTypes = subTypes.ToList(); + j.JobSubTypes = jobSubTypes; + + dbContext.Jobs.Add(j); + + switch (type.Id) + { + case JobType.JobTypeIds.HWar: + dbContext.JobMetaWarranties.Add(new JobMetaWarranty() { Job = j }); + break; + case JobType.JobTypeIds.HNWar: + dbContext.JobMetaNonWarranties.Add(new JobMetaNonWarranty() { Job = j }); + if (device != null) + { + // Add Job Components + var components = dbContext.DeviceComponents.Include("JobSubTypes").Where(c => !c.DeviceModelId.HasValue || c.DeviceModelId == j.Device.DeviceModelId); + var addedComponents = new List(); + foreach (var c in components) + { + if (c.JobSubTypes.Count == 0) + { // No Filter + addedComponents.Add(c); + } + else + { + foreach (var st in c.JobSubTypes) + { + foreach (var jst in jobSubTypes) + { + if (st.JobTypeId == jst.JobTypeId && st.Id == jst.Id) + { + addedComponents.Add(c); + break; + } + } + if (addedComponents.Contains(c)) + break; + } + } + } + foreach (var c in addedComponents) + dbContext.JobComponents.Add(new JobComponent() + { + Job = j, + TechUserId = initialTech.Id, + Cost = c.Cost, + Description = c.Description + }); + } + break; + } + + return j; + } + + public static string JobStatusDescription(string StatusId, Job j = null) + { + switch (StatusId) + { + case Job.JobStatusIds.Open: + return "Open"; + case Job.JobStatusIds.Closed: + return "Closed"; + case Job.JobStatusIds.AwaitingWarrantyRepair: + if (j == null) + return "Awaiting Warranty Repair"; + else + if (j.DeviceHeld.HasValue) + return string.Format("Awaiting Warranty Repair ({0})", j.JobMetaWarranty.ExternalName); + else + return string.Format("Awaiting Warranty Repair - Not Held ({0})", j.JobMetaWarranty.ExternalName); + case Job.JobStatusIds.AwaitingRepairs: + if (j == null) + return "Awaiting Repairs"; + else + if (j.DeviceHeld.HasValue) + return string.Format("Awaiting Repairs ({0})", j.JobMetaNonWarranty.RepairerName); + else + return string.Format("Awaiting Repairs - Not Held ({0})", j.JobMetaNonWarranty.RepairerName); + case Job.JobStatusIds.AwaitingDeviceReturn: + return "Awaiting Device Return"; + case Job.JobStatusIds.AwaitingUserAction: + return "Awaiting User Action"; + case Job.JobStatusIds.AwaitingAccountingPayment: + return "Awaiting Accounting Payment"; + case Job.JobStatusIds.AwaitingAccountingCharge: + return "Awaiting Accounting Charge"; + case Job.JobStatusIds.AwaitingInsuranceProcessing: + return "Awaiting Insurance Processing"; + default: + return "Unknown"; + } + } + public static string JobStatusDescription(string StatusId, JobTableModel.JobTableItemModelIncludeStatus j = null) + { + switch (StatusId) + { + case Job.JobStatusIds.Open: + return "Open"; + case Job.JobStatusIds.Closed: + return "Closed"; + case Job.JobStatusIds.AwaitingWarrantyRepair: + if (j == null) + return "Awaiting Warranty Repair"; + else + if (j.DeviceHeld.HasValue) + return string.Format("Awaiting Warranty Repair ({0})", j.JobMetaWarranty_ExternalName); + else + return string.Format("Awaiting Warranty Repair - Not Held ({0})", j.JobMetaWarranty_ExternalName); + case Job.JobStatusIds.AwaitingRepairs: + if (j == null) + return "Awaiting Repairs"; + else + if (j.DeviceHeld.HasValue) + return string.Format("Awaiting Repairs ({0})", j.JobMetaNonWarranty_RepairerName); + else + return string.Format("Awaiting Repairs - Not Held ({0})", j.JobMetaNonWarranty_RepairerName); + case Job.JobStatusIds.AwaitingDeviceReturn: + return "Awaiting Device Return"; + case Job.JobStatusIds.AwaitingUserAction: + return "Awaiting User Action"; + case Job.JobStatusIds.AwaitingAccountingPayment: + return "Awaiting Accounting Payment"; + case Job.JobStatusIds.AwaitingAccountingCharge: + return "Awaiting Accounting Charge"; + case Job.JobStatusIds.AwaitingInsuranceProcessing: + return "Awaiting Insurance Processing"; + default: + return "Unknown"; + } + } + + } +} diff --git a/Disco.BI/BI/UserBI/Searching.cs b/Disco.BI/BI/UserBI/Searching.cs new file mode 100644 index 00000000..9ced0649 --- /dev/null +++ b/Disco.BI/BI/UserBI/Searching.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Models.BI.Search; +using Disco.Models.Repository; +using Disco.Data.Repository; + +namespace Disco.BI.UserBI +{ + public static class Searching + { + + public static List SearchUpstream(string Term) + { + return Interop.ActiveDirectory.ActiveDirectory.SearchUsers(Term).Select(adU => adU.ToRepositoryUser()).ToList(); + } + + private static List Search_SelectUserSearchResultItems(IQueryable Query, int? LimitCount = null) + { + if (LimitCount.HasValue) + Query = Query.Take(LimitCount.Value); + + return Query.Select(u => new UserSearchResultItem() + { + Id = u.Id, + Surname = u.Surname, + GivenName = u.GivenName, + DisplayName = u.DisplayName, + AssignedDevicesCount = u.DeviceUserAssignments.Where(dua => !dua.UnassignedDate.HasValue).Count(), + JobCount = u.Jobs.Count() + }).ToList(); + } + + public static List Search(DiscoDataContext dbContext, string Term, int? LimitCount = null) + { + if (string.IsNullOrWhiteSpace(Term) || Term.Length < 2) + throw new ArgumentException("Search Term must contain at least two characters", "Term"); + + // Search Active Directory & Import Relevant Users + var adImportedUsers = Interop.ActiveDirectory.ActiveDirectory.SearchUsers(Term).Select(adU => adU.ToRepositoryUser()); + foreach (var adU in adImportedUsers) + { + var existingUser = dbContext.Users.Find(adU.Id); + if (existingUser != null) + existingUser.UpdateSelf(adU); + else + dbContext.Users.Add(adU); + dbContext.SaveChanges(); + UserCache.InvalidateValue(adU.Id); + } + + return Search_SelectUserSearchResultItems(dbContext.Users.Where(u => + u.Id.Contains(Term) || + u.Surname.Contains(Term) || + u.GivenName.Contains(Term) || + u.DisplayName.Contains(Term) + ), LimitCount); + } + } +} diff --git a/Disco.BI/BI/UserBI/UserCache.cs b/Disco.BI/BI/UserBI/UserCache.cs new file mode 100644 index 00000000..fe145b43 --- /dev/null +++ b/Disco.BI/BI/UserBI/UserCache.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Collections.Concurrent; +using Disco.Models.Repository; +using Disco.Data.Repository; +using System.Web; +using Quartz; +using Quartz.Impl; +using Disco.Services.Tasks; + +namespace Disco.BI.UserBI +{ + public class UserCache : ScheduledTask + { + private static ConcurrentDictionary> _Cache = new ConcurrentDictionary>(); + private const long CacheTimeoutTicks = 6000000000; // 10 Minutes + private const string CacheHttpRequestKey = "Disco_CurrentUser"; + + public static User CurrentUser + { + get + { + string username = null; + User user; + + // Check for ASP.NET + if (HttpContext.Current != null) + { + if (HttpContext.Current.Request.IsAuthenticated) + { + user = (User)HttpContext.Current.Items[CacheHttpRequestKey]; + if (user != null) + return user; + + username = HttpContext.Current.User.Identity.Name; + } + else + { + return null; + //throw new PlatformNotSupportedException("ASP.NET Authentication is not correctly configured"); + } + } + + // User default User + if (username == null) + { + username = System.Security.Principal.WindowsIdentity.GetCurrent().Name; + } + + user = GetUser(username); + + if (HttpContext.Current != null && HttpContext.Current.Request.IsAuthenticated) + { + // Cache in current request + HttpContext.Current.Items[CacheHttpRequestKey] = user; + } + + return user; + } + } + + public static User GetUser(string Username) + { + // Check Cache + User u = TryUserCache(Username); + + if (u == null) + { + // Load from Repository + using (DiscoDataContext dbContext = new DiscoDataContext()) + { + u = GetUser(Username, dbContext, true); + } + } + return u; + } + + public static User GetUser(string Username, DiscoDataContext dbContext, bool ForceRefresh = false) + { + User u = null; + + // Check Cache + if (!ForceRefresh) + u = TryUserCache(Username); + + if (u == null) + { + string username = Username.ToLower(); + u = UserBI.Utilities.LoadUser(dbContext, username); + SetValue(username, u); + } + return u; + } + + private static User TryUserCache(string Username) + { + string username = Username.ToLower(); + Tuple userRecord; + if (_Cache.TryGetValue(username, out userRecord)) + { + if (userRecord.Item2 > DateTime.Now) + return userRecord.Item1; + else + _Cache.TryRemove(username, out userRecord); + } + return null; + } + + public static bool InvalidateValue(string Key) + { + Tuple userRecord; + return _Cache.TryRemove(Key.ToLower(), out userRecord); + } + + private static bool SetValue(string Key, User User) + { + string key = Key.ToLower(); + Tuple userRecord = new Tuple(User, DateTime.Now.AddTicks(CacheTimeoutTicks)); + if (_Cache.ContainsKey(key)) + { + Tuple oldUser; + if (_Cache.TryGetValue(key, out oldUser)) + { + return _Cache.TryUpdate(key, userRecord, oldUser); + } + } + return _Cache.TryAdd(key, userRecord); + } + + private static void CleanStaleCache() + { + var usernames = _Cache.Keys.ToArray(); + foreach (string username in usernames) + { + Tuple userRecord; + if (_Cache.TryGetValue(username, out userRecord)) + { + if (userRecord.Item2 <= DateTime.Now) + _Cache.TryRemove(username, out userRecord); + } + } + } + + //public void InitalizeScheduledTask(DiscoDataContext dbContext, IScheduler Scheduler) + //{ + // // Run @ every 15mins + + // // Next 15min interval + // DateTime now = DateTime.Now; + // int mins = (15 - (now.Minute % 15)); + // if (mins < 10) + // mins += 15; + // DateTimeOffset startAt = new DateTimeOffset(now).AddMinutes(mins).AddSeconds(now.Second * -1).AddMilliseconds(now.Millisecond * -1); + + // IJobDetail jobDetail = new JobDetailImpl("UserCache_CleanStaleCache", typeof(UserCache)); + // ITrigger trigger = TriggerBuilder.Create(). + // WithIdentity("UserCache_CleanStaleCacheTrigger").StartAt(startAt). + // WithSchedule(SimpleScheduleBuilder.RepeatMinutelyForever(15)). + // Build(); + // Scheduler.ScheduleJob(jobDetail, trigger); + //} + + public override string TaskName { get { return "User Cache - Clean Stale Cache"; } } + + public override bool SingleInstanceTask { get { return true; } } + public override bool CancelInitiallySupported { get { return false; } } + public override bool LogExceptionsOnly { get { return true; } } + + public override void InitalizeScheduledTask(DiscoDataContext dbContext) + { + // Run @ every 15mins + + // Next 15min interval + DateTime now = DateTime.Now; + int mins = (15 - (now.Minute % 15)); + if (mins < 10) + mins += 15; + DateTimeOffset startAt = new DateTimeOffset(now).AddMinutes(mins).AddSeconds(now.Second * -1).AddMilliseconds(now.Millisecond * -1); + + TriggerBuilder triggerBuilder = TriggerBuilder.Create().StartAt(startAt). + WithSchedule(SimpleScheduleBuilder.RepeatMinutelyForever(15)); + + this.ScheduleTask(triggerBuilder); + } + + protected override void ExecuteTask() + { + CleanStaleCache(); + } + } +} diff --git a/Disco.BI/BI/UserBI/Utilities.cs b/Disco.BI/BI/UserBI/Utilities.cs new file mode 100644 index 00000000..7839071f --- /dev/null +++ b/Disco.BI/BI/UserBI/Utilities.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Models.Repository; +using Disco.Data.Repository; +using Disco.Models.BI.Search; +using System.Runtime.InteropServices; +using System.DirectoryServices.ActiveDirectory; +using Disco.Services.Logging; + +namespace Disco.BI.UserBI +{ + public static class Utilities + { + + public static User LoadUser(DiscoDataContext dbContext, string Username) + { + // Machine Account ? + if (Username.EndsWith("$")) + { + return Interop.ActiveDirectory.ActiveDirectory.GetMachineAccount(Username).ToRepositoryUser(); + } + + // User Account + User user = null; + try + { + var ADUser = Interop.ActiveDirectory.ActiveDirectory.GetUserAccount(Username); + if (ADUser == null) + throw new ArgumentException(string.Format("Invalid Username: '{0}'", Username), "Username"); + user = ADUser.ToRepositoryUser(); + } + catch (COMException ex) + { + // If "Server is not operational" then Try Cache + if (ex.ErrorCode != -2147016646) + { + throw ex; + } + SystemLog.LogException("Primary Domain Controller Down? Disco.BI.UserBI.Utilities.LoadUser", ex); + } + catch (ActiveDirectoryOperationException ex) + { + // Try From Cache... + SystemLog.LogException("Primary Domain Controller Down? Disco.BI.UserBI.Utilities.LoadUser", ex); + } + + // Update Repository + User existingUser; + if (user == null) + { + string username = Username.Contains(@"\") ? Username.Substring(Username.IndexOf(@"\") + 1) : Username; + existingUser = dbContext.Users.Find(username); + if (existingUser == null) + throw new ArgumentException(string.Format("Invalid User - Not In Disco DB: '{0}'", Username), "Username"); + else + return existingUser; + } + existingUser = dbContext.Users.Find(user.Id); + if (existingUser == null) + { + dbContext.Users.Add(user); + } + else + { + existingUser.UpdateSelf(user); + user = existingUser; + } + dbContext.SaveChanges(); + + return user; + } + + } +} diff --git a/Disco.BI/BI/Wireless/BaseWirelessProvider.cs b/Disco.BI/BI/Wireless/BaseWirelessProvider.cs new file mode 100644 index 00000000..dc4efa20 --- /dev/null +++ b/Disco.BI/BI/Wireless/BaseWirelessProvider.cs @@ -0,0 +1,83 @@ +using Disco.BI.Wireless.eduSTAR; +using Disco.Data.Configuration; +using Disco.Data.Repository; +using Disco.BI.Extensions; +using Disco.Models.Repository; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Threading; +namespace Disco.BI.Wireless +{ + public abstract class BaseWirelessProvider + { + protected DiscoDataContext dbContext; + private static object _CertificateAllocateLock = System.Runtime.CompilerServices.RuntimeHelpers.GetObjectValue(new object()); + public static BaseWirelessProvider GetProvider(DiscoDataContext dbContext) + { + string provider = dbContext.DiscoConfiguration.Wireless.Provider; + if (provider == "eduSTAR") + { + return new eduSTARWirelessProvider(dbContext); + } + throw new System.NotSupportedException(string.Format("Wireless Provider Not Supported: '{0}'", dbContext.DiscoConfiguration.Wireless.Provider)); + } + protected BaseWirelessProvider(DiscoDataContext dbContext) + { + this.dbContext = dbContext; + } + private DeviceCertificate CertificateAllocate(ref Device repoDevice) + { + lock (BaseWirelessProvider._CertificateAllocateLock) + { + this.FillCertificateAutoBuffer(); + int timeout = 60; + int freeCertCount = this.dbContext.DeviceCertificates.Where(c => c.DeviceSerialNumber == null && c.Enabled).Count(); + while (!(freeCertCount > 0 | timeout <= 0)) + { + System.Threading.Thread.Sleep(500); + freeCertCount = this.dbContext.DeviceCertificates.Where(c => c.DeviceSerialNumber == null && c.Enabled).Count(); + timeout--; + } + DeviceCertificate cert = this.dbContext.DeviceCertificates.Where(c => c.DeviceSerialNumber == null && c.Enabled).FirstOrDefault(); + if (cert == null) + { + WirelessCertificatesLog.LogAllocationFailed(repoDevice.SerialNumber); + throw new System.InvalidOperationException("Unable to Allocate a Wireless Certificate"); + } + WirelessCertificatesLog.LogAllocated(cert.Name, repoDevice.SerialNumber); + cert.DeviceSerialNumber = repoDevice.SerialNumber; + cert.AllocatedDate = System.DateTime.Now; + this.dbContext.SaveChanges(); + return cert; + } + } + public DeviceCertificate Enrol(Device repoDevice) + { + DeviceCertificate allocatedCert = this.dbContext.DeviceCertificates.Where(c => c.DeviceSerialNumber == repoDevice.SerialNumber && c.Enabled).FirstOrDefault(); + if (allocatedCert != null) + { + return allocatedCert; + } + + // Removed 2012-06-14 G# - Properties moved to DeviceProfile model & DB Migrated in DBv3. + //if (repoDevice.DeviceProfile.Configuration(this.dbContext).AllocateWirelessCertificate) + if (repoDevice.DeviceProfile.AllocateCertificate) + { + allocatedCert = this.CertificateAllocate(ref repoDevice); + return allocatedCert; + } + else + { + return null; + } + } + protected abstract void FillCertificateAutoBuffer(); + public abstract void FillCertificateBuffer(int Amount); + public abstract System.Collections.Generic.List RemoveExistingCertificateNames(); + } +} diff --git a/Disco.BI/BI/Wireless/WirelessCertificatesLog.cs b/Disco.BI/BI/Wireless/WirelessCertificatesLog.cs new file mode 100644 index 00000000..78ab679e --- /dev/null +++ b/Disco.BI/BI/Wireless/WirelessCertificatesLog.cs @@ -0,0 +1,304 @@ +using Disco.Logging; +using Disco.Logging.Models; +using System; +using System.Collections.Generic; +using System.Diagnostics; +namespace Disco.BI.Wireless +{ + public class WirelessCertificatesLog : LogBase + { + public enum EventTypeIds + { + RetrievalStarting = 10, + RetrievalProgress, + RetrievalFinished, + RetrievalWarning = 15, + RetrievalError, + RetrievalCertificateStarting = 20, + RetrievalCertificateFinished = 22, + RetrievalCertificateWarning = 25, + RetrievalCertificateError, + Allocated = 40, + AllocationFailed = 50 + } + private const int _ModuleId = 60; + private static bool _IsCertificateRetrievalProcessing; + private static string _CertificateRetrievalStatus; + private static int _CertificateRetrievalProgress; + public static WirelessCertificatesLog Current + { + get + { + return (WirelessCertificatesLog)LogContext.LogModules[60]; + } + } + public static bool IsCertificateRetrievalProcessing + { + get + { + return WirelessCertificatesLog._IsCertificateRetrievalProcessing; + } + } + public override string ModuleDescription + { + get + { + return "Wireless Certificates"; + } + } + public override int ModuleId + { + get + { + return 60; + } + } + public override string ModuleName + { + get + { + return "WirelessCertificates"; + } + } + [System.Diagnostics.DebuggerNonUserCode] + public WirelessCertificatesLog() + { + } + private static void Log(WirelessCertificatesLog.EventTypeIds EventTypeId, params object[] Args) + { + WirelessCertificatesLog.Current.Log((int)EventTypeId, Args); + } + public static void LogRetrievalStarting(int CertificateCount, int CertificateIdFrom, int CertificateIdTo) + { + WirelessCertificatesLog.Log(WirelessCertificatesLog.EventTypeIds.RetrievalStarting, new object[] + { + CertificateCount, + CertificateIdFrom, + CertificateIdTo + }); + } + public static void LogRetrievalFinished() + { + WirelessCertificatesLog.Log(WirelessCertificatesLog.EventTypeIds.RetrievalFinished, new object[0]); + } + public static void LogRetrievalWarning(string Message) + { + WirelessCertificatesLog.Log(WirelessCertificatesLog.EventTypeIds.RetrievalWarning, new object[] + { + Message + }); + } + public static void LogRetrievalError(string Message) + { + WirelessCertificatesLog.Log(WirelessCertificatesLog.EventTypeIds.RetrievalError, new object[] + { + Message + }); + } + public static void LogRetrievalCertificateStarting(string CertificateId) + { + WirelessCertificatesLog.Log(WirelessCertificatesLog.EventTypeIds.RetrievalCertificateStarting, new object[] + { + CertificateId + }); + } + public static void LogRetrievalCertificateFinished(string CertificateId) + { + WirelessCertificatesLog.Log(WirelessCertificatesLog.EventTypeIds.RetrievalCertificateFinished, new object[] + { + CertificateId + }); + } + public static void LogRetrievalCertificateWarning(string CertificateId, string Message) + { + WirelessCertificatesLog.Log(WirelessCertificatesLog.EventTypeIds.RetrievalCertificateWarning, new object[] + { + CertificateId, + Message + }); + } + public static void LogRetrievalCertificateError(string CertificateId, string Message) + { + WirelessCertificatesLog.Log(WirelessCertificatesLog.EventTypeIds.RetrievalCertificateError, new object[] + { + CertificateId, + Message + }); + } + public static void LogAllocated(string CertificateId, string DeviceSerialNumber) + { + WirelessCertificatesLog.Log(WirelessCertificatesLog.EventTypeIds.Allocated, new object[] + { + CertificateId, + DeviceSerialNumber + }); + } + public static void LogAllocationFailed(string DeviceSerialNumber) + { + WirelessCertificatesLog.Log(WirelessCertificatesLog.EventTypeIds.AllocationFailed, new object[] + { + DeviceSerialNumber + }); + } + public static void LogCertificateRetrievalProgress(bool? IsProcessing, int? Progress, string Status) + { + bool flag = IsProcessing.HasValue; + if (flag) + { + WirelessCertificatesLog._IsCertificateRetrievalProcessing = IsProcessing.Value; + } + flag = WirelessCertificatesLog._IsCertificateRetrievalProcessing; + if (flag) + { + bool flag2 = Status != null; + if (flag2) + { + WirelessCertificatesLog._CertificateRetrievalStatus = Status; + } + flag2 = Progress.HasValue; + if (flag2) + { + WirelessCertificatesLog._CertificateRetrievalProgress = Progress.Value; + } + } + else + { + WirelessCertificatesLog._CertificateRetrievalStatus = null; + WirelessCertificatesLog._CertificateRetrievalProgress = 0; + } + WirelessCertificatesLog.Log(WirelessCertificatesLog.EventTypeIds.RetrievalProgress, new object[] + { + WirelessCertificatesLog._IsCertificateRetrievalProcessing, + WirelessCertificatesLog._CertificateRetrievalProgress, + WirelessCertificatesLog._CertificateRetrievalStatus + }); + } + protected override System.Collections.Generic.List LoadEventTypes() + { + return new System.Collections.Generic.List + { + new LogEventType + { + Id = 10, + ModuleId = 60, + Name = "Retrieval Starting", + Format = "Starting retrieval of {0} certificate/s ({1} to {2})", + Severity = 0, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 11, + ModuleId = 60, + Name = "Retrieval Progress", + Format = "Processing: {0}; {1}% Complete; Status: {2}", + Severity = 0, + UseLive = true, + UsePersist = false, + UseDisplay = false + }, + new LogEventType + { + Id = 12, + ModuleId = 60, + Name = "Retrieval Finished", + Format = "Retrieval of Certificates Complete", + Severity = 0, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 15, + ModuleId = 60, + Name = "Retrieval Warning", + Format = "Retrieval Warning: {0}", + Severity = 1, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 16, + ModuleId = 60, + Name = "Retrieval Error", + Format = "Retrieval Error: {0}", + Severity = 2, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 20, + ModuleId = 60, + Name = "Retrieval Certificate Starting", + Format = "Retrieving Certificate: {0}", + Severity = 0, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 22, + ModuleId = 60, + Name = "Retrieval Certificate Finished", + Format = "Certificate Retrieved: {0}", + Severity = 0, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 25, + ModuleId = 60, + Name = "Retrieval Certificate Warning", + Format = "{0} Certificate Warning: {1}", + Severity = 1, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 26, + ModuleId = 60, + Name = "Retrieval Certificate Error", + Format = "{0} Certificate Error: {1}", + Severity = 2, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 40, + ModuleId = 60, + Name = "Allocated", + Format = "Certificate {0} allocated to {1}", + Severity = 0, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 50, + ModuleId = 60, + Name = "Allocation Failed", + Format = "No certificates available for Device: {0}", + Severity = 2, + UseLive = true, + UsePersist = true, + UseDisplay = true + } + }; + } + } +} diff --git a/Disco.BI/BI/Wireless/eduSTAR/eduSTARWirelessProvider.cs b/Disco.BI/BI/Wireless/eduSTAR/eduSTARWirelessProvider.cs new file mode 100644 index 00000000..fa30817f --- /dev/null +++ b/Disco.BI/BI/Wireless/eduSTAR/eduSTARWirelessProvider.cs @@ -0,0 +1,283 @@ +using Disco.BI.Wireless.eduSTAR.eduSTARWirelessCertService; +using Disco.Data.Repository; +using Disco.Models.Repository; +using Ionic.Zip; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Security.Cryptography.X509Certificates; +using System.ServiceModel; +using System.ServiceModel.Channels; +using System.Threading; + +namespace Disco.BI.Wireless.eduSTAR +{ + public class eduSTARWirelessProvider : BaseWirelessProvider + { + private class BulkLoadCertificatesContract + { + public int Start { get; set; } + public int Count { get; set; } + } + private static object _BulkLoadThreadLock = new object(); + private static System.Threading.Thread _BulkLoadThread; + public eduSTARWirelessProvider(DiscoDataContext dbContext) + : base(dbContext) + { + } + protected override void FillCertificateAutoBuffer() + { + int freeCertCount = this.dbContext.DeviceCertificates.Where(c => c.DeviceSerialNumber == null && c.Enabled).Count(); + if (freeCertCount <= this.dbContext.DiscoConfiguration.Wireless.CertificateAutoBufferLow) + { + this.BulkLoadCertificates(0); + } + } + public override void FillCertificateBuffer(int Amount) + { + this.BulkLoadCertificates(Amount); + } + public override System.Collections.Generic.List RemoveExistingCertificateNames() + { + return new System.Collections.Generic.List + { + "(eduPaSS)", + "(CN=Computers, ?DC=services, ?DC=education, ?DC=vic, ?DC=gov, ?DC=au)" + }; + } + private void BulkLoadCertificates(int Amount = 0) + { + if (eduSTARWirelessProvider._BulkLoadThread == null) + { + lock (eduSTARWirelessProvider._BulkLoadThreadLock) + { + if (eduSTARWirelessProvider._BulkLoadThread == null) + { + int start = 0; + if (this.dbContext.DeviceCertificates.Count() > 0) + { + start = this.dbContext.DeviceCertificates.Max(c => c.ProviderIndex) + 1; + } + int buffer = this.dbContext.DeviceCertificates.Count(c => c.DeviceSerialNumber == null && c.Enabled); + int count = this.dbContext.DiscoConfiguration.Wireless.CertificateAutoBufferMax - buffer; + if (Amount > 0) + { + count = Amount; + } + if (count > 0) + { + eduSTARWirelessProvider.BulkLoadCertificatesContract contract = new eduSTARWirelessProvider.BulkLoadCertificatesContract + { + Start = start, + Count = count + }; + System.Threading.ParameterizedThreadStart threadStart = delegate(object a0) + { + this.BulkLoadCertificatesStart((eduSTARWirelessProvider.BulkLoadCertificatesContract)a0); + } + ; + eduSTARWirelessProvider._BulkLoadThread = new System.Threading.Thread(threadStart); + eduSTARWirelessProvider._BulkLoadThread.Start(contract); + } + } + } + } + } + private void BulkLoadCertificatesStart(eduSTARWirelessProvider.BulkLoadCertificatesContract contract) + { + try + { + WirelessCertificatesLog.LogRetrievalStarting(contract.Count, contract.Start, contract.Start + contract.Count - 1); + WirelessCertificatesLog.LogCertificateRetrievalProgress(true, 0, string.Format("Starting Bulk Retrieval (Loading {0} Certificate/s)", contract.Count)); + DiscoDataContext dbLocalContext = new DiscoDataContext(); + try + { + WirelessCertServiceSoapClient proxy = this.GetProxy(); + try + { + int num = contract.Start + contract.Count - 1; + int index = contract.Start; + while (true) + { + int num2 = num; + if (index > num2) + { + break; + } + WirelessCertificatesLog.LogCertificateRetrievalProgress(true, (int)System.Math.Round(unchecked(((double)checked(index - contract.Start) + 0.5) / (double)contract.Count * 100.0)), string.Format("Retrieving Certificate {0} of {1}", index - contract.Start + 1, contract.Count)); + DeviceCertificate cert = this.LoadCertificate(index, proxy, dbLocalContext); + dbLocalContext.DeviceCertificates.Add(cert); + dbLocalContext.SaveChanges(); + WirelessCertificatesLog.LogRetrievalCertificateFinished(cert.Name); + index++; + } + } + finally + { + bool flag = proxy != null; + if (flag) + { + ((System.IDisposable)proxy).Dispose(); + } + } + } + finally + { + bool flag = dbLocalContext != null; + if (flag) + { + ((System.IDisposable)dbLocalContext).Dispose(); + } + } + } + catch (System.Exception ex) + { + WirelessCertificatesLog.LogRetrievalError(string.Format("[{0}] {1}", ex.GetType().Name, ex.Message)); + throw ex; + } + finally + { + lock (eduSTARWirelessProvider._BulkLoadThreadLock) + { + eduSTARWirelessProvider._BulkLoadThread = null; + } + WirelessCertificatesLog.LogRetrievalFinished(); + WirelessCertificatesLog.LogCertificateRetrievalProgress(false, null, null); + } + } + private DeviceCertificate LoadCertificate(int Index, DiscoDataContext dbContext) + { + DeviceCertificate LoadCertificate; + try + { + WirelessCertServiceSoapClient proxy = this.GetProxy(); + try + { + LoadCertificate = this.LoadCertificate(Index, proxy, dbContext); + } + finally + { + bool flag = proxy != null; + if (flag) + { + ((System.IDisposable)proxy).Dispose(); + } + } + } + catch (System.Exception ex) + { + WirelessCertificatesLog.LogRetrievalCertificateError(Index.ToString(), string.Format("[{0}] {1}", ex.GetType().Name, ex.Message)); + throw ex; + } + return LoadCertificate; + } + private DeviceCertificate LoadCertificate(int Index, WirelessCertServiceSoapClient Proxy, DiscoDataContext dbContext) + { + bool flag = string.IsNullOrWhiteSpace(dbContext.DiscoConfiguration.Wireless.eduSTAR_ServiceAccountSchoolId); + if (flag) + { + throw new System.ArgumentException("Wireless Certificates: Invalid ServiceAccount SchoolId"); + } + flag = string.IsNullOrWhiteSpace(dbContext.DiscoConfiguration.Wireless.eduSTAR_ServiceAccountUsername); + if (flag) + { + throw new System.ArgumentException("Wireless Certificates: Invalid ServiceAccount Username"); + } + flag = string.IsNullOrWhiteSpace(dbContext.DiscoConfiguration.Wireless.eduSTAR_ServiceAccountPassword); + if (flag) + { + throw new System.ArgumentException("Wireless Certificates: Invalid ServiceAccount Password"); + } + DeviceCertificate cert = new DeviceCertificate + { + ProviderIndex = Index, + Name = string.Format("{0}-{1}", dbContext.DiscoConfiguration.Wireless.eduSTAR_ServiceAccountSchoolId, Index.ToString("00000")), + Enabled = true + }; + WirelessCertificatesLog.LogRetrievalCertificateStarting(cert.Name); + string response; + try + { + response = Proxy.GetWirelessCert(dbContext.DiscoConfiguration.Wireless.eduSTAR_ServiceAccountSchoolId, cert.Name, "password", dbContext.DiscoConfiguration.Wireless.eduSTAR_ServiceAccountUsername, dbContext.DiscoConfiguration.Wireless.eduSTAR_ServiceAccountPassword); + } + catch (System.Exception ex) + { + WirelessCertificatesLog.LogRetrievalCertificateError(cert.Name, ex.Message); + throw ex; + } + try + { + byte[] responseBytes = System.Convert.FromBase64String(response); + System.IO.MemoryStream responseByteStream = new System.IO.MemoryStream(responseBytes); + try + { + ZipFile responseZip = ZipFile.Read(responseByteStream); + ZipEntry certFile = responseZip.FirstOrDefault((ZipEntry ze) => ze.FileName.EndsWith(".pfx", System.StringComparison.InvariantCultureIgnoreCase)); + System.IO.MemoryStream certByteStream = new System.IO.MemoryStream(); + try + { + certFile.Extract(certByteStream); + cert.Content = certByteStream.ToArray(); + } + finally + { + flag = (certByteStream != null); + if (flag) + { + ((System.IDisposable)certByteStream).Dispose(); + } + } + } + finally + { + flag = (responseByteStream != null); + if (flag) + { + ((System.IDisposable)responseByteStream).Dispose(); + } + } + } + catch (System.Exception ex2) + { + if (response.Contains("Computer with this name already exists")) + { + WirelessCertificatesLog.LogRetrievalCertificateWarning(cert.Name, "Already exists on eduSTAR server, disabling and skipping."); + cert.ExpirationDate = System.DateTime.Now; + cert.Enabled = false; + cert.Content = null; + return cert; + } + throw new System.InvalidOperationException(string.Format("Unable to Uncompress (Server returned: {0})", response), ex2); + } + try + { + X509Certificate2 x509Cert = new X509Certificate2(cert.Content, "password"); + cert.ExpirationDate = x509Cert.NotAfter; + } + catch (System.Exception ex3) + { + throw new System.InvalidOperationException("Invalid Certificate returned by Server", ex3); + } + return cert; + } + private WirelessCertServiceSoapClient GetProxy() + { + BasicHttpBinding binding = new BasicHttpBinding(); + + // Don't Use Proxy + binding.UseDefaultWebProxy = false; + binding.ProxyAddress = null; + + binding.Security.Mode = BasicHttpSecurityMode.Transport; + binding.MaxReceivedMessageSize = 524288L; + binding.ReaderQuotas.MaxStringContentLength = 524288; + EndpointAddress endpointAddress = new EndpointAddress(new Uri("https://www.eduweb.vic.gov.au/edustar/WirelessCertWS/wirelesscertws.asmx"), new AddressHeader[0]); + return new WirelessCertServiceSoapClient(binding, endpointAddress); + } + } +} diff --git a/Disco.BI/Disco.BI.csproj b/Disco.BI/Disco.BI.csproj new file mode 100644 index 00000000..9d5753ed --- /dev/null +++ b/Disco.BI/Disco.BI.csproj @@ -0,0 +1,233 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {095E6F94-3C34-47AE-BB83-46203535E0F6} + Library + Properties + Disco + Disco.BI + v4.5 + 512 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + ..\..\Resources\Libraries\LibTiff.NET\BitMiracle.LibTiff.NET.dll + + + ..\packages\EntityFramework.5.0.0\lib\net45\EntityFramework.dll + + + ..\..\Resources\Libraries\DotNetZip\Ionic.Zip.Reduced.dll + + + ..\..\Resources\Libraries\iTextSharp\itextsharp.dll + + + True + ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll + + + False + ..\packages\Newtonsoft.Json.4.5.11\lib\net40\Newtonsoft.Json.dll + + + False + ..\..\Resources\Libraries\Quartz\Quartz.dll + + + False + ..\packages\SignalR.Server.0.5.3\lib\net40\SignalR.dll + + + False + ..\packages\SignalR.Hosting.AspNet.0.5.3\lib\net45\SignalR.Hosting.AspNet.dll + + + ..\packages\SignalR.Hosting.Common.0.5.3\lib\net40\SignalR.Hosting.Common.dll + + + ..\..\Resources\Libraries\Spring.NET\Spring.Core.dll + + + + + + + + + + + + + + + + + + ..\..\Resources\Libraries\SharpSSH\Tamir.SharpSSH.dll + + + ..\..\Resources\Libraries\ZXing\zxing.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + Resources.resx + + + + + {85A6BD19-2C64-4746-8F2C-A68A86E8C2D7} + Disco.Data + + + {FBC05512-FCCA-4B16-9E76-8C413C5DE6C9} + Disco.Models + + + {B80A737F-BD6A-4986-9182-DD7B932BD950} + Disco.Services + + + + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Disco.BI/Properties/AssemblyInfo.cs b/Disco.BI/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..038f7cae --- /dev/null +++ b/Disco.BI/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("Disco - Business Intelligence")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Disco")] +[assembly: AssemblyCopyright("")] +[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("01887659-9fd6-4464-8493-cf0506ee2569")] + +// 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.2.0131.2002")] +[assembly: AssemblyFileVersion("1.2.0131.2002")] diff --git a/Disco.BI/Properties/Resources.Designer.cs b/Disco.BI/Properties/Resources.Designer.cs new file mode 100644 index 00000000..0f01f133 --- /dev/null +++ b/Disco.BI/Properties/Resources.Designer.cs @@ -0,0 +1,113 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.17929 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Disco.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Disco.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap MimeType_doc48 { + get { + object obj = ResourceManager.GetObject("MimeType_doc48", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap MimeType_img16 { + get { + object obj = ResourceManager.GetObject("MimeType_img16", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap MimeType_pdf16 { + get { + object obj = ResourceManager.GetObject("MimeType_pdf16", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap MimeType_pdf48 { + get { + object obj = ResourceManager.GetObject("MimeType_pdf48", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap MimeType_unknown48 { + get { + object obj = ResourceManager.GetObject("MimeType_unknown48", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + } +} diff --git a/Disco.BI/Properties/Resources.resx b/Disco.BI/Properties/Resources.resx new file mode 100644 index 00000000..7d09641a --- /dev/null +++ b/Disco.BI/Properties/Resources.resx @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\Resources\MimeType-doc48.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\MimeType-img16.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\MimeType-pdf16.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\MimeType-pdf48.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + ..\Resources\MimeType-unknown48.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/Disco.BI/Resources/EmptyLogo.png b/Disco.BI/Resources/EmptyLogo.png new file mode 100644 index 00000000..766f006f Binary files /dev/null and b/Disco.BI/Resources/EmptyLogo.png differ diff --git a/Disco.BI/Resources/MimeType-doc48.png b/Disco.BI/Resources/MimeType-doc48.png new file mode 100644 index 00000000..cf7caee9 Binary files /dev/null and b/Disco.BI/Resources/MimeType-doc48.png differ diff --git a/Disco.BI/Resources/MimeType-img16.png b/Disco.BI/Resources/MimeType-img16.png new file mode 100644 index 00000000..60d7f35b Binary files /dev/null and b/Disco.BI/Resources/MimeType-img16.png differ diff --git a/Disco.BI/Resources/MimeType-pdf16.png b/Disco.BI/Resources/MimeType-pdf16.png new file mode 100644 index 00000000..9d115de1 Binary files /dev/null and b/Disco.BI/Resources/MimeType-pdf16.png differ diff --git a/Disco.BI/Resources/MimeType-pdf48.png b/Disco.BI/Resources/MimeType-pdf48.png new file mode 100644 index 00000000..34f1b26c Binary files /dev/null and b/Disco.BI/Resources/MimeType-pdf48.png differ diff --git a/Disco.BI/Resources/MimeType-unknown48.png b/Disco.BI/Resources/MimeType-unknown48.png new file mode 100644 index 00000000..ccbf7b3a Binary files /dev/null and b/Disco.BI/Resources/MimeType-unknown48.png differ diff --git a/Disco.BI/app.config b/Disco.BI/app.config new file mode 100644 index 00000000..8fd131a9 --- /dev/null +++ b/Disco.BI/app.config @@ -0,0 +1,21 @@ + + + + +
+ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Disco.BI/packages.config b/Disco.BI/packages.config new file mode 100644 index 00000000..943dade1 --- /dev/null +++ b/Disco.BI/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/Disco.Client/App.config b/Disco.Client/App.config new file mode 100644 index 00000000..84bc4207 --- /dev/null +++ b/Disco.Client/App.config @@ -0,0 +1,6 @@ + + + + + + diff --git a/Disco.Client/Disco.Client.csproj b/Disco.Client/Disco.Client.csproj new file mode 100644 index 00000000..c4c1cb70 --- /dev/null +++ b/Disco.Client/Disco.Client.csproj @@ -0,0 +1,138 @@ + + + + + Debug + AnyCPU + {D6B85A86-0FAA-4B04-BC9E-D01A08C03387} + Exe + Properties + Disco.Client + Disco.Client + v4.0 + 512 + Client + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + Disco.Client.Program + + + OnOutputUpdated + + + Icon.ico + + + + Properties\app.manifest + + + + False + ..\packages\Newtonsoft.Json.4.5.11\lib\net40\Newtonsoft.Json.dll + + + + + + + + + + + + + + Models\ClientServices\Enrol.cs + + + Models\ClientServices\EnrolResponse.cs + + + Models\ClientServices\MacEnrol.cs + + + Models\ClientServices\MacEnrolResponse.cs + + + Models\ClientServices\MacSecureEnrolResponse.cs + + + Models\ClientServices\ServiceBase.cs + + + Models\ClientServices\WhoAmI.cs + + + Models\ClientServices\WhoAmIResponse.cs + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + + PreserveNewest + + + + + + + + + + + + + + + DEL "$(ProjectDir)Package Creation\PreparationClient.zip" +"$(ProjectDir)Package Creation\7z.exe" a -tzip "$(ProjectDir)Package Creation\PreparationClient.zip" "$(TargetDir)*.*" -x!*.tmp -x!*.vshost.* -x!Newtonsoft.Json.xml -x!*.pdb +COPY "$(ProjectDir)Package Creation\PreparationClient.zip" "$(ProjectDir)..\Disco.Web\ClientBin" + + + \ No newline at end of file diff --git a/Disco.Client/ErrorReporting.cs b/Disco.Client/ErrorReporting.cs new file mode 100644 index 00000000..bb75bc37 --- /dev/null +++ b/Disco.Client/ErrorReporting.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Text; +using Disco.Client.Extensions; +using Newtonsoft.Json; + +namespace Disco.Client +{ + public static class ErrorReporting + { + private const string ServicePathTemplate = "http://DISCO:9292/Services/Client/ClientError"; + public static string DeviceIdentifier { get; set; } + public static string EnrolmentSessionId { get; set; } + + public static void ReportError(Exception Ex, bool ReportToServer) + { + bool isClientServiceException = Ex is ClientServiceException; + + ErrorReport report = new ErrorReport() + { + DeviceIdentifier = DeviceIdentifier, + SessionId = EnrolmentSessionId, + JsonException = Ex.IntenseExceptionSerialization() + }; + + try + { + LogToFile(report); + } + catch (Exception) { } + + try + { + LogToEventLog(report); + } + catch (Exception) { } + + // Don't log server errors back to the server + if (!isClientServiceException && ReportToServer) + { + try + { + LogToServer(report); + } + catch (Exception) { } + } + + try + { + Presentation.WriteFatalError(Ex); + } + catch (Exception) { } + } + + #region Log Actions + + private static void LogToFile(ErrorReport report) + { + var logPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "ErrorLog.csv"); + + using (var streamWriter = File.AppendText(logPath)) + { + streamWriter.Write(DateTime.Now.ToString("s")); + streamWriter.Write(","); + streamWriter.Write(report.DeviceIdentifier); + streamWriter.Write(",\""); + streamWriter.Write(report.JsonException); + streamWriter.Write("\""); + streamWriter.Flush(); + } + } + private static void LogToEventLog(ErrorReport report) + { + string eventSource = "Disco Client"; + + if (!EventLog.SourceExists(eventSource)) + EventLog.CreateEventSource(eventSource, "Application"); + + EventLog.WriteEntry(eventSource, report.JsonException, EventLogEntryType.Error, 400); + } + private static void LogToServer(ErrorReport report) + { + string reportJson = JsonConvert.SerializeObject(report); + string reportResponse; + + HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(ServicePathTemplate); + request.UserAgent = string.Format("Disco-Client/{0}", Assembly.GetExecutingAssembly().GetName().Version.ToString(3)); + request.ContentType = "application/json"; + request.Method = WebRequestMethods.Http.Post; + request.UseDefaultCredentials = true; + request.Timeout = 300000; // 5 Minutes + + using (StreamWriter requestWriter = new StreamWriter(request.GetRequestStream())) + { + requestWriter.Write(reportJson); + } + + using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) + { + using (StreamReader responseReader = new StreamReader(response.GetResponseStream())) + { + reportResponse = responseReader.ReadToEnd(); + } + } + + System.Diagnostics.Debug.WriteLine("Error Report Logged to Server; Response: {0}", reportResponse); + } + + #endregion + + public class ErrorReport + { + public string SessionId { get; set; } + public string DeviceIdentifier { get; set; } + public string JsonException { get; set; } + } + + public static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) + { + try + { + var ex = e.ExceptionObject as Exception; + if (ex != null) + { + ReportError(ex, true); + } + } + catch (Exception) + { + // Igore failure to Log Errors + } + } + + #region Exception Serialization + private static string IntenseExceptionSerialization(this Exception Ex) + { + try + { + return JsonConvert.SerializeObject(Ex); + } + catch (Exception) + { + try + { + var encapsulatedEx = Ex.ToEncapsulatedException(); + return JsonConvert.SerializeObject(encapsulatedEx); + } + catch (Exception) + { + try + { + var encapsulatedEx = Ex.ToEncapsulatedException(0); + return JsonConvert.SerializeObject(encapsulatedEx); + } + catch (Exception) + { + return JsonConvert.SerializeObject(Ex.Message); + } + } + } + } + + private static EncapsulatedException ToEncapsulatedException(this Exception ex, int InnerRecursionDepth = 4) + { + EncapsulatedException inner = null; + if (InnerRecursionDepth > 0 && ex.InnerException != null) + inner = ex.InnerException.ToEncapsulatedException(--InnerRecursionDepth); + + return new EncapsulatedException() + { + EncapsulatedType = ex.GetType().Name, + Message = ex.Message, + StackTrace = ex.StackTrace, + InnerException = inner + }; + } + public class EncapsulatedException + { + public string Message { get; set; } + public string EncapsulatedType { get; set; } + public string StackTrace { get; set; } + public EncapsulatedException InnerException { get; set; } + } + #endregion + + } +} diff --git a/Disco.Client/Extensions/ClientServiceException.cs b/Disco.Client/Extensions/ClientServiceException.cs new file mode 100644 index 00000000..ab7dc4c6 --- /dev/null +++ b/Disco.Client/Extensions/ClientServiceException.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Disco.Client.Extensions +{ + public class ClientServiceException : Exception + { + public string ServiceFeature { get; private set; } + + public ClientServiceException(string ServiceFeature, string ServerMessage) : base(ServerMessage) + { + this.ServiceFeature = ServiceFeature; + } + } +} diff --git a/Disco.Client/Extensions/ClientServicesExtensions.cs b/Disco.Client/Extensions/ClientServicesExtensions.cs new file mode 100644 index 00000000..a919e7c2 --- /dev/null +++ b/Disco.Client/Extensions/ClientServicesExtensions.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Disco.Models.ClientServices; +using Newtonsoft.Json; + +namespace Disco.Client.Extensions +{ + public static class ClientServicesExtensions + { + public const string ServicePathAuthenticatedTemplate = "http://DISCO:9292/Services/Client/Authenticated/{0}"; + public const string ServicePathUnauthenticatedTemplate = "http://DISCO:9292/Services/Client/Unauthenticated/{0}"; + + public static ResponseType Post(this ServiceBase Service, bool Authenticated) + { + string jsonResponse; + string serviceUrl; + if (Authenticated) + serviceUrl = string.Format(ServicePathAuthenticatedTemplate, Service.Feature); + else + serviceUrl = string.Format(ServicePathUnauthenticatedTemplate, Service.Feature); + + HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(serviceUrl); + request.UserAgent = string.Format("Disco-Client/{0}", Assembly.GetExecutingAssembly().GetName().Version.ToString(3)); + request.ContentType = "application/json"; + request.Method = WebRequestMethods.Http.Post; + request.UseDefaultCredentials = true; + request.Timeout = 300000; // 5 Minutes + string jsonRequest = JsonConvert.SerializeObject(Service); + + using (StreamWriter requestWriter = new StreamWriter(request.GetRequestStream())) + { + requestWriter.Write(jsonRequest); + } + + using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) + { + using (StreamReader responseReader = new StreamReader(response.GetResponseStream())) + { + jsonResponse = responseReader.ReadToEnd(); + } + } + + if (string.IsNullOrEmpty(jsonResponse)) + return default(ResponseType); + else + return JsonConvert.DeserializeObject(jsonResponse); + } + + } +} diff --git a/Disco.Client/Extensions/EnrolExtensions.cs b/Disco.Client/Extensions/EnrolExtensions.cs new file mode 100644 index 00000000..0ebfc369 --- /dev/null +++ b/Disco.Client/Extensions/EnrolExtensions.cs @@ -0,0 +1,188 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Models.ClientServices; +using System.Security.Cryptography.X509Certificates; +using System.IO; +using System.Diagnostics; +using Microsoft.Win32; +using System.Text.RegularExpressions; + +namespace Disco.Client.Extensions +{ + public static class EnrolExtensions + { + + public static void Build(this Enrol enrol) + { + enrol.DeviceUUID = Interop.SystemAudit.DeviceUUID; + enrol.DeviceSerialNumber = Interop.SystemAudit.DeviceSerialNumber; + + enrol.DeviceComputerName = Interop.LocalAuthentication.ComputerName; + + enrol.DeviceManufacturer = Interop.SystemAudit.DeviceManufacturer; + enrol.DeviceModel = Interop.SystemAudit.DeviceModel; + enrol.DeviceModelType = Interop.SystemAudit.DeviceType; + + enrol.DeviceIsPartOfDomain = Interop.SystemAudit.DeviceIsPartOfDomain; + + // LAN + enrol.DeviceLanMacAddress = Interop.Network.PrimaryLanMacAddress; + + // WAN + enrol.DeviceWlanMacAddress = Interop.Network.PrimaryWlanMacAddress; + + // Certificates + enrol.DeviceCertificates = Interop.Certificates.GetCertificateSubjects(StoreName.My, StoreLocation.LocalMachine); + } + + public static void Process(this EnrolResponse enrolResponse) + { + if (enrolResponse == null) + throw new ClientServiceException("Enrolment", "Server denied enrolment (Empty Response)"); + + ErrorReporting.EnrolmentSessionId = enrolResponse.SessionId; + + if (!string.IsNullOrEmpty(enrolResponse.ErrorMessage)) + throw new ClientServiceException("Enrolment", enrolResponse.ErrorMessage); + + // Offline Domain Join + bool requireReboot = enrolResponse.ApplyOfflineDomainJoin(); + + // Certificates + enrolResponse.ApplyDeviceCertificates(); + + // Device Owner + enrolResponse.ApplyDeviceAssignedUser(); + + + Presentation.UpdateStatus("Enrolling Device", "Device Enrolment Successfully Completed", false, 0, 1500); + + Program.RebootRequired = requireReboot; + Program.AllowUninstall = enrolResponse.AllowBootstrapperUninstall; + } + + /// + /// Processes a Client Service Enrol Response for Offline Domain Join Actions + /// + /// + /// Boolean indicating whether a reboot is required. + private static bool ApplyOfflineDomainJoin(this EnrolResponse enrolResponse) + { + if (!string.IsNullOrWhiteSpace(enrolResponse.OfflineDomainJoin)) + { + Presentation.UpdateStatus("Enrolling Device", string.Format("Performing Offline Domain Join:{0}Renaming Computer: {1} -> {2}", Environment.NewLine, Interop.LocalAuthentication.ComputerName, enrolResponse.DeviceComputerName), true, -1, 1500); + + string odjFile = Path.GetTempFileName(); + File.WriteAllBytes(odjFile, Convert.FromBase64String(enrolResponse.OfflineDomainJoin)); + + string odjWindowsPath = Environment.GetEnvironmentVariable("SystemRoot"); + string odjProcessArguments = string.Format("/REQUESTODJ /LOADFILE \"{0}\" /WINDOWSPATH \"{1}\" /LOCALOS", odjFile, odjWindowsPath); + + ProcessStartInfo odjProcessStartInfo = new ProcessStartInfo("DJOIN.EXE", odjProcessArguments) + { + CreateNoWindow = true, + ErrorDialog = false, + LoadUserProfile = true, + RedirectStandardOutput = true, + UseShellExecute = false + }; + string odjResult; + using (Process odjProcess = System.Diagnostics.Process.Start(odjProcessStartInfo)) + { + odjResult = odjProcess.StandardOutput.ReadToEnd(); + odjProcess.WaitForExit(20000); // 20 Seconds + } + Presentation.UpdateStatus("Enrolling Device", string.Format("Offline Domain Join Result:{0}{1}", Environment.NewLine, odjResult), true, -1, 3000); + + if (File.Exists(odjFile)) + File.Delete(odjFile); + + // Flush Logged-On History + if (!string.IsNullOrEmpty(enrolResponse.DeviceDomainName)) + { + using (RegistryKey regWinlogon = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon", true)){ + regWinlogon.SetValue("DefaultDomainName", enrolResponse.DeviceDomainName, RegistryValueKind.String); + regWinlogon.SetValue("DefaultUserName", String.Empty, RegistryValueKind.String); + } + using (RegistryKey regLogonUI = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI", true)) + { + regLogonUI.DeleteValue("LastLoggedOnUser", false); + } + } + + return true; // Reboot required + } + else + { + // No Domain Join + return false; // Reboot not required + } + } + + /// + /// Processes a Client Service Enrol Response for Device Assigned User Actions + /// + /// + private static void ApplyDeviceAssignedUser(this EnrolResponse enrolResponse) + { + // Only run task if Assigned User was specified + if (!string.IsNullOrWhiteSpace(enrolResponse.DeviceAssignedUserSID)) + { + Presentation.UpdateStatus("Enrolling Device", string.Format(@"Configuring permissions for the device owner:{0}{1} ({2}\{3})", Environment.NewLine, enrolResponse.DeviceAssignedUserName, enrolResponse.DeviceAssignedUserDomain, enrolResponse.DeviceAssignedUserUsername), true, -1, 3000); + + Interop.LocalAuthentication.AddLocalGroupMembership("Administrators", enrolResponse.DeviceAssignedUserSID, enrolResponse.DeviceAssignedUserUsername, enrolResponse.DeviceAssignedUserDomain); + + // Make Windows think this user was the last to logon + using (RegistryKey regWinlogon = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon", true)) + { + regWinlogon.SetValue("DefaultDomainName", enrolResponse.DeviceAssignedUserDomain, RegistryValueKind.String); + regWinlogon.SetValue("DefaultUserName", enrolResponse.DeviceAssignedUserUsername, RegistryValueKind.String); + } + using (RegistryKey regLogonUI = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI", true)) + { + regLogonUI.SetValue("LastLoggedOnUser", string.Format(@"{0}\{1}", enrolResponse.DeviceAssignedUserDomain, enrolResponse.DeviceAssignedUserUsername), RegistryValueKind.String); + } + } + } + + /// + /// Processes a Client Service Enrol Response for Device Certificate Actions + /// + /// + private static void ApplyDeviceCertificates(this EnrolResponse enrolResponse) + { + // Only run if a Certificate was supplied + if (!string.IsNullOrEmpty(enrolResponse.DeviceCertificate)) + { + Presentation.UpdateStatus("Enrolling Device", "Configuring Wireless Certificates", true, -1, 1000); + + var certPersonalBytes = Convert.FromBase64String(enrolResponse.DeviceCertificate); + var certPersonal = new X509Certificate2(certPersonalBytes, "password", X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet); + if (certPersonal == null) + throw new ClientServiceException("Enrolment > Device Certificate", "Unable to Import Device Certificate Provided, possibly check password."); + + // Certificate Removal + if (enrolResponse.DeviceCertificateRemoveExisting != null && enrolResponse.DeviceCertificateRemoveExisting.Count > 0) + { + List regExMatchesSubject = new List(); + foreach (var subjectRegEx in enrolResponse.DeviceCertificateRemoveExisting) + regExMatchesSubject.Add(new Regex(subjectRegEx, RegexOptions.IgnoreCase)); + + // Remove from 'My' Store + Interop.Certificates.RemoveCertificates(StoreName.My, StoreLocation.LocalMachine, regExMatchesSubject, certPersonal); + // Remove from 'Root' Store + Interop.Certificates.RemoveCertificates(StoreName.Root, StoreLocation.LocalMachine, regExMatchesSubject, certPersonal); + // Remove from 'CertificateAuthority' Store + Interop.Certificates.RemoveCertificates(StoreName.CertificateAuthority, StoreLocation.LocalMachine, regExMatchesSubject, certPersonal); + } + + // Add Certificate + Presentation.UpdateStatus("Enrolling Device", string.Format("Configuring Wireless Certificates{0}Installing Certificate: {1}", Environment.NewLine, Interop.Certificates.GetCertificateFriendlyName(certPersonal)), true, -1); + Interop.Certificates.AddCertificate(StoreName.My, StoreLocation.LocalMachine, certPersonal); + } + } + + } +} diff --git a/Disco.Client/Extensions/WhoAmIExtensions.cs b/Disco.Client/Extensions/WhoAmIExtensions.cs new file mode 100644 index 00000000..6030a5dc --- /dev/null +++ b/Disco.Client/Extensions/WhoAmIExtensions.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Disco.Models.ClientServices; + +namespace Disco.Client.Extensions +{ + public static class WhoAmIExtensions + { + + public static void Process(this WhoAmIResponse whoAmIResponse) + { + Program.IsAuthenticated = true; + whoAmIResponse.PresentResponse(); + } + + private static void PresentResponse(this WhoAmIResponse whoAmIResponse) + { + StringBuilder message = new StringBuilder(); + message.AppendLine("Authenticated Connection:"); + message.Append("Username: ").AppendLine(whoAmIResponse.Username); + message.Append("Name: ").Append(whoAmIResponse.DisplayName); + message.Append(" (").Append(whoAmIResponse.Type).AppendLine(")"); + Presentation.UpdateStatus("Connection Established to Preparation Server", message.ToString(), false, 0, 1500); + } + public static void UnauthenticatedResponse() + { + Program.IsAuthenticated = false; + Presentation.UpdateStatus("Connection Established to Preparation Server", "Unauthenticated connection to the server...", false, 0, 1500); + } + + + } +} diff --git a/Disco.Client/Icon.ico b/Disco.Client/Icon.ico new file mode 100644 index 00000000..ffd2b06e Binary files /dev/null and b/Disco.Client/Icon.ico differ diff --git a/Disco.Client/Interop/Certificates.cs b/Disco.Client/Interop/Certificates.cs new file mode 100644 index 00000000..f122f21d --- /dev/null +++ b/Disco.Client/Interop/Certificates.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Text.RegularExpressions; + +namespace Disco.Client.Interop +{ + public static class Certificates + { + + public static string GetCertificateFriendlyName(X509Certificate2 Certificate) + { + string subject = Certificate.Subject; + return subject.Substring(subject.IndexOf("=") + 1, subject.IndexOf(",") - subject.IndexOf("=") - 1); + } + + public static List GetCertificateSubjects(StoreName StoreName, StoreLocation StoreLocation) + { + X509Store certStore = new X509Store(StoreName, StoreLocation); + certStore.Open(OpenFlags.ReadOnly); + var certSubjects = certStore.Certificates.Cast().Select(c => c.Subject).ToList(); + certStore.Close(); + return certSubjects; + } + + public static bool AddCertificate(StoreName StoreName, StoreLocation StoreLocation, X509Certificate2 Certificate) + { + X509Store certStore = new X509Store(StoreName, StoreLocation); + bool certAlreadyAdded = false; + + certStore.Open(OpenFlags.ReadWrite); + + try + { + foreach (X509Certificate2 cert in certStore.Certificates) + { + if (cert.SerialNumber.Equals(Certificate.SerialNumber)) + { + certAlreadyAdded = true; + break; + } + } + + if (!certAlreadyAdded) + { + Presentation.UpdateStatus("Enrolling Device", string.Format("Configuring Wireless Certificates{0}Adding Certificate: '{1}' from {2}@{3}", Environment.NewLine, GetCertificateFriendlyName(Certificate), StoreName.ToString(), StoreLocation.ToString()), true, -1, 3000); + certStore.Add(Certificate); + } + } + catch (Exception) { throw; } + finally + { + certStore.Close(); + } + + return !certAlreadyAdded; + } + + public static List RemoveCertificates(StoreName StoreName, StoreLocation StoreLocation, List RegExMatchesSubject, X509Certificate2 CertificateException) + { + X509Store certStore = new X509Store(StoreName, StoreLocation); + List results = new List(); + List certStoreRemove = new List(); + + certStore.Open(OpenFlags.ReadWrite); + + try + { + foreach (X509Certificate2 cert in certStore.Certificates) + { + if (!cert.SerialNumber.Equals(CertificateException.SerialNumber)) + { + foreach (var subjectRegEx in RegExMatchesSubject) + { + if (subjectRegEx.IsMatch(cert.Subject)) + { + certStoreRemove.Add(cert); + break; + } + } + } + } + + foreach (var cert in certStoreRemove) + { + results.Add(cert.Subject); + + Presentation.UpdateStatus("Enrolling Device", string.Format("Configuring Wireless Certificates{0}Removing Certificate: '{1}' from {2}@{3}", Environment.NewLine, GetCertificateFriendlyName(cert), StoreName.ToString(), StoreLocation.ToString()), true, -1, 1500); + certStore.Remove(cert); + } + } + catch (Exception) { throw; } + finally + { + certStore.Close(); + } + + return results; + } + } +} diff --git a/Disco.Client/Interop/LocalAuthentication.cs b/Disco.Client/Interop/LocalAuthentication.cs new file mode 100644 index 00000000..4243cdee --- /dev/null +++ b/Disco.Client/Interop/LocalAuthentication.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.DirectoryServices; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Disco.Client.Interop +{ + public static class LocalAuthentication + { + + public static bool AddLocalGroupMembership(string GroupName, string UserSID, string Username, string UserDomain) + { + + using (DirectoryEntry group = new DirectoryEntry(string.Format("WinNT://./{0},group", GroupName))) + { + // Check to see if the User is already a member + foreach (object memberRef in (IEnumerable)group.Invoke("Members")) + { + using (DirectoryEntry member = new DirectoryEntry(memberRef)) + { + var memberPath = member.Path; + if (memberPath.Equals(string.Format("WinNT://{0}/{1}", UserDomain, Username), StringComparison.InvariantCultureIgnoreCase) || + memberPath.Equals(string.Format("WinNT://{0}", UserSID), StringComparison.InvariantCultureIgnoreCase)) + return false; + } + } + group.Invoke("Add", string.Format("WinNT://{0}", UserSID)); + } + return true; + } + + public static string CurrentUserDomain + { + get + { + return Environment.UserDomainName; + } + } + public static string CurrentUserName + { + get + { + return Environment.UserName; + } + } + + public static string ComputerName + { + get + { + return Environment.MachineName; + } + } + + } +} diff --git a/Disco.Client/Interop/Network.cs b/Disco.Client/Interop/Network.cs new file mode 100644 index 00000000..f01bcde1 --- /dev/null +++ b/Disco.Client/Interop/Network.cs @@ -0,0 +1,327 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management; +using System.Runtime.InteropServices; +using System.Text; + +namespace Disco.Client.Interop +{ + public static class Network + { + private static List NetworkAdapters { get; set; } + private static NetworkAdapterInfo PrimaryLanNetworkAdapter { get; set; } + private static NetworkAdapterInfo PrimaryWlanNetworkAdapter { get; set; } + + static Network() + { + // Get All Adapters + RetrieveLanAdapters(); + + if (NetworkAdapters.Count > 0) + { + // Only Retrieve Wlan Adapters if at least one adapter was found by WMI + RetrieveWlanAdapters(); + + // Determine Primary Adapters + + // Lan + PrimaryLanNetworkAdapter = NetworkAdapters.Where(n => !n.IsWLanAdapter && n.NetConnectionId.StartsWith("Local Area Connection", StringComparison.InvariantCultureIgnoreCase)).OrderByDescending(n => n.Speed).FirstOrDefault(); + // Might be too restrictive - remove name restriction just in case. + if (PrimaryLanNetworkAdapter == null) + PrimaryLanNetworkAdapter = NetworkAdapters.Where(n => !n.IsWLanAdapter).OrderByDescending(n => n.Speed).FirstOrDefault(); + + // Wan + PrimaryWlanNetworkAdapter = NetworkAdapters.Where(n => n.IsWLanAdapter).OrderByDescending(n => n.Speed).FirstOrDefault(); + } + } + + private static void RetrieveLanAdapters() + { + // Get NetworkAdapter Information + try + { + using (ManagementObjectSearcher mSearcher = new ManagementObjectSearcher("SELECT Index, GUID, MACAddress, Name, NetConnectionID, Speed FROM Win32_NetworkAdapter WHERE PhysicalAdapter=true AND MACAddress IS NOT NULL AND Name IS NOT NULL AND NetConnectionID IS NOT NULL AND Speed IS NOT NULL")) + { + using (ManagementObjectCollection mResults = mSearcher.Get()) + { + NetworkAdapters = new List(); + foreach (var mResult in mResults.Cast()) + { + NetworkAdapterInfo nic = new NetworkAdapterInfo() + { + Index = (UInt32)mResult.GetPropertyValue("Index"), + Guid = Guid.Parse((string)mResult.GetPropertyValue("GUID")), + MacAddress = mResult.GetPropertyValue("MACAddress").ToString(), + Name = mResult.GetPropertyValue("Name").ToString(), + NetConnectionId = mResult.GetPropertyValue("NetConnectionID").ToString(), + Speed = Convert.ToUInt64(mResult.GetPropertyValue("Speed")), + IsWLanAdapter = false + }; + NetworkAdapters.Add(nic); + } + } + } + } + catch (Exception ex) + { + throw new Exception("Disco Client was unable to retrieve NetworkAdapter information from WMI", ex); + } + } + + private static void RetrieveWlanAdapters() + { + WLAN_INTERFACE_INFO_LIST wlanApiInterfaceList; + + IntPtr wlanApiHandle = IntPtr.Zero; + uint wlanApiServiceVersion = 0; + + if (WlanOpenHandle(WLAN_API_VERSION_2_0, IntPtr.Zero, out wlanApiServiceVersion, ref wlanApiHandle) == ERROR_SUCCESS) + { + IntPtr wlanApiInterfaceListPointer = IntPtr.Zero; + + if (WlanEnumInterfaces(wlanApiHandle, IntPtr.Zero, ref wlanApiInterfaceListPointer) == ERROR_SUCCESS) + { + wlanApiInterfaceList = new WLAN_INTERFACE_INFO_LIST(wlanApiInterfaceListPointer); + WlanFreeMemory(wlanApiInterfaceListPointer); + } + else + { + // Error - No Wlan Adapters Reported + WlanCloseHandle(wlanApiHandle, IntPtr.Zero); + return; + } + + WlanCloseHandle(wlanApiHandle, IntPtr.Zero); + } + else + { + // Error - No Wlan Adapters Reported + return; + } + + if (wlanApiInterfaceList.InterfaceInfo != null) + { + foreach (var wlanApiAdapter in wlanApiInterfaceList.InterfaceInfo) + { + var wlanApiAdapterInfo = wlanApiAdapter; + var wmiAdapterInfo = NetworkAdapters.FirstOrDefault(n => n.Guid == wlanApiAdapterInfo.InterfaceGuid); + if (wmiAdapterInfo != null) + { + wmiAdapterInfo.IsWLanAdapter = true; + wmiAdapterInfo.WlanState = wlanApiAdapterInfo.isState; + } + } + } + } + + public static string PrimaryLanMacAddress + { + get + { + // Return null if no Primary LAN Network Adapter found on this Device + + return (PrimaryLanNetworkAdapter == null) ? null : PrimaryLanNetworkAdapter.MacAddress; + } + } + public static string PrimaryWlanMacAddress + { + get + { + // Return null if no Primary WLAN Network Adapter found on this Device + + return (PrimaryWlanNetworkAdapter == null) ? null : PrimaryWlanNetworkAdapter.MacAddress; + } + } + + private class NetworkAdapterInfo + { + public UInt32 Index { get; set; } + public Guid Guid { get; set; } + public string Name { get; set; } + public string NetConnectionId { get; set; } + public string MacAddress { get; set; } + public UInt64 Speed { get; set; } + + public bool IsWLanAdapter { get; set; } + public WLAN_INTERFACE_STATE WlanState { get; set; } + + public string WlanStateDescription + { + get + { + switch (WlanState) + { + case WLAN_INTERFACE_STATE.wlan_interface_state_not_ready: + return "Not Ready"; + case WLAN_INTERFACE_STATE.wlan_interface_state_connected: + return "Connected"; + case WLAN_INTERFACE_STATE.wlan_interface_state_ad_hoc_network_formed: + return "Ad Hoc Network Formed"; + case WLAN_INTERFACE_STATE.wlan_interface_state_disconnecting: + return "Disconnecting"; + case WLAN_INTERFACE_STATE.wlan_interface_state_disconnected: + return "Disconnected"; + case WLAN_INTERFACE_STATE.wlan_interface_state_associating: + return "Associating"; + case WLAN_INTERFACE_STATE.wlan_interface_state_discovering: + return "Discovering"; + case WLAN_INTERFACE_STATE.wlan_interface_state_authenticating: + return "Authenticating"; + default: + return "Unknown"; + } + } + } + } + + #region Wlan Win32 Interop + + private const uint WLAN_API_VERSION_2_0 = 2; // Windows Vista WiFi API Version + private const int ERROR_SUCCESS = 0; + + /// + /// Opens a connection to the server + /// + [DllImport("Wlanapi.dll")] + private static extern int WlanOpenHandle( + uint dwClientVersion, + IntPtr pReserved, //not in MSDN but required + [Out] out uint pdwNegotiatedVersion, + ref IntPtr ClientHandle); + + /// + /// Closes a connection to the server + /// + [DllImport("Wlanapi", EntryPoint = "WlanCloseHandle")] + private static extern uint WlanCloseHandle( + [In] IntPtr hClientHandle, + IntPtr pReserved); + + /// + /// Enumerates all wireless interfaces in the laptop + /// + [DllImport("Wlanapi", EntryPoint = "WlanEnumInterfaces")] + private static extern uint WlanEnumInterfaces( + [In] IntPtr hClientHandle, + IntPtr pReserved, + ref IntPtr ppInterfaceList); + + /// + /// Frees memory returned by native WiFi functions + /// + [DllImport("Wlanapi", EntryPoint = "WlanFreeMemory")] + private static extern void WlanFreeMemory([In] IntPtr pMemory); + + /// + /// Defines the state of the interface. e.g. connected, disconnected. + /// + private enum WLAN_INTERFACE_STATE + { + /// + /// wlan_interface_state_not_ready -> 0 + /// + wlan_interface_state_not_ready = 0, + /// + /// wlan_interface_state_connected -> 1 + /// + wlan_interface_state_connected = 1, + /// + /// wlan_interface_state_ad_hoc_network_formed -> 2 + /// + wlan_interface_state_ad_hoc_network_formed = 2, + /// + /// wlan_interface_state_disconnecting -> 3 + /// + wlan_interface_state_disconnecting = 3, + /// + /// wlan_interface_state_disconnected -> 4 + /// + wlan_interface_state_disconnected = 4, + /// + /// wlan_interface_state_associating -> 5 + /// + wlan_interface_state_associating = 5, + /// + /// wlan_interface_state_discovering -> 6 + /// + wlan_interface_state_discovering = 6, + /// + /// wlan_interface_state_authenticating -> 7 + /// + wlan_interface_state_authenticating = 7, + } + + + /// + /// Stores interface info + /// + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct WLAN_INTERFACE_INFO + { + /// GUID->_GUID + public Guid InterfaceGuid; + + /// WCHAR[256] + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] + public string strInterfaceDescription; + + /// WLAN_INTERFACE_STATE->_WLAN_INTERFACE_STATE + public WLAN_INTERFACE_STATE isState; + } + + /// + /// Contains an array of NIC information + /// + [StructLayout(LayoutKind.Sequential)] + private struct WLAN_INTERFACE_INFO_LIST + { + /// + /// Length of array + /// + public Int32 dwNumberOfItems; + /// + /// This member is not used by the wireless service. Applications can use this member when processing individual interfaces. + /// + public Int32 dwIndex; + /// + /// Array of WLAN interfaces. + /// + public WLAN_INTERFACE_INFO[] InterfaceInfo; + + /// + /// Constructor for WLAN_INTERFACE_INFO_LIST. + /// Constructor is needed because the InterfaceInfo member varies based on how many adapters are in the system. + /// + /// the unmanaged pointer containing the list. + public WLAN_INTERFACE_INFO_LIST(IntPtr pList) + { + // The first 4 bytes are the number of WLAN_INTERFACE_INFO structures. + dwNumberOfItems = Marshal.ReadInt32(pList, 0); + + // The next 4 bytes are the index of the current item in the unmanaged API. + dwIndex = Marshal.ReadInt32(pList, 4); + + // Construct the array of WLAN_INTERFACE_INFO structures. + InterfaceInfo = new WLAN_INTERFACE_INFO[dwNumberOfItems]; + + for (int i = 0; i <= dwNumberOfItems - 1; i++) + { + // The offset of the array of structures is 8 bytes past the beginning. + // Then, take the index and multiply it by the number of bytes in the + // structure. + // The length of the WLAN_INTERFACE_INFO structure is 532 bytes - this + // was determined by doing a Marshall.SizeOf(WLAN_INTERFACE_INFO) + IntPtr pItemList = new IntPtr(pList.ToInt64() + (i * 532) + 8); + + // Construct the WLAN_INTERFACE_INFO structure, marshal the unmanaged + // structure into it, then copy it to the array of structures. + InterfaceInfo[i] = (WLAN_INTERFACE_INFO)Marshal.PtrToStructure(pItemList, typeof(WLAN_INTERFACE_INFO)); + } + } + } + + #endregion + + } +} diff --git a/Disco.Client/Interop/SystemAudit.cs b/Disco.Client/Interop/SystemAudit.cs new file mode 100644 index 00000000..0a426b7e --- /dev/null +++ b/Disco.Client/Interop/SystemAudit.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management; +using System.Text; +using System.Threading.Tasks; + +namespace Disco.Client.Interop +{ + public static class SystemAudit + { + + public static string DeviceSerialNumber { get; private set; } + public static string DeviceSMBIOSVersion { get; private set; } + public static string DeviceManufacturer { get; private set; } + public static string DeviceModel { get; private set; } + public static string DeviceType { get; private set; } + public static string DeviceUUID { get; private set; } + public static bool DeviceIsPartOfDomain { get; private set; } + + static SystemAudit() + { + // Get BIOS Information + try + { + using (ManagementObjectSearcher mSearcher = new ManagementObjectSearcher("SELECT SerialNumber, SMBIOSBIOSVersion FROM Win32_BIOS WHERE PrimaryBios=true")) + { + using (ManagementObjectCollection mResults = mSearcher.Get()) + { + using (var mItem = mResults.Cast().FirstOrDefault()) + { + if (mItem != null) + { + DeviceSerialNumber = mItem.GetPropertyValue("SerialNumber").ToString().Trim(); + ErrorReporting.DeviceIdentifier = DeviceSerialNumber; + DeviceSMBIOSVersion = mItem.GetPropertyValue("SMBIOSBIOSVersion").ToString().Trim(); + } + else + { + throw new Exception("No Win32_BIOS WHERE PrimaryBios=true was found"); + } + } + } + } + } + catch (Exception ex) + { + throw new Exception("Disco Client was unable to retrieve BIOS information from WMI", ex); + } + + // Get System Information + try + { + using (ManagementObjectSearcher mSearcher = new ManagementObjectSearcher("SELECT Manufacturer, Model, PartOfDomain, PCSystemType FROM Win32_ComputerSystem")) + { + using (ManagementObjectCollection mResults = mSearcher.Get()) + { + using (var mItem = mResults.Cast().FirstOrDefault()) + { + if (mItem != null) + { + DeviceManufacturer = mItem.GetPropertyValue("Manufacturer").ToString().Trim(); + DeviceModel = mItem.GetPropertyValue("Model").ToString().Trim(); + DeviceIsPartOfDomain = bool.Parse(mItem.GetPropertyValue("PartOfDomain").ToString()); + DeviceType = PCSystemTypeToString((UInt16)mItem.GetPropertyValue("PCSystemType")); + } + else + { + throw new Exception("No Win32_ComputerSystem was found"); + } + } + } + } + } + catch (Exception ex) + { + throw new Exception("Disco Client was unable to retrieve ComputerSystem information from WMI", ex); + } + + // Get System Product Information + string ComputerSystemProductSerialNumber; + try + { + using (ManagementObjectSearcher mSearcher = new ManagementObjectSearcher("SELECT IdentifyingNumber, UUID FROM Win32_ComputerSystemProduct")) + { + using (ManagementObjectCollection mResults = mSearcher.Get()) + { + using (var mItem = mResults.Cast().FirstOrDefault()) + { + if (mItem != null) + { + ComputerSystemProductSerialNumber = mItem.GetPropertyValue("IdentifyingNumber").ToString().Trim(); + DeviceUUID = mItem.GetPropertyValue("UUID").ToString().Trim(); + } + else + { + throw new Exception("No Win32_ComputerSystemProduct was found"); + } + } + } + } + } + catch (Exception ex) + { + throw new Exception("Disco Client was unable to retrieve ComputerSystemProduct information from WMI", ex); + } + + // Added 2012-11-22 G# - Lenovo IdeaPad Serial SHIM + // http://www.discoict.com.au/forum/feature-requests/2012/11/serial-number-detection-on-ideapads.aspx + if (string.IsNullOrWhiteSpace(DeviceSerialNumber) || + (DeviceManufacturer.Equals("LENOVO", StringComparison.InvariantCultureIgnoreCase) && + (DeviceModel.Equals("S10-3", StringComparison.InvariantCultureIgnoreCase) // S10-3 + || DeviceModel.Equals("2957", StringComparison.InvariantCultureIgnoreCase)))) // S10-2 + { + try + { + using (ManagementObjectSearcher mSearcher = new ManagementObjectSearcher("SELECT SerialNumber FROM Win32_BaseBoard")) + { + using (ManagementObjectCollection mResults = mSearcher.Get()) + { + using (var mItem = mResults.Cast().FirstOrDefault()) + { + if (mItem != null) + { + DeviceSerialNumber = mItem.GetPropertyValue("SerialNumber").ToString().Trim(); + } + else + { + throw new Exception("No Win32_BaseBoard was found"); + } + } + } + } + } + catch (Exception ex) + { + throw new Exception("Disco Client was unable to retrieve BaseBoard information from WMI", ex); + } + if (string.IsNullOrWhiteSpace(DeviceSerialNumber)) + DeviceSerialNumber = ComputerSystemProductSerialNumber; + } + // End Added 2012-11-22 G# + + ErrorReporting.DeviceIdentifier = DeviceSerialNumber; + + // Validate Device 'State' + if (string.IsNullOrWhiteSpace(DeviceSerialNumber)) + throw new Exception("This device has no serial number stored in BIOS or BaseBoard"); + if (DeviceSerialNumber.Length > 60) + throw new Exception(string.Format("The serial number reported by this device is over 60 characters long:{0}{1}", Environment.NewLine, DeviceSerialNumber)); + } + + private static string PCSystemTypeToString(UInt16 PCSystemType) + { + switch (PCSystemType) + { + case 0: + return "Unknown"; + case 1: + return "Desktop"; + case 2: + return "Mobile"; + case 3: + return "Workstation"; + case 4: + return "EnterpriseServer"; + case 5: + return "SmallOfficeAndHomeOfficeServer"; + case 6: + return "AppliancePC"; + case 7: + return "PerformanceServer"; + case 8: + return "Maximum"; + default: + return "Unknown"; + } + } + } +} diff --git a/Disco.Client/Package Creation/7z.dll b/Disco.Client/Package Creation/7z.dll new file mode 100644 index 00000000..2bdaed6a Binary files /dev/null and b/Disco.Client/Package Creation/7z.dll differ diff --git a/Disco.Client/Package Creation/7z.exe b/Disco.Client/Package Creation/7z.exe new file mode 100644 index 00000000..101884c3 Binary files /dev/null and b/Disco.Client/Package Creation/7z.exe differ diff --git a/Disco.Client/Package Creation/PreparationClient.zip b/Disco.Client/Package Creation/PreparationClient.zip new file mode 100644 index 00000000..e4e3f315 Binary files /dev/null and b/Disco.Client/Package Creation/PreparationClient.zip differ diff --git a/Disco.Client/Presentation.cs b/Disco.Client/Presentation.cs new file mode 100644 index 00000000..c1c41fd8 --- /dev/null +++ b/Disco.Client/Presentation.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading; +using Disco.Client.Extensions; + +namespace Disco.Client +{ + public static class Presentation + { + public static bool DelayUI { get; set; } + + private static string EscapeMessage(this string Message) + { + if (!string.IsNullOrEmpty(Message)) + return Message.Replace(",", "{comma}").Replace(Environment.NewLine, "{newline}"); + else + return null; + } + public static void UpdateStatus(string SubHeading, string Message, bool ShowProgress, int Progress, int TryDelay) + { + Presentation.UpdateStatus(SubHeading, Message, ShowProgress, Progress); + if (TryDelay > 0) + Presentation.TryDelay(TryDelay); + } + public static void UpdateStatus(string SubHeading, string Message, bool ShowProgress, int Progress) + { + Console.WriteLine("#{0},{1},{2},{3}", SubHeading.EscapeMessage(), Message.EscapeMessage(), ShowProgress.ToString(), Progress.ToString()); + } + public static void TryDelay(int Milliseconds) + { + if (DelayUI) + Thread.Sleep(Milliseconds); + } + + public static void WriteBanner() + { + StringBuilder message = new StringBuilder(); + message.Append("Version: ").AppendLine(Assembly.GetExecutingAssembly().GetName().Version.ToString(3)); + message.Append("Device: ").Append(Interop.SystemAudit.DeviceSerialNumber).Append(" (").Append(Interop.SystemAudit.DeviceManufacturer).Append(" ").Append(Interop.SystemAudit.DeviceModel).AppendLine(")"); + Console.ForegroundColor = ConsoleColor.Yellow; + UpdateStatus("Preparation Client Started", message.ToString(), false, 0); + Console.ForegroundColor = ConsoleColor.White; + TryDelay(3000); + } + public static void WriteFatalError(Exception ex) + { + + Console.ForegroundColor = ConsoleColor.Magenta; + ClientServiceException clientServiceException = ex as ClientServiceException; + if (clientServiceException != null) + { + UpdateStatus(string.Format("An error occurred during {0}", clientServiceException.ServiceFeature), + clientServiceException.Message, false, 0); + } + else + { + StringBuilder message = new StringBuilder(); + message.Append("Type: ").AppendLine(ex.GetType().Name); + message.Append("Message: ").AppendLine(ex.Message); + message.Append("Source: ").AppendLine(ex.Source); + message.Append("Stack: ").AppendLine(ex.StackTrace); + UpdateStatus("An error occurred", message.ToString(), false, 0); + } + Console.ForegroundColor = ConsoleColor.White; + + TryDelay(30000); + } + public static void WriteFooter(bool RebootRequired, bool AllowUninstall, bool ErrorEncountered) + { + Console.ForegroundColor = ConsoleColor.Yellow; + + if (ErrorEncountered) + { + UpdateStatus("Client Finished due to Error", "An error occurred which caused the Preparation Client to stop running.", false, 0); + TryDelay(5000); + } + else + { + UpdateStatus("Client Finished Successfully", "The Preparation Client finished successfully.", false, 0); + TryDelay(1000); + } + + if (RebootRequired) + RegisterBootstrapperPostActions(ShutdownActions.Reboot, AllowUninstall && !ErrorEncountered); + else + RegisterBootstrapperPostActions(ShutdownActions.None, AllowUninstall && !ErrorEncountered); + } + + public static void RegisterBootstrapperPostActions(ShutdownActions ShutdownAction, bool Uninstall) + { + Console.WriteLine("!{0},{1}", Enum.GetName(typeof(ShutdownActions), ShutdownAction), Uninstall ? "UninstallBootstrapper" : "DontUninstallBootstrapper"); + } + public enum ShutdownActions + { + None, + Shutdown, + Reboot + } + + } +} diff --git a/Disco.Client/Program.cs b/Disco.Client/Program.cs new file mode 100644 index 00000000..5a320fb4 --- /dev/null +++ b/Disco.Client/Program.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using Disco.Models.ClientServices; +using Disco.Client.Extensions; + +namespace Disco.Client +{ + public static class Program + { + public static bool IsAuthenticated { get; set; } + public static bool RebootRequired { get; set; } + public static bool AllowUninstall { get; set; } + + [STAThread] + public static void Main(string[] args) + { + bool keepProcessing; + + // Initialize Environment Settings + SetupEnvironment(); + + // Report to Bootstrapper + Presentation.WriteBanner(); + + // WhoAmI Phase + keepProcessing = Program.WhoAmI(); + + // Enrol Phase + if (keepProcessing) + keepProcessing = Program.Enrol(); + + // End conversation with Bootstrapper + Presentation.WriteFooter(Program.RebootRequired, Program.AllowUninstall, !keepProcessing); + } + + public static void SetupEnvironment() + { + // Hookup Unhandled Error Handling + AppDomain.CurrentDomain.UnhandledException += ErrorReporting.CurrentDomain_UnhandledException; + + // Ignore Local Proxies + WebRequest.DefaultWebProxy = new WebProxy(); + // Override Http 100 Continue Behavour + ServicePointManager.Expect100Continue = false; + + // Assume success unless otherwise notified + AllowUninstall = true; + + // Detect Disco.Bootstrapper - Create Enable UI Delay if Running + try + { + Presentation.DelayUI = (System.Diagnostics.Process.GetProcessesByName("Disco.ClientBootstrapper").Length > 0); + } + catch (Exception) + { + Presentation.DelayUI = true; // Add Delays on Error + } + } + + public static bool WhoAmI() + { + try + { + + WhoAmIResponse response; + WhoAmI request; + + // Build Request + request = new WhoAmI(); + + // Send Request + Presentation.UpdateStatus("Connecting to Preparation Server", "Determining connection authentication, please wait...", true, -1); + response = request.Post(true); + + // Process Response + response.Process(); + + // Complete + return true; + } + catch (WebException webEx) + { + // Check for 'Unauthenticated' Connection + if ((webEx.Status == WebExceptionStatus.ProtocolError) && ((HttpWebResponse)webEx.Response).StatusCode == HttpStatusCode.Unauthorized) + { + WhoAmIExtensions.UnauthenticatedResponse(); + return true; + } + else + { + // Some other Web Error + ErrorReporting.ReportError(webEx, false); + } + } + catch (Exception ex) + { + ErrorReporting.ReportError(ex, true); + } + return false; + } + + public static bool Enrol() + { + try + { + Enrol request; + EnrolResponse response = null; + + // Build Request + Presentation.UpdateStatus("Enrolling Device", "Building enrolment request and preparing to send data to the server.", true, -1); + request = new Enrol(); + request.Build(); + + // Send Request + Presentation.UpdateStatus("Enrolling Device", "Sending the enrolment request to the server.", true, -1); + response = request.Post(Program.IsAuthenticated); + + // Process Response + Presentation.UpdateStatus("Enrolling Device", "Processing the enrolment response from the server.", true, -1); + response.Process(); + + // Complete + return true; + } + catch (Exception ex) + { + ErrorReporting.ReportError(ex, true); + return false; + } + } + + } +} diff --git a/Disco.Client/Properties/AssemblyInfo.cs b/Disco.Client/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..913f9eb7 --- /dev/null +++ b/Disco.Client/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("Disco - Client")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Disco")] +[assembly: AssemblyCopyright("")] +[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("aa39bf57-b0e8-43ba-a29a-f38b3e5b189c")] + +// 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.2.0131.2002")] +[assembly: AssemblyFileVersion("1.2.0131.2002")] diff --git a/Disco.Client/Properties/app.manifest b/Disco.Client/Properties/app.manifest new file mode 100644 index 00000000..66b901b5 --- /dev/null +++ b/Disco.Client/Properties/app.manifest @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Disco.Client/PsExec.exe b/Disco.Client/PsExec.exe new file mode 100644 index 00000000..ed674807 Binary files /dev/null and b/Disco.Client/PsExec.exe differ diff --git a/Disco.Client/Start.bat b/Disco.Client/Start.bat new file mode 100644 index 00000000..51d657a0 --- /dev/null +++ b/Disco.Client/Start.bat @@ -0,0 +1,9 @@ +@ECHO OFF +IF /I "%USERDOMAIN%"=="NT AUTHORITY" GOTO RunAsNetworkService +Disco.Client.exe +EXIT /B 0 + +:RunAsNetworkService +ECHO #Running,Launching Preparation Client, Please wait...{newline}Starting client as 'NT AUTHORITY\Network Service',true,-1 +PsExec -acceptula -i -u "NT AUTHORITY\Network Service" -w "%CD%" "%CD%\Start.bat" +EXIT /B 0 \ No newline at end of file diff --git a/Disco.Client/packages.config b/Disco.Client/packages.config new file mode 100644 index 00000000..d0a33249 --- /dev/null +++ b/Disco.Client/packages.config @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/Disco.ClientBootstrapper/BootstrapperLoop.cs b/Disco.ClientBootstrapper/BootstrapperLoop.cs new file mode 100644 index 00000000..ac9a06d1 --- /dev/null +++ b/Disco.ClientBootstrapper/BootstrapperLoop.cs @@ -0,0 +1,252 @@ +//#define Debug + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Net; +using System.IO; +using System.Diagnostics; + +namespace Disco.ClientBootstrapper +{ + class BootstrapperLoop + { + + public Thread LoopThread; + public delegate void LoopCompleteCallback(); + private LoopCompleteCallback mLoopCompleteCallback; + private IStatus statusUI; + private string tempWorkingDirectory; + private StringBuilder errorMessage; + private Process clientProcess; + + public BootstrapperLoop(IStatus StatusUI, LoopCompleteCallback Callback) + { + this.statusUI = StatusUI; + this.mLoopCompleteCallback = Callback; + this.errorMessage = new StringBuilder(); + } + + public void Start() + { + this.LoopThread = new Thread(new ThreadStart(loopHost)); + this.LoopThread.Start(); + } + + private void loopHost() + { + try + { + loop(); + } + catch (Exception ex) + { + if (ex.GetType() == typeof(ThreadAbortException)) + return; + if (ex.GetType() == typeof(ThreadInterruptedException)) + return; + Program.WriteAppError(ex); + throw; + } + } + + private void loop() + { + +#if Debug + statusUI.UpdateStatus("Waiting for Debugger", "Please wait...", true, -1); + try + { + do + { + System.Threading.Thread.Sleep(10); + } while (!System.Diagnostics.Debugger.IsAttached); + } + catch (Exception ex) + { + statusUI.UpdateStatus("Error", ex.Message, true, -1); + return; + } +#else + statusUI.UpdateStatus("System Preparation (Bootstrapper)", "Starting", "Please wait...", true, -1); +#endif + + tempWorkingDirectory = Path.Combine(Path.GetPathRoot(Environment.SystemDirectory), "Disco\\Temp"); + if (!Directory.Exists(tempWorkingDirectory)) + Directory.CreateDirectory(tempWorkingDirectory); + + // Check for Network Connectivity + statusUI.UpdateStatus(null, "Detecting Network", "Checking network connectivity, Please wait...", true, -1); + if (!Interop.NetworkInterop.PingDisco()) + { + statusUI.UpdateStatus(null, "Detecting Network", "No network connectivity detected, Diagnosing...", true, -1); + statusUI_WriteAdapterInfo(); + + if (!Interop.NetworkInterop.PingDisco()) + { + // Check for Wireless + var hasWireless = (Interop.NetworkInterop.NetworkAdapters.Count(na => na.IsWireless) > 0); + if (hasWireless) + { + // True: Do wireless loop + statusUI.UpdateStatus(null, "Configuring Wireless Network", "Wireless adapter detected, Configuring...", true, -1); + Interop.NetworkInterop.ConfigureWireless(); + statusUI.UpdateStatus(null, "Waiting for Wireless Network", null, true, 0); + for (int i = 0; i < 100; i++) + { + statusUI_WriteAdapterInfo(); + statusUI.UpdateStatus(null, null, null, true, i); + Program.SleepThread(500, false); + if (Interop.NetworkInterop.PingDisco()) + break; + } + if (!Interop.NetworkInterop.PingDisco()) + { + statusUI.UpdateStatus(null, "Wireless Network Failed", "Unable to connect to the wireless network, please connect the network cable...", false); + Program.SleepThread(3000, false); + } + } + + if (!Interop.NetworkInterop.PingDisco()) + { + // Instruct user to connect network cable + statusUI.UpdateStatus(null, "Please connect the network cable", null); + for (int i = 0; i < 100; i++) + { + statusUI_WriteAdapterInfo(); + statusUI.UpdateStatus(null, null, null, true, i); + Program.SleepThread(500, false); + if (Interop.NetworkInterop.PingDisco()) + break; + } + } + } + + if (!Interop.NetworkInterop.PingDisco()) + { + // Client Failed + if (this.mLoopCompleteCallback != null) + { + this.mLoopCompleteCallback.BeginInvoke(null, null); + } + return; + } + } + + // Download Client + statusUI.UpdateStatus(null, "Downloading", "Retrieving Preparation Client, Please wait...", true, -1); + string clientSourceLocation = Path.Combine(tempWorkingDirectory, "PreparationClient.zip"); + using (var webClient = new WebClient()) + { + webClient.DownloadFile("http://disco:9292/Services/Client/PreparationClient", clientSourceLocation); + } + + // Unzip Client + statusUI.UpdateStatus(null, "Extracting", "Retrieving Preparation Client, Please wait...", true, -1); + string clientLocation = Path.Combine(tempWorkingDirectory, "PreparationClient"); + if (!Directory.Exists(clientLocation)) + Directory.CreateDirectory(clientLocation); + using (var clientSource = Ionic.Zip.ZipFile.Read(clientSourceLocation)) + { + clientSource.ExtractAll(clientLocation, Ionic.Zip.ExtractExistingFileAction.OverwriteSilently); + } + + // Launch Client + statusUI.UpdateStatus("System Preparation (Client)", "Running", "Launching Preparation Client, Please wait...", true, -1); + ProcessStartInfo clientProcessStart = new ProcessStartInfo(Path.Combine(clientLocation, "Start.bat")) + { + WorkingDirectory = clientLocation, + CreateNoWindow = true, + RedirectStandardOutput = true, + UseShellExecute = false, + }; + using (clientProcess = Process.Start(clientProcessStart)) + { + // Read StdOutput until End + try + { + clientProcess.OutputDataReceived += new DataReceivedEventHandler(clientProcess_OutputDataReceived); + clientProcess.BeginOutputReadLine(); + clientProcess.WaitForExit(); + } + catch (Exception) { throw; } + finally + { + try { clientProcess.CloseMainWindow(); } + catch (Exception) { } + } + } + clientProcess = null; + + // Cleanup + if (Directory.Exists(tempWorkingDirectory)) + Directory.Delete(tempWorkingDirectory, true); + Interop.CertificateInterop.RemoveTempCerts(); + + // Pause if Error + if (this.errorMessage.Length > 0) + { + Program.SleepThread(10000, true); + } + + // End Of Loop + if (this.mLoopCompleteCallback != null) + { + this.mLoopCompleteCallback.BeginInvoke(null, null); + } + } + + void statusUI_WriteAdapterInfo() + { + + var info = new StringBuilder(); + foreach (var na in Interop.NetworkInterop.NetworkAdapters) + { + if (na.IsWireless) + { + info.AppendLine(string.Format("{0}: {1}", na.NetConnectionID, na.WirelessConnectionStatusMeaning(na.WirelessConnectionStatus))); + } + else + { + info.AppendLine(string.Format("{0}: {1}", na.NetConnectionID, na.ConnectionStatusMeaning(na.ConnectionStatus))); + } + } + statusUI.UpdateStatus(null, null, info.ToString()); + + } + + void clientProcess_OutputDataReceived(object sender, DataReceivedEventArgs e) + { + if (!string.IsNullOrWhiteSpace(e.Data)) + { + System.Diagnostics.Debug.WriteLine(string.Format("OUTPUT: {0}", e.Data)); + var data = e.Data.Substring(1).Split(new char[] { ',' }); + switch (e.Data[0]) + { + case '#': + if (data.Length == 4) + { + statusUI.UpdateStatus(null, data[0].Replace("{comma}", ","), data[1].Replace("{comma}", ",").Replace("{newline}", Environment.NewLine), bool.Parse(data[2]), int.Parse(data[3])); + } + break; + case '!': + Program.PostBootstrapperActions = new List(data); + break; + } + } + } + + //void clientProcess_ErrorDataReceived(object sender, DataReceivedEventArgs e) + //{ + // if (!string.IsNullOrEmpty(e.Data)) + // { + // System.Diagnostics.Debug.WriteLine(string.Format("ERROR: {0}", e.Data)); + // this.errorMessage.AppendLine(e.Data); + // statusUI.UpdateStatus(null, "An Error Occurred", this.errorMessage.ToString(), false); + // } + //} + + } +} diff --git a/Disco.ClientBootstrapper/BootstrapperWorkstationInstall.vbs b/Disco.ClientBootstrapper/BootstrapperWorkstationInstall.vbs new file mode 100644 index 00000000..0b3f6a6b --- /dev/null +++ b/Disco.ClientBootstrapper/BootstrapperWorkstationInstall.vbs @@ -0,0 +1,11 @@ +Option Explicit + +Dim objShell, BootstrapperLocation + +Set objShell = CreateObject("WScript.Shell") + +BootstrapperLocation = Mid(WScript.ScriptFullName, 1, InStrRev(WScript.ScriptFullName, "\")) & "\Disco.ClientBootstrapper.exe" + +Call objShell.Run("""" & BootstrapperLocation & """ /Install", , True) + +WScript.Echo "Disco Client Bootstrapper Installed" \ No newline at end of file diff --git a/Disco.ClientBootstrapper/Disco.ClientBootstrapper.csproj b/Disco.ClientBootstrapper/Disco.ClientBootstrapper.csproj new file mode 100644 index 00000000..cf216eb9 --- /dev/null +++ b/Disco.ClientBootstrapper/Disco.ClientBootstrapper.csproj @@ -0,0 +1,335 @@ + + + + Debug + x86 + 8.0.30703 + 2.0 + {15BD9561-A3C7-4608-9F7E-F1A1CFB60055} + WinExe + Properties + Disco.ClientBootstrapper + Disco.ClientBootstrapper + v4.0 + Client + 512 + publish\ + true + Disk + false + Foreground + 7 + Days + false + false + true + 0 + 1.0.0.%2a + false + false + true + + + x86 + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + Properties\app.manifest + + + true + bin\Debug\ + DEBUG;TRACE + full + AnyCPU + prompt + true + true + + + bin\Release\ + TRACE + true + pdbonly + AnyCPU + prompt + true + true + + + Icon.ico + + + OnOutputUpdated + + + + + + + + + + + + + + + + + DotNetZip\BZip2\BitWriter.cs + + + DotNetZip\BZip2\BZip2Compressor.cs + + + DotNetZip\BZip2\BZip2InputStream.cs + + + DotNetZip\BZip2\BZip2OutputStream.cs + + + DotNetZip\BZip2\ParallelBZip2OutputStream.cs + + + DotNetZip\BZip2\Rand.cs + + + DotNetZip\CRC32.cs + + + DotNetZip\Zip\ComHelper.cs + + + DotNetZip\Zip\EncryptionAlgorithm.cs + + + DotNetZip\Zip\Events.cs + + + DotNetZip\Zip\Exceptions.cs + + + DotNetZip\Zip\ExtractExistingFileAction.cs + + + DotNetZip\Zip\FileSelector.cs + + + DotNetZip\Zip\OffsetStream.cs + + + DotNetZip\Zip\Shared.cs + + + DotNetZip\Zip\WinZipAes.cs + + + DotNetZip\Zip\ZipConstants.cs + + + DotNetZip\Zip\ZipCrypto.cs + + + DotNetZip\Zip\ZipDirEntry.cs + + + DotNetZip\Zip\ZipEntry.cs + + + DotNetZip\Zip\ZipEntry.Extract.cs + + + DotNetZip\Zip\ZipEntry.Read.cs + + + DotNetZip\Zip\ZipEntry.Write.cs + + + DotNetZip\Zip\ZipEntrySource.cs + + + DotNetZip\Zip\ZipErrorAction.cs + + + DotNetZip\Zip\ZipFile.AddUpdate.cs + + + DotNetZip\Zip\ZipFile.Check.cs + + + DotNetZip\Zip\ZipFile.cs + + + DotNetZip\Zip\ZipFile.Events.cs + + + DotNetZip\Zip\ZipFile.Extract.cs + + + DotNetZip\Zip\ZipFile.Read.cs + + + DotNetZip\Zip\ZipFile.Save.cs + + + DotNetZip\Zip\ZipFile.SaveSelfExtractor.cs + + + DotNetZip\Zip\ZipFile.Selector.cs + + + DotNetZip\Zip\ZipFile.x-IEnumerable.cs + + + DotNetZip\Zip\ZipInputStream.cs + + + DotNetZip\Zip\ZipOutputStream.cs + + + DotNetZip\Zip\ZipSegmentedStream.cs + + + DotNetZip\Zlib\Deflate.cs + + + DotNetZip\Zlib\DeflateStream.cs + + + DotNetZip\Zlib\GZipStream.cs + + + DotNetZip\Zlib\Inflate.cs + + + DotNetZip\Zlib\InfTree.cs + + + DotNetZip\Zlib\ParallelDeflateOutputStream.cs + + + DotNetZip\Zlib\Tree.cs + + + DotNetZip\Zlib\Zlib.cs + + + DotNetZip\Zlib\ZlibBaseStream.cs + + + DotNetZip\Zlib\ZlibCodec.cs + + + DotNetZip\Zlib\ZlibConstants.cs + + + DotNetZip\Zlib\ZlibStream.cs + + + + + + Form + + + FormStatus.cs + + + + + + + + + + + + + FormStatus.cs + + + ResXFileCodeGenerator + Designer + Resources.Designer.cs + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + True + True + Resources.resx + + + True + Settings.settings + True + + + + + False + Microsoft .NET Framework 4 Client Profile %28x86 and x64%29 + true + + + False + .NET Framework 3.5 SP1 Client Profile + false + + + False + .NET Framework 3.5 SP1 + false + + + False + Windows Installer 3.1 + true + + + + + Always + + + + + + + + + + + + + + + COPY "$(TargetPath)" "$(ProjectDir)..\Disco.Web\ClientBin" + + + \ No newline at end of file diff --git a/Disco.ClientBootstrapper/EmbedBootstrapper.vbs b/Disco.ClientBootstrapper/EmbedBootstrapper.vbs new file mode 100644 index 00000000..e69de29b diff --git a/Disco.ClientBootstrapper/FormStatus.Designer.cs b/Disco.ClientBootstrapper/FormStatus.Designer.cs new file mode 100644 index 00000000..81e7df29 --- /dev/null +++ b/Disco.ClientBootstrapper/FormStatus.Designer.cs @@ -0,0 +1,125 @@ +namespace Disco.ClientBootstrapper +{ + partial class FormStatus + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.labelHeading = new System.Windows.Forms.Label(); + this.progressBar = new System.Windows.Forms.ProgressBar(); + this.labelSubHeading = new System.Windows.Forms.Label(); + this.labelMessage = new System.Windows.Forms.Label(); + this.labelVersion = new System.Windows.Forms.Label(); + this.SuspendLayout(); + // + // labelHeading + // + this.labelHeading.BackColor = System.Drawing.Color.Transparent; + this.labelHeading.Font = new System.Drawing.Font("Microsoft Sans Serif", 11.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.labelHeading.Location = new System.Drawing.Point(15, 15); + this.labelHeading.Name = "labelHeading"; + this.labelHeading.Size = new System.Drawing.Size(270, 20); + this.labelHeading.TabIndex = 0; + this.labelHeading.Text = "System Preparation"; + // + // progressBar + // + this.progressBar.BackColor = System.Drawing.Color.White; + this.progressBar.Location = new System.Drawing.Point(15, 100); + this.progressBar.Margin = new System.Windows.Forms.Padding(2); + this.progressBar.MarqueeAnimationSpeed = 50; + this.progressBar.Name = "progressBar"; + this.progressBar.Size = new System.Drawing.Size(381, 15); + this.progressBar.Style = System.Windows.Forms.ProgressBarStyle.Marquee; + this.progressBar.TabIndex = 1; + this.progressBar.Visible = false; + // + // labelSubHeading + // + this.labelSubHeading.BackColor = System.Drawing.Color.White; + this.labelSubHeading.Font = new System.Drawing.Font("Microsoft Sans Serif", 9.75F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.labelSubHeading.Location = new System.Drawing.Point(15, 35); + this.labelSubHeading.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.labelSubHeading.Name = "labelSubHeading"; + this.labelSubHeading.Size = new System.Drawing.Size(381, 20); + this.labelSubHeading.TabIndex = 2; + // + // labelMessage + // + this.labelMessage.BackColor = System.Drawing.Color.White; + this.labelMessage.Location = new System.Drawing.Point(15, 55); + this.labelMessage.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); + this.labelMessage.Name = "labelMessage"; + this.labelMessage.Size = new System.Drawing.Size(381, 60); + this.labelMessage.TabIndex = 3; + // + // labelVersion + // + this.labelVersion.BackColor = System.Drawing.Color.White; + this.labelVersion.Font = new System.Drawing.Font("Microsoft Sans Serif", 6.75F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); + this.labelVersion.ForeColor = System.Drawing.Color.Gray; + this.labelVersion.Location = new System.Drawing.Point(229, 15); + this.labelVersion.Name = "labelVersion"; + this.labelVersion.Size = new System.Drawing.Size(167, 20); + this.labelVersion.TabIndex = 0; + this.labelVersion.Text = "Disco Bootstrapper v"; + this.labelVersion.TextAlign = System.Drawing.ContentAlignment.TopRight; + // + // FormStatus + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.BackgroundImage = global::Disco.ClientBootstrapper.Properties.Resources.Background_BW; + this.ClientSize = new System.Drawing.Size(411, 130); + this.Controls.Add(this.labelHeading); + this.Controls.Add(this.labelSubHeading); + this.Controls.Add(this.progressBar); + this.Controls.Add(this.labelVersion); + this.Controls.Add(this.labelMessage); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "FormStatus"; + this.ShowIcon = false; + this.ShowInTaskbar = false; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; + this.Text = "Disco - Client Bootstrapper"; + this.TopMost = true; + this.TransparencyKey = System.Drawing.Color.Magenta; + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.Label labelHeading; + private System.Windows.Forms.ProgressBar progressBar; + private System.Windows.Forms.Label labelSubHeading; + private System.Windows.Forms.Label labelMessage; + private System.Windows.Forms.Label labelVersion; + } +} + diff --git a/Disco.ClientBootstrapper/FormStatus.cs b/Disco.ClientBootstrapper/FormStatus.cs new file mode 100644 index 00000000..77831965 --- /dev/null +++ b/Disco.ClientBootstrapper/FormStatus.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Windows.Forms; + +namespace Disco.ClientBootstrapper +{ + public partial class FormStatus : Form, IStatus + { + + private delegate void dUpdateStatus(string Heading, string SubHeading, string Message, Nullable ShowProgress, Nullable Progress); + private dUpdateStatus mUpdateStatus; + + public FormStatus() + { + InitializeComponent(); + + var version = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version; + this.labelVersion.Text = string.Format("v{0}", version.ToString(3)); + + this.FormClosed += new FormClosedEventHandler(FormStatus_FormClosed); + + mUpdateStatus = new dUpdateStatus(UpdateStatusDo); + Cursor.Hide(); + } + + void FormStatus_FormClosed(object sender, FormClosedEventArgs e) + { + Cursor.Show(); + Program.ExitApplication(); + } + + public void UpdateStatus(string Heading, string SubHeading, string Message, Nullable ShowProgress = null, Nullable Progress = null) + { + try + { + this.Invoke(mUpdateStatus, Heading, SubHeading, Message, ShowProgress, Progress); + } + catch (Exception) { } + } + private void UpdateStatusDo(string Heading, string SubHeading, string Message, Nullable ShowProgress, Nullable Progress) + { + if (Heading != null) + if (this.labelHeading.Text != Heading) + this.labelHeading.Text = Heading; + if (SubHeading != null) + if (this.labelSubHeading.Text != SubHeading) + this.labelSubHeading.Text = SubHeading; + if (Message != null) + if (this.labelMessage.Text != Message) + this.labelMessage.Text = Message; + + if (ShowProgress.HasValue) + { + if (ShowProgress.Value) + { + this.progressBar.Visible = true; + if (Progress.HasValue) + { + if (Progress.Value > 0) + { + this.progressBar.Value = Math.Min(Progress.Value, 100); + this.progressBar.Style = ProgressBarStyle.Continuous; + } + else + { + this.progressBar.Style = ProgressBarStyle.Marquee; + } + } + } + else + { + this.progressBar.Visible = false; + } + } + } + } +} diff --git a/Disco.ClientBootstrapper/FormStatus.resx b/Disco.ClientBootstrapper/FormStatus.resx new file mode 100644 index 00000000..29dcb1b3 --- /dev/null +++ b/Disco.ClientBootstrapper/FormStatus.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Disco.ClientBootstrapper/IStatus.cs b/Disco.ClientBootstrapper/IStatus.cs new file mode 100644 index 00000000..dff2a4a8 --- /dev/null +++ b/Disco.ClientBootstrapper/IStatus.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Disco.ClientBootstrapper +{ + interface IStatus + { + void UpdateStatus(string Heading, string SubHeading, string Message, Nullable ShowProgress = null, Nullable Progress = null); + } +} diff --git a/Disco.ClientBootstrapper/Icon.ico b/Disco.ClientBootstrapper/Icon.ico new file mode 100644 index 00000000..ffd2b06e Binary files /dev/null and b/Disco.ClientBootstrapper/Icon.ico differ diff --git a/Disco.ClientBootstrapper/InstallBootstrapper.vbs b/Disco.ClientBootstrapper/InstallBootstrapper.vbs new file mode 100644 index 00000000..1f120736 --- /dev/null +++ b/Disco.ClientBootstrapper/InstallBootstrapper.vbs @@ -0,0 +1,62 @@ +Option Explicit + +Dim objFSO, objReg, objShell, objFile +Dim SourceFolder, DestinationFolder, GroupPolicyScriptLocation +Const HKLM = &H80000002 + +DestinationFolder = "C:\Disco" + +Set objFSO = CreateObject("Scripting.FileSystemObject") +Set objReg = GetObject("winmgmts:root\default:StdRegProv") +Set objShell = CreateObject("WScript.Shell") + +If objFSO.FolderExists(DestinationFolder) Then + Call objFSO.DeleteFolder(DestinationFolder) +End If +Call objFSO.CreateFolder(DestinationFolder) +SourceFolder = Mid(WScript.ScriptFullName, 1, InStrRev(WScript.ScriptFullName, "\")) +Call objFSO.CopyFile(SourceFolder & "*.*", DestinationFolder, True) + +GroupPolicyScriptLocation = objShell.ExpandEnvironmentStrings("%WinDir%\System32\GroupPolicy\Machine\Scripts\scripts.ini") +If objFSO.FileExists(GroupPolicyScriptLocation) Then + Call objFSO.DeleteFile(GroupPolicyScriptLocation) +End If +Set objFile = objFSO.CreateTextFile(GroupPolicyScriptLocation, True, True) +Call objFile.WriteLine() +Call objFile.WriteLine("[Startup]") +Call objFile.WriteLine("0CmdLine=C:\Disco\Disco.ClientBootstrapper.exe") +Call objFile.WriteLine("0Parameters=/Uninstall") +Call objFile.Close() +Set objFile = Nothing + +Set objFSO = Nothing + +Call objReg.SetDWORDValue(HKLM, "SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon", "HideStartupScripts", 0) +Call objReg.SetDWORDValue (HKLM, "SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon", "RunStartupScriptSync", 1) + +Call objReg.CreateKey(HKLM, "SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\Scripts\Shutdown") +Call objReg.CreateKey(HKLM, "SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\Scripts\Startup\0\0") +Call objReg.SetStringValue(HKLM, "SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\Scripts\Startup\0", "GPO-ID", "LocalGPO") +Call objReg.SetStringValue(HKLM, "SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\Scripts\Startup\0", "SOM-ID", "Local") +Call objReg.SetStringValue(HKLM, "SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\Scripts\Startup\0", "FileSysPath", "C:\WINDOWS\System32\GroupPolicy\Machine") +Call objReg.SetStringValue(HKLM, "SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\Scripts\Startup\0", "DisplayName", "Local Group Policy") +Call objReg.SetStringValue(HKLM, "SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\Scripts\Startup\0", "GPOName", "Local Group Policy") +Call objReg.SetDWORDValue(HKLM, "SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\Scripts\Startup\0", "PSScriptOrder", 1) +Call objReg.SetStringValue(HKLM, "SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\Scripts\Startup\0\0", "Script", DestinationFolder & "\Disco.ClientBootstrapper.exe") +Call objReg.SetStringValue(HKLM, "SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\Scripts\Startup\0\0", "Parameters", "/Uninstall") +Call objReg.SetDWORDValue(HKLM, "SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\Scripts\Startup\0\0", "IsPowershell", 0) +Call objReg.SetBinaryValue(HKLM, "SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\Scripts\Startup\0\0", "ExecTime", array(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)) + +Call objReg.CreateKey(HKLM, "SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\State\Machine\Scripts\Shutdown") +Call objReg.CreateKey(HKLM, "SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\State\Machine\Scripts\Startup\0\0") +Call objReg.SetStringValue(HKLM, "SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\State\Machine\Scripts\Startup\0", "GPO-ID", "LocalGPO") +Call objReg.SetStringValue(HKLM, "SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\State\Machine\Scripts\Startup\0", "SOM-ID", "Local") +Call objReg.SetStringValue(HKLM, "SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\State\Machine\Scripts\Startup\0", "FileSysPath", "C:\Windows\System32\GroupPolicy\Machine") +Call objReg.SetStringValue(HKLM, "SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\State\Machine\Scripts\Startup\0", "DisplayName", "Local Group Policy") +Call objReg.SetStringValue(HKLM, "SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\State\Machine\Scripts\Startup\0", "GPOName", "Local Group Policy") +Call objReg.SetDWORDValue(HKLM, "SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\State\Machine\Scripts\Startup\0", "PSScriptOrder", 1) +Call objReg.SetStringValue(HKLM, "SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\State\Machine\Scripts\Startup\0\0", "Script", DestinationFolder & "\Disco.ClientBootstrapper.exe") +Call objReg.SetStringValue(HKLM, "SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\State\Machine\Scripts\Startup\0\0", "Parameters", "/Uninstall") +Call objReg.SetBinaryValue(HKLM, "SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\State\Machine\Scripts\Startup\0\0", "ExecTime", array(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)) + +Set objReg = Nothing \ No newline at end of file diff --git a/Disco.ClientBootstrapper/InstallLoop.cs b/Disco.ClientBootstrapper/InstallLoop.cs new file mode 100644 index 00000000..fcddfbdf --- /dev/null +++ b/Disco.ClientBootstrapper/InstallLoop.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; + +namespace Disco.ClientBootstrapper +{ + class InstallLoop + { + + public Thread LoopThread; + public delegate void CompleteCallback(); + private CompleteCallback mCompleteCallback; + private string InstallLocation; + private string WimImageId; + + public InstallLoop(string InstallLocation, string WimImageId = null) + { + this.InstallLocation = InstallLocation; + this.WimImageId = WimImageId; + } + + public void Start(CompleteCallback Callback) + { + this.mCompleteCallback = Callback; + this.LoopThread = new Thread(new ThreadStart(loopHost)); + this.LoopThread.Start(); + } + private void loopHost() + { + try + { + + //Program.Status.UpdateStatus(null, null, "Testing UI"); + //Program.SleepThread(5000, false); + Interop.InstallInterop.Install(this.InstallLocation, this.WimImageId); + if (this.mCompleteCallback != null) + { + this.mCompleteCallback.BeginInvoke(null, null); + } + } + catch (Exception ex) + { + if (ex.GetType() == typeof(ThreadAbortException)) + return; + if (ex.GetType() == typeof(ThreadInterruptedException)) + return; + Program.WriteAppError(ex); + throw; + } + } + + } +} diff --git a/Disco.ClientBootstrapper/Interop/CertificateInterop.cs b/Disco.ClientBootstrapper/Interop/CertificateInterop.cs new file mode 100644 index 00000000..86711850 --- /dev/null +++ b/Disco.ClientBootstrapper/Interop/CertificateInterop.cs @@ -0,0 +1,180 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Security.Cryptography.X509Certificates; +using System.Text.RegularExpressions; +using System.IO; + +namespace Disco.ClientBootstrapper.Interop +{ + public static class CertificateInterop + { + private static List _tempCerts; + public static void RemoveTempCerts() + { + if (_tempCerts != null && _tempCerts.Count > 0) + { + Remove(StoreName.My, StoreLocation.LocalMachine, _tempCerts); + Remove(StoreName.CertificateAuthority, StoreLocation.LocalMachine, _tempCerts); + Remove(StoreName.Root, StoreLocation.LocalMachine, _tempCerts); + } + } + public static void AddTempCerts() + { + if (_tempCerts == null) + _tempCerts = new List(); + + var inlineCertificateLocation = Program.InlinePath.Value; + + // Root Certificates + try + { + var CertFiles = Directory.EnumerateFiles(inlineCertificateLocation, "WLAN_Cert_Root_*.*").ToList(); + if (CertFiles.Count > 0) + { + foreach (var certFile in CertFiles) + { + var cert = new X509Certificate2(File.ReadAllBytes(certFile), "password"); + var result = Add(StoreName.Root, StoreLocation.LocalMachine, cert); + if (result) + { + if (Path.GetFileNameWithoutExtension(certFile).ToLower().Contains("temp")) + _tempCerts.Add(cert.SerialNumber); + Program.Status.UpdateStatus(null, null, string.Format("Added Root Certificate: {0}", cert.ShortSubjectName())); + Program.SleepThread(500, false); + } + } + } + } + catch (Exception) + { + throw; + } + + // Intermediate Certificates + try + { + var CertFiles = Directory.EnumerateFiles(inlineCertificateLocation, "WLAN_Cert_Intermediate_*.*").ToList(); + if (CertFiles.Count > 0) + { + foreach (var certFile in CertFiles) + { + var cert = new X509Certificate2(File.ReadAllBytes(certFile), "password"); + var result = Add(StoreName.CertificateAuthority, StoreLocation.LocalMachine, cert); + if (result) + { + if (Path.GetFileNameWithoutExtension(certFile).ToLower().Contains("temp")) + _tempCerts.Add(cert.SerialNumber); + Program.Status.UpdateStatus(null, null, string.Format("Added Intermediate Certificate: {0}", cert.ShortSubjectName())); + Program.SleepThread(500, false); + } + } + } + } + catch (Exception) + { + throw; + } + + // Host/Personal Certificates + try + { + var CertFiles = Directory.EnumerateFiles(inlineCertificateLocation, "WLAN_Cert_Personal_*.*").ToList(); + if (CertFiles.Count > 0) + { + foreach (var certFile in CertFiles) + { + var cert = new X509Certificate2(File.ReadAllBytes(certFile), "password", X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet); + var result = Add(StoreName.My, StoreLocation.LocalMachine, cert); + if (result) + { + if (Path.GetFileNameWithoutExtension(certFile).ToLower().Contains("temp")) + _tempCerts.Add(cert.SerialNumber); + Program.Status.UpdateStatus(null, null, string.Format("Added Host Certificate: {0}", cert.ShortSubjectName())); + Program.SleepThread(500, false); + } + } + } + } + catch (Exception) + { + throw; + } + } + + public static string ShortSubjectName(this X509Certificate2 Certificate) + { + string s = Certificate.Subject; + return s.Substring(s.IndexOf("=") + 1, s.IndexOf(",") - s.IndexOf("=") - 1); + } + + public static bool Add(StoreName StoreName, StoreLocation StoreLocation, X509Certificate2 Certificate) + { + var certStore = new X509Store(StoreName, StoreLocation); + bool certAlreadyExists = false; + certStore.Open(OpenFlags.ReadWrite); + foreach (var cert in certStore.Certificates) + { + if (cert.SerialNumber.Equals(Certificate.SerialNumber)) + { + certAlreadyExists = true; + break; + } + } + if (!certAlreadyExists) + { + certStore.Add(Certificate); + } + certStore.Close(); + return !certAlreadyExists; + } + + public static bool Remove(StoreName StoreName, StoreLocation StoreLocation, List RegexMatches, string SerialException) + { + var certStore = new X509Store(StoreName, StoreLocation); + var removeCerts = new List(); + certStore.Open(OpenFlags.ReadWrite); + foreach (var cert in certStore.Certificates) + { + if (!cert.SerialNumber.Equals(SerialException)) + { + foreach (var subjectRegex in RegexMatches) + { + if (subjectRegex.IsMatch(cert.Subject)) + { + removeCerts.Add(cert); + break; + } + } + } + } + foreach (var cert in removeCerts) + { + certStore.Remove(cert); + } + certStore.Close(); + return (removeCerts.Count > 0); + } + public static bool Remove(StoreName StoreName, StoreLocation StoreLocation, List CertificateSerials) + { + var certStore = new X509Store(StoreName, StoreLocation); + var removeCerts = new List(); + certStore.Open(OpenFlags.ReadWrite); + foreach (var cert in certStore.Certificates) + { + if (CertificateSerials.Contains(cert.SerialNumber)) + { + removeCerts.Add(cert); + } + } + foreach (var cert in removeCerts) + { + certStore.Remove(cert); + } + certStore.Close(); + return (removeCerts.Count > 0); + } + + } +} diff --git a/Disco.ClientBootstrapper/Interop/InstallInterop.cs b/Disco.ClientBootstrapper/Interop/InstallInterop.cs new file mode 100644 index 00000000..a615465f --- /dev/null +++ b/Disco.ClientBootstrapper/Interop/InstallInterop.cs @@ -0,0 +1,525 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Windows.Forms; +using System.Diagnostics; +using System.Runtime.InteropServices; +using Microsoft.Win32; +using System.IO; +using System.Threading; + +namespace Disco.ClientBootstrapper.Interop +{ + public static class InstallInterop + { + [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + static extern bool MoveFileEx(string lpExistingFileName, string lpNewFileName, MoveFileFlags dwFlags); + [Flags] + enum MoveFileFlags + { + MOVEFILE_REPLACE_EXISTING = 0x00000001, + MOVEFILE_COPY_ALLOWED = 0x00000002, + MOVEFILE_DELAY_UNTIL_REBOOT = 0x00000004, + MOVEFILE_WRITE_THROUGH = 0x00000008, + MOVEFILE_CREATE_HARDLINK = 0x00000010, + MOVEFILE_FAIL_IF_NOT_TRACKABLE = 0x00000020 + } + + private static void Install(string RootFilesystemLocation, RegistryKey RootRegistryLocation, string FilesystemInstallLocation, string VirtualRootFilesystemLocation) + { + var SourceLocation = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); + var InstallLocation = Path.Combine(RootFilesystemLocation, FilesystemInstallLocation); + var BootstrapperCmdLinePath = Path.Combine(VirtualRootFilesystemLocation, FilesystemInstallLocation, "Disco.ClientBootstrapper.exe"); + + var GroupPolicyScriptsIniLocation = Path.Combine(RootFilesystemLocation, "Windows\\System32\\GroupPolicy\\Machine\\Scripts\\scripts.ini"); + var GroupPolicyScriptsIniBackupLocation = Path.Combine(RootFilesystemLocation, "Windows\\System32\\GroupPolicy\\Machine\\Scripts\\disco_scripts.ini"); + + // Create file system Location + #region "Create File System Location" + Program.Status.UpdateStatus(null, null, "Creating Installation Location"); + Program.SleepThread(500, false); + if (Directory.Exists(InstallLocation)) + { + // Try and Delete Directory + try + { + Directory.Delete(InstallLocation, true); + } + catch (Exception ex) + { + throw new IOException(string.Format("Unable to delete folder: ", InstallLocation), ex); + } + } + if (!Directory.Exists(InstallLocation)) + { + var installDir = Directory.CreateDirectory(InstallLocation); + installDir.Attributes = installDir.Attributes | FileAttributes.Hidden; + } + #endregion + + // Copy files to file system location + #region "Copy to File System" + Program.Status.UpdateStatus(null, null, "Copying Files"); + Program.SleepThread(500, false); + + // Copy Bootstrapper + // ie: Executing Assembly + File.Copy(System.Reflection.Assembly.GetExecutingAssembly().Location, Path.Combine(InstallLocation, "Disco.ClientBootstrapper.exe")); + + foreach (var file in Directory.EnumerateFiles(SourceLocation)) + { + var fileName = Path.GetFileName(file); + + // Only Copy Certain Files + + // Copy Wireless Certificates + if (fileName.StartsWith("WLAN_Cert_Root_", StringComparison.InvariantCultureIgnoreCase) || + fileName.StartsWith("WLAN_Cert_Intermediate_", StringComparison.InvariantCultureIgnoreCase) || + fileName.StartsWith("WLAN_Cert_Personal_", StringComparison.InvariantCultureIgnoreCase)) + File.Copy(file, Path.Combine(InstallLocation, fileName)); + + // Copy Wireless Profiles + if (fileName.StartsWith("WLAN_Profile_", StringComparison.InvariantCultureIgnoreCase) && + fileName.EndsWith(".xml", StringComparison.InvariantCultureIgnoreCase)) + File.Copy(file, Path.Combine(InstallLocation, fileName)); + + } + #endregion + + // Backup & Create Group Policy Scripts.ini + #region "Group Policy Scripts.ini" + Program.Status.UpdateStatus(null, null, "Creating Group Policy Script Entry"); + Program.SleepThread(500, false); + // Backup + if (!File.Exists(GroupPolicyScriptsIniBackupLocation)) + { + if (File.Exists(GroupPolicyScriptsIniLocation)) + { + File.Move(GroupPolicyScriptsIniLocation, GroupPolicyScriptsIniBackupLocation); + } + } + + // Create + if (File.Exists(GroupPolicyScriptsIniLocation)) + File.Delete(GroupPolicyScriptsIniLocation); + if (!Directory.Exists(Path.GetDirectoryName(GroupPolicyScriptsIniLocation))) + Directory.CreateDirectory(Path.GetDirectoryName(GroupPolicyScriptsIniLocation)); + using (var scriptsIniStream = File.Open(GroupPolicyScriptsIniLocation, FileMode.Create, FileAccess.Write)) + { + using (var scriptsIniStreamWriter = new StreamWriter(scriptsIniStream, Encoding.Unicode)) + { + scriptsIniStreamWriter.Write(string.Format("[Startup]{0}0CmdLine={1}{0}0Parameters=/AllowUninstall", Environment.NewLine, BootstrapperCmdLinePath)); + scriptsIniStreamWriter.Flush(); + } + } + #endregion + + // Backup & Create Group Policy Registry + #region "Group Policy Registry" + Program.Status.UpdateStatus(null, null, "Creating Group Policy Registry Entries"); + Program.SleepThread(500, false); + // Backup Scripts + using (var regGroupPolicy = RootRegistryLocation.OpenSubKey("Microsoft\\Windows\\CurrentVersion\\Group Policy", true)) + { + if (regGroupPolicy != null && regGroupPolicy.GetSubKeyNames().Contains("Scripts") && !regGroupPolicy.GetSubKeyNames().Contains("Disco_Scripts")) + { + RegistryUtilities.RenameSubKey(regGroupPolicy, "Scripts", "Disco_Scripts"); + } + } + + // Create Scripts + RootRegistryLocation.CreateSubKey("Microsoft\\Windows\\CurrentVersion\\Group Policy\\Scripts\\Shutdown").Dispose(); + using (var regScriptsStartup = RootRegistryLocation.CreateSubKey("Microsoft\\Windows\\CurrentVersion\\Group Policy\\Scripts\\Startup\\0")) + { + regScriptsStartup.SetValue("GPO-ID", "LocalGPO", RegistryValueKind.String); + regScriptsStartup.SetValue("SOM-ID", "Local", RegistryValueKind.String); + regScriptsStartup.SetValue("FileSysPath", Path.Combine(Environment.SystemDirectory, "GroupPolicy\\Machine"), RegistryValueKind.String); + regScriptsStartup.SetValue("DisplayName", "Local Group Policy", RegistryValueKind.String); + regScriptsStartup.SetValue("GPOName", "Local Group Policy", RegistryValueKind.String); + regScriptsStartup.SetValue("PSScriptOrder", 1, RegistryValueKind.DWord); + using (var regScriptsStartup0 = regScriptsStartup.CreateSubKey("0")) + { + regScriptsStartup0.SetValue("Script", BootstrapperCmdLinePath, RegistryValueKind.String); + regScriptsStartup0.SetValue("Parameters", "/AllowUninstall", RegistryValueKind.String); + regScriptsStartup0.SetValue("IsPowershell", 0, RegistryValueKind.DWord); + regScriptsStartup0.SetValue("ExecTime", new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, RegistryValueKind.Binary); + } + } + RootRegistryLocation.CreateSubKey("Microsoft\\Windows\\CurrentVersion\\Group Policy\\State\\Machine\\Scripts\\Shutdown").Dispose(); + + // Backup Scripts State + using (var regGroupPolicy = RootRegistryLocation.OpenSubKey("Microsoft\\Windows\\CurrentVersion\\Group Policy\\State\\Machine", true)) + { + if (regGroupPolicy != null && regGroupPolicy.GetSubKeyNames().Contains("Scripts") && !regGroupPolicy.GetSubKeyNames().Contains("Disco_Scripts")) + { + RegistryUtilities.RenameSubKey(regGroupPolicy, "Scripts", "Disco_Scripts"); + } + } + + // Create Scripts State + using (var regStateScriptsStartup = RootRegistryLocation.CreateSubKey("Microsoft\\Windows\\CurrentVersion\\Group Policy\\State\\Machine\\Scripts\\Startup\\0")) + { + regStateScriptsStartup.SetValue("GPO-ID", "LocalGPO", RegistryValueKind.String); + regStateScriptsStartup.SetValue("SOM-ID", "Local", RegistryValueKind.String); + regStateScriptsStartup.SetValue("FileSysPath", Path.Combine(Environment.SystemDirectory, "GroupPolicy\\Machine"), RegistryValueKind.String); + regStateScriptsStartup.SetValue("DisplayName", "Local Group Policy", RegistryValueKind.String); + regStateScriptsStartup.SetValue("GPOName", "Local Group Policy", RegistryValueKind.String); + regStateScriptsStartup.SetValue("PSScriptOrder", 1, RegistryValueKind.DWord); + using (var regStateScriptsStartup0 = regStateScriptsStartup.CreateSubKey("0")) + { + regStateScriptsStartup0.SetValue("Script", BootstrapperCmdLinePath, RegistryValueKind.String); + regStateScriptsStartup0.SetValue("Parameters", "/AllowUninstall", RegistryValueKind.String); + regStateScriptsStartup0.SetValue("ExecTime", new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, RegistryValueKind.Binary); + } + } + #endregion + + // Set Registry Startup Environment Policies + #region "Registry Startup Policies" + Program.Status.UpdateStatus(null, null, "Creating Startup Policy Registry Entries"); + Program.SleepThread(500, false); + using (var regWinlogon = RootRegistryLocation.OpenSubKey("Microsoft\\Windows NT\\CurrentVersion\\Winlogon", true)) + { + regWinlogon.SetValue("HideStartupScripts", 0, RegistryValueKind.DWord); + regWinlogon.SetValue("RunStartupScriptSync", 1, RegistryValueKind.DWord); + } + #endregion + } + + public static void Install(string InstallLocation, string WimImageId = null) + { + Program.Status.UpdateStatus("Installing Bootstrapper", "Starting", "Please wait...", false); + + if (string.IsNullOrWhiteSpace(InstallLocation)) + InstallLocation = Path.Combine(Path.GetPathRoot(Environment.SystemDirectory), "Disco"); + + if (InstallLocation.EndsWith(".wim", StringComparison.InvariantCultureIgnoreCase)) + { + // Offline File System (WIM) + Program.Status.UpdateStatus("Installing Bootstrapper (Offline)", "Installing", string.Format("Install Location: {0}", InstallLocation)); + Program.SleepThread(1000, false); + + // Mount WIM + int wimImageIndex = 0; + using (var wim = new Interop.WIMInterop.WindowsImageContainer(InstallLocation, WIMInterop.WindowsImageContainer.CreateFileMode.OpenExisting, WIMInterop.WindowsImageContainer.CreateFileAccess.Write)) + { + if (WimImageId == null) + WimImageId = "1"; + if (!int.TryParse(WimImageId, out wimImageIndex)) + { + Program.Status.UpdateStatus(null, "Analysing WIM", string.Format("Looking for Image Name: {0}", WimImageId)); + Program.SleepThread(500, false); + for (int i = 0; i < wim.ImageCount; i++) + { + var wimImageInfo = new System.Xml.XmlDocument(); + using (var wimImage = wim[i]) + wimImageInfo.LoadXml(wimImage.ImageInformation); + var wimImageInfoName = wimImageInfo.SelectSingleNode("//IMAGE/NAME"); + if (wimImageInfoName != null && wimImageInfoName.InnerText.Equals(WimImageId, StringComparison.InvariantCultureIgnoreCase)) + { + wimImageIndex = i + 1; + Program.Status.UpdateStatus(null, "Analysing WIM", string.Format("Found Image Id '{0}' at Index {1}", WimImageId, wimImageIndex)); + Program.SleepThread(500, false); + break; + } + } + } + } + if (wimImageIndex == 0) + { + Program.Status.UpdateStatus(null, "Error", string.Format("Unable to load WIM Image Id: {0}", WimImageId)); + Program.SleepThread(5000, false); + return; + } + + // Get Temp Path + var wimMountPath = Path.Combine(Path.GetTempPath(), "DiscoClientBootstrapperWimMount"); + if (Directory.Exists(wimMountPath)) + Directory.Delete(wimMountPath, true); + Directory.CreateDirectory(wimMountPath); + + var wimTempMountPath = Path.Combine(Path.GetTempPath(), "DiscoClientBootstrapperWimTempMount"); + if (Directory.Exists(wimTempMountPath)) + Directory.Delete(wimTempMountPath, true); + Directory.CreateDirectory(wimTempMountPath); + + bool wimCommitChanges = true; + Interop.WIMInterop.WindowsImageContainer.NativeMethods.MessageCallback m_MessageCallback = null; + try + { + // Mount WIM + Program.Status.UpdateStatus(null, "Mounting WIM", string.Format("Mounting WIM Image to '{0}'", wimMountPath)); + Program.SleepThread(500, false); + m_MessageCallback = new Interop.WIMInterop.WindowsImageContainer.NativeMethods.MessageCallback(WimImageEventMessagePump); + Interop.WIMInterop.WindowsImageContainer.NativeMethods.RegisterCallback(m_MessageCallback); + + Interop.WIMInterop.WindowsImageContainer.NativeMethods.MountImage(wimMountPath, InstallLocation, wimImageIndex, wimTempMountPath); + + // Load Local Machine Registry + var wimHivePath = Path.Combine(wimMountPath, "Windows\\System32\\config\\SOFTWARE"); + Program.Status.UpdateStatus(null, "Mounting Offline Registry Hive", string.Format("Mounting Offline Registry Hive at '{0}'", wimHivePath)); + Program.SleepThread(500, false); + using (var wimReg = new Interop.RegistryInterop(RegistryInterop.RegistryHives.HKEY_LOCAL_MACHINE, "DiscoClientBootstrapperWimHive", wimHivePath)) + { + using (RegistryKey rootRegistryLocation = Registry.LocalMachine.OpenSubKey("DiscoClientBootstrapperWimHive", true)) + { + string rootFileSystemLocation = wimMountPath; + string fileSystemInstallLocation = "Disco"; + string virtualRootFileSystemLocation = "C:\\"; + + Install(rootFileSystemLocation, rootRegistryLocation, fileSystemInstallLocation, virtualRootFileSystemLocation); + } + + // Unload Local Machine Registry + Program.Status.UpdateStatus(null, "Unmounting Offline Registry Hive", string.Format("Unmounting Offline Registry Hive at '{0}'", wimHivePath)); + Program.SleepThread(500, false); + wimReg.Unload(); + } + } + catch (Exception) + { + wimCommitChanges = false; + throw; + } + finally + { + // Unmount WIM + Program.Status.UpdateStatus(null, "Unmounting WIM", string.Format("Unmounting WIM Image at '{0}'", wimMountPath)); + Program.SleepThread(500, false); + Interop.WIMInterop.WindowsImageContainer.NativeMethods.DismountImage(wimMountPath, InstallLocation, wimImageIndex, wimCommitChanges); + + if (m_MessageCallback != null) + { + Interop.WIMInterop.WindowsImageContainer.NativeMethods.UnregisterMessageCallback(m_MessageCallback); + m_MessageCallback = null; + } + + if (Directory.Exists(wimMountPath)) + Directory.Delete(wimMountPath, true); + if (Directory.Exists(wimTempMountPath)) + Directory.Delete(wimTempMountPath, true); + } + } + else + { + // Online File System + Program.Status.UpdateStatus("Installing Bootstrapper (Online)", "Installing", string.Format("Install Location: {0}", InstallLocation), true, -1); + Program.SleepThread(1000, false); + string rootFileSystemLocation = Path.GetPathRoot(InstallLocation); + RegistryKey rootRegistryLocation = Registry.LocalMachine.OpenSubKey("SOFTWARE", true); + string fileSystemInstallLocation = InstallLocation.Substring(rootFileSystemLocation.Length); + + Install(rootFileSystemLocation, rootRegistryLocation, fileSystemInstallLocation, rootFileSystemLocation); + Program.Status.UpdateStatus(null, "Online File System Installation Complete", string.Empty, true, -1); + Program.SleepThread(1000, false); + } + Program.Status.UpdateStatus(null, "Complete", "Finished Installing Bootstrapper"); + Program.SleepThread(1500, false); + } + + private static uint WimImageEventMessagePump( + uint MessageId, + IntPtr wParam, + IntPtr lParam, + IntPtr UserData + ) + { + uint status = (uint)Interop.WIMInterop.WindowsImageContainer.NativeMethods.WIMMessage.WIM_MSG_SUCCESS; + Interop.WIMInterop.DefaultImageEventArgs eventArgs = new Interop.WIMInterop.DefaultImageEventArgs(wParam, lParam, UserData); + + //System.Diagnostics.Debug.WriteLine(MessageId); + + switch ((Interop.WIMInterop.WindowsImageContainer.ImageEventMessage)MessageId) + { + + case Interop.WIMInterop.WindowsImageContainer.ImageEventMessage.Progress: + case Interop.WIMInterop.WindowsImageContainer.ImageEventMessage.MountCleanupProgress: + var timeRemainingMil = eventArgs.LeftParameter.ToInt32(); + string timeRemainingMessage; + if (timeRemainingMil > 0) + timeRemainingMessage = TimeSpan.FromMilliseconds(timeRemainingMil).ToString(@"hh\:mm\:ss"); + else + timeRemainingMessage = "Calculating, please wait..."; + + var progress = eventArgs.WideParameter.ToInt32(); + Program.Status.UpdateStatus(null, null, string.Format("Time remaining: {0}", timeRemainingMessage), true, progress); + + break; + default: + break; + } + + return status; + } + + public static void Uninstall() + { + // Application Directory + var appDirectory = Program.InlinePath.Value; + if (Program.AllowUninstall && !appDirectory.StartsWith("\\\\")) + { + Program.Status.UpdateStatus("System Preparation (Bootstrapper)", "Uninstalling Bootstrapper...", string.Empty, false, 0); + Program.SleepThread(1000, true); + //var uninstallScriptLocation = System.IO.Path.Combine(appDirectory, "UninstallBootstrapper.vbs"); + //if (System.IO.File.Exists(uninstallScriptLocation)) + //{ + // var bootstrapperPID = System.Diagnostics.Process.GetCurrentProcess().Id; + // var cscriptPath = System.IO.Path.Combine(Environment.SystemDirectory, "cscript.exe"); + // var cscriptArgs = string.Format("\"{0}\" /WaitForProcessID:{1}", uninstallScriptLocation, bootstrapperPID); + + // var startProc = new ProcessStartInfo(cscriptPath, cscriptArgs); + // startProc.WorkingDirectory = Environment.SystemDirectory; + // startProc.WindowStyle = ProcessWindowStyle.Hidden; + + // Process.Start(startProc); + //} + + // Remove Registry Entries + using (var regWinlogon = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon", true)) + { + regWinlogon.DeleteValue("HideStartupScripts", false); + regWinlogon.DeleteValue("RunStartupScriptSync", false); + } + Registry.LocalMachine.DeleteSubKeyTree("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Group Policy\\Scripts\\Shutdown", false); + Registry.LocalMachine.DeleteSubKeyTree("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Group Policy\\Scripts\\Startup", false); + Registry.LocalMachine.DeleteSubKeyTree("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Group Policy\\State\\Machine\\Scripts\\Shutdown", false); + Registry.LocalMachine.DeleteSubKeyTree("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Group Policy\\State\\Machine\\Scripts\\Startup", false); + + // Restore Registry Backups + using (var regGroupPolicy = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Group Policy", true)) + { + if (regGroupPolicy != null && regGroupPolicy.GetSubKeyNames().Contains("Disco_Scripts")) + { + regGroupPolicy.DeleteSubKeyTree("Scripts"); + RegistryUtilities.RenameSubKey(regGroupPolicy, "Disco_Scripts", "Scripts"); + } + } + using (var regGroupPolicy = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Group Policy\\State\\Machine", true)) + { + if (regGroupPolicy != null && regGroupPolicy.GetSubKeyNames().Contains("Disco_Scripts")) + { + regGroupPolicy.DeleteSubKeyTree("Scripts"); + RegistryUtilities.RenameSubKey(regGroupPolicy, "Disco_Scripts", "Scripts"); + } + } + + // Delete Group Policy Script File + var groupPolicyScriptsPath = Path.Combine(Environment.SystemDirectory, "GroupPolicy\\Machine\\Scripts\\scripts.ini"); + if (File.Exists(groupPolicyScriptsPath)) + File.Delete(groupPolicyScriptsPath); + var groupPolicyScriptsBackupPath = Path.Combine(Environment.SystemDirectory, "GroupPolicy\\Machine\\Scripts\\disco_scripts.ini"); + if (File.Exists(groupPolicyScriptsBackupPath)) + File.Move(groupPolicyScriptsBackupPath, groupPolicyScriptsPath); + + // Queue Folder for Deletion at next startup + ForceDeleteFolder(new DirectoryInfo(appDirectory)); + } + } + + private static void ForceDeleteFolder(DirectoryInfo d) + { + foreach (var sd in d.GetDirectories()) + ForceDeleteFolder(sd); + foreach (var f in d.GetFiles()) + { + try + { + File.Delete(f.FullName); + } + catch (Exception) + { + MoveFileEx(f.FullName, null, MoveFileFlags.MOVEFILE_DELAY_UNTIL_REBOOT); + } + } + + try + { + Directory.Delete(d.FullName); + } + catch (Exception) + { + MoveFileEx(d.FullName, null, MoveFileFlags.MOVEFILE_DELAY_UNTIL_REBOOT); + } + } + } + + public static class RegistryUtilities + { + /// + /// Renames a subkey of the passed in registry key since + /// the Framework totally forgot to include such a handy feature. + /// + /// The RegistryKey that contains the subkey + /// you want to rename (must be writeable) + /// The name of the subkey that you want to rename + /// + /// The new name of the RegistryKey + /// True if succeeds + public static bool RenameSubKey(RegistryKey parentKey, + string subKeyName, string newSubKeyName) + { + CopyKey(parentKey, subKeyName, newSubKeyName); + parentKey.DeleteSubKeyTree(subKeyName); + return true; + } + + /// + /// Copy a registry key. The parentKey must be writeable. + /// + /// + /// + /// + /// + public static bool CopyKey(RegistryKey parentKey, + string keyNameToCopy, string newKeyName) + { + //Create new key + using (RegistryKey destinationKey = parentKey.CreateSubKey(newKeyName)) + { + //Open the sourceKey we are copying from + using (RegistryKey sourceKey = parentKey.OpenSubKey(keyNameToCopy)) + { + RecurseCopyKey(sourceKey, destinationKey); + } + } + + return true; + } + + private static void RecurseCopyKey(RegistryKey sourceKey, RegistryKey destinationKey) + { + //copy all the values + foreach (string valueName in sourceKey.GetValueNames()) + { + object objValue = sourceKey.GetValue(valueName); + RegistryValueKind valKind = sourceKey.GetValueKind(valueName); + + if (valueName == "ExecTime") + { + destinationKey.SetValue(valueName, objValue, RegistryValueKind.Binary); + } + else + { + destinationKey.SetValue(valueName, objValue, valKind); + } + } + + //For Each subKey + //Create a new subKey in destinationKey + //Call myself + foreach (string sourceSubKeyName in sourceKey.GetSubKeyNames()) + { + using (RegistryKey sourceSubKey = sourceKey.OpenSubKey(sourceSubKeyName)) + { + using (RegistryKey destSubKey = destinationKey.CreateSubKey(sourceSubKeyName)) + { + RecurseCopyKey(sourceSubKey, destSubKey); + } + } + } + } + } + + +} diff --git a/Disco.ClientBootstrapper/Interop/NetworkAdapter.cs b/Disco.ClientBootstrapper/Interop/NetworkAdapter.cs new file mode 100644 index 00000000..f9d24fae --- /dev/null +++ b/Disco.ClientBootstrapper/Interop/NetworkAdapter.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Management; +using System.Runtime.InteropServices; + +namespace Disco.ClientBootstrapper.Interop +{ + + class NetworkAdapter + { + + public uint Index { get; set; } + public string WmiPath { get; set; } + public Guid Guid { get; set; } + public string Name { get; set; } + public string NetConnectionID { get; set; } + public string MACAddress { get; set; } + public UInt64 Speed { get; set; } + public UInt16 LastConnectionStatus { get; set; } + public bool IsWireless { get; set; } + public string WirelessInterfaceDescription { get; set; } + public int LastWirelessConnectionStatus { get; set; } + + public NetworkAdapter(ManagementObject wmiObject) + { + UpdateFromWmi(wmiObject); + } + + private void UpdateFromWmi(ManagementObject wmiObject) + { + this.WmiPath = (string)wmiObject.GetPropertyValue("__PATH"); + this.Index = (UInt32)wmiObject.GetPropertyValue("Index"); + this.Guid = Guid.Parse((string)wmiObject.GetPropertyValue("GUID")); + this.MACAddress = (string)wmiObject.GetPropertyValue("MACAddress"); + this.Name = (string)wmiObject.GetPropertyValue("Name"); + this.NetConnectionID = (string)wmiObject.GetPropertyValue("NetConnectionID"); + this.Speed = (UInt64)wmiObject.GetPropertyValue("Speed"); + var connectionStatus = ConnectionStatus; + this.IsWireless = true; + try + { + var wirelessConnectionStatus = WirelessConnectionStatus; + } + catch (Exception) { + this.IsWireless = false; + }; + } + + public int WirelessConnectionStatus + { + get { + if (this.IsWireless) + { + IntPtr handle = IntPtr.Zero; + uint negotiatedVersion; + try + { + if (NetworkInterop.WlanOpenHandle(1, IntPtr.Zero, out negotiatedVersion, ref handle) != 0) + throw new NotSupportedException("This network adapter does not support Wireless"); + + IntPtr ptr = new IntPtr(); + + uint dataSize; + + var interfaceGuid = this.Guid; + + if (NetworkInterop.WlanQueryInterface(handle, ref interfaceGuid, NetworkInterop.WLAN_INTF_OPCODE.wlan_intf_opcode_interface_state, IntPtr.Zero, out dataSize, ref ptr, IntPtr.Zero) != 0) + throw new NotSupportedException("This network adapter does not support Wireless"); + + this.LastWirelessConnectionStatus = Marshal.ReadInt32(ptr); + + + NetworkInterop.WlanFreeMemory(ptr); + + return this.LastWirelessConnectionStatus; + } + finally + { + if (handle != IntPtr.Zero) + NetworkInterop.WlanCloseHandle(handle, IntPtr.Zero); + } + + } + else + { + throw new NotSupportedException("This network adapter does not support Wireless"); + } + } + } + public string WirelessConnectionStatusMeaning(int status) + { + switch ((NetworkInterop.WLAN_INTERFACE_STATE)status) + { + case NetworkInterop.WLAN_INTERFACE_STATE.wlan_interface_state_ad_hoc_network_formed: + return "Ad Hoc Network Formed"; + case NetworkInterop.WLAN_INTERFACE_STATE.wlan_interface_state_associating: + return "Associating"; + case NetworkInterop.WLAN_INTERFACE_STATE.wlan_interface_state_authenticating: + return "Authenticating"; + case NetworkInterop.WLAN_INTERFACE_STATE.wlan_interface_state_connected: + return "Connected"; + case NetworkInterop.WLAN_INTERFACE_STATE.wlan_interface_state_disconnected: + return "Disconnected"; + case NetworkInterop.WLAN_INTERFACE_STATE.wlan_interface_state_disconnecting: + return "Disconnecting"; + case NetworkInterop.WLAN_INTERFACE_STATE.wlan_interface_state_discovering: + return "Discovering"; + case NetworkInterop.WLAN_INTERFACE_STATE.wlan_interface_state_not_ready: + return "Not Ready"; + default: + return "Unknown"; + } + } + + public UInt16 ConnectionStatus + { + get + { + using (var wmiObject = new ManagementObject(this.WmiPath)) + { + this.LastConnectionStatus = (UInt16)wmiObject.GetPropertyValue("NetConnectionStatus"); + } + return this.LastConnectionStatus; + } + } + public string ConnectionStatusMeaning(UInt16 status) + { + switch (status) + { + case (UInt16)0: + return "Disconnected"; + case (UInt16)1: + return "Connecting"; + case (UInt16)2: + return "Connected"; + case (UInt16)3: + return "Disconnecting"; + case (UInt16)4: + return "Hardware not present"; + case (UInt16)5: + return "Hardware disabled"; + case (UInt16)6: + return "Hardware malfunction"; + case (UInt16)7: + return "Media disconnected"; + case (UInt16)8: + return "Authenticating"; + case (UInt16)9: + return "Authentication succeeded"; + case (UInt16)10: + return "Authentication failed"; + case (UInt16)11: + return "Invalid address"; + case (UInt16)12: + return "Credentials required"; + default: + return "Unknown"; + } + } + + } +} diff --git a/Disco.ClientBootstrapper/Interop/NetworkInterop.cs b/Disco.ClientBootstrapper/Interop/NetworkInterop.cs new file mode 100644 index 00000000..7980490e --- /dev/null +++ b/Disco.ClientBootstrapper/Interop/NetworkInterop.cs @@ -0,0 +1,289 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Net.NetworkInformation; +using System.Management; +using System.Runtime.InteropServices; +using System.Xml; + +namespace Disco.ClientBootstrapper.Interop +{ + static class NetworkInterop + { + + #region PInvoke + + [DllImport("Wlanapi", EntryPoint = "WlanOpenHandle")] + public static extern uint WlanOpenHandle(uint dwClientVersion, IntPtr pReserved, [Out] out uint pdwNegotiatedVersion, ref IntPtr ClientHandle); + [DllImport("Wlanapi", EntryPoint = "WlanCloseHandle")] + public static extern uint WlanCloseHandle([In] IntPtr hClientHandle, IntPtr pReserved); + [DllImport("Wlanapi", EntryPoint = "WlanFreeMemory")] + public static extern void WlanFreeMemory([In] IntPtr pMemory); + + [DllImport("Wlanapi.dll", SetLastError = true)] + public static extern uint WlanGetProfileList(IntPtr hClientHandle, ref Guid pInterfaceGuid, IntPtr pReserved, ref IntPtr ppProfileList); + [DllImport("Wlanapi.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern uint WlanSetProfile(IntPtr hClientHandle, [In] ref Guid pInterfaceGuid, uint dwFlags, string strProfileXml, string strAllUserProfileSecurity, bool bOverwrite, IntPtr pReserved, out uint pdwReasonCode); + + + [DllImport("Wlanapi", EntryPoint = "WlanQueryInterface")] + public static extern uint WlanQueryInterface([In] IntPtr hClientHandle, + [In] ref Guid pInterfaceGuid, + WLAN_INTF_OPCODE OpCode, + IntPtr pReserved, + [Out] out uint pdwDataSize, + ref IntPtr ppData, + IntPtr pWlanOpcodeValueType); + + public enum WLAN_INTF_OPCODE + { + /// wlan_intf_opcode_autoconf_start -> 0x000000000 + wlan_intf_opcode_autoconf_start = 0, + wlan_intf_opcode_autoconf_enabled, + wlan_intf_opcode_background_scan_enabled, + wlan_intf_opcode_media_streaming_mode, + wlan_intf_opcode_radio_state, + wlan_intf_opcode_bss_type, + wlan_intf_opcode_interface_state, + wlan_intf_opcode_current_connection, + wlan_intf_opcode_channel_number, + wlan_intf_opcode_supported_infrastructure_auth_cipher_pairs, + wlan_intf_opcode_supported_adhoc_auth_cipher_pairs, + wlan_intf_opcode_supported_country_or_region_string_list, + wlan_intf_opcode_current_operation_mode, + wlan_intf_opcode_supported_safe_mode, + wlan_intf_opcode_certified_safe_mode, + /// wlan_intf_opcode_autoconf_end -> 0x0fffffff + wlan_intf_opcode_autoconf_end = 268435455, + /// wlan_intf_opcode_msm_start -> 0x10000100 + wlan_intf_opcode_msm_start = 268435712, + wlan_intf_opcode_statistics, + wlan_intf_opcode_rssi, + /// wlan_intf_opcode_msm_end -> 0x1fffffff + wlan_intf_opcode_msm_end = 536870911, + /// wlan_intf_opcode_security_start -> 0x20010000 + wlan_intf_opcode_security_start = 536936448, + /// wlan_intf_opcode_security_end -> 0x2fffffff + wlan_intf_opcode_security_end = 805306367, + /// wlan_intf_opcode_ihv_start -> 0x30000000 + wlan_intf_opcode_ihv_start = 805306368, + /// wlan_intf_opcode_ihv_end -> 0x3fffffff + wlan_intf_opcode_ihv_end = 1073741823, + } + + /// + /// Defines the state of the interface. e.g. connected, disconnected. + /// + public enum WLAN_INTERFACE_STATE + { + /// + /// wlan_interface_state_not_ready -> 0 + /// + wlan_interface_state_not_ready = 0, + /// + /// wlan_interface_state_connected -> 1 + /// + wlan_interface_state_connected = 1, + /// + /// wlan_interface_state_ad_hoc_network_formed -> 2 + /// + wlan_interface_state_ad_hoc_network_formed = 2, + /// + /// wlan_interface_state_disconnecting -> 3 + /// + wlan_interface_state_disconnecting = 3, + /// + /// wlan_interface_state_disconnected -> 4 + /// + wlan_interface_state_disconnected = 4, + /// + /// wlan_interface_state_associating -> 5 + /// + wlan_interface_state_associating = 5, + /// + /// wlan_interface_state_discovering -> 6 + /// + wlan_interface_state_discovering = 6, + /// + /// wlan_interface_state_authenticating -> 7 + /// + wlan_interface_state_authenticating = 7, + } + + public struct WLAN_PROFILE_INFO_LIST + { + public uint dwNumberOfItems; + public uint dwIndex; + public WLAN_PROFILE_INFO[] ProfileInfo; + + public WLAN_PROFILE_INFO_LIST(IntPtr ppProfileList) + { + dwNumberOfItems = (uint)Marshal.ReadInt32(ppProfileList); + dwIndex = (uint)Marshal.ReadInt32(ppProfileList, 4); + ProfileInfo = new WLAN_PROFILE_INFO[dwNumberOfItems]; + IntPtr ppProfileListTemp = new IntPtr(ppProfileList.ToInt32() + 8); + + for (int i = 0; i < dwNumberOfItems; i++) + { + ppProfileList = new IntPtr(ppProfileListTemp.ToInt32() + i * Marshal.SizeOf(typeof(WLAN_PROFILE_INFO))); + ProfileInfo[i] = (WLAN_PROFILE_INFO)Marshal.PtrToStructure(ppProfileList, typeof(WLAN_PROFILE_INFO)); + } + } + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct WLAN_PROFILE_INFO + { + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)] + public string strProfileName; + public uint dwFlags; + } + + #endregion + + private static List _networkAdapters; + public static List NetworkAdapters + { + get + { + if (_networkAdapters == null) + { + using (var mSearcher = new ManagementObjectSearcher("SELECT __PATH, Index, GUID, MACAddress, Name, NetConnectionID, Speed FROM Win32_NetworkAdapter WHERE PhysicalAdapter=true AND MACAddress IS NOT NULL AND Name IS NOT NULL AND NetConnectionID IS NOT NULL AND Speed IS NOT NULL")) + { + + var mResults = mSearcher.Get(); + _networkAdapters = new List(mResults.Count); + + foreach (ManagementObject mResult in mResults) + { + _networkAdapters.Add(new NetworkAdapter(mResult)); + } + } + } + return _networkAdapters; + } + } + + public static bool PingDisco() + { + using (Ping p = new Ping()) + { + try + { + PingReply pr = p.Send("disco", 2000); + if (pr.Status == IPStatus.Success) + return true; + else + return false; + } + catch (Exception) + { + return false; + } + } + } + + public static void ConfigureWireless() + { + // Add Certificates + Program.Status.UpdateStatus(null, null, "Configuring Wireless Certificates"); + CertificateInterop.AddTempCerts(); + + // Add Wireless Profiles + Program.Status.UpdateStatus(null, null, "Configuring Wireless Profiles"); + var wirelessInlineProfiles = GetInlineWirelessProfiles(); + if (wirelessInlineProfiles.Count > 0) + { + + IntPtr wlanHandle = IntPtr.Zero; + uint negotiatedVersion; + + try + { + if (WlanOpenHandle(1, IntPtr.Zero, out negotiatedVersion, ref wlanHandle) != 0) + throw new NotSupportedException("This device does not support Wireless"); + + // Add Profile to Each Wireless Adapter + var wirelessAdapters = NetworkAdapters.Where(na => na.IsWireless).ToList(); + foreach (var na in wirelessAdapters) + { + foreach (var inlineWirelessProfile in wirelessInlineProfiles) + { + if (inlineWirelessProfile.AddProfile(wlanHandle, na.Guid)) + { + Program.Status.UpdateStatus(null, null, string.Format("Added Wireless Profile: {0}", inlineWirelessProfile.ProfileName)); + Program.SleepThread(500, false); + } + else + { + Program.Status.UpdateStatus(null, null, string.Format("Unable to add Wireless Profile: {0}", inlineWirelessProfile.ProfileName)); + Program.SleepThread(5000, false); + } + } + } + } + finally + { + if (wlanHandle != IntPtr.Zero) + NetworkInterop.WlanCloseHandle(wlanHandle, IntPtr.Zero); + } + } + + } + private class WirelessProfile + { + public string Filename { get; set; } + public string ProfileXml { get; set; } + public string ProfileName { get; set; } + + public bool AddProfile(IntPtr WlanHandle, Guid interfaceGuid) + { + var pInterfaceGuid = interfaceGuid; + var pProfileXml = this.ProfileXml; + uint pFlag = 0; + uint failReason; + return (WlanSetProfile(WlanHandle, ref pInterfaceGuid, pFlag, pProfileXml, null, true, IntPtr.Zero, out failReason) == 0); + } + } + + private static List GetInlineWirelessProfiles() + { + var inlineProfileFiles = System.IO.Directory.EnumerateFiles(Program.InlinePath.Value, "WLAN_Profile_*.xml").ToList(); + var inlineProfiles = new List(inlineProfileFiles.Count); + foreach (var filename in inlineProfileFiles) + { + var profile = new WirelessProfile() + { + Filename = filename, + ProfileXml = System.IO.File.ReadAllText(filename) + }; + var profileXml = new XmlDocument(); + profileXml.LoadXml(profile.ProfileXml); + var profileXmlNS = new XmlNamespaceManager(profileXml.NameTable); + profileXmlNS.AddNamespace("p", "http://www.microsoft.com/networking/WLAN/profile/v1"); + var profileXmlNameNode = profileXml.SelectSingleNode("/p:WLANProfile/p:name", profileXmlNS); + if (profileXmlNameNode != null) + { + profile.ProfileName = profileXmlNameNode.InnerText; + inlineProfiles.Add(profile); + } + } + return inlineProfiles; + } + + private static WLAN_PROFILE_INFO_LIST GetWirelessProfiles(IntPtr WlanHandle, Guid interfaceGuid) + { + Guid pInterfaceGuid = interfaceGuid; + + IntPtr ppProfileList = new IntPtr(); + WlanGetProfileList(WlanHandle, ref pInterfaceGuid, new IntPtr(), ref ppProfileList); + WLAN_PROFILE_INFO_LIST wlanProfileInfoList = new WLAN_PROFILE_INFO_LIST(ppProfileList); + + NetworkInterop.WlanFreeMemory(ppProfileList); + + return wlanProfileInfoList; + } + + } +} diff --git a/Disco.ClientBootstrapper/Interop/RegistryInterop.cs b/Disco.ClientBootstrapper/Interop/RegistryInterop.cs new file mode 100644 index 00000000..07f0aaa9 --- /dev/null +++ b/Disco.ClientBootstrapper/Interop/RegistryInterop.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Runtime.InteropServices; + +namespace Disco.ClientBootstrapper.Interop +{ + class RegistryInterop : IDisposable + { + + [StructLayout(LayoutKind.Sequential)] + private struct LUID + { + public int LowPart; + public int HighPart; + } + [StructLayout(LayoutKind.Sequential)] + private struct TOKEN_PRIVILEGES + { + public LUID Luid; + public int Attributes; + public int PrivilegeCount; + } + + [DllImport("advapi32.dll", CharSet = CharSet.Auto)] + private static extern int OpenProcessToken(int ProcessHandle, int DesiredAccess, ref int tokenhandle); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto)] + private static extern int GetCurrentProcess(); + + [DllImport("advapi32.dll", CharSet = CharSet.Auto)] + private static extern int LookupPrivilegeValue(string lpsystemname, string lpname, [MarshalAs(UnmanagedType.Struct)] ref LUID lpLuid); + + [DllImport("advapi32.dll", CharSet = CharSet.Auto)] + private static extern int AdjustTokenPrivileges(int tokenhandle, int disableprivs, [MarshalAs(UnmanagedType.Struct)]ref TOKEN_PRIVILEGES Newstate, int bufferlength, int PreivousState, int Returnlength); + + [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern int RegLoadKey(uint hKey, string lpSubKey, string lpFile); + + [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern int RegUnLoadKey(uint hKey, string lpSubKey); + + private const int TOKEN_ADJUST_PRIVILEGES = 0x00000020; + private const int TOKEN_QUERY = 0x00000008; + private const int SE_PRIVILEGE_ENABLED = 0x00000002; + private const string SE_RESTORE_NAME = "SeRestorePrivilege"; + private const string SE_BACKUP_NAME = "SeBackupPrivilege"; + private const uint HKEY_USERS = 0x80000003; + + private RegistryHives Hive { get; set; } + private string SubKey { get; set; } + private bool IsUnloaded { get; set; } + + public enum RegistryHives : uint + { + HKEY_USERS = 0x80000003, + HKEY_LOCAL_MACHINE = 0x80000002 + } + + public RegistryInterop(RegistryHives hive, string subKey, string filePath) + { + int token = 0; + int retval = 0; + + TOKEN_PRIVILEGES TP = new TOKEN_PRIVILEGES(); + TOKEN_PRIVILEGES TP2 = new TOKEN_PRIVILEGES(); + LUID RestoreLuid = new LUID(); + LUID BackupLuid = new LUID(); + + retval = OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, ref token); + retval = LookupPrivilegeValue(null, SE_RESTORE_NAME, ref RestoreLuid); + retval = LookupPrivilegeValue(null, SE_BACKUP_NAME, ref BackupLuid); + TP.PrivilegeCount = 1; + TP.Attributes = SE_PRIVILEGE_ENABLED; + TP.Luid = RestoreLuid; + TP2.PrivilegeCount = 1; + TP2.Attributes = SE_PRIVILEGE_ENABLED; + TP2.Luid = BackupLuid; + + retval = AdjustTokenPrivileges(token, 0, ref TP, 1024, 0, 0); + retval = AdjustTokenPrivileges(token, 0, ref TP2, 1024, 0, 0); + + uint regHive = (uint)hive; + + this.Hive = hive; + this.SubKey = subKey; + RegLoadKey(regHive, subKey, filePath); + this.IsUnloaded = false; + } + + public void Unload() + { + if (!IsUnloaded) + { + uint regHive = (uint)this.Hive; + string subKey = this.SubKey; + RegUnLoadKey(regHive, subKey); + this.IsUnloaded = true; + } + } + + public void Dispose() + { + Unload(); + } + } +} diff --git a/Disco.ClientBootstrapper/Interop/ShutdownInterop.cs b/Disco.ClientBootstrapper/Interop/ShutdownInterop.cs new file mode 100644 index 00000000..67a8ffe3 --- /dev/null +++ b/Disco.ClientBootstrapper/Interop/ShutdownInterop.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Management; +using System.Runtime.InteropServices; + +namespace Disco.ClientBootstrapper.Interop +{ + public static class ShutdownInterop + { + public static void Shutdown() + { + // 8 = Power Off + Shutdown(EWX_POWEROFF); + } + public static void Reboot() + { + // 2 = Reboot + Shutdown(EWX_REBOOT); + } + + private static void Shutdown(int flag) + { + // Removed 2012-11-23 G# - Migrate to Win32 PInvoke Shutdown for better Privilege Handling + //ManagementBaseObject mboShutdown = null; + //ManagementClass mcWin32 = new ManagementClass("Win32_OperatingSystem"); + //mcWin32.Get(); + + //// You can't shutdown without security privileges + //mcWin32.Scope.Options.EnablePrivileges = true; + //ManagementBaseObject mboShutdownParams = + // mcWin32.GetMethodParameters("Win32Shutdown"); + + //// Flag 1 means we want to shut down the system. Use "2" to reboot. + //mboShutdownParams["Flags"] = flag; + //mboShutdownParams["Reserved"] = "0"; + //foreach (ManagementObject manObj in mcWin32.GetInstances()) + //{ + // mboShutdown = manObj.InvokeMethod("Win32Shutdown", + // mboShutdownParams, null); + //} + // End Removed 2012-11-23 G# + + // Added 2012-11-23 G# - Migrate to Win32 PInvoke Shutdown + bool result; + TokPriv1Luid tp; + + IntPtr hproc = GetCurrentProcess(); + IntPtr htok = IntPtr.Zero; + + result = OpenProcessToken(hproc, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, ref htok); + + tp.Count = 1; + tp.Luid = 0; + tp.Attr = SE_PRIVILEGE_ENABLED; + + result = LookupPrivilegeValue(null, SE_SHUTDOWN_NAME, ref tp.Luid); + result = AdjustTokenPrivileges(htok, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero); + result = ExitWindowsEx(flag, 0); + // End Added 2012-11-23 G# + } + + #region Win32 PInvoke Interop + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + private struct TokPriv1Luid + { + public int Count; + public long Luid; + public int Attr; + } + + [DllImport("kernel32.dll", ExactSpelling = true)] + private static extern IntPtr GetCurrentProcess(); + + [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] + private static extern bool OpenProcessToken(IntPtr h, int acc, ref IntPtr phtok); + + [DllImport("advapi32.dll", SetLastError = true)] + private static extern bool LookupPrivilegeValue(string host, string name, ref long pluid); + + [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] + private static extern bool AdjustTokenPrivileges(IntPtr htok, bool disall, ref TokPriv1Luid newst, int len, IntPtr prev, IntPtr relen); + + [DllImport("user32.dll", ExactSpelling = true, SetLastError = true)] + private static extern bool ExitWindowsEx(int flg, int rea); + + private const int SE_PRIVILEGE_ENABLED = 0x00000002; + private const int TOKEN_QUERY = 0x00000008; + private const int TOKEN_ADJUST_PRIVILEGES = 0x00000020; + private const string SE_SHUTDOWN_NAME = "SeShutdownPrivilege"; + private const int EWX_LOGOFF = 0x00000000; + private const int EWX_SHUTDOWN = 0x00000001; + private const int EWX_REBOOT = 0x00000002; + private const int EWX_FORCE = 0x00000004; + private const int EWX_POWEROFF = 0x00000008; + private const int EWX_FORCEIFHUNG = 0x00000010; + + #endregion + + } +} diff --git a/Disco.ClientBootstrapper/Interop/WIMInterop.cs b/Disco.ClientBootstrapper/Interop/WIMInterop.cs new file mode 100644 index 00000000..00eefdda --- /dev/null +++ b/Disco.ClientBootstrapper/Interop/WIMInterop.cs @@ -0,0 +1,1585 @@ +using System; +using System.Collections; +using System.Globalization; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using System.Xml; + +namespace Disco.ClientBootstrapper.Interop.WIMInterop +{ + /// + ///Pulic interface to a WindowsImage object. + /// + public + interface IImage : IDisposable + { + /// + ///Gets image information from within a .wim file. + /// + string ImageInformation + { + get; + } + + /// + ///Sets image information about an image within a .wim file. + /// + ///The string being passed in should be in the form of a unicode XML file. + ///Calling this function replaces any customized image data. To preserve existing XML information, call ImageInformation + ///and append/edit desired data. + /// + void SetImageInformation( + string imageInformation + ); + + /// + ///Mounts an image in a .wim file to the specified directory. + /// + void Mount( + string pathToMountTo + ); + + /// + ///Retrieves the path to which an image has been mounted. + /// + string MountedPath + { + get; + } + + /// + ///Unmounts a mounted image in a .wim file from the specified directory. + /// + ///Indicates whether changes (if any) to the .wim file should be committed + ///before unmounting the .wim file. This flag will have no effect if the .wim file was mounted not to allow edits. + /// + void DismountImage( + bool commitChanges + ); + + /// + ///Applies an image to a drive root or to a directory path from a .wim file. + /// + void Apply( + string pathToApplyTo + ); + } + + /// + ///Class representing a .wim file. + /// + public + sealed + class + WindowsImageContainer : IDisposable + { + /// + ///Specifies the type of access to the .wim file. + /// + public + enum + CreateFileAccess + { + /// + ///Specifies read-only access to the .wim file. + /// + Read, + + /// + ///Specifies write access to the .wim file. + ///Includes WIM_GENERIC_READ access to enable apply and append operations with existing images. + /// + Write + } + + /// + ///Specifies which action to take on files that exist and + ///which action to take when files do not exist. + /// + public + enum + CreateFileMode + { + /// + ///RESERVED, DO NOT USE! + /// + None = 0, + + /// + ///Creates a new .wim file. The function fails if the specified file already exists. + /// + CreateNew = 1, + + /// + ///Creates a new .wim file. If the file exists, the function overwrites the file. + /// + CreateAlways = 2, + + /// + ///Opens the .wim file. The function fails if the file does not exist. + /// + OpenExisting = 3, + + /// + ///Opens the .wim file if it exists. If the file does not exist and the caller requests WIM_GENERIC_WRITE access, the + ///function creates the file. + /// + OpenAlways = 4 + } + + /// + ///Public constructor to create a WIM object + /// + ///Path of WIM to create or to open. + ///Specifies Open, Create, Create/Open disposition of the .wim file. + ///Specifies access level of Read Only or Write. + //[CLSCompliant(false)] + public WindowsImageContainer(string imageFilePath, CreateFileMode mode, CreateFileAccess access) + { + CreateFileAccessPrivate fileAccess = GetMappedFileAccess(access); + if (fileAccess == CreateFileAccessPrivate.Read && (!File.Exists(imageFilePath) || (CreateFileMode.OpenExisting != mode))) + { + throw new System.UnauthorizedAccessException(string.Format(CultureInfo.CurrentCulture, + "Read access can be specified only with OpenExisting mode or OpenAlways mode when the .wim file does not exist.")); + } + + // + //Imaging DLLs must be in the same directory. + // + try + { + m_ImageContainerHandle = NativeMethods.CreateFile(imageFilePath, (uint)fileAccess, (uint)mode); + m_WindowsImageFilePath = imageFilePath; + } + catch (System.DllNotFoundException ex) + { + throw new System.DllNotFoundException(string.Format(CultureInfo.CurrentCulture, + "Unable to load WIM libraries. Make sure the correct DLLs are present (Wimgapi.dll and Xmlrw.dll)."), ex.InnerException); + } + + if (!m_ImageContainerHandle.Equals(IntPtr.Zero)) + { + // + //Set the temporary path so that we can write to an image. This + //cannot be %TEMP% as it does not exist on Windows PE + // + string tempDirectory = System.Environment.GetEnvironmentVariable("systemdrive"); + NativeMethods.SetTemporaryPath(m_ImageContainerHandle, tempDirectory); + + } + else + { + // + //Throw an exception + // + throw new System.InvalidOperationException(string.Format(CultureInfo.CurrentCulture, + "Unable to open the .wim file {0}.", imageFilePath)); + } + + // + //Finally, we must hook into the events. + // + m_MessageCallback = new NativeMethods.MessageCallback(ImageEventMessagePump); + NativeMethods.RegisterCallback(m_MessageCallback); + } + + /// Destructor to close open handles. + ~WindowsImageContainer() + { + DisposeInner(); + } + + /// + ///Dispose all unmanaged resources + /// + public + void + Dispose() + { + DisposeInner(); + GC.SuppressFinalize(this); + } + + private + void + DisposeInner() + { + if (m_ImageContainerHandle != IntPtr.Zero) + { + NativeMethods.CloseHandle(m_ImageContainerHandle); + m_ImageContainerHandle = IntPtr.Zero; + } + + if (m_MessageCallback != null) + { + NativeMethods.UnregisterMessageCallback(m_MessageCallback); + m_MessageCallback = null; + } + GC.KeepAlive(this); + } + + /// + ///Used to enumerate the WindowsImage array + /// + public + IEnumerator + GetEnumerator( + ) + { + return m_Images.GetEnumerator(); + } + + /// + ///[] overload, used to enumerate the WindowsImage array + /// + public + IImage + this[int imageIndex] + { + get + { + // + //Delay load images + // + if (m_Images == null || m_Images[imageIndex] == null) + { + + ArrayList tempImages = new ArrayList(); + tempImages.Add(new WindowsImage(m_ImageContainerHandle, m_WindowsImageFilePath, imageIndex + 1)); + m_Images = (WindowsImage[])tempImages.ToArray(typeof(WindowsImage)); + } + GC.KeepAlive(this); + return m_Images[imageIndex]; + } + } + + /// + ///Retrieve the number of images in the .wim file. + /// + public int ImageCount + { + get + { + // + //Verify if there is an image count; if not, get it. + // + if (m_ImageCount == 0) + { + m_ImageCount = NativeMethods.GetImageCount(m_ImageContainerHandle); + } + + GC.KeepAlive(this); + return m_ImageCount; + } + } + + /// + ///Capture an image from the root of a drive or from an individual directory. + /// + public void CaptureImage(string pathToCapture) + { + // + //Capture the image. + // + IntPtr windowsImageHandle = NativeMethods.CaptureImage(m_ImageContainerHandle, pathToCapture); + NativeMethods.CloseHandle(windowsImageHandle); + GC.KeepAlive(this); + } + + /// + ///Default event handler + /// + //[CLSCompliant(false)] + public delegate void DefaultImageEventHandler(object sender, DefaultImageEventArgs e); + //public delegate void DefaultImageEventHandler(IntPtr wParam, IntPtr lParam, IntPtr UserData); + /// + ///ProcessFileEvent handler + /// + //[CLSCompliant(false)] + public delegate void ProcessFileEventHandler(object sender, ProcessFileEventArgs e); + //public delegate void ProcessFileEventHandler(ProcessFile fileToProcess); + + + /// + ///Indicate an update in the progress of an image application. + /// + //[CLSCompliant(false)] + public event DefaultImageEventHandler ProgressEvent; + /// + ///Enable the caller to prevent a file or a directory from being captured or applied. + /// + //[CLSCompliant(false)] + public event ProcessFileEventHandler ProcessFileEvent; + /// + ///Enable the caller to prevent a file resource from being compressed during a capture. + /// + //[CLSCompliant(false)] + public event DefaultImageEventHandler CompressEvent; + /// + ///Alert the caller that an error has occurred while capturing or applying an image. + /// + //[CLSCompliant(false)] + public event DefaultImageEventHandler ErrorEvent; + /// + ///Enable the caller to align a file resource on a particular alignment boundary. + /// + //[CLSCompliant(false)] + public event DefaultImageEventHandler AlignmentEvent; + /// + ///Enable the caller to align a file resource on a particular alignment boundary. + /// + //[CLSCompliant(false)] + public event DefaultImageEventHandler SplitEvent; + /// + ///Indicate that volume information is being gathered during an image capture. + /// + //[CLSCompliant(false)] + public event DefaultImageEventHandler ScanningEvent; + /// + ///Indicate the number of files that will be captured or applied. + /// + //[CLSCompliant(false)] + public event DefaultImageEventHandler SetRangeEvent; + /// + ///Indicate the number of files that have been captured or applied. + /// + //[CLSCompliant(false)] + public event DefaultImageEventHandler SetPosEvent; + /// + ///Indicate that a file has been either captured or applied. + /// + //[CLSCompliant(false)] + public event DefaultImageEventHandler StepItEvent; + + + /// + ///Event callback to the Wimgapi events + /// + private + uint + ImageEventMessagePump( + uint MessageId, + IntPtr wParam, + IntPtr lParam, + IntPtr UserData + ) + { + uint status = (uint)NativeMethods.WIMMessage.WIM_MSG_SUCCESS; + DefaultImageEventArgs eventArgs = new DefaultImageEventArgs(wParam, lParam, UserData); + + switch ((ImageEventMessage)MessageId) + { + + case ImageEventMessage.Progress: + ProgressEvent(this, eventArgs); + break; + + case ImageEventMessage.Process: + string fileToImage = Marshal.PtrToStringUni(wParam); + ProcessFileEventArgs fileToProcess = new ProcessFileEventArgs(fileToImage, lParam); + ProcessFileEvent(this, fileToProcess); + if (fileToProcess.Abort == true) + { + status = (uint)ImageEventMessage.Abort; + } + break; + + case ImageEventMessage.Compress: + CompressEvent(this, eventArgs); + break; + + case ImageEventMessage.Error: + ErrorEvent(this, eventArgs); + break; + + case ImageEventMessage.Alignment: + AlignmentEvent(this, eventArgs); + break; + + case ImageEventMessage.Split: + SplitEvent(this, eventArgs); + break; + + case ImageEventMessage.Scanning: + ScanningEvent(this, eventArgs); + break; + + case ImageEventMessage.SetRange: + SetRangeEvent(this, eventArgs); + break; + + case ImageEventMessage.SetPos: + SetPosEvent(this, eventArgs); + break; + + case ImageEventMessage.StepIt: + StepItEvent(this, eventArgs); + break; + + default: + break; + } + + return status; + } + + /// + ///Image inside of a .wim file + /// + private + class + WindowsImage : IImage + { + /// + ///Public constructor to create an image object from inside a .wim file + /// + public WindowsImage(IntPtr imageContainerHandle, string imageContainerFilePath, int imageIndex) + { + m_ParentWindowsImageHandle = imageContainerHandle; + m_ParentWindowsImageFilePath = imageContainerFilePath; + m_Index = imageIndex; + + // + //Load the image and stash away the handle. + // + m_ImageHandle = NativeMethods.LoadImage(imageContainerHandle, imageIndex); + } + + /// Destructor to close open handles. + ~WindowsImage() + { + DisposeInner(); + } + + /// + ///Dispose all unmanaged resources. + /// + public + void + Dispose() + { + DisposeInner(); + GC.SuppressFinalize(this); + } + + private + void + DisposeInner() + { + // + //Do not leave any open handles or mounted images. + // + if (m_ImageHandle != IntPtr.Zero) + { + NativeMethods.CloseHandle(m_ImageHandle); + m_ImageHandle = IntPtr.Zero; + } + + if (m_Mounted == true) + { + // + //Never commit changes when destroying this object. + // + this.DismountImage(false); + } + GC.KeepAlive(this); + } + + /// + ///Gets an image information header. + /// + /// + public string ImageInformation + { + get + { + // + //Always get the image header (even if we have it already), removing the unicode file marker. + // + string str = NativeMethods.GetImageInformation(m_ImageHandle).Remove(0, 1); + GC.KeepAlive(this); + + return str; + } + } + + // /// + // ///Returns an XmlTextReader for a given node name. + // /// + // private XmlTextReader ImageInformationHelper(string raw) { + + // XmlTextReader xmlTextReader = null; + + // if (raw != null) { + // XmlDocument xmlDocument = new XmlDocument(); + // try { + // xmlDocument.LoadXml(raw); + + // // + // //Look at all nodes. + // // + // foreach(XmlNode node in xmlDocument.ChildNodes) { + + // // + // //Now, find the specified node + // // + // foreach(XmlNode childNode in node) { + // if + //(String.Equals(childNode.Name, node.ToString(), StringComparison.InvariantCultureIgnoreCase)) { + + // StringWriter stringWriter = new StringWriter(CultureInfo.InvariantCulture); + // XmlTextWriter xmlTextWriter = new XmlTextWriter(stringWriter); + + // xmlTextWriter.WriteStartElement(childNode.Name); + // // + // //We are in the specified node. Now, get all the attributes + // // + // foreach(XmlAttribute attribute in childNode.Attributes) { + // xmlTextWriter.WriteAttributeString(attribute.Name, attribute.InnerXml); + // } + // xmlTextWriter.WriteEndElement(); + + // xmlTextReader = new XmlTextReader(null, new StringReader(stringWriter.ToString())); + // } + // } + // } + // } + // catch (System.Xml.XmlException) { + // throw new System.Xml.XmlException(string.Format(CultureInfo.CurrentCulture, + // "Unable to read XML header information from {0}", + // this.m_ParentWindowsImageFilePath)); + // } + // } + // return xmlTextReader; + // } + + /// + ///Retrieves the path to which an image has been mounted. + /// + public string MountedPath + { + get + { + return (m_MountedPath != null) ? m_MountedPath : null; + } + } + + /// + ///Sets the image information header + /// + /// + public void SetImageInformation(string imageInformation) + { + // + //Format the incoming XML so that we can set the header. The XML must have: + //1. Leading 0xFEFF + //2. Be in + // + string formattedXml = String.Format(CultureInfo.InvariantCulture, + "{0}{1}{2}{3}", + UNICODE_FILE_MARKER, + "", + imageInformation, + ""); + + NativeMethods.SetImageInformation(m_ImageHandle, formattedXml); + GC.KeepAlive(this); + } + + /// + ///Mounts an image to a directory. + /// + /// + public void Mount(string pathToMountTo) + { + // + //Mount the image + // + m_MountedPath = pathToMountTo; + NativeMethods.MountImage(pathToMountTo, m_ParentWindowsImageFilePath, m_Index, System.IO.Path.GetTempPath()); + m_Mounted = true; + } + + /// + ///Unmounts an image from a directory. + /// + /// + public void DismountImage(bool commitChanges) + { + if (m_Mounted == true) + { + NativeMethods.DismountImage(m_MountedPath, m_ParentWindowsImageFilePath, m_Index, commitChanges); + } + } + + /// + ///Applies an image to a drive root or to a directory path. + /// + /// + public void Apply(string pathToApplyTo) + { + NativeMethods.ApplyImage(m_ImageHandle, pathToApplyTo); + GC.KeepAlive(this); + } + + private IntPtr m_ParentWindowsImageHandle = IntPtr.Zero; //.wim file handle + private string m_ParentWindowsImageFilePath; //path to .wim file + + private IntPtr m_ImageHandle = IntPtr.Zero; //image handle + private int m_Index; //index of image + + private string m_MountedPath; //where the image has been mounted + private bool m_Mounted; //true if image has been mounted + + // + //DO NOT CHANGE! This controls the format of the image header + //and it must be present. + // + private const string UNICODE_FILE_MARKER = "\uFEFF"; + } + + /// + ///Interop to Wimgapi.dll + /// + public + class + NativeMethods + { + /// + ///Private null constructor + /// + private + NativeMethods() { } + + [DllImport("Wimgapi.dll", ExactSpelling = true, + EntryPoint = "WIMCreateFile", + CallingConvention = CallingConvention.StdCall, + SetLastError = true)] + private static extern + IntPtr + WimCreateFile( + [MarshalAs(UnmanagedType.LPWStr)] string WimPath, + uint DesiredAccess, + uint CreationDisposition, + uint FlagsAndAttributes, + uint CompressionType, + out IntPtr CreationResult + ); + + /// + ///Creates a new .wim file or opens an existing .wim file. + /// + ///Path to the .wim file to open or to create. + ///Specifies the file access to grant the file. + ///Specifies the mode in which the file should be opened or created. + ///If the function succeeds, the return value is an open handle to the specified image file. + ///If the function fails, the return value is NULL. + public + static + IntPtr + CreateFile(string imageFile, uint access, uint mode) + { + IntPtr creationResult = IntPtr.Zero; + IntPtr windowsImageHandle = IntPtr.Zero; + int rc = -1; + + windowsImageHandle = NativeMethods.WimCreateFile(imageFile, access, mode, 0, 0, out creationResult); + rc = Marshal.GetLastWin32Error(); + if (windowsImageHandle == IntPtr.Zero) + { + // + //Everything failed; throw an exception + // + throw new System.InvalidOperationException(string.Format(CultureInfo.CurrentCulture, + "Unable to open/create .wim file {0}. Error = {1}", + imageFile, rc)); + } + + return windowsImageHandle; + } + + [DllImport("Wimgapi.dll", + ExactSpelling = true, + EntryPoint = "WIMCloseHandle", + CallingConvention = CallingConvention.StdCall, + SetLastError = true)] + private static extern + bool + WimCloseHandle( + IntPtr Handle + ); + + /// + ///Closes an open .wim file or an image handle. + /// + ///Handle to an open imaging-based object. + ///If the function succeeds, the return value is nonzero. + ///If the function fails, the return value is zero. + public + static + void + CloseHandle(IntPtr handle) + { + bool status = NativeMethods.WimCloseHandle(handle); + int rc = Marshal.GetLastWin32Error(); + if (status == false) + { + // + //Throw an exception + // + throw new System.InvalidOperationException(string.Format(CultureInfo.CurrentCulture, + "Unable to close image handle. Error = {0}", rc)); + } + } + + [DllImport("Wimgapi.dll", + ExactSpelling = true, + EntryPoint = "WIMSetTemporaryPath", + CallingConvention = CallingConvention.StdCall, + SetLastError = true)] + private static extern + bool + WimSetTemporaryPath( + IntPtr Handle, + [MarshalAs(UnmanagedType.LPWStr)] string TemporaryPath + ); + + /// + ///Sets the location where temporary imaging files are to be stored. + /// + ///Handle to a WIM file returned by CreateFile + ///String value of path to set as a temporary location. + ///If the function succeeds, the return value is nonzero. + ///If the function fails, the return value is NULL. + public + static + void + SetTemporaryPath(IntPtr handle, string temporaryPath) + { + bool status = NativeMethods.WimSetTemporaryPath(handle, temporaryPath); + int rc = Marshal.GetLastWin32Error(); + if (status == false) + { + // + //Throw an exception + // + throw new System.InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Unable to set temporary path. Error = {0}", rc)); + } + } + + [DllImport("Wimgapi.dll", + ExactSpelling = true, + EntryPoint = "WIMLoadImage", + CallingConvention = CallingConvention.StdCall, + SetLastError = true)] + private static extern + IntPtr + WimLoadImage( + IntPtr Handle, + uint ImageIndex + ); + + /// + ///Loads a volume image or from within a .wim file. + /// + ///Wim handle. + ///Index of the image to load. + ///If the function succeeds, the return value is a handle to an object representing the volume image. + ///If the function fails, the return value is NULL. + public + static + IntPtr + LoadImage(IntPtr handle, int imageIndex) + { + //Load the image data based on the .wim handle + // + IntPtr hWim = NativeMethods.WimLoadImage(handle, (uint)imageIndex); + int rc = Marshal.GetLastWin32Error(); + if (hWim == IntPtr.Zero) + { + // + //Throw an exception + // + throw new System.InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Unable to load image. Error = {0}", rc)); + } + + return hWim; + + } + + [DllImport("Wimgapi.dll", + ExactSpelling = true, + EntryPoint = "WIMCaptureImage", + CallingConvention = CallingConvention.StdCall, + SetLastError = true)] + private static extern + IntPtr + WimCaptureImage( + IntPtr Handle, + [MarshalAs(UnmanagedType.LPWStr)] string Path, + uint CaptureFlags + ); + + /// + ///Captures an image from a drive root or from a directory path and stores it in an image file. + /// + ///Handle to a .wim file returned by CreateFile. + ///Drive root or directory path from where the image data will be captured. + ///Handle to an object representing the volume image. If the function fails, the return value is NULL. + public + static + IntPtr + CaptureImage(IntPtr handle, string path) + { + IntPtr hImage = NativeMethods.WimCaptureImage(handle, path, 0); + int rc = Marshal.GetLastWin32Error(); + if (hImage == IntPtr.Zero) + { + // + //Throw an exception + // + throw new System.InvalidOperationException(string.Format(CultureInfo.CurrentCulture, + "Failed to capture image from {0}. Error = {1}", path, rc)); + } + return hImage; + } + + /// + ///Gets the number of volume images stored in a .wim file. + /// + ///Handle to a .wim file returned by CreateFile. + ///The number of images within the .wim file. + [DllImport("Wimgapi.dll", + ExactSpelling = true, + EntryPoint = "WIMGetImageCount", + CallingConvention = CallingConvention.StdCall, + SetLastError = true)] + private static extern + int + WimGetImageCount( + IntPtr Handle + ); + + /// + ///Returns the number of volume images stored in a .wim file. + /// + ///Handle to a .wim file returned by WIMCreateFile. + ///The return value is the number of images within the .wim file. + ///If this value is zero, then the .wim file is invalid or does not contain any images that can be applied. + /// + public + static + int + GetImageCount(IntPtr windowsImageHandle) + { + int count = NativeMethods.WimGetImageCount(windowsImageHandle); + int rc = Marshal.GetLastWin32Error(); + if (count == -1) + { + // + //Throw an exception + // + throw new System.InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Unable to get image count. Error = {0}", rc)); + } + + return count; + } + + //[DllImport("Wimgapi.dll", + // ExactSpelling = true, + // EntryPoint = "WIMMountImageHandle", + // CallingConvention = CallingConvention.StdCall, + // SetLastError = true)] + //private static extern + //bool + //WIMMountImageHandle( + // IntPtr hImage, + // [MarshalAs(UnmanagedType.LPWStr)] string pszMountPath, + // uint dwMountFlags + // ); + ///// + /////Mounts an image in a .wim file to the specified directory. + ///// + /////Returns TRUE and sets the LastError to ERROR_SUCCESS. + /////Returns FALSE in case of a failure and the LastError is set to the appropriate Win32 error value. + ///// + //public + //static + //void + //MountImageHandle(string mountPath, ) + //{ + // bool status = false; + // int rc; + + // try + // { + // status = NativeMethods.WimMountImage(mountPath, + // windowsImageFileName, + // (uint)imageIndex, + // System.Environment.GetEnvironmentVariable("temp")); + // rc = Marshal.GetLastWin32Error(); + // } + // catch (System.StackOverflowException) + // { + // // + // //Throw an exception + // // + // throw new System.InvalidOperationException(string.Format(CultureInfo.CurrentCulture, + // "Unable to mount image {0} to {1}.", windowsImageFileName, mountPath)); + // } + // if (status == false) + // { + // // + // //Throw an exception + // // + // throw new System.InvalidOperationException(string.Format(CultureInfo.CurrentCulture, + // "Unable to mount image {0} to {1}. Error = {2}", + // windowsImageFileName, mountPath, rc)); + // } + //} + + + [DllImport("Wimgapi.dll", + ExactSpelling = true, + EntryPoint = "WIMMountImage", + CallingConvention = CallingConvention.StdCall, + SetLastError = true)] + private static extern + bool + WimMountImage( + [MarshalAs(UnmanagedType.LPWStr)] string MountPath, + [MarshalAs(UnmanagedType.LPWStr)] string WimFileName, + uint ImageIndex, + [MarshalAs(UnmanagedType.LPWStr)] string TemporaryPath + + ); + + /// + ///Mounts an image in a .wim file to the specified directory. + /// + ///Returns TRUE and sets the LastError to ERROR_SUCCESS. + ///Returns FALSE in case of a failure and the LastError is set to the appropriate Win32 error value. + /// + public + static + void + MountImage(string mountPath, string windowsImageFileName, int imageIndex, string temporaryPath) + { + bool status = false; + int rc; + + try + { + status = NativeMethods.WimMountImage(mountPath, + windowsImageFileName, + (uint)imageIndex, + temporaryPath); + rc = Marshal.GetLastWin32Error(); + } + catch (System.StackOverflowException) + { + // + //Throw an exception + // + throw new System.InvalidOperationException(string.Format(CultureInfo.CurrentCulture, + "Unable to mount image {0} to {1}.", windowsImageFileName, mountPath)); + } + if (status == false) + { + // + //Throw an exception + // + throw new System.InvalidOperationException(string.Format(CultureInfo.CurrentCulture, + "Unable to mount image {0} to {1}. Error = {2}", + windowsImageFileName, mountPath, rc)); + } + } + + [DllImport("Wimgapi.dll", + ExactSpelling = true, + EntryPoint = "WIMApplyImage", + CallingConvention = CallingConvention.StdCall, + SetLastError = true)] + private static extern + bool + WimApplyImage( + IntPtr Handle, + [MarshalAs(UnmanagedType.LPWStr)] string Path, + uint Flags + ); + + /// + ///Applies an image to a drive root or to a directory path from a .wim file. + /// + ///If the function succeeds, the return value is nonzero. + ///If the function fails, the return value is zero + public + static + void + ApplyImage(IntPtr imageHandle, string applicationPath) + { + // + //Call WimApplyImage always with the Index flag for performance reasons. + // + bool status = NativeMethods.WimApplyImage(imageHandle, applicationPath, NativeMethods.WIM_FLAG_INDEX); + int rc = Marshal.GetLastWin32Error(); + if (status == false) + { + // + //Throw an exception + // + throw new System.InvalidOperationException(string.Format(CultureInfo.CurrentCulture, + "Unable to apply image to {0}. Error = {1}", applicationPath, rc)); + } + } + + [DllImport("Wimgapi.dll", + ExactSpelling = true, + EntryPoint = "WIMGetImageInformation", + CallingConvention = CallingConvention.StdCall, + SetLastError = true)] + private static extern + bool + WimGetImageInformation( + IntPtr Handle, + out IntPtr ImageInfo, + out IntPtr SizeOfImageInfo + ); + + /// + ///Returns information about an image within the .wim file. + /// + ///Handle returned by CreateImage, LoadImage + ///If the function succeeds, the return value is nonzero. + ///If the function fails, the return value is zero. + /// + public + static + string + GetImageInformation(IntPtr handle) + { + IntPtr info = IntPtr.Zero, sizeOfInfo = IntPtr.Zero; + bool status; + + status = NativeMethods.WimGetImageInformation(handle, out info, out sizeOfInfo); + int rc = Marshal.GetLastWin32Error(); + + if (status == false) + { + // + //Throw an exception + // + throw new System.InvalidOperationException(string.Format(CultureInfo.CurrentCulture, + "Unable to get image information. Error = {0}", rc)); + } + string s = Marshal.PtrToStringUni(info); + + //If the function succeeds, return the pointer to the string. Otherwise, return NULL. + // + return s; + } + + [DllImport("Wimgapi.dll", + ExactSpelling = true, + EntryPoint = "WIMSetImageInformation", + CallingConvention = CallingConvention.StdCall, + SetLastError = true)] + private static extern + bool + WimSetImageInformation( + IntPtr Handle, + IntPtr ImageInfo, + uint SizeOfImageInfo + ); + + /// + ///Stores information about an image within the .wim file. + /// + ///Handle returned by CreateImage, LoadImage + ///String containing the unicode XML data to set. + ///If the function succeeds, the return value is nonzero. + ///If the function fails, the return value is zero. + public + static + void + SetImageInformation(IntPtr handle, string imageInfo) + { + //Create a byte array for the stream, allocate some unmanaged memory, and then copy the bytes to the unmanaged memory. + // + byte[] byteBuffer = Encoding.Unicode.GetBytes(imageInfo); + int byteBufferSize = byteBuffer.Length; + IntPtr xmlBuffer = Marshal.AllocHGlobal(byteBufferSize); + Marshal.Copy(byteBuffer, 0, xmlBuffer, byteBufferSize); + + bool status = NativeMethods.WimSetImageInformation(handle, xmlBuffer, (uint)byteBufferSize); + int rc = Marshal.GetLastWin32Error(); + if (status == false) + { + // + //Throw an exception + // + throw new System.InvalidOperationException(string.Format(CultureInfo.CurrentCulture, + "Unable to set image information. Error = {0}", rc)); + } + } + + [DllImport("Wimgapi.dll", + ExactSpelling = true, + EntryPoint = "WIMUnmountImage", + CallingConvention = CallingConvention.StdCall, + SetLastError = true)] + private static extern + bool + WimUnmountImage( + [MarshalAs(UnmanagedType.LPWStr)] string MountPath, + [MarshalAs(UnmanagedType.LPWStr)] string WimFileName, + uint ImageIndex, + bool CommitChanges + ); + + /// + ///Unmounts a mounted image in a .wim file from the specified directory. + /// + ///Returns TRUE and sets the LastError to ERROR_SUCCESS. Returns FALSE in case of a failure and the LastError is set + ///to the appropriate Win32 error value. + public + static + void + DismountImage(string mountPath, string wimdowsImageFileName, int imageIndex, bool commitChanges) + { + bool status = false; + int rc; + + try + { + status = NativeMethods.WimUnmountImage(mountPath, wimdowsImageFileName, (uint)imageIndex, commitChanges); + rc = Marshal.GetLastWin32Error(); + } + catch (System.StackOverflowException ex) + { + // + //Throw an exception + // + throw new System.StackOverflowException(string.Format(CultureInfo.CurrentCulture, + "Unable to unmount image {0} from {1}.", wimdowsImageFileName, mountPath), + ex.InnerException); + } + if (status == false) + { + // + //Throw an exception + // + throw new System.InvalidOperationException(string.Format(CultureInfo.CurrentCulture, + "Unable to unmount image {0} from {1}. Error = {2}", + wimdowsImageFileName, mountPath, rc)); + } + } + + /// + ///User-defined function used with the RegisterMessageCallback or UnregisterMessageCallback function. + /// + ///Specifies the message being sent. + ///Specifies additional message information. The contents of this parameter depend on the value of the + ///MessageId parameter. + ///Specifies additional message information. The contents of this parameter depend on the value of the + ///MessageId parameter. + ///Specifies the user-defined value passed to RegisterCallback. + /// + ///To indicate success and to enable other subscribers to process the message return WIM_MSG_SUCCESS. + ///To prevent other subscribers from receiving the message, return WIM_MSG_DONE. + ///To cancel an image apply or capture, return WIM_MSG_ABORT_IMAGE when handling the WIM_MSG_PROCESS message. + /// + public + delegate + uint + MessageCallback( + uint MessageId, + IntPtr wParam, + IntPtr lParam, + IntPtr UserData + ); + + [DllImport("Wimgapi.dll", + ExactSpelling = true, + EntryPoint = "WIMRegisterMessageCallback", + CallingConvention = CallingConvention.StdCall, + SetLastError = true)] + private static extern + uint + WimRegisterMessageCallback( + IntPtr hWim, + MessageCallback MessageProc, + IntPtr ImageInfo + ); + + /// + ///Registers a function to be called with imaging-specific data. + /// + public + static + void + RegisterCallback(MessageCallback callback) + { + uint callbackZeroBasedIndex = NativeMethods.WimRegisterMessageCallback(IntPtr.Zero, callback, IntPtr.Zero); + int rc = Marshal.GetLastWin32Error(); + if (rc != 0) + { + // + //Throw an exception + // + throw new System.InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Unable to register message callback.")); + } + } + + [DllImport("Wimgapi.dll", + ExactSpelling = true, + EntryPoint = "WIMUnregisterMessageCallback", + CallingConvention = CallingConvention.StdCall, + SetLastError = true)] + private static extern + bool + WimUnregisterMessageCallback( + IntPtr hWim, + MessageCallback MessageProc + ); + + /// + ///Unregisters a function from being called with imaging-specific data. + /// + ///Callback to be unregistered. + public + static + void + UnregisterMessageCallback(MessageCallback registeredCallback) + { + bool status = NativeMethods.WimUnregisterMessageCallback(IntPtr.Zero, registeredCallback); + int rc = Marshal.GetLastWin32Error(); + if (status != true) + { + // + // Throw an exception + // + throw new System.InvalidOperationException(string.Format(CultureInfo.CurrentCulture, "Unable to unregister message callback.")); + } + } + + private const uint WM_APP = 0x8000; + + /// + ///Imaging Messages + /// + public enum WIMMessage : uint + { + WIM_MSG = WM_APP + 0x1476, + WIM_MSG_TEXT, + /// + ///Indicates an update in the progress of an image application. + /// + WIM_MSG_PROGRESS, + /// + ///Enables the caller to prevent a file or a directory from being captured or applied. + /// + WIM_MSG_PROCESS, + /// + ///Indicates that volume information is being gathered during an image capture. + /// + WIM_MSG_SCANNING, + /// + ///Indicates the number of files that will be captured or applied. + /// + WIM_MSG_SETRANGE, + /// + ///Indicates the number of files that have been captured or applied. + /// + WIM_MSG_SETPOS, + /// + ///Indicates that a file has been either captured or applied. + /// + WIM_MSG_STEPIT, + /// + ///Enables the caller to prevent a file resource from being compressed during a capture. + /// + WIM_MSG_COMPRESS, + /// + ///Alerts the caller that an error has occurred while capturing or applying an image. + /// + WIM_MSG_ERROR, + /// + ///Enables the caller to align a file resource on a particular alignment boundary. + /// + WIM_MSG_ALIGNMENT, + WIM_MSG_RETRY, + /// + ///Enables the caller to align a file resource on a particular alignment boundary. + /// + WIM_MSG_SPLIT, + WIM_MSG_FILEINFO, + WIM_MSG_INFO, + WIM_MSG_WARNING, + WIM_MSG_CHK_PROCESS, + WIM_MSG_WARNING_OBJECTID, + WIM_MSG_STALE_MOUNT_DIR, + WIM_MSG_STALE_MOUNT_FILE, + WIM_MSG_MOUNT_CLEANUP_PROGRESS, + WIM_MSG_SUCCESS = 0x00000000, + WIM_MSG_ABORT_IMAGE = 0xFFFFFFFF + }; + + /// + ///Capture will do byte-by-byte verification of single instance files. + /// + public const uint WIM_FLAG_VERIFY = 0x00000002; + /// + ///Specifies that the image is to be sequentially read for caching/performance purposes. + /// + public const uint WIM_FLAG_INDEX = 0x00000004; + + } + + /// + ///Maps CreateFileAccess to CreateFileAccessPrivate + /// + /// + private + CreateFileAccessPrivate + GetMappedFileAccess(CreateFileAccess access) + { + // + //Map the file access specified from an int to uint. + // + CreateFileAccessPrivate fileAccess; + switch (access) + { + case CreateFileAccess.Read: + fileAccess = CreateFileAccessPrivate.Read; + break; + case CreateFileAccess.Write: + fileAccess = CreateFileAccessPrivate.Write; + break; + default: + throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, "No file access level specified.")); + } + return fileAccess; + } + + //[CLSCompliant(false)] + [FlagsAttribute] + private + enum + CreateFileAccessPrivate : uint + { + /// + ///Mapping from CreateFileAccess.Read + /// + Read = 0x80000000, + /// + ///Mapping from CreateFileAccess.Write + /// + Write = 0x40000000 + } + + /// + ///Image event messages. + /// + //[CLSCompliant(false)] + public + enum + ImageEventMessage : uint + { + /// + ///Enables the caller to prevent a file or a directory from being captured or applied. + /// + Progress = NativeMethods.WIMMessage.WIM_MSG_PROGRESS, + /// + ///Notification sent to enable the caller to prevent a file or a directory from being captured or applied. + ///To prevent a file or a directory from being captured or applied, call WindowsImageContainer.SkipFile(). + /// + Process = NativeMethods.WIMMessage.WIM_MSG_PROCESS, + /// + ///Enables the caller to prevent a file resource from being compressed during a capture. + /// + Compress = NativeMethods.WIMMessage.WIM_MSG_COMPRESS, + /// + ///Alerts the caller that an error has occurred while capturing or applying an image. + /// + Error = NativeMethods.WIMMessage.WIM_MSG_ERROR, + /// + ///Enables the caller to align a file resource on a particular alignment boundary. + /// + Alignment = NativeMethods.WIMMessage.WIM_MSG_ALIGNMENT, + /// + ///Enables the caller to align a file resource on a particular alignment boundary. + /// + Split = NativeMethods.WIMMessage.WIM_MSG_SPLIT, + /// + ///Indicates that volume information is being gathered during an image capture. + /// + Scanning = NativeMethods.WIMMessage.WIM_MSG_SCANNING, + /// + ///Indicates the number of files that will be captured or applied. + /// + SetRange = NativeMethods.WIMMessage.WIM_MSG_SETRANGE, + /// + ///Indicates the number of files that have been captured or applied. + /// + SetPos = NativeMethods.WIMMessage.WIM_MSG_SETPOS, + /// + ///Indicates that a file has been either captured or applied. + /// + StepIt = NativeMethods.WIMMessage.WIM_MSG_STEPIT, + /// + ///Success. + /// + Success = NativeMethods.WIMMessage.WIM_MSG_SUCCESS, + /// + ///Abort. + /// + Abort = NativeMethods.WIMMessage.WIM_MSG_ABORT_IMAGE, + + MountCleanupProgress = NativeMethods.WIMMessage.WIM_MSG_MOUNT_CLEANUP_PROGRESS + } + + // + //WindowsImageContainer Member Data + // + private IntPtr m_ImageContainerHandle; //Handle to the .wim file + private string m_WindowsImageFilePath; //Path to the .wim file + + private WindowsImage[] m_Images; //Array of image objects inside a .wim file + private int m_ImageCount; //Number of images inside a .wim file + + // + //DO NOT CHANGE! + // + private static NativeMethods.MessageCallback m_MessageCallback; + } + + /// + ///Describes the file that is being processed for the ProcessFileEvent. + /// + public + class + DefaultImageEventArgs : EventArgs + { + /// + ///Default constructor. + /// + public + DefaultImageEventArgs(IntPtr wideParameter, IntPtr leftParameter, IntPtr userData) + { + m_wParam = wideParameter; + m_lParam = leftParameter; + m_UserData = userData; + + + } + /// + ///wParam + /// + public IntPtr WideParameter + { + get + { + return m_wParam; + } + } + /// + ///lParam + /// + public IntPtr LeftParameter + { + get + { + return m_lParam; + } + } + /// + ///UserData + /// + public IntPtr UserData + { + get + { + return m_UserData; + } + } + + private IntPtr m_wParam; + private IntPtr m_lParam; + private IntPtr m_UserData; + } + + /// + ///Describes the file that is being processed for the ProcessFileEvent. + /// + public + class + ProcessFileEventArgs : EventArgs + { + /// + ///Default constructor. + /// + ///Fully qualified path and file name. For example: c:\file.sys. + ///Default is false - skip file and continue. + ///Set to true to abort the entire image capture. + public + ProcessFileEventArgs(string file, IntPtr skipFileFlag) + { + m_FilePath = file; + m_SkipFileFlag = skipFileFlag; + } + + /// + ///Skip file from being imaged. + /// + public + void + SkipFile() + { + byte[] byteBuffer = { + 0 + }; + int byteBufferSize = byteBuffer.Length; + Marshal.Copy(byteBuffer, 0, m_SkipFileFlag, byteBufferSize); + } + + /// + ///Fully qualified path and file name. + /// + public string FilePath + { + get + { + string stringToReturn = ""; + if (m_FilePath != null) + { + stringToReturn = m_FilePath; + } + return stringToReturn; + } + } + + /// + ///Flag to indicate if the entire image capture should be aborted. + ///Default is false - skip file and continue. Setting to true will + ///abort the entire image capture. + /// + public bool Abort + { + set + { + m_Abort = value; + } + + get + { + return m_Abort; + } + } + private string m_FilePath; + private bool m_Abort; + private IntPtr m_SkipFileFlag; + + } +} diff --git a/Disco.ClientBootstrapper/NullStatus.cs b/Disco.ClientBootstrapper/NullStatus.cs new file mode 100644 index 00000000..998f48fc --- /dev/null +++ b/Disco.ClientBootstrapper/NullStatus.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Disco.ClientBootstrapper +{ + class NullStatus : IStatus + { + public void UpdateStatus(string Heading, string SubHeading, string Message, bool? ShowProgress = null, int? Progress = null) + { + // Do Nothing + } + } +} diff --git a/Disco.ClientBootstrapper/Program.cs b/Disco.ClientBootstrapper/Program.cs new file mode 100644 index 00000000..847c1a2c --- /dev/null +++ b/Disco.ClientBootstrapper/Program.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows.Forms; +using System.Threading; + +namespace Disco.ClientBootstrapper +{ + static class Program + { + public static IStatus Status { get; set; } + public static BootstrapperLoop BootstrapperLoop { get; set; } + public static InstallLoop InstallLoop { get; set; } + + public static List PostBootstrapperActions { get; set; } + public static bool AllowUninstall { get; set; } + public static bool ApplicationExiting { get; set; } + public static Lazy InlinePath = new Lazy(() => + { + return System.IO.Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); + }); + + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main(string[] args) + { + Application.ThreadException += new ThreadExceptionEventHandler(Application_ThreadException); + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + + if (args.Length > 0) + { + switch (args[0].ToLower()) + { + case "/install": + var statusForm = new FormStatus(); + Status = statusForm; + statusForm.Show(); + string installLocation = null; + string wimImage = null; + if (args.Length > 1) + installLocation = args[1]; + if (args.Length > 2) + wimImage = args[2]; + InstallLoop = new InstallLoop(installLocation, wimImage); + InstallLoop.Start(new InstallLoop.CompleteCallback(InstallComplete)); + Application.Run(); + return; + case "/uninstall": + AllowUninstall = true; + Status = new NullStatus(); + Interop.InstallInterop.Uninstall(); + return; + case "/allowuninstall": + AllowUninstall = true; + break; + default: + AllowUninstall = false; + break; + } + } + + if (Status == null) + { + var statusForm = new FormStatus(); + Status = statusForm; + statusForm.Show(); + } + + BootstrapperLoop = new BootstrapperLoop(Status, new BootstrapperLoop.LoopCompleteCallback(LoopComplete)); + BootstrapperLoop.Start(); + + Application.Run(); + } + + static void Application_ThreadException(object sender, ThreadExceptionEventArgs e) + { + WriteAppError(e.Exception); + } + + public static void WriteAppError(Exception ex) + { + try + { + string AppErrorPath = string.Format("{0}{1}", System.Reflection.Assembly.GetExecutingAssembly().Location, ".errors.txt"); + System.Text.StringBuilder ErrorMessage = new System.Text.StringBuilder(); + ErrorMessage.AppendLine(); + ErrorMessage.AppendLine(DateTime.Now.ToLongDateString()); + ErrorMessage.AppendLine(DateTime.Now.ToLongTimeString()); + ErrorMessage.AppendLine(string.Format("Type: {0}", ex.GetType().FullName)); + ErrorMessage.AppendLine(string.Format("Message: {0}", ex.Message)); + ErrorMessage.AppendLine(string.Format("Source: {0}", ex.Source)); + ErrorMessage.AppendLine(string.Format("Stack: {0}", ex.StackTrace)); + System.IO.File.AppendAllText(AppErrorPath, ErrorMessage.ToString()); + } + catch (Exception) { } + } + + public static void LoopComplete() + { + // Run Post Actions + if (PostBootstrapperActions != null) + { + // Check Uninstall + if (AllowUninstall && PostBootstrapperActions.Contains("UninstallBootstrapper")) + { + Interop.InstallInterop.Uninstall(); + } + + // Check ShutdownActions + if (PostBootstrapperActions.Contains("Shutdown")) + { + Status.UpdateStatus("System Preparation (Bootstrapper)", "Shutting Down; Finished...", string.Empty, false, 0); + SleepThread(4000, true); + Interop.ShutdownInterop.Shutdown(); + } + else + if (PostBootstrapperActions.Contains("Reboot")) + { + Status.UpdateStatus("System Preparation (Bootstrapper)", "Rebooting; Finished...", string.Empty, false, 0); + SleepThread(4000, true); + Interop.ShutdownInterop.Reboot(); + } + else + { + Status.UpdateStatus("System Preparation (Bootstrapper)", "Starting System; Finished...", string.Empty, false, 0); + SleepThread(2000, true); + } + } + else + { + Status.UpdateStatus("System Preparation (Bootstrapper)", "Starting System; Finished...", string.Empty, false, 0); + SleepThread(2000, true); + } + + ExitApplication(); + } + + public static void InstallComplete() + { + ExitApplication(); + } + + public static void ExitApplication() + { + if (!ApplicationExiting) + { + ApplicationExiting = true; + if (BootstrapperLoop != null) + { + if (BootstrapperLoop.LoopThread != null) + { + if (BootstrapperLoop.LoopThread.ThreadState == System.Threading.ThreadState.WaitSleepJoin) + { + BootstrapperLoop.LoopThread.Interrupt(); + } + if (BootstrapperLoop.LoopThread.ThreadState == System.Threading.ThreadState.Running) + { + BootstrapperLoop.LoopThread.Abort(); + } + } + } + Application.Exit(); + } + } + + public static void Trace(string Format, params string[] args) + { + System.Diagnostics.Debug.WriteLine(Format, args); + } + + public static void SleepThread(int millisecondsTimeout, bool updateUI) + { + if (updateUI) + { + for (int i = 0; i < millisecondsTimeout; i += 500) + { + int progress = Convert.ToInt32(((Convert.ToDouble(i) / Convert.ToDouble(millisecondsTimeout)) * 100)); + Status.UpdateStatus(null, null, null, true, progress); + Thread.Sleep(500); + } + } + else + { + Thread.Sleep(millisecondsTimeout); + } + } + } +} diff --git a/Disco.ClientBootstrapper/Properties/AssemblyInfo.cs b/Disco.ClientBootstrapper/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..80ae7a9a --- /dev/null +++ b/Disco.ClientBootstrapper/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("Disco - Client Bootstrapper")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Disco")] +[assembly: AssemblyCopyright("")] +[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("5d11beb8-8b5f-4842-8604-9f2d8284f04d")] + +// 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.2.0131.2002")] +[assembly: AssemblyFileVersion("1.2.0131.2002")] diff --git a/Disco.ClientBootstrapper/Properties/Resources.Designer.cs b/Disco.ClientBootstrapper/Properties/Resources.Designer.cs new file mode 100644 index 00000000..d3a32f4e --- /dev/null +++ b/Disco.ClientBootstrapper/Properties/Resources.Designer.cs @@ -0,0 +1,73 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.17929 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Disco.ClientBootstrapper.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Disco.ClientBootstrapper.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap Background_BW { + get { + object obj = ResourceManager.GetObject("Background_BW", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + } +} diff --git a/Disco.ClientBootstrapper/Properties/Resources.resx b/Disco.ClientBootstrapper/Properties/Resources.resx new file mode 100644 index 00000000..729a7a7a --- /dev/null +++ b/Disco.ClientBootstrapper/Properties/Resources.resx @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\Resources\Background-BW.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/Disco.ClientBootstrapper/Properties/Settings.Designer.cs b/Disco.ClientBootstrapper/Properties/Settings.Designer.cs new file mode 100644 index 00000000..fbfd459f --- /dev/null +++ b/Disco.ClientBootstrapper/Properties/Settings.Designer.cs @@ -0,0 +1,30 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.225 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Disco.ClientBootstrapper.Properties +{ + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "10.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase + { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default + { + get + { + return defaultInstance; + } + } + } +} diff --git a/Disco.ClientBootstrapper/Properties/Settings.settings b/Disco.ClientBootstrapper/Properties/Settings.settings new file mode 100644 index 00000000..abf36c5d --- /dev/null +++ b/Disco.ClientBootstrapper/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Disco.ClientBootstrapper/Properties/app.manifest b/Disco.ClientBootstrapper/Properties/app.manifest new file mode 100644 index 00000000..66b901b5 --- /dev/null +++ b/Disco.ClientBootstrapper/Properties/app.manifest @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Disco.ClientBootstrapper/Resources/Background-BW.png b/Disco.ClientBootstrapper/Resources/Background-BW.png new file mode 100644 index 00000000..88bded41 Binary files /dev/null and b/Disco.ClientBootstrapper/Resources/Background-BW.png differ diff --git a/Disco.ClientBootstrapper/UninstallBootstrapper.vbs b/Disco.ClientBootstrapper/UninstallBootstrapper.vbs new file mode 100644 index 00000000..4a9ce48c --- /dev/null +++ b/Disco.ClientBootstrapper/UninstallBootstrapper.vbs @@ -0,0 +1,54 @@ +Option Explicit + +On Error Resume Next + +Dim objWMIService, objWMIProcesses, objFSO, objShell +Dim WaitForProcessID, DeleteDirectory, GroupPolicyScriptLocation + +'WaitForProcessID = CInt(WScript.Arguments.Named.Item("WaitForProcessID")) +DeleteDirectory = Mid(WScript.ScriptFullName, 1, InStrRev(WScript.ScriptFullName, "\") - 1) + +'If WaitForProcessID > 0 Then +' Set objWMIService = GetObject("winmgmts:{impersonationLevel=impersonate}!\\.\root\cimv2") +' Do +' Set objWMIProcesses = objWMIService.ExecQuery("SELECT ProcessId FROM Win32_Process WHERE ProcessId=" & WaitForProcessID) +' If objWMIProcesses.Count = 0 Then +' Exit Do +' End If +' WScript.Sleep 500 +' Loop +' Err.Clear +'End If +'Set objWMIService = Nothing +'Set objWMIProcesses = Nothing + +Set objShell = CreateObject("WScript.Shell") +Set objFSO = CreateObject("Scripting.FileSystemObject") + +Do + Call Err.Clear() + If objFSO.FolderExists(DeleteDirectory) Then + objFSO.DeleteFolder DeleteDirectory, True + End If + WScript.Sleep 1000 +Loop Until Err.Number = 0 + +GroupPolicyScriptLocation = objShell.ExpandEnvironmentStrings("%WinDir%\System32\GroupPolicy\Machine\Scripts\scripts.ini") +If objFSO.FileExists(GroupPolicyScriptLocation) Then + Call objFSO.DeleteFile(GroupPolicyScriptLocation) +End If + +Set objFSO = Nothing + +objShell.RegDelete("HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\HideStartupScripts") +objShell.RegDelete("HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\RunStartupScriptSync") +objShell.RegDelete("HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\Scripts\Shutdown\") +objShell.RegDelete("HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\Scripts\Startup\0\0\") +objShell.RegDelete("HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\Scripts\Startup\0\") +objShell.RegDelete("HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\Scripts\Startup\") +objShell.RegDelete("HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\State\Machine\Scripts\Shutdown\") +objShell.RegDelete("HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\State\Machine\Scripts\Startup\0\0\") +objShell.RegDelete("HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\State\Machine\Scripts\Startup\0\") +objShell.RegDelete("HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\State\Machine\Scripts\Startup\") + +Set objShell = Nothing \ No newline at end of file diff --git a/Disco.Configuration/Disco.Configuration.vbproj b/Disco.Configuration/Disco.Configuration.vbproj new file mode 100644 index 00000000..c1df6e66 --- /dev/null +++ b/Disco.Configuration/Disco.Configuration.vbproj @@ -0,0 +1,116 @@ + + + + Debug + AnyCPU + + + + + {CD7BB28C-B74D-4880-8B7E-4487AB5E6AFC} + Library + Disco.Configuration + Disco.Configuration + 512 + Windows + v4.0 + + + true + full + true + true + bin\Debug\ + Disco.Configuration.xml + 42016,41999,42017,42018,42019,42032,42036,42020,42021,42022 + + + pdbonly + false + true + true + bin\Release\ + Disco.Configuration.xml + 42016,41999,42017,42018,42019,42032,42036,42020,42021,42022 + + + On + + + Binary + + + Off + + + On + + + + + + + + + + + + + + + + + + + + + + + True + Application.myapp + + + True + True + Resources.resx + + + True + Settings.settings + True + + + + + VbMyResourcesResXFileCodeGenerator + Resources.Designer.vb + My.Resources + Designer + + + + + MyApplicationCodeGenerator + Application.Designer.vb + + + SettingsSingleFileGenerator + My + Settings.Designer.vb + + + + + {40F222A9-CC05-4035-AFF4-15A78250EF2B} + Disco.Models + + + + + \ No newline at end of file diff --git a/Disco.Configuration/My Project/Application.Designer.vb b/Disco.Configuration/My Project/Application.Designer.vb new file mode 100644 index 00000000..6015516a --- /dev/null +++ b/Disco.Configuration/My Project/Application.Designer.vb @@ -0,0 +1,13 @@ +'------------------------------------------------------------------------------ +' +' This code was generated by a tool. +' Runtime Version:4.0.30319.235 +' +' Changes to this file may cause incorrect behavior and will be lost if +' the code is regenerated. +' +'------------------------------------------------------------------------------ + +Option Strict On +Option Explicit On + diff --git a/Disco.Configuration/My Project/Application.myapp b/Disco.Configuration/My Project/Application.myapp new file mode 100644 index 00000000..0167050e --- /dev/null +++ b/Disco.Configuration/My Project/Application.myapp @@ -0,0 +1,10 @@ + + + false + false + 0 + true + 0 + 1 + true + diff --git a/Disco.Configuration/My Project/AssemblyInfo.vb b/Disco.Configuration/My Project/AssemblyInfo.vb new file mode 100644 index 00000000..0070d33c --- /dev/null +++ b/Disco.Configuration/My Project/AssemblyInfo.vb @@ -0,0 +1,35 @@ +Imports System +Imports System.Reflection +Imports 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. + +' Review the values of the assembly attributes + + + + + + + + + + +'The following GUID is for the ID of the typelib if this project is exposed to COM + + +' 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: +' + + + diff --git a/Disco.Configuration/My Project/Resources.Designer.vb b/Disco.Configuration/My Project/Resources.Designer.vb new file mode 100644 index 00000000..72cffc23 --- /dev/null +++ b/Disco.Configuration/My Project/Resources.Designer.vb @@ -0,0 +1,62 @@ +'------------------------------------------------------------------------------ +' +' This code was generated by a tool. +' Runtime Version:4.0.30319.235 +' +' Changes to this file may cause incorrect behavior and will be lost if +' the code is regenerated. +' +'------------------------------------------------------------------------------ + +Option Strict On +Option Explicit On + + +Namespace My.Resources + + 'This class was auto-generated by the StronglyTypedResourceBuilder + 'class via a tool like ResGen or Visual Studio. + 'To add or remove a member, edit your .ResX file then rerun ResGen + 'with the /str option, or rebuild your VS project. + ''' + ''' A strongly-typed resource class, for looking up localized strings, etc. + ''' + _ + Friend Module Resources + + Private resourceMan As Global.System.Resources.ResourceManager + + Private resourceCulture As Global.System.Globalization.CultureInfo + + ''' + ''' Returns the cached ResourceManager instance used by this class. + ''' + _ + Friend ReadOnly Property ResourceManager() As Global.System.Resources.ResourceManager + Get + If Object.ReferenceEquals(resourceMan, Nothing) Then + Dim temp As Global.System.Resources.ResourceManager = New Global.System.Resources.ResourceManager("Disco.Configuration.Resources", GetType(Resources).Assembly) + resourceMan = temp + End If + Return resourceMan + End Get + End Property + + ''' + ''' Overrides the current thread's CurrentUICulture property for all + ''' resource lookups using this strongly typed resource class. + ''' + _ + Friend Property Culture() As Global.System.Globalization.CultureInfo + Get + Return resourceCulture + End Get + Set(ByVal value As Global.System.Globalization.CultureInfo) + resourceCulture = value + End Set + End Property + End Module +End Namespace diff --git a/Disco.Configuration/My Project/Resources.resx b/Disco.Configuration/My Project/Resources.resx new file mode 100644 index 00000000..ffecec85 --- /dev/null +++ b/Disco.Configuration/My Project/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Disco.Configuration/My Project/Settings.Designer.vb b/Disco.Configuration/My Project/Settings.Designer.vb new file mode 100644 index 00000000..4cd7656c --- /dev/null +++ b/Disco.Configuration/My Project/Settings.Designer.vb @@ -0,0 +1,73 @@ +'------------------------------------------------------------------------------ +' +' This code was generated by a tool. +' Runtime Version:4.0.30319.235 +' +' Changes to this file may cause incorrect behavior and will be lost if +' the code is regenerated. +' +'------------------------------------------------------------------------------ + +Option Strict On +Option Explicit On + + +Namespace My + + _ + Partial Friend NotInheritable Class MySettings + Inherits Global.System.Configuration.ApplicationSettingsBase + + Private Shared defaultInstance As MySettings = CType(Global.System.Configuration.ApplicationSettingsBase.Synchronized(New MySettings), MySettings) + +#Region "My.Settings Auto-Save Functionality" +#If _MyType = "WindowsForms" Then + Private Shared addedHandler As Boolean + + Private Shared addedHandlerLockObject As New Object + + _ + Private Shared Sub AutoSaveSettings(ByVal sender As Global.System.Object, ByVal e As Global.System.EventArgs) + If My.Application.SaveMySettingsOnExit Then + My.Settings.Save() + End If + End Sub +#End If +#End Region + + Public Shared ReadOnly Property [Default]() As MySettings + Get + +#If _MyType = "WindowsForms" Then + If Not addedHandler Then + SyncLock addedHandlerLockObject + If Not addedHandler Then + AddHandler My.Application.Shutdown, AddressOf AutoSaveSettings + addedHandler = True + End If + End SyncLock + End If +#End If + Return defaultInstance + End Get + End Property + End Class +End Namespace + +Namespace My + + _ + Friend Module MySettingsProperty + + _ + Friend ReadOnly Property Settings() As Global.Disco.Configuration.My.MySettings + Get + Return Global.Disco.Configuration.My.MySettings.Default + End Get + End Property + End Module +End Namespace diff --git a/Disco.Configuration/My Project/Settings.settings b/Disco.Configuration/My Project/Settings.settings new file mode 100644 index 00000000..377f56d6 --- /dev/null +++ b/Disco.Configuration/My Project/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + diff --git a/Disco.Data/App.config b/Disco.Data/App.config new file mode 100644 index 00000000..56b8a360 --- /dev/null +++ b/Disco.Data/App.config @@ -0,0 +1,20 @@ + + + + +
+ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Disco.Data/Configuration/ConfigurationBase.cs b/Disco.Data/Configuration/ConfigurationBase.cs new file mode 100644 index 00000000..529a6555 --- /dev/null +++ b/Disco.Data/Configuration/ConfigurationBase.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Disco.Data.Configuration +{ + public abstract class ConfigurationBase + { + private ConfigurationContext _context; + + public ConfigurationContext Context + { + get + { + return _context; + } + } + public abstract string Scope { get; } + + public ConfigurationBase(ConfigurationContext Context) + { + this._context = Context; + } + + protected void SetValue(string Key, ValueType Value) + { + this.Context.SetConfigurationValue(this.Scope, Key, Value); + } + protected ValueType GetValue(string Key, ValueType Default) + { + return this.Context.GetConfigurationValue(this.Scope, Key, Default); + } + + } +} diff --git a/Disco.Data/Configuration/ConfigurationContext.cs b/Disco.Data/Configuration/ConfigurationContext.cs new file mode 100644 index 00000000..178fa632 --- /dev/null +++ b/Disco.Data/Configuration/ConfigurationContext.cs @@ -0,0 +1,472 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Data.Repository; +using Disco.Models.Repository; +using System.IO; +using System.Security.Cryptography; +using Disco.Models.BI.Interop.Community; +using Newtonsoft.Json; + +namespace Disco.Data.Configuration +{ + public class ConfigurationContext + { + private DiscoDataContext _dbContext; + private DiscoDataContext dbContext + { + get + { + if (_dbContext != null) + return _dbContext; + else + throw new InvalidOperationException("Cache-miss where Configuration Item requested from Cache-Only Configuration Context"); + } + } + + public bool CacheOnly + { + get + { + return _dbContext == null; + } + } + + public ConfigurationContext(DiscoDataContext dbContext) + { + this._dbContext = dbContext; + + // Init Modules + this.moduleBootstrapperConfiguration = new Lazy(() => new Modules.BootstrapperConfiguration(this)); + this.moduleDeviceProfilesConfiguration = new Lazy(() => new Modules.DeviceProfilesConfiguration(this)); + this.moduleOrganisationAddressesConfiguration = new Lazy(() => new Modules.OrganisationAddressesConfiguration(this)); + this.moduleWirelessConfiguration = new Lazy(() => new Modules.WirelessConfiguration(this)); + } + + #region Item Cache + + private static Dictionary> configDictionary = new Dictionary>(); + private static List configurationItems = new List(); + private static object configurationItemsLock = new object(); + + private void loadConfigurationItems(string Scope, bool Reload) + { + if (Reload || !configDictionary.ContainsKey(Scope)) + { + lock (configurationItemsLock) + { + if (Reload || !configDictionary.ContainsKey(Scope)) + { + var newItems = this.dbContext.ConfigurationItems.Where(ci => ci.Scope == Scope).ToArray(); + + if (configDictionary.ContainsKey(Scope)) + { + var existingItems = configDictionary[Scope]; + foreach (var existingItem in existingItems.Values) + { + configurationItems.Remove(existingItem); + } + } + configurationItems.AddRange(newItems); + configDictionary[Scope] = newItems.ToDictionary(ci => ci.Key); + } + } + } + } + public Dictionary> ConfigurationDictionary(string IncludingScope) + { + this.loadConfigurationItems(IncludingScope, false); + return configDictionary; + } + public ConfigurationItem ConfigurationItem(string Scope, string Key) + { + Dictionary scopeDict = default(Dictionary); + if (this.ConfigurationDictionary(Scope).TryGetValue(Scope, out scopeDict)) + { + ConfigurationItem item = default(ConfigurationItem); + if (scopeDict.TryGetValue(Key, out item)) + return item; + } + return null; + } + private List ConfigurationItems(string IncludingScope) + { + this.loadConfigurationItems(IncludingScope, false); + return configurationItems; + } + + #endregion + + #region Helpers + public ValueType GetConfigurationValue(string Scope, string Key, ValueType Default) + { + var ci = this.ConfigurationItem(Scope, Key); + if (ci == null) + return Default; + else + return (ValueType)Convert.ChangeType(ci.Value, typeof(ValueType)); + } + public void SetConfigurationValue(string Scope, string Key, ValueType Value) + { + if (CacheOnly) + throw new InvalidOperationException("Cannot save changes with a CacheOnly Context"); + + var ci = this.ConfigurationItem(Scope, Key); + if (ci == null && Value != null) + { + lock (configurationItemsLock) + { + ci = this.ConfigurationItem(Scope, Key); + if (ci == null) + { + // Create Configuration Item + ci = new ConfigurationItem() { Scope = Scope, Key = Key, Value = Value.ToString() }; + // Add Item to DB & Internal Collections + this.dbContext.ConfigurationItems.Add(ci); + this.ConfigurationItems(Scope).Add(ci); + this.ConfigurationDictionary(Scope)[Scope].Add(Key, ci); + ci = null; + } + } + } + if (ci != null) + { + lock (configurationItemsLock) + { + var entityInfo = dbContext.Entry(ci); + if (entityInfo.State == System.Data.EntityState.Detached) + { + // Reload Scope from DB + this.loadConfigurationItems(Scope, true); + ci = this.ConfigurationItem(Scope, Key); + } + + if (Value == null) + { + dbContext.ConfigurationItems.Remove(ci); + configurationItems.Remove(ci); + configDictionary[Scope].Remove(Key); + } + else + { + ci.Value = Value.ToString(); + } + } + } + } + + public static string ObsfucateValue(string Value) + { + if (string.IsNullOrEmpty(Value)) + return Value; + else + return Convert.ToBase64String(Encoding.Unicode.GetBytes(Value)); + } + public static string DeobsfucateValue(string ObsfucatedValue) + { + if (string.IsNullOrEmpty(ObsfucatedValue)) + return ObsfucatedValue; + else + return Encoding.Unicode.GetString(Convert.FromBase64String(ObsfucatedValue)); + } + #endregion + + #region Configuration Modules + + private Lazy moduleBootstrapperConfiguration; + private Lazy moduleDeviceProfilesConfiguration; + private Lazy moduleOrganisationAddressesConfiguration; + private Lazy moduleWirelessConfiguration; + + public Modules.BootstrapperConfiguration Bootstrapper + { + get + { + return moduleBootstrapperConfiguration.Value; + } + } + public Modules.DeviceProfilesConfiguration DeviceProfiles + { + get + { + return moduleDeviceProfilesConfiguration.Value; + } + } + public Modules.OrganisationAddressesConfiguration OrganisationAddresses + { + get + { + return moduleOrganisationAddressesConfiguration.Value; + } + } + public Modules.WirelessConfiguration Wireless + { + get + { + return moduleWirelessConfiguration.Value; + } + } + + #endregion + + #region System Configuration Items + + public string Scope { get { return "System"; } } + + public string DataStoreLocation + { + get + { + var result = this.GetConfigurationValue(this.Scope, "DataStoreLocation", null); + if (result == null) + { + var appDataPath = System.Web.HttpContext.Current.Server.MapPath("~/App_Data"); + if (appDataPath.EndsWith("\\")) + return appDataPath; + else + return string.Concat(appDataPath, '\\'); + } + else + return result; + } + set + { + if (value == null) + throw new ArgumentNullException("value"); + if (!System.IO.Directory.Exists(value)) + throw new System.IO.DirectoryNotFoundException(string.Format("DataStoreLocation: '{0}' could not be found", value)); + string storePath; + if (value.EndsWith("\\")) + storePath = value; + else + storePath = string.Concat(value, '\\'); + this.SetConfigurationValue(this.Scope, "DataStoreLocation", storePath); + } + } + public string PluginsLocation + { + get + { + return System.IO.Path.Combine(this.DataStoreLocation, @"Plugins\"); + } + } + public string PluginStorageLocation + { + get + { + return System.IO.Path.Combine(this.DataStoreLocation, @"PluginStorage\"); + } + } + #region Organisation Logo + private string OrganisationLogoPath + { + get + { + return System.IO.Path.Combine(DataStoreLocation, "OrganisationLogo.png"); + } + } + //private static string _OrganisationLogoHash; + //private static byte[] _OrganisationLogo; + //private static object _OrganisationLogoLock = new object(); + //private static void LoadOrganisationLogo(ConfigurationContext context, bool reload = false) + //{ + // if (_OrganisationLogoHash == null || reload) + // { + // lock (_OrganisationLogoLock) + // { + // if (_OrganisationLogoHash == null || reload) + // { + // _OrganisationLogo = null; + // _OrganisationLogoHash = null; + + // string organisationLogoPath = context.OrganisationLogoPath; + // if (System.IO.File.Exists(organisationLogoPath)) + // _OrganisationLogo = System.IO.File.ReadAllBytes(organisationLogoPath); + // } + // if (_OrganisationLogo == null || _OrganisationLogo.Length == 0) + // { + // _OrganisationLogo = Disco.Data.Properties.Resources.EmptyLogo; + // } + // if (_OrganisationLogoHash == null) + // { + // using (SHA256 h = SHA256.Create()) + // { + // _OrganisationLogoHash = Convert.ToBase64String(h.ComputeHash(_OrganisationLogo)); + // } + // } + // } + // } + //} + public string OrganisationLogoHash + { + get + { + var path = this.OrganisationLogoPath; + if (File.Exists(path)) + return File.GetLastWriteTimeUtc(path).ToBinary().ToString(); + else + return "-1"; + } + } + public Stream OrganisationLogo + { + get + { + //LoadOrganisationLogo(this); + //if (_OrganisationLogo == null || _OrganisationLogo.Length != 0) + // return new MemoryStream(_OrganisationLogo); + //else + // return null; + var path = this.OrganisationLogoPath; + if (File.Exists(path)) + return new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); + else + return new MemoryStream(Disco.Data.Properties.Resources.EmptyLogo); + } + set + { + string organisationLogoPath = this.OrganisationLogoPath; + if (value == null) + { + if (System.IO.File.Exists(organisationLogoPath)) + System.IO.File.Delete(organisationLogoPath); + } + else + { + using (FileStream fs = new FileStream(organisationLogoPath, FileMode.Create, FileAccess.Write, FileShare.None)) + { + value.CopyTo(fs); + } + } + //LoadOrganisationLogo(this, true); + } + } + #endregion + public string OrganisationName + { + get + { + return this.GetConfigurationValue(this.Scope, "OrganisationName", null); + } + set + { + this.SetConfigurationValue(this.Scope, "OrganisationName", value); + } + } + public bool MultiSiteMode + { + get + { + return this.GetConfigurationValue(this.Scope, "MultiSiteMode", false); + } + set + { + this.SetConfigurationValue(this.Scope, "MultiSiteMode", value); + } + } + + #region Proxy Configuration + public string ProxyAddress + { + get + { + return this.GetConfigurationValue(this.Scope, "ProxyAddress", null); + } + set + { + this.SetConfigurationValue(this.Scope, "ProxyAddress", value); + } + } + public int ProxyPort + { + get + { + return this.GetConfigurationValue(this.Scope, "ProxyPort", 8080); + } + set + { + this.SetConfigurationValue(this.Scope, "ProxyPort", value); + } + } + public string ProxyUsername + { + get + { + return this.GetConfigurationValue(this.Scope, "ProxyUsername", null); + } + set + { + this.SetConfigurationValue(this.Scope, "ProxyUsername", value); + } + } + public string ProxyPassword + { + get + { + return DeobsfucateValue(this.GetConfigurationValue(this.Scope, "ProxyPassword", null)); + } + set + { + this.SetConfigurationValue(this.Scope, "ProxyPassword", ObsfucateValue(value)); + } + } + #endregion + + #region UpdateCheck + public string DeploymentId + { + get + { + return this.GetConfigurationValue(this.Scope, "DeploymentId", null); + } + } + public UpdateResponse UpdateLastCheck + { + get + { + var json = this.GetConfigurationValue(this.Scope, "UpdateLastCheck", null); + if (json != null) + { + try + { + return JsonConvert.DeserializeObject(json); + } + catch (Exception) + { }// Ignore Serialization Issues + } + return null; + } + set + { + if (value == null) + this.SetConfigurationValue(this.Scope, "UpdateLastCheck", null); + + var json = JsonConvert.SerializeObject(value); + this.SetConfigurationValue(this.Scope, "UpdateLastCheck", json); + } + } + public bool UpdateBetaDeployment + { + get + { + return this.GetConfigurationValue(this.Scope, "UpdateBetaDeployment", false); + } + } + public string InstalledDatabaseVersion + { + get + { + return this.GetConfigurationValue(this.Scope, "InstalledDatabaseVersion", null); + } + set + { + this.SetConfigurationValue(this.Scope, "InstalledDatabaseVersion", value); + } + } + #endregion + + #endregion + + } +} diff --git a/Disco.Data/Configuration/Modules/BootstrapperConfiguration.cs b/Disco.Data/Configuration/Modules/BootstrapperConfiguration.cs new file mode 100644 index 00000000..310d88fb --- /dev/null +++ b/Disco.Data/Configuration/Modules/BootstrapperConfiguration.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Disco.Data.Configuration.Modules +{ + public class BootstrapperConfiguration : ConfigurationBase + { + public BootstrapperConfiguration(ConfigurationContext Context) : base(Context) { } + + public override string Scope + { + get { return "Bootstrapper"; } + } + + public string MacSshUsername + { + get + { + return this.GetValue("MacSshUsername", "root"); + } + set + { + this.SetValue("MacSshUsername", value); + } + } + + public string MacSshPassword + { + get + { + return ConfigurationContext.DeobsfucateValue(this.GetValue("MacSshPassword", string.Empty)); + } + set + { + this.SetValue("MacSshPassword", ConfigurationContext.ObsfucateValue(value)); + } + } + } +} diff --git a/Disco.Data/Configuration/Modules/DeviceProfileConfiguration.cs b/Disco.Data/Configuration/Modules/DeviceProfileConfiguration.cs new file mode 100644 index 00000000..f2fa5eea --- /dev/null +++ b/Disco.Data/Configuration/Modules/DeviceProfileConfiguration.cs @@ -0,0 +1,86 @@ +// Removed 2012-06-14 G# - Properties moved to DeviceProfile model & DB Migrated in DBv3. +// +// +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using System.Text; +//using Disco.Models.Repository; + +//namespace Disco.Data.Configuration.Modules +//{ +// public class DeviceProfileConfiguration : ConfigurationBase +// { +// private DeviceProfilesConfiguration deviceProfilesConfig; +// private DeviceProfile deviceProfile; + +// public DeviceProfileConfiguration(ConfigurationContext Context, DeviceProfile DeviceProfile) +// : base(Context) +// { +// this.deviceProfilesConfig = Context.DeviceProfiles; +// this.deviceProfile = DeviceProfile; +// } + +// public override string Scope +// { +// get +// { +// return string.Format("DeviceProfile:{0}", this.deviceProfile.Id); +// } +// } + +// public string ComputerNameTemplate +// { +// get +// { +// return this.GetValue("ComputerNameTemplate", "DeviceProfile.ShortName + '-' + SerialNumber"); +// } +// set +// { +// this.SetValue("ComputerNameTemplate", value); +// } +// } + +// public enum DeviceProfileDistributionTypes : int +// { +// OneToMany = 0, +// OneToOne = 1 +// } +// public DeviceProfileDistributionTypes DistributionType +// { +// get +// { +// return (DeviceProfileDistributionTypes)this.GetValue("DistributionType", (int)DeviceProfileDistributionTypes.OneToMany); +// } +// set +// { +// this.SetValue("DistributionType", (int)value); +// } +// } +// public string OrganisationalUnit +// { +// get +// { +// return this.GetValue("OrganisationalUnit", null); +// } +// set +// { +// this.SetValue("OrganisationalUnit", value); +// } +// } +// public bool AllocateWirelessCertificate +// { +// get +// { +// return this.GetValue("AllocateWirelessCertificate", false); +// } +// set +// { +// this.SetValue("AllocateWirelessCertificate", value); +// } +// } + + + +// } +//} diff --git a/Disco.Data/Configuration/Modules/DeviceProfilesConfiguration.cs b/Disco.Data/Configuration/Modules/DeviceProfilesConfiguration.cs new file mode 100644 index 00000000..4307da85 --- /dev/null +++ b/Disco.Data/Configuration/Modules/DeviceProfilesConfiguration.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Models.Repository; + +namespace Disco.Data.Configuration.Modules +{ + public class DeviceProfilesConfiguration : ConfigurationBase + { + // Removed 2012-06-14 G# - Properties moved to DeviceProfile model & DB Migrated in DBv3. + //private Dictionary deviceProfileConfigurations; + + public DeviceProfilesConfiguration(ConfigurationContext Context) + : base(Context) + { + // Removed 2012-06-14 G# - Properties moved to DeviceProfile model & DB Migrated in DBv3. + //this.deviceProfileConfigurations = new Dictionary(); + } + + public override string Scope + { + get { return "DeviceProfiles"; } + } + + // Removed 2012-06-14 G# - Properties moved to DeviceProfile model & DB Migrated in DBv3. + //public DeviceProfileConfiguration DeviceProfile(DeviceProfile Profile) + //{ + // DeviceProfileConfiguration dpc = default(DeviceProfileConfiguration); + // if (!this.deviceProfileConfigurations.TryGetValue(Profile.Id, out dpc)) + // { + // dpc = new DeviceProfileConfiguration(this.Context, Profile); + // this.deviceProfileConfigurations[Profile.Id] = dpc; + // } + // return dpc; + //} + + public int DefaultDeviceProfileId + { + get + { + var v = this.GetValue("DefaultDeviceProfileId", 1); + if (v > 0) + return v; + else + return 1; + } + set + { + if (value < 1) + throw new ArgumentOutOfRangeException("value", "Expected >= 1"); + this.SetValue("DefaultDeviceProfileId", value); + } + } + public int DefaultAddDeviceOfflineDeviceProfileId + { + get + { + return this.GetValue("DefaultAddDeviceOfflineDeviceProfileId", 0); + } + set + { + if (value < 0) + throw new ArgumentOutOfRangeException("value", "Expected >= 0"); + this.SetValue("DefaultAddDeviceOfflineDeviceProfileId", value); + } + } + + } +} diff --git a/Disco.Data/Configuration/Modules/OrganisationAddressesConfiguration.cs b/Disco.Data/Configuration/Modules/OrganisationAddressesConfiguration.cs new file mode 100644 index 00000000..c507b1c0 --- /dev/null +++ b/Disco.Data/Configuration/Modules/OrganisationAddressesConfiguration.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Models.BI.Config; +using Disco.Models.Repository; +using Newtonsoft.Json; + +namespace Disco.Data.Configuration.Modules +{ + public class OrganisationAddressesConfiguration : ConfigurationBase + { + public OrganisationAddressesConfiguration(ConfigurationContext Context) : base(Context) { } + + public override string Scope + { + get { return "OrganisationAddresses"; } + } + + public OrganisationAddress GetAddress(int Id) + { + var address = default(OrganisationAddress); + var addressString = this.GetValue(Id.ToString(), null); + if (addressString != null) + { + if (addressString.StartsWith("{")) + { + // Assume Json + address = JsonConvert.DeserializeObject(addressString); + } + else + { + // Assume Old Storage Method + address = OrganisationAddress.FromConfigurationEntry(Id, addressString); + } + } + return address; + } + public OrganisationAddress SetAddress(OrganisationAddress Address) + { + if (!Address.Id.HasValue) + { + Address.Id = NextOrganisationAddressId; + } + + string addressString = JsonConvert.SerializeObject(Address); + + this.SetValue(Address.Id.ToString(), addressString); //Address.ToConfigurationEntry()); + return Address; + } + public void RemoveAddress(int Id) + { + // Set Config Item to null = Remove Configuration Item + this.SetValue(Id.ToString(), null); + } + + public List Addresses + { + get + { + Dictionary configAddress = default(Dictionary); + if (this.Context.ConfigurationDictionary(this.Scope).TryGetValue(this.Scope, out configAddress)) + return configAddress.Select( + ca => ca.Value.Value.StartsWith("{") ? + JsonConvert.DeserializeObject(ca.Value.Value) : + OrganisationAddress.FromConfigurationEntry(int.Parse(ca.Key), ca.Value.Value) + ).ToList(); + else + return new List(); // Empty List - No Addresses + } + } + + private int NextOrganisationAddressId + { + get + { + int nextId = 0; + while (true) + { + if (this.Context.ConfigurationItem(this.Scope, nextId.ToString()) == null) + break; + nextId++; + } + return nextId; + } + } + + } +} diff --git a/Disco.Data/Configuration/Modules/WirelessConfiguration.cs b/Disco.Data/Configuration/Modules/WirelessConfiguration.cs new file mode 100644 index 00000000..75b1d682 --- /dev/null +++ b/Disco.Data/Configuration/Modules/WirelessConfiguration.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Disco.Data.Configuration.Modules +{ + public class WirelessConfiguration : ConfigurationBase + { + public const string Provider_eduSTAR = "eduSTAR"; + public const string Provider_eduPaSS = "eduPaSS"; + + public WirelessConfiguration(ConfigurationContext Context) : base(Context) { } + + public override string Scope + { + get { return "Wireless"; } + } + + public int CertificateAutoBufferMax + { + get + { + return this.GetValue("CertificateAutoBufferMax", 50); + } + set + { + this.SetValue("CertificateAutoBufferMax", value); + } + } + public int CertificateAutoBufferLow + { + get + { + return this.GetValue("CertificateAutoBufferLow", 10); + } + set + { + this.SetValue("CertificateAutoBufferLow", value); + } + } + public string Provider + { + get + { + return this.GetValue("Provider", Provider_eduSTAR); + } + set + { + if (string.IsNullOrEmpty(value)) + throw new ArgumentNullException("value"); + if (value.Equals(Provider_eduSTAR, StringComparison.InvariantCultureIgnoreCase)) + this.SetValue("Provider", Provider_eduSTAR); + else + throw new NotSupportedException(string.Format("Unsupported Wireless Provider: ", value)); + } + } + + #region eduSTAR Configuration + + public string eduSTAR_Scope + { + get { return "Wireless_eduSTAR"; } + } + + public string eduSTAR_ServiceAccountSchoolId + { + get + { + return this.Context.GetConfigurationValue(this.eduSTAR_Scope, "ServiceAccountSchoolId", null); + } + set + { + if (string.IsNullOrEmpty(value)) + throw new ArgumentNullException("value"); + this.Context.SetConfigurationValue(this.eduSTAR_Scope, "ServiceAccountSchoolId", value); + } + } + public string eduSTAR_ServiceAccountUsername + { + get + { + return this.Context.GetConfigurationValue(this.eduSTAR_Scope, "ServiceAccountUsername", null); + } + set + { + if (string.IsNullOrEmpty(value)) + throw new ArgumentNullException("value"); + this.Context.SetConfigurationValue(this.eduSTAR_Scope, "ServiceAccountUsername", value); + } + } + public string eduSTAR_ServiceAccountPassword + { + get + { + return ConfigurationContext.DeobsfucateValue(this.Context.GetConfigurationValue(this.eduSTAR_Scope, "ServiceAccountPassword", null)); + } + set + { + if (string.IsNullOrEmpty(value)) + throw new ArgumentNullException("value"); + this.Context.SetConfigurationValue(this.eduSTAR_Scope, "ServiceAccountPassword", ConfigurationContext.ObsfucateValue(value)); + } + } + + #endregion + + } +} diff --git a/Disco.Data/Disco.Data.csproj b/Disco.Data/Disco.Data.csproj new file mode 100644 index 00000000..db7e8ce5 --- /dev/null +++ b/Disco.Data/Disco.Data.csproj @@ -0,0 +1,147 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {85A6BD19-2C64-4746-8F2C-A68A86E8C2D7} + Library + Properties + Disco.Data + Disco.Data + v4.5 + 512 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + False + ..\packages\EntityFramework.5.0.0\lib\net45\EntityFramework.dll + + + False + ..\packages\Newtonsoft.Json.4.5.11\lib\net40\Newtonsoft.Json.dll + + + + + + + + + + + + + + + + + + + + + + + + 201204250418485_DBv0.cs + + + + 201205100307196_DBv1.cs + + + + 201205290205162_DBv2.cs + + + + 201206140712161_DBv3.cs + + + + 201206280337277_DBv4.cs + + + + 201211090325116_DBv5.cs + + + + 201301150107063_DBv6.cs + + + + + + True + True + Resources.resx + + + + + + + + + Designer + + + + + {FBC05512-FCCA-4B16-9E76-8C413C5DE6C9} + Disco.Models + + + + + 201211090325116_DBv5.cs + + + 201301150107063_DBv6.cs + + + ResXFileCodeGenerator + Resources.Designer.cs + Designer + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Disco.Data/Disco.Data.sln b/Disco.Data/Disco.Data.sln new file mode 100644 index 00000000..852c6c41 --- /dev/null +++ b/Disco.Data/Disco.Data.sln @@ -0,0 +1,20 @@ + +Microsoft Visual Studio Solution File, Format Version 11.00 +# Visual Studio 2010 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Disco.Data", "Disco.Data.csproj", "{85A6BD19-2C64-4746-8F2C-A68A86E8C2D7}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {85A6BD19-2C64-4746-8F2C-A68A86E8C2D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85A6BD19-2C64-4746-8F2C-A68A86E8C2D7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85A6BD19-2C64-4746-8F2C-A68A86E8C2D7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85A6BD19-2C64-4746-8F2C-A68A86E8C2D7}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/Disco.Data/Migrations/201204250418485_DBv0.Designer.cs b/Disco.Data/Migrations/201204250418485_DBv0.Designer.cs new file mode 100644 index 00000000..3b11514a --- /dev/null +++ b/Disco.Data/Migrations/201204250418485_DBv0.Designer.cs @@ -0,0 +1,24 @@ +// +namespace Disco.Data.Migrations +{ + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + + public sealed partial class DBv0 : IMigrationMetadata + { + string IMigrationMetadata.Id + { + get { return "201204250418485_DBv0"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return "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"; } + } + } +} diff --git a/Disco.Data/Migrations/201204250418485_DBv0.cs b/Disco.Data/Migrations/201204250418485_DBv0.cs new file mode 100644 index 00000000..3422696b --- /dev/null +++ b/Disco.Data/Migrations/201204250418485_DBv0.cs @@ -0,0 +1,604 @@ +namespace Disco.Data.Migrations +{ + using System.Data.Entity.Migrations; + + public partial class DBv0 : DbMigration + { + public override void Up() + { + CreateTable( + "Configuration", + c => new + { + Scope = c.String(nullable: false, maxLength: 80), + Key = c.String(nullable: false, maxLength: 80), + Value = c.String(), + }) + .PrimaryKey(t => new { t.Scope, t.Key }); + + CreateTable( + "DocumentTemplates", + c => new + { + Id = c.String(nullable: false, maxLength: 30), + Description = c.String(nullable: false, maxLength: 250), + Scope = c.String(nullable: false, maxLength: 6), + FilterExpression = c.String(maxLength: 250), + }) + .PrimaryKey(t => t.Id); + + CreateTable( + "JobSubTypes", + c => new + { + Id = c.String(nullable: false, maxLength: 20), + JobTypeId = c.String(nullable: false, maxLength: 5), + Description = c.String(nullable: false, maxLength: 100), + }) + .PrimaryKey(t => new { t.Id, t.JobTypeId }) + .ForeignKey("JobTypes", t => t.JobTypeId) + .Index(t => t.JobTypeId); + + CreateTable( + "DeviceComponents", + c => new + { + Id = c.Int(nullable: false, identity: true), + DeviceModelId = c.Int(), + Description = c.String(maxLength: 100), + Cost = c.Decimal(nullable: false, precision: 18, scale: 2), + }) + .PrimaryKey(t => t.Id) + .ForeignKey("DeviceModels", t => t.DeviceModelId) + .Index(t => t.DeviceModelId); + + CreateTable( + "DeviceModels", + c => new + { + Id = c.Int(nullable: false, identity: true), + Description = c.String(maxLength: 500), + Manufacturer = c.String(maxLength: 200), + Model = c.String(maxLength: 200), + ModelType = c.String(maxLength: 40), + Image = c.Binary(), + DefaultPurchaseDate = c.DateTime(), + DeviceCost = c.Decimal(precision: 18, scale: 2), + DefaultWarrantyProvider = c.String(maxLength: 40), + }) + .PrimaryKey(t => t.Id); + + CreateTable( + "Devices", + c => new + { + SerialNumber = c.String(nullable: false, maxLength: 40), + AssetNumber = c.String(maxLength: 40), + Location = c.String(maxLength: 250), + DeviceModelId = c.Int(), + DeviceProfileId = c.Int(nullable: false), + DeviceBatchId = c.Int(), + ComputerName = c.String(maxLength: 24), + AssignedUserId = c.String(maxLength: 50), + LastNetworkLogonDate = c.DateTime(), + CertificateStoreReference = c.String(maxLength: 24), + AllowUnauthenticatedEnrol = c.Boolean(nullable: false), + Active = c.Boolean(nullable: false), + CreatedDate = c.DateTime(nullable: false), + EnrolledDate = c.DateTime(), + LastEnrolDate = c.DateTime(), + DecommissionedDate = c.DateTime(), + }) + .PrimaryKey(t => t.SerialNumber) + .ForeignKey("DeviceModels", t => t.DeviceModelId) + .ForeignKey("DeviceProfiles", t => t.DeviceProfileId) + .ForeignKey("DeviceBatches", t => t.DeviceBatchId) + .ForeignKey("Users", t => t.AssignedUserId) + .Index(t => t.DeviceModelId) + .Index(t => t.DeviceProfileId) + .Index(t => t.DeviceBatchId) + .Index(t => t.AssignedUserId); + + CreateTable( + "DeviceProfiles", + c => new + { + Id = c.Int(nullable: false, identity: true), + Name = c.String(nullable: false, maxLength: 100), + ShortName = c.String(nullable: false, maxLength: 10), + Description = c.String(maxLength: 500), + DefaultOrganisationAddress = c.Int(), + }) + .PrimaryKey(t => t.Id); + + CreateTable( + "DeviceBatches", + c => new + { + Id = c.Int(nullable: false, identity: true), + Name = c.String(maxLength: 500), + PurchaseDate = c.DateTime(nullable: false), + Supplier = c.String(maxLength: 200), + PurchaseDetails = c.String(maxLength: 500), + UnitCost = c.Decimal(precision: 18, scale: 2), + UnitQuantity = c.Int(), + DefaultDeviceModelId = c.Int(), + WarrantyValidUntil = c.DateTime(), + WarrantyDetails = c.String(), + InsuredDate = c.DateTime(), + InsuranceSupplier = c.String(maxLength: 200), + InsuredUntil = c.DateTime(), + InsuranceDetails = c.String(), + Comments = c.String(), + }) + .PrimaryKey(t => t.Id) + .ForeignKey("DeviceModels", t => t.DefaultDeviceModelId) + .Index(t => t.DefaultDeviceModelId); + + CreateTable( + "Users", + c => new + { + Id = c.String(nullable: false, maxLength: 50), + DisplayName = c.String(maxLength: 200), + Surname = c.String(maxLength: 200), + GivenName = c.String(maxLength: 200), + Type = c.String(maxLength: 8), + PhoneNumber = c.String(maxLength: 100), + EmailAddress = c.String(maxLength: 150), + }) + .PrimaryKey(t => t.Id); + + CreateTable( + "UserDetails", + c => new + { + UserId = c.String(nullable: false, maxLength: 50), + Scope = c.String(nullable: false, maxLength: 100), + Key = c.String(nullable: false, maxLength: 100), + Value = c.String(), + }) + .PrimaryKey(t => new { t.UserId, t.Scope, t.Key }) + .ForeignKey("Users", t => t.UserId) + .Index(t => t.UserId); + + CreateTable( + "UserAttachments", + c => new + { + Id = c.Int(nullable: false, identity: true), + UserId = c.String(maxLength: 50), + TechUserId = c.String(nullable: false, maxLength: 50), + Filename = c.String(nullable: false, maxLength: 500), + MimeType = c.String(nullable: false, maxLength: 500), + Timestamp = c.DateTime(nullable: false), + Comments = c.String(nullable: false, maxLength: 500), + DocumentTemplateId = c.String(maxLength: 30), + }) + .PrimaryKey(t => t.Id) + .ForeignKey("Users", t => t.UserId) + .ForeignKey("Users", t => t.TechUserId) + .ForeignKey("DocumentTemplates", t => t.DocumentTemplateId) + .Index(t => t.UserId) + .Index(t => t.TechUserId) + .Index(t => t.DocumentTemplateId); + + CreateTable( + "DeviceUserAssignments", + c => new + { + DeviceSerialNumber = c.String(nullable: false, maxLength: 40), + AssignedDate = c.DateTime(nullable: false), + AssignedUserId = c.String(maxLength: 50), + UnassignedDate = c.DateTime(), + }) + .PrimaryKey(t => new { t.DeviceSerialNumber, t.AssignedDate }) + .ForeignKey("Users", t => t.AssignedUserId) + .ForeignKey("Devices", t => t.DeviceSerialNumber) + .Index(t => t.AssignedUserId) + .Index(t => t.DeviceSerialNumber); + + CreateTable( + "Jobs", + c => new + { + Id = c.Int(nullable: false, identity: true), + JobTypeId = c.String(nullable: false, maxLength: 5), + DeviceSerialNumber = c.String(maxLength: 40), + UserId = c.String(maxLength: 50), + OpenedTechUserId = c.String(nullable: false, maxLength: 50), + OpenedDate = c.DateTime(nullable: false), + ExpectedClosedDate = c.DateTime(), + ClosedTechUserId = c.String(maxLength: 50), + ClosedDate = c.DateTime(), + DeviceHeld = c.DateTime(), + DeviceHeldTechUserId = c.String(maxLength: 50), + DeviceHeldLocation = c.String(maxLength: 100), + DeviceReadyForReturn = c.DateTime(), + DeviceReadyForReturnTechUserId = c.String(maxLength: 50), + DeviceReturnedDate = c.DateTime(), + DeviceReturnedTechUserId = c.String(maxLength: 50), + WaitingForUserAction = c.DateTime(), + }) + .PrimaryKey(t => t.Id) + .ForeignKey("JobTypes", t => t.JobTypeId) + .ForeignKey("Users", t => t.OpenedTechUserId) + .ForeignKey("Users", t => t.ClosedTechUserId) + .ForeignKey("Users", t => t.DeviceHeldTechUserId) + .ForeignKey("Users", t => t.DeviceReadyForReturnTechUserId) + .ForeignKey("Users", t => t.DeviceReturnedTechUserId) + .ForeignKey("Users", t => t.UserId) + .ForeignKey("Devices", t => t.DeviceSerialNumber) + .Index(t => t.JobTypeId) + .Index(t => t.OpenedTechUserId) + .Index(t => t.ClosedTechUserId) + .Index(t => t.DeviceHeldTechUserId) + .Index(t => t.DeviceReadyForReturnTechUserId) + .Index(t => t.DeviceReturnedTechUserId) + .Index(t => t.UserId) + .Index(t => t.DeviceSerialNumber); + + CreateTable( + "JobTypes", + c => new + { + Id = c.String(nullable: false, maxLength: 5), + Description = c.String(maxLength: 100), + }) + .PrimaryKey(t => t.Id); + + CreateTable( + "JobAttachments", + c => new + { + Id = c.Int(nullable: false, identity: true), + JobId = c.Int(nullable: false), + TechUserId = c.String(nullable: false, maxLength: 50), + Filename = c.String(nullable: false, maxLength: 500), + MimeType = c.String(nullable: false, maxLength: 500), + Timestamp = c.DateTime(nullable: false), + Comments = c.String(nullable: false, maxLength: 500), + DocumentTemplateId = c.String(maxLength: 30), + }) + .PrimaryKey(t => t.Id) + .ForeignKey("Jobs", t => t.JobId) + .ForeignKey("Users", t => t.TechUserId) + .ForeignKey("DocumentTemplates", t => t.DocumentTemplateId) + .Index(t => t.JobId) + .Index(t => t.TechUserId) + .Index(t => t.DocumentTemplateId); + + CreateTable( + "JobComponents", + c => new + { + Id = c.Int(nullable: false, identity: true), + JobId = c.Int(nullable: false), + TechUserId = c.String(nullable: false, maxLength: 50), + Description = c.String(maxLength: 500), + Cost = c.Decimal(nullable: false, precision: 18, scale: 2), + }) + .PrimaryKey(t => t.Id) + .ForeignKey("Jobs", t => t.JobId) + .ForeignKey("Users", t => t.TechUserId) + .Index(t => t.JobId) + .Index(t => t.TechUserId); + + CreateTable( + "JobLogs", + c => new + { + Id = c.Int(nullable: false, identity: true), + JobId = c.Int(nullable: false), + TechUserId = c.String(nullable: false, maxLength: 50), + Timestamp = c.DateTime(nullable: false), + Comments = c.String(nullable: false), + }) + .PrimaryKey(t => t.Id) + .ForeignKey("Jobs", t => t.JobId) + .ForeignKey("Users", t => t.TechUserId) + .Index(t => t.JobId) + .Index(t => t.TechUserId); + + CreateTable( + "JobMetaInsurances", + c => new + { + JobId = c.Int(nullable: false), + LossOrDamageDate = c.DateTime(), + EventLocation = c.String(maxLength: 200), + Description = c.String(), + ThirdPartyCaused = c.Boolean(nullable: false), + ThirdPartyCausedName = c.String(maxLength: 200), + ThirdPartyCausedWhy = c.String(maxLength: 600), + WitnessesNamesAddresses = c.String(maxLength: 1200), + BurglaryTheftMethodOfEntry = c.String(maxLength: 200), + PropertyLastSeenDate = c.DateTime(), + PoliceNotified = c.Boolean(nullable: false), + PoliceNotifiedStation = c.String(maxLength: 200), + PoliceNotifiedDate = c.DateTime(), + PoliceNotifiedCrimeReportNo = c.String(maxLength: 400), + RecoverReduceAction = c.String(maxLength: 800), + OtherInterestedParties = c.String(maxLength: 500), + DateOfPurchase = c.DateTime(), + ClaimFormSentDate = c.DateTime(), + ClaimFormSentUserId = c.String(maxLength: 50), + }) + .PrimaryKey(t => t.JobId) + .ForeignKey("Jobs", t => t.JobId) + .ForeignKey("Users", t => t.ClaimFormSentUserId) + .Index(t => t.JobId) + .Index(t => t.ClaimFormSentUserId); + + CreateTable( + "JobMetaWarranties", + c => new + { + JobId = c.Int(nullable: false), + ExternalName = c.String(maxLength: 100), + ExternalLoggedDate = c.DateTime(), + ExternalReference = c.String(maxLength: 100), + ExternalCompletedDate = c.DateTime(), + }) + .PrimaryKey(t => t.JobId) + .ForeignKey("Jobs", t => t.JobId) + .Index(t => t.JobId); + + CreateTable( + "JobMetaNonWarranties", + c => new + { + JobId = c.Int(nullable: false), + IsInsuranceClaim = c.Boolean(nullable: false), + AccountingChargeAddedDate = c.DateTime(), + AccountingChargeAddedUserId = c.String(maxLength: 50), + AccountingChargePaidDate = c.DateTime(), + AccountingChargePaidUserId = c.String(maxLength: 50), + PurchaseOrderRaisedDate = c.DateTime(), + PurchaseOrderRaisedUserId = c.String(maxLength: 50), + PurchaseOrderReference = c.String(maxLength: 20), + PurchaseOrderSentDate = c.DateTime(), + PurchaseOrderSentUserId = c.String(maxLength: 50), + InvoiceReceivedDate = c.DateTime(), + InvoiceReceivedUserId = c.String(maxLength: 50), + RepairerName = c.String(maxLength: 100), + RepairerLoggedDate = c.DateTime(), + RepairerReference = c.String(maxLength: 100), + RepairerCompletedDate = c.DateTime(), + }) + .PrimaryKey(t => t.JobId) + .ForeignKey("Users", t => t.AccountingChargeAddedUserId) + .ForeignKey("Users", t => t.AccountingChargePaidUserId) + .ForeignKey("Users", t => t.PurchaseOrderRaisedUserId) + .ForeignKey("Users", t => t.PurchaseOrderSentUserId) + .ForeignKey("Users", t => t.InvoiceReceivedUserId) + .ForeignKey("Jobs", t => t.JobId) + .Index(t => t.AccountingChargeAddedUserId) + .Index(t => t.AccountingChargePaidUserId) + .Index(t => t.PurchaseOrderRaisedUserId) + .Index(t => t.PurchaseOrderSentUserId) + .Index(t => t.InvoiceReceivedUserId) + .Index(t => t.JobId); + + CreateTable( + "DeviceDetails", + c => new + { + DeviceSerialNumber = c.String(nullable: false, maxLength: 40), + Scope = c.String(nullable: false, maxLength: 100), + Key = c.String(nullable: false, maxLength: 100), + Value = c.String(), + }) + .PrimaryKey(t => new { t.DeviceSerialNumber, t.Scope, t.Key }) + .ForeignKey("Devices", t => t.DeviceSerialNumber) + .Index(t => t.DeviceSerialNumber); + + CreateTable( + "DeviceAttachments", + c => new + { + Id = c.Int(nullable: false, identity: true), + DeviceSerialNumber = c.String(maxLength: 40), + TechUserId = c.String(nullable: false, maxLength: 50), + Filename = c.String(nullable: false, maxLength: 500), + MimeType = c.String(nullable: false, maxLength: 500), + Timestamp = c.DateTime(nullable: false), + Comments = c.String(nullable: false, maxLength: 500), + DocumentTemplateId = c.String(maxLength: 30), + }) + .PrimaryKey(t => t.Id) + .ForeignKey("Devices", t => t.DeviceSerialNumber) + .ForeignKey("Users", t => t.TechUserId) + .ForeignKey("DocumentTemplates", t => t.DocumentTemplateId) + .Index(t => t.DeviceSerialNumber) + .Index(t => t.TechUserId) + .Index(t => t.DocumentTemplateId); + + CreateTable( + "WirelessCertificates", + c => new + { + Id = c.Int(nullable: false, identity: true), + Index = c.Int(nullable: false), + Name = c.String(maxLength: 28), + Content = c.Binary(), + Enabled = c.Boolean(nullable: false), + ExpirationDate = c.DateTime(), + AllocatedDate = c.DateTime(), + DeviceSerialNumber = c.String(maxLength: 40), + }) + .PrimaryKey(t => t.Id) + .ForeignKey("Devices", t => t.DeviceSerialNumber) + .Index(t => t.DeviceSerialNumber); + + CreateTable( + "Jobs_JobSubTypes", + c => new + { + Job_Id = c.Int(nullable: false), + JobSubType_Id = c.String(nullable: false, maxLength: 20), + JobSubType_JobTypeId = c.String(nullable: false, maxLength: 5), + }) + .PrimaryKey(t => new { t.Job_Id, t.JobSubType_Id, t.JobSubType_JobTypeId }) + .ForeignKey("Jobs", t => t.Job_Id, cascadeDelete: true) + .ForeignKey("JobSubTypes", t => new { t.JobSubType_Id, t.JobSubType_JobTypeId }, cascadeDelete: true) + .Index(t => t.Job_Id) + .Index(t => new { t.JobSubType_Id, t.JobSubType_JobTypeId }); + + CreateTable( + "DeviceComponents_JobSubTypes", + c => new + { + DeviceComponent_Id = c.Int(nullable: false), + JobSubType_Id = c.String(nullable: false, maxLength: 20), + JobSubType_JobTypeId = c.String(nullable: false, maxLength: 5), + }) + .PrimaryKey(t => new { t.DeviceComponent_Id, t.JobSubType_Id, t.JobSubType_JobTypeId }) + .ForeignKey("DeviceComponents", t => t.DeviceComponent_Id, cascadeDelete: true) + .ForeignKey("JobSubTypes", t => new { t.JobSubType_Id, t.JobSubType_JobTypeId }, cascadeDelete: true) + .Index(t => t.DeviceComponent_Id) + .Index(t => new { t.JobSubType_Id, t.JobSubType_JobTypeId }); + + CreateTable( + "DocumentTemplates_JobSubTypes", + c => new + { + DocumentTemplate_Id = c.String(nullable: false, maxLength: 30), + JobSubType_Id = c.String(nullable: false, maxLength: 20), + JobSubType_JobTypeId = c.String(nullable: false, maxLength: 5), + }) + .PrimaryKey(t => new { t.DocumentTemplate_Id, t.JobSubType_Id, t.JobSubType_JobTypeId }) + .ForeignKey("DocumentTemplates", t => t.DocumentTemplate_Id, cascadeDelete: true) + .ForeignKey("JobSubTypes", t => new { t.JobSubType_Id, t.JobSubType_JobTypeId }, cascadeDelete: true) + .Index(t => t.DocumentTemplate_Id) + .Index(t => new { t.JobSubType_Id, t.JobSubType_JobTypeId }); + + } + + public override void Down() + { + DropIndex("DocumentTemplates_JobSubTypes", new[] { "JobSubType_Id", "JobSubType_JobTypeId" }); + DropIndex("DocumentTemplates_JobSubTypes", new[] { "DocumentTemplate_Id" }); + DropIndex("DeviceComponents_JobSubTypes", new[] { "JobSubType_Id", "JobSubType_JobTypeId" }); + DropIndex("DeviceComponents_JobSubTypes", new[] { "DeviceComponent_Id" }); + DropIndex("Jobs_JobSubTypes", new[] { "JobSubType_Id", "JobSubType_JobTypeId" }); + DropIndex("Jobs_JobSubTypes", new[] { "Job_Id" }); + DropIndex("WirelessCertificates", new[] { "DeviceSerialNumber" }); + DropIndex("DeviceAttachments", new[] { "DocumentTemplateId" }); + DropIndex("DeviceAttachments", new[] { "TechUserId" }); + DropIndex("DeviceAttachments", new[] { "DeviceSerialNumber" }); + DropIndex("DeviceDetails", new[] { "DeviceSerialNumber" }); + DropIndex("JobMetaNonWarranties", new[] { "JobId" }); + DropIndex("JobMetaNonWarranties", new[] { "InvoiceReceivedUserId" }); + DropIndex("JobMetaNonWarranties", new[] { "PurchaseOrderSentUserId" }); + DropIndex("JobMetaNonWarranties", new[] { "PurchaseOrderRaisedUserId" }); + DropIndex("JobMetaNonWarranties", new[] { "AccountingChargePaidUserId" }); + DropIndex("JobMetaNonWarranties", new[] { "AccountingChargeAddedUserId" }); + DropIndex("JobMetaWarranties", new[] { "JobId" }); + DropIndex("JobMetaInsurances", new[] { "ClaimFormSentUserId" }); + DropIndex("JobMetaInsurances", new[] { "JobId" }); + DropIndex("JobLogs", new[] { "TechUserId" }); + DropIndex("JobLogs", new[] { "JobId" }); + DropIndex("JobComponents", new[] { "TechUserId" }); + DropIndex("JobComponents", new[] { "JobId" }); + DropIndex("JobAttachments", new[] { "DocumentTemplateId" }); + DropIndex("JobAttachments", new[] { "TechUserId" }); + DropIndex("JobAttachments", new[] { "JobId" }); + DropIndex("Jobs", new[] { "DeviceSerialNumber" }); + DropIndex("Jobs", new[] { "UserId" }); + DropIndex("Jobs", new[] { "DeviceReturnedTechUserId" }); + DropIndex("Jobs", new[] { "DeviceReadyForReturnTechUserId" }); + DropIndex("Jobs", new[] { "DeviceHeldTechUserId" }); + DropIndex("Jobs", new[] { "ClosedTechUserId" }); + DropIndex("Jobs", new[] { "OpenedTechUserId" }); + DropIndex("Jobs", new[] { "JobTypeId" }); + DropIndex("DeviceUserAssignments", new[] { "DeviceSerialNumber" }); + DropIndex("DeviceUserAssignments", new[] { "AssignedUserId" }); + DropIndex("UserAttachments", new[] { "DocumentTemplateId" }); + DropIndex("UserAttachments", new[] { "TechUserId" }); + DropIndex("UserAttachments", new[] { "UserId" }); + DropIndex("UserDetails", new[] { "UserId" }); + DropIndex("DeviceBatches", new[] { "DefaultDeviceModelId" }); + DropIndex("Devices", new[] { "AssignedUserId" }); + DropIndex("Devices", new[] { "DeviceBatchId" }); + DropIndex("Devices", new[] { "DeviceProfileId" }); + DropIndex("Devices", new[] { "DeviceModelId" }); + DropIndex("DeviceComponents", new[] { "DeviceModelId" }); + DropIndex("JobSubTypes", new[] { "JobTypeId" }); + DropForeignKey("DocumentTemplates_JobSubTypes", new[] { "JobSubType_Id", "JobSubType_JobTypeId" }, "JobSubTypes"); + DropForeignKey("DocumentTemplates_JobSubTypes", "DocumentTemplate_Id", "DocumentTemplates"); + DropForeignKey("DeviceComponents_JobSubTypes", new[] { "JobSubType_Id", "JobSubType_JobTypeId" }, "JobSubTypes"); + DropForeignKey("DeviceComponents_JobSubTypes", "DeviceComponent_Id", "DeviceComponents"); + DropForeignKey("Jobs_JobSubTypes", new[] { "JobSubType_Id", "JobSubType_JobTypeId" }, "JobSubTypes"); + DropForeignKey("Jobs_JobSubTypes", "Job_Id", "Jobs"); + DropForeignKey("WirelessCertificates", "DeviceSerialNumber", "Devices"); + DropForeignKey("DeviceAttachments", "DocumentTemplateId", "DocumentTemplates"); + DropForeignKey("DeviceAttachments", "TechUserId", "Users"); + DropForeignKey("DeviceAttachments", "DeviceSerialNumber", "Devices"); + DropForeignKey("DeviceDetails", "DeviceSerialNumber", "Devices"); + DropForeignKey("JobMetaNonWarranties", "JobId", "Jobs"); + DropForeignKey("JobMetaNonWarranties", "InvoiceReceivedUserId", "Users"); + DropForeignKey("JobMetaNonWarranties", "PurchaseOrderSentUserId", "Users"); + DropForeignKey("JobMetaNonWarranties", "PurchaseOrderRaisedUserId", "Users"); + DropForeignKey("JobMetaNonWarranties", "AccountingChargePaidUserId", "Users"); + DropForeignKey("JobMetaNonWarranties", "AccountingChargeAddedUserId", "Users"); + DropForeignKey("JobMetaWarranties", "JobId", "Jobs"); + DropForeignKey("JobMetaInsurances", "ClaimFormSentUserId", "Users"); + DropForeignKey("JobMetaInsurances", "JobId", "Jobs"); + DropForeignKey("JobLogs", "TechUserId", "Users"); + DropForeignKey("JobLogs", "JobId", "Jobs"); + DropForeignKey("JobComponents", "TechUserId", "Users"); + DropForeignKey("JobComponents", "JobId", "Jobs"); + DropForeignKey("JobAttachments", "DocumentTemplateId", "DocumentTemplates"); + DropForeignKey("JobAttachments", "TechUserId", "Users"); + DropForeignKey("JobAttachments", "JobId", "Jobs"); + DropForeignKey("Jobs", "DeviceSerialNumber", "Devices"); + DropForeignKey("Jobs", "UserId", "Users"); + DropForeignKey("Jobs", "DeviceReturnedTechUserId", "Users"); + DropForeignKey("Jobs", "DeviceReadyForReturnTechUserId", "Users"); + DropForeignKey("Jobs", "DeviceHeldTechUserId", "Users"); + DropForeignKey("Jobs", "ClosedTechUserId", "Users"); + DropForeignKey("Jobs", "OpenedTechUserId", "Users"); + DropForeignKey("Jobs", "JobTypeId", "JobTypes"); + DropForeignKey("DeviceUserAssignments", "DeviceSerialNumber", "Devices"); + DropForeignKey("DeviceUserAssignments", "AssignedUserId", "Users"); + DropForeignKey("UserAttachments", "DocumentTemplateId", "DocumentTemplates"); + DropForeignKey("UserAttachments", "TechUserId", "Users"); + DropForeignKey("UserAttachments", "UserId", "Users"); + DropForeignKey("UserDetails", "UserId", "Users"); + DropForeignKey("DeviceBatches", "DefaultDeviceModelId", "DeviceModels"); + DropForeignKey("Devices", "AssignedUserId", "Users"); + DropForeignKey("Devices", "DeviceBatchId", "DeviceBatches"); + DropForeignKey("Devices", "DeviceProfileId", "DeviceProfiles"); + DropForeignKey("Devices", "DeviceModelId", "DeviceModels"); + DropForeignKey("DeviceComponents", "DeviceModelId", "DeviceModels"); + DropForeignKey("JobSubTypes", "JobTypeId", "JobTypes"); + DropTable("DocumentTemplates_JobSubTypes"); + DropTable("DeviceComponents_JobSubTypes"); + DropTable("Jobs_JobSubTypes"); + DropTable("WirelessCertificates"); + DropTable("DeviceAttachments"); + DropTable("DeviceDetails"); + DropTable("JobMetaNonWarranties"); + DropTable("JobMetaWarranties"); + DropTable("JobMetaInsurances"); + DropTable("JobLogs"); + DropTable("JobComponents"); + DropTable("JobAttachments"); + DropTable("JobTypes"); + DropTable("Jobs"); + DropTable("DeviceUserAssignments"); + DropTable("UserAttachments"); + DropTable("UserDetails"); + DropTable("Users"); + DropTable("DeviceBatches"); + DropTable("DeviceProfiles"); + DropTable("Devices"); + DropTable("DeviceModels"); + DropTable("DeviceComponents"); + DropTable("JobSubTypes"); + DropTable("DocumentTemplates"); + DropTable("Configuration"); + } + } +} diff --git a/Disco.Data/Migrations/201205100307196_DBv1.Designer.cs b/Disco.Data/Migrations/201205100307196_DBv1.Designer.cs new file mode 100644 index 00000000..cf7f06df --- /dev/null +++ b/Disco.Data/Migrations/201205100307196_DBv1.Designer.cs @@ -0,0 +1,24 @@ +// +namespace Disco.Data.Migrations +{ + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + + public sealed partial class DBv1 : IMigrationMetadata + { + string IMigrationMetadata.Id + { + get { return "201205100307196_DBv1"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return "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"; } + } + } +} diff --git a/Disco.Data/Migrations/201205100307196_DBv1.cs b/Disco.Data/Migrations/201205100307196_DBv1.cs new file mode 100644 index 00000000..d25c4a23 --- /dev/null +++ b/Disco.Data/Migrations/201205100307196_DBv1.cs @@ -0,0 +1,25 @@ +namespace Disco.Data.Migrations +{ + using System.Data.Entity.Migrations; + + public partial class DBv1 : DbMigration + { + public override void Up() + { + AddColumn("DocumentTemplates", "FlattenForm", c => c.Boolean(nullable: false, defaultValue: false)); + AddColumn("JobMetaNonWarranties", "AccountingChargeRequiredDate", c => c.DateTime()); + AddColumn("JobMetaNonWarranties", "AccountingChargeRequiredUserId", c => c.String(maxLength: 50)); + AddForeignKey("JobMetaNonWarranties", "AccountingChargeRequiredUserId", "Users", "Id"); + CreateIndex("JobMetaNonWarranties", "AccountingChargeRequiredUserId"); + } + + public override void Down() + { + DropIndex("JobMetaNonWarranties", new[] { "AccountingChargeRequiredUserId" }); + DropForeignKey("JobMetaNonWarranties", "AccountingChargeRequiredUserId", "Users"); + DropColumn("JobMetaNonWarranties", "AccountingChargeRequiredUserId"); + DropColumn("JobMetaNonWarranties", "AccountingChargeRequiredDate"); + DropColumn("DocumentTemplates", "FlattenForm"); + } + } +} diff --git a/Disco.Data/Migrations/201205290205162_DBv2.Designer.cs b/Disco.Data/Migrations/201205290205162_DBv2.Designer.cs new file mode 100644 index 00000000..d56876d9 --- /dev/null +++ b/Disco.Data/Migrations/201205290205162_DBv2.Designer.cs @@ -0,0 +1,24 @@ +// +namespace Disco.Data.Migrations +{ + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + + public sealed partial class DBv2 : IMigrationMetadata + { + string IMigrationMetadata.Id + { + get { return "201205290205162_DBv2"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return "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"; } + } + } +} diff --git a/Disco.Data/Migrations/201205290205162_DBv2.cs b/Disco.Data/Migrations/201205290205162_DBv2.cs new file mode 100644 index 00000000..eda6b55c --- /dev/null +++ b/Disco.Data/Migrations/201205290205162_DBv2.cs @@ -0,0 +1,17 @@ +namespace Disco.Data.Migrations +{ + using System.Data.Entity.Migrations; + + public partial class DBv2 : DbMigration + { + public override void Up() + { + AddColumn("Jobs", "Flags", c => c.Long()); + } + + public override void Down() + { + DropColumn("Jobs", "Flags"); + } + } +} diff --git a/Disco.Data/Migrations/201206140712161_DBv3.Designer.cs b/Disco.Data/Migrations/201206140712161_DBv3.Designer.cs new file mode 100644 index 00000000..48691b6a --- /dev/null +++ b/Disco.Data/Migrations/201206140712161_DBv3.Designer.cs @@ -0,0 +1,24 @@ +// +namespace Disco.Data.Migrations +{ + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + + public sealed partial class DBv3 : IMigrationMetadata + { + string IMigrationMetadata.Id + { + get { return "201206140712161_DBv3"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return "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"; } + } + } +} diff --git a/Disco.Data/Migrations/201206140712161_DBv3.cs b/Disco.Data/Migrations/201206140712161_DBv3.cs new file mode 100644 index 00000000..2944d7bb --- /dev/null +++ b/Disco.Data/Migrations/201206140712161_DBv3.cs @@ -0,0 +1,30 @@ +namespace Disco.Data.Migrations +{ + using System.Data.Entity.Migrations; + + public partial class DBv3 : DbMigration + { + public override void Up() + { + AddColumn("DeviceProfiles", "ComputerNameTemplate", c => c.String(nullable: true)); + Sql(@"UPDATE DeviceProfiles SET ComputerNameTemplate='DeviceProfile.ShortName + ''-'' + SerialNumber'"); + AlterColumn("DeviceProfiles", "ComputerNameTemplate", c => c.String(nullable: false)); + + AddColumn("DeviceProfiles", "DistributionType", c => c.Int(nullable: false)); + AddColumn("DeviceProfiles", "OrganisationalUnit", c => c.String()); + AddColumn("DeviceProfiles", "AllocateWirelessCertificate", c => c.Boolean(nullable: false)); + AddColumn("DeviceProfiles", "EnforceComputerNameConvention", c => c.Boolean(nullable: false)); + AddColumn("DeviceProfiles", "EnforceOrganisationalUnit", c => c.Boolean(nullable: false)); + } + + public override void Down() + { + DropColumn("DeviceProfiles", "EnforceOrganisationalUnit"); + DropColumn("DeviceProfiles", "EnforceComputerNameConvention"); + DropColumn("DeviceProfiles", "AllocateWirelessCertificate"); + DropColumn("DeviceProfiles", "OrganisationalUnit"); + DropColumn("DeviceProfiles", "DistributionType"); + DropColumn("DeviceProfiles", "ComputerNameTemplate"); + } + } +} diff --git a/Disco.Data/Migrations/201206280337277_DBv4.Designer.cs b/Disco.Data/Migrations/201206280337277_DBv4.Designer.cs new file mode 100644 index 00000000..84b8410f --- /dev/null +++ b/Disco.Data/Migrations/201206280337277_DBv4.Designer.cs @@ -0,0 +1,24 @@ +// +namespace Disco.Data.Migrations +{ + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + + public sealed partial class DBv4 : IMigrationMetadata + { + string IMigrationMetadata.Id + { + get { return "201206280337277_DBv4"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return "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"; } + } + } +} diff --git a/Disco.Data/Migrations/201206280337277_DBv4.cs b/Disco.Data/Migrations/201206280337277_DBv4.cs new file mode 100644 index 00000000..cba14a45 --- /dev/null +++ b/Disco.Data/Migrations/201206280337277_DBv4.cs @@ -0,0 +1,52 @@ +namespace Disco.Data.Migrations +{ + using System.Data.Entity.Migrations; + + public partial class DBv4 : DbMigration + { + public override void Up() + { + AddColumn("DeviceProfiles", "ProvisionADAccount", c => c.Boolean(nullable: false)); + Sql(@"UPDATE [DeviceProfiles] SET [ProvisionADAccount]=1;"); + + DropColumn("Devices", "CertificateStoreReference"); + + RenameTable(name: "WirelessCertificates", newName: "DeviceCertificates"); + AddColumn("DeviceCertificates", "ProviderId", c => c.String(maxLength: 64)); + RenameColumn("DeviceCertificates", "Index", "ProviderIndex"); + Sql("UPDATE DeviceCertificates SET ProviderId='EduSTARnetCertificateProvider'"); + AlterColumn("DeviceCertificates", "ProviderId", c => c.String(nullable: false, maxLength: 64)); + + //RenameColumn("DeviceProfiles", "AllocateWirelessCertificate", "AllocateCertificate"); + AddColumn("DeviceProfiles", "CertificateProviderId", c => c.String(maxLength: 64)); + Sql(@"UPDATE [DeviceProfiles] SET [CertificateProviderId]='EduSTARnetCertificateProvider' WHERE [AllocateWirelessCertificate]=1;"); + + // Migrate eduSTAR.net Configuration + Sql(@"UPDATE [Configuration] SET [Scope]='CertificateProvider_eduSTAR.net', [Key]='AutoBufferMin' WHERE [Scope]='Wireless' AND [Key]='CertificateAutoBufferLow'; +UPDATE [Configuration] SET [Scope]='CertificateProvider_eduSTAR.net', [Key]='AutoBufferMax' WHERE [Scope]='Wireless' AND [Key]='CertificateAutoBufferMax'; +UPDATE [Configuration] SET [Scope]='CertificateProvider_eduSTAR.net', [Key]='ServicePassword' WHERE [Scope]='Wireless_eduSTAR' AND [Key]='ServiceAccountPassword'; +UPDATE [Configuration] SET [Scope]='CertificateProvider_eduSTAR.net', [Key]='SchoolId' WHERE [Scope]='Wireless_eduSTAR' AND [Key]='ServiceAccountSchoolId'; +UPDATE [Configuration] SET [Scope]='CertificateProvider_eduSTAR.net', [Key]='ServiceUsername' WHERE [Scope]='Wireless_eduSTAR' AND [Key]='ServiceAccountUsername';" + ); + + Sql(@"UPDATE [DeviceModels] SET [DefaultWarrantyProvider]='LWTWarrantyProvider' WHERE [DefaultWarrantyProvider]='LWT';"); + + DropColumn("DeviceProfiles", "AllocateWirelessCertificate"); + } + + public override void Down() + { + AddColumn("DeviceProfiles", "AllocateWirelessCertificate", c => c.Boolean(nullable: false)); + + RenameColumn("DeviceCertificates", "ProviderIndex", "Index"); + DropColumn("DeviceCertificates", "ProviderId"); + RenameTable(name: "DeviceCertificates", newName: "WirelessCertificates"); + + DropColumn("DeviceProfiles", "CertificateProviderId"); + + AddColumn("Devices", "CertificateStoreReference", c => c.String(maxLength: 24)); + + DropColumn("DeviceProfiles", "ProvisionADAccount"); + } + } +} diff --git a/Disco.Data/Migrations/201211090325116_DBv5.Designer.cs b/Disco.Data/Migrations/201211090325116_DBv5.Designer.cs new file mode 100644 index 00000000..3111a16b --- /dev/null +++ b/Disco.Data/Migrations/201211090325116_DBv5.Designer.cs @@ -0,0 +1,27 @@ +// +namespace Disco.Data.Migrations +{ + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + public sealed partial class DBv5 : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(DBv5)); + + string IMigrationMetadata.Id + { + get { return "201211090325116_DBv5"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/Disco.Data/Migrations/201211090325116_DBv5.cs b/Disco.Data/Migrations/201211090325116_DBv5.cs new file mode 100644 index 00000000..6fb5bfa8 --- /dev/null +++ b/Disco.Data/Migrations/201211090325116_DBv5.cs @@ -0,0 +1,96 @@ +namespace Disco.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class DBv5 : DbMigration + { + public override void Up() + { + // Drop Foreign Keys + + // 2012-11-09 - G# + // ForeignKey Names are not consistant among databases - Especially version 4.3.1 -> 5.0.0.net45 + #region "Support inconsistant foreign key names" + // DeviceCertificates was renamed from WirelessCertificates + Sql(@" + BEGIN TRY + ALTER TABLE [dbo].[DeviceCertificates] DROP CONSTRAINT [FK_dbo.DeviceCertificates_dbo.Devices_DeviceSerialNumber]; + END TRY + BEGIN CATCH + ALTER TABLE [dbo].[DeviceCertificates] DROP CONSTRAINT [FK_WirelessCertificates_Devices_DeviceSerialNumber]; + END CATCH;", true); + // DeviceAttachments + Sql(@" + BEGIN TRY + ALTER TABLE [dbo].[DeviceAttachments] DROP CONSTRAINT [FK_dbo.DeviceAttachments_dbo.Devices_DeviceSerialNumber]; + END TRY + BEGIN CATCH + ALTER TABLE [dbo].[DeviceAttachments] DROP CONSTRAINT [FK_DeviceAttachments_Devices_DeviceSerialNumber]; + END CATCH;", true); + // DeviceDetails + Sql(@" + BEGIN TRY + ALTER TABLE [dbo].[DeviceDetails] DROP CONSTRAINT [FK_dbo.DeviceDetails_dbo.Devices_DeviceSerialNumber]; + END TRY + BEGIN CATCH + ALTER TABLE [dbo].[DeviceDetails] DROP CONSTRAINT [FK_DeviceDetails_Devices_DeviceSerialNumber]; + END CATCH;", true); + // Jobs + Sql(@" + BEGIN TRY + ALTER TABLE [dbo].[Jobs] DROP CONSTRAINT [FK_dbo.Jobs_dbo.Devices_DeviceSerialNumber]; + END TRY + BEGIN CATCH + ALTER TABLE [dbo].[Jobs] DROP CONSTRAINT [FK_Jobs_Devices_DeviceSerialNumber]; + END CATCH;", true); + // DeviceUserAssignments + Sql(@" + BEGIN TRY + ALTER TABLE [dbo].[DeviceUserAssignments] DROP CONSTRAINT [FK_dbo.DeviceUserAssignments_dbo.Devices_DeviceSerialNumber]; + END TRY + BEGIN CATCH + ALTER TABLE [dbo].[DeviceUserAssignments] DROP CONSTRAINT [FK_DeviceUserAssignments_Devices_DeviceSerialNumber]; + END CATCH;", true); + #endregion + + AlterColumn("dbo.Devices", "SerialNumber", c => c.String(nullable: false, maxLength: 60)); + AlterColumn("dbo.DeviceUserAssignments", "DeviceSerialNumber", c => c.String(nullable: false, maxLength: 60)); + AlterColumn("dbo.Jobs", "DeviceSerialNumber", c => c.String(maxLength: 60)); + AlterColumn("dbo.DeviceDetails", "DeviceSerialNumber", c => c.String(nullable: false, maxLength: 60)); + AlterColumn("dbo.DeviceAttachments", "DeviceSerialNumber", c => c.String(maxLength: 60)); + AlterColumn("dbo.DeviceCertificates", "DeviceSerialNumber", c => c.String(maxLength: 60)); + + // Re-create Foreign Keys + AddForeignKey("dbo.DeviceCertificates", "DeviceSerialNumber", "dbo.Devices", "SerialNumber"); + AddForeignKey("dbo.DeviceAttachments", "DeviceSerialNumber", "dbo.Devices", "SerialNumber"); + AddForeignKey("dbo.DeviceDetails", "DeviceSerialNumber", "dbo.Devices", "SerialNumber"); + AddForeignKey("dbo.Jobs", "DeviceSerialNumber", "dbo.Devices", "SerialNumber"); + AddForeignKey("dbo.DeviceUserAssignments", "DeviceSerialNumber", "dbo.Devices", "SerialNumber"); + } + + public override void Down() + { + // Drop Foreign Keys + DropForeignKey("dbo.DeviceCertificates", "DeviceSerialNumber", "dbo.Devices", "SerialNumber"); + DropForeignKey("dbo.DeviceAttachments", "DeviceSerialNumber", "dbo.Devices", "SerialNumber"); + DropForeignKey("dbo.DeviceDetails", "DeviceSerialNumber", "dbo.Devices", "SerialNumber"); + DropForeignKey("dbo.Jobs", "DeviceSerialNumber", "dbo.Devices", "SerialNumber"); + DropForeignKey("dbo.DeviceUserAssignments", "DeviceSerialNumber", "dbo.Devices", "SerialNumber"); + + AlterColumn("dbo.DeviceCertificates", "DeviceSerialNumber", c => c.String(maxLength: 40)); + AlterColumn("dbo.DeviceAttachments", "DeviceSerialNumber", c => c.String(maxLength: 40)); + AlterColumn("dbo.DeviceDetails", "DeviceSerialNumber", c => c.String(nullable: false, maxLength: 40)); + AlterColumn("dbo.Jobs", "DeviceSerialNumber", c => c.String(maxLength: 40)); + AlterColumn("dbo.DeviceUserAssignments", "DeviceSerialNumber", c => c.String(nullable: false, maxLength: 40)); + AlterColumn("dbo.Devices", "SerialNumber", c => c.String(nullable: false, maxLength: 40)); + + // Re-create Foreign Keys + AddForeignKey("dbo.DeviceCertificates", "DeviceSerialNumber", "dbo.Devices", "SerialNumber"); + AddForeignKey("dbo.DeviceAttachments", "DeviceSerialNumber", "dbo.Devices", "SerialNumber"); + AddForeignKey("dbo.DeviceDetails", "DeviceSerialNumber", "dbo.Devices", "SerialNumber"); + AddForeignKey("dbo.Jobs", "DeviceSerialNumber", "dbo.Devices", "SerialNumber"); + AddForeignKey("dbo.DeviceUserAssignments", "DeviceSerialNumber", "dbo.Devices", "SerialNumber"); + } + } +} diff --git a/Disco.Data/Migrations/201211090325116_DBv5.resx b/Disco.Data/Migrations/201211090325116_DBv5.resx new file mode 100644 index 00000000..b1b4ea98 --- /dev/null +++ b/Disco.Data/Migrations/201211090325116_DBv5.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + \ No newline at end of file diff --git a/Disco.Data/Migrations/201301150107063_DBv6.Designer.cs b/Disco.Data/Migrations/201301150107063_DBv6.Designer.cs new file mode 100644 index 00000000..2a4d4f83 --- /dev/null +++ b/Disco.Data/Migrations/201301150107063_DBv6.Designer.cs @@ -0,0 +1,27 @@ +// +namespace Disco.Data.Migrations +{ + using System.Data.Entity.Migrations; + using System.Data.Entity.Migrations.Infrastructure; + using System.Resources; + + public sealed partial class DBv6 : IMigrationMetadata + { + private readonly ResourceManager Resources = new ResourceManager(typeof(DBv6)); + + string IMigrationMetadata.Id + { + get { return "201301150107063_DBv6"; } + } + + string IMigrationMetadata.Source + { + get { return null; } + } + + string IMigrationMetadata.Target + { + get { return Resources.GetString("Target"); } + } + } +} diff --git a/Disco.Data/Migrations/201301150107063_DBv6.cs b/Disco.Data/Migrations/201301150107063_DBv6.cs new file mode 100644 index 00000000..69354537 --- /dev/null +++ b/Disco.Data/Migrations/201301150107063_DBv6.cs @@ -0,0 +1,18 @@ +namespace Disco.Data.Migrations +{ + using System; + using System.Data.Entity.Migrations; + + public partial class DBv6 : DbMigration + { + public override void Up() + { + DropColumn("dbo.DeviceModels", "Image"); + } + + public override void Down() + { + AddColumn("dbo.DeviceModels", "Image", c => c.Binary()); + } + } +} diff --git a/Disco.Data/Migrations/201301150107063_DBv6.resx b/Disco.Data/Migrations/201301150107063_DBv6.resx new file mode 100644 index 00000000..6b2efbde --- /dev/null +++ b/Disco.Data/Migrations/201301150107063_DBv6.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 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 + + \ No newline at end of file diff --git a/Disco.Data/Migrations/Configuration.cs b/Disco.Data/Migrations/Configuration.cs new file mode 100644 index 00000000..55509047 --- /dev/null +++ b/Disco.Data/Migrations/Configuration.cs @@ -0,0 +1,21 @@ +namespace Disco.Data.Migrations +{ + using System; + using System.Data.Entity; + using System.Data.Entity.Migrations; + using System.Linq; + using Disco.Data.Repository; + + internal sealed class Configuration : DbMigrationsConfiguration + { + public Configuration() + { + AutomaticMigrationsEnabled = false; + } + + protected override void Seed(DiscoDataContext context) + { + context.SeedDatabase(); + } + } +} diff --git a/Disco.Data/Migrations/DiscoDataMigrator.cs b/Disco.Data/Migrations/DiscoDataMigrator.cs new file mode 100644 index 00000000..ce8dc00e --- /dev/null +++ b/Disco.Data/Migrations/DiscoDataMigrator.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Data.Entity.Migrations; +using System.Data.Entity.Migrations.Infrastructure; +using Disco.Data.Repository; + +namespace Disco.Data.Migrations +{ + public static class DiscoDataMigrator + { + + private static DbMigrator GetMigrator() + { + var migContext = new DbMigrationsConfiguration(); + migContext.MigrationsAssembly = typeof(DiscoDataMigrator).Assembly; + migContext.MigrationsNamespace = "Disco.Data.Migrations"; + + return new DbMigrator(migContext); + } + + public static void MigrateLatest(bool Seed) + { + var migrator = GetMigrator(); + + migrator.Update(); + + if (Seed) + SeedDatabase(); + } + public static void ForceMigration(string TargetMigration, bool Seed) + { + var migrator = GetMigrator(); + + migrator.Update(TargetMigration); + + if (Seed) + SeedDatabase(); + } + + public static string MigrationScript(string CurrentMigration, string TargetMigration) + { + var migrator = GetMigrator(); + var scriptor = new MigratorScriptingDecorator(migrator); + return scriptor.ScriptUpdate(CurrentMigration, TargetMigration); + } + + public static void SeedDatabase() + { + // Seed/Update Database + using (DiscoDataContext dbContext = new DiscoDataContext()) + { + dbContext.SeedDatabase(); + try + { + dbContext.SaveChanges(); + } + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine(ex.Message); + throw; + } + } + } + + public static MigrationStatus Status() + { + var migrator = GetMigrator(); + + var appliedMigrations = migrator.GetDatabaseMigrations().ToList(); + var pendingMigrations = migrator.GetPendingMigrations().ToList(); + var currentMigration = appliedMigrations.LastOrDefault(); + + return new MigrationStatus() + { + CurrentMigration = currentMigration, + AppliedMigrations = appliedMigrations, + PendingMigrations = pendingMigrations, + AllMigrations = appliedMigrations.Union(pendingMigrations) + }; + } + + public class MigrationStatus + { + public string CurrentMigration { get; internal set; } + public IEnumerable AppliedMigrations { get; internal set; } + public IEnumerable PendingMigrations { get; internal set; } + public IEnumerable AllMigrations { get; internal set; } + + internal MigrationStatus() + { + // Private Constructor + } + } + } +} diff --git a/Disco.Data/Properties/AssemblyInfo.cs b/Disco.Data/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..4200fafc --- /dev/null +++ b/Disco.Data/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("Disco - Data")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Disco")] +[assembly: AssemblyCopyright("")] +[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("77cc3aa4-5055-44aa-97b1-c42e5f5e1acd")] + +// 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.2.0131.2002")] +[assembly: AssemblyFileVersion("1.2.0131.2002")] diff --git a/Disco.Data/Properties/Resources.Designer.cs b/Disco.Data/Properties/Resources.Designer.cs new file mode 100644 index 00000000..2070c0c5 --- /dev/null +++ b/Disco.Data/Properties/Resources.Designer.cs @@ -0,0 +1,73 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.17929 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Disco.Data.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Disco.Data.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized resource of type System.Byte[]. + /// + internal static byte[] EmptyLogo { + get { + object obj = ResourceManager.GetObject("EmptyLogo", resourceCulture); + return ((byte[])(obj)); + } + } + } +} diff --git a/Disco.Data/Properties/Resources.resx b/Disco.Data/Properties/Resources.resx new file mode 100644 index 00000000..e701f043 --- /dev/null +++ b/Disco.Data/Properties/Resources.resx @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + + ..\Resources\EmptyLogo.png;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/Disco.Data/Repository/DiscoDataContext.cs b/Disco.Data/Repository/DiscoDataContext.cs new file mode 100644 index 00000000..59e3d321 --- /dev/null +++ b/Disco.Data/Repository/DiscoDataContext.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Data.Entity; +using Disco.Models.Repository; +using System.Data.Entity.ModelConfiguration.Conventions; + +namespace Disco.Data.Repository +{ + public class DiscoDataContext : DbContext + { + private Lazy _Configuration; + + public DiscoDataContext() + { + this._Configuration = new Lazy(() => new Configuration.ConfigurationContext(this)); + } + + public virtual DbSet ConfigurationItems { get; set; } + + public virtual DbSet DocumentTemplates { get; set; } + + public virtual DbSet Users { get; set; } + public virtual DbSet UserAttachments { get; set; } + + public virtual DbSet DeviceUserAssignments { get; set; } + + public virtual DbSet Devices { get; set; } + public virtual DbSet DeviceDetails { get; set; } + public virtual DbSet DeviceModels { get; set; } + public virtual DbSet DeviceProfiles { get; set; } + public virtual DbSet DeviceBatches { get; set; } + public virtual DbSet DeviceComponents { get; set; } + public virtual DbSet DeviceAttachments { get; set; } + + public virtual DbSet DeviceCertificates { get; set; } + + public virtual DbSet Jobs { get; set; } + public virtual DbSet JobTypes { get; set; } + public virtual DbSet JobSubTypes { get; set; } + public virtual DbSet JobLogs { get; set; } + public virtual DbSet JobAttachments { get; set; } + public virtual DbSet JobComponents { get; set; } + + public virtual DbSet JobMetaWarranties { get; set; } + public virtual DbSet JobMetaNonWarranties { get; set; } + public virtual DbSet JobMetaInsurances { get; set; } + + public Configuration.ConfigurationContext DiscoConfiguration + { + get + { + return this._Configuration.Value; + } + } + + protected override void OnModelCreating(DbModelBuilder modelBuilder) + { + modelBuilder.Conventions.Remove(); + + modelBuilder.Entity().HasMany(m => m.JobSubTypes).WithMany(m => m.DeviceComponents).Map(m => m.ToTable("DeviceComponents_JobSubTypes")); + modelBuilder.Entity().HasMany(m => m.JobSubTypes).WithMany(m => m.AttachmentTypes).Map(m => m.ToTable("DocumentTemplates_JobSubTypes")); + + modelBuilder.Entity().HasMany(m => m.JobSubTypes).WithMany(m => m.Jobs).Map(m => m.ToTable("Jobs_JobSubTypes")); + modelBuilder.Entity().HasMany(m => m.Jobs).WithOptional(m => m.User); + modelBuilder.Entity().HasMany(m => m.Jobs).WithOptional(m => m.Device); + modelBuilder.Entity().Property(DeviceProfile.PropertyAccessExpressions.DistributionTypeDb); + } + + } +} diff --git a/Disco.Data/Repository/DiscoDataContextInitializer.cs b/Disco.Data/Repository/DiscoDataContextInitializer.cs new file mode 100644 index 00000000..869a5b74 --- /dev/null +++ b/Disco.Data/Repository/DiscoDataContextInitializer.cs @@ -0,0 +1,19 @@ +// Shouldn't Need this => Moved to Entity Migrations... + +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using System.Text; +//using System.Data.Entity; + +//namespace Disco.Data.Repository +//{ +// class DiscoDataContextInitializer : CreateDatabaseIfNotExists +// { +// protected override void Seed(DiscoDataContext context) +// { +// context.SeedDatabase(); +// context.SaveChanges(); +// } +// } +//} diff --git a/Disco.Data/Repository/DiscoDataSeeder.cs b/Disco.Data/Repository/DiscoDataSeeder.cs new file mode 100644 index 00000000..296a639e --- /dev/null +++ b/Disco.Data/Repository/DiscoDataSeeder.cs @@ -0,0 +1,270 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Models.Repository; + +namespace Disco.Data.Repository +{ + public static class DiscoDataSeeder + { + public static void SeedDatabase(this DiscoDataContext context) + { + context.SeedDeploymentId(); + context.SeedDeviceModels(); + context.SeedDeviceProfiles(); + context.SeedJobTypes(); + context.SeedJobSubTypes(); + } + + public static void SeedDeploymentId(this DiscoDataContext context) + { + if (context.ConfigurationItems.Count(ci => ci.Scope == "System" && ci.Key == "DeploymentId") == 0) + { + var deploymentId = Guid.NewGuid().ToString("D"); + context.ConfigurationItems.Add(new ConfigurationItem { Scope = "System", Key = "DeploymentId", Value = deploymentId }); + } + } + public static void SeedJobTypes(this DiscoDataContext context) + { + if (context.JobTypes.Count() == 0) + { + context.JobTypes.Add(new JobType { Id = JobType.JobTypeIds.HWar, Description = "Hardware - Warranty" }); + context.JobTypes.Add(new JobType { Id = JobType.JobTypeIds.HNWar, Description = "Hardware - Non-Warranty" }); + context.JobTypes.Add(new JobType { Id = JobType.JobTypeIds.HMisc, Description = "Hardware - Misc" }); + context.JobTypes.Add(new JobType { Id = JobType.JobTypeIds.SImg, Description = "Software - Reimage" }); + context.JobTypes.Add(new JobType { Id = JobType.JobTypeIds.SApp, Description = "Software - Application" }); + context.JobTypes.Add(new JobType { Id = JobType.JobTypeIds.SOS, Description = "Software - Operating System" }); + } + // 2012-05-22 + #region "User Management" Added + if (context.JobTypes.Count(jt => jt.Id == JobType.JobTypeIds.UMgmt) == 0) + context.JobTypes.Add(new JobType { Id = JobType.JobTypeIds.UMgmt, Description = "User - Management" }); + #endregion + // End + } + public static void SeedDeviceModels(this DiscoDataContext context) + { + if (context.DeviceModels.Count() == 0) + { + context.DeviceModels.Add(new DeviceModel { Manufacturer = "Unknown", Model = "Unknown", Description = "Unknown Device Model" }); + } + UpdateDeviceModelConfiguration(context); + // Removed: 2013-01-14 G# + //UpdateDeviceModelImageStorage(context); + } + public static void SeedDeviceProfiles(this DiscoDataContext context) + { + if (context.DeviceProfiles.Count() == 0) + { + context.DeviceProfiles.Add(new DeviceProfile { ShortName = "WS", Name = "Default", Description = "Initial Default Workstation Profile", ComputerNameTemplate = "DeviceProfile.ShortName + ''-'' + SerialNumber" }); + } + } + public static void SeedJobSubTypes(this DiscoDataContext context) + { + if (context.JobSubTypes.Count() == 0) + { + context.JobSubTypes.Add(new JobSubType { Id = "Bag", JobTypeId = JobType.JobTypeIds.HWar, Description = "Bag" }); + context.JobSubTypes.Add(new JobSubType { Id = "Battery", JobTypeId = JobType.JobTypeIds.HWar, Description = "Battery" }); + context.JobSubTypes.Add(new JobSubType { Id = "BezelCaseBottom", JobTypeId = JobType.JobTypeIds.HWar, Description = "Bezel - Case Bottom" }); + context.JobSubTypes.Add(new JobSubType { Id = "BezelCaseTop", JobTypeId = JobType.JobTypeIds.HWar, Description = "Bezel - Case Top" }); + context.JobSubTypes.Add(new JobSubType { Id = "BezelScreenInner", JobTypeId = JobType.JobTypeIds.HWar, Description = "Bezel - Screen Inner" }); + context.JobSubTypes.Add(new JobSubType { Id = "BezelScreenTop", JobTypeId = JobType.JobTypeIds.HWar, Description = "Bezel - Screen Top" }); + context.JobSubTypes.Add(new JobSubType { Id = "BluetoothAdapter", JobTypeId = JobType.JobTypeIds.HWar, Description = "Bluetooth Adapter" }); + context.JobSubTypes.Add(new JobSubType { Id = "CPU", JobTypeId = JobType.JobTypeIds.HWar, Description = "CPU" }); + context.JobSubTypes.Add(new JobSubType { Id = "HardDrive", JobTypeId = JobType.JobTypeIds.HWar, Description = "Hard Drive" }); + context.JobSubTypes.Add(new JobSubType { Id = "Keyboard", JobTypeId = JobType.JobTypeIds.HWar, Description = "Keyboard" }); + context.JobSubTypes.Add(new JobSubType { Id = "Motherboard", JobTypeId = JobType.JobTypeIds.HWar, Description = "Motherboard" }); + context.JobSubTypes.Add(new JobSubType { Id = "Mouse", JobTypeId = JobType.JobTypeIds.HWar, Description = "Mouse/Track Pad" }); + context.JobSubTypes.Add(new JobSubType { Id = "PowerAdapter", JobTypeId = JobType.JobTypeIds.HWar, Description = "Power Adapter" }); + context.JobSubTypes.Add(new JobSubType { Id = "PowerCord", JobTypeId = JobType.JobTypeIds.HWar, Description = "Power Cord/Socket" }); + context.JobSubTypes.Add(new JobSubType { Id = "RAM", JobTypeId = JobType.JobTypeIds.HWar, Description = "RAM" }); + context.JobSubTypes.Add(new JobSubType { Id = "ReplacementDevice", JobTypeId = JobType.JobTypeIds.HWar, Description = "Replacement Device" }); + context.JobSubTypes.Add(new JobSubType { Id = "Screen", JobTypeId = JobType.JobTypeIds.HWar, Description = "Screen" }); + context.JobSubTypes.Add(new JobSubType { Id = "Speakers", JobTypeId = JobType.JobTypeIds.HWar, Description = "Speakers" }); + context.JobSubTypes.Add(new JobSubType { Id = "WebCamera", JobTypeId = JobType.JobTypeIds.HWar, Description = "Web Camera" }); + context.JobSubTypes.Add(new JobSubType { Id = "WirelessAdapter", JobTypeId = JobType.JobTypeIds.HWar, Description = "Wireless Adapter" }); + context.JobSubTypes.Add(new JobSubType { Id = "Other", JobTypeId = JobType.JobTypeIds.HWar, Description = "Other" }); + context.JobSubTypes.Add(new JobSubType { Id = "Bag", JobTypeId = JobType.JobTypeIds.HNWar, Description = "Bag" }); + context.JobSubTypes.Add(new JobSubType { Id = "Battery", JobTypeId = JobType.JobTypeIds.HNWar, Description = "Battery" }); + context.JobSubTypes.Add(new JobSubType { Id = "BezelCaseBottom", JobTypeId = JobType.JobTypeIds.HNWar, Description = "Bezel - Case Bottom" }); + context.JobSubTypes.Add(new JobSubType { Id = "BezelCaseTop", JobTypeId = JobType.JobTypeIds.HNWar, Description = "Bezel - Case Top" }); + context.JobSubTypes.Add(new JobSubType { Id = "BezelScreenInner", JobTypeId = JobType.JobTypeIds.HNWar, Description = "Bezel - Screen Inner" }); + context.JobSubTypes.Add(new JobSubType { Id = "BezelScreenTop", JobTypeId = JobType.JobTypeIds.HNWar, Description = "Bezel - Screen Top" }); + context.JobSubTypes.Add(new JobSubType { Id = "BluetoothAdapter", JobTypeId = JobType.JobTypeIds.HNWar, Description = "Bluetooth Adapter" }); + context.JobSubTypes.Add(new JobSubType { Id = "CPU", JobTypeId = JobType.JobTypeIds.HNWar, Description = "CPU" }); + context.JobSubTypes.Add(new JobSubType { Id = "HardDrive", JobTypeId = JobType.JobTypeIds.HNWar, Description = "Hard Drive" }); + context.JobSubTypes.Add(new JobSubType { Id = "Keyboard", JobTypeId = JobType.JobTypeIds.HNWar, Description = "Keyboard" }); + context.JobSubTypes.Add(new JobSubType { Id = "Motherboard", JobTypeId = JobType.JobTypeIds.HNWar, Description = "Motherboard" }); + context.JobSubTypes.Add(new JobSubType { Id = "Mouse", JobTypeId = JobType.JobTypeIds.HNWar, Description = "Mouse/Track Pad" }); + context.JobSubTypes.Add(new JobSubType { Id = "PowerAdapter", JobTypeId = JobType.JobTypeIds.HNWar, Description = "Power Adapter" }); + context.JobSubTypes.Add(new JobSubType { Id = "PowerCord", JobTypeId = JobType.JobTypeIds.HNWar, Description = "Power Cord/Socket" }); + context.JobSubTypes.Add(new JobSubType { Id = "RAM", JobTypeId = JobType.JobTypeIds.HNWar, Description = "RAM" }); + context.JobSubTypes.Add(new JobSubType { Id = "ReplacementDevice", JobTypeId = JobType.JobTypeIds.HNWar, Description = "Replacement Device" }); + context.JobSubTypes.Add(new JobSubType { Id = "Screen", JobTypeId = JobType.JobTypeIds.HNWar, Description = "Screen" }); + context.JobSubTypes.Add(new JobSubType { Id = "Speakers", JobTypeId = JobType.JobTypeIds.HNWar, Description = "Speakers" }); + context.JobSubTypes.Add(new JobSubType { Id = "WebCamera", JobTypeId = JobType.JobTypeIds.HNWar, Description = "Web Camera" }); + context.JobSubTypes.Add(new JobSubType { Id = "WirelessAdapter", JobTypeId = JobType.JobTypeIds.HNWar, Description = "Wireless Adapter" }); + context.JobSubTypes.Add(new JobSubType { Id = "Other", JobTypeId = JobType.JobTypeIds.HNWar, Description = "Other" }); + context.JobSubTypes.Add(new JobSubType { Id = "ExternalHardDrive", JobTypeId = JobType.JobTypeIds.HMisc, Description = "External Hard Drive" }); + context.JobSubTypes.Add(new JobSubType { Id = "IWB", JobTypeId = JobType.JobTypeIds.HMisc, Description = "Interactive Whiteboard" }); + context.JobSubTypes.Add(new JobSubType { Id = "InternetDongle", JobTypeId = JobType.JobTypeIds.HMisc, Description = "Internet Dongle" }); + context.JobSubTypes.Add(new JobSubType { Id = "Keyboard", JobTypeId = JobType.JobTypeIds.HMisc, Description = "External Keyboard" }); + context.JobSubTypes.Add(new JobSubType { Id = "MobilePhone", JobTypeId = JobType.JobTypeIds.HMisc, Description = "Mobile Phone" }); + context.JobSubTypes.Add(new JobSubType { Id = "Mouse", JobTypeId = JobType.JobTypeIds.HMisc, Description = "External Mouse" }); + context.JobSubTypes.Add(new JobSubType { Id = "MP3Player", JobTypeId = JobType.JobTypeIds.HMisc, Description = "MP3 Player" }); + context.JobSubTypes.Add(new JobSubType { Id = "PrinterScanner", JobTypeId = JobType.JobTypeIds.HMisc, Description = "Printer/Scanner" }); + context.JobSubTypes.Add(new JobSubType { Id = "Projector", JobTypeId = JobType.JobTypeIds.HMisc, Description = "Projector" }); + context.JobSubTypes.Add(new JobSubType { Id = "USBFlashDrive", JobTypeId = JobType.JobTypeIds.HMisc, Description = "USB Flash Drive" }); + context.JobSubTypes.Add(new JobSubType { Id = "WebCamera", JobTypeId = JobType.JobTypeIds.HMisc, Description = "Web Camera" }); + context.JobSubTypes.Add(new JobSubType { Id = "Other", JobTypeId = JobType.JobTypeIds.HMisc, Description = "Other" }); + context.JobSubTypes.Add(new JobSubType { Id = "ContentIllegal", JobTypeId = JobType.JobTypeIds.SImg, Description = "Content - Illegal" }); + context.JobSubTypes.Add(new JobSubType { Id = "ContentInappropriate", JobTypeId = JobType.JobTypeIds.SImg, Description = "Content - Inappropriate" }); + context.JobSubTypes.Add(new JobSubType { Id = "CorruptOS", JobTypeId = JobType.JobTypeIds.SImg, Description = "Corrupt Operating System" }); + context.JobSubTypes.Add(new JobSubType { Id = "HardwareChanges", JobTypeId = JobType.JobTypeIds.SImg, Description = "Hardware Changes" }); + context.JobSubTypes.Add(new JobSubType { Id = "Malware", JobTypeId = JobType.JobTypeIds.SImg, Description = "Malware" }); + context.JobSubTypes.Add(new JobSubType { Id = "Performance", JobTypeId = JobType.JobTypeIds.SImg, Description = "Performance" }); + context.JobSubTypes.Add(new JobSubType { Id = "UserRequest", JobTypeId = JobType.JobTypeIds.SImg, Description = "User Request" }); + context.JobSubTypes.Add(new JobSubType { Id = "UpdatedImage", JobTypeId = JobType.JobTypeIds.SImg, Description = "Updated Image" }); + context.JobSubTypes.Add(new JobSubType { Id = "Other", JobTypeId = JobType.JobTypeIds.SImg, Description = "Other" }); + context.JobSubTypes.Add(new JobSubType { Id = "CurriculumTool", JobTypeId = JobType.JobTypeIds.SApp, Description = "Curriculum Tool" }); + context.JobSubTypes.Add(new JobSubType { Id = "GamingEntertainment", JobTypeId = JobType.JobTypeIds.SApp, Description = "Gaming/Entertainment" }); + context.JobSubTypes.Add(new JobSubType { Id = "ImageManipulation", JobTypeId = JobType.JobTypeIds.SApp, Description = "Image Manipulation" }); + context.JobSubTypes.Add(new JobSubType { Id = "MultimediaPlayback", JobTypeId = JobType.JobTypeIds.SApp, Description = "Multimedia Playback" }); + context.JobSubTypes.Add(new JobSubType { Id = "Presentation", JobTypeId = JobType.JobTypeIds.SApp, Description = "Presentation" }); + context.JobSubTypes.Add(new JobSubType { Id = "Spreadsheet", JobTypeId = JobType.JobTypeIds.SApp, Description = "Spreadsheet" }); + context.JobSubTypes.Add(new JobSubType { Id = "StaffTool", JobTypeId = JobType.JobTypeIds.SApp, Description = "Staff Tool" }); + context.JobSubTypes.Add(new JobSubType { Id = "StudentReporting", JobTypeId = JobType.JobTypeIds.SApp, Description = "Student Reporting" }); + context.JobSubTypes.Add(new JobSubType { Id = "VideoEditing", JobTypeId = JobType.JobTypeIds.SApp, Description = "Video Editing" }); + context.JobSubTypes.Add(new JobSubType { Id = "WebBrowser", JobTypeId = JobType.JobTypeIds.SApp, Description = "Web Browser" }); + context.JobSubTypes.Add(new JobSubType { Id = "WebBrowserPlugin", JobTypeId = JobType.JobTypeIds.SApp, Description = "Web Browser Plugin" }); + context.JobSubTypes.Add(new JobSubType { Id = "WordProcessing", JobTypeId = JobType.JobTypeIds.SApp, Description = "Word Processing" }); + context.JobSubTypes.Add(new JobSubType { Id = "Other", JobTypeId = JobType.JobTypeIds.SApp, Description = "Other" }); + context.JobSubTypes.Add(new JobSubType { Id = "Appearance", JobTypeId = JobType.JobTypeIds.SOS, Description = "Appearance & Personalisation" }); + context.JobSubTypes.Add(new JobSubType { Id = "DisplaySettings", JobTypeId = JobType.JobTypeIds.SOS, Description = "Display Settings" }); + context.JobSubTypes.Add(new JobSubType { Id = "DriversIWB", JobTypeId = JobType.JobTypeIds.SOS, Description = "Drivers - Interactive Whiteboard" }); + context.JobSubTypes.Add(new JobSubType { Id = "DriversPrintScan", JobTypeId = JobType.JobTypeIds.SOS, Description = "Drivers - Printers/Scanners" }); + context.JobSubTypes.Add(new JobSubType { Id = "DriversSystem", JobTypeId = JobType.JobTypeIds.SOS, Description = "Drivers - System" }); + context.JobSubTypes.Add(new JobSubType { Id = "DriversOther", JobTypeId = JobType.JobTypeIds.SOS, Description = "Drivers - Other" }); + context.JobSubTypes.Add(new JobSubType { Id = "Group Policy", JobTypeId = JobType.JobTypeIds.SOS, Description = "Group Policy" }); + context.JobSubTypes.Add(new JobSubType { Id = "KeyboardMouseConfig", JobTypeId = JobType.JobTypeIds.SOS, Description = "Keyboard/Mouse Configuration" }); + context.JobSubTypes.Add(new JobSubType { Id = "MalwareProtection", JobTypeId = JobType.JobTypeIds.SOS, Description = "Malware Protection" }); + context.JobSubTypes.Add(new JobSubType { Id = "NetworkDrives", JobTypeId = JobType.JobTypeIds.SOS, Description = "Network Drives" }); + context.JobSubTypes.Add(new JobSubType { Id = "NetworkWired", JobTypeId = JobType.JobTypeIds.SOS, Description = "Network - Wired" }); + context.JobSubTypes.Add(new JobSubType { Id = "NetworkWireless", JobTypeId = JobType.JobTypeIds.SOS, Description = "Network - Wireless" }); + context.JobSubTypes.Add(new JobSubType { Id = "NetworkOther", JobTypeId = JobType.JobTypeIds.SOS, Description = "Network - Other" }); + context.JobSubTypes.Add(new JobSubType { Id = "PatchUpdate", JobTypeId = JobType.JobTypeIds.SOS, Description = "Patches/Updates" }); + context.JobSubTypes.Add(new JobSubType { Id = "PowerManagement", JobTypeId = JobType.JobTypeIds.SOS, Description = "Power Management" }); + context.JobSubTypes.Add(new JobSubType { Id = "PrintersScanners", JobTypeId = JobType.JobTypeIds.SOS, Description = "Printers/Scanners" }); + context.JobSubTypes.Add(new JobSubType { Id = "SoundConfig", JobTypeId = JobType.JobTypeIds.SOS, Description = "Sound Configuration" }); + context.JobSubTypes.Add(new JobSubType { Id = "Other", JobTypeId = JobType.JobTypeIds.SOS, Description = "Other" }); + } + + // Feature Request 2012-04-27 by Elijah: https://disco.uservoice.com/forums/159707-feedback/suggestions/2803945-customisable-job-sub-types + #region "Optical Drive" Added + if (context.JobSubTypes.Count(jst => jst.JobTypeId == JobType.JobTypeIds.HNWar && jst.Id == "OpticalDrive") == 0) + context.JobSubTypes.Add(new JobSubType { Id = "OpticalDrive", JobTypeId = JobType.JobTypeIds.HNWar, Description = "Optical Drive" }); + if (context.JobSubTypes.Count(jst => jst.JobTypeId == JobType.JobTypeIds.HWar && jst.Id == "OpticalDrive") == 0) + context.JobSubTypes.Add(new JobSubType { Id = "OpticalDrive", JobTypeId = JobType.JobTypeIds.HWar, Description = "Optical Drive" }); + #endregion + // End Feature Request + + // 2012-05-22 + #region "User Management" Added + if (context.JobSubTypes.Count(jst => jst.JobTypeId == JobType.JobTypeIds.UMgmt) == 0) + { + context.JobSubTypes.Add(new JobSubType { Id = JobSubType.UserManagementJobSubTypes.Infringement, JobTypeId = JobType.JobTypeIds.UMgmt, Description = JobSubType.UserManagementJobSubTypes.Infringement }); + context.JobSubTypes.Add(new JobSubType { Id = JobSubType.UserManagementJobSubTypes.Contact, JobTypeId = JobType.JobTypeIds.UMgmt, Description = JobSubType.UserManagementJobSubTypes.Contact }); + } + #endregion + // End + + // 2012-05-29 - Audits + #region "Audit" Added + if (context.JobSubTypes.Count(jst => jst.JobTypeId == JobType.JobTypeIds.HMisc && jst.Id == "Audit") == 0) + context.JobSubTypes.Add(new JobSubType { Id = "Audit", JobTypeId = JobType.JobTypeIds.HMisc, Description = "Audit" }); + if (context.JobSubTypes.Count(jst => jst.JobTypeId == JobType.JobTypeIds.SApp && jst.Id == "Audit") == 0) + context.JobSubTypes.Add(new JobSubType { Id = "Audit", JobTypeId = JobType.JobTypeIds.SApp, Description = "Audit" }); + #endregion + // End + + // Feature Request 2012-06-16 by James: https://disco.uservoice.com/forums/159707-feedback/suggestions/3002911-add-in-microphone-in-hardware-warranty-and-non-war + #region "Microphone" Added + if (context.JobSubTypes.Count(jst => jst.JobTypeId == JobType.JobTypeIds.HNWar && jst.Id == "Microphone") == 0) + context.JobSubTypes.Add(new JobSubType { Id = "Microphone", JobTypeId = JobType.JobTypeIds.HNWar, Description = "Microphone" }); + if (context.JobSubTypes.Count(jst => jst.JobTypeId == JobType.JobTypeIds.HWar && jst.Id == "Microphone") == 0) + context.JobSubTypes.Add(new JobSubType { Id = "Microphone", JobTypeId = JobType.JobTypeIds.HWar, Description = "Microphone" }); + #endregion + // End Feature Request + } + + private static void UpdateDeviceModelConfiguration(this DiscoDataContext context) + { + if (context.ConfigurationItems.Where(c => c.Scope.StartsWith("DeviceProfile:")).Count() > 0) + { + var configurationItems = context.ConfigurationItems.Where(c => c.Scope.StartsWith("DeviceProfile:")).ToList(); + + var deviceProfiles = context.DeviceProfiles.ToDictionary(dp => dp.Id); + foreach (var configurationItem in configurationItems) + { + int profileId = int.Parse(configurationItem.Scope.Substring(configurationItem.Scope.IndexOf(":") + 1)); + DeviceProfile dp; + if (deviceProfiles.TryGetValue(profileId, out dp)) + { + switch (configurationItem.Key) + { + case "ComputerNameTemplate": + dp.ComputerNameTemplate = configurationItem.Value; + break; + case "DistributionType": + dp.DistributionType = (DeviceProfile.DistributionTypes)(int.Parse(configurationItem.Value)); + break; + case "OrganisationalUnit": + dp.OrganisationalUnit = configurationItem.Value; + break; + case "AllocateWirelessCertificate": + if (bool.Parse(configurationItem.Value)) + dp.CertificateProviderId = ""; + else + dp.CertificateProviderId = null; + break; + default: + continue; // Unknown Configuration Item - Leave in DB & Ignore + } + } + // Remove from DB + context.ConfigurationItems.Remove(configurationItem); + } + } + + } + + // Removed: 2013-01-14 G# +// private static void UpdateDeviceModelImageStorage(this DiscoDataContext dbContext) +// { +//#pragma warning disable 0618 +// var updateModels = dbContext.DeviceModels.Where(dm => dm.Image != null); +// if (updateModels.Count() > 0) +// { +// var dataStoreLocation = dbContext.ConfigurationItems.Where(ci => ci.Scope == "System" && ci.Key == "DataStoreLocation").Select(ci => ci.Value).FirstOrDefault(); +// if (!string.IsNullOrEmpty(dataStoreLocation) && System.IO.Directory.Exists(dataStoreLocation)) +// { +// var deviceModelImagesLocation = System.IO.Path.Combine(dataStoreLocation, "DeviceModelImages"); +// if (!System.IO.Directory.Exists(deviceModelImagesLocation)) +// System.IO.Directory.CreateDirectory(deviceModelImagesLocation); +// foreach (var model in updateModels) +// { +// var modelpath = System.IO.Path.Combine(deviceModelImagesLocation, string.Format("{0}.png", model.Id)); + +// if (model.Image != null && model.Image.Length > 0) +// { +// System.IO.File.WriteAllBytes(modelpath, model.Image); +// } +// model.Image = null; +// } +// } +// } +//#pragma warning restore 0618 +// } + } +} diff --git a/Disco.Data/Repository/DiscoDatabaseConnectionFactory.cs b/Disco.Data/Repository/DiscoDatabaseConnectionFactory.cs new file mode 100644 index 00000000..6f8ea8a0 --- /dev/null +++ b/Disco.Data/Repository/DiscoDatabaseConnectionFactory.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Data.Entity.Infrastructure; +using Microsoft.Win32; +using System.Data.Common; +using System.Security; +using System.Security.Permissions; + +namespace Disco.Data.Repository +{ + public class DiscoDatabaseConnectionFactory : IDbConnectionFactory + { + private const string DiscoRegistryKey = @"SOFTWARE\Disco"; + + private IDbConnectionFactory DefaultConnectionFactory; + private IDbConnectionFactory SqlCeConnectionFactory; + private static string _DiscoDataContextConnectionString; + + public static string DiscoDataContextConnectionString + { + get + { + if (_DiscoDataContextConnectionString == null) + { + // Retrieve from Registry + using (var key = Registry.LocalMachine.OpenSubKey(DiscoRegistryKey)) + { + if (key != null) + _DiscoDataContextConnectionString = (string)key.GetValue("DatabaseConnectionString", null); + } + } + return _DiscoDataContextConnectionString; + } + } + + public static void SetDiscoDataContextConnectionString(string ConnectionString, bool Persist) + { + // Set to Local Cache + _DiscoDataContextConnectionString = ConnectionString; + + if (Persist) + { + // Set to Registry + try + { + using (var key = Registry.LocalMachine.CreateSubKey(DiscoRegistryKey)) + { + key.SetValue("DatabaseConnectionString", ConnectionString, RegistryValueKind.String); + } + } + catch (UnauthorizedAccessException ex) + { + throw new SecurityException(string.Format("Unable to write to the Registry Location: HKML\\{0}[DatabaseConnectionString]", DiscoRegistryKey), ex); + } + } + } + + public DiscoDatabaseConnectionFactory(IDbConnectionFactory Default) + { + this.DefaultConnectionFactory = Default; + this.SqlCeConnectionFactory = new SqlCeConnectionFactory("System.Data.SqlServerCe.4.0"); + } + + public System.Data.Common.DbConnection CreateConnection(string nameOrConnectionString) + { + if (nameOrConnectionString == "Disco.Data.Repository.DiscoDataContext") + { + + var connectionString = DiscoDataContextConnectionString; + if (connectionString == null) + { + throw new InvalidOperationException("The Disco DataContext Connection String has not been configured"); + } + + // Build DiscoDataContext - Use Default Connection Factory (SQLClient) + + //return this.DefaultConnectionFactory.CreateConnection(connectionString); + var connection = DbProviderFactories.GetFactory("System.Data.SqlClient").CreateConnection(); + connection.ConnectionString = connectionString; + return connection; + } + + return SqlCeConnectionFactory.CreateConnection(nameOrConnectionString); + } + } +} diff --git a/Disco.Data/Resources/EmptyLogo.png b/Disco.Data/Resources/EmptyLogo.png new file mode 100644 index 00000000..766f006f Binary files /dev/null and b/Disco.Data/Resources/EmptyLogo.png differ diff --git a/Disco.Data/packages.config b/Disco.Data/packages.config new file mode 100644 index 00000000..bdb15dfe --- /dev/null +++ b/Disco.Data/packages.config @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/Disco.Logging/App.config b/Disco.Logging/App.config new file mode 100644 index 00000000..d87ec9c4 --- /dev/null +++ b/Disco.Logging/App.config @@ -0,0 +1,14 @@ + + + + +
+ + + + + + + + + \ No newline at end of file diff --git a/Disco.Logging/Disco.Logging.csproj b/Disco.Logging/Disco.Logging.csproj new file mode 100644 index 00000000..a70bd2f7 --- /dev/null +++ b/Disco.Logging/Disco.Logging.csproj @@ -0,0 +1,125 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {BD16C575-70C2-4DDE-A572-FC79EF028CB2} + Library + Properties + Disco.Logging + Disco.Logging + v4.0 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + False + ..\packages\EntityFramework.4.3.1\lib\net40\EntityFramework.dll + + + ..\..\Resources\Libraries\fastJSON\fastJSON.dll + + + True + ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll + + + False + ..\packages\Newtonsoft.Json.4.5.1\lib\net40\Newtonsoft.Json.dll + + + ..\..\Resources\Libraries\Quartz\Quartz.dll + + + False + ..\packages\SignalR.Server.0.4.0.0\lib\net40\SignalR.dll + + + ..\packages\SignalR.Hosting.AspNet.0.4.0.0\lib\net40\SignalR.Hosting.AspNet.dll + + + + + + + True + ..\packages\SqlServerCompact.4.0.8482.1\lib\System.Data.SqlServerCe.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + {85A6BD19-2C64-4746-8F2C-A68A86E8C2D7} + Disco.Data + + + {FBC05512-FCCA-4B16-9E76-8C413C5DE6C9} + Disco.Models + + + + + + + + + + + + + + +REM if not exist "$(TargetDir)x86" md "$(TargetDir)x86" +REM xcopy /s /y "$(SolutionDir)packages\SqlServerCompact.4.0.8482.1\NativeBinaries\x86\*.*" "$(TargetDir)x86" +REM if not exist "$(TargetDir)amd64" md "$(TargetDir)amd64" +REM xcopy /s /y "$(SolutionDir)packages\SqlServerCompact.4.0.8482.1\NativeBinaries\amd64\*.*" "$(TargetDir)amd64" + + + \ No newline at end of file diff --git a/Disco.Logging/LogBase.cs b/Disco.Logging/LogBase.cs new file mode 100644 index 00000000..b7eefbca --- /dev/null +++ b/Disco.Logging/LogBase.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Disco.Logging +{ + public abstract class LogBase + { + private Dictionary _EventTypes; + + public LogBase() + { + // Cache Event Types + _EventTypes = this.LoadEventTypes().ToDictionary(et => et.Id); + } + + public abstract int ModuleId { get; } + public abstract string ModuleName { get; } + public abstract string ModuleDescription { get; } + protected abstract List LoadEventTypes(); + + public Dictionary EventTypes + { + get + { + return _EventTypes; + } + } + protected void Log(int EventTypeId, params object[] Args) + { + LogContext.Current.Log(this.ModuleId, EventTypeId, Args); + } + public string LiveLogGroupName + { + get + { + return this.ModuleName; + } + } + + } +} diff --git a/Disco.Logging/LogContext.cs b/Disco.Logging/LogContext.cs new file mode 100644 index 00000000..98213619 --- /dev/null +++ b/Disco.Logging/LogContext.cs @@ -0,0 +1,311 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Data.Repository; +using System.IO; +using System.Management; +using System.Diagnostics; +using System.Data.SqlServerCe; +using System.Data.EntityClient; +using System.Data.Entity; +using Quartz; +using Quartz.Impl; +using Quartz.Impl.Triggers; + +namespace Disco.Logging +{ + public class LogContext + { + public static Dictionary LogModules { get; private set; } + private static object _LogModulesLock = new object(); + + private static LogContext _Current; + private static object _CurrentLock = new Object(); + public static LogContext Current + { + get + { + lock (_CurrentLock) + { + if (_Current == null) + throw new InvalidOperationException("Logging Context has not been Initialized"); + return _Current; + } + } + private set + { + lock (_CurrentLock) + { + _Current = value; + } + } + } + + private static void InitalizeModules() + { + if (LogModules == null) + { + lock (_LogModulesLock) + { + if (LogModules == null) + { + LogModules = new Dictionary(); + // Load all LogModules (Only from Disco Assemblies) + var appDomain = AppDomain.CurrentDomain; + + var logModuleTypes = (from a in appDomain.GetAssemblies() + where !a.GlobalAssemblyCache && !a.IsDynamic && a.FullName.StartsWith("Disco.", StringComparison.InvariantCultureIgnoreCase) + from type in a.GetTypes() + where typeof(LogBase).IsAssignableFrom(type) && !type.IsAbstract + select type); + foreach (var logModuleType in logModuleTypes) + { + var instance = (LogBase)Activator.CreateInstance(logModuleType); + LogModules[instance.ModuleId] = instance; + } + } + } + } + } + + private static void InitalizeDatabase(Targets.LogPersistContext logDbContext) + { + // Add Modules + var existingModules = logDbContext.Modules.Include("EventTypes").ToDictionary(m => m.Id); + foreach (var module in LogModules) + { + // Update/Insert Module + Models.LogModule dbModule; + if (existingModules.TryGetValue(module.Key, out dbModule)) + { + // Update + if (dbModule.Name != module.Value.ModuleName) + dbModule.Name = module.Value.ModuleName; + if (dbModule.Description != module.Value.ModuleDescription) + dbModule.Description = module.Value.ModuleDescription; + } + else + { + // Insert + dbModule = new Models.LogModule() + { + Id = module.Key, + Name = module.Value.ModuleName, + Description = module.Value.ModuleDescription + }; + logDbContext.Modules.Add(dbModule); + } + // Update/Insert Event Types + Dictionary existingEventTypes = (dbModule.EventTypes == null) ? new Dictionary() : dbModule.EventTypes.ToDictionary(et => et.Id); + foreach (var eventType in module.Value.EventTypes) + { + Models.LogEventType dbEventType; + if (existingEventTypes.TryGetValue(eventType.Key, out dbEventType)) + { + // Update + if (dbEventType.Name != eventType.Value.Name) + dbEventType.Name = eventType.Value.Name; + if (dbEventType.Severity != eventType.Value.Severity) + dbEventType.Severity = eventType.Value.Severity; + if (dbEventType.Format != eventType.Value.Format) + dbEventType.Format = eventType.Value.Format; + } + else + { + // Insert + dbEventType = new Models.LogEventType() + { + Id = eventType.Key, + ModuleId = module.Key, + Name = eventType.Value.Name, + Severity = eventType.Value.Severity, + Format = eventType.Value.Format + }; + logDbContext.EventTypes.Add(dbEventType); + } + } + } + + logDbContext.SaveChanges(); + } + + public static string LogFileBasePath(DiscoDataContext DiscoContext) + { + var logDirectoryBase = Path.Combine(DiscoContext.DiscoConfiguration.DataStoreLocation, "Logs"); + // Create Directory Structure + if (!Directory.Exists(logDirectoryBase)) + { + Directory.CreateDirectory(logDirectoryBase); + } + // Ensure Logs are NTFS Compressed - TODO... + //Utilities.CompressDirectory(logDirectory); + // WMI - Doesn't Work for Network Folders... + //var logDirectoryBaseInfo = new DirectoryInfo(logDirectoryBase); + //if ((logDirectoryBaseInfo.Attributes & FileAttributes.Compressed) != FileAttributes.Compressed) + //{ + // var logDirectoryWmiPath = string.Format("Win32_Directory.Name=\"{0}\"", logDirectoryBase); + // using (ManagementObject logDirectoryBaseMO = new ManagementObject(logDirectoryWmiPath)) + // { + // ManagementBaseObject outParams = logDirectoryBaseMO.InvokeMethod("Compress", null, null); + // Debug.WriteLine("LoggingContext.InitalizeCurrent: Compressing Log Folder; Result: " + outParams.Properties["ReturnValue"].Value.ToString()); + // } + //} + return logDirectoryBase; + } + + public static string LogFilePath(DiscoDataContext DiscoContext, DateTime Date, bool CreateDirectory = true) + { + var logDirectoryBase = LogFileBasePath(DiscoContext); + var logDirectory = Path.Combine(logDirectoryBase, Date.Year.ToString()); + if (CreateDirectory && !Directory.Exists(logDirectory)) + { + Directory.CreateDirectory(logDirectory); + } + var logFileName = string.Format("DiscoLog_{0:yyy-MM-dd}.sdf", Date); + return Path.Combine(logDirectory, logFileName); + } + + internal static void ReInitalize(DiscoDataContext DiscoContext) + { + lock (_CurrentLock) + { + var logPath = LogFilePath(DiscoContext, DateTime.Today); + + //var connectionString = string.Format("Data Source=\"{0}\"", logPath); + + SqlCeConnectionStringBuilder sqlCeCSB = new SqlCeConnectionStringBuilder(); + sqlCeCSB.DataSource = logPath; + var connectionString = sqlCeCSB.ToString(); + + // Ensure Database Exists + if (!File.Exists(logPath)) + { + // Create Database + using (var context = new Targets.LogPersistContext(connectionString)) + { + context.Database.CreateIfNotExists(); + } + } + + // Add Modules/Event Types + InitalizeModules(); + using (var context = new Targets.LogPersistContext(connectionString)) + { + InitalizeDatabase(context); + } + + // Create Current LogContext + var currentLogContext = new LogContext(logPath, connectionString); + _Current = currentLogContext; + } + SystemLog.LogLogInitialized(_Current.PersistantStorePath); + try + { + // Get Yesterdays Log + var yesterdaysLogPath = LogFilePath(DiscoContext, DateTime.Today.AddDays(-1), false); + if (File.Exists(yesterdaysLogPath)) + { + SqlCeConnectionStringBuilder sqlCeCSB = new SqlCeConnectionStringBuilder(); + sqlCeCSB.DataSource = yesterdaysLogPath; + var connectionString = sqlCeCSB.ToString(); + int logCount; + using (var context = new Targets.LogPersistContext(connectionString)) + { + logCount = context.Events.Where(e => !(e.ModuleId == 0 && e.EventTypeId == 100)).Count(); + if (logCount == 0) + { + // Delete (empty) Database + context.Database.Delete(); + } + } + } + } + catch (Exception ex) + { + SystemLog.LogError("Error occurred while investigating yesterdays log for deletion", ex.GetType().Name, ex.Message, ex.StackTrace); + } + } + + private static IScheduler _ReInitializeScheduler; + public static void Initalize(DiscoDataContext DiscoContext, ISchedulerFactory SchedulerFactory) + { + ReInitalize(DiscoContext); + + _ReInitializeScheduler = SchedulerFactory.GetScheduler(); + + var reInitalizeJobDetail = new JobDetailImpl("DiscoLogContextReinialize", typeof(LogReInitalizeJob)); + + // Simple Trigger - Issue with Day light savings + //var reInitalizeTrigger = TriggerBuilder.Create() + // .WithIdentity("DiscoLogContextReinializeTrigger") + // .StartAt(DateBuilder.TomorrowAt(0,0,0)) + // .WithSchedule(SimpleScheduleBuilder.Create().WithIntervalInHours(24).RepeatForever()) + // .Build(); + // Use Cron Schedule instead + var reInitalizeTrigger = TriggerBuilder.Create() + .WithIdentity("DiscoLogContextReinializeTrigger") + .StartNow() + .WithSchedule(CronScheduleBuilder.DailyAtHourAndMinute(0, 0)) // Midnight + .Build(); + + _ReInitializeScheduler.ScheduleJob(reInitalizeJobDetail, reInitalizeTrigger); + } + public static string LiveLogAllEventsGroupName + { + get + { + return Targets.LogLiveContext.LiveLogNameAll; + } + } + + private LogContext(string PersistantStorePath, string PersistantStoreConnectionString) + { + this.PersistantStorePath = PersistantStorePath; + this.PersistantStoreConnectionString = PersistantStoreConnectionString; + } + + public string PersistantStorePath { get; private set; } + public string PersistantStoreConnectionString { get; private set; } + + public void Log(int ModuleId, int EventTypeId, params object[] Args) + { + LogBase logModule; + if (LogModules.TryGetValue(ModuleId, out logModule)) + { + Models.LogEventType eventType; + if (logModule.EventTypes.TryGetValue(EventTypeId, out eventType)) + { + var eventTimestamp = DateTime.Now; + if (eventType.UseLive) + { + Targets.LogLiveContext.Broadcast(logModule, eventType, eventTimestamp, Args); + } + if (eventType.UsePersist) + { + string args = null; + if (Args != null && Args.Length > 0) + args = fastJSON.JSON.Instance.ToJSON(Args, false); + using (var context = new Targets.LogPersistContext(PersistantStoreConnectionString)) + { + var e = new Models.LogEvent() + { + Timestamp = eventTimestamp, + ModuleId = logModule.ModuleId, + EventTypeId = eventType.Id, + Arguments = args + }; + context.Events.Add(e); + context.SaveChanges(); + } + } + } + else + throw new InvalidOperationException(string.Format("Unknown Log Event Type Called: {0} (for Module: {1})", EventTypeId, ModuleId)); + } + else + throw new InvalidOperationException(string.Format("Unknown Log Module Called: {0}", ModuleId)); + } + + } +} diff --git a/Disco.Logging/LogReInitalizeJob.cs b/Disco.Logging/LogReInitalizeJob.cs new file mode 100644 index 00000000..bf2b1d54 --- /dev/null +++ b/Disco.Logging/LogReInitalizeJob.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Quartz; +using Disco.Data.Repository; + +namespace Disco.Logging +{ + class LogReInitalizeJob : IJob + { + public void Execute(IJobExecutionContext context) + { + using (DiscoDataContext DiscoContext = new DiscoDataContext()) + { + LogContext.ReInitalize(DiscoContext); + } + } + } +} diff --git a/Disco.Logging/Models/LogEvent.cs b/Disco.Logging/Models/LogEvent.cs new file mode 100644 index 00000000..ef15417c --- /dev/null +++ b/Disco.Logging/Models/LogEvent.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel.DataAnnotations; + +namespace Disco.Logging.Models +{ + [Table("Events")] + public class LogEvent + { + [Required, Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + [Required] + public int ModuleId { get; set; } + [Required] + public int EventTypeId { get; set; } + [Required] + public DateTime Timestamp { get; set; } + public string Arguments { get; set; } + } +} diff --git a/Disco.Logging/Models/LogEventType.cs b/Disco.Logging/Models/LogEventType.cs new file mode 100644 index 00000000..56c55327 --- /dev/null +++ b/Disco.Logging/Models/LogEventType.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel.DataAnnotations; + +namespace Disco.Logging.Models +{ + [Table("EventTypes")] + public class LogEventType + { + [Required, Key, Column(Order=0), DatabaseGenerated(DatabaseGeneratedOption.None)] + public int ModuleId { get; set; } + [Required, Key, Column(Order = 1), DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + [Required, MaxLength(200)] + public string Name { get; set; } + [Required] + public int Severity { get; set; } + [MaxLength(1024)] + public string Format { get; set; } + + [NotMapped] + public bool UsePersist { get; set; } + [NotMapped] + public bool UseLive { get; set; } + [NotMapped] + public bool UseDisplay { get; set; } + + [ForeignKey("ModuleId")] + public LogModule Module { get; set; } + + public enum Severities + { + Information = 0, + Warning = 1, + Error = 2 + } + + public string FormatMessage(object[] Arguments) + { + + if (Arguments != null && Arguments.Length > 0) + { + if (!string.IsNullOrEmpty(Format)) + { + return string.Format(Format, Arguments); + } + else + { + return Arguments + .Select(v => v == null ? string.Empty : v.ToString()) + .Aggregate((a, b) => a + ", " + (b == null ? string.Empty : b)); + } + } + else + { + if (!string.IsNullOrEmpty(Format)) + { + return Format; + } + } + return string.Empty; + } + } +} diff --git a/Disco.Logging/Models/LogLiveEvent.cs b/Disco.Logging/Models/LogLiveEvent.cs new file mode 100644 index 00000000..bb558014 --- /dev/null +++ b/Disco.Logging/Models/LogLiveEvent.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Collections; + +namespace Disco.Logging.Models +{ + public class LogLiveEvent + { + public int ModuleId { get; set; } + public string ModuleName { get; set; } + public string ModuleDescription { get; set; } + public int EventTypeId { get; set; } + public string EventTypeName { get; set; } + public int EventTypeSeverity { get; set; } + + public DateTime Timestamp { get; set; } + public object[] Arguments { get; set; } + public string FormattedMessage { get; set; } + public string FormattedTimestamp { get; set; } + public bool UseDisplay { get; set; } + + public static LogLiveEvent Create(LogBase logModule, Models.LogEventType eventType, DateTime Timestamp, string jsonArguments) + { + object[] Arguments = null; + if (jsonArguments != null) + { + var alArguments = fastJSON.JSON.Instance.Parse(jsonArguments) as ArrayList; + if (alArguments != null) + { + Arguments = alArguments.ToArray(); + } + } + return Create(logModule, eventType, Timestamp, Arguments); + } + + public static LogLiveEvent Create(LogBase logModule, Models.LogEventType eventType, DateTime Timestamp, params object[] Arguments) + { + return new Models.LogLiveEvent() + { + ModuleId = logModule.ModuleId, + ModuleName = logModule.ModuleName, + ModuleDescription = logModule.ModuleDescription, + EventTypeId = eventType.Id, + EventTypeName = eventType.Name, + EventTypeSeverity = eventType.Severity, + Timestamp = Timestamp, + Arguments = Arguments, + FormattedMessage = eventType.FormatMessage(Arguments), + FormattedTimestamp = Timestamp.ToString("dd/MM/yyy hh:mm:ss tt"), + UseDisplay = eventType.UseDisplay + }; + } + + } +} diff --git a/Disco.Logging/Models/LogModule.cs b/Disco.Logging/Models/LogModule.cs new file mode 100644 index 00000000..88488575 --- /dev/null +++ b/Disco.Logging/Models/LogModule.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel.DataAnnotations; + +namespace Disco.Logging.Models +{ + [Table("Modules")] + public class LogModule + { + [Required, Key, DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + [Required, MaxLength(200)] + public string Name { get; set; } + [Required, MaxLength(500)] + public string Description { get; set; } + + public virtual IList EventTypes { get; set; } + } +} diff --git a/Disco.Logging/Properties/AssemblyInfo.cs b/Disco.Logging/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..4e064f91 --- /dev/null +++ b/Disco.Logging/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("Disco - Logging")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Disco")] +[assembly: AssemblyCopyright("")] +[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("03a4a5bf-60c6-4dff-b623-387043b8be86")] + +// 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.2012.0627.1427")] +[assembly: AssemblyFileVersion("1.2012.0627.1427")] diff --git a/Disco.Logging/ReadLogContext.cs b/Disco.Logging/ReadLogContext.cs new file mode 100644 index 00000000..cf13976d --- /dev/null +++ b/Disco.Logging/ReadLogContext.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Logging.Targets; +using Disco.Data.Repository; +using System.IO; +using System.Text.RegularExpressions; +using System.Data.SqlServerCe; +using Disco.Logging.Models; + +namespace Disco.Logging +{ + public class ReadLogContext + { + public DateTime? Start { get; set; } + public DateTime? End { get; set; } + public int? Take { get; set; } + public int? Module { get; set; } + public List EventTypes { get; set; } + + public bool Validate() + { + if (this.Start.HasValue && this.End.HasValue && this.End.Value < this.Start.Value) + throw new ArgumentOutOfRangeException("End", "End must be greater than Start"); + if (this.Start.HasValue && !this.End.HasValue && this.Start > DateTime.Now) + throw new ArgumentOutOfRangeException("Start", "Start must be less than current time"); + + return true; + } + + public List Query(DiscoDataContext DiscoContext) + { + List results = new List(); + + // Validate Options + this.Validate(); + + var relevantLogFiles = RelevantLogFiles(DiscoContext); + relevantLogFiles.Reverse(); + foreach (var logFile in relevantLogFiles) + { + SqlCeConnectionStringBuilder sqlCeCSB = new SqlCeConnectionStringBuilder(); + sqlCeCSB.DataSource = logFile.Item1; + + var logModules = LogContext.LogModules; + + using (var context = new Targets.LogPersistContext(sqlCeCSB.ToString())) + { + var query = this.BuildQuery(context, logFile.Item2, results.Count); + IEnumerable queryResults = query; // Run the Query + results.AddRange(queryResults.Select(le => Models.LogLiveEvent.Create(logModules[le.ModuleId], logModules[le.ModuleId].EventTypes[le.EventTypeId], le.Timestamp, le.Arguments))); + } + if (this.Take.HasValue && this.Take.Value < results.Count) + break; + } + return results; + } + + private static Regex LogFileDateRegex = new Regex("DiscoLog_([0-9]{4})-([0-9]{2})-([0-9]{2}).sdf", RegexOptions.IgnoreCase); + private static DateTime? LogFileDate(string LogFilePath) + { + var fileNameMatch = LogFileDateRegex.Match(LogFilePath); + if (fileNameMatch.Success) + { + return new DateTime(int.Parse(fileNameMatch.Groups[1].Value), + int.Parse(fileNameMatch.Groups[2].Value), + int.Parse(fileNameMatch.Groups[3].Value)); + } + else + { + return null; + } + } + + private List> RelevantLogFiles(DiscoDataContext DiscoContext) + { + List> relevantFiles = new List>(); + var logDirectoryBase = LogContext.LogFileBasePath(DiscoContext); + var logDirectoryBaseInfo = new DirectoryInfo(logDirectoryBase); + var endDate = this.End.HasValue ? this.End.Value : DateTime.Now; + var endDateYear = endDate.Year.ToString(); + + // Try Shortcut ( < 31 Days in Query) + if (this.Start.HasValue) + { + if ((this.End.HasValue && this.End.Value.Subtract(this.Start.Value).Days < 31) || + (!this.End.HasValue && DateTime.Now.Subtract(this.Start.Value).Days < 31)) + { + // Less than 31 Days in Query - Just evaluate each Path + var queryDate = this.Start.Value.Date; + while (queryDate <= endDate) + { + var fileName = LogContext.LogFilePath(DiscoContext, queryDate, false); + if (File.Exists(fileName)) + relevantFiles.Add(new Tuple(fileName, LogFileDate(fileName).Value)); + + queryDate = queryDate.AddDays(1); + } + return relevantFiles; + } + } + + List logYears = new List(); + foreach (var directoryName in logDirectoryBaseInfo.GetDirectories()) + { + int directoryYear; + if (int.TryParse(directoryName.Name, out directoryYear)) + { + logYears.Add(directoryName.Name); + } + } + logYears.Sort(); + + foreach (var logYear in logYears) + { + List logFiles = Directory.EnumerateFiles(Path.Combine(logDirectoryBase, logYear), "DiscoLog_*.sdf").ToList(); + logFiles.Sort(); + if (logYear != endDateYear) + { + foreach (var logFile in logFiles) + { + relevantFiles.Add(new Tuple(logFile, LogFileDate(logFile).Value)); + } + } + else + { + foreach (var logFile in logFiles) + { + var fileNameDate = LogFileDate(logFile); + if (fileNameDate != null) + { + if (fileNameDate.Value < endDate) + { + relevantFiles.Add(new Tuple(logFile, fileNameDate.Value)); + } + else + { + break; // Files are sorted, must be no more... + } + } + } + break; // Years are sorted, must be no more... + } + } + return relevantFiles; + } + + private IQueryable BuildQuery(LogPersistContext LogContext, DateTime LogDate, int Taken) + { + IQueryable query = LogContext.Events.OrderByDescending(le => le.Timestamp); + if (this.Module.HasValue) + { + query = query.Where(le => le.ModuleId == this.Module.Value); + } + if (this.EventTypes != null && this.EventTypes.Count > 0) + { + query = query.Where(le => this.EventTypes.Contains(le.EventTypeId)); + } + if (this.Start.HasValue && this.Start.Value > LogDate) + { + var startValue = DateTime.SpecifyKind(this.Start.Value, DateTimeKind.Local); + query = query.Where(le => le.Timestamp > startValue); + } + if (this.End.HasValue && this.End.Value <= LogDate.AddDays(1)) + { + var endValue = DateTime.SpecifyKind(this.End.Value, DateTimeKind.Local); + query = query.Where(le => le.Timestamp < endValue); + } + if (this.Take.HasValue && this.Take.Value > 0) + { + var take = this.Take.Value - Taken; + query = query.Take(take); + } + return query; + } + + } +} diff --git a/Disco.Logging/SystemLog.cs b/Disco.Logging/SystemLog.cs new file mode 100644 index 00000000..aac8683b --- /dev/null +++ b/Disco.Logging/SystemLog.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Logging.Models; + +namespace Disco.Logging +{ + public class SystemLog : LogBase + { + private const int _ModuleId = 0; + public enum EventTypeIds : int + { + Information = 0, + Warning = 1, + Error = 2, + Exception = 10, + ExceptionWithInner = 11, + LogInitialized = 100 + } + public static SystemLog Current + { + get + { + return (SystemLog)LogContext.LogModules[_ModuleId]; + } + } + private static void Log(EventTypeIds EventTypeId, params object[] Args) + { + Current.Log((int)EventTypeId, Args); + } + public static void LogInformation(params object[] Messages) + { + Log(EventTypeIds.Information, Messages); + } + public static void LogWarning(params object[] Messages) + { + Log(EventTypeIds.Warning, Messages); + } + public static void LogError(params object[] Messages) + { + Log(EventTypeIds.Error, Messages); + } + public static void LogException(string Component, Exception ex) + { + if (ex.InnerException != null) + { + Log(EventTypeIds.ExceptionWithInner, Component, ex.GetType().Name, ex.Message, ex.StackTrace, ex.InnerException.GetType().Name, ex.InnerException.Message, ex.InnerException.StackTrace); + } + else + { + Log(EventTypeIds.Exception, Component, ex.GetType().Name, ex.Message, ex.StackTrace); + } + } + + public static void LogLogInitialized(string PersistantStorePath) + { + Log(EventTypeIds.LogInitialized, PersistantStorePath); + } + + public override int ModuleId + { + get { return _ModuleId; } + } + + public override string ModuleName + { + get { return "System"; } + } + + public override string ModuleDescription + { + get { return "Core System Log"; } + } + + protected override List LoadEventTypes() + { + List eventTypes = new List() { + new LogEventType() { + Id = (int)EventTypeIds.Information, + ModuleId = _ModuleId, + Name = "Information", + Format = null, + Severity = (int)LogEventType.Severities.Information, + UseLive = true, UsePersist = true, UseDisplay = true }, + new LogEventType() { + Id = (int)EventTypeIds.Warning, + ModuleId = _ModuleId, + Name = "Warning", + Format = null, + Severity = (int)LogEventType.Severities.Warning, + UseLive = true, UsePersist = true, UseDisplay = true }, + new LogEventType() { + Id = (int)EventTypeIds.Error, + ModuleId = _ModuleId, + Name = "Error", + Format = null, + Severity = (int)LogEventType.Severities.Error, + UseLive = true, UsePersist = true, UseDisplay = true }, + new LogEventType() { + Id = (int)EventTypeIds.Exception, + ModuleId = _ModuleId, + Name = "Exception", + Format = "{0}; {1}: {2}; {3}", + Severity = (int)LogEventType.Severities.Error, + UseLive = true, UsePersist = true, UseDisplay = true }, + new LogEventType() { + Id = (int)EventTypeIds.ExceptionWithInner, + ModuleId = _ModuleId, + Name = "Exception with Inner Exception", + Format = "{0}; {1}: {2}; {3}; {4}: {5}; {6}", + Severity = (int)LogEventType.Severities.Error, + UseLive = true, UsePersist = true, UseDisplay = true }, + new LogEventType() { + Id = (int)EventTypeIds.LogInitialized, + ModuleId = _ModuleId, + Name = "Log Initialized", + Format = "Log Initialized to '{0}'", + Severity = (int)LogEventType.Severities.Information, + UseLive = false, UsePersist = true, UseDisplay = true } + }; + + return eventTypes; + } + } +} diff --git a/Disco.Logging/Targets/LogLiveContext.cs b/Disco.Logging/Targets/LogLiveContext.cs new file mode 100644 index 00000000..1b0d8d98 --- /dev/null +++ b/Disco.Logging/Targets/LogLiveContext.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using SignalR; +using SignalR.Hosting.AspNet; +using SignalR.Infrastructure; + +namespace Disco.Logging.Targets +{ + public class LogLiveContext : PersistentConnection + { + protected override System.Threading.Tasks.Task OnReceivedAsync(string connectionId, string data) + { + // Add to Group + if (!string.IsNullOrWhiteSpace(data) && data.StartsWith("/addToGroups:") && data.Length > 13) + { + var groups = data.Substring(13).Split(','); + foreach (var g in groups) + { + this.AddToGroup(connectionId, g); + } + } + + return base.OnReceivedAsync(connectionId, data); + } + + internal static void Broadcast(LogBase logModule, Models.LogEventType eventType, DateTime Timestamp, params object[] Arguments) + { + var message = Models.LogLiveEvent.Create(logModule, eventType, Timestamp, Arguments); + + var connectionManager = AspNetHost.DependencyResolver.Resolve(); + var connection = connectionManager.GetConnection(); + connection.Broadcast(_QualifiedTypeNameAll, message); + connection.Broadcast(LiveLogNameGroup(logModule.ModuleName), message); + } + + private const string _GroupNameAll = "__All"; + private static string _QualifiedTypeName = typeof(LogLiveContext).FullName + "."; + private static string _QualifiedTypeNameAll = _QualifiedTypeName + "__All"; + private static string LiveLogNameGroup(string LogName) + { + return string.Concat(_QualifiedTypeName, LogName); + } + public static string LiveLogNameAll + { + get + { + //return _QualifiedTypeNameAll; + return _GroupNameAll; + } + } + + } +} diff --git a/Disco.Logging/Targets/LogPersistContext.cs b/Disco.Logging/Targets/LogPersistContext.cs new file mode 100644 index 00000000..ac335469 --- /dev/null +++ b/Disco.Logging/Targets/LogPersistContext.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Data.Entity; +using System.Data.Entity.Infrastructure; + +namespace Disco.Logging.Targets +{ + public class LogPersistContext : DbContext + { + public LogPersistContext(string ConnectionString) : base(ConnectionString) { } + + public DbSet Modules { get; set; } + public DbSet EventTypes { get; set; } + public DbSet Events { get; set; } + + protected override void OnModelCreating(DbModelBuilder modelBuilder) + { + //modelBuilder.Conventions.Remove(); + } + } +} diff --git a/Disco.Logging/Utilities.cs b/Disco.Logging/Utilities.cs new file mode 100644 index 00000000..249fbc03 --- /dev/null +++ b/Disco.Logging/Utilities.cs @@ -0,0 +1,263 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Win32.SafeHandles; +using System.Runtime.InteropServices; +using System.IO; +using System.Web.Mvc; + +namespace Disco.Logging +{ + public static class Utilities + { + + public const string LogEventCSVHeader = "Timestamp,ModuleId,ModuleName,ModuleDescription,EventTypeId,EventTypeName,Severity,Message"; + public static void ToCsvLine(this Models.LogLiveEvent e, TextWriter writer) + { + writer.Write(e.Timestamp.ToString("yyy-MM-dd HH:mm:ss")); + writer.Write(","); + writer.Write(e.ModuleId); + writer.Write(",\""); + writer.Write(e.ModuleName); + writer.Write("\",\""); + writer.Write(e.ModuleDescription); + writer.Write("\","); + writer.Write(e.EventTypeId); + writer.Write(",\""); + writer.Write(e.EventTypeName); + writer.Write("\","); + writer.Write(e.EventTypeSeverity); + writer.Write(",\""); + writer.Write(e.FormattedMessage.Replace("\"", "'")); + writer.Write("\""); + if (e.Arguments != null) + { + foreach (var arg in e.Arguments) + { + writer.Write(",\""); + writer.Write(arg.ToString().Replace("\"", "'")); + writer.Write("\""); + } + } + writer.WriteLine(); + } + public static MemoryStream ToCsv(this List e) + { + var ms = new MemoryStream(); + StreamWriter sw = new StreamWriter(ms); + sw.WriteLine(LogEventCSVHeader); + if (e != null) + { + foreach (var le in e) + { + le.ToCsvLine(sw); + } + } + sw.Flush(); + ms.Position = 0; + return ms; + } + + public static List ToSelectListItems(this List items) + { + return items.Select(et => new SelectListItem() { Value = et.Id.ToString(), Text = et.Name }).ToList(); + } + + #region Win32 APIs + /// + /// The CreateFile function creates or opens a file, file stream, directory, physical disk, volume, console buffer, tape drive, + /// communications resource, mailslot, or named pipe. The function returns a handle that can be used to access an object. + /// + /// + /// access to the object, which can be read, write, or both + /// The sharing mode of an object, which can be read, write, both, or none + /// A pointer to a SECURITY_ATTRIBUTES structure that determines whether or not the returned handle can + /// be inherited by child processes. Can be null + /// An action to take on files that exist and do not exist + /// The file attributes and flags. + /// A handle to a template file with the GENERIC_READ access right. The template file supplies file attributes + /// and extended attributes for the file that is being created. This parameter can be null + /// If the function succeeds, the return value is an open handle to a specified file. If a specified file exists before the function + /// all and dwCreationDisposition is CREATE_ALWAYS or OPEN_ALWAYS, a call to GetLastError returns ERROR_ALREADY_EXISTS, even when the function + /// succeeds. If a file does not exist before the call, GetLastError returns 0 (zero). + /// If the function fails, the return value is INVALID_HANDLE_VALUE. To get extended error information, call GetLastError. + /// + [DllImport("kernel32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall, SetLastError = true)] + private static extern SafeFileHandle CreateFile( + string lpFileName, + EFileAccess dwDesiredAccess, + EFileShare dwShareMode, + IntPtr SecurityAttributes, + ECreationDisposition dwCreationDisposition, + EFileAttributes dwFlagsAndAttributes, + IntPtr hTemplateFile + ); + [Flags] + private enum EFileAccess : uint + { + Delete = 0x10000, + ReadControl = 0x20000, + WriteDAC = 0x40000, + WriteOwner = 0x80000, + Synchronize = 0x100000, + + StandardRightsRequired = 0xF0000, + StandardRightsRead = ReadControl, + StandardRightsWrite = ReadControl, + StandardRightsExecute = ReadControl, + StandardRightsAll = 0x1F0000, + SpecificRightsAll = 0xFFFF, + + AccessSystemSecurity = 0x1000000, // AccessSystemAcl access type + + MaximumAllowed = 0x2000000, // MaximumAllowed access type + + GenericRead = 0x80000000, + GenericWrite = 0x40000000, + GenericExecute = 0x20000000, + GenericAll = 0x10000000 + } + [Flags] + private enum EFileShare : uint + { + /// + /// + /// + None = 0x00000000, + /// + /// Enables subsequent open operations on an object to request read access. + /// Otherwise, other processes cannot open the object if they request read access. + /// If this flag is not specified, but the object has been opened for read access, the function fails. + /// + Read = 0x00000001, + /// + /// Enables subsequent open operations on an object to request write access. + /// Otherwise, other processes cannot open the object if they request write access. + /// If this flag is not specified, but the object has been opened for write access, the function fails. + /// + Write = 0x00000002, + /// + /// Enables subsequent open operations on an object to request delete access. + /// Otherwise, other processes cannot open the object if they request delete access. + /// If this flag is not specified, but the object has been opened for delete access, the function fails. + /// + Delete = 0x00000004 + } + private enum ECreationDisposition : uint + { + /// + /// Creates a new file. The function fails if a specified file exists. + /// + New = 1, + /// + /// Creates a new file, always. + /// If a file exists, the function overwrites the file, clears the existing attributes, combines the specified file attributes, + /// and flags with FILE_ATTRIBUTE_ARCHIVE, but does not set the security descriptor that the SECURITY_ATTRIBUTES structure specifies. + /// + CreateAlways = 2, + /// + /// Opens a file. The function fails if the file does not exist. + /// + OpenExisting = 3, + /// + /// Opens a file, always. + /// If a file does not exist, the function creates a file as if dwCreationDisposition is CREATE_NEW. + /// + OpenAlways = 4, + /// + /// Opens a file and truncates it so that its size is 0 (zero) bytes. The function fails if the file does not exist. + /// The calling process must open the file with the GENERIC_WRITE access right. + /// + TruncateExisting = 5 + } + [Flags] + private enum EFileAttributes : uint + { + None = 0x0000000, + Readonly = 0x00000001, + Hidden = 0x00000002, + System = 0x00000004, + Directory = 0x00000010, + Archive = 0x00000020, + Device = 0x00000040, + Normal = 0x00000080, + Temporary = 0x00000100, + SparseFile = 0x00000200, + ReparsePoint = 0x00000400, + Compressed = 0x00000800, + Offline = 0x00001000, + NotContentIndexed = 0x00002000, + Encrypted = 0x00004000, + Write_Through = 0x80000000, + Overlapped = 0x40000000, + NoBuffering = 0x20000000, + RandomAccess = 0x10000000, + SequentialScan = 0x08000000, + DeleteOnClose = 0x04000000, + BackupSemantics = 0x02000000, + PosixSemantics = 0x01000000, + OpenReparsePoint = 0x00200000, + OpenNoRecall = 0x00100000, + FirstPipeInstance = 0x00080000 + } + + private const int FSCTL_SET_COMPRESSION = 0x9C040; + private const short COMPRESSION_FORMAT_DEFAULT = 1; + [DllImport("kernel32.dll", SetLastError = true)] + private static extern int DeviceIoControl( + SafeFileHandle hDevice, + int dwIoControlCode, + ref short lpInBuffer, + int nInBufferSize, + IntPtr lpOutBuffer, + int nOutBufferSize, + ref int lpBytesReturned, + IntPtr lpOverlapped); + #endregion + + public static void CompressDirectory(string DirectoryPath) + { + if (DirectoryPath.Length > 250) + throw new InvalidOperationException(string.Format("Directory Path to Long (>250) to Compress: {0}", DirectoryPath)); + + DirectoryInfo dirInfo = new DirectoryInfo(DirectoryPath); + if (dirInfo.Exists) + { + if ((dirInfo.Attributes & FileAttributes.Compressed) != FileAttributes.Compressed) + { + var dirHandle = CreateFile(DirectoryPath, EFileAccess.GenericWrite, EFileShare.Read, IntPtr.Zero, ECreationDisposition.OpenExisting, EFileAttributes.None, IntPtr.Zero); + if (dirHandle.IsInvalid) + { + Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); + } + else + { + EnableCompression(dirHandle); + dirHandle.Close(); + } + } + } + else + { + throw new InvalidOperationException(string.Format("Directory doesn't exist: {0}", DirectoryPath)); + } + } + + private static void EnableCompression(SafeFileHandle handle) + { + int lpBytesReturned = 0; + short lpInBuffer = COMPRESSION_FORMAT_DEFAULT; + + int result = DeviceIoControl(handle, FSCTL_SET_COMPRESSION, + ref lpInBuffer, sizeof(short), IntPtr.Zero, 0, + ref lpBytesReturned, IntPtr.Zero); + + if (result != 0) + { + Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error()); + } + } + + } +} diff --git a/Disco.Logging/packages.config b/Disco.Logging/packages.config new file mode 100644 index 00000000..2ba417d6 --- /dev/null +++ b/Disco.Logging/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/Disco.Models/App.config b/Disco.Models/App.config new file mode 100644 index 00000000..cbfc1faf --- /dev/null +++ b/Disco.Models/App.config @@ -0,0 +1,17 @@ + + + + +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/Disco.Models/BI/Config/OrganisationAddress.cs b/Disco.Models/BI/Config/OrganisationAddress.cs new file mode 100644 index 00000000..ab83ba5f --- /dev/null +++ b/Disco.Models/BI/Config/OrganisationAddress.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel.DataAnnotations; + +namespace Disco.Models.BI.Config +{ + public class OrganisationAddress + { + + public int? Id { get; set; } + [Required] + public string Name { get; set; } + [Required] + public string Address { get; set; } + [Required] + public string Suburb { get; set; } + [Required] + public string Postcode { get; set; } + [Required] + public string State { get; set; } + [Required] + public string Country { get; set; } + [Required] + public string ShortName { get; set; } + + // Added 2012-12-11 G# + // http://discoict.com.au/forum/support/2012/12/address-details.aspx + public string PhoneNumber { get; set; } + public string FaxNumber { get; set; } + // End Added 2012-12-11 G# + + public string ToConfigurationEntry() + { + StringBuilder entryBuilder = new StringBuilder(); + + entryBuilder.AppendLine(Name.Trim()); + entryBuilder.AppendLine(Address.Trim()); + entryBuilder.AppendLine(Suburb.Trim()); + entryBuilder.AppendLine(Postcode.Trim()); + entryBuilder.AppendLine(State.Trim()); + entryBuilder.AppendLine(Country.Trim()); + + if (!string.IsNullOrEmpty(ShortName)) + { + entryBuilder.AppendLine(ShortName.Trim()); + } + + return entryBuilder.ToString(); + } + + public static OrganisationAddress FromConfigurationEntry(int Id, string Entry) + { + string[] entryLines = Entry.Split(new string[] { Environment.NewLine }, StringSplitOptions.None); + if (entryLines.Length >= 6) + { + return new OrganisationAddress() + { + Id = Id, + Name = entryLines[0].Trim(), + Address = entryLines[1].Trim(), + Suburb = entryLines[2].Trim(), + Postcode = entryLines[3].Trim(), + State = entryLines[4].Trim(), + Country = entryLines[5].Trim(), + ShortName = (entryLines.Length > 6 ? entryLines[6].Trim() : string.Empty) + }; + } + throw new ArgumentException("Invalid Configuration Address Entry", "entry"); + } + + } +} diff --git a/Disco.Models/BI/DocumentTemplate/DocumentState.cs b/Disco.Models/BI/DocumentTemplate/DocumentState.cs new file mode 100644 index 00000000..c1554925 --- /dev/null +++ b/Disco.Models/BI/DocumentTemplate/DocumentState.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Collections; + +namespace Disco.Models.BI.DocumentTemplates +{ + public class DocumentState : IDisposable + { + public int SequenceNumber { get; set; } + public Hashtable FieldCache { get; set; } + public Hashtable ScopeCache { get; set; } + public Hashtable DocumentCache { get; set; } + + public DocumentState(int SequenceNumber) + { + this.SequenceNumber = SequenceNumber; + this.FieldCache = new Hashtable(); + this.ScopeCache = new Hashtable(); + this.DocumentCache = new Hashtable(); + } + + public void FlushFieldCache() + { + FlushDictionary(this.FieldCache); + } + public void FlushScopeCache() + { + FlushFieldCache(); + FlushDictionary(this.ScopeCache); + } + public void FlushDocumentCache() + { + FlushScopeCache(); + FlushDictionary(this.DocumentCache); + } + private static void FlushDictionary(Hashtable d) + { + foreach (var key in d.Keys) + { + var disposeItem = d[key] as IDisposable; + if (disposeItem != null) + disposeItem.Dispose(); + } + d.Clear(); + } + + public static DocumentState DefaultState() + { + return new DocumentState(1); + } + + public void Dispose() + { + FlushDocumentCache(); + } + } +} diff --git a/Disco.Models/BI/Expressions/IImageExpressionResult.cs b/Disco.Models/BI/Expressions/IImageExpressionResult.cs new file mode 100644 index 00000000..d5ae6970 --- /dev/null +++ b/Disco.Models/BI/Expressions/IImageExpressionResult.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; + +namespace Disco.Models.BI.Expressions +{ + public interface IImageExpressionResult + { + Stream GetImage(int Width, int Height); + byte Quality { get; set; } + bool LosslessFormat { get; set; } + bool ShowField { get; set; } + string BackgroundColour { get; set; } + bool BackgroundPreferTransparent { get; set; } + } +} diff --git a/Disco.Models/BI/Interop/Community/UpdateRequestBase.cs b/Disco.Models/BI/Interop/Community/UpdateRequestBase.cs new file mode 100644 index 00000000..33bcba22 --- /dev/null +++ b/Disco.Models/BI/Interop/Community/UpdateRequestBase.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using System.Xml.Linq; + +namespace Disco.Models.BI.Interop.Community +{ + public class UpdateRequestBase + { + public virtual int RequestVersion { get; set; } + } +} diff --git a/Disco.Models/BI/Interop/Community/UpdateRequestV1.cs b/Disco.Models/BI/Interop/Community/UpdateRequestV1.cs new file mode 100644 index 00000000..9c910310 --- /dev/null +++ b/Disco.Models/BI/Interop/Community/UpdateRequestV1.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; + +namespace Disco.Models.BI.Interop.Community +{ + public class UpdateRequestV1 : UpdateRequestBase + { + public UpdateRequestV1() + { + this.RequestVersion = 1; + } + + public string DeploymentId { get; set; } + public string CurrentDiscoVersion { get; set; } + public bool BetaDeployment { get; set; } + + public string OrganisationName { get; set; } + public string BroadbandDoeWanId { get; set; } + + public List Stat_JobCounts { get; set; } + public List Stat_OpenJobCounts { get; set; } + public List Stat_ActiveDeviceModelCounts { get; set; } + public List Stat_UserCounts { get; set; } + + public class Stat + { + public string Key { get; set; } + public int Count { get; set; } + } + + } +} diff --git a/Disco.Models/BI/Interop/Community/UpdateResponse.cs b/Disco.Models/BI/Interop/Community/UpdateResponse.cs new file mode 100644 index 00000000..15693040 --- /dev/null +++ b/Disco.Models/BI/Interop/Community/UpdateResponse.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Disco.Models.BI.Interop.Community +{ + public class UpdateResponse + { + public string Version { get; set; } + public DateTime VersionReleasedTimestamp { get; set; } + public string Blurb { get; set; } + public string UrlLink { get; set; } + public DateTime ResponseTimestamp { get; set; } + public bool BetaRelease { get; set; } + + public bool IsUpdatable(Version TestVersion) + { + var updateVersion = System.Version.Parse(this.Version); + return (updateVersion > TestVersion); + } + } +} diff --git a/Disco.Models/BI/Job/JobTableModel.cs b/Disco.Models/BI/Job/JobTableModel.cs new file mode 100644 index 00000000..e0bb8bbe --- /dev/null +++ b/Disco.Models/BI/Job/JobTableModel.cs @@ -0,0 +1,74 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Disco.Models.BI.Job +{ + public class JobTableModel + { + public bool ShowId { get; set; } + public bool? ShowDeviceAddress { get; set; } + public bool ShowDates { get; set; } + public bool ShowType { get; set; } + public bool ShowDevice { get; set; } + public bool ShowUser { get; set; } + public bool ShowTechnician { get; set; } + public bool ShowLocation { get; set; } + public bool ShowStatus { get; set; } + public bool IsSmallTable { get; set; } + public bool HideClosedJobs { get; set; } + public List Items { get; set; } + + public JobTableModel() + { + ShowId = true; + ShowDates = true; + ShowType = true; + ShowDevice = true; + ShowUser = true; + ShowTechnician = true; + } + + public class JobTableItemModel + { + public int Id { get; set; } + public int? DeviceAddressId { get; set; } + public string DeviceAddress { get; set; } + public DateTime OpenedDate { get; set; } + public DateTime? ClosedDate { get; set; } + public string TypeId { get; set; } + public string TypeDescription { get; set; } + public string DeviceSerialNumber { get; set; } + public string DeviceModelDescription { get; set; } + public string UserId { get; set; } + public string UserDisplayName { get; set; } + public string OpenedTechUserId { get; set; } + public string OpenedTechUserDisplayName { get; set; } + public string StatusDescription { get; set; } + public string StatusId { get; set; } + public string Location { get; set; } + } + + public class JobTableItemModelIncludeStatus : JobTableItemModel + { + public string JobMetaWarranty_ExternalReference { get; set; } + public DateTime? JobMetaWarranty_ExternalCompletedDate { get; set; } + + public DateTime? JobMetaNonWarranty_RepairerLoggedDate { get; set; } + public DateTime? JobMetaNonWarranty_RepairerCompletedDate { get; set; } + public DateTime? JobMetaNonWarranty_AccountingChargeAddedDate { get; set; } + public DateTime? JobMetaNonWarranty_AccountingChargePaidDate { get; set; } + public DateTime? JobMetaNonWarranty_AccountingChargeRequiredDate { get; set; } + public bool? JobMetaNonWarranty_IsInsuranceClaim { get; set; } + public DateTime? JobMetaInsurance_ClaimFormSentDate { get; set; } + + public DateTime? WaitingForUserAction { get; set; } + public DateTime? DeviceReadyForReturn { get; set; } + public DateTime? DeviceHeld { get; set; } + public DateTime? DeviceReturnedDate { get; set; } + public string JobMetaWarranty_ExternalName { get; set; } + public string JobMetaNonWarranty_RepairerName { get; set; } + } + } +} diff --git a/Disco.Models/BI/Job/Statistics/DailyOpenedClosedItem.cs b/Disco.Models/BI/Job/Statistics/DailyOpenedClosedItem.cs new file mode 100644 index 00000000..cdc561a4 --- /dev/null +++ b/Disco.Models/BI/Job/Statistics/DailyOpenedClosedItem.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Disco.Models.BI.Job.Statistics +{ + public class DailyOpenedClosedItem + { + public DateTime Timestamp { get; set; } + public int TotalJobs { get; set; } + public int OpenedJobs { get; set; } + public int ClosedJobs { get; set; } + } +} diff --git a/Disco.Models/BI/Search/DeviceSearchResultItem.cs b/Disco.Models/BI/Search/DeviceSearchResultItem.cs new file mode 100644 index 00000000..283b6a1d --- /dev/null +++ b/Disco.Models/BI/Search/DeviceSearchResultItem.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Disco.Models.BI.Search +{ + public class DeviceSearchResultItem + { + public string AssetNumber { get; set; } + public string AssignedUserDescription + { + get + { + if (AssignedUserId != null) + { + if (AssignedUserDisplayName != null) + return string.Format("{0} ({1})", AssignedUserDisplayName, AssignedUserId); + else + return AssignedUserId; + } + return string.Empty; + } + } + public string AssignedUserDisplayName { get; set; } + public string AssignedUserId { get; set; } + public string ComputerName { get; set; } + public string DeviceModelDescription { get; set; } + public string DeviceProfileDescription { get; set; } + public int JobCount { get; set; } + public DateTime? DecommissionedDate { get; set; } + public string SerialNumber { get; set; } + } +} diff --git a/Disco.Models/BI/Search/UserSearchResultItem.cs b/Disco.Models/BI/Search/UserSearchResultItem.cs new file mode 100644 index 00000000..40b8c9a8 --- /dev/null +++ b/Disco.Models/BI/Search/UserSearchResultItem.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Disco.Models.BI.Search +{ + public class UserSearchResultItem + { + public int AssignedDevicesCount { get; set; } + public string DisplayName { get; set; } + public string GivenName { get; set; } + public string Id { get; set; } + public int JobCount { get; set; } + public string Surname { get; set; } + } +} diff --git a/Disco.Models/ClientServices/Enrol.cs b/Disco.Models/ClientServices/Enrol.cs new file mode 100644 index 00000000..471ada30 --- /dev/null +++ b/Disco.Models/ClientServices/Enrol.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Disco.Models.ClientServices +{ + public class Enrol : ServiceBase + { + public override string Feature + { + get { return "Enrol"; } + } + + public string DeviceSerialNumber { get; set; } + public string DeviceUUID { get; set; } + + public string DeviceComputerName { get; set; } + public bool DeviceIsPartOfDomain { get; set; } + + public string DeviceManufacturer { get; set; } + public string DeviceModel { get; set; } + public string DeviceModelType { get; set; } + + public string DeviceLanMacAddress { get; set; } + + public string DeviceWlanMacAddress { get; set; } + + public List DeviceCertificates { get; set; } + } +} diff --git a/Disco.Models/ClientServices/EnrolResponse.cs b/Disco.Models/ClientServices/EnrolResponse.cs new file mode 100644 index 00000000..3741ce82 --- /dev/null +++ b/Disco.Models/ClientServices/EnrolResponse.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Disco.Models.ClientServices +{ + public class EnrolResponse + { + public string SessionId { get; set; } + + public string DeviceDomainName { get; set; } + public string DeviceComputerName { get; set; } + + public string DeviceAssignedUserDomain { get; set; } + public string DeviceAssignedUserName { get; set; } + public string DeviceAssignedUserSID { get; set; } + public string DeviceAssignedUserUsername { get; set; } + + public string OfflineDomainJoin { get; set; } + + public string DeviceCertificate { get; set; } + public List DeviceCertificateRemoveExisting { get; set; } + + // Actions + public bool AllowBootstrapperUninstall { get; set; } + public bool RequireReboot { get; set; } + + public string ErrorMessage { get; set; } + } +} diff --git a/Disco.Models/ClientServices/MacEnrol.cs b/Disco.Models/ClientServices/MacEnrol.cs new file mode 100644 index 00000000..ea63f53f --- /dev/null +++ b/Disco.Models/ClientServices/MacEnrol.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Disco.Models.ClientServices +{ + public class MacEnrol : ServiceBase + { + public override string Feature + { + get { return "MacEnrol"; } + } + + public string DeviceSerialNumber { get; set; } + public string DeviceUUID { get; set; } + + public string DeviceComputerName { get; set; } + + public string DeviceManufacturer { get; set; } + public string DeviceModel { get; set; } + public string DeviceModelType { get; set; } + + public string DeviceLanMacAddress { get; set; } + + public string DeviceWlanMacAddress { get; set; } + } +} diff --git a/Disco.Models/ClientServices/MacEnrolResponse.cs b/Disco.Models/ClientServices/MacEnrolResponse.cs new file mode 100644 index 00000000..3dd63e0a --- /dev/null +++ b/Disco.Models/ClientServices/MacEnrolResponse.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Disco.Models.ClientServices +{ + public class MacEnrolResponse + { + public string DeviceComputerName { get; set; } + + public string DeviceAssignedUserDomain { get; set; } + public string DeviceAssignedUserName { get; set; } + public string DeviceAssignedUserSID { get; set; } + public string DeviceAssignedUserUsername { get; set; } + + public string ErrorMessage { get; set; } + } +} diff --git a/Disco.Models/ClientServices/MacSecureEnrolResponse.cs b/Disco.Models/ClientServices/MacSecureEnrolResponse.cs new file mode 100644 index 00000000..c7c1cf0a --- /dev/null +++ b/Disco.Models/ClientServices/MacSecureEnrolResponse.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Disco.Models.ClientServices +{ + public class MacSecureEnrolResponse + { + public string DeviceComputerName { get; set; } + + public string DeviceAssignedUserDomain { get; set; } + public string DeviceAssignedUserName { get; set; } + public string DeviceAssignedUserSID { get; set; } + public string DeviceAssignedUserUsername { get; set; } + + public string ErrorMessage { get; set; } + + public static MacSecureEnrolResponse FromMacEnrolResponse(MacEnrolResponse mer) + { + return new MacSecureEnrolResponse + { + DeviceComputerName = mer.DeviceComputerName, + DeviceAssignedUserDomain = mer.DeviceAssignedUserDomain, + DeviceAssignedUserName = mer.DeviceAssignedUserName, + DeviceAssignedUserSID = mer.DeviceAssignedUserSID, + DeviceAssignedUserUsername = mer.DeviceAssignedUserUsername + }; + } + + } +} diff --git a/Disco.Models/ClientServices/ServiceBase.cs b/Disco.Models/ClientServices/ServiceBase.cs new file mode 100644 index 00000000..428e4ad3 --- /dev/null +++ b/Disco.Models/ClientServices/ServiceBase.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Disco.Models.ClientServices +{ + public abstract class ServiceBase + { + internal ServiceBase() + { + } + + public abstract string Feature { get; } + } +} diff --git a/Disco.Models/ClientServices/WhoAmI.cs b/Disco.Models/ClientServices/WhoAmI.cs new file mode 100644 index 00000000..403e30e8 --- /dev/null +++ b/Disco.Models/ClientServices/WhoAmI.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Disco.Models.ClientServices +{ + public class WhoAmI : ServiceBase + { + + public override string Feature + { + get { return "WhoAmI"; } + } + } +} diff --git a/Disco.Models/ClientServices/WhoAmIResponse.cs b/Disco.Models/ClientServices/WhoAmIResponse.cs new file mode 100644 index 00000000..0fb18b85 --- /dev/null +++ b/Disco.Models/ClientServices/WhoAmIResponse.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Disco.Models.ClientServices +{ + public class WhoAmIResponse + { + public string DisplayName { get; set; } + public string Type { get; set; } + public string Username { get; set; } + + public override string ToString() + { + return string.Format("{0} ({1})", DisplayName, Username); + } + } +} diff --git a/Disco.Models/Disco.Models.csproj b/Disco.Models/Disco.Models.csproj new file mode 100644 index 00000000..f7b59d36 --- /dev/null +++ b/Disco.Models/Disco.Models.csproj @@ -0,0 +1,114 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {FBC05512-FCCA-4B16-9E76-8C413C5DE6C9} + Library + Properties + Disco.Models + Disco.Models + v4.5 + 512 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Disco.Models/Interop/ActiveDirectory/ActiveDirectoryMachineAccount.cs b/Disco.Models/Interop/ActiveDirectory/ActiveDirectoryMachineAccount.cs new file mode 100644 index 00000000..00eb8169 --- /dev/null +++ b/Disco.Models/Interop/ActiveDirectory/ActiveDirectoryMachineAccount.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Models.Repository; + +namespace Disco.Models.Interop.ActiveDirectory +{ + public class ActiveDirectoryMachineAccount + { + public string DistinguishedName { get; set; } + public string DnsName { get; set; } + public string Domain { get; set; } + public string Name { get; set; } + public Guid NetbootGUID { get; set; } + public string ObjectSid { get; set; } + public string Path { get; set; } + public string sAMAccountName { get; set; } + public bool IsCriticalSystemObject { get; set; } + public Dictionary LoadedProperties { get; set; } + + public string ParentDistinguishedName + { + get + { + // Determine Parent + if (!string.IsNullOrWhiteSpace(DistinguishedName)) + return DistinguishedName.Substring(0, DistinguishedName.IndexOf(",DC=")).Substring(DistinguishedName.IndexOf(",") + 1); + else + return null; + } + } + + public User ToRepositoryUser() + { + return new User + { + Id = this.sAMAccountName, + Type = "Computer", + DisplayName = this.Name + }; + } + + } +} diff --git a/Disco.Models/Interop/ActiveDirectory/ActiveDirectoryUserAccount.cs b/Disco.Models/Interop/ActiveDirectory/ActiveDirectoryUserAccount.cs new file mode 100644 index 00000000..6cfb1822 --- /dev/null +++ b/Disco.Models/Interop/ActiveDirectory/ActiveDirectoryUserAccount.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Models.Repository; + +namespace Disco.Models.Interop.ActiveDirectory +{ + public class ActiveDirectoryUserAccount + { + public string DisplayName { get; set; } + public string DistinguishedName { get; set; } + public string Domain { get; set; } + public string Email { get; set; } + public string GivenName { get; set; } + public List Groups { get; set; } + public string Name { get; set; } + public string ObjectSid { get; set; } + public string Path { get; set; } + public string Phone { get; set; } + public string sAMAccountName { get; set; } + public string Surname { get; set; } + public string Type { get; set; } + public Dictionary LoadedProperties { get; set; } + + public User ToRepositoryUser() + { + return new User + { + Id = this.sAMAccountName, + DisplayName = this.DisplayName, + Surname = this.Surname, + GivenName = this.GivenName, + EmailAddress = this.Email, + PhoneNumber = this.Phone, + Type = this.Type + }; + } + + } +} diff --git a/Disco.Models/Properties/AssemblyInfo.cs b/Disco.Models/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..c9f40db8 --- /dev/null +++ b/Disco.Models/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("Disco - Data Models")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Disco")] +[assembly: AssemblyCopyright("")] +[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("44f11caf-e8d3-4c5a-9406-d84779e6d427")] + +// 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.2.0131.2002")] +[assembly: AssemblyFileVersion("1.2.0131.2002")] diff --git a/Disco.Models/Repository/ConfigurationItem.cs b/Disco.Models/Repository/ConfigurationItem.cs new file mode 100644 index 00000000..4e6dc9a7 --- /dev/null +++ b/Disco.Models/Repository/ConfigurationItem.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Disco.Models.Repository +{ + [Table("Configuration")] + public class ConfigurationItem + { + [StringLength(80), Column(Order = 1), Key] + public string Key { get; set; } + [Column(Order = 0), StringLength(80), Key] + public string Scope { get; set; } + public string Value { get; set; } + } +} diff --git a/Disco.Models/Repository/Device/Device.cs b/Disco.Models/Repository/Device/Device.cs new file mode 100644 index 00000000..e34e547d --- /dev/null +++ b/Disco.Models/Repository/Device/Device.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Disco.Models.Repository +{ + public class Device + { + [Required(ErrorMessage="The Serial Number is Required"), Key, StringLength(60)] + public string SerialNumber { get; set; } + + [StringLength(40)] + public string AssetNumber { get; set; } + [StringLength(250)] + public string Location { get; set; } + + public int? DeviceModelId { get; set; } + [Range(1, int.MaxValue, ErrorMessage="A valid Device Profile is Required")] + public int DeviceProfileId { get; set; } + public int? DeviceBatchId { get; set; } + + [StringLength(24)] + public string ComputerName { get; set; } + public string AssignedUserId { get; set; } + public DateTime? LastNetworkLogonDate { get; set; } + + // 2012-06-21 - Removed + //[StringLength(24)] + //public string CertificateStoreReference { get; set; } + + public bool AllowUnauthenticatedEnrol { get; set; } + + public bool Active { get; set; } + + public DateTime CreatedDate { get; set; } + public DateTime? EnrolledDate { get; set; } + public DateTime? LastEnrolDate { get; set; } + public DateTime? DecommissionedDate { get; set; } + + [ForeignKey("DeviceModelId")] + public virtual DeviceModel DeviceModel { get; set; } + [ForeignKey("DeviceProfileId")] + public virtual DeviceProfile DeviceProfile { get; set; } + [ForeignKey("DeviceBatchId")] + public virtual DeviceBatch DeviceBatch { get; set; } + [ForeignKey("AssignedUserId")] + public virtual User AssignedUser { get; set; } + + public virtual IList DeviceUserAssignments { get; set; } + public virtual IList DeviceDetails { get; set; } + public virtual IList DeviceAttachments { get; set; } + + [InverseProperty("DeviceSerialNumber")] + public virtual IList Jobs { get; set; } + + public override string ToString() + { + if (DeviceModel != null) + return string.Format("{0} - {1}", this.DeviceModel, this.SerialNumber); + else + return this.SerialNumber; + } + } +} diff --git a/Disco.Models/Repository/Device/DeviceAttachment.cs b/Disco.Models/Repository/Device/DeviceAttachment.cs new file mode 100644 index 00000000..595e81b2 --- /dev/null +++ b/Disco.Models/Repository/Device/DeviceAttachment.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Disco.Models.Repository +{ + public class DeviceAttachment + { + [Key] + public int Id { get; set; } + public string DeviceSerialNumber { get; set; } + [Required] + public string TechUserId { get; set; } + [StringLength(500), Required] + public string Filename { get; set; } + [Required, StringLength(500)] + public string MimeType { get; set; } + public DateTime Timestamp { get; set; } + [Required, StringLength(500)] + public string Comments { get; set; } + + public string DocumentTemplateId { get; set; } + + [InverseProperty("DeviceAttachments"), ForeignKey("DeviceSerialNumber")] + public virtual Device Device { get; set; } + + [ForeignKey("TechUserId")] + public virtual User TechUser { get; set; } + + [ForeignKey("DocumentTemplateId")] + public virtual DocumentTemplate DocumentTemplate { get; set; } + + } +} diff --git a/Disco.Models/Repository/Device/DeviceBatch.cs b/Disco.Models/Repository/Device/DeviceBatch.cs new file mode 100644 index 00000000..72c40cc1 --- /dev/null +++ b/Disco.Models/Repository/Device/DeviceBatch.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Disco.Models.Repository +{ + public class DeviceBatch + { + [Key] + public int Id { get; set; } + [StringLength(500)] + public string Name { get; set; } + + [Required, DisplayFormat(ApplyFormatInEditMode = true, ConvertEmptyStringToNull = true, DataFormatString = "{0:yyyy/MM/dd}", HtmlEncode = false)] + public DateTime PurchaseDate { get; set; } + [StringLength(200)] + public string Supplier { get; set; } + [DataType(DataType.MultilineText), StringLength(500)] + public string PurchaseDetails { get; set; } + + [DisplayFormat(ApplyFormatInEditMode = true, ConvertEmptyStringToNull = true, DataFormatString = "{0:c}")] + public decimal? UnitCost { get; set; } + public int? UnitQuantity { get; set; } + + public int? DefaultDeviceModelId { get; set; } + + [DisplayFormat(ApplyFormatInEditMode = true, ConvertEmptyStringToNull = true, DataFormatString = "{0:yyyy/MM/dd}", HtmlEncode = false)] + public DateTime? WarrantyValidUntil { get; set; } + [DataType(DataType.MultilineText)] + public string WarrantyDetails { get; set; } + + [DisplayFormat(ApplyFormatInEditMode = true, ConvertEmptyStringToNull = true, DataFormatString = "{0:yyyy/MM/dd}", HtmlEncode = false)] + public DateTime? InsuredDate { get; set; } + [StringLength(200)] + public string InsuranceSupplier { get; set; } + [DisplayFormat(ApplyFormatInEditMode = true, ConvertEmptyStringToNull = true, DataFormatString = "{0:yyyy/MM/dd}", HtmlEncode = false)] + public DateTime? InsuredUntil { get; set; } + [DataType(DataType.MultilineText)] + public string InsuranceDetails { get; set; } + + [DataType(DataType.MultilineText)] + public string Comments { get; set; } + + [ForeignKey("DefaultDeviceModelId")] + public virtual DeviceModel DefaultDeviceModel { get; set; } + + public virtual IList Devices { get; set; } + + public override string ToString() + { + if (string.IsNullOrWhiteSpace(this.Name)) + { + return string.Format("{0}: {1}", this.Id, this.PurchaseDate.ToLongDateString()); + } + return this.Name; + } + } +} diff --git a/Disco.Models/Repository/Device/DeviceCertificate.cs b/Disco.Models/Repository/Device/DeviceCertificate.cs new file mode 100644 index 00000000..1b8cfd3e --- /dev/null +++ b/Disco.Models/Repository/Device/DeviceCertificate.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Disco.Models.Repository +{ + public class DeviceCertificate + { + [Key] + public int Id { get; set; } + + [Required, StringLength(64)] + public string ProviderId { get; set; } + public int ProviderIndex { get; set; } + + [StringLength(28)] + public string Name { get; set; } + [MaxLength(16384)] + public byte[] Content { get; set; } + public bool Enabled { get; set; } + + // Added 2011-10-24 G# + public DateTime? ExpirationDate { get; set; } + // Added 2011-10-24 G# + public DateTime? AllocatedDate { get; set; } + + public string DeviceSerialNumber { get; set; } + + [ForeignKey("DeviceSerialNumber")] + public virtual Device Device { get; set; } + } +} diff --git a/Disco.Models/Repository/Device/DeviceComponent.cs b/Disco.Models/Repository/Device/DeviceComponent.cs new file mode 100644 index 00000000..6fe2e617 --- /dev/null +++ b/Disco.Models/Repository/Device/DeviceComponent.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Disco.Models.Repository +{ + public class DeviceComponent + { + [Key] + public int Id { get; set; } + public int? DeviceModelId { get; set; } + [StringLength(100)] + public string Description { get; set; } + public decimal Cost { get; set; } + + [ForeignKey("DeviceModelId")] + public virtual DeviceModel DeviceModel { get; set; } + + public virtual IList JobSubTypes { get; set; } + } +} diff --git a/Disco.Models/Repository/Device/DeviceDetail.cs b/Disco.Models/Repository/Device/DeviceDetail.cs new file mode 100644 index 00000000..696a2168 --- /dev/null +++ b/Disco.Models/Repository/Device/DeviceDetail.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Disco.Models.Repository +{ + public class DeviceDetail + { + [Column(Order = 0), Key] + public string DeviceSerialNumber { get; set; } + + [Key, StringLength(100), Column(Order = 2)] + public string Key { get; set; } + + [Column(Order = 1), StringLength(100), Key] + public string Scope { get; set; } + + public string Value { get; set; } + + [ForeignKey("DeviceSerialNumber")] + public virtual Device Device { get; set; } + } +} diff --git a/Disco.Models/Repository/Device/DeviceModel.cs b/Disco.Models/Repository/Device/DeviceModel.cs new file mode 100644 index 00000000..30021a59 --- /dev/null +++ b/Disco.Models/Repository/Device/DeviceModel.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel.DataAnnotations; + +namespace Disco.Models.Repository +{ + public class DeviceModel + { + [Key] + public int Id { get; set; } + [StringLength(500)] + public string Description { get; set; } + + [StringLength(200)] + public string Manufacturer { get; set; } + [StringLength(200)] + public string Model { get; set; } + [StringLength(40)] + public string ModelType { get; set; } + + // Remove Reliance On! + // Removed: 2013-01-14 G# + //[Obsolete("Image to be removed from the Database")] + //public byte[] Image { get; set; } + + [DisplayFormat(ApplyFormatInEditMode = true, ConvertEmptyStringToNull = true, DataFormatString = "{0:yyyy/MM/dd}", HtmlEncode = false)] + public DateTime? DefaultPurchaseDate { get; set; } + [DisplayFormat(ApplyFormatInEditMode = true, ConvertEmptyStringToNull = true, DataFormatString = "{0:c}")] + public decimal? DeviceCost { get; set; } + + [StringLength(40)] + public string DefaultWarrantyProvider { get; set; } + + public virtual IList DeviceComponents { get; set; } + + public virtual IList Devices { get; set; } + + public override string ToString() + { + if (string.IsNullOrWhiteSpace(this.Description)) + { + return string.Format("{0} {1}", this.Manufacturer, this.Model); + } + return this.Description; + } + } +} diff --git a/Disco.Models/Repository/Device/DeviceProfile.cs b/Disco.Models/Repository/Device/DeviceProfile.cs new file mode 100644 index 00000000..b57f6e98 --- /dev/null +++ b/Disco.Models/Repository/Device/DeviceProfile.cs @@ -0,0 +1,83 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.ComponentModel; +using System.Linq.Expressions; + +namespace Disco.Models.Repository +{ + public partial class DeviceProfile + { + [Key] + public int Id { get; set; } + + [Required, StringLength(100)] + public string Name { get; set; } + + [Required, StringLength(10)] + public string ShortName { get; set; } + + [StringLength(500)] + public string Description { get; set; } + + public int? DefaultOrganisationAddress { get; set; } + + // Migration from DeviceProfile Configuration + // 2012-06-14 G# + [Required()] + public string ComputerNameTemplate { get; set; } + public enum DistributionTypes : int + { + OneToMany = 0, + OneToOne = 1 + } + [Column("DistributionType"), EditorBrowsable(EditorBrowsableState.Never)] + public int DistributionTypeDb { get; set; } + [NotMapped] + public DistributionTypes DistributionType + { + get + { + return (DistributionTypes)this.DistributionTypeDb; + } + set + { + this.DistributionTypeDb = (int)value; + } + } + public string OrganisationalUnit { get; set; } + // End Migration + + // 2012-06-14 G# + public bool EnforceComputerNameConvention { get; set; } + public bool EnforceOrganisationalUnit { get; set; } + + // 2012-06-28 G# + public bool ProvisionADAccount { get; set; } + + public virtual IList Devices { get; set; } + + public override string ToString() + { + if (string.IsNullOrEmpty(this.ShortName)) + { + return this.Name; + } + return string.Format("{0} ({1})", this.Name, this.ShortName); + } + + // 2012-06-21 + // public bool AllocateCertificate { get; set; } // Renamed from 'AllocateWirelessCertificate' + [StringLength(64)] + public string CertificateProviderId { get; set; } + } + public partial class DeviceProfile + { + public class PropertyAccessExpressions { + public static readonly Expression> DistributionTypeDb = x => x.DistributionTypeDb; + } + } +} diff --git a/Disco.Models/Repository/Device/DeviceUserAssignment.cs b/Disco.Models/Repository/Device/DeviceUserAssignment.cs new file mode 100644 index 00000000..c0bca3d1 --- /dev/null +++ b/Disco.Models/Repository/Device/DeviceUserAssignment.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Disco.Models.Repository +{ + public class DeviceUserAssignment + { + [Key, Column(Order = 0)] + public string DeviceSerialNumber { get; set; } + public string AssignedUserId { get; set; } + + [Column(Order = 1), Key] + public DateTime AssignedDate { get; set; } + public DateTime? UnassignedDate { get; set; } + + [ForeignKey("AssignedUserId")] + public virtual User AssignedUser { get; set; } + + [ForeignKey("DeviceSerialNumber")] + public virtual Device Device { get; set; } + } +} diff --git a/Disco.Models/Repository/DocumentTemplate/DocumentTemplate.cs b/Disco.Models/Repository/DocumentTemplate/DocumentTemplate.cs new file mode 100644 index 00000000..3cda79e2 --- /dev/null +++ b/Disco.Models/Repository/DocumentTemplate/DocumentTemplate.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Disco.Models.Repository +{ + public class DocumentTemplate + { + public const string PdfMimeType = "application/pdf"; + + [StringLength(30), Required, Key] + public string Id { get; set; } + + [StringLength(250), Required] + public string Description { get; set; } + [Required, StringLength(6)] + public string Scope { get; set; } + [StringLength(250)] + public string FilterExpression { get; set; } + + // Feature Request 2012-05-10 by G#: https://disco.uservoice.com/forums/159707-feedback/suggestions/2811092-document-template-option-flatten-form-on-generate + public bool FlattenForm { get; set; } + // End Feature Request + + [InverseProperty("DocumentTemplates")] + public virtual IList JobSubTypes { get; set; } + + public static class DocumentTemplateScopes + { + public const string Device = "Device"; + public const string Job = "Job"; + public const string User = "User"; + + public static List ToList() + { + return new List { Device, Job, User }; + } + } + + } +} diff --git a/Disco.Models/Repository/Job/Job.cs b/Disco.Models/Repository/Job/Job.cs new file mode 100644 index 00000000..ba121484 --- /dev/null +++ b/Disco.Models/Repository/Job/Job.cs @@ -0,0 +1,132 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Disco.Models.Repository +{ + public class Job + { + [Key] + public int Id { get; set; } + [Required] + public string JobTypeId { get; set; } + + public string DeviceSerialNumber { get; set; } + public string UserId { get; set; } + + [Required] + public string OpenedTechUserId { get; set; } + + public DateTime OpenedDate { get; set; } + [DisplayFormat(ApplyFormatInEditMode = true, ConvertEmptyStringToNull = true, DataFormatString = "{0:yyyy/MM/dd hh:mm tt}", HtmlEncode = false)] + public DateTime? ExpectedClosedDate { get; set; } + public string ClosedTechUserId { get; set; } + public DateTime? ClosedDate { get; set; } + + public long? Flags { get; set; } + + [Display(Name = "Technician Held Device")] + public DateTime? DeviceHeld { get; set; } + public string DeviceHeldTechUserId { get; set; } + [StringLength(100)] + public string DeviceHeldLocation { get; set; } + + public DateTime? DeviceReadyForReturn { get; set; } + public string DeviceReadyForReturnTechUserId { get; set; } + public DateTime? DeviceReturnedDate { get; set; } + public string DeviceReturnedTechUserId { get; set; } + + public DateTime? WaitingForUserAction { get; set; } + + [ForeignKey("JobTypeId")] + public virtual JobType JobType { get; set; } + public virtual IList JobSubTypes { get; set; } + + [ForeignKey("DeviceSerialNumber")] + public virtual Device Device { get; set; } + [ForeignKey("UserId")] + public virtual User User { get; set; } + + [ForeignKey("OpenedTechUserId")] + public virtual User OpenedTechUser { get; set; } + [ForeignKey("ClosedTechUserId")] + public virtual User ClosedTechUser { get; set; } + + [ForeignKey("DeviceHeldTechUserId")] + public virtual User DeviceHeldTechUser { get; set; } + [ForeignKey("DeviceReadyForReturnTechUserId")] + public virtual User DeviceReadyForReturnTechUser { get; set; } + [ForeignKey("DeviceReturnedTechUserId")] + public virtual User DeviceReturnedTechUser { get; set; } + + //// Added 2012-10-23 G# - DBv5 Migration + //public virtual IList JobAssignments { get; set; } + //// End Added 2012-10-23 G# - DBv5 Migration + + public virtual IList JobAttachments { get; set; } + public virtual IList JobComponents { get; set; } + public virtual IList JobLogs { get; set; } + + public virtual JobMetaInsurance JobMetaInsurance { get; set; } + public virtual JobMetaWarranty JobMetaWarranty { get; set; } + public virtual JobMetaNonWarranty JobMetaNonWarranty { get; set; } + + #region Helper Members + public decimal JobComponentsTotalCost() + { + if (this.JobComponents != null) + { + return this.JobComponents.Sum(jc => jc.Cost); + } + return decimal.Zero; + } + #endregion + + public static class JobStatusIds + { + public const string AwaitingAccountingPayment = "AwaitingAccountingPayment"; + public const string AwaitingAccountingCharge = "AwaitingAccountingCharge"; + public const string AwaitingDeviceReturn = "AwaitingDeviceReturn"; + public const string AwaitingInsuranceProcessing = "AwaitingInsuranceProcessing"; + public const string AwaitingRepairs = "AwaitingRepairs"; + public const string AwaitingUserAction = "AwaitingUserAction"; + public const string AwaitingWarrantyRepair = "AwaitingWarrantyRepair"; + public const string Closed = "Closed"; + public const string Open = "Open"; + } + + [Flags] + public enum UserManagementFlags : long + { + [Display(GroupName = JobSubType.UserManagementJobSubTypes.Infringement, Name = "Content - Games")] + Infringement_ContentGames = 1, + [Display(GroupName = JobSubType.UserManagementJobSubTypes.Infringement, Name = "Content - Illegal")] + Infringement_ContentIllegal = 2, + [Display(GroupName = JobSubType.UserManagementJobSubTypes.Infringement, Name = "Content - Violence")] + Infringement_ContentViolence = 4, + [Display(GroupName = JobSubType.UserManagementJobSubTypes.Infringement, Name = "Content - Pornography")] + Infringement_ContentPornography = 8, + [Display(GroupName = JobSubType.UserManagementJobSubTypes.Infringement, Name = "Hacking")] + Infringement_Hacking = 16, + [Display(GroupName = JobSubType.UserManagementJobSubTypes.Infringement, Name = "Proxy Bypass")] + Infringement_ProxyBypass = 32, + [Display(GroupName = JobSubType.UserManagementJobSubTypes.Infringement, Name = "Breach Usage Agreement")] + Infringement_BreachUsageAgreement = 64, + [Display(GroupName = JobSubType.UserManagementJobSubTypes.Infringement, Name = "Breach Financial Agreement")] + Infringement_BreachFinancialAgreement = 128, + [Display(GroupName = JobSubType.UserManagementJobSubTypes.Contact, Name = "Phone")] + Contact_Phone = 4294967296, + [Display(GroupName = JobSubType.UserManagementJobSubTypes.Contact, Name = "Email")] + Contact_Email = 8589934592, + [Display(GroupName = JobSubType.UserManagementJobSubTypes.Contact, Name = "In Person")] + Contact_InPerson = 17179869184, + [Display(GroupName = JobSubType.UserManagementJobSubTypes.Contact, Name = "SMS")] + Contact_SMS = 34359738368, + [Display(GroupName = JobSubType.UserManagementJobSubTypes.Contact, Name = "Mail")] + Contact_Mail = 68719476736, + } + } +} diff --git a/Disco.Models/Repository/Job/JobAssignment.cs b/Disco.Models/Repository/Job/JobAssignment.cs new file mode 100644 index 00000000..6de71101 --- /dev/null +++ b/Disco.Models/Repository/Job/JobAssignment.cs @@ -0,0 +1,30 @@ +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using System.Text; +//using System.ComponentModel.DataAnnotations; +//using System.ComponentModel.DataAnnotations.Schema; + +//namespace Disco.Models.Repository +//{ +// // Added 2012-10-23 G# - DBv5 Migration +// public class JobAssignment +// { +// [Key, Required, ColumnAttribute(Order = 0)] +// public int JobId { get; set; } +// [Key, Required, ColumnAttribute(Order = 1)] +// public string TechUserId { get; set; } +// [Key, Required, ColumnAttribute(Order = 2)] +// public DateTime AssignedDate { get; set; } + +// public DateTime? UnassignedDate { get; set; } + +// public DateTime? TargetCompletionDate { get; set; } + +// [ForeignKey("JobId"), InverseProperty("JobAssignments")] +// public virtual Job Job { get; set; } + +// [ForeignKey("TechUserId")] +// public User TechUser { get; set; } +// } +//} diff --git a/Disco.Models/Repository/Job/JobAttachment.cs b/Disco.Models/Repository/Job/JobAttachment.cs new file mode 100644 index 00000000..2f046560 --- /dev/null +++ b/Disco.Models/Repository/Job/JobAttachment.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Disco.Models.Repository +{ + public class JobAttachment + { + [Key] + public int Id { get; set; } + public int JobId { get; set; } + + [Required] + public string TechUserId { get; set; } + [Required, StringLength(500)] + public string Filename { get; set; } + [Required, StringLength(500)] + public string MimeType { get; set; } + public DateTime Timestamp { get; set; } + [StringLength(500), Required] + public string Comments { get; set; } + + public string DocumentTemplateId { get; set; } + + [ForeignKey("JobId"), InverseProperty("JobAttachments")] + public virtual Job Job { get; set; } + + [ForeignKey("TechUserId")] + public virtual User TechUser { get; set; } + + [ForeignKey("DocumentTemplateId")] + public virtual DocumentTemplate DocumentTemplate { get; set; } + } +} diff --git a/Disco.Models/Repository/Job/JobComponent.cs b/Disco.Models/Repository/Job/JobComponent.cs new file mode 100644 index 00000000..683c45c2 --- /dev/null +++ b/Disco.Models/Repository/Job/JobComponent.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Disco.Models.Repository +{ + public class JobComponent + { + [Key] + public int Id { get; set; } + public int JobId { get; set; } + + [Required] + public string TechUserId { get; set; } + [StringLength(500)] + public string Description { get; set; } + public decimal Cost { get; set; } + + [ForeignKey("JobId")] + public virtual Job Job { get; set; } + + [ForeignKey("TechUserId")] + public virtual User TechUser { get; set; } + } +} diff --git a/Disco.Models/Repository/Job/JobLog.cs b/Disco.Models/Repository/Job/JobLog.cs new file mode 100644 index 00000000..0610ffc2 --- /dev/null +++ b/Disco.Models/Repository/Job/JobLog.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Disco.Models.Repository +{ + public class JobLog + { + [Key] + public int Id { get; set; } + public int JobId { get; set; } + + [Required] + public string TechUserId { get; set; } + public DateTime Timestamp { get; set; } + [Required] + public string Comments { get; set; } + + [ForeignKey("JobId")] + public Job Job { get; set; } + + [ForeignKey("TechUserId")] + public User TechUser { get; set; } + } +} diff --git a/Disco.Models/Repository/Job/JobMeta/JobMetaInsurance.cs b/Disco.Models/Repository/Job/JobMeta/JobMetaInsurance.cs new file mode 100644 index 00000000..e7b595f0 --- /dev/null +++ b/Disco.Models/Repository/Job/JobMeta/JobMetaInsurance.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Disco.Models.Repository +{ + public class JobMetaInsurance + { + [Required, Key] + public int JobId { get; set; } + + [DisplayFormat(ApplyFormatInEditMode = true, ConvertEmptyStringToNull = true, DataFormatString = "{0:yyyy/MM/dd hh:mm tt}", HtmlEncode = false)] + public DateTime? LossOrDamageDate { get; set; } + + [StringLength(200)] + public string EventLocation { get; set; } + + [DataType(DataType.MultilineText)] + public string Description { get; set; } + + [Display(Name = "Caused by Third Party")] + public bool ThirdPartyCaused { get; set; } + [StringLength(200)] + public string ThirdPartyCausedName { get; set; } + [DataType(DataType.MultilineText), StringLength(600)] + public string ThirdPartyCausedWhy { get; set; } + + [StringLength(1200), DataType(DataType.MultilineText)] + public string WitnessesNamesAddresses { get; set; } + + [StringLength(200)] + public string BurglaryTheftMethodOfEntry { get; set; } + + [DisplayFormat(ApplyFormatInEditMode = true, ConvertEmptyStringToNull = true, DataFormatString = "{0:yyyy/MM/dd hh:mm tt}", HtmlEncode = false)] + public DateTime? PropertyLastSeenDate { get; set; } + + [Display(Name = "Police Notified")] + public bool PoliceNotified { get; set; } + [StringLength(200)] + public string PoliceNotifiedStation { get; set; } + [DisplayFormat(ApplyFormatInEditMode = true, ConvertEmptyStringToNull = true, DataFormatString = "{0:yyyy/MM/dd}", HtmlEncode = false)] + public DateTime? PoliceNotifiedDate { get; set; } + [StringLength(400)] + public string PoliceNotifiedCrimeReportNo { get; set; } + + [DataType(DataType.MultilineText), StringLength(800)] + public string RecoverReduceAction { get; set; } + + [StringLength(500)] + public string OtherInterestedParties { get; set; } + + [DisplayFormat(ApplyFormatInEditMode = true, ConvertEmptyStringToNull = true, DataFormatString = "{0:yyyy/MM/dd}", HtmlEncode = false)] + public DateTime? DateOfPurchase { get; set; } + + [DisplayFormat(ApplyFormatInEditMode = true, ConvertEmptyStringToNull = true, DataFormatString = "{0:yyyy/MM/dd hh:mm tt}", HtmlEncode = false)] + public DateTime? ClaimFormSentDate { get; set; } + public string ClaimFormSentUserId { get; set; } + + [Required, ForeignKey("JobId")] + public virtual Job Job { get; set; } + + [ForeignKey("ClaimFormSentUserId")] + public virtual User ClaimFormSentUser { get; set; } + } +} diff --git a/Disco.Models/Repository/Job/JobMeta/JobMetaNonWarranty.cs b/Disco.Models/Repository/Job/JobMeta/JobMetaNonWarranty.cs new file mode 100644 index 00000000..95f68fdc --- /dev/null +++ b/Disco.Models/Repository/Job/JobMeta/JobMetaNonWarranty.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Disco.Models.Repository +{ + public class JobMetaNonWarranty + { + [Key, Required] + public int JobId { get; set; } + + public bool IsInsuranceClaim { get; set; } + + // Feature Request 2012-05-10 by Michael E: https://disco.uservoice.com/forums/159707-feedback/suggestions/2811092-document-template-option-flatten-form-on-generate + [DisplayFormat(ApplyFormatInEditMode = true, ConvertEmptyStringToNull = true, DataFormatString = "{0:yyyy/MM/dd hh:mm tt}", HtmlEncode = false)] + public DateTime? AccountingChargeRequiredDate { get; set; } + [ForeignKey("AccountingChargeRequiredUserId")] + public virtual User AccountingChargeRequiredUser { get; set; } + public string AccountingChargeRequiredUserId { get; set; } + // End Feature Request + + [DisplayFormat(ApplyFormatInEditMode = true, ConvertEmptyStringToNull = true, DataFormatString = "{0:yyyy/MM/dd hh:mm tt}", HtmlEncode = false)] + public DateTime? AccountingChargeAddedDate { get; set; } + [ForeignKey("AccountingChargeAddedUserId")] + public virtual User AccountingChargeAddedUser { get; set; } + public string AccountingChargeAddedUserId { get; set; } + + [DisplayFormat(ApplyFormatInEditMode = true, ConvertEmptyStringToNull = true, DataFormatString = "{0:yyyy/MM/dd hh:mm tt}", HtmlEncode = false)] + public DateTime? AccountingChargePaidDate { get; set; } + [ForeignKey("AccountingChargePaidUserId")] + public virtual User AccountingChargePaidUser { get; set; } + public string AccountingChargePaidUserId { get; set; } + + [DisplayFormat(ApplyFormatInEditMode = true, ConvertEmptyStringToNull = true, DataFormatString = "{0:yyyy/MM/dd hh:mm tt}", HtmlEncode = false)] + public DateTime? PurchaseOrderRaisedDate { get; set; } + [ForeignKey("PurchaseOrderRaisedUserId")] + public virtual User PurchaseOrderRaisedUser { get; set; } + public string PurchaseOrderRaisedUserId { get; set; } + [StringLength(20)] + public string PurchaseOrderReference { get; set; } + [DisplayFormat(ApplyFormatInEditMode = true, ConvertEmptyStringToNull = true, DataFormatString = "{0:yyyy/MM/dd hh:mm tt}", HtmlEncode = false)] + public DateTime? PurchaseOrderSentDate { get; set; } + [ForeignKey("PurchaseOrderSentUserId")] + public virtual User PurchaseOrderSentUser { get; set; } + public string PurchaseOrderSentUserId { get; set; } + [DisplayFormat(ApplyFormatInEditMode = true, ConvertEmptyStringToNull = true, DataFormatString = "{0:yyyy/MM/dd hh:mm tt}", HtmlEncode = false)] + public DateTime? InvoiceReceivedDate { get; set; } + [ForeignKey("InvoiceReceivedUserId")] + public virtual User InvoiceReceivedUser { get; set; } + public string InvoiceReceivedUserId { get; set; } + + [StringLength(100)] + public string RepairerName { get; set; } + [DisplayFormat(ApplyFormatInEditMode = true, ConvertEmptyStringToNull = true, DataFormatString = "{0:yyyy/MM/dd hh:mm tt}", HtmlEncode = false)] + public DateTime? RepairerLoggedDate { get; set; } + [StringLength(100)] + public string RepairerReference { get; set; } + [DisplayFormat(ApplyFormatInEditMode = true, ConvertEmptyStringToNull = true, DataFormatString = "{0:yyyy/MM/dd hh:mm tt}", HtmlEncode = false)] + public DateTime? RepairerCompletedDate { get; set; } + + [ForeignKey("JobId"), Required] + public virtual Job Job { get; set; } + } +} diff --git a/Disco.Models/Repository/Job/JobMeta/JobMetaWarranty.cs b/Disco.Models/Repository/Job/JobMeta/JobMetaWarranty.cs new file mode 100644 index 00000000..a78a3686 --- /dev/null +++ b/Disco.Models/Repository/Job/JobMeta/JobMetaWarranty.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Disco.Models.Repository +{ + public class JobMetaWarranty + { + [Required, Key] + public int JobId { get; set; } + + [StringLength(100)] + public string ExternalName { get; set; } + [DisplayFormat(ApplyFormatInEditMode = true, ConvertEmptyStringToNull = true, DataFormatString = "{0:yyyy/MM/dd hh:mm tt}", HtmlEncode = false)] + public DateTime? ExternalLoggedDate { get; set; } + [StringLength(100)] + public string ExternalReference { get; set; } + [DisplayFormat(ApplyFormatInEditMode = true, ConvertEmptyStringToNull = true, DataFormatString = "{0:yyyy/MM/dd hh:mm tt}", HtmlEncode = false)] + public DateTime? ExternalCompletedDate { get; set; } + + [ForeignKey("JobId"), Required] + public virtual Job Job { get; set; } + } +} diff --git a/Disco.Models/Repository/Job/JobSubType.cs b/Disco.Models/Repository/Job/JobSubType.cs new file mode 100644 index 00000000..48157aea --- /dev/null +++ b/Disco.Models/Repository/Job/JobSubType.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Disco.Models.Repository +{ + public class JobSubType + { + [Key, StringLength(20), Column(Order = 0)] + public string Id { get; set; } + [Key, Required, Column(Order = 1)] + public string JobTypeId { get; set; } + [Required, StringLength(100)] + public string Description { get; set; } + + public virtual IList AttachmentTypes { get; set; } + public virtual IList DeviceComponents { get; set; } + + [ForeignKey("JobTypeId")] + public virtual JobType JobType { get; set; } + public virtual IList Jobs { get; set; } + + public static class UserManagementJobSubTypes + { + public const string Infringement = "Infringement"; + public const string Contact = "Contact"; + } + + public override string ToString() + { + return this.Description; + } + } +} diff --git a/Disco.Models/Repository/Job/JobType.cs b/Disco.Models/Repository/Job/JobType.cs new file mode 100644 index 00000000..03663f9d --- /dev/null +++ b/Disco.Models/Repository/Job/JobType.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel.DataAnnotations; + +namespace Disco.Models.Repository +{ + public class JobType + { + [StringLength(5), Key] + public string Id { get; set; } + [StringLength(100)] + public string Description { get; set; } + + public virtual IList JobSubTypes { get; set; } + + public override string ToString() + { + return this.Description; + } + + public static class JobTypeIds + { + public const string HMisc = "HMisc"; + public const string HNWar = "HNWar"; + public const string HWar = "HWar"; + public const string SApp = "SApp"; + public const string SImg = "SImg"; + public const string SOS = "SOS"; + public const string UMgmt = "UMgmt"; + } + } +} diff --git a/Disco.Models/Repository/User/User.cs b/Disco.Models/Repository/User/User.cs new file mode 100644 index 00000000..076cc0c4 --- /dev/null +++ b/Disco.Models/Repository/User/User.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Disco.Models.Repository +{ + public class User + { + [StringLength(50), Key] + public string Id { get; set; } + + [StringLength(200)] + public string DisplayName { get; set; } + [StringLength(200)] + public string Surname { get; set; } + [StringLength(200)] + public string GivenName { get; set; } + + [StringLength(8)] + public string Type { get; set; } + [StringLength(100)] + public string PhoneNumber { get; set; } + [StringLength(150)] + public string EmailAddress { get; set; } + + public virtual IList UserDetails { get; set; } + public virtual IList UserAttachments { get; set; } + public virtual IList DeviceUserAssignments { get; set; } + [InverseProperty("UserId")] + public virtual IList Jobs { get; set; } + + //#region Helper Members + //[NotMapped, XmlIgnore, ScriptIgnore] + //public List CurrentDeviceUserAssignments + //{ + // get + // { + // return this.DeviceUserAssignments.Where(dua => !dua.UnassignedDate.HasValue).ToList(); + // } + //} + //#endregion + + public override string ToString() + { + return string.Format("{0} ({1})", this.DisplayName, this.Id); + } + + public void UpdateSelf(User u) + { + if (!this.Id.Equals(u.Id, StringComparison.InvariantCultureIgnoreCase)) + throw new ArgumentException("User Id's do not match", "u"); + + if (this.Surname != u.Surname) + this.Surname = u.Surname; + if (this.GivenName != u.GivenName) + this.GivenName = u.GivenName; + if (this.DisplayName != u.DisplayName) + this.DisplayName = u.DisplayName; + if (this.EmailAddress != u.EmailAddress) + this.EmailAddress = u.EmailAddress; + if (this.PhoneNumber != u.PhoneNumber) + this.PhoneNumber = u.PhoneNumber; + if (this.Type != u.Type) + this.Type = u.Type; + } + + public static class Types + { + public const string Admin = "Admin"; + public const string Computer = "Computer"; + public const string Staff = "Staff"; + public const string Student = "Student"; + } + + } +} diff --git a/Disco.Models/Repository/User/UserAttachment.cs b/Disco.Models/Repository/User/UserAttachment.cs new file mode 100644 index 00000000..51e22a6d --- /dev/null +++ b/Disco.Models/Repository/User/UserAttachment.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Disco.Models.Repository +{ + public class UserAttachment + { + [Key] + public int Id { get; set; } + public string UserId { get; set; } + [Required] + public string TechUserId { get; set; } + [Required, StringLength(500)] + public string Filename { get; set; } + [StringLength(500), Required] + public string MimeType { get; set; } + public DateTime Timestamp { get; set; } + [Required, StringLength(500)] + public string Comments { get; set; } + + public string DocumentTemplateId { get; set; } + + [ForeignKey("UserId"), InverseProperty("UserAttachments")] + public virtual User User { get; set; } + + [ForeignKey("TechUserId")] + public virtual User TechUser { get; set; } + + [ForeignKey("DocumentTemplateId")] + public virtual DocumentTemplate DocumentTemplate { get; set; } + } +} diff --git a/Disco.Models/Repository/User/UserDetail.cs b/Disco.Models/Repository/User/UserDetail.cs new file mode 100644 index 00000000..ea429a11 --- /dev/null +++ b/Disco.Models/Repository/User/UserDetail.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Disco.Models.Repository +{ + public class UserDetail + { + [Key, Column(Order = 0)] + public string UserId { get; set; } + + [Column(Order = 1), Key, StringLength(100)] + public string Scope { get; set; } + + [Key, Column(Order = 2), StringLength(100)] + public string Key { get; set; } + + public string Value { get; set; } + + [ForeignKey("UserId")] + public virtual User User { get; set; } + } +} diff --git a/Disco.Services/App.config b/Disco.Services/App.config new file mode 100644 index 00000000..f1f62828 --- /dev/null +++ b/Disco.Services/App.config @@ -0,0 +1,17 @@ + + + + +
+ + + + + + + + + + + + \ No newline at end of file diff --git a/Disco.Services/App_Start/RazorGeneratorMvcStart.cs b/Disco.Services/App_Start/RazorGeneratorMvcStart.cs new file mode 100644 index 00000000..42440fc8 --- /dev/null +++ b/Disco.Services/App_Start/RazorGeneratorMvcStart.cs @@ -0,0 +1,21 @@ +using System.Web; +using System.Web.Mvc; +using System.Web.WebPages; +using RazorGenerator.Mvc; + +[assembly: WebActivator.PostApplicationStartMethod(typeof(Disco.Services.App_Start.RazorGeneratorMvcStart), "Start")] + +namespace Disco.Services.App_Start { + public static class RazorGeneratorMvcStart { + public static void Start() { + var engine = new PrecompiledMvcEngine(typeof(RazorGeneratorMvcStart).Assembly) { + UsePhysicalViewsIfNewer = HttpContext.Current.Request.IsLocal + }; + + ViewEngines.Engines.Insert(0, engine); + + // StartPage lookups are done by WebPages. + VirtualPathFactoryManager.RegisterVirtualPathFactory(engine); + } + } +} diff --git a/Disco.Services/Disco.Services.csproj b/Disco.Services/Disco.Services.csproj new file mode 100644 index 00000000..dcb5df4d --- /dev/null +++ b/Disco.Services/Disco.Services.csproj @@ -0,0 +1,186 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {B80A737F-BD6A-4986-9182-DD7B932BD950} + Library + Properties + Disco.Services + Disco.Services + v4.5 + 512 + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + ..\packages\EntityFramework.5.0.0\lib\net45\EntityFramework.dll + + + True + ..\packages\Microsoft.Web.Infrastructure.1.0.0.0\lib\net40\Microsoft.Web.Infrastructure.dll + + + False + ..\packages\Newtonsoft.Json.4.5.11\lib\net40\Newtonsoft.Json.dll + + + False + ..\..\Resources\Libraries\Quartz\Quartz.dll + + + False + ..\packages\RazorGenerator.Mvc.1.5.0.0\lib\net40\RazorGenerator.Mvc.dll + + + False + ..\packages\SignalR.Server.0.5.3\lib\net40\SignalR.dll + + + False + ..\packages\SignalR.Hosting.AspNet.0.5.3\lib\net45\SignalR.Hosting.AspNet.dll + + + ..\packages\SignalR.Hosting.Common.0.5.3\lib\net40\SignalR.Hosting.Common.dll + + + + + + + True + ..\packages\Microsoft.SqlServer.Compact.4.0.8876.1\lib\net40\System.Data.SqlServerCe.dll + + + + + + + + + + + + + False + ..\packages\WebActivator.1.5.3\lib\net40\WebActivator.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {85A6BD19-2C64-4746-8F2C-A68A86E8C2D7} + Disco.Data + + + {FBC05512-FCCA-4B16-9E76-8C413C5DE6C9} + Disco.Models + + + + + + + + + + + + + + + + if not exist "$(TargetDir)x86" md "$(TargetDir)x86" + xcopy /s /y "$(SolutionDir)packages\Microsoft.SqlServer.Compact.4.0.8876.1\NativeBinaries\x86\*.*" "$(TargetDir)x86" + if not exist "$(TargetDir)amd64" md "$(TargetDir)amd64" + xcopy /s /y "$(SolutionDir)packages\Microsoft.SqlServer.Compact.4.0.8876.1\NativeBinaries\amd64\*.*" "$(TargetDir)amd64" + + + \ No newline at end of file diff --git a/Disco.Services/Logging/LogBase.cs b/Disco.Services/Logging/LogBase.cs new file mode 100644 index 00000000..14060559 --- /dev/null +++ b/Disco.Services/Logging/LogBase.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Disco.Services.Logging +{ + public abstract class LogBase + { + private Dictionary _EventTypes; + + public LogBase() + { + // Cache Event Types + _EventTypes = this.LoadEventTypes().ToDictionary(et => et.Id); + } + + public abstract int ModuleId { get; } + public abstract string ModuleName { get; } + public abstract string ModuleDescription { get; } + protected abstract List LoadEventTypes(); + + public Dictionary EventTypes + { + get + { + return _EventTypes; + } + } + protected void Log(int EventTypeId, params object[] Args) + { + LogContext.Current.Log(this.ModuleId, EventTypeId, Args); + } + public string LiveLogGroupName + { + get + { + return this.ModuleName; + } + } + + } +} diff --git a/Disco.Services/Logging/LogContext.cs b/Disco.Services/Logging/LogContext.cs new file mode 100644 index 00000000..ac5c856b --- /dev/null +++ b/Disco.Services/Logging/LogContext.cs @@ -0,0 +1,314 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Data.Repository; +using System.IO; +using System.Management; +using System.Diagnostics; +using System.Data.SqlServerCe; +using System.Data.EntityClient; +using System.Data.Entity; +using Quartz; +using Quartz.Impl; +using Quartz.Impl.Triggers; +using Newtonsoft.Json; + +namespace Disco.Services.Logging +{ + public class LogContext + { + public static Dictionary LogModules { get; private set; } + private static object _LogModulesLock = new object(); + + private static LogContext _Current; + private static object _CurrentLock = new Object(); + public static LogContext Current + { + get + { + lock (_CurrentLock) + { + if (_Current == null) + throw new InvalidOperationException("Logging Context has not been Initialized"); + return _Current; + } + } + private set + { + lock (_CurrentLock) + { + _Current = value; + } + } + } + + private static void InitalizeModules() + { + if (LogModules == null) + { + lock (_LogModulesLock) + { + if (LogModules == null) + { + LogModules = new Dictionary(); + // Load all LogModules (Only from Disco Assemblies) + var appDomain = AppDomain.CurrentDomain; + + var logModuleTypes = (from a in appDomain.GetAssemblies() + where !a.GlobalAssemblyCache && !a.IsDynamic && a.FullName.StartsWith("Disco.", StringComparison.InvariantCultureIgnoreCase) + from type in a.GetTypes() + where typeof(LogBase).IsAssignableFrom(type) && !type.IsAbstract + select type); + foreach (var logModuleType in logModuleTypes) + { + var instance = (LogBase)Activator.CreateInstance(logModuleType); + LogModules[instance.ModuleId] = instance; + } + } + } + } + } + + private static void InitalizeDatabase(Targets.LogPersistContext logDbContext) + { + // Add Modules + var existingModules = logDbContext.Modules.Include("EventTypes").ToDictionary(m => m.Id); + foreach (var module in LogModules) + { + // Update/Insert Module + Models.LogModule dbModule; + if (existingModules.TryGetValue(module.Key, out dbModule)) + { + // Update + if (dbModule.Name != module.Value.ModuleName) + dbModule.Name = module.Value.ModuleName; + if (dbModule.Description != module.Value.ModuleDescription) + dbModule.Description = module.Value.ModuleDescription; + } + else + { + // Insert + dbModule = new Models.LogModule() + { + Id = module.Key, + Name = module.Value.ModuleName, + Description = module.Value.ModuleDescription + }; + logDbContext.Modules.Add(dbModule); + } + // Update/Insert Event Types + Dictionary existingEventTypes = (dbModule.EventTypes == null) ? new Dictionary() : dbModule.EventTypes.ToDictionary(et => et.Id); + foreach (var eventType in module.Value.EventTypes) + { + Models.LogEventType dbEventType; + if (existingEventTypes.TryGetValue(eventType.Key, out dbEventType)) + { + // Update + if (dbEventType.Name != eventType.Value.Name) + dbEventType.Name = eventType.Value.Name; + if (dbEventType.Severity != eventType.Value.Severity) + dbEventType.Severity = eventType.Value.Severity; + if (dbEventType.Format != eventType.Value.Format) + dbEventType.Format = eventType.Value.Format; + } + else + { + // Insert + dbEventType = new Models.LogEventType() + { + Id = eventType.Key, + ModuleId = module.Key, + Name = eventType.Value.Name, + Severity = eventType.Value.Severity, + Format = eventType.Value.Format + }; + logDbContext.EventTypes.Add(dbEventType); + } + } + } + + logDbContext.SaveChanges(); + } + + public static string LogFileBasePath(DiscoDataContext DiscoContext) + { + var logDirectoryBase = Path.Combine(DiscoContext.DiscoConfiguration.DataStoreLocation, "Logs"); + // Create Directory Structure + if (!Directory.Exists(logDirectoryBase)) + { + Directory.CreateDirectory(logDirectoryBase); + } + // Ensure Logs are NTFS Compressed - TODO... + //Utilities.CompressDirectory(logDirectory); + // WMI - Doesn't Work for Network Folders... + //var logDirectoryBaseInfo = new DirectoryInfo(logDirectoryBase); + //if ((logDirectoryBaseInfo.Attributes & FileAttributes.Compressed) != FileAttributes.Compressed) + //{ + // var logDirectoryWmiPath = string.Format("Win32_Directory.Name=\"{0}\"", logDirectoryBase); + // using (ManagementObject logDirectoryBaseMO = new ManagementObject(logDirectoryWmiPath)) + // { + // ManagementBaseObject outParams = logDirectoryBaseMO.InvokeMethod("Compress", null, null); + // Debug.WriteLine("LoggingContext.InitalizeCurrent: Compressing Log Folder; Result: " + outParams.Properties["ReturnValue"].Value.ToString()); + // } + //} + return logDirectoryBase; + } + + public static string LogFilePath(DiscoDataContext DiscoContext, DateTime Date, bool CreateDirectory = true) + { + var logDirectoryBase = LogFileBasePath(DiscoContext); + var logDirectory = Path.Combine(logDirectoryBase, Date.Year.ToString()); + if (CreateDirectory && !Directory.Exists(logDirectory)) + { + Directory.CreateDirectory(logDirectory); + } + var logFileName = string.Format("DiscoLog_{0:yyy-MM-dd}.sdf", Date); + return Path.Combine(logDirectory, logFileName); + } + + internal static void ReInitalize(DiscoDataContext DiscoContext) + { + lock (_CurrentLock) + { + var logPath = LogFilePath(DiscoContext, DateTime.Today); + + //var connectionString = string.Format("Data Source=\"{0}\"", logPath); + + SqlCeConnectionStringBuilder sqlCeCSB = new SqlCeConnectionStringBuilder(); + sqlCeCSB.DataSource = logPath; + var connectionString = sqlCeCSB.ToString(); + + // Ensure Database Exists + if (!File.Exists(logPath)) + { + // Create Database + using (var context = new Targets.LogPersistContext(connectionString)) + { + context.Database.CreateIfNotExists(); + } + } + + // Add Modules/Event Types + InitalizeModules(); + using (var context = new Targets.LogPersistContext(connectionString)) + { + InitalizeDatabase(context); + } + + // Create Current LogContext + var currentLogContext = new LogContext(logPath, connectionString); + _Current = currentLogContext; + } + SystemLog.LogLogInitialized(_Current.PersistantStorePath); + try + { + // Get Yesterdays Log + var yesterdaysLogPath = LogFilePath(DiscoContext, DateTime.Today.AddDays(-1), false); + if (File.Exists(yesterdaysLogPath)) + { + SqlCeConnectionStringBuilder sqlCeCSB = new SqlCeConnectionStringBuilder(); + sqlCeCSB.DataSource = yesterdaysLogPath; + var connectionString = sqlCeCSB.ToString(); + int logCount; + using (var context = new Targets.LogPersistContext(connectionString)) + { + logCount = context.Events.Where(e => !(e.ModuleId == 0 && e.EventTypeId == 100)).Count(); + if (logCount == 0) + { + // Delete (empty) Database + context.Database.Delete(); + } + } + } + } + catch (Exception ex) + { + SystemLog.LogError("Error occurred while investigating yesterdays log for deletion", ex.GetType().Name, ex.Message, ex.StackTrace); + } + } + + private static IScheduler _ReInitializeScheduler; + public static void Initalize(DiscoDataContext DiscoContext, ISchedulerFactory SchedulerFactory) + { + ReInitalize(DiscoContext); + + _ReInitializeScheduler = SchedulerFactory.GetScheduler(); + + var reInitalizeJobDetail = new JobDetailImpl("DiscoLogContextReinialize", typeof(LogReInitalizeJob)); + + // Simple Trigger - Issue with Day light savings + //var reInitalizeTrigger = TriggerBuilder.Create() + // .WithIdentity("DiscoLogContextReinializeTrigger") + // .StartAt(DateBuilder.TomorrowAt(0,0,0)) + // .WithSchedule(SimpleScheduleBuilder.Create().WithIntervalInHours(24).RepeatForever()) + // .Build(); + // Use Cron Schedule instead + var reInitalizeTrigger = TriggerBuilder.Create() + .WithIdentity("DiscoLogContextReinializeTrigger") + .StartNow() + .WithSchedule(CronScheduleBuilder.DailyAtHourAndMinute(0, 0)) // Midnight + .Build(); + + _ReInitializeScheduler.ScheduleJob(reInitalizeJobDetail, reInitalizeTrigger); + } + public static string LiveLogAllEventsGroupName + { + get + { + return Targets.LogLiveContext.LiveLogNameAll; + } + } + + private LogContext(string PersistantStorePath, string PersistantStoreConnectionString) + { + this.PersistantStorePath = PersistantStorePath; + this.PersistantStoreConnectionString = PersistantStoreConnectionString; + } + + public string PersistantStorePath { get; private set; } + public string PersistantStoreConnectionString { get; private set; } + + public void Log(int ModuleId, int EventTypeId, params object[] Args) + { + LogBase logModule; + if (LogModules.TryGetValue(ModuleId, out logModule)) + { + Models.LogEventType eventType; + if (logModule.EventTypes.TryGetValue(EventTypeId, out eventType)) + { + var eventTimestamp = DateTime.Now; + if (eventType.UseLive) + { + Targets.LogLiveContext.Broadcast(logModule, eventType, eventTimestamp, Args); + } + if (eventType.UsePersist) + { + string args = null; + if (Args != null && Args.Length > 0) + { //args = fastJSON.JSON.Instance.ToJSON(Args, false); // Old fastJSON Implementation + args = JsonConvert.SerializeObject(Args); + } + using (var context = new Targets.LogPersistContext(PersistantStoreConnectionString)) + { + var e = new Models.LogEvent() + { + Timestamp = eventTimestamp, + ModuleId = logModule.ModuleId, + EventTypeId = eventType.Id, + Arguments = args + }; + context.Events.Add(e); + context.SaveChanges(); + } + } + } + else + throw new InvalidOperationException(string.Format("Unknown Log Event Type Called: {0} (for Module: {1})", EventTypeId, ModuleId)); + } + else + throw new InvalidOperationException(string.Format("Unknown Log Module Called: {0}", ModuleId)); + } + + } +} diff --git a/Disco.Services/Logging/LogReInitalizeJob.cs b/Disco.Services/Logging/LogReInitalizeJob.cs new file mode 100644 index 00000000..ad268da4 --- /dev/null +++ b/Disco.Services/Logging/LogReInitalizeJob.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Quartz; +using Disco.Data.Repository; + +namespace Disco.Services.Logging +{ + class LogReInitalizeJob : IJob + { + public void Execute(IJobExecutionContext context) + { + using (DiscoDataContext DiscoContext = new DiscoDataContext()) + { + LogContext.ReInitalize(DiscoContext); + } + } + } +} diff --git a/Disco.Services/Logging/Models/LogEvent.cs b/Disco.Services/Logging/Models/LogEvent.cs new file mode 100644 index 00000000..1bd764cd --- /dev/null +++ b/Disco.Services/Logging/Models/LogEvent.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Disco.Services.Logging.Models +{ + [Table("Events")] + public class LogEvent + { + [Required, Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int Id { get; set; } + [Required] + public int ModuleId { get; set; } + [Required] + public int EventTypeId { get; set; } + [Required] + public DateTime Timestamp { get; set; } + public string Arguments { get; set; } + } +} diff --git a/Disco.Services/Logging/Models/LogEventType.cs b/Disco.Services/Logging/Models/LogEventType.cs new file mode 100644 index 00000000..2dcc07b3 --- /dev/null +++ b/Disco.Services/Logging/Models/LogEventType.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Disco.Services.Logging.Models +{ + [Table("EventTypes")] + public class LogEventType + { + [Required, Key, Column(Order=0), DatabaseGenerated(DatabaseGeneratedOption.None)] + public int ModuleId { get; set; } + [Required, Key, Column(Order = 1), DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + [Required, MaxLength(200)] + public string Name { get; set; } + [Required] + public int Severity { get; set; } + [MaxLength(1024)] + public string Format { get; set; } + + [NotMapped] + public bool UsePersist { get; set; } + [NotMapped] + public bool UseLive { get; set; } + [NotMapped] + public bool UseDisplay { get; set; } + + [ForeignKey("ModuleId")] + public LogModule Module { get; set; } + + public enum Severities + { + Information = 0, + Warning = 1, + Error = 2 + } + + public string FormatMessage(object[] Arguments) + { + + if (Arguments != null && Arguments.Length > 0) + { + if (!string.IsNullOrEmpty(Format)) + { + return string.Format(Format, Arguments); + } + else + { + return Arguments + .Select(v => v == null ? string.Empty : v.ToString()) + .Aggregate((a, b) => a + ", " + (b == null ? string.Empty : b)); + } + } + else + { + if (!string.IsNullOrEmpty(Format)) + { + return Format; + } + } + return string.Empty; + } + } +} diff --git a/Disco.Services/Logging/Models/LogLiveEvent.cs b/Disco.Services/Logging/Models/LogLiveEvent.cs new file mode 100644 index 00000000..db2bcc7b --- /dev/null +++ b/Disco.Services/Logging/Models/LogLiveEvent.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Collections; +using Newtonsoft.Json; + +namespace Disco.Services.Logging.Models +{ + public class LogLiveEvent + { + public int ModuleId { get; set; } + public string ModuleName { get; set; } + public string ModuleDescription { get; set; } + public int EventTypeId { get; set; } + public string EventTypeName { get; set; } + public int EventTypeSeverity { get; set; } + + public DateTime Timestamp { get; set; } + public object[] Arguments { get; set; } + public string FormattedMessage { get; set; } + public string FormattedTimestamp { get; set; } + public bool UseDisplay { get; set; } + + public static LogLiveEvent Create(LogBase logModule, Models.LogEventType eventType, DateTime Timestamp, string jsonArguments) + { + object[] Arguments = null; + if (jsonArguments != null) + { + //var alArguments = fastJSON.JSON.Instance.Parse(jsonArguments) as ArrayList; // Old fastJSON Implementation + Arguments = JsonConvert.DeserializeObject(jsonArguments); + //if (alArguments != null) + //{ + // Arguments = alArguments.ToArray(); + //} + } + return Create(logModule, eventType, Timestamp, Arguments); + } + + public static LogLiveEvent Create(LogBase logModule, Models.LogEventType eventType, DateTime Timestamp, params object[] Arguments) + { + return new Models.LogLiveEvent() + { + ModuleId = logModule.ModuleId, + ModuleName = logModule.ModuleName, + ModuleDescription = logModule.ModuleDescription, + EventTypeId = eventType.Id, + EventTypeName = eventType.Name, + EventTypeSeverity = eventType.Severity, + Timestamp = Timestamp, + Arguments = Arguments, + FormattedMessage = eventType.FormatMessage(Arguments), + FormattedTimestamp = Timestamp.ToString("dd/MM/yyy hh:mm:ss tt"), + UseDisplay = eventType.UseDisplay + }; + } + + } +} diff --git a/Disco.Services/Logging/Models/LogModule.cs b/Disco.Services/Logging/Models/LogModule.cs new file mode 100644 index 00000000..1115ec16 --- /dev/null +++ b/Disco.Services/Logging/Models/LogModule.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Disco.Services.Logging.Models +{ + [Table("Modules")] + public class LogModule + { + [Required, Key, DatabaseGenerated(DatabaseGeneratedOption.None)] + public int Id { get; set; } + [Required, MaxLength(200)] + public string Name { get; set; } + [Required, MaxLength(500)] + public string Description { get; set; } + + public virtual IList EventTypes { get; set; } + } +} diff --git a/Disco.Services/Logging/ReadLogContext.cs b/Disco.Services/Logging/ReadLogContext.cs new file mode 100644 index 00000000..adf3209f --- /dev/null +++ b/Disco.Services/Logging/ReadLogContext.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Services.Logging.Targets; +using Disco.Data.Repository; +using System.IO; +using System.Text.RegularExpressions; +using System.Data.SqlServerCe; +using Disco.Services.Logging.Models; + +namespace Disco.Services.Logging +{ + public class ReadLogContext + { + public DateTime? Start { get; set; } + public DateTime? End { get; set; } + public int? Take { get; set; } + public int? Module { get; set; } + public List EventTypes { get; set; } + + public bool Validate() + { + if (this.Start.HasValue && this.End.HasValue && this.End.Value < this.Start.Value) + throw new ArgumentOutOfRangeException("End", "End must be greater than Start"); + if (this.Start.HasValue && !this.End.HasValue && this.Start > DateTime.Now) + throw new ArgumentOutOfRangeException("Start", "Start must be less than current time"); + + return true; + } + + public List Query(DiscoDataContext DiscoContext) + { + List results = new List(); + + // Validate Options + this.Validate(); + + var relevantLogFiles = RelevantLogFiles(DiscoContext); + relevantLogFiles.Reverse(); + foreach (var logFile in relevantLogFiles) + { + SqlCeConnectionStringBuilder sqlCeCSB = new SqlCeConnectionStringBuilder(); + sqlCeCSB.DataSource = logFile.Item1; + + var logModules = LogContext.LogModules; + + using (var context = new Targets.LogPersistContext(sqlCeCSB.ToString())) + { + var query = this.BuildQuery(context, logFile.Item2, results.Count); + IEnumerable queryResults = query; // Run the Query + results.AddRange(queryResults.Select(le => Models.LogLiveEvent.Create(logModules[le.ModuleId], logModules[le.ModuleId].EventTypes[le.EventTypeId], le.Timestamp, le.Arguments))); + } + if (this.Take.HasValue && this.Take.Value < results.Count) + break; + } + return results; + } + + private static Regex LogFileDateRegex = new Regex("DiscoLog_([0-9]{4})-([0-9]{2})-([0-9]{2}).sdf", RegexOptions.IgnoreCase); + private static DateTime? LogFileDate(string LogFilePath) + { + var fileNameMatch = LogFileDateRegex.Match(LogFilePath); + if (fileNameMatch.Success) + { + return new DateTime(int.Parse(fileNameMatch.Groups[1].Value), + int.Parse(fileNameMatch.Groups[2].Value), + int.Parse(fileNameMatch.Groups[3].Value)); + } + else + { + return null; + } + } + + private List> RelevantLogFiles(DiscoDataContext DiscoContext) + { + List> relevantFiles = new List>(); + var logDirectoryBase = LogContext.LogFileBasePath(DiscoContext); + var logDirectoryBaseInfo = new DirectoryInfo(logDirectoryBase); + var endDate = this.End.HasValue ? this.End.Value : DateTime.Now; + var endDateYear = endDate.Year.ToString(); + + // Try Shortcut ( < 31 Days in Query) + if (this.Start.HasValue) + { + if ((this.End.HasValue && this.End.Value.Subtract(this.Start.Value).Days < 31) || + (!this.End.HasValue && DateTime.Now.Subtract(this.Start.Value).Days < 31)) + { + // Less than 31 Days in Query - Just evaluate each Path + var queryDate = this.Start.Value.Date; + while (queryDate <= endDate) + { + var fileName = LogContext.LogFilePath(DiscoContext, queryDate, false); + if (File.Exists(fileName)) + relevantFiles.Add(new Tuple(fileName, LogFileDate(fileName).Value)); + + queryDate = queryDate.AddDays(1); + } + return relevantFiles; + } + } + + List logYears = new List(); + foreach (var directoryName in logDirectoryBaseInfo.GetDirectories()) + { + int directoryYear; + if (int.TryParse(directoryName.Name, out directoryYear)) + { + logYears.Add(directoryName.Name); + } + } + logYears.Sort(); + + foreach (var logYear in logYears) + { + List logFiles = Directory.EnumerateFiles(Path.Combine(logDirectoryBase, logYear), "DiscoLog_*.sdf").ToList(); + logFiles.Sort(); + if (logYear != endDateYear) + { + foreach (var logFile in logFiles) + { + relevantFiles.Add(new Tuple(logFile, LogFileDate(logFile).Value)); + } + } + else + { + foreach (var logFile in logFiles) + { + var fileNameDate = LogFileDate(logFile); + if (fileNameDate != null) + { + if (fileNameDate.Value < endDate) + { + relevantFiles.Add(new Tuple(logFile, fileNameDate.Value)); + } + else + { + break; // Files are sorted, must be no more... + } + } + } + break; // Years are sorted, must be no more... + } + } + return relevantFiles; + } + + private IQueryable BuildQuery(LogPersistContext LogContext, DateTime LogDate, int Taken) + { + IQueryable query = LogContext.Events.OrderByDescending(le => le.Timestamp); + if (this.Module.HasValue) + { + query = query.Where(le => le.ModuleId == this.Module.Value); + } + if (this.EventTypes != null && this.EventTypes.Count > 0) + { + query = query.Where(le => this.EventTypes.Contains(le.EventTypeId)); + } + if (this.Start.HasValue && this.Start.Value > LogDate) + { + var startValue = DateTime.SpecifyKind(this.Start.Value, DateTimeKind.Local); + query = query.Where(le => le.Timestamp > startValue); + } + if (this.End.HasValue && this.End.Value <= LogDate.AddDays(1)) + { + var endValue = DateTime.SpecifyKind(this.End.Value, DateTimeKind.Local); + query = query.Where(le => le.Timestamp < endValue); + } + if (this.Take.HasValue && this.Take.Value > 0) + { + var take = this.Take.Value - Taken; + query = query.Take(take); + } + return query; + } + + } +} diff --git a/Disco.Services/Logging/SystemLog.cs b/Disco.Services/Logging/SystemLog.cs new file mode 100644 index 00000000..f3efb7d0 --- /dev/null +++ b/Disco.Services/Logging/SystemLog.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Services.Logging.Models; + +namespace Disco.Services.Logging +{ + public class SystemLog : LogBase + { + private const int _ModuleId = 0; + public enum EventTypeIds : int + { + Information = 0, + Warning = 1, + Error = 2, + Exception = 10, + ExceptionWithInner = 11, + LogInitialized = 100, + Uninitialized = 200 + } + public static SystemLog Current + { + get + { + return (SystemLog)LogContext.LogModules[_ModuleId]; + } + } + private static void Log(EventTypeIds EventTypeId, params object[] Args) + { + Current.Log((int)EventTypeId, Args); + } + public static void LogInformation(params object[] Messages) + { + Log(EventTypeIds.Information, Messages); + } + public static void LogWarning(params object[] Messages) + { + Log(EventTypeIds.Warning, Messages); + } + public static void LogError(params object[] Messages) + { + Log(EventTypeIds.Error, Messages); + } + public static void LogException(string Component, Exception ex) + { + if (ex.InnerException != null) + { + Log(EventTypeIds.ExceptionWithInner, Component, ex.GetType().Name, ex.Message, ex.StackTrace, ex.InnerException.GetType().Name, ex.InnerException.Message, ex.InnerException.StackTrace); + } + else + { + Log(EventTypeIds.Exception, Component, ex.GetType().Name, ex.Message, ex.StackTrace); + } + } + + public static void LogLogInitialized(string PersistantStorePath) + { + Log(EventTypeIds.LogInitialized, PersistantStorePath); + } + + public static void LogUninitialized() + { + if (Current != null) + Log(EventTypeIds.Uninitialized); + } + + public override int ModuleId + { + get { return _ModuleId; } + } + + public override string ModuleName + { + get { return "System"; } + } + + public override string ModuleDescription + { + get { return "Core System Log"; } + } + + protected override List LoadEventTypes() + { + List eventTypes = new List() { + new LogEventType() { + Id = (int)EventTypeIds.Information, + ModuleId = _ModuleId, + Name = "Information", + Format = null, + Severity = (int)LogEventType.Severities.Information, + UseLive = true, UsePersist = true, UseDisplay = true }, + new LogEventType() { + Id = (int)EventTypeIds.Warning, + ModuleId = _ModuleId, + Name = "Warning", + Format = null, + Severity = (int)LogEventType.Severities.Warning, + UseLive = true, UsePersist = true, UseDisplay = true }, + new LogEventType() { + Id = (int)EventTypeIds.Error, + ModuleId = _ModuleId, + Name = "Error", + Format = null, + Severity = (int)LogEventType.Severities.Error, + UseLive = true, UsePersist = true, UseDisplay = true }, + new LogEventType() { + Id = (int)EventTypeIds.Exception, + ModuleId = _ModuleId, + Name = "Exception", + Format = "{0}; {1}: {2}; {3}", + Severity = (int)LogEventType.Severities.Error, + UseLive = true, UsePersist = true, UseDisplay = true }, + new LogEventType() { + Id = (int)EventTypeIds.ExceptionWithInner, + ModuleId = _ModuleId, + Name = "Exception with Inner Exception", + Format = "{0}; {1}: {2}; {3}; {4}: {5}; {6}", + Severity = (int)LogEventType.Severities.Error, + UseLive = true, UsePersist = true, UseDisplay = true }, + new LogEventType() { + Id = (int)EventTypeIds.LogInitialized, + ModuleId = _ModuleId, + Name = "Log Initialized", + Format = "Log Initialized to '{0}'", + Severity = (int)LogEventType.Severities.Information, + UseLive = false, UsePersist = true, UseDisplay = true }, + new LogEventType() { + Id = (int)EventTypeIds.Uninitialized, + ModuleId = _ModuleId, + Name = "Disco Uninitialized", + Format = "Disco Uninitialized", + Severity = (int)LogEventType.Severities.Information, + UseLive = false, UsePersist = true, UseDisplay = false } + }; + + return eventTypes; + } + } +} diff --git a/Disco.Services/Logging/Targets/LogLiveContext.cs b/Disco.Services/Logging/Targets/LogLiveContext.cs new file mode 100644 index 00000000..82476da4 --- /dev/null +++ b/Disco.Services/Logging/Targets/LogLiveContext.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using SignalR; +using SignalR.Hosting.AspNet; +using SignalR.Infrastructure; + +namespace Disco.Services.Logging.Targets +{ + public class LogLiveContext : PersistentConnection + { + + protected override System.Threading.Tasks.Task OnReceivedAsync(IRequest request, string connectionId, string data) + { + // Add to Group + if (!string.IsNullOrWhiteSpace(data) && data.StartsWith("/addToGroups:") && data.Length > 13) + { + var groups = data.Substring(13).Split(','); + foreach (var g in groups) + { + this.Groups.Add(connectionId, g); + } + } + + return base.OnReceivedAsync(request, connectionId, data); + } + + internal static void Broadcast(LogBase logModule, Models.LogEventType eventType, DateTime Timestamp, params object[] Arguments) + { + var message = Models.LogLiveEvent.Create(logModule, eventType, Timestamp, Arguments); + + var connectionManager = GlobalHost.ConnectionManager; + var connectionContext = connectionManager.GetConnectionContext(); + connectionContext.Groups.Send(_GroupNameAll, message); + connectionContext.Groups.Send(logModule.ModuleName, message); + } + + private const string _GroupNameAll = "__All"; + //private static string _QualifiedTypeName = typeof(LogLiveContext).FullName + "."; + //private static string _QualifiedTypeNameAll = _QualifiedTypeName + "__All"; + //private static string LiveLogNameGroup(string LogName) + //{ + // return string.Concat(_QualifiedTypeName, LogName); + //} + public static string LiveLogNameAll + { + get + { + //return _QualifiedTypeNameAll; + return _GroupNameAll; + } + } + + } +} diff --git a/Disco.Services/Logging/Targets/LogPersistContext.cs b/Disco.Services/Logging/Targets/LogPersistContext.cs new file mode 100644 index 00000000..df6c3067 --- /dev/null +++ b/Disco.Services/Logging/Targets/LogPersistContext.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Data.Entity; +using System.Data.Entity.Infrastructure; + +namespace Disco.Services.Logging.Targets +{ + public class LogPersistContext : DbContext + { + public LogPersistContext(string ConnectionString) : base(ConnectionString) { } + + public DbSet Modules { get; set; } + public DbSet EventTypes { get; set; } + public DbSet Events { get; set; } + + protected override void OnModelCreating(DbModelBuilder modelBuilder) + { + //modelBuilder.Conventions.Remove(); + } + } +} diff --git a/Disco.Services/Logging/Utilities.cs b/Disco.Services/Logging/Utilities.cs new file mode 100644 index 00000000..a0ebd40d --- /dev/null +++ b/Disco.Services/Logging/Utilities.cs @@ -0,0 +1,266 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Win32.SafeHandles; +using System.Runtime.InteropServices; +using System.IO; +using System.Web.Mvc; + +namespace Disco.Services.Logging +{ + public static class Utilities + { + + public const string LogEventCSVHeader = "Timestamp,ModuleId,ModuleName,ModuleDescription,EventTypeId,EventTypeName,Severity,Message"; + public static void ToCsvLine(this Models.LogLiveEvent e, TextWriter writer) + { + writer.Write(e.Timestamp.ToString("yyy-MM-dd HH:mm:ss")); + writer.Write(","); + writer.Write(e.ModuleId); + writer.Write(",\""); + writer.Write(e.ModuleName); + writer.Write("\",\""); + writer.Write(e.ModuleDescription); + writer.Write("\","); + writer.Write(e.EventTypeId); + writer.Write(",\""); + writer.Write(e.EventTypeName); + writer.Write("\","); + writer.Write(e.EventTypeSeverity); + writer.Write(",\""); + writer.Write(e.FormattedMessage.Replace("\"", "'")); + writer.Write("\""); + if (e.Arguments != null) + { + foreach (var arg in e.Arguments) + { + writer.Write(",\""); + if (arg == null) + writer.Write("null"); + else + writer.Write(arg.ToString().Replace("\"", "'")); + writer.Write("\""); + } + } + writer.WriteLine(); + } + public static MemoryStream ToCsv(this List e) + { + var ms = new MemoryStream(); + StreamWriter sw = new StreamWriter(ms); + sw.WriteLine(LogEventCSVHeader); + if (e != null) + { + foreach (var le in e) + { + le.ToCsvLine(sw); + } + } + sw.Flush(); + ms.Position = 0; + return ms; + } + + public static List ToSelectListItems(this List items) + { + return items.Select(et => new SelectListItem() { Value = et.Id.ToString(), Text = et.Name }).ToList(); + } + + #region Win32 APIs + /// + /// The CreateFile function creates or opens a file, file stream, directory, physical disk, volume, console buffer, tape drive, + /// communications resource, mailslot, or named pipe. The function returns a handle that can be used to access an object. + /// + /// + /// access to the object, which can be read, write, or both + /// The sharing mode of an object, which can be read, write, both, or none + /// A pointer to a SECURITY_ATTRIBUTES structure that determines whether or not the returned handle can + /// be inherited by child processes. Can be null + /// An action to take on files that exist and do not exist + /// The file attributes and flags. + /// A handle to a template file with the GENERIC_READ access right. The template file supplies file attributes + /// and extended attributes for the file that is being created. This parameter can be null + /// If the function succeeds, the return value is an open handle to a specified file. If a specified file exists before the function + /// all and dwCreationDisposition is CREATE_ALWAYS or OPEN_ALWAYS, a call to GetLastError returns ERROR_ALREADY_EXISTS, even when the function + /// succeeds. If a file does not exist before the call, GetLastError returns 0 (zero). + /// If the function fails, the return value is INVALID_HANDLE_VALUE. To get extended error information, call GetLastError. + /// + [DllImport("kernel32.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall, SetLastError = true)] + private static extern SafeFileHandle CreateFile( + string lpFileName, + EFileAccess dwDesiredAccess, + EFileShare dwShareMode, + IntPtr SecurityAttributes, + ECreationDisposition dwCreationDisposition, + EFileAttributes dwFlagsAndAttributes, + IntPtr hTemplateFile + ); + [Flags] + private enum EFileAccess : uint + { + Delete = 0x10000, + ReadControl = 0x20000, + WriteDAC = 0x40000, + WriteOwner = 0x80000, + Synchronize = 0x100000, + + StandardRightsRequired = 0xF0000, + StandardRightsRead = ReadControl, + StandardRightsWrite = ReadControl, + StandardRightsExecute = ReadControl, + StandardRightsAll = 0x1F0000, + SpecificRightsAll = 0xFFFF, + + AccessSystemSecurity = 0x1000000, // AccessSystemAcl access type + + MaximumAllowed = 0x2000000, // MaximumAllowed access type + + GenericRead = 0x80000000, + GenericWrite = 0x40000000, + GenericExecute = 0x20000000, + GenericAll = 0x10000000 + } + [Flags] + private enum EFileShare : uint + { + /// + /// + /// + None = 0x00000000, + /// + /// Enables subsequent open operations on an object to request read access. + /// Otherwise, other processes cannot open the object if they request read access. + /// If this flag is not specified, but the object has been opened for read access, the function fails. + /// + Read = 0x00000001, + /// + /// Enables subsequent open operations on an object to request write access. + /// Otherwise, other processes cannot open the object if they request write access. + /// If this flag is not specified, but the object has been opened for write access, the function fails. + /// + Write = 0x00000002, + /// + /// Enables subsequent open operations on an object to request delete access. + /// Otherwise, other processes cannot open the object if they request delete access. + /// If this flag is not specified, but the object has been opened for delete access, the function fails. + /// + Delete = 0x00000004 + } + private enum ECreationDisposition : uint + { + /// + /// Creates a new file. The function fails if a specified file exists. + /// + New = 1, + /// + /// Creates a new file, always. + /// If a file exists, the function overwrites the file, clears the existing attributes, combines the specified file attributes, + /// and flags with FILE_ATTRIBUTE_ARCHIVE, but does not set the security descriptor that the SECURITY_ATTRIBUTES structure specifies. + /// + CreateAlways = 2, + /// + /// Opens a file. The function fails if the file does not exist. + /// + OpenExisting = 3, + /// + /// Opens a file, always. + /// If a file does not exist, the function creates a file as if dwCreationDisposition is CREATE_NEW. + /// + OpenAlways = 4, + /// + /// Opens a file and truncates it so that its size is 0 (zero) bytes. The function fails if the file does not exist. + /// The calling process must open the file with the GENERIC_WRITE access right. + /// + TruncateExisting = 5 + } + [Flags] + private enum EFileAttributes : uint + { + None = 0x0000000, + Readonly = 0x00000001, + Hidden = 0x00000002, + System = 0x00000004, + Directory = 0x00000010, + Archive = 0x00000020, + Device = 0x00000040, + Normal = 0x00000080, + Temporary = 0x00000100, + SparseFile = 0x00000200, + ReparsePoint = 0x00000400, + Compressed = 0x00000800, + Offline = 0x00001000, + NotContentIndexed = 0x00002000, + Encrypted = 0x00004000, + Write_Through = 0x80000000, + Overlapped = 0x40000000, + NoBuffering = 0x20000000, + RandomAccess = 0x10000000, + SequentialScan = 0x08000000, + DeleteOnClose = 0x04000000, + BackupSemantics = 0x02000000, + PosixSemantics = 0x01000000, + OpenReparsePoint = 0x00200000, + OpenNoRecall = 0x00100000, + FirstPipeInstance = 0x00080000 + } + + private const int FSCTL_SET_COMPRESSION = 0x9C040; + private const short COMPRESSION_FORMAT_DEFAULT = 1; + [DllImport("kernel32.dll", SetLastError = true)] + private static extern int DeviceIoControl( + SafeFileHandle hDevice, + int dwIoControlCode, + ref short lpInBuffer, + int nInBufferSize, + IntPtr lpOutBuffer, + int nOutBufferSize, + ref int lpBytesReturned, + IntPtr lpOverlapped); + #endregion + + public static void CompressDirectory(string DirectoryPath) + { + if (DirectoryPath.Length > 250) + throw new InvalidOperationException(string.Format("Directory Path to Long (>250) to Compress: {0}", DirectoryPath)); + + DirectoryInfo dirInfo = new DirectoryInfo(DirectoryPath); + if (dirInfo.Exists) + { + if ((dirInfo.Attributes & FileAttributes.Compressed) != FileAttributes.Compressed) + { + var dirHandle = CreateFile(DirectoryPath, EFileAccess.GenericWrite, EFileShare.Read, IntPtr.Zero, ECreationDisposition.OpenExisting, EFileAttributes.None, IntPtr.Zero); + if (dirHandle.IsInvalid) + { + Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error()); + } + else + { + EnableCompression(dirHandle); + dirHandle.Close(); + } + } + } + else + { + throw new InvalidOperationException(string.Format("Directory doesn't exist: {0}", DirectoryPath)); + } + } + + private static void EnableCompression(SafeFileHandle handle) + { + int lpBytesReturned = 0; + short lpInBuffer = COMPRESSION_FORMAT_DEFAULT; + + int result = DeviceIoControl(handle, FSCTL_SET_COMPRESSION, + ref lpInBuffer, sizeof(short), IntPtr.Zero, 0, + ref lpBytesReturned, IntPtr.Zero); + + if (result != 0) + { + Marshal.ThrowExceptionForHR(Marshal.GetLastWin32Error()); + } + } + + } +} diff --git a/Disco.Services/Plugins/Features/CertificateProvider/CertificateProviderFeature.cs b/Disco.Services/Plugins/Features/CertificateProvider/CertificateProviderFeature.cs new file mode 100644 index 00000000..34ba14d5 --- /dev/null +++ b/Disco.Services/Plugins/Features/CertificateProvider/CertificateProviderFeature.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Disco.Data.Repository; +using Disco.Models.Repository; + +namespace Disco.Services.Plugins.Features.CertificateProvider +{ + [PluginFeatureCategory(DisplayName = "Certificate Providers")] + public abstract class CertificateProviderFeature : PluginFeature + { + // Certificate Plugin Requirements + public abstract string CertificateProviderId { get; } + public abstract Tuple> AllocateCertificate(DiscoDataContext dbContext, Device Device); + } +} diff --git a/Disco.Services/Plugins/Features/CertificateProvider/CertificateProviderLog.cs b/Disco.Services/Plugins/Features/CertificateProvider/CertificateProviderLog.cs new file mode 100644 index 00000000..81921cad --- /dev/null +++ b/Disco.Services/Plugins/Features/CertificateProvider/CertificateProviderLog.cs @@ -0,0 +1,304 @@ +using Disco.Services.Logging; +using Disco.Services.Logging.Models; +using System; +using System.Collections.Generic; +using System.Diagnostics; +namespace Disco.Services.Plugins.Features.CertificateProvider +{ + public class CertificateProvidersLog : LogBase + { + public enum EventTypeIds + { + RetrievalStarting = 10, + RetrievalProgress, + RetrievalFinished, + RetrievalWarning = 15, + RetrievalError, + RetrievalCertificateStarting = 20, + RetrievalCertificateFinished = 22, + RetrievalCertificateWarning = 25, + RetrievalCertificateError, + Allocated = 40, + AllocationFailed = 50 + } + private const int _ModuleId = 60; + private static bool _IsCertificateRetrievalProcessing; + private static string _CertificateRetrievalStatus; + private static int _CertificateRetrievalProgress; + public static CertificateProvidersLog Current + { + get + { + return (CertificateProvidersLog)LogContext.LogModules[60]; + } + } + public static bool IsCertificateRetrievalProcessing + { + get + { + return CertificateProvidersLog._IsCertificateRetrievalProcessing; + } + } + public override string ModuleDescription + { + get + { + return "Certificate Providers"; + } + } + public override int ModuleId + { + get + { + return 60; + } + } + public override string ModuleName + { + get + { + return "CertificateProviders"; + } + } + [System.Diagnostics.DebuggerNonUserCode] + public CertificateProvidersLog() + { + } + private static void Log(CertificateProvidersLog.EventTypeIds EventTypeId, params object[] Args) + { + CertificateProvidersLog.Current.Log((int)EventTypeId, Args); + } + public static void LogRetrievalStarting(int CertificateCount, int CertificateIdFrom, int CertificateIdTo) + { + CertificateProvidersLog.Log(CertificateProvidersLog.EventTypeIds.RetrievalStarting, new object[] + { + CertificateCount, + CertificateIdFrom, + CertificateIdTo + }); + } + public static void LogRetrievalFinished() + { + CertificateProvidersLog.Log(CertificateProvidersLog.EventTypeIds.RetrievalFinished, new object[0]); + } + public static void LogRetrievalWarning(string Message) + { + CertificateProvidersLog.Log(CertificateProvidersLog.EventTypeIds.RetrievalWarning, new object[] + { + Message + }); + } + public static void LogRetrievalError(string Message) + { + CertificateProvidersLog.Log(CertificateProvidersLog.EventTypeIds.RetrievalError, new object[] + { + Message + }); + } + public static void LogRetrievalCertificateStarting(string CertificateId) + { + CertificateProvidersLog.Log(CertificateProvidersLog.EventTypeIds.RetrievalCertificateStarting, new object[] + { + CertificateId + }); + } + public static void LogRetrievalCertificateFinished(string CertificateId) + { + CertificateProvidersLog.Log(CertificateProvidersLog.EventTypeIds.RetrievalCertificateFinished, new object[] + { + CertificateId + }); + } + public static void LogRetrievalCertificateWarning(string CertificateId, string Message) + { + CertificateProvidersLog.Log(CertificateProvidersLog.EventTypeIds.RetrievalCertificateWarning, new object[] + { + CertificateId, + Message + }); + } + public static void LogRetrievalCertificateError(string CertificateId, string Message) + { + CertificateProvidersLog.Log(CertificateProvidersLog.EventTypeIds.RetrievalCertificateError, new object[] + { + CertificateId, + Message + }); + } + public static void LogAllocated(string CertificateId, string DeviceSerialNumber) + { + CertificateProvidersLog.Log(CertificateProvidersLog.EventTypeIds.Allocated, new object[] + { + CertificateId, + DeviceSerialNumber + }); + } + public static void LogAllocationFailed(string DeviceSerialNumber) + { + CertificateProvidersLog.Log(CertificateProvidersLog.EventTypeIds.AllocationFailed, new object[] + { + DeviceSerialNumber + }); + } + public static void LogCertificateRetrievalProgress(bool? IsProcessing, int? Progress, string Status) + { + bool flag = IsProcessing.HasValue; + if (flag) + { + CertificateProvidersLog._IsCertificateRetrievalProcessing = IsProcessing.Value; + } + flag = CertificateProvidersLog._IsCertificateRetrievalProcessing; + if (flag) + { + bool flag2 = Status != null; + if (flag2) + { + CertificateProvidersLog._CertificateRetrievalStatus = Status; + } + flag2 = Progress.HasValue; + if (flag2) + { + CertificateProvidersLog._CertificateRetrievalProgress = Progress.Value; + } + } + else + { + CertificateProvidersLog._CertificateRetrievalStatus = null; + CertificateProvidersLog._CertificateRetrievalProgress = 0; + } + CertificateProvidersLog.Log(CertificateProvidersLog.EventTypeIds.RetrievalProgress, new object[] + { + CertificateProvidersLog._IsCertificateRetrievalProcessing, + CertificateProvidersLog._CertificateRetrievalProgress, + CertificateProvidersLog._CertificateRetrievalStatus + }); + } + protected override System.Collections.Generic.List LoadEventTypes() + { + return new System.Collections.Generic.List + { + new LogEventType + { + Id = 10, + ModuleId = 60, + Name = "Retrieval Starting", + Format = "Starting retrieval of {0} certificate/s ({1} to {2})", + Severity = 0, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 11, + ModuleId = 60, + Name = "Retrieval Progress", + Format = "Processing: {0}; {1}% Complete; Status: {2}", + Severity = 0, + UseLive = true, + UsePersist = false, + UseDisplay = false + }, + new LogEventType + { + Id = 12, + ModuleId = 60, + Name = "Retrieval Finished", + Format = "Retrieval of Certificates Complete", + Severity = 0, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 15, + ModuleId = 60, + Name = "Retrieval Warning", + Format = "Retrieval Warning: {0}", + Severity = 1, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 16, + ModuleId = 60, + Name = "Retrieval Error", + Format = "Retrieval Error: {0}", + Severity = 2, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 20, + ModuleId = 60, + Name = "Retrieval Certificate Starting", + Format = "Retrieving Certificate: {0}", + Severity = 0, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 22, + ModuleId = 60, + Name = "Retrieval Certificate Finished", + Format = "Certificate Retrieved: {0}", + Severity = 0, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 25, + ModuleId = 60, + Name = "Retrieval Certificate Warning", + Format = "{0} Certificate Warning: {1}", + Severity = 1, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 26, + ModuleId = 60, + Name = "Retrieval Certificate Error", + Format = "{0} Certificate Error: {1}", + Severity = 2, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 40, + ModuleId = 60, + Name = "Allocated", + Format = "Certificate {0} allocated to {1}", + Severity = 0, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 50, + ModuleId = 60, + Name = "Allocation Failed", + Format = "No certificates available for Device: {0}", + Severity = 2, + UseLive = true, + UsePersist = true, + UseDisplay = true + } + }; + } + } +} diff --git a/Disco.Services/Plugins/Features/InteroperabilityProvider/InteroperabilityProviderFeature.cs b/Disco.Services/Plugins/Features/InteroperabilityProvider/InteroperabilityProviderFeature.cs new file mode 100644 index 00000000..71357485 --- /dev/null +++ b/Disco.Services/Plugins/Features/InteroperabilityProvider/InteroperabilityProviderFeature.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Disco.Services.Plugins.Features.InteroperabilityProvider +{ + [PluginFeatureCategory(DisplayName = "Interoperability Providers")] + public abstract class InteroperabilityProviderFeature : PluginFeature + { + } +} diff --git a/Disco.Services/Plugins/Features/Other/OtherFeature.cs b/Disco.Services/Plugins/Features/Other/OtherFeature.cs new file mode 100644 index 00000000..fbbb18ca --- /dev/null +++ b/Disco.Services/Plugins/Features/Other/OtherFeature.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Disco.Services.Plugins.Features.Other +{ + [PluginFeatureCategory(DisplayName = "Other")] + public abstract class OtherFeature : PluginFeature + { + } +} diff --git a/Disco.Services/Plugins/Features/WarrantyProvider/WarrantyProviderFeature.cs b/Disco.Services/Plugins/Features/WarrantyProvider/WarrantyProviderFeature.cs new file mode 100644 index 00000000..f5998282 --- /dev/null +++ b/Disco.Services/Plugins/Features/WarrantyProvider/WarrantyProviderFeature.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Web.Mvc; +using Disco.Data.Repository; +using Disco.Models.BI.Config; +using Disco.Models.Repository; + +namespace Disco.Services.Plugins.Features.WarrantyProvider +{ + [PluginFeatureCategory(DisplayName = "Warranty Providers")] + public abstract class WarrantyProviderFeature : PluginFeature + { + // Warranty Plugin Requirements + public abstract string WarrantyProviderId { get; } + public abstract Type SubmitJobViewType { get; } + public abstract dynamic SubmitJobViewModel(DiscoDataContext dbContext, Controller controller, Job Job, OrganisationAddress Address, User TechUser); + public abstract Dictionary SubmitJobParseProperties(DiscoDataContext dbContext, FormCollection form, Controller controller, Job Job, OrganisationAddress Address, User TechUser, string FaultDescription); + public abstract Dictionary SubmitJobDiscloseInfo(DiscoDataContext dbContext, Job Job, OrganisationAddress Address, User TechUser, string FaultDescription, Dictionary WarrantyProviderProperties); + public abstract string SubmitJob(DiscoDataContext dbContext, Job Job, OrganisationAddress Address, User TechUser, string FaultDescription, Dictionary WarrantyProviderProperties); + + public abstract Type JobDetailsViewType { get; } + public bool JobDetailsSupported { get { return this.JobDetailsViewType != null; } } + public abstract dynamic JobDetailsViewModel(DiscoDataContext dbContext, Controller controller, Job Job); + + public static PluginFeatureManifest FindPluginFeature(string PluginIdOrWarrantyProviderId) + { + var defs = Plugins.GetPluginFeatures(typeof(WarrantyProviderFeature)); + var def = defs.FirstOrDefault(d => d.PluginManifest.Id.Equals(PluginIdOrWarrantyProviderId, StringComparison.InvariantCultureIgnoreCase)); + if (def != null) + return def; + else + foreach (var d in defs) + { + using (var providerInstance = d.CreateInstance()) + { + if (providerInstance.WarrantyProviderId != null && providerInstance.WarrantyProviderId.Equals(PluginIdOrWarrantyProviderId, StringComparison.InvariantCultureIgnoreCase)) + { + return d; + } + } + } + + return null; + } + } +} diff --git a/Disco.Services/Plugins/Features/WarrantyProvider/WarrantyProviderSubmitJobException.cs b/Disco.Services/Plugins/Features/WarrantyProvider/WarrantyProviderSubmitJobException.cs new file mode 100644 index 00000000..46c55206 --- /dev/null +++ b/Disco.Services/Plugins/Features/WarrantyProvider/WarrantyProviderSubmitJobException.cs @@ -0,0 +1,12 @@ +using System; + +namespace Disco.Services.Plugins.Features.WarrantyProvider +{ + public class WarrantyProviderSubmitJobException : Exception + { + public WarrantyProviderSubmitJobException(string Message) + : base(Message) + { + } + } +} diff --git a/Disco.Services/Plugins/InvalidFeatureCategoryTypeException.cs b/Disco.Services/Plugins/InvalidFeatureCategoryTypeException.cs new file mode 100644 index 00000000..e5b8efe7 --- /dev/null +++ b/Disco.Services/Plugins/InvalidFeatureCategoryTypeException.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Disco.Services.Plugins +{ + public class InvalidFeatureCategoryTypeException : Exception + { + private string _pluginRequested; + private Type _categoryType; + + public string PluginRequested + { + get + { + return _pluginRequested; + } + } + public Type CategoryType + { + get + { + return _categoryType; + } + } + + public InvalidFeatureCategoryTypeException(Type CategoryType) + : this(CategoryType, null) + { + } + public InvalidFeatureCategoryTypeException(Type CategoryType, string PluginRequested) + { + this._categoryType = CategoryType; + this._pluginRequested = PluginRequested; + } + + public override string Message + { + get + { + if (string.IsNullOrEmpty(_pluginRequested)) + return string.Format("Invalid Category Type [{0}]", _categoryType.Name); + else + return string.Format("Plugin [{1}] is not of the correct Category Type [{0}]", _categoryType.Name, _pluginRequested); + } + } + + } +} diff --git a/Disco.Services/Plugins/Plugin.cs b/Disco.Services/Plugins/Plugin.cs new file mode 100644 index 00000000..9a12f7bc --- /dev/null +++ b/Disco.Services/Plugins/Plugin.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Disco.Data.Repository; + +namespace Disco.Services.Plugins +{ + public abstract class Plugin : IDisposable + { + public PluginManifest Manifest {get; internal set;} + + #region Lifecycle + public abstract bool Install(DiscoDataContext dbContext); + public abstract bool Initalize(DiscoDataContext dbContext); + public abstract bool Uninstall(DiscoDataContext dbContext); + public abstract bool BeforeUpdate(DiscoDataContext dbContext, PluginManifest updateManifest); + #endregion + + public virtual void Dispose() + { + // Nothing in Base Class + } + + public override sealed string ToString() + { + return string.Format("{0} ({1}) - v{2}", this.Manifest.Name, this.Manifest.Id, this.Manifest.Version.ToString(4)); + } + } +} diff --git a/Disco.Services/Plugins/PluginAttribute.cs b/Disco.Services/Plugins/PluginAttribute.cs new file mode 100644 index 00000000..f9cbf4bc --- /dev/null +++ b/Disco.Services/Plugins/PluginAttribute.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Disco.Services.Plugins +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public class PluginAttribute : Attribute + { + public string Id { get; set; } + public string Name { get; set; } + public string Author { get; set; } + } +} diff --git a/Disco.Services/Plugins/PluginConfigurationHandler.cs b/Disco.Services/Plugins/PluginConfigurationHandler.cs new file mode 100644 index 00000000..3341c4db --- /dev/null +++ b/Disco.Services/Plugins/PluginConfigurationHandler.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Web.Mvc; +using Disco.Data.Repository; + +namespace Disco.Services.Plugins +{ + public abstract class PluginConfigurationHandler : IDisposable + { + public PluginManifest Manifest { get; set; } + + public abstract PluginConfigurationHandlerGetResponse Get(DiscoDataContext dbContext, Controller controller); + public abstract bool Post(DiscoDataContext dbContext, FormCollection form, Controller controller); + + public virtual void Dispose() + { + // Nothing in Base Class + } + + protected PluginConfigurationHandlerGetResponse GetResponse(Type ViewType, dynamic ViewModel = null) + { + return new PluginConfigurationHandlerGetResponse(this.Manifest, ViewType, ViewModel); + } + public class PluginConfigurationHandlerGetResponse + { + public PluginManifest Manifest { get; set; } + public Type ViewType { get; set; } + public dynamic ViewModel { get; set; } + + public PluginConfigurationHandlerGetResponse(PluginManifest Manifest, Type ViewType, dynamic ViewModel = null) + { + if (ViewType == null) + throw new ArgumentNullException("ViewType"); + if (!typeof(WebViewPage).IsAssignableFrom(ViewType)) + throw new ArgumentException("The PluginConfigurationHandler ViewType must inherit System.Web.Mvc.WebViewPage", "ViewType"); + + this.Manifest = Manifest; + + this.ViewType = ViewType; + this.ViewModel = ViewModel; + } + } + } +} diff --git a/Disco.Services/Plugins/PluginExtensions.cs b/Disco.Services/Plugins/PluginExtensions.cs new file mode 100644 index 00000000..85f378fe --- /dev/null +++ b/Disco.Services/Plugins/PluginExtensions.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Data.Repository; +using System.IO; +using System.Web.Mvc; +using System.Web.Routing; +using System.Web; +using System.Web.Mvc.Html; +using System.Globalization; + +namespace Disco.Services.Plugins +{ + public static class PluginExtensions + { + #region Model Binding from Controller + public static bool TryUpdateModel(this Controller controller, TModel model) where TModel : class + { + return controller.TryUpdateModel(model, null, controller.ValueProvider); + } + public static bool TryUpdateModel(this Controller controller, TModel model, IValueProvider valueProvider) where TModel : class + { + return controller.TryUpdateModel(model, null, valueProvider); + } + public static bool TryUpdateModel(this Controller controller, TModel model, string prefix) where TModel : class + { + return controller.TryUpdateModel(model, prefix, controller.ValueProvider); + } + public static bool TryUpdateModel(this Controller controller, TModel model, string prefix, IValueProvider valueProvider) where TModel : class + { + if (model == null) + throw new ArgumentNullException("model"); + if (valueProvider == null) + throw new ArgumentNullException("valueProvider"); + + Predicate predicate = propertyName => true; + IModelBinder binder = ModelBinders.Binders.GetBinder(typeof(TModel)); + + ModelBindingContext context2 = new ModelBindingContext + { + ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, typeof(TModel)), + ModelName = prefix, + ModelState = controller.ModelState, + PropertyFilter = predicate, + ValueProvider = valueProvider + }; + + ModelBindingContext bindingContext = context2; + + binder.BindModel(controller.ControllerContext, bindingContext); + + return controller.ModelState.IsValid; + } + #endregion + + #region Virtual Directories + //public static string WebHandlerResource(this PluginManifest pluginManifest, string resourcePath, RequestContext requestContext) + //{ + // var rootPath = WebHandlerRootUrl(pluginManifest, requestContext); + // return string.Concat(rootPath, resourcePath); + //} + //public static string WebHandlerRootUrl(this PluginManifest pluginManifest, RequestContext requestContext) + //{ + // var tempPath = pluginManifest.WebHandlerActionUrl(requestContext, "_"); + // return tempPath.Substring(0, tempPath.LastIndexOf(@"/") + 1); + //} + //public static string WebHandlerActionUrl(this PluginManifest pluginManifest, RequestContext requestContext, string PluginAction) + //{ + // var routeValues = new RouteValueDictionary(new { PluginId = pluginManifest.Id, PluginAction = PluginAction }); + // return UrlHelper.GenerateUrl("Plugin", "PluginWebHandler", "Index", routeValues, RouteTable.Routes, requestContext, true); + //} + //public static string WebHandlerResourceUrl(this PluginManifest pluginManifest, RequestContext requestContext, string PluginAction) + //{ + // var routeValues = new RouteValueDictionary(new { PluginId = pluginManifest.Id, PluginAction = PluginAction }); + + + + // return UrlHelper.GenerateUrl("Plugin", "PluginWebHandler", "Index", routeValues, RouteTable.Routes, requestContext, true); + //} + + public static HtmlString DiscoPluginResourceUrl(this WebViewPage ViewPage, string Resource) + { + return ViewPage.DiscoPluginResourceUrl(Resource, false); + } + public static HtmlString DiscoPluginResourceUrl(this WebViewPage ViewPage, string Resource, bool Download) + { + if (string.IsNullOrEmpty(Resource)) + throw new ArgumentNullException("Resource"); + + // Find Plugin + var pageType = ViewPage.GetType(); + var pageAssembly = pageType.Assembly; + var manifest = Plugins.GetPlugin(pageAssembly); + + var resourcePath = manifest.WebResourcePath(Resource); + + var routeValues = new RouteValueDictionary(new { PluginId = manifest.Id, res = Resource }); + string pluginActionUrl = UrlHelper.GenerateUrl("Plugin_Resources", null, null, routeValues, RouteTable.Routes, ViewPage.ViewContext.RequestContext, false); + + pluginActionUrl += string.Format("?v={0}", resourcePath.Item2); + + if (Download) + pluginActionUrl += "&Download=true"; + + return new HtmlString(pluginActionUrl); + } + public static HtmlString DiscoPluginActionUrl(this WebViewPage ViewPage, string PluginAction) + { + if (string.IsNullOrEmpty(PluginAction)) + throw new ArgumentNullException("PluginAction"); + + // Find Plugin + var pageType = ViewPage.GetType(); + var pageAssembly = pageType.Assembly; + var manifest = Plugins.GetPlugin(pageAssembly); + + var routeValues = new RouteValueDictionary(new { PluginId = manifest.Id, PluginAction = PluginAction }); + string pluginActionUrl = UrlHelper.GenerateUrl("Plugin", null, null, routeValues, RouteTable.Routes, ViewPage.ViewContext.RequestContext, false); + return new HtmlString(pluginActionUrl); + } + public static HtmlString DiscoPluginConfigureUrl(this WebViewPage ViewPage) + { + // Find Plugin + var pageType = ViewPage.GetType(); + var pageAssembly = pageType.Assembly; + var manifest = Plugins.GetPlugin(pageAssembly); + + var routeValues = new RouteValueDictionary(new { PluginId = manifest.Id }); + string pluginActionUrl = UrlHelper.GenerateUrl("Config_Plugins_Configure", null, null, routeValues, RouteTable.Routes, ViewPage.ViewContext.RequestContext, false); + return new HtmlString(pluginActionUrl); + } + public static MvcForm DiscoPluginActionBeginForm(this WebViewPage ViewPage, string PluginAction, FormMethod method, IDictionary htmlAttributes) + { + if (string.IsNullOrEmpty(PluginAction)) + throw new ArgumentNullException("PluginAction"); + + // Find Plugin + var pageType = ViewPage.GetType(); + var pageAssembly = pageType.Assembly; + var manifest = Plugins.GetPlugin(pageAssembly); + + var routeValues = new RouteValueDictionary(new { PluginId = manifest.Id, PluginAction = PluginAction }); + string pluginActionUrl = UrlHelper.GenerateUrl("Plugin", null, null, routeValues, RouteTable.Routes, ViewPage.ViewContext.RequestContext, false); + + return ViewPage.FormHelper(pluginActionUrl, method, htmlAttributes); + } + public static MvcForm DiscoPluginActionBeginForm(this WebViewPage ViewPage, string PluginAction, FormMethod method) + { + return ViewPage.DiscoPluginActionBeginForm(PluginAction, method, null); + } + public static MvcForm DiscoPluginActionBeginForm(this WebViewPage ViewPage, string PluginAction, IDictionary htmlAttributes) + { + return ViewPage.DiscoPluginActionBeginForm(PluginAction, FormMethod.Post, htmlAttributes); + } + public static MvcForm DiscoPluginActionBeginForm(this WebViewPage ViewPage, string PluginAction) + { + return ViewPage.DiscoPluginActionBeginForm(PluginAction, FormMethod.Post, null); + } + + private static MvcForm FormHelper(this WebViewPage ViewPage, string formAction, FormMethod method, IDictionary htmlAttributes) + { + TagBuilder builder = new TagBuilder("form"); + builder.MergeAttributes(htmlAttributes); + builder.MergeAttribute("action", formAction); + builder.MergeAttribute("method", HtmlHelper.GetFormMethodString(method), true); + bool flag = ViewPage.ViewContext.ClientValidationEnabled && !ViewPage.ViewContext.UnobtrusiveJavaScriptEnabled; + if (flag) + { + object obj2 = ViewPage.ViewContext.HttpContext.Items["DiscoPluginLastFormNum"]; + int num = (obj2 != null) ? (((int)obj2) + 1) : 1000; + ViewPage.ViewContext.HttpContext.Items["DiscoPluginLastFormNum"] = num; + + builder.GenerateId(string.Format(CultureInfo.InvariantCulture, "form{0}", new object[] { num })); + } + ViewPage.ViewContext.Writer.Write(builder.ToString(TagRenderMode.StartTag)); + MvcForm form = new MvcForm(ViewPage.ViewContext); + if (flag) + { + ViewPage.ViewContext.FormContext.FormId = builder.Attributes["id"]; + } + return form; + } + + + + + #endregion + } +} diff --git a/Disco.Services/Plugins/PluginFeature.cs b/Disco.Services/Plugins/PluginFeature.cs new file mode 100644 index 00000000..68d8b2f9 --- /dev/null +++ b/Disco.Services/Plugins/PluginFeature.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Disco.Data.Repository; + +namespace Disco.Services.Plugins +{ + public abstract class PluginFeature : IDisposable + { + public PluginFeatureManifest Manifest {get; internal set;} + + public abstract bool Initalize(DiscoDataContext dbContext); + + public virtual void Dispose() + { + // Nothing in Base Class + } + } +} diff --git a/Disco.Services/Plugins/PluginFeatureAttribute.cs b/Disco.Services/Plugins/PluginFeatureAttribute.cs new file mode 100644 index 00000000..f41d0c95 --- /dev/null +++ b/Disco.Services/Plugins/PluginFeatureAttribute.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Disco.Services.Plugins +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public class PluginFeatureAttribute : Attribute + { + public string Id { get; set; } + public string Name { get; set; } + } +} diff --git a/Disco.Services/Plugins/PluginFeatureCategoryAttribute.cs b/Disco.Services/Plugins/PluginFeatureCategoryAttribute.cs new file mode 100644 index 00000000..6358bfb4 --- /dev/null +++ b/Disco.Services/Plugins/PluginFeatureCategoryAttribute.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Disco.Services.Plugins +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + public class PluginFeatureCategoryAttribute : Attribute + { + public string DisplayName { get; set; } + } +} diff --git a/Disco.Services/Plugins/PluginFeatureManifest.cs b/Disco.Services/Plugins/PluginFeatureManifest.cs new file mode 100644 index 00000000..b5d55082 --- /dev/null +++ b/Disco.Services/Plugins/PluginFeatureManifest.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Disco.Data.Repository; +using Newtonsoft.Json; +using System.Reflection; + +namespace Disco.Services.Plugins +{ + public class PluginFeatureManifest + { + public string Id { get; set; } + public string Name { get; set; } + public string TypeName { get; set; } + + [JsonProperty] + private string CategoryTypeName { get; set; } + + [JsonIgnore] + public PluginManifest PluginManifest { get; private set; } + [JsonIgnore] + internal Type Type { get; private set; } + [JsonIgnore] + public Type CategoryType { get; private set; } + + internal bool Initialize(DiscoDataContext dbContext, PluginManifest pluginManifest) + { + this.PluginManifest = pluginManifest; + + if (this.Type == null) + this.Type = this.PluginManifest.PluginAssembly.GetType(this.TypeName, true, true); + + if (this.CategoryType == null) + this.CategoryType = Type.GetType(this.CategoryTypeName, true, true); + + using (var instance = this.CreateInstance()) + { + instance.Initalize(dbContext); + } + + PluginsLog.LogInitializedPluginFeature(this.PluginManifest, this); + + return true; + } + + public PluginFeature CreateInstance() + { + var i = (PluginFeature)Activator.CreateInstance(Type); + i.Manifest = this; + return i; + } + public CategoryType CreateInstance() where CategoryType : PluginFeature + { + if (typeof(CategoryType).IsAssignableFrom(this.Type)) + { + var i = (CategoryType)Activator.CreateInstance(Type); + i.Manifest = this; + return i; + } + else + throw new InvalidOperationException(string.Format("The feature [{0}] cannot be cast into type [{1}]", this.Type.Name, typeof(CategoryType).Name)); + } + + /// + /// Uses reflection to build a Plugin Manifest + /// + /// Assembly containing a plugin + /// A plugin manifest for the first encountered plugin within the assembly + public static PluginFeatureManifest FromPluginFeatureType(Type featureType, PluginManifest pluginManifest) + { + var featureAttribute = (PluginFeatureAttribute)featureType.GetCustomAttributes(typeof(PluginFeatureAttribute), false).FirstOrDefault(); + + if (featureAttribute == null) + throw new ArgumentException(string.Format("Plugin Feature found [{0}], but no PluginFeatureAttribute found", featureType.Name), "featureType"); + + var featureId = featureAttribute.Id; + var featureName = featureAttribute.Name; + + // Determine Feature Category + var featureCategoryType = featureType.BaseType; + + if (featureCategoryType == null) + throw new ArgumentException(string.Format("Plugin Feature found [{0}], but has no Base Type to determine its Category", featureType.Name), "featureType"); + + if (featureCategoryType == typeof(PluginFeature) || !typeof(PluginFeature).IsAssignableFrom(featureCategoryType)) + throw new ArgumentException(string.Format("Plugin Feature found [{0}], but its Base Type is not a valid Feature Category Type (Base Feature or not assignable)", featureType.Name), "featureType"); + + var featureCategoryAttribute = (PluginFeatureCategoryAttribute)featureCategoryType.GetCustomAttributes(typeof(PluginFeatureCategoryAttribute), false).FirstOrDefault(); + + if (featureCategoryAttribute == null) + throw new ArgumentException(string.Format("Plugin Feature found [{0}], but its Base Type is not a valid Feature Category Type (no attribute)", featureType.Name), "featureType"); + + return new PluginFeatureManifest() + { + PluginManifest = pluginManifest, + Id = featureId, + Name = featureName, + Type = featureType, + TypeName = featureType.FullName, + CategoryType = featureCategoryType, + CategoryTypeName = featureCategoryType.FullName + }; + } + } +} diff --git a/Disco.Services/Plugins/PluginManifest.cs b/Disco.Services/Plugins/PluginManifest.cs new file mode 100644 index 00000000..719535fc --- /dev/null +++ b/Disco.Services/Plugins/PluginManifest.cs @@ -0,0 +1,321 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Security.Cryptography; +using System.Text; +using System.Threading.Tasks; +using System.Web; +using System.Web.Mvc; +using Disco.Data.Repository; +using Newtonsoft.Json; + +namespace Disco.Services.Plugins +{ + public class PluginManifest + { + public string Id { get; set; } + public string Name { get; set; } + public string Author { get; set; } + public Version Version { get; set; } + [JsonProperty] + internal string AssemblyPath { get; set; } + [JsonProperty] + private string TypeName { get; set; } + [JsonProperty] + private string ConfigurationHandlerTypeName { get; set; } + [JsonProperty] + private string WebHandlerTypeName { get; set; } + + [JsonProperty] + internal Dictionary AssemblyReferences { get; set; } + + [JsonProperty] + public List Features { get; private set; } + + [JsonIgnore] + internal Assembly PluginAssembly { get; private set; } + [JsonIgnore] + internal Type Type { get; private set; } + [JsonIgnore] + private Type ConfigurationHandlerType { get; set; } + [JsonIgnore] + private Type WebHandlerType { get; set; } + + [JsonIgnore] + public string PluginLocation { get; private set; } + [JsonIgnore] + public string StorageLocation { get; private set; } + + private static Dictionary> WebResourceHashes = new Dictionary>(); + + public List GetFeatures(Type FeatureCategoryType) + { + return this.Features.Where(fm => fm.CategoryType.IsAssignableFrom(FeatureCategoryType)).ToList(); + } + public PluginFeatureManifest GetFeature(string PluginFeatureId) + { + return this.Features.Where(fm => fm.Id == PluginFeatureId).FirstOrDefault(); + } + + public Plugin CreateInstance() + { + var i = (Plugin)Activator.CreateInstance(Type); + i.Manifest = this; + return i; + } + + /// + /// Deserializes a Json Manifest + /// + /// Path to the Json Manifest file + /// + public static PluginManifest FromPluginManifestFile(string FilePath) + { + using (Stream manifestStream = File.OpenRead(FilePath)) + { + PluginManifest manifest = FromPluginManifestFile(manifestStream, Path.GetDirectoryName(FilePath)); + return manifest; + } + } + + /// + /// Deserializes a Json Manifest + /// + /// Stream containing the encoded Json Manifest File + /// PluginLocation to be set in the manifest + /// + public static PluginManifest FromPluginManifestFile(Stream FileStream, string PluginLocation = null) + { + string manifestString; + using (StreamReader manifestStreamReader = new StreamReader(FileStream)) + { + manifestString = manifestStreamReader.ReadToEnd(); + } + + var manifest = JsonConvert.DeserializeObject(manifestString); + + manifest.PluginLocation = PluginLocation; + + return manifest; + } + /// + /// Uses reflection to build a Plugin Manifest + /// + /// Assembly containing a plugin + /// A plugin manifest for the first encountered plugin within the assembly + public static PluginManifest FromPluginAssembly(Assembly assembly) + { + // Determine Plugin Properties + var pluginType = (from type in assembly.GetTypes() + where typeof(Plugin).IsAssignableFrom(type) && !type.IsAbstract + select type).FirstOrDefault(); + + if (pluginType == null) + throw new ArgumentException("No Plugin was found in this Assembly", "pluginAssembly"); + + var assemblyName = assembly.GetName(); + + var pluginAttributes = pluginType.GetCustomAttribute(false); + + if (pluginAttributes == null) + throw new ArgumentException(string.Format("Plugin found [{0}], but no PluginAttribute found", pluginType.Name), "pluginAssembly"); + + var pluginId = pluginAttributes.Id; + var pluginName = pluginAttributes.Name; + var pluginAuthor = pluginAttributes.Author; + + var pluginVersion = assemblyName.Version; + var pluginAssemblyPath = Path.GetFileName(assembly.Location); + var pluginTypeName = pluginType.FullName; + var pluginLocation = Path.GetDirectoryName(assembly.Location); + + // Find Configuration Handler + var pluginConfigurationHandlerType = (from type in assembly.GetTypes() + where typeof(PluginConfigurationHandler).IsAssignableFrom(type) && !type.IsAbstract + select type).FirstOrDefault(); + if (pluginConfigurationHandlerType == null) + throw new ArgumentException("A Plugin was found, but no Configuration Handler was found in this Assembly - this is required", "pluginAssembly"); + + // Find Web Handler + var pluginWebHandlerType = (from type in assembly.GetTypes() + where typeof(PluginWebHandler).IsAssignableFrom(type) && !type.IsAbstract + select type).FirstOrDefault(); + + Dictionary pluginAssemblyReferences = new Dictionary(); + foreach (string referenceFilename in Directory.EnumerateFiles(pluginLocation, "*.dll", SearchOption.TopDirectoryOnly)) + { + if (!referenceFilename.Equals(assembly.Location, StringComparison.InvariantCultureIgnoreCase)) + { + try + { + Assembly pluginRefAssembly = Assembly.ReflectionOnlyLoadFrom(referenceFilename); + pluginAssemblyReferences[pluginRefAssembly.FullName] = referenceFilename.Substring(pluginLocation.Length + 1); + } + catch (Exception) { } // Ignore Load Exceptions + } + } + + PluginManifest pluginManifest = new PluginManifest() + { + Id = pluginId, + Name = pluginName, + Author = pluginAuthor, + Version = pluginVersion, + AssemblyPath = pluginAssemblyPath, + TypeName = pluginTypeName, + AssemblyReferences = pluginAssemblyReferences, + PluginAssembly = assembly, + Type = pluginType, + PluginLocation = pluginLocation, + ConfigurationHandlerType = pluginConfigurationHandlerType, + ConfigurationHandlerTypeName = pluginConfigurationHandlerType.FullName, + WebHandlerType = pluginWebHandlerType, + WebHandlerTypeName = (pluginWebHandlerType == null ? null : pluginWebHandlerType.FullName) + }; + + pluginManifest.Features = (from type in assembly.GetTypes() + where typeof(PluginFeature).IsAssignableFrom(type) && !type.IsAbstract + select PluginFeatureManifest.FromPluginFeatureType(type, pluginManifest)).ToList(); + + return pluginManifest; + } + + public string ToManifestFile() + { + return JsonConvert.SerializeObject(this, Formatting.Indented); + } + public bool InitializePlugin(DiscoDataContext dbContext) + { + var assemblyFullPath = Path.Combine(this.PluginLocation, this.AssemblyPath); + + if (!File.Exists(assemblyFullPath)) + throw new FileNotFoundException(string.Format("Plugin Assembly [{0}] not found at: {1}", this.Id, assemblyFullPath), assemblyFullPath); + + if (this.PluginAssembly == null) + this.PluginAssembly = Assembly.LoadFile(assemblyFullPath); + + if (this.PluginAssembly == null) + throw new InvalidOperationException(string.Format("Unable to load Plugin Assembly [{0}] at: {1}", this.Id, assemblyFullPath)); + + PluginsLog.LogInitializingPluginAssembly(this.PluginAssembly); + + // Check Manifest/Assembly Versions Match + if (this.Version != this.PluginAssembly.GetName().Version) + throw new InvalidOperationException(string.Format("The plugin [{0}] manifest version [{1}] doesn't match the plugin assembly [{2} : {3}]", this.Id, this.Version, assemblyFullPath, this.PluginAssembly.GetName().Version)); + + if (this.Type == null) + this.Type = this.PluginAssembly.GetType(this.TypeName, true, true); + + if (this.ConfigurationHandlerType == null) + this.ConfigurationHandlerType = this.PluginAssembly.GetType(this.ConfigurationHandlerTypeName, true, true); + + if (!string.IsNullOrEmpty(this.WebHandlerTypeName) && this.WebHandlerType == null) + this.WebHandlerType = this.PluginAssembly.GetType(this.WebHandlerTypeName, true, true); + + // Update non-static values + this.StorageLocation = Path.Combine(dbContext.DiscoConfiguration.PluginStorageLocation, this.Id); + + // Initialize Plugin + using (var pluginInstance = this.CreateInstance()) + { + pluginInstance.Initalize(dbContext); + } + PluginsLog.LogInitializedPlugin(this); + + // Initialize Plugin Features + if (Features != null) + { + foreach (var feature in Features) + { + feature.Initialize(dbContext, this); + } + } + else + { + Features = new List(); + } + + return true; + } + + public PluginConfigurationHandler CreateConfigurationHandler() + { + // Configuration Handler is Required + if (this.ConfigurationHandlerType == null) + throw new ArgumentNullException("ConfigurationType"); + if (!typeof(PluginConfigurationHandler).IsAssignableFrom(this.ConfigurationHandlerType)) + throw new ArgumentException("The Plugin ConfigurationHandlerType must inherit Disco.Services.Plugins.PluginConfigurationHandler", "ConfigurationHandlerType"); + + var handler = (PluginConfigurationHandler)Activator.CreateInstance(this.ConfigurationHandlerType); + + handler.Manifest = this; + + return handler; + } + [JsonIgnore] + public bool HasWebHandler + { + get + { + return this.WebHandlerType != null; + } + } + public PluginWebHandler CreateWebHandler(Controller HostController) + { + // Web Handler is Not Required + if (this.WebHandlerType == null) + return null; + + if (!typeof(PluginWebHandler).IsAssignableFrom(this.WebHandlerType)) + throw new ArgumentException("The Plugin WebHandlerType must inherit Disco.Services.Plugins.PluginWebHandler", "WebHandlerType"); + + var handler = (PluginWebHandler)Activator.CreateInstance(this.WebHandlerType); + + handler.Manifest = this; + handler.HostController = HostController; + + return handler; + } + + public Tuple WebResourcePath(string Resource) + { + if (string.IsNullOrWhiteSpace(Resource)) + throw new ArgumentNullException("Resource"); + + if (Resource.Contains("..")) + throw new ArgumentException("Resource Paths cannot navigate to the parent", "Resource"); + + var resourcePath = Path.Combine(this.PluginLocation, "WebResources", Resource.Replace(@"/", @"\")); + + Tuple resourceHash; + string resourceKey = string.Format("{0}://{1}", this.Name, Resource); + if (WebResourceHashes.TryGetValue(resourceKey, out resourceHash)) + { +#if DEBUG + var fileDateCheck = System.IO.File.GetLastWriteTime(resourcePath); + if (fileDateCheck == resourceHash.Item2) +#endif + return new Tuple(resourcePath, resourceHash.Item1); + } + + if (!File.Exists(resourcePath)) + throw new FileNotFoundException(string.Format("Resource [{0}] not found", Resource), resourcePath); + + var fileDate = System.IO.File.GetLastWriteTime(resourcePath); + var fileBytes = System.IO.File.ReadAllBytes(resourcePath); + if (fileBytes.Length > 0) + { + using (SHA256 sha = SHA256.Create()) + { + byte[] hash = sha.ComputeHash(fileBytes); + resourceHash = new Tuple(HttpServerUtility.UrlTokenEncode(hash), fileDate); + } + } + WebResourceHashes[resourceKey] = resourceHash; + + return new Tuple(resourcePath, resourceHash.Item1); + } + } +} diff --git a/Disco.Services/Plugins/PluginWebHandler.cs b/Disco.Services/Plugins/PluginWebHandler.cs new file mode 100644 index 00000000..903c6755 --- /dev/null +++ b/Disco.Services/Plugins/PluginWebHandler.cs @@ -0,0 +1,229 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Web.Mvc; +using System.Web.Routing; +using RazorGenerator.Mvc; + +namespace Disco.Services.Plugins +{ + public abstract class PluginWebHandler : IDisposable + { + public PluginManifest Manifest { get; set; } + public Controller HostController { get; set; } + + public abstract ActionResult ExecuteAction(string ActionName); + + public virtual void Dispose() + { + // Nothing in Base Class + } + + + #region Action Results + + #region Compiled View + private static string[] _viewFileNames = new string[] { "cshtml" }; + public ActionResult CompiledView(Type CompiledViewType, object Model, bool UseDiscoLayout) + { + string layoutPath = UseDiscoLayout ? "~/Views/Shared/_Layout.cshtml" : null; + + IView v = new PrecompiledMvcView(this.HostController.Request.Path, layoutPath, CompiledViewType, false, _viewFileNames); + + if (Model != null) + this.HostController.ViewData.Model = Model; + + return new ViewResult { View = v, ViewData = this.HostController.ViewData, TempData = this.HostController.TempData }; + } + public ActionResult CompiledView(Type CompiledViewType, bool UseDiscoLayout) + { + return this.CompiledView(CompiledViewType, null, UseDiscoLayout); + } + public ActionResult CompiledView(Type CompiledViewType, object Model) + { + return this.CompiledView(CompiledViewType, Model, true); + } + public ActionResult CompiledView(Type CompiledViewType) + { + return this.CompiledView(CompiledViewType, false, true); + } + public ActionResult CompiledPartialView(Type PartialCompiledViewType, object Model) + { + IView v = new PrecompiledMvcView(this.HostController.Request.Path, PartialCompiledViewType, false, _viewFileNames); + + if (Model != null) + this.HostController.ViewData.Model = Model; + + return new PartialViewResult { View = v, ViewData = this.HostController.ViewData, TempData = this.HostController.TempData }; + } + public ActionResult CompiledPartialView(Type PartialCompiledViewType) + { + return this.CompiledView(PartialCompiledViewType, null); + } + #endregion + + #region Content + public ActionResult Content(string content, string contentType, Encoding contentEncoding) + { + return new ContentResult { Content = content, ContentType = contentType, ContentEncoding = contentEncoding }; + } + public ActionResult Content(string content, string contentType) + { + return this.Content(content, null, null); + } + public ActionResult Content(string content) + { + return this.Content(content, null); + } + #endregion + + #region Json + public ActionResult Json(object data, JsonRequestBehavior behavior) + { + return new JsonResult { Data = data, ContentType = null, ContentEncoding = null, JsonRequestBehavior = behavior }; + } + #endregion + + #region File + public ActionResult File(Stream fileStream, string contentType) + { + return this.File(fileStream, contentType, null); + } + public ActionResult File(Stream fileStream, string contentType, string fileDownloadName) + { + return new FileStreamResult(fileStream, contentType) { FileDownloadName = fileDownloadName }; + } + public ActionResult File(byte[] fileContents, string contentType) + { + return this.File(fileContents, contentType, null); + } + public ActionResult File(byte[] fileContents, string contentType, string fileDownloadName) + { + return new FileContentResult(fileContents, contentType) { FileDownloadName = fileDownloadName }; + } + #endregion + + #region HttpNotFound + public ActionResult HttpNotFound(string statusDescription) + { + return new HttpNotFoundResult(statusDescription); + } + public ActionResult HttpNotFound() + { + return this.HttpNotFound(null); + } + #endregion + + #region Redirect + public ActionResult RedirectToScheduledTaskStatus(string SessionId) + { + if (string.IsNullOrEmpty(SessionId)) + throw new ArgumentNullException(SessionId); + + return this.RedirectToAction("TaskStatus", "Logging", "Config", new { id = SessionId }); + } + public ActionResult Redirect(string url) + { + if (string.IsNullOrEmpty(url)) + throw new ArgumentNullException("url"); + + return new RedirectResult(url); + } + public ActionResult RedirectPermanent(string url) + { + if (string.IsNullOrEmpty(url)) + throw new ArgumentNullException("url"); + + return new RedirectResult(url, true); + } + public ActionResult RedirectToPluginAction(string PluginAction) + { + if (string.IsNullOrEmpty(PluginAction)) + throw new ArgumentNullException("PluginAction"); + + var routeValues = new RouteValueDictionary(new { PluginId = this.Manifest.Id, PluginAction = PluginAction }); + string pluginActionUrl = UrlHelper.GenerateUrl("Plugin", null, null, routeValues, RouteTable.Routes, this.HostController.Request.RequestContext, false); + + return new RedirectResult(pluginActionUrl, false); + } + public ActionResult RedirectToPluginResource(string Resource, bool? Download) + { + var resourcePath = this.Manifest.WebResourcePath(Resource); + + var routeValues = new RouteValueDictionary(new { PluginId = this.Manifest.Id, res = Resource }); + string pluginActionUrl = UrlHelper.GenerateUrl("Plugin_Resources", null, null, routeValues, RouteTable.Routes, this.HostController.Request.RequestContext, false); + + pluginActionUrl += string.Format("?v={0}", resourcePath.Item2); + + if (Download.HasValue && Download.Value) + { + pluginActionUrl += "&Download=true"; + } + + return new RedirectResult(pluginActionUrl, false); + } + public ActionResult RedirectToPluginResource(string Resource) + { + return this.RedirectToPluginResource(Resource, null); + } + public ActionResult RedirectToRoute(string routeName, object routeValues) + { + RouteValueDictionary routeValueDictionary; + if (routeValues != null) + routeValueDictionary = new RouteValueDictionary(routeValues); + else + routeValueDictionary = new RouteValueDictionary(); + + return new RedirectToRouteResult(routeName, routeValueDictionary); + } + public ActionResult RedirectToRoute(string routeName) + { + return this.RedirectToRoute(routeName, null); + } + public ActionResult RedirectToAction(string actionName, string controller, string areaName, object routeValues) + { + RouteValueDictionary routeValueDictionary; + if (routeValues != null) + routeValueDictionary = new RouteValueDictionary(routeValues); + else + routeValueDictionary = new RouteValueDictionary(); + + routeValueDictionary["action"] = actionName; + routeValueDictionary["controller"] = controller; + if (areaName != null) + routeValueDictionary["area"] = areaName; + + return new RedirectToRouteResult(routeValueDictionary); + } + public ActionResult RedirectToAction(string actionName, string controller, string areaName) + { + return this.RedirectToAction(actionName, controller, areaName, null); + } + public ActionResult RedirectToAction(string actionName, string controller, object routeValues) + { + return this.RedirectToAction(actionName, controller, null, routeValues); + } + public ActionResult RedirectToAction(string actionName, string controller) + { + return this.RedirectToAction(actionName, controller, null, null); + } + public ActionResult RedirectToDiscoJob(int jobId) + { + return this.RedirectToAction("Show", "Job", null, new { id = jobId.ToString() }); + } + public ActionResult RedirectToDiscoDevice(string DeviceSerialNumber) + { + return this.RedirectToAction("Show", "Device", null, new { id = DeviceSerialNumber }); + } + public ActionResult RedirectToDiscoUser(string UserId) + { + return this.RedirectToAction("Show", "User", null, new { id = UserId }); + } + #endregion + + #endregion + } +} diff --git a/Disco.Services/Plugins/PluginWebHandlerController.cs b/Disco.Services/Plugins/PluginWebHandlerController.cs new file mode 100644 index 00000000..b2be9fe4 --- /dev/null +++ b/Disco.Services/Plugins/PluginWebHandlerController.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Web.Mvc; + +namespace Disco.Services.Plugins +{ + public abstract class PluginWebHandlerController : PluginWebHandler + { + + public override ActionResult ExecuteAction(string ActionName) + { + var handlerType = this.GetType(); + var methodDescriptor = FindControllerMethod(handlerType, ActionName); + + if (methodDescriptor == null) + return this.HttpNotFound("Unknown Plugin Method"); + + var methodParams = BuildMethodParameters(handlerType, methodDescriptor.MethodInfo, ActionName, this.HostController); + + return (ActionResult)methodDescriptor.MethodInfo.Invoke(this, methodParams); + } + + private static WebHandlerCachedItem FindControllerMethod(Type Handler, string ActionName) + { + var descriptors = CacheWebHandler(Handler); + WebHandlerCachedItem method; + if (descriptors.TryGetValue(ActionName.ToLower(), out method)) + return method; // Not Found + else + return null; // Not Found + } + private static object[] BuildMethodParameters(Type Handler, MethodInfo methodInfo, string ActionName, Controller HostController) + { + var methodParams = methodInfo.GetParameters(); + var result = new object[methodParams.Length]; + + for (int i = 0; i < methodParams.Length; i++) + { + var methodParam = methodParams[i]; + + Type parameterType = methodParam.ParameterType; + IModelBinder modelBinder = ModelBinders.Binders.GetBinder(parameterType); + IValueProvider valueProvider = HostController.ValueProvider; + string parameterName = methodParam.Name; + + ModelBindingContext bindingContext = new ModelBindingContext() + { + FallbackToEmptyPrefix = true, + ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, parameterType), + ModelName = parameterName, + ModelState = HostController.ViewData.ModelState, + PropertyFilter = (p) => true, + ValueProvider = valueProvider + }; + + var parameterValue = modelBinder.BindModel(HostController.ControllerContext, bindingContext); + + if (parameterValue == null && methodParam.HasDefaultValue) + parameterValue = methodParam.DefaultValue; + + result[i] = parameterValue; + + //var paramInstance = Activator.CreateInstance(methodParam.ParameterType); + + //IModelBinder binder = ModelBinders.Binders.GetBinder(methodParam.ParameterType); + //ModelBindingContext bindingContext = new ModelBindingContext + //{ + // ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => paramInstance, methodParam.ParameterType), + // ModelName = methodParam.Name, + // ModelState = HostController.ModelState, + // PropertyFilter = (p) => true, + // ValueProvider = HostController.ValueProvider + //}; + //binder.BindModel(HostController.ControllerContext, bindingContext); + + //if (methodParam.HasDefaultValue && paramInstance == null) + // paramInstance = methodParam.DefaultValue; + + //result[i] = paramInstance; + } + + return result; + } + + #region Method Cache + private static Dictionary> WebHandlerCachedItems = new Dictionary>(); + private static Dictionary CacheWebHandler(Type Handler) + { + Dictionary result; + + if (!WebHandlerCachedItems.TryGetValue(Handler, out result)) + { + // Cache Miss + result = new Dictionary(); + var methods = Array.FindAll(Handler.GetMethods(BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly), mi => { return !mi.IsSpecialName && typeof(ActionResult).IsAssignableFrom(mi.ReturnType); }); + foreach (var method in methods) + { + var item = new WebHandlerCachedItem() + { + Method = method.Name, + MethodInfo = method + }; + result.Add(item.Method.ToLower(), item); + } + WebHandlerCachedItems[Handler] = result; + } + + return result; + } + private class WebHandlerCachedItem + { + public string Method { get; set; } + public MethodInfo MethodInfo { get; set; } + } + #endregion + } +} diff --git a/Disco.Services/Plugins/Plugins.cs b/Disco.Services/Plugins/Plugins.cs new file mode 100644 index 00000000..027dfe46 --- /dev/null +++ b/Disco.Services/Plugins/Plugins.cs @@ -0,0 +1,363 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Disco.Data.Repository; +using System.IO.Compression; + +namespace Disco.Services.Plugins +{ + public static class Plugins + { + private static Dictionary _PluginAssemblyManifests; + private static Dictionary _PluginManifests; + internal static Dictionary FeatureCategoryDisplayNames; + + private static object _PluginLock = new object(); + public static string PluginPath { get; private set; } + + public static PluginManifest GetPlugin(string PluginId, Type ContainsCategoryType) + { + if (_PluginManifests == null) + throw new InvalidOperationException("Plugins have not been initialized"); + + PluginManifest manifest; + if (_PluginManifests.TryGetValue(PluginId, out manifest)) + { + if (ContainsCategoryType == null) + return manifest; + else + { + foreach (var featureManifest in manifest.Features) + { + if (ContainsCategoryType.IsAssignableFrom(featureManifest.CategoryType)) + return manifest; + } + + throw new InvalidFeatureCategoryTypeException(ContainsCategoryType, PluginId); + } + } + else + { + throw new UnknownPluginException(PluginId); + } + } + public static PluginManifest GetPlugin(string PluginId) + { + return GetPlugin(PluginId, null); + } + public static PluginManifest GetPlugin(Assembly PluginAssembly) + { + if (_PluginAssemblyManifests == null) + throw new InvalidOperationException("Plugins have not been initialized"); + + PluginManifest manifest; + if (_PluginAssemblyManifests.TryGetValue(PluginAssembly, out manifest)) + { + return manifest; + } + else + { + throw new UnknownPluginException(PluginAssembly.FullName); + } + } + public static List GetPlugins() + { + if (_PluginManifests == null) + throw new InvalidOperationException("Plugins have not been initialized"); + + return _PluginManifests.Values.ToList(); + } + + public static PluginFeatureManifest GetPluginFeature(string PluginFeatureId, Type CategoryType) + { + if (_PluginManifests == null) + throw new InvalidOperationException("Plugins have not been initialized"); + + var featureManifest = _PluginManifests.Values.SelectMany(pm => pm.Features).Where(fm => fm.Id == PluginFeatureId).FirstOrDefault(); + + if (featureManifest == null) + throw new UnknownPluginException(PluginFeatureId, "Unknown Feature"); + + if (CategoryType == null) + return featureManifest; + else + if (CategoryType.IsAssignableFrom(featureManifest.CategoryType)) + return featureManifest; + else + throw new InvalidFeatureCategoryTypeException(CategoryType, PluginFeatureId); + } + public static PluginFeatureManifest GetPluginFeature(string PluginFeatureId) + { + return GetPluginFeature(PluginFeatureId, null); + } + public static List GetPluginFeatures(Type FeatureCategoryType) + { + if (_PluginManifests == null) + throw new InvalidOperationException("Plugins have not been initialized"); + + return _PluginManifests.Values.SelectMany(pm => pm.Features).Where(fm => fm.CategoryType.IsAssignableFrom(FeatureCategoryType)).OrderBy(fm => fm.PluginManifest.Name).ToList(); + } + public static List GetPluginFeatures() + { + if (_PluginManifests == null) + throw new InvalidOperationException("Plugins have not been initialized"); + + return _PluginManifests.Values.SelectMany(pm => pm.Features).ToList(); + } + + + public static string PluginFeatureCategoryDisplayName(Type FeatureCategoryType) + { + if (FeatureCategoryType == null) + throw new ArgumentNullException("FeatureType"); + + string displayName; + if (FeatureCategoryDisplayNames.TryGetValue(FeatureCategoryType, out displayName)) + return displayName; + else + throw new InvalidOperationException(string.Format("Unknown Plugin Feature Category Type: [{0}]", FeatureCategoryType.Name)); + } + + public static void InitalizePlugins(DiscoDataContext dbContext) + { + if (_PluginManifests == null) + { + lock (_PluginLock) + { + if (_PluginManifests == null) + { + Dictionary loadedPlugins = new Dictionary(); + + PluginPath = dbContext.DiscoConfiguration.PluginsLocation; + + AppDomain appDomain = AppDomain.CurrentDomain; + + // Subscribe to Assembly Resolving + appDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; + + DirectoryInfo pluginDirectoryRoot = new DirectoryInfo(PluginPath); + if (pluginDirectoryRoot.Exists) + { + foreach (DirectoryInfo pluginDirectory in pluginDirectoryRoot.EnumerateDirectories()) + { + string pluginManifestFilename = Path.Combine(pluginDirectory.FullName, "manifest.json"); + if (File.Exists(pluginManifestFilename)) + { + PluginManifest pluginManifest = null; + try + { + pluginManifest = PluginManifest.FromPluginManifestFile(pluginManifestFilename); + + if (pluginManifest != null) + { + if (loadedPlugins.ContainsKey(pluginManifest.Id)) + throw new InvalidOperationException(string.Format("The plugin [{0}] is already initialized", pluginManifest.Id)); + + pluginManifest.InitializePlugin(dbContext); + + loadedPlugins[pluginManifest.Id] = pluginManifest; + } + } + catch (Exception ex) { PluginsLog.LogInitializeException(pluginManifestFilename, ex); } + } + } + } + + _PluginManifests = loadedPlugins; + + ReinitializePluginEnvironment(); + + // Install Plugins - TEMPORARY? Workaround until UI in place? Or Useful for 'built-in' plugins? + if (pluginDirectoryRoot.Exists) + { + foreach (FileInfo pluginPackageFile in pluginDirectoryRoot.EnumerateFiles("*.discoPlugin", SearchOption.TopDirectoryOnly)) + { + // Install Plugin + InstallPlugin(dbContext, pluginPackageFile.FullName); + // Delete Package File + pluginPackageFile.Delete(); + } + } + } + } + } + } + + private static void ReinitializePluginEnvironment() + { + FeatureCategoryDisplayNames = InitializeFeatureCategoryDetails(_PluginManifests.Values); + _PluginAssemblyManifests = _PluginManifests.Values.ToDictionary(p => p.PluginAssembly, p => p); + } + + public static void InstallPlugin(DiscoDataContext dbContext, String PackageFilePath) + { + using (var packageStream = File.OpenRead(PackageFilePath)) + { + InstallPlugin(dbContext, packageStream); + } + } + + public static void InstallPlugin(DiscoDataContext dbContext, Stream PluginPackage) + { + if (_PluginManifests == null) + throw new InvalidOperationException("Plugins have not been initialized"); + + using (MemoryStream packageStream = new MemoryStream()) + { + PluginPackage.CopyTo(packageStream); + packageStream.Position = 0; + + using (ZipArchive packageArchive = new ZipArchive(packageStream, ZipArchiveMode.Read, false)) + { + + ZipArchiveEntry packageManifestEntry = packageArchive.GetEntry("manifest.json"); + if (packageManifestEntry == null) + throw new InvalidDataException("The plugin package does not contain the 'manifest.json' entry"); + + PluginManifest packageManifest; + + using (Stream packageManifestStream = packageManifestEntry.Open()) + { + packageManifest = PluginManifest.FromPluginManifestFile(packageManifestStream); + } + + lock (_PluginLock) + { + if (_PluginManifests == null) + throw new InvalidOperationException("Plugins have not been initialized"); + + // Ensure not already installed + if (_PluginManifests.ContainsKey(packageManifest.Id)) + throw new InvalidOperationException(string.Format("The '{0} [{1}]' Plugin is already installed, please uninstall any existing versions before trying again", packageManifest.Name, packageManifest.Id)); + + string packagePath = Path.Combine(dbContext.DiscoConfiguration.PluginsLocation, packageManifest.Id); + + // Force Delete of Existing Folder + if (Directory.Exists(packagePath)) + Directory.Delete(packagePath, true); + + Directory.CreateDirectory(packagePath); + + // Extract Package Contents + foreach (var packageEntry in packageArchive.Entries) + { + // Determine Extraction Path + var packageEntryTarget = Path.Combine(packagePath, packageEntry.FullName); + + // Create Sub Directories + Directory.CreateDirectory(Path.GetDirectoryName(packageEntryTarget)); + + using (var packageEntryStream = packageEntry.Open()) + { + using (var packageTargetStream = File.Open(packageEntryTarget, FileMode.Create, FileAccess.Write, FileShare.None)) + { + packageEntryStream.CopyTo(packageTargetStream); + } + } + } + + // Reload Manifest + packageManifest = PluginManifest.FromPluginManifestFile(Path.Combine(packagePath, "manifest.json")); + + // Initialize Plugin + packageManifest.InitializePlugin(dbContext); + + // Add Plugin Manifest to Environment + _PluginManifests[packageManifest.Id] = packageManifest; + + // Reinitialize Plugin Environment + ReinitializePluginEnvironment(); + } + + } + } + } + + private static Dictionary InitializeFeatureCategoryDetails(IEnumerable pluginManifests) + { + Dictionary categoryDisplayNames = new Dictionary(); + + // Always add 'Other' + var otherFeatureType = typeof(Features.Other.OtherFeature); + categoryDisplayNames.Add(otherFeatureType, ((PluginFeatureCategoryAttribute)otherFeatureType.GetCustomAttributes(typeof(PluginFeatureCategoryAttribute), false).FirstOrDefault()).DisplayName); + + foreach (var pluginManifest in pluginManifests) + { + foreach (var featureManifest in pluginManifest.Features) + { + if (!categoryDisplayNames.ContainsKey(featureManifest.CategoryType)) + { + string displayName = null; + + var displayAttributes = featureManifest.CategoryType.GetCustomAttributes(typeof(PluginFeatureCategoryAttribute), true); + if (displayAttributes != null && displayAttributes.Length > 0) + displayName = ((PluginFeatureCategoryAttribute)(displayAttributes[0])).DisplayName; + + if (string.IsNullOrWhiteSpace(displayName)) + displayName = featureManifest.CategoryType.Name; + + categoryDisplayNames[featureManifest.CategoryType] = displayName; + } + } + } + return categoryDisplayNames; + } + + #region Plugin Referenced Assemblies Resolving + + public static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) + { + if (args.RequestingAssembly.Location.StartsWith(PluginPath, StringComparison.InvariantCultureIgnoreCase) && _PluginManifests != null) + { + // Try best guess first + PluginManifest requestingPlugin = _PluginManifests.Values.Where(p => p.Type.Assembly == args.RequestingAssembly).FirstOrDefault(); + if (requestingPlugin != null) + { + Assembly loadedAssembly = CurrentDomain_AssemblyResolve_ByPlugin(requestingPlugin, args); + if (loadedAssembly != null) + return loadedAssembly; + } + + // Try all Plugin References + foreach (var pluginDef in _PluginManifests.Values) + { + Assembly loadedAssembly = CurrentDomain_AssemblyResolve_ByPlugin(pluginDef, args); + if (loadedAssembly != null) + return loadedAssembly; + } + } + return null; + } + private static Assembly CurrentDomain_AssemblyResolve_ByPlugin(PluginManifest pluginManifest, ResolveEventArgs args) + { + if (pluginManifest.AssemblyReferences != null) + { + string assemblyPath; + if (pluginManifest.AssemblyReferences.TryGetValue(args.Name, out assemblyPath)) + { + var resolvedAssemblyPath = Path.Combine(pluginManifest.PluginLocation, assemblyPath); + + try + { + Assembly loadedAssembly = Assembly.LoadFile(resolvedAssemblyPath); + + PluginsLog.LogPluginReferenceAssemblyLoaded(args.Name, resolvedAssemblyPath, args.RequestingAssembly.FullName); + + return loadedAssembly; + } + catch (Exception ex) + { + PluginsLog.LogPluginException(string.Format("Resolving Plugin Reference Assembly: '{0}' [{1}]; Requested by: '{2}' [{3}]; Disco.Plugins.DiscoPlugins.CurrentDomain_AssemblyResolve()", args.Name, resolvedAssemblyPath, args.RequestingAssembly.FullName, args.RequestingAssembly.Location), ex); + } + } + } + return null; + } + + #endregion + } +} diff --git a/Disco.Services/Plugins/PluginsLog.cs b/Disco.Services/Plugins/PluginsLog.cs new file mode 100644 index 00000000..895ed691 --- /dev/null +++ b/Disco.Services/Plugins/PluginsLog.cs @@ -0,0 +1,275 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Services.Logging; +using Disco.Services.Logging.Models; +using System.Reflection; + +namespace Disco.Services.Plugins +{ + public class PluginsLog : LogBase + { + private const int _ModuleId = 10; + + public override string ModuleDescription { get { return "Plugins"; } } + public override int ModuleId { get { return _ModuleId; } } + public override string ModuleName { get { return "Plugins"; } } + + public enum EventTypeIds + { + InitializingPlugins = 10, + InitializingPluginAssembly, + InitializedPlugin, + InitializedPluginFeature, + InitializeWarning = 15, + InitializeError, + InitializeException, + InitializeExceptionWithInner, + PluginException = 20, + PluginExceptionWithInner, + PluginReferenceAssemblyLoaded = 50, + PluginConfigurationLoaded = 100, + PluginConfigurationSaved = 104, + PluginWebControllerAccessed = 200 + } + + public static PluginsLog Current + { + get + { + return (PluginsLog)LogContext.LogModules[_ModuleId]; + } + } + private static void Log(EventTypeIds EventTypeId, params object[] Args) + { + Current.Log((int)EventTypeId, Args); + } + + public static void LogInitializingPlugins(string PluginDirectory) + { + Current.Log((int)EventTypeIds.InitializingPlugins, PluginDirectory); + } + public static void LogInitializingPluginAssembly(Assembly PluginAssembly) + { + Current.Log((int)EventTypeIds.InitializingPluginAssembly, PluginAssembly.FullName, PluginAssembly.Location); + } + public static void LogInitializedPlugin(PluginManifest Menifest) + { + Current.Log((int)EventTypeIds.InitializedPlugin, Menifest.Id, Menifest.Version.ToString(3), Menifest.Type.Name, Menifest.Type.Assembly.Location); + } + public static void LogInitializedPluginFeature(PluginManifest PluginMenifest, PluginFeatureManifest FeatureManifest) + { + Current.Log((int)EventTypeIds.InitializedPluginFeature, PluginMenifest.Id, FeatureManifest.Type.Name); + } + public static void LogInitializeWarning(string Warning) + { + Current.Log((int)EventTypeIds.InitializeWarning, Warning); + } + public static void LogInitializeError(string Error) + { + Current.Log((int)EventTypeIds.InitializeError, Error); + } + public static void LogPluginReferenceAssemblyLoaded(string AssemblyFullName, string AssemblyPath, string RequestedBy) + { + Current.Log((int)EventTypeIds.PluginReferenceAssemblyLoaded, AssemblyFullName, AssemblyPath, RequestedBy); + } + public static void LogPluginConfigurationLoaded(string PluginId, string UserId) + { + Current.Log((int)EventTypeIds.PluginConfigurationLoaded, PluginId, UserId); + } + public static void LogPluginConfigurationSaved(string PluginId, string UserId) + { + Current.Log((int)EventTypeIds.PluginConfigurationSaved, PluginId, UserId); + } + public static void LogPluginWebControllerAccessed(string PluginId, string PluginAction, string UserId) + { + Current.Log((int)EventTypeIds.PluginWebControllerAccessed, PluginId, PluginAction, UserId); + } + + public static void LogInitializeException(string PluginFilename, Exception ex) + { + if (ex.InnerException != null) + { + Log(EventTypeIds.InitializeExceptionWithInner, PluginFilename, ex.GetType().Name, ex.Message, ex.StackTrace, ex.InnerException.GetType().Name, ex.InnerException.Message, ex.InnerException.StackTrace); + } + else + { + Log(EventTypeIds.InitializeException, PluginFilename, ex.GetType().Name, ex.Message, ex.StackTrace); + } + } + + public static void LogPluginException(string Component, Exception ex) + { + if (ex.InnerException != null) + { + Log(EventTypeIds.PluginExceptionWithInner, Component, ex.GetType().Name, ex.Message, ex.StackTrace, ex.InnerException.GetType().Name, ex.InnerException.Message, ex.InnerException.StackTrace); + } + else + { + Log(EventTypeIds.PluginException, Component, ex.GetType().Name, ex.Message, ex.StackTrace); + } + } + + protected override List LoadEventTypes() + { + return new System.Collections.Generic.List + { + new LogEventType + { + Id = (int)EventTypeIds.InitializingPlugins, + ModuleId = _ModuleId, + Name = "Initializing Plugins", + Format = "Starting plugin discovery and initialization from: {0}", + Severity = (int)LogEventType.Severities.Information, + UseLive = false, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.InitializingPluginAssembly, + ModuleId = _ModuleId, + Name = "Initializing Plugin Assembly", + Format = "Initializing Plugin Assembly: [{0}] From '{1}'", + Severity = (int)LogEventType.Severities.Information, + UseLive = false, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.InitializedPlugin, + ModuleId = _ModuleId, + Name = "Initialized Plugin", + Format = "Initialized Plugin: '{0} (v{1})' [{2}] From '{3}'", + Severity = (int)LogEventType.Severities.Information, + UseLive = false, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.InitializedPluginFeature, + ModuleId = _ModuleId, + Name = "Initialized Plugin Feature", + Format = "Initialized Plugin Feature: '{1}' From '{0}'", + Severity = (int)LogEventType.Severities.Information, + UseLive = false, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.InitializeWarning, + ModuleId = _ModuleId, + Name = "Initialize Warning", + Format = "Initialize Warning: {0}", + Severity = (int)LogEventType.Severities.Warning, + UseLive = false, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.InitializeError, + ModuleId = _ModuleId, + Name = "Initialize Error", + Format = "Initialize Error: {0}", + Severity = (int)LogEventType.Severities.Error, + UseLive = false, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.InitializeException, + ModuleId = _ModuleId, + Name = "Initialize Exception", + Format = "Exception: {0}; {1}: {2}; {3}", + Severity = (int)LogEventType.Severities.Error, + UseLive = false, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.InitializeExceptionWithInner, + ModuleId = _ModuleId, + Name = "Initialize Exception with Inner Exception", + Format = "Exception: {0}; {1}: {2}; {3}; Inner: {4}: {5}; {6}", + Severity = (int)LogEventType.Severities.Error, + UseLive = false, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.PluginException, + ModuleId = _ModuleId, + Name = "Plugin Exception", + Format = "Exception: {0}; {1}: {2}; {3}", + Severity = (int)LogEventType.Severities.Error, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.PluginExceptionWithInner, + ModuleId = _ModuleId, + Name = "Plugin Exception with Inner Exception", + Format = "Exception: {0}; {1}: {2}; {3}; Inner: {4}: {5}; {6}", + Severity = (int)LogEventType.Severities.Error, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.PluginReferenceAssemblyLoaded, + ModuleId = _ModuleId, + Name = "Plugin Reference Assembly Loaded", + Format = "Loaded Plugin Reference Assembly: [{0}] From: '{1}'; Requested by: [{2}]", + Severity = (int)LogEventType.Severities.Information, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.PluginConfigurationLoaded, + ModuleId = _ModuleId, + Name = "Plugin Configuration Loaded", + Format = "Plugin Configuration Loaded: [{0}] by [{1}]", + Severity = (int)LogEventType.Severities.Information, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.PluginConfigurationSaved, + ModuleId = _ModuleId, + Name = "Plugin Configuration Saved", + Format = "Plugin Configuration Saved: [{0}] by [{1}]", + Severity = (int)LogEventType.Severities.Information, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.PluginWebControllerAccessed, + ModuleId = _ModuleId, + Name = "Plugin Web Controller Accessed", + Format = "Plugin Web Controller Accessed: Plugin [{0}], Action [{1}], By [{2}]", + Severity = (int)LogEventType.Severities.Information, + UseLive = true, + UsePersist = true, + UseDisplay = true + } + }; + } + } +} diff --git a/Disco.Services/Plugins/UnknownPluginException.cs b/Disco.Services/Plugins/UnknownPluginException.cs new file mode 100644 index 00000000..1b1da494 --- /dev/null +++ b/Disco.Services/Plugins/UnknownPluginException.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Disco.Services.Plugins +{ + public class UnknownPluginException : Exception + { + private string _pluginRequested; + + public string PluginRequested + { + get + { + return _pluginRequested; + } + } + + public UnknownPluginException(string PluginRequested) + { + this._pluginRequested = PluginRequested; + } + public UnknownPluginException(string PluginRequested, string Message) : base(Message) + { + this._pluginRequested = PluginRequested; + } + + public override string Message + { + get + { + return string.Format("Unknown Plugin Id: [{0}]", _pluginRequested); + } + } + } +} diff --git a/Disco.Services/Properties/AssemblyInfo.cs b/Disco.Services/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..3c1813ce --- /dev/null +++ b/Disco.Services/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("Disco.Services")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Disco.Services")] +[assembly: AssemblyCopyright("")] +[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("3f4b432d-2abd-4bc2-86cd-f90650b73930")] + +// 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.2.0131.2002")] +[assembly: AssemblyFileVersion("1.2.0131.2002")] diff --git a/Disco.Services/Tasks/ScheduledTask.cs b/Disco.Services/Tasks/ScheduledTask.cs new file mode 100644 index 00000000..86bbb932 --- /dev/null +++ b/Disco.Services/Tasks/ScheduledTask.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Quartz; +using Disco.Data.Repository; + +namespace Disco.Services.Tasks +{ + public abstract class ScheduledTask : IJob + { + public abstract void InitalizeScheduledTask(DiscoDataContext dbContext); + + internal protected ScheduledTaskStatus Status { get; private set; } + internal protected IJobExecutionContext ExecutionContext { get; private set; } + + public abstract bool CancelInitiallySupported { get; } + public abstract bool SingleInstanceTask { get; } + public virtual bool IsSilent { get { return false; } } + public virtual bool LogExceptionsOnly { get { return false; } } + public abstract string TaskName { get; } + protected abstract void ExecuteTask(); + + #region Protected Triggers + /// + /// Schedules the Task to Begin Immediately + /// + protected ScheduledTaskStatus ScheduleTask() + { + return ScheduleTask(null, null); + } + /// + /// Schedules the Task to Begin Immediately + /// + /// DataMap passed into the executing Task + /// + protected ScheduledTaskStatus ScheduleTask(JobDataMap DataMap) + { + return ScheduleTask(null, DataMap); + } + /// + /// Schedules the Task to Begin based on the Trigger + /// + /// Trigger for the Task + protected ScheduledTaskStatus ScheduleTask(TriggerBuilder Trigger) + { + return ScheduleTask(Trigger, null); + } + /// + /// Schedules the Task to Begin based on the Trigger including the DataMap + /// + /// Trigger for the Task + /// DataMap passed into the executing Task + /// + protected ScheduledTaskStatus ScheduleTask(TriggerBuilder Trigger, JobDataMap DataMap) + { + if (Trigger == null) + Trigger = TriggerBuilder.Create(); // Defaults to Start Immediately + + if (DataMap != null) + Trigger = Trigger.UsingJobData(DataMap); + + return ScheduledTasks.RegisterTask(this, Trigger); + } + #endregion + + public void Execute(IJobExecutionContext context) + { + // Task Status + this.ExecutionContext = context; + this.Status = context.GetDiscoScheduledTaskStatus(); + if (this.Status == null) + this.Status = ScheduledTasks.RegisterTask(this); + + try + { + if (!this.LogExceptionsOnly) + ScheduledTasksLog.LogScheduledTaskExecuted(this.Status.TaskName, this.Status.SessionId); + + this.Status.Started(); + this.ExecuteTask(); + } + catch (Exception ex) + { + ScheduledTasksLog.LogScheduledTaskException(this.Status.TaskName, this.Status.SessionId, this.GetType(), ex); + this.Status.SetTaskException(ex); + } + finally + { + if (!this.Status.FinishedTimestamp.HasValue) // Scheduled Task Didn't Trigger 'Finished' + this.Status.Finished(); + + var nextTriggerTime = context.NextFireTimeUtc; + if (nextTriggerTime.HasValue) + { // Continuous Task + this.Status.Reset(nextTriggerTime.Value.LocalDateTime); + } + else + { + this.UnregisterTask(); + } + + if (!this.LogExceptionsOnly) + ScheduledTasksLog.LogScheduledTaskFinished(this.Status.TaskName, this.Status.SessionId); + } + } + } +} diff --git a/Disco.Services/Tasks/ScheduledTaskCleanup.cs b/Disco.Services/Tasks/ScheduledTaskCleanup.cs new file mode 100644 index 00000000..f3bef948 --- /dev/null +++ b/Disco.Services/Tasks/ScheduledTaskCleanup.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Quartz; + +namespace Disco.Services.Tasks +{ + +} diff --git a/Disco.Services/Tasks/ScheduledTaskStatus.cs b/Disco.Services/Tasks/ScheduledTaskStatus.cs new file mode 100644 index 00000000..7d4ba841 --- /dev/null +++ b/Disco.Services/Tasks/ScheduledTaskStatus.cs @@ -0,0 +1,351 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Quartz; +using System.Web.Script.Serialization; + +namespace Disco.Services.Tasks +{ + public class ScheduledTaskStatus + { + #region Backing Fields + + private string _sessionId; + private string _triggerKey; + private string _taskName; + private Type _taskType; + private bool _isSilent; + + private byte _progress; + private string _currentProcess; + private string _currentDescription; + + private Exception _taskException; + private bool _cancelInitiallySupported; + private bool _cancelSupported; + private bool _isCanceling; + + private DateTime? _startedTimestamp; + private DateTime? _nextScheduledTimestamp; + private DateTime? _finishedTimestamp; + + private string _finishedMessage; + private string _finishedUrl; + + private int _statusVersion = 0; + + #endregion + + #region Properties + + public string SessionId { get { return this._sessionId; } } + public string TriggerKey { get { return this._triggerKey; } } + public string TaskName { get { return this._taskName; } } + public Type TaskType { get { return this._taskType; } } + public bool IsSilent { get { return this._isSilent; } } + + public byte Progress { get { return this._progress; } } + public string CurrentProcess { get { return this._currentProcess; } } + public string CurrentDescription { get { return this._currentDescription; } } + + public Exception TaskException { get { return this._taskException; } } + public bool CancelSupported { get { return this._cancelSupported; } } + public bool IsCanceling { get { return this._isCanceling; } } + + public DateTime? StartedTimestamp { get { return this._startedTimestamp; } } + public DateTime? FinishedTimestamp { get { return this._finishedTimestamp; } } + public DateTime? NextScheduledTimestamp { get { return this._nextScheduledTimestamp; } } + + public string FinishedMessage { get { return this._finishedMessage; } } + public string FinishedUrl { get { return this._finishedUrl; } } + + public int StatusVersion { get { return this._statusVersion; } } + + public bool IsRunning + { + get + { + return _startedTimestamp.HasValue && !_finishedTimestamp.HasValue; + } + } + + #endregion + + #region Events + public delegate void UpdatedEvent(ScheduledTaskStatus sender, string[] ChangedProperties); + public delegate void CancelingEvent(ScheduledTaskStatus sender); + public event UpdatedEvent Updated; + public event CancelingEvent Canceling; + #endregion + + public ScheduledTaskStatus(ScheduledTask Task, string SessionId, string TriggerKey, string FinishedUrl = null) + { + this._taskName = Task.TaskName; + this._taskType = Task.GetType(); + + this._sessionId = SessionId; + this._triggerKey = TriggerKey; + this._cancelInitiallySupported = Task.CancelInitiallySupported; + this._cancelSupported = this._cancelInitiallySupported; + + this._finishedUrl = FinishedUrl; + + this._currentProcess = "Scheduled"; + this._currentDescription = "Scheduled Task for Execution"; + + this._progress = 0; + } + + #region Progress Actions + public void UpdateStatus(byte Progress) + { + this._progress = Progress; + UpdateTriggered(new string[] { "Progress" }); + } + public void UpdateStatus(double Progress) + { + UpdateStatus((byte)Progress); + } + public void UpdateStatus(string CurrentDescription) + { + this._currentDescription = CurrentDescription; + UpdateTriggered(new string[] { "CurrentDescription" }); + } + public void UpdateStatus(byte Progress, string CurrentDescription) + { + this._progress = Progress; + this._currentDescription = CurrentDescription; + UpdateTriggered(new string[] { "Progress", "CurrentDescription" }); + } + public void UpdateStatus(double Progress, string CurrentDescription) + { + UpdateStatus((byte)Progress, CurrentDescription); + } + public void UpdateStatus(byte Progress, string CurrentProcess, string CurrentDescription) + { + this._progress = Progress; + this._currentProcess = CurrentProcess; + this._currentDescription = CurrentDescription; + UpdateTriggered(new string[] { "Progress", "CurrentProcess", "CurrentDescription" }); + } + public void UpdateStatus(double Progress, string CurrentProcess, string CurrentDescription) + { + UpdateStatus((byte)Progress, CurrentProcess, CurrentDescription); + } + #endregion + + #region State Actions + public bool Canceled() + { + if (!this._isCanceling) + { + if (_cancelSupported) + { // Cancelling + this._isCanceling = true; + UpdateTriggered(new string[] { "IsCancelling" }); + if (this.Canceling != null) + Canceling(this); + return true; + } + else + { // Cancelling not supported + return false; + } + } + else + { // Already Cancelling + return true; + } + } + public void SetCancelSupported(bool CancelSupported) + { + if (this._cancelSupported != CancelSupported) + { + this._cancelSupported = CancelSupported; + UpdateTriggered(new string[] { "CancelSupported" }); + } + } + public void SetTaskException(Exception TaskException) + { + if (this._taskException != TaskException) + { + this._taskException = TaskException; + UpdateTriggered(new string[] { "TaskException" }); + } + } + public void SetIsSilent(bool IsSilent) + { + if (this._isSilent != IsSilent) + this._isSilent = IsSilent; + } + public void SetFinishedUrl(string FinishedUrl) + { + if (this._finishedUrl != FinishedUrl) + { + this._finishedUrl = FinishedUrl; + UpdateTriggered(new string[] { "FinishedUrl" }); + } + } + public void SetFinishedMessage(string FinishedMessage) + { + if (this._finishedMessage != FinishedMessage) + { + this._finishedMessage = FinishedMessage; + UpdateTriggered(new string[] { "FinishedMessage" }); + } + } + public void SetNextScheduledTimestamp(DateTime? NextScheduledTimestamp) + { + if (this._nextScheduledTimestamp != NextScheduledTimestamp) + { + this._nextScheduledTimestamp = NextScheduledTimestamp; + UpdateTriggered(new string[] { "NextScheduledTimestamp" }); + } + } + public void Started() + { + List changedProperties = new List() { "IsRunning", "StartedTimestamp" }; + + this._startedTimestamp = DateTime.Now; + + if (this._nextScheduledTimestamp != null) + { + this._nextScheduledTimestamp = null; + changedProperties.Add("NextScheduledTimestamp"); + } + if (this._finishedTimestamp != null) + { + this._finishedTimestamp = null; + changedProperties.Add("FinishedTimestamp"); + } + if (this._progress != 0) + { + this._progress = 0; + changedProperties.Add("Progress"); + } + if (this._currentProcess != "Starting") + { + this._currentProcess = "Starting"; + changedProperties.Add("CurrentProcess"); + } + if (this._currentDescription != "Initializing Task for Execution") + { + this._currentDescription = "Initializing Task for Execution"; + changedProperties.Add("CurrentDescription"); + } + if (this._taskException != null) + { + this._taskException = null; + changedProperties.Add("TaskException"); + } + if (this._cancelSupported != this._cancelInitiallySupported) + { + this._cancelSupported = this._cancelInitiallySupported; + changedProperties.Add("CancelSupported"); + } + { + this._isCanceling = false; + changedProperties.Add("IsCanceling"); + } + if (this._isCanceling) + { + this._isCanceling = false; + changedProperties.Add("IsCanceling"); + } + UpdateTriggered(changedProperties.ToArray()); + } + public void Finished() + { + Finished(this._finishedMessage, this._finishedUrl); + } + public void Finished(string FinishedMessage, string FinishedUrl) + { + List changedProperties = new List() { "IsRunning", "FinishedTimestamp" }; + + this._finishedTimestamp = DateTime.Now; + + if (FinishedMessage != this._finishedMessage) + { + this._finishedMessage = FinishedMessage; + changedProperties.Add("FinishedMessage"); + } + if (FinishedUrl != this._finishedUrl) + { + this._finishedUrl = FinishedUrl; + changedProperties.Add("FinishedUrl"); + } + + if (this._isCanceling) + { + this._isCanceling = false; + changedProperties.Add("IsCanceling"); + } + UpdateTriggered(changedProperties.ToArray()); + } + public void Reset(DateTime? NextScheduledTimestamp) + { + List changedProperties = new List(); + + if (this._nextScheduledTimestamp != NextScheduledTimestamp) + { + this._nextScheduledTimestamp = NextScheduledTimestamp; + changedProperties.Add("NextScheduledTimestamp"); + } + + if (this._startedTimestamp != null) + { + this._startedTimestamp = null; + changedProperties.Add("StartedTimestamp"); + } + if (this._finishedTimestamp != null) + { + this._finishedTimestamp = null; + changedProperties.Add("FinishedTimestamp"); + } + if (this._finishedMessage != null) + { + this._finishedMessage = null; + changedProperties.Add("FinishedMessage"); + } + if (this._finishedUrl != null) + { + this._finishedUrl = null; + changedProperties.Add("FinishedUrl"); + } + if (this._progress != 0) + { + this._progress = 0; + changedProperties.Add("Progress"); + } + if (this._currentProcess != "Scheduled") + { + this._currentProcess = "Scheduled"; + changedProperties.Add("CurrentProcess"); + } + if (this._currentDescription != "Scheduled Task for Execution") + { + this._currentDescription = "Scheduled Task for Execution"; + changedProperties.Add("CurrentDescription"); + } + if (this._isCanceling) + { + this._isCanceling = false; + changedProperties.Add("IsCanceling"); + } + UpdateTriggered(changedProperties.ToArray()); + } + #endregion + + private void UpdateTriggered(string[] ChangedProperties) + { + this._statusVersion++; + + if (Updated != null) + Updated(this, ChangedProperties); + + if (!_isSilent) + ScheduledTasksLiveStatusService.Broadcast(ScheduledTaskStatusLive.FromScheduledTaskStatus(this, ChangedProperties)); + } + } +} diff --git a/Disco.Services/Tasks/ScheduledTaskStatusLive.cs b/Disco.Services/Tasks/ScheduledTaskStatusLive.cs new file mode 100644 index 00000000..ed8ea7d3 --- /dev/null +++ b/Disco.Services/Tasks/ScheduledTaskStatusLive.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Disco.Services.Tasks +{ + public class ScheduledTaskStatusLive + { + public string TaskName { get; set; } + public string SessionId { get; set; } + + public byte Progress { get; set; } + public string CurrentProcess { get; set; } + public string CurrentDescription { get; set; } + + public string TaskExceptionMessage { get; set; } + public bool CancelSupported { get; set; } + public bool IsCancelling { get; set; } + + public bool IsRunning { get; set; } + public DateTime? StartedTimestamp { get; set; } + public DateTime? FinishedTimestamp { get; set; } + public DateTime? NextScheduledTimestamp { get; set; } + + public string FinishedMessage { get; set; } + public string FinishedUrl { get; set; } + + public int StatusVersion { get; set; } + + public string[] ChangedProperties { get; set; } + + public static ScheduledTaskStatusLive FromScheduledTaskStatus(ScheduledTaskStatus Status, string[] ChangedProperties) + { + return new ScheduledTaskStatusLive() + { + TaskName = Status.TaskName, + SessionId = Status.SessionId, + Progress = Status.Progress, + CurrentProcess = Status.CurrentProcess, + CurrentDescription = Status.CurrentDescription, + CancelSupported = Status.CancelSupported, + TaskExceptionMessage = (Status.TaskException == null ? null : Status.TaskException.Message), + IsCancelling = Status.IsCanceling, + IsRunning = Status.IsRunning, + StartedTimestamp = Status.StartedTimestamp, + FinishedTimestamp = Status.FinishedTimestamp, + NextScheduledTimestamp = Status.NextScheduledTimestamp, + FinishedMessage = Status.FinishedMessage, + FinishedUrl = Status.FinishedUrl, + StatusVersion = Status.StatusVersion, + ChangedProperties = ChangedProperties + }; + } + } +} diff --git a/Disco.Services/Tasks/ScheduledTasks.cs b/Disco.Services/Tasks/ScheduledTasks.cs new file mode 100644 index 00000000..f3c46c4e --- /dev/null +++ b/Disco.Services/Tasks/ScheduledTasks.cs @@ -0,0 +1,190 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Quartz; +using Quartz.Impl; +using Disco.Data.Repository; + +namespace Disco.Services.Tasks +{ + public static class ScheduledTasks + { + internal const string SchedulerGroupName = "DiscoScheduledTasks"; + private static IScheduler _TaskScheduler; + + private static object _RunningTasksLock = new object(); + private static List _RunningTasks = new List(); + + public static void InitalizeScheduledTasks(DiscoDataContext dbContext, ISchedulerFactory SchedulerFactory) + { + ScheduledTasksLog.LogInitializingScheduledTasks(); + + try + { + _TaskScheduler = SchedulerFactory.GetScheduler(); + _TaskScheduler.Start(); + + // Scheduled Cleanup + ScheduledTaskCleanup.Schedule(_TaskScheduler); + + // Discover DiscoScheduledTask + var appDomain = AppDomain.CurrentDomain; + + var scheduledTaskTypes = (from a in appDomain.GetAssemblies() + where !a.GlobalAssemblyCache && !a.IsDynamic + from type in a.GetTypes() + where typeof(ScheduledTask).IsAssignableFrom(type) && !type.IsAbstract + select type); + foreach (Type scheduledTaskType in scheduledTaskTypes) + { + ScheduledTask instance = (ScheduledTask)Activator.CreateInstance(scheduledTaskType); + try + { + instance.InitalizeScheduledTask(dbContext); + } + catch (Exception ex) + { + ScheduledTasksLog.LogInitializeException(ex, scheduledTaskType); + } + } + } + catch (Exception ex) + { + ScheduledTasksLog.LogInitializeException(ex); + } + + } + + public static ScheduledTaskStatus GetTaskStatus(string TaskSessionId) + { + return _RunningTasks.Where(t => t.SessionId == TaskSessionId).FirstOrDefault(); + } + public static List GetTaskStatuses(Type TaskType) + { + return _RunningTasks.Where(t => t.TaskType == TaskType).ToList(); + } + public static List GetTaskStatuses() + { + return _RunningTasks.ToList(); + } + + public static ScheduledTaskStatus RegisterTask(ScheduledTask Task) + { + return RegisterTask(Task, null); + } + public static ScheduledTaskStatus RegisterTask(ScheduledTask Task, TriggerBuilder TaskBuilder) + { + var sessionId = Guid.NewGuid().ToString("D"); + var triggerKey = GenerateTriggerKey(); + var taskType = Task.GetType(); + + var taskStatus = new ScheduledTaskStatus(Task, sessionId, triggerKey.Name); + + lock (_RunningTasksLock) + { + if (Task.SingleInstanceTask) + { + var existingGuid = _RunningTasks.Where(t => t.IsRunning && t.TaskType == taskType).Select(t => t.SessionId).FirstOrDefault(); + if (existingGuid != null) + throw new InvalidOperationException(string.Format("This Single-Instance Task is already running, SessionId: {0}", existingGuid)); + } + _RunningTasks.Add(taskStatus); + } + + if (TaskBuilder != null) + { + ITrigger trigger = TaskBuilder.WithIdentity(triggerKey).Build(); + IJobDetail jobDetails = new JobDetailImpl(sessionId, taskType) + { + Description = Task.TaskName, + JobDataMap = trigger.JobDataMap + }; + + _TaskScheduler.ScheduleJob(jobDetails, trigger); + + var nextTriggerTime = trigger.GetNextFireTimeUtc(); + if (nextTriggerTime.HasValue) + taskStatus.SetNextScheduledTimestamp(nextTriggerTime.Value.LocalDateTime); + } + + return taskStatus; + } + public static bool UnregisterTask(this ScheduledTask Task) + { + lock (_RunningTasksLock) + { + if (_RunningTasks.Contains(Task.Status)) + { + //_RunningTasks.Remove(Task.Status); + _TaskScheduler.UnscheduleJob(Task.ExecutionContext.Trigger.Key); + return true; + } + } + return false; + } + public static bool UnregisterTask(this ScheduledTaskStatus TaskStatus) + { + lock (_RunningTasksLock) + { + if (_RunningTasks.Contains(TaskStatus)) + { + //_RunningTasks.Remove(Task.Status); + _TaskScheduler.UnscheduleJob(new TriggerKey(TaskStatus.TriggerKey, SchedulerGroupName)); + return true; + } + } + return false; + } + + public static TriggerKey GenerateTriggerKey() + { + return new TriggerKey(Guid.NewGuid().ToString("D"), SchedulerGroupName); + } + + public static ScheduledTaskStatus GetDiscoScheduledTaskStatus(this IJobExecutionContext context) + { + return GetTaskStatus(context.JobDetail.Key.Name); + } + + private class ScheduledTaskCleanup : IJob + { + public void Execute(IJobExecutionContext context) + { + lock (ScheduledTasks._RunningTasksLock) + { + // Lifetime = 5mins + var expiredTime = DateTime.Now.AddMinutes(-1); + var expiredTasks = ScheduledTasks._RunningTasks.Where( + t => !t.IsRunning && + !t.NextScheduledTimestamp.HasValue && + t.FinishedTimestamp < expiredTime + ).ToArray(); + + foreach (var expiredTask in expiredTasks) + ScheduledTasks._RunningTasks.Remove(expiredTask); + } + } + public static void Schedule(IScheduler TaskScheduler) + { + // Next 10min interval + DateTime now = DateTime.Now; + int mins = (10 - (now.Minute % 10)); + if (mins < 2) + mins += 10; + DateTimeOffset startAt = new DateTimeOffset(now).AddMinutes(mins).AddSeconds(now.Second * -1).AddMilliseconds(now.Millisecond * -1); + ITrigger trigger = TriggerBuilder.Create() + .StartAt(startAt) + .WithSchedule(SimpleScheduleBuilder.RepeatMinutelyForever(10)) + .WithIdentity("ScheduledTaskCleanupTrigger", ScheduledTasks.SchedulerGroupName + "_System") + .Build(); + + IJobDetail job = JobBuilder.Create() + .WithIdentity("ScheduledTaskCleanupJob", ScheduledTasks.SchedulerGroupName + "_System") + .Build(); + + _TaskScheduler.ScheduleJob(job, trigger); + } + } + } +} diff --git a/Disco.Services/Tasks/ScheduledTasksLiveStatusService.cs b/Disco.Services/Tasks/ScheduledTasksLiveStatusService.cs new file mode 100644 index 00000000..1fb7d7f8 --- /dev/null +++ b/Disco.Services/Tasks/ScheduledTasksLiveStatusService.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using SignalR; +using SignalR.Hosting.AspNet; +using SignalR.Infrastructure; + +namespace Disco.Services.Tasks +{ + public class ScheduledTasksLiveStatusService : PersistentConnection + { + + protected override System.Threading.Tasks.Task OnReceivedAsync(IRequest request, string connectionId, string data) + { + // Add to Group + if (!string.IsNullOrWhiteSpace(data) && data.StartsWith("/addToGroups:") && data.Length > 13) + { + var groups = data.Substring(13).Split(','); + foreach (var g in groups) + { + this.Groups.Add(connectionId, g); + } + } + + return base.OnReceivedAsync(request, connectionId, data); + } + + internal static void Broadcast(ScheduledTaskStatusLive SessionStatus) + { + //var message = Models.LogLiveEvent.Create(logModule, eventType, Timestamp, Arguments); + + var connectionManager = GlobalHost.ConnectionManager; //AspNetHost.DependencyResolver.Resolve(); + var connectionContext = connectionManager.GetConnectionContext(); + connectionContext.Groups.Send(_GroupNameAll, SessionStatus); + connectionContext.Groups.Send(SessionStatus.SessionId, SessionStatus); + } + + private const string _GroupNameAll = "__All"; + //private static string _QualifiedSessionName = typeof(ScheduledTasksLiveStatusService).FullName + "."; + //private static string _QualifiedSessionNameAll = _QualifiedSessionName + "__All"; + //private static string LiveStatusGroup(string SessionId) + //{ + // return string.Concat(_QualifiedSessionName, SessionId); + //} + public static string LiveStatusAll + { + get + { + //return _QualifiedTypeNameAll; + return _GroupNameAll; + } + } + + } +} diff --git a/Disco.Services/Tasks/ScheduledTasksLog.cs b/Disco.Services/Tasks/ScheduledTasksLog.cs new file mode 100644 index 00000000..91133787 --- /dev/null +++ b/Disco.Services/Tasks/ScheduledTasksLog.cs @@ -0,0 +1,196 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Services.Logging; +using Disco.Services.Logging.Models; + +namespace Disco.Services.Tasks +{ + public class ScheduledTasksLog : LogBase + { + private const int _ModuleId = 20; + + public override string ModuleDescription { get { return "Scheduled Tasks"; } } + public override int ModuleId { get { return _ModuleId; } } + public override string ModuleName { get { return "ScheduledTasks"; } } + + public enum EventTypeIds + { + InitializingScheduledTasks = 10, + InitializeException = 15, + InitializeExceptionWithInner, + InitializeScheduledTasksException, + InitializeScheduledTasksExceptionWithInner, + ScheduledTasksException = 30, + ScheduledTasksExceptionWithInner, + ScheduledTaskExecuted = 50, + ScheduledTaskFinished = 80, + } + public static ScheduledTasksLog Current + { + get + { + return (ScheduledTasksLog)LogContext.LogModules[_ModuleId]; + } + } + private static void Log(EventTypeIds EventTypeId, params object[] Args) + { + Current.Log((int)EventTypeId, Args); + } + + public static void LogInitializingScheduledTasks() + { + Current.Log((int)EventTypeIds.InitializingScheduledTasks); + } + public static void LogScheduledTaskExecuted(string TaskName, string SessionId) + { + Current.Log((int)EventTypeIds.ScheduledTaskExecuted, TaskName, SessionId); + } + public static void LogScheduledTaskFinished(string TaskName, string SessionId) + { + Current.Log((int)EventTypeIds.ScheduledTaskFinished, TaskName, SessionId); + } + + public static void LogInitializeException(Exception ex) + { + if (ex.InnerException != null) + { + Log(EventTypeIds.InitializeExceptionWithInner, ex.GetType().Name, ex.Message, ex.StackTrace, ex.InnerException.GetType().Name, ex.InnerException.Message, ex.InnerException.StackTrace); + } + else + { + Log(EventTypeIds.InitializeException, ex.GetType().Name, ex.Message, ex.StackTrace); + } + } + public static void LogInitializeException(Exception ex, Type ScheduledTaskType) + { + if (ex.InnerException != null) + { + Log(EventTypeIds.InitializeScheduledTasksExceptionWithInner, ScheduledTaskType.Name, ScheduledTaskType.Assembly.Location, ex.GetType().Name, ex.Message, ex.StackTrace, ex.InnerException.GetType().Name, ex.InnerException.Message, ex.InnerException.StackTrace); + } + else + { + Log(EventTypeIds.InitializeScheduledTasksException, ScheduledTaskType.Name, ScheduledTaskType.Assembly.Location, ex.GetType().Name, ex.Message, ex.StackTrace); + } + } + + public static void LogScheduledTaskException(string ScheduledTaskName, string SessionId, Type ScheduledTaskType, Exception ex) + { + if (ex.InnerException != null) + { + Log(EventTypeIds.ScheduledTasksExceptionWithInner, ScheduledTaskName, SessionId, ScheduledTaskType.Assembly.Location, ex.GetType().Name, ex.Message, ex.StackTrace, ex.InnerException.GetType().Name, ex.InnerException.Message, ex.InnerException.StackTrace); + } + else + { + Log(EventTypeIds.ScheduledTasksException, ScheduledTaskName, SessionId, ScheduledTaskType.Assembly.Location, ex.GetType().Name, ex.Message, ex.StackTrace); + } + } + + protected override List LoadEventTypes() + { + return new System.Collections.Generic.List + { + new LogEventType + { + Id = (int)EventTypeIds.InitializingScheduledTasks, + ModuleId = _ModuleId, + Name = "Initializing Scheduled Tasks", + Format = "Starting Scheduled Task discovery and initialization", + Severity = (int)LogEventType.Severities.Information, + UseLive = false, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.InitializeException, + ModuleId = _ModuleId, + Name = "Initialize Exception", + Format = "Exception: {0}: {1}; {2}", + Severity = (int)LogEventType.Severities.Error, + UseLive = false, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.InitializeExceptionWithInner, + ModuleId = _ModuleId, + Name = "Initialize Exception with Inner Exception", + Format = "Exception: {0}: {1}; {2}; Inner: {3}: {4}; {5}", + Severity = (int)LogEventType.Severities.Error, + UseLive = false, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.InitializeScheduledTasksException, + ModuleId = _ModuleId, + Name = "Initialize Task Exception", + Format = "[{0}] At '{1}'; Exception: {2}: {3}; {4}", + Severity = (int)LogEventType.Severities.Error, + UseLive = false, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.InitializeScheduledTasksExceptionWithInner, + ModuleId = _ModuleId, + Name = "Initialize Task Exception with Inner Exception", + Format = "[{0}] At '{1}'; Exception: {2}: {3}; {4}; Inner: {5}: {6}; {7}", + Severity = (int)LogEventType.Severities.Error, + UseLive = false, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.ScheduledTasksException, + ModuleId = _ModuleId, + Name = "Scheduled Task Exception", + Format = "Task Name: {0}; SessionId: {1}; At: '{2}'; Exception: {3}: {4}; {5}", + Severity = (int)LogEventType.Severities.Error, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.ScheduledTasksExceptionWithInner, + ModuleId = _ModuleId, + Name = "Scheduled Task Exception with Inner Exception", + Format = "Task Name: {0}; SessionId: {1}; At: '{2}'; Exception: {3}: {4}; {5}; Inner: {6}: {7}; {8}", + Severity = (int)LogEventType.Severities.Error, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.ScheduledTaskExecuted, + ModuleId = _ModuleId, + Name = "Scheduled Task Started", + Format = "Scheduled Task Started: {0}; Session Id: {1}", + Severity = (int)LogEventType.Severities.Information, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.ScheduledTaskFinished, + ModuleId = _ModuleId, + Name = "Scheduled Task Finished", + Format = "Scheduled Task Finished: {0}; Session Id: {1}", + Severity = (int)LogEventType.Severities.Information, + UseLive = true, + UsePersist = true, + UseDisplay = true + } + }; + } + } +} diff --git a/Disco.Services/_Plugins/Categories/CertificateProvider/CertificateProviderLog.cs b/Disco.Services/_Plugins/Categories/CertificateProvider/CertificateProviderLog.cs new file mode 100644 index 00000000..36c2dbc1 --- /dev/null +++ b/Disco.Services/_Plugins/Categories/CertificateProvider/CertificateProviderLog.cs @@ -0,0 +1,304 @@ +using Disco.Services.Logging; +using Disco.Services.Logging.Models; +using System; +using System.Collections.Generic; +using System.Diagnostics; +namespace Disco.Services.Plugins.Categories.CertificateProvider +{ + public class CertificateProvidersLog : LogBase + { + public enum EventTypeIds + { + RetrievalStarting = 10, + RetrievalProgress, + RetrievalFinished, + RetrievalWarning = 15, + RetrievalError, + RetrievalCertificateStarting = 20, + RetrievalCertificateFinished = 22, + RetrievalCertificateWarning = 25, + RetrievalCertificateError, + Allocated = 40, + AllocationFailed = 50 + } + private const int _ModuleId = 60; + private static bool _IsCertificateRetrievalProcessing; + private static string _CertificateRetrievalStatus; + private static int _CertificateRetrievalProgress; + public static CertificateProvidersLog Current + { + get + { + return (CertificateProvidersLog)LogContext.LogModules[60]; + } + } + public static bool IsCertificateRetrievalProcessing + { + get + { + return CertificateProvidersLog._IsCertificateRetrievalProcessing; + } + } + public override string ModuleDescription + { + get + { + return "Certificate Providers"; + } + } + public override int ModuleId + { + get + { + return 60; + } + } + public override string ModuleName + { + get + { + return "CertificateProviders"; + } + } + [System.Diagnostics.DebuggerNonUserCode] + public CertificateProvidersLog() + { + } + private static void Log(CertificateProvidersLog.EventTypeIds EventTypeId, params object[] Args) + { + CertificateProvidersLog.Current.Log((int)EventTypeId, Args); + } + public static void LogRetrievalStarting(int CertificateCount, int CertificateIdFrom, int CertificateIdTo) + { + CertificateProvidersLog.Log(CertificateProvidersLog.EventTypeIds.RetrievalStarting, new object[] + { + CertificateCount, + CertificateIdFrom, + CertificateIdTo + }); + } + public static void LogRetrievalFinished() + { + CertificateProvidersLog.Log(CertificateProvidersLog.EventTypeIds.RetrievalFinished, new object[0]); + } + public static void LogRetrievalWarning(string Message) + { + CertificateProvidersLog.Log(CertificateProvidersLog.EventTypeIds.RetrievalWarning, new object[] + { + Message + }); + } + public static void LogRetrievalError(string Message) + { + CertificateProvidersLog.Log(CertificateProvidersLog.EventTypeIds.RetrievalError, new object[] + { + Message + }); + } + public static void LogRetrievalCertificateStarting(string CertificateId) + { + CertificateProvidersLog.Log(CertificateProvidersLog.EventTypeIds.RetrievalCertificateStarting, new object[] + { + CertificateId + }); + } + public static void LogRetrievalCertificateFinished(string CertificateId) + { + CertificateProvidersLog.Log(CertificateProvidersLog.EventTypeIds.RetrievalCertificateFinished, new object[] + { + CertificateId + }); + } + public static void LogRetrievalCertificateWarning(string CertificateId, string Message) + { + CertificateProvidersLog.Log(CertificateProvidersLog.EventTypeIds.RetrievalCertificateWarning, new object[] + { + CertificateId, + Message + }); + } + public static void LogRetrievalCertificateError(string CertificateId, string Message) + { + CertificateProvidersLog.Log(CertificateProvidersLog.EventTypeIds.RetrievalCertificateError, new object[] + { + CertificateId, + Message + }); + } + public static void LogAllocated(string CertificateId, string DeviceSerialNumber) + { + CertificateProvidersLog.Log(CertificateProvidersLog.EventTypeIds.Allocated, new object[] + { + CertificateId, + DeviceSerialNumber + }); + } + public static void LogAllocationFailed(string DeviceSerialNumber) + { + CertificateProvidersLog.Log(CertificateProvidersLog.EventTypeIds.AllocationFailed, new object[] + { + DeviceSerialNumber + }); + } + public static void LogCertificateRetrievalProgress(bool? IsProcessing, int? Progress, string Status) + { + bool flag = IsProcessing.HasValue; + if (flag) + { + CertificateProvidersLog._IsCertificateRetrievalProcessing = IsProcessing.Value; + } + flag = CertificateProvidersLog._IsCertificateRetrievalProcessing; + if (flag) + { + bool flag2 = Status != null; + if (flag2) + { + CertificateProvidersLog._CertificateRetrievalStatus = Status; + } + flag2 = Progress.HasValue; + if (flag2) + { + CertificateProvidersLog._CertificateRetrievalProgress = Progress.Value; + } + } + else + { + CertificateProvidersLog._CertificateRetrievalStatus = null; + CertificateProvidersLog._CertificateRetrievalProgress = 0; + } + CertificateProvidersLog.Log(CertificateProvidersLog.EventTypeIds.RetrievalProgress, new object[] + { + CertificateProvidersLog._IsCertificateRetrievalProcessing, + CertificateProvidersLog._CertificateRetrievalProgress, + CertificateProvidersLog._CertificateRetrievalStatus + }); + } + protected override System.Collections.Generic.List LoadEventTypes() + { + return new System.Collections.Generic.List + { + new LogEventType + { + Id = 10, + ModuleId = 60, + Name = "Retrieval Starting", + Format = "Starting retrieval of {0} certificate/s ({1} to {2})", + Severity = 0, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 11, + ModuleId = 60, + Name = "Retrieval Progress", + Format = "Processing: {0}; {1}% Complete; Status: {2}", + Severity = 0, + UseLive = true, + UsePersist = false, + UseDisplay = false + }, + new LogEventType + { + Id = 12, + ModuleId = 60, + Name = "Retrieval Finished", + Format = "Retrieval of Certificates Complete", + Severity = 0, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 15, + ModuleId = 60, + Name = "Retrieval Warning", + Format = "Retrieval Warning: {0}", + Severity = 1, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 16, + ModuleId = 60, + Name = "Retrieval Error", + Format = "Retrieval Error: {0}", + Severity = 2, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 20, + ModuleId = 60, + Name = "Retrieval Certificate Starting", + Format = "Retrieving Certificate: {0}", + Severity = 0, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 22, + ModuleId = 60, + Name = "Retrieval Certificate Finished", + Format = "Certificate Retrieved: {0}", + Severity = 0, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 25, + ModuleId = 60, + Name = "Retrieval Certificate Warning", + Format = "{0} Certificate Warning: {1}", + Severity = 1, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 26, + ModuleId = 60, + Name = "Retrieval Certificate Error", + Format = "{0} Certificate Error: {1}", + Severity = 2, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 40, + ModuleId = 60, + Name = "Allocated", + Format = "Certificate {0} allocated to {1}", + Severity = 0, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = 50, + ModuleId = 60, + Name = "Allocation Failed", + Format = "No certificates available for Device: {0}", + Severity = 2, + UseLive = true, + UsePersist = true, + UseDisplay = true + } + }; + } + } +} diff --git a/Disco.Services/_Plugins/Categories/CertificateProvider/CertificateProviderPlugin.cs b/Disco.Services/_Plugins/Categories/CertificateProvider/CertificateProviderPlugin.cs new file mode 100644 index 00000000..c1cf4153 --- /dev/null +++ b/Disco.Services/_Plugins/Categories/CertificateProvider/CertificateProviderPlugin.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Data.Repository; +using Disco.Models.Repository; + +namespace Disco.Services.Plugins.Categories.CertificateProvider +{ + [PluginCategory(DisplayName = "Certificate Providers")] + public abstract class CertificateProviderPlugin : Plugin + { + public override sealed Type PluginCategoryType + { + get { return typeof(CertificateProviderPlugin); } + } + + // Certificate Plugin Requirements + public abstract string CertificateProviderId { get; } + public abstract Tuple> AllocateCertificate(DiscoDataContext dbContext, Device Device); + + } +} diff --git a/Disco.Services/_Plugins/Categories/InteroperabilityProvider/InteroperabilityProviderPlugin.cs b/Disco.Services/_Plugins/Categories/InteroperabilityProvider/InteroperabilityProviderPlugin.cs new file mode 100644 index 00000000..66be1088 --- /dev/null +++ b/Disco.Services/_Plugins/Categories/InteroperabilityProvider/InteroperabilityProviderPlugin.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Disco.Services.Plugins.Categories.InteroperabilityProvider +{ + [PluginCategory(DisplayName = "Interoperability Providers")] + public abstract class InteroperabilityProviderPlugin : Plugin + { + public override sealed Type PluginCategoryType + { + get { return typeof(InteroperabilityProviderPlugin); } + } + } +} diff --git a/Disco.Services/_Plugins/Categories/WarrantyProvider/WarrantyProviderPlugin.cs b/Disco.Services/_Plugins/Categories/WarrantyProvider/WarrantyProviderPlugin.cs new file mode 100644 index 00000000..05439ef9 --- /dev/null +++ b/Disco.Services/_Plugins/Categories/WarrantyProvider/WarrantyProviderPlugin.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web.Mvc; +using Disco.Data.Repository; +using Disco.Models.BI.Config; +using Disco.Models.Repository; + +namespace Disco.Services.Plugins.Categories.WarrantyProvider +{ + [PluginCategory(DisplayName = "Warranty Providers")] + public abstract class WarrantyProviderPlugin : Plugin + { + public override sealed Type PluginCategoryType + { + get { return typeof(WarrantyProviderPlugin); } + } + + // Warranty Plugin Requirements + public abstract string WarrantyProviderId { get; } + public abstract Type SubmitJobViewType { get; } + public abstract dynamic SubmitJobViewModel(DiscoDataContext dbContext, Controller controller, Job Job, OrganisationAddress Address, User TechUser); + public abstract Dictionary SubmitJobParseProperties(DiscoDataContext dbContext, FormCollection form, Controller controller, Job Job, OrganisationAddress Address, User TechUser, string FaultDescription); + public abstract Dictionary SubmitJobDiscloseInfo(DiscoDataContext dbContext, Job Job, OrganisationAddress Address, User TechUser, string FaultDescription, Dictionary WarrantyProviderProperties); + public abstract string SubmitJob(DiscoDataContext dbContext, Job Job, OrganisationAddress Address, User TechUser, string FaultDescription, Dictionary WarrantyProviderProperties); + + public abstract Type JobDetailsViewType { get; } + public bool JobDetailsSupported { get { return this.JobDetailsViewType != null; } } + public abstract dynamic JobDetailsViewModel(DiscoDataContext dbContext, Controller controller, Job Job); + + public static PluginDefinition FindPlugin(string PlugIdOrWarrantyProviderId) + { + var defs = Plugins.GetPlugins(typeof(WarrantyProviderPlugin)); + var def = defs.FirstOrDefault(d => d.Id.Equals(PlugIdOrWarrantyProviderId, StringComparison.InvariantCultureIgnoreCase)); + if (def != null) + return def; + else + foreach (var d in defs) + { + using (var providerInstance = d.CreateInstance()) + { + if (providerInstance.WarrantyProviderId != null && providerInstance.WarrantyProviderId.Equals(PlugIdOrWarrantyProviderId, StringComparison.InvariantCultureIgnoreCase)) + { + return d; + } + } + } + + return null; + } + } +} diff --git a/Disco.Services/_Plugins/Categories/WarrantyProvider/WarrantyProviderSubmitJobException.cs b/Disco.Services/_Plugins/Categories/WarrantyProvider/WarrantyProviderSubmitJobException.cs new file mode 100644 index 00000000..c582d74b --- /dev/null +++ b/Disco.Services/_Plugins/Categories/WarrantyProvider/WarrantyProviderSubmitJobException.cs @@ -0,0 +1,12 @@ +using System; + +namespace Disco.Services.Plugins.Categories.WarrantyProvider +{ + public class WarrantyProviderSubmitJobException : Exception + { + public WarrantyProviderSubmitJobException(string Message) + : base(Message) + { + } + } +} diff --git a/Disco.Services/_Plugins/IPluginConfiguration.cs b/Disco.Services/_Plugins/IPluginConfiguration.cs new file mode 100644 index 00000000..83c5c678 --- /dev/null +++ b/Disco.Services/_Plugins/IPluginConfiguration.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Data.Repository; +using System.Web.Mvc; + +namespace Disco.Services.Plugins +{ + public interface IPluginConfiguration + { + Type ConfigurationViewType { get; } + dynamic ConfigurationViewModel(DiscoDataContext dbContext, Controller controller); + bool ConfigurationSave(DiscoDataContext dbContext, FormCollection form, Controller controller); + } +} diff --git a/Disco.Services/_Plugins/IPluginWebController.cs b/Disco.Services/_Plugins/IPluginWebController.cs new file mode 100644 index 00000000..8256c569 --- /dev/null +++ b/Disco.Services/_Plugins/IPluginWebController.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Web.Mvc; + +namespace Disco.Services.Plugins +{ + public interface IPluginWebController + { + ActionResult ExecuteAction(string ActionName, Controller HostController); + } +} diff --git a/Disco.Services/_Plugins/InvalidCategoryTypeException.cs b/Disco.Services/_Plugins/InvalidCategoryTypeException.cs new file mode 100644 index 00000000..66990a78 --- /dev/null +++ b/Disco.Services/_Plugins/InvalidCategoryTypeException.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Disco.Services.Plugins +{ + public class InvalidCategoryTypeException : Exception + { + private string _pluginRequested; + private Type _categoryType; + + public string PluginRequested + { + get + { + return _pluginRequested; + } + } + public Type CategoryType + { + get + { + return _categoryType; + } + } + + public InvalidCategoryTypeException(Type CategoryType) + : this(CategoryType, null) + { + } + public InvalidCategoryTypeException(Type CategoryType, string PluginRequested) + { + this._categoryType = CategoryType; + this._pluginRequested = PluginRequested; + } + + public override string Message + { + get + { + if (string.IsNullOrEmpty(_pluginRequested)) + return string.Format("Invalid Category Type [{0}]", _categoryType.Name); + else + return string.Format("Plugin [{1}] is not of the correct Category Type [{0}]", _categoryType.Name, _pluginRequested); + } + } + + } +} diff --git a/Disco.Services/_Plugins/Plugin.cs b/Disco.Services/_Plugins/Plugin.cs new file mode 100644 index 00000000..6079ce52 --- /dev/null +++ b/Disco.Services/_Plugins/Plugin.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Data.Repository; +using System.Web.Mvc; + +namespace Disco.Services.Plugins +{ + public abstract class Plugin : IDisposable + { + public string Id { get { return this.GetType().Name; } } + public abstract string Name { get; } + public abstract string Author { get; } + public abstract Version Version { get; } + + public abstract Type PluginCategoryType { get; } + + public abstract bool Initalize(DiscoDataContext dbContext); + + public abstract void Dispose(); + + public override sealed string ToString() + { + return string.Format("{0} ({1}) - v{2}", this.Name, this.Id, this.Version.ToString(3)); + } + } +} diff --git a/Disco.Services/_Plugins/PluginCategoryAttribute.cs b/Disco.Services/_Plugins/PluginCategoryAttribute.cs new file mode 100644 index 00000000..d2ec3d4e --- /dev/null +++ b/Disco.Services/_Plugins/PluginCategoryAttribute.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Disco.Services.Plugins +{ + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public class PluginCategoryAttribute : Attribute + { + public string DisplayName { get; set; } + } +} diff --git a/Disco.Services/_Plugins/PluginDefinition.cs b/Disco.Services/_Plugins/PluginDefinition.cs new file mode 100644 index 00000000..1393e4fa --- /dev/null +++ b/Disco.Services/_Plugins/PluginDefinition.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Reflection; +using System.Web.Mvc; + +namespace Disco.Services.Plugins +{ + public class PluginDefinition + { + public string Id { get; private set; } + public string Name { get; private set; } + public string Author { get; private set; } + public Version Version { get; private set; } + + public Type PluginType { get; private set; } + public Type PluginCategoryType { get; private set; } + + public Dictionary PluginReferenceAssemblies { get; private set; } + public string PluginHostDirectory { get; private set; } + + public bool HasConfiguration { get; private set; } + public bool HasWebController { get; private set; } + + public PluginDefinition(Plugin PluginInstance, string HostDirectory, Dictionary ReferencedAssemblies = null) + { + Type pluginType = PluginInstance.GetType(); + + if (string.IsNullOrWhiteSpace(PluginInstance.Id)) + throw new ArgumentNullException("PluginInstance.Id"); + if (string.IsNullOrWhiteSpace(PluginInstance.Name)) + throw new ArgumentNullException("PluginInstance.Name"); + if (string.IsNullOrWhiteSpace(PluginInstance.Author)) + throw new ArgumentNullException("PluginInstance.Author"); + if (string.IsNullOrWhiteSpace(HostDirectory)) + throw new ArgumentNullException("HostDirectory"); + if (PluginInstance.Version == null) + throw new ArgumentNullException("PluginInstance.Version"); + if (PluginInstance.PluginCategoryType == null) + throw new ArgumentNullException("PluginInstance.PluginCategoryType"); + if (!PluginInstance.PluginCategoryType.IsAssignableFrom(pluginType)) + throw new ArgumentException(string.Format("The plugin [{0}] does not inherit the Category [{1}]", pluginType.Name, PluginInstance.PluginCategoryType.Name), "PluginInstance.PluginCategoryType"); + + this.Id = PluginInstance.Id; + this.Name = PluginInstance.Name; + this.Author = PluginInstance.Author; + this.Version = PluginInstance.Version; + this.PluginType = pluginType; + this.PluginCategoryType = PluginInstance.PluginCategoryType; + + this.PluginHostDirectory = HostDirectory; + + if (ReferencedAssemblies != null && ReferencedAssemblies.Count > 0) + this.PluginReferenceAssemblies = ReferencedAssemblies; + + // Determine (and Validate) if has Configuration + IPluginConfiguration pluginConfiguration = PluginInstance as IPluginConfiguration; + if (pluginConfiguration != null) + { + if (pluginConfiguration.ConfigurationViewType == null) + throw new ArgumentNullException("PluginInstance.ConfigurationViewType"); + if (!typeof(WebViewPage).IsAssignableFrom(pluginConfiguration.ConfigurationViewType)) + throw new ArgumentException(string.Format("The plugin [{0}] ConfigurationViewType must inherit System.Web.Mvc.WebViewPage", pluginType.Name), "PluginInstance.ConfigurationViewType"); + + this.HasConfiguration = true; + } + + // Determine if has Web Controller + IPluginWebController pluginWebController = PluginInstance as IPluginWebController; + this.HasWebController = (pluginWebController != null); + } + + public Plugin CreateInstance() + { + return (Plugin)Activator.CreateInstance(PluginType); + } + public PluginType CreateInstance() + { + if (typeof(PluginType).IsAssignableFrom(this.PluginType)) + return (PluginType)Activator.CreateInstance(this.PluginType); + else + throw new InvalidOperationException(string.Format("The plugin [{0}] cannot be cast into type [{1}]", this.PluginType.Name, typeof(PluginType).Name)); + } + } +} diff --git a/Disco.Services/_Plugins/PluginExtensions.cs b/Disco.Services/_Plugins/PluginExtensions.cs new file mode 100644 index 00000000..1bab88b3 --- /dev/null +++ b/Disco.Services/_Plugins/PluginExtensions.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Data.Repository; +using System.IO; +using System.Web.Mvc; +using System.Web.Routing; +using System.Web; + +namespace Disco.Services.Plugins +{ + public static class PluginExtensions + { + public static string PluginStorage(this Plugin plugin, DiscoDataContext dbContext) + { + string pluginStorageLocationRoot = dbContext.DiscoConfiguration.PluginStorageLocation; + + string storageLocation = Path.Combine(pluginStorageLocationRoot, plugin.Id); + + if (!Directory.Exists(storageLocation)) + Directory.CreateDirectory(storageLocation); + + return storageLocation; + } + + public static string PluginCategoryDisplayName(this Plugin plugin) + { + if (plugin == null) + throw new ArgumentNullException("plugin"); + + return Plugins.CategoryDisplayNames[plugin.PluginCategoryType]; + } + + #region Model Binding from Controller + public static bool TryUpdateModel(this Controller controller, TModel model) where TModel : class + { + return controller.TryUpdateModel(model, null, controller.ValueProvider); + } + public static bool TryUpdateModel(this Controller controller, TModel model, IValueProvider valueProvider) where TModel : class + { + return controller.TryUpdateModel(model, null, valueProvider); + } + public static bool TryUpdateModel(this Controller controller, TModel model, string prefix) where TModel : class + { + return controller.TryUpdateModel(model, prefix, controller.ValueProvider); + } + public static bool TryUpdateModel(this Controller controller, TModel model, string prefix, IValueProvider valueProvider) where TModel : class + { + if (model == null) + throw new ArgumentNullException("model"); + if (valueProvider == null) + throw new ArgumentNullException("valueProvider"); + + Predicate predicate = propertyName => true; + IModelBinder binder = ModelBinders.Binders.GetBinder(typeof(TModel)); + + ModelBindingContext context2 = new ModelBindingContext + { + ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, typeof(TModel)), + ModelName = prefix, + ModelState = controller.ModelState, + PropertyFilter = predicate, + ValueProvider = valueProvider + }; + + ModelBindingContext bindingContext = context2; + + binder.BindModel(controller.ControllerContext, bindingContext); + + return controller.ModelState.IsValid; + } + #endregion + } +} diff --git a/Disco.Services/_Plugins/PluginWebControllerException.cs b/Disco.Services/_Plugins/PluginWebControllerException.cs new file mode 100644 index 00000000..74dc16e2 --- /dev/null +++ b/Disco.Services/_Plugins/PluginWebControllerException.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Disco.Services.Plugins +{ + public class PluginWebControllerException : Exception + { + public PluginWebControllerException(string PluginId, string PluginAction, Exception InnerException) + : base(string.Format("An error occurred executing the Disco Plugin [{0}] Web Controller Action: [{0}]", PluginId, PluginAction), InnerException) + { + } + } +} diff --git a/Disco.Services/_Plugins/PluginWebControllerExtensions.cs b/Disco.Services/_Plugins/PluginWebControllerExtensions.cs new file mode 100644 index 00000000..52c86c07 --- /dev/null +++ b/Disco.Services/_Plugins/PluginWebControllerExtensions.cs @@ -0,0 +1,210 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Web.Routing; +using System.Web.Mvc; +using RazorGenerator.Mvc; +using System.IO; + +namespace Disco.Services.Plugins +{ + public static class PluginWebControllerExtensions + { + #region Virtual Directories + public static string WebControllerRootUrl(this IPluginWebController plugin, RequestContext requestContext) + { + var tempPath = plugin.WebControllerActionUrl(requestContext, "_"); + return tempPath.Substring(0, tempPath.LastIndexOf(@"/") + 1); + } + public static string WebControllerActionUrl(this IPluginWebController plugin, RequestContext requestContext, string PluginAction) + { + //return string.Format("~/Config/Plugins/{0}/{1}", HttpUtility.UrlEncode(((IDiscoPlugin)plugin).Id), HttpUtility.UrlEncode(PluginAction)); + var routeValues = new RouteValueDictionary(new { PluginId = ((Plugin)plugin).Id, PluginAction = PluginAction }); + return UrlHelper.GenerateUrl("Config_Plugins_PluginWebControllerActions", "PluginAction", "Plugins", routeValues, RouteTable.Routes, requestContext, true); + } + #endregion + + #region Action Results + + #region Compiled View + private static string[] _viewFileNames = new string[] { "cshtml" }; + public static ActionResult CompiledView(this Controller HostController, Type CompiledViewType, object Model, bool UseDiscoLayout) + { + string layoutPath = UseDiscoLayout ? "~/Views/Shared/_Layout.cshtml" : null; + + IView v = new PrecompiledMvcView(HostController.Request.Path, layoutPath, CompiledViewType, false, _viewFileNames); + + if (Model != null) + HostController.ViewData.Model = Model; + + return new ViewResult { View = v, ViewData = HostController.ViewData, TempData = HostController.TempData }; + } + public static ActionResult CompiledView(this Controller HostController, Type CompiledViewType, bool UseDiscoLayout) + { + return HostController.CompiledView(CompiledViewType, null, UseDiscoLayout); + } + public static ActionResult CompiledView(this Controller HostController, Type CompiledViewType, object Model) + { + return HostController.CompiledView(CompiledViewType, Model, true); + } + public static ActionResult CompiledView(this Controller HostController, Type CompiledViewType) + { + return HostController.CompiledView(CompiledViewType, false, true); + } + public static ActionResult CompiledPartialView(this Controller HostController, Type PartialCompiledViewType, object Model) + { + IView v = new PrecompiledMvcView(HostController.Request.Path, PartialCompiledViewType, false, _viewFileNames); + + if (Model != null) + HostController.ViewData.Model = Model; + + return new PartialViewResult { View = v, ViewData = HostController.ViewData, TempData = HostController.TempData }; + } + public static ActionResult CompiledPartialView(this Controller HostController, Type PartialCompiledViewType) + { + return HostController.CompiledView(PartialCompiledViewType, null); + } + #endregion + + #region Content + public static ActionResult Content(this Controller HostController, string content, string contentType, Encoding contentEncoding) + { + return new ContentResult { Content = content, ContentType = contentType, ContentEncoding = contentEncoding }; + } + public static ActionResult Content(this Controller HostController, string content, string contentType) + { + return HostController.Content(content, null, null); + } + public static ActionResult Content(this Controller HostController, string content) + { + return HostController.Content(content, null); + } + #endregion + + #region Json + public static ActionResult Json(this Controller HostController, object data, JsonRequestBehavior behavior) + { + return new JsonResult { Data = data, ContentType = null, ContentEncoding = null, JsonRequestBehavior = behavior }; + } + #endregion + + #region File + public static ActionResult File(this Controller HostController, Stream fileStream, string contentType) + { + return HostController.File(fileStream, contentType, null); + } + public static ActionResult File(this Controller HostController, Stream fileStream, string contentType, string fileDownloadName) + { + return new FileStreamResult(fileStream, contentType) { FileDownloadName = fileDownloadName }; + } + public static ActionResult File(this Controller HostController, byte[] fileContents, string contentType) + { + return HostController.File(fileContents, contentType, null); + } + public static ActionResult File(this Controller HostController, byte[] fileContents, string contentType, string fileDownloadName) + { + return new FileContentResult(fileContents, contentType) { FileDownloadName = fileDownloadName }; + } + #endregion + + #region HttpNotFound + public static ActionResult HttpNotFound(this Controller HostController, string statusDescription) + { + return new HttpNotFoundResult(statusDescription); + } + public static ActionResult HttpNotFound(this Controller HostController) + { + return HostController.HttpNotFound(null); + } + #endregion + + #region Redirect + public static ActionResult RedirectToScheduledTaskStatus(this Controller HostController, string SessionId) + { + if (string.IsNullOrEmpty(SessionId)) + throw new ArgumentNullException(SessionId); + + return HostController.RedirectToAction("TaskStatus", "Logging", "Config", new { id = SessionId }); + } + public static ActionResult Redirect(this Controller HostController, string url) + { + if (string.IsNullOrEmpty(url)) + throw new ArgumentNullException("url"); + + return new RedirectResult(url); + } + public static ActionResult RedirectPermanent(this Controller HostController, string url) + { + if (string.IsNullOrEmpty(url)) + throw new ArgumentNullException("url"); + + return new RedirectResult(url, true); + } + public static ActionResult RedirectToAction(this Controller HostController, IPluginWebController Plugin, string PluginAction) + { + if (string.IsNullOrEmpty(PluginAction)) + throw new ArgumentNullException("PluginAction"); + + string pluginActionUrl = Plugin.WebControllerActionUrl(HostController.Request.RequestContext, PluginAction); + return new RedirectResult(pluginActionUrl, false); + } + public static ActionResult RedirectToRoute(this Controller HostController, string routeName, object routeValues) + { + RouteValueDictionary routeValueDictionary; + if (routeValues != null) + routeValueDictionary = new RouteValueDictionary(routeValues); + else + routeValueDictionary = new RouteValueDictionary(); + + return new RedirectToRouteResult(routeName, routeValueDictionary); + } + public static ActionResult RedirectToRoute(this Controller HostController, string routeName) + { + return HostController.RedirectToRoute(routeName, null); + } + public static ActionResult RedirectToAction(this Controller HostController, string actionName, string controllerName, string areaName, object routeValues) + { + RouteValueDictionary routeValueDictionary; + if (routeValues != null) + routeValueDictionary = new RouteValueDictionary(routeValues); + else + routeValueDictionary = new RouteValueDictionary(); + + routeValueDictionary["action"] = actionName; + routeValueDictionary["controller"] = controllerName; + if (areaName != null) + routeValueDictionary["area"] = areaName; + + return new RedirectToRouteResult(routeValueDictionary); + } + public static ActionResult RedirectToAction(this Controller HostController, string actionName, string controllerName, string areaName) + { + return HostController.RedirectToAction(actionName, controllerName, areaName, null); + } + public static ActionResult RedirectToAction(this Controller HostController, string actionName, string controllerName, object routeValues) + { + return HostController.RedirectToAction(actionName, controllerName, null, routeValues); + } + public static ActionResult RedirectToAction(this Controller HostController, string actionName, string controllerName) + { + return HostController.RedirectToAction(actionName, controllerName, null, null); + } + public static ActionResult RedirectToDiscoJob(this Controller HostController, int jobId) + { + return HostController.RedirectToAction("Show", "Job", null, new { id = jobId.ToString() }); + } + public static ActionResult RedirectToDiscoDevice(this Controller HostController, string DeviceSerialNumber) + { + return HostController.RedirectToAction("Show", "Device", null, new { id = DeviceSerialNumber }); + } + public static ActionResult RedirectToDiscoUser(this Controller HostController, string UserId) + { + return HostController.RedirectToAction("Show", "User", null, new { id = UserId }); + } + #endregion + + #endregion + + } +} diff --git a/Disco.Services/_Plugins/Plugins.cs b/Disco.Services/_Plugins/Plugins.cs new file mode 100644 index 00000000..a1fc58ae --- /dev/null +++ b/Disco.Services/_Plugins/Plugins.cs @@ -0,0 +1,290 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Reflection; +using Disco.Data.Repository; +using System.IO; +using System.ComponentModel.DataAnnotations; +using System.Web; +using System.Web.Mvc; +using RazorGenerator.Mvc; +using System.Web.WebPages; + +namespace Disco.Services.Plugins +{ + public static class Plugins + { + private static Dictionary _LoadedPlugins; + internal static Dictionary CategoryDisplayNames; + + private static object _PluginLock = new object(); + + public static string PluginPath { get; private set; } + + public static PluginDefinition GetPlugin(string PluginId, Type CategoryType = null) + { + if (_LoadedPlugins == null) + throw new InvalidOperationException("Plugins have not been initialized"); + + PluginDefinition def; + if (_LoadedPlugins.TryGetValue(PluginId, out def)) + { + if (CategoryType == null) + return def; + else + { + if (CategoryType.IsAssignableFrom(def.PluginCategoryType)) + return def; + else + throw new InvalidCategoryTypeException(CategoryType, PluginId); + } + } + else + { + throw new UnknownPluginException(PluginId); + } + } + public static List GetPlugins(Type CategoryType) + { + if (_LoadedPlugins == null) + throw new InvalidOperationException("Plugins have not been initialized"); + + return _LoadedPlugins.Values.Where(p => p.PluginCategoryType.IsAssignableFrom(CategoryType)).OrderBy(p => p.Name).ToList(); + } + public static List GetPlugins() + { + if (_LoadedPlugins == null) + throw new InvalidOperationException("Plugins have not been initialized"); + + return _LoadedPlugins.Values.ToList(); + } + + public static string PluginCategoryDisplayName(Type CategoryType) + { + if (CategoryType == null) + throw new ArgumentNullException("CategoryType"); + + string displayName; + if (CategoryDisplayNames.TryGetValue(CategoryType, out displayName)) + return displayName; + else + throw new InvalidOperationException(string.Format("Unknown Plugin Category Type: [{0}]", CategoryType.Name)); + } + + #region Initalizing + + public static void InitalizePlugins(DiscoDataContext dbContext) + { + if (_LoadedPlugins == null) + { + lock (_PluginLock) + { + if (_LoadedPlugins == null) + { + Dictionary loadedPlugins = new Dictionary(); + + PluginPath = dbContext.DiscoConfiguration.PluginsLocation; + + AppDomain appDomain = AppDomain.CurrentDomain; + + // Subscribe to Assembly Resolving + appDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; + + // Load Internal (Default?) Plugins + IEnumerable discoAssemblies = (from a in appDomain.GetAssemblies() + where !a.GlobalAssemblyCache && !a.IsDynamic && + a.FullName.StartsWith("Disco.", StringComparison.InvariantCultureIgnoreCase) + select a); + foreach (var discoAssembly in discoAssemblies) + { + List assemblyPluginDefinitions = InitializePluginAssembly(dbContext, discoAssembly, false); + foreach (PluginDefinition definition in assemblyPluginDefinitions) + loadedPlugins[definition.Id] = definition; + } + + // Load External (DataStore) Plugins + DirectoryInfo pluginDirectoryRoot = new DirectoryInfo(PluginPath); + if (pluginDirectoryRoot.Exists) + { + foreach (DirectoryInfo pluginDirectory in pluginDirectoryRoot.EnumerateDirectories()) + { + string pluginAssemblyFilename = Path.Combine(pluginDirectory.FullName, string.Format("{0}.dll", pluginDirectory.Name)); + if (File.Exists(pluginAssemblyFilename)) + { + Assembly pluginAssembly = null; + try + { + pluginAssembly = Assembly.LoadFile(pluginAssemblyFilename); + + if (pluginAssembly != null) + { + PluginsLog.LogInitializingPluginAssembly(pluginAssembly); + List assemblyPluginDefinitions = InitializePluginAssembly(dbContext, pluginAssembly, true); + foreach (PluginDefinition definition in assemblyPluginDefinitions) + loadedPlugins[definition.Id] = definition; + } + } + catch (Exception ex) { PluginsLog.LogInitializeException(pluginAssemblyFilename, ex); } + } + } + } + + // Determine Category Information + CategoryDisplayNames = InitializeCategoryDetails(loadedPlugins.Values); + + + _LoadedPlugins = loadedPlugins; + } + } + } + } + + private static Dictionary InitializeCategoryDetails(IEnumerable plugins) + { + Dictionary categoryDisplayNames = new Dictionary(); + foreach (var pluginDefinition in plugins) + { + if (!categoryDisplayNames.ContainsKey(pluginDefinition.PluginCategoryType)) + { + string displayName = null; + + var displayAttributes = pluginDefinition.PluginCategoryType.GetCustomAttributes(typeof(PluginCategoryAttribute), true); + if (displayAttributes != null && displayAttributes.Length > 0) + displayName = ((PluginCategoryAttribute)(displayAttributes[0])).DisplayName; + + if (string.IsNullOrWhiteSpace(displayName)) + displayName = pluginDefinition.PluginCategoryType.Name; + + categoryDisplayNames[pluginDefinition.PluginCategoryType] = displayName; + } + } + return categoryDisplayNames; + } + + private static List InitializePluginAssembly(DiscoDataContext dbContext, Assembly PluginAssembly, bool ResolveReferences) + { + List pluginDefinitions = new List(); + + var pluginTypes = (from type in PluginAssembly.GetTypes() + where typeof(Plugin).IsAssignableFrom(type) && !type.IsAbstract + select type).ToList(); + + if (pluginTypes.Count > 0) + { + Dictionary referencedAssemblies = null; + + string hostDirectory = Path.GetDirectoryName(PluginAssembly.Location); + + if (ResolveReferences) + referencedAssemblies = ImportReferencedAssemblies(PluginAssembly, hostDirectory); + + foreach (Type t in pluginTypes) + { + var p = InitializePlugin(dbContext, t, hostDirectory, referencedAssemblies); + if (p != null) + pluginDefinitions.Add(p); + } + + } + + return pluginDefinitions; + } + + private static Dictionary ImportReferencedAssemblies(Assembly PluginAssembly, string HostDirectory) + { + Dictionary referencedAssemblies = new Dictionary(); + foreach (string referenceFilename in Directory.EnumerateFiles(HostDirectory, "*.dll", SearchOption.TopDirectoryOnly)) + { + if (!referenceFilename.Equals(PluginAssembly.Location, StringComparison.InvariantCultureIgnoreCase)) + { + try + { + Assembly pluginRefAssembly = Assembly.ReflectionOnlyLoadFrom(referenceFilename); + referencedAssemblies[pluginRefAssembly.FullName] = referenceFilename; + } + catch (Exception) { } // Ignore Load Exceptions + } + } + return referencedAssemblies; + } + + private static PluginDefinition InitializePlugin(DiscoDataContext dbContext, Type PluginType, string HostDirectory, Dictionary ReferencedAssemblies = null) + { + if (!typeof(Plugin).IsAssignableFrom(PluginType)) + throw new ArgumentException(string.Format("Plugins [{0}] does not inherit from [IDiscoPlugin]", PluginType.Name)); + + using (Plugin instance = (Plugin)Activator.CreateInstance(PluginType)) + { + try + { + PluginDefinition definition = new PluginDefinition(instance, HostDirectory, ReferencedAssemblies); + + PluginsLog.LogInitializingPlugin(definition); + + instance.Initalize(dbContext); + + return definition; + } + catch (Exception ex) + { + PluginsLog.LogInitializeException(PluginType.Assembly.Location, ex); + return null; + } + } + } + + #endregion + + #region Plugin Referenced Assemblies Resolving + + public static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) + { + if (args.RequestingAssembly.Location.StartsWith(PluginPath, StringComparison.InvariantCultureIgnoreCase) && _LoadedPlugins != null) + { + // Try best guess first + PluginDefinition requestingPlugin = _LoadedPlugins.Values.Where(p => p.PluginType.Assembly == args.RequestingAssembly).FirstOrDefault(); + if (requestingPlugin != null) + { + Assembly loadedAssembly = CurrentDomain_AssemblyResolve_ByPlugin(requestingPlugin, args); + if (loadedAssembly != null) + return loadedAssembly; + } + + // Try all Plugin References + foreach (var pluginDef in _LoadedPlugins.Values) + { + Assembly loadedAssembly = CurrentDomain_AssemblyResolve_ByPlugin(pluginDef, args); + if (loadedAssembly != null) + return loadedAssembly; + } + } + return null; + } + private static Assembly CurrentDomain_AssemblyResolve_ByPlugin(PluginDefinition PluginDefinition, ResolveEventArgs args) + { + if (PluginDefinition.PluginReferenceAssemblies != null) + { + string assemblyPath; + if (PluginDefinition.PluginReferenceAssemblies.TryGetValue(args.Name, out assemblyPath)) + { + try + { + Assembly loadedAssembly = Assembly.LoadFile(assemblyPath); + + PluginsLog.LogPluginReferenceAssemblyLoaded(args.Name, assemblyPath, args.RequestingAssembly.FullName); + + return loadedAssembly; + } + catch (Exception ex) + { + PluginsLog.LogPluginException(string.Format("Resolving Plugin Reference Assembly: '{0}' [{1}]; Requested by: '{2}' [{3}]; Disco.Plugins.DiscoPlugins.CurrentDomain_AssemblyResolve()", args.Name, assemblyPath, args.RequestingAssembly.FullName, args.RequestingAssembly.Location), ex); + } + } + } + return null; + } + + #endregion + } +} diff --git a/Disco.Services/_Plugins/PluginsLog.cs b/Disco.Services/_Plugins/PluginsLog.cs new file mode 100644 index 00000000..11099258 --- /dev/null +++ b/Disco.Services/_Plugins/PluginsLog.cs @@ -0,0 +1,259 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Disco.Services.Logging; +using Disco.Services.Logging.Models; +using System.Reflection; + +namespace Disco.Services.Plugins +{ + public class PluginsLog : LogBase + { + private const int _ModuleId = 10; + + public override string ModuleDescription { get { return "Plugins"; } } + public override int ModuleId { get { return _ModuleId; } } + public override string ModuleName { get { return "Plugins"; } } + + public enum EventTypeIds + { + InitializingPlugins = 10, + InitializingPluginAssembly, + InitializedPlugin, + InitializeWarning = 15, + InitializeError, + InitializeException, + InitializeExceptionWithInner, + PluginException = 20, + PluginExceptionWithInner, + PluginReferenceAssemblyLoaded = 50, + PluginConfigurationLoaded = 100, + PluginConfigurationSaved = 104, + PluginWebControllerAccessed = 200 + } + + public static PluginsLog Current + { + get + { + return (PluginsLog)LogContext.LogModules[_ModuleId]; + } + } + private static void Log(EventTypeIds EventTypeId, params object[] Args) + { + Current.Log((int)EventTypeId, Args); + } + + public static void LogInitializingPlugins(string PluginDirectory) + { + Current.Log((int)EventTypeIds.InitializingPlugins, PluginDirectory); + } + public static void LogInitializingPluginAssembly(Assembly PluginAssembly) + { + Current.Log((int)EventTypeIds.InitializingPluginAssembly, PluginAssembly.FullName, PluginAssembly.Location); + } + public static void LogInitializingPlugin(PluginDefinition Definition) + { + Current.Log((int)EventTypeIds.InitializedPlugin, Definition.Id, Definition.Version.ToString(3), Definition.PluginType.Name, Definition.PluginType.Assembly.Location); + } + public static void LogInitializeWarning(string Warning) + { + Current.Log((int)EventTypeIds.InitializeWarning, Warning); + } + public static void LogInitializeError(string Error) + { + Current.Log((int)EventTypeIds.InitializeError, Error); + } + public static void LogPluginReferenceAssemblyLoaded(string AssemblyFullName, string AssemblyPath, string RequestedBy) + { + Current.Log((int)EventTypeIds.PluginReferenceAssemblyLoaded, AssemblyFullName, AssemblyPath, RequestedBy); + } + public static void LogPluginConfigurationLoaded(string PluginId, string UserId) + { + Current.Log((int)EventTypeIds.PluginConfigurationLoaded, PluginId, UserId); + } + public static void LogPluginConfigurationSaved(string PluginId, string UserId) + { + Current.Log((int)EventTypeIds.PluginConfigurationSaved, PluginId, UserId); + } + public static void LogPluginWebControllerAccessed(string PluginId, string PluginAction, string UserId) + { + Current.Log((int)EventTypeIds.PluginWebControllerAccessed, PluginId, PluginAction, UserId); + } + + public static void LogInitializeException(string PluginFilename, Exception ex) + { + if (ex.InnerException != null) + { + Log(EventTypeIds.InitializeExceptionWithInner, PluginFilename, ex.GetType().Name, ex.Message, ex.StackTrace, ex.InnerException.GetType().Name, ex.InnerException.Message, ex.InnerException.StackTrace); + } + else + { + Log(EventTypeIds.InitializeException, PluginFilename, ex.GetType().Name, ex.Message, ex.StackTrace); + } + } + + public static void LogPluginException(string Component, Exception ex) + { + if (ex.InnerException != null) + { + Log(EventTypeIds.PluginExceptionWithInner, Component, ex.GetType().Name, ex.Message, ex.StackTrace, ex.InnerException.GetType().Name, ex.InnerException.Message, ex.InnerException.StackTrace); + } + else + { + Log(EventTypeIds.PluginException, Component, ex.GetType().Name, ex.Message, ex.StackTrace); + } + } + + protected override List LoadEventTypes() + { + return new System.Collections.Generic.List + { + new LogEventType + { + Id = (int)EventTypeIds.InitializingPlugins, + ModuleId = _ModuleId, + Name = "Initializing Plugins", + Format = "Starting plugin discovery and initialization from: {0}", + Severity = (int)LogEventType.Severities.Information, + UseLive = false, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.InitializingPluginAssembly, + ModuleId = _ModuleId, + Name = "Initializing Plugin Assembly", + Format = "Initializing Plugin Assembly: [{0}] From '{1}'", + Severity = (int)LogEventType.Severities.Information, + UseLive = false, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.InitializedPlugin, + ModuleId = _ModuleId, + Name = "Initialized Plugin", + Format = "Initialized Plugin: '{0} (v{1})' [{2}] From '{3}'", + Severity = (int)LogEventType.Severities.Information, + UseLive = false, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.InitializeWarning, + ModuleId = _ModuleId, + Name = "Initialize Warning", + Format = "Initialize Warning: {0}", + Severity = (int)LogEventType.Severities.Warning, + UseLive = false, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.InitializeError, + ModuleId = _ModuleId, + Name = "Initialize Error", + Format = "Initialize Error: {0}", + Severity = (int)LogEventType.Severities.Error, + UseLive = false, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.InitializeException, + ModuleId = _ModuleId, + Name = "Initialize Exception", + Format = "Exception: {0}; {1}: {2}; {3}", + Severity = (int)LogEventType.Severities.Error, + UseLive = false, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.InitializeExceptionWithInner, + ModuleId = _ModuleId, + Name = "Initialize Exception with Inner Exception", + Format = "Exception: {0}; {1}: {2}; {3}; Inner: {4}: {5}; {6}", + Severity = (int)LogEventType.Severities.Error, + UseLive = false, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.PluginException, + ModuleId = _ModuleId, + Name = "Plugin Exception", + Format = "Exception: {0}; {1}: {2}; {3}", + Severity = (int)LogEventType.Severities.Error, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.PluginExceptionWithInner, + ModuleId = _ModuleId, + Name = "Plugin Exception with Inner Exception", + Format = "Exception: {0}; {1}: {2}; {3}; Inner: {4}: {5}; {6}", + Severity = (int)LogEventType.Severities.Error, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.PluginReferenceAssemblyLoaded, + ModuleId = _ModuleId, + Name = "Plugin Reference Assembly Loaded", + Format = "Loaded Plugin Reference Assembly: [{0}] From: '{1}'; Requested by: [{2}]", + Severity = (int)LogEventType.Severities.Information, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.PluginConfigurationLoaded, + ModuleId = _ModuleId, + Name = "Plugin Configuration Loaded", + Format = "Plugin Configuration Loaded: [{0}] by [{1}]", + Severity = (int)LogEventType.Severities.Information, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.PluginConfigurationSaved, + ModuleId = _ModuleId, + Name = "Plugin Configuration Saved", + Format = "Plugin Configuration Saved: [{0}] by [{1}]", + Severity = (int)LogEventType.Severities.Information, + UseLive = true, + UsePersist = true, + UseDisplay = true + }, + new LogEventType + { + Id = (int)EventTypeIds.PluginWebControllerAccessed, + ModuleId = _ModuleId, + Name = "Plugin Web Controller Accessed", + Format = "Plugin Web Controller Accessed: Plugin [{0}], Action [{1}], By [{2}]", + Severity = (int)LogEventType.Severities.Information, + UseLive = true, + UsePersist = true, + UseDisplay = true + } + }; + } + } +} diff --git a/Disco.Services/_Plugins/UnknownPluginException.cs b/Disco.Services/_Plugins/UnknownPluginException.cs new file mode 100644 index 00000000..1b1da494 --- /dev/null +++ b/Disco.Services/_Plugins/UnknownPluginException.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Disco.Services.Plugins +{ + public class UnknownPluginException : Exception + { + private string _pluginRequested; + + public string PluginRequested + { + get + { + return _pluginRequested; + } + } + + public UnknownPluginException(string PluginRequested) + { + this._pluginRequested = PluginRequested; + } + public UnknownPluginException(string PluginRequested, string Message) : base(Message) + { + this._pluginRequested = PluginRequested; + } + + public override string Message + { + get + { + return string.Format("Unknown Plugin Id: [{0}]", _pluginRequested); + } + } + } +} diff --git a/Disco.Services/packages.config b/Disco.Services/packages.config new file mode 100644 index 00000000..762682ff --- /dev/null +++ b/Disco.Services/packages.config @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/Disco.Silverlight.AttachmentUpload/App.xaml b/Disco.Silverlight.AttachmentUpload/App.xaml new file mode 100644 index 00000000..ffcbba45 --- /dev/null +++ b/Disco.Silverlight.AttachmentUpload/App.xaml @@ -0,0 +1,14 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Disco.Silverlight.AttachmentUpload/App.xaml.vb b/Disco.Silverlight.AttachmentUpload/App.xaml.vb new file mode 100644 index 00000000..f6914a55 --- /dev/null +++ b/Disco.Silverlight.AttachmentUpload/App.xaml.vb @@ -0,0 +1,37 @@ +Partial Public Class App + Inherits Application + + Public Shared Property UploadUrl As Uri + Public Shared Property MainPage As MainPage + + Public Sub New() + InitializeComponent() + End Sub + + Private Sub Application_Startup(ByVal o As Object, ByVal e As StartupEventArgs) Handles Me.Startup + MainPage = New MainPage + + Me.RootVisual = MainPage + + UploadUrl = New Uri(Application.Current.Host.Source.AbsoluteUri.Substring(0, Application.Current.Host.Source.AbsoluteUri.IndexOf("/", 8)) & e.InitParams.Item("UploadUrl")) + + End Sub + + Private Sub Application_UnhandledException(ByVal sender As Object, ByVal e As ApplicationUnhandledExceptionEventArgs) Handles Me.UnhandledException + + ' If the app is running outside of the debugger then report the exception using + ' the browser's exception mechanism. On IE this will display it a yellow alert + ' icon in the status bar and Firefox will display a script error. + If Not System.Diagnostics.Debugger.IsAttached Then + + ' NOTE: This will allow the application to continue running after an exception has been thrown + ' but not handled. + ' For production applications this error handling should be replaced with something that will + ' report the error to the website and stop the application. + e.Handled = True + Dim errorWindow As ChildWindow = New ErrorWindow(e.ExceptionObject) + errorWindow.Show() + End If + End Sub + +End Class diff --git a/Disco.Silverlight.AttachmentUpload/Assets/Styles.xaml b/Disco.Silverlight.AttachmentUpload/Assets/Styles.xaml new file mode 100644 index 00000000..4487c8f6 --- /dev/null +++ b/Disco.Silverlight.AttachmentUpload/Assets/Styles.xaml @@ -0,0 +1,350 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Disco.Silverlight.AttachmentUpload/Assets/dropTarget.png b/Disco.Silverlight.AttachmentUpload/Assets/dropTarget.png new file mode 100644 index 00000000..5f1eaf57 Binary files /dev/null and b/Disco.Silverlight.AttachmentUpload/Assets/dropTarget.png differ diff --git a/Disco.Silverlight.AttachmentUpload/Disco.Silverlight.AttachmentUpload.vbproj b/Disco.Silverlight.AttachmentUpload/Disco.Silverlight.AttachmentUpload.vbproj new file mode 100644 index 00000000..cc36ec39 --- /dev/null +++ b/Disco.Silverlight.AttachmentUpload/Disco.Silverlight.AttachmentUpload.vbproj @@ -0,0 +1,196 @@ + + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {85747DDC-1389-4A40-9AFC-F57E0992294E} + {A1591282-1198-4647-A2B1-27E5FF5F6F3B};{F184B08F-C81C-45F6-A57F-5ABD9991F28F} + Library + Disco.Silverlight.AttachmentUpload + Disco.Silverlight.AttachmentUpload + Silverlight + v4.0 + $(TargetFrameworkVersion) + true + + + true + true + Disco.Silverlight.AttachmentUpload.xap + My Project\AppManifest.xml + Disco.Silverlight.AttachmentUpload.App + Disco.Silverlight.AttachmentUploadTestPage.html + true + true + false + My Project\OutOfBrowserSettings.xml + true + false + + + + + + v3.5 + + + true + full + true + true + true + true + Empty + Bin\Debug + Disco.Silverlight.AttachmentUpload.xml + 42016,41999,42017,42018,42019,42032,42036,42020,42021,42022 + SILVERLIGHT=1 + + + pdbonly + false + true + true + true + true + Bin\Release + Disco.Silverlight.AttachmentUpload.xml + 42016,41999,42017,42018,42019,42032,42036,42020,42021,42022 + SILVERLIGHT=1 + + + On + + + Binary + + + Off + + + On + + + + ..\..\Resources\Libraries\fastJSON\fastJSON-SL.dll + + + ..\..\Resources\Libraries\FJ.Core\FJ.Core.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Utilities.vb + + + App.xaml + + + + + MainPage.xaml + + + + FileWindow.xaml + + + ErrorWindow.xaml + + + File.xaml + + + Hidden.xaml + + + WebCam.xaml + + + + + + MSBuild:Compile + Designer + + + MSBuild:Compile + Designer + + + Designer + MSBuild:Compile + + + MSBuild:Compile + Designer + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + Designer + MSBuild:Compile + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Disco.Silverlight.AttachmentUpload/FileUploader.vb b/Disco.Silverlight.AttachmentUpload/FileUploader.vb new file mode 100644 index 00000000..091bd17e --- /dev/null +++ b/Disco.Silverlight.AttachmentUpload/FileUploader.vb @@ -0,0 +1,77 @@ +Imports System.Text +Imports Disco.Silverlight.AttachmentUpload.BI + +Public Class FileUploader + + Public Property Url As Uri + Public Property Stream As IO.Stream + Public Property StreamFilename As String + Public Property StreamMimeType As String + Public Property Form As Dictionary(Of String, String) + + Private _HttpSend As HttpWebRequest + Private _UploadBoundaryValue As String + + Private _Complete As UploadComplete + + Public Delegate Sub UploadComplete(Sender As FileUploader, Success As Boolean, Id As Integer) + + Public Sub New(Url As Uri, Stream As IO.Stream, StreamFilename As String, StreamMimeType As String, Form As Dictionary(Of String, String), Optional Complete As UploadComplete = Nothing) + Me.Url = Url + Me.Stream = Stream + Me.StreamFilename = StreamFilename + Me.StreamMimeType = StreamMimeType + Me.Form = Form + Me._Complete = Complete + + Start() + End Sub + + Public Sub Start() + Stream.Position = 0 + Me._HttpSend = WebRequest.Create(App.UploadUrl) + Me._UploadBoundaryValue = ("----------------------------" & DateTime.Now.Ticks.ToString("x")) + Me._HttpSend.ContentType = "multipart/form-data; boundary=" & Me._UploadBoundaryValue + Me._HttpSend.Method = "POST" + Me._HttpSend.BeginGetRequestStream(New AsyncCallback(AddressOf Me.BeginSendRequest), Nothing) + End Sub + + Private Sub BeginSendRequest(ByVal result As IAsyncResult) + Using requestStream As IO.Stream = _HttpSend.EndGetRequestStream(result) + Dim format As String = ("{0}--" & _UploadBoundaryValue & "{0}Content-Disposition: form-data; name=""{1}"";{0}{0}{2}") + Dim pair As KeyValuePair(Of String, String) + For Each pair In Form + Dim str4 As String = String.Format(format, Environment.NewLine, pair.Key, pair.Value) + Dim buffer3 As Byte() = Encoding.UTF8.GetBytes(str4) + requestStream.Write(buffer3, 0, buffer3.Length) + Next + Dim s As String = String.Format(("{0}--" & _UploadBoundaryValue & "{0}Content-Disposition: form-data; name=""{1}""; filename=""{2}""{0}Content-Type: {3}{0}{0}"), Environment.NewLine, "File", Me.StreamFilename, Me.StreamMimeType) + Dim bytes As Byte() = Encoding.UTF8.GetBytes(s) + requestStream.Write(bytes, 0, bytes.Length) + Me.Stream.CopyTo(requestStream) + Me.Stream.Close() + Me.Stream.Dispose() + Dim buffer As Byte() = Encoding.UTF8.GetBytes(String.Format("{0}--{1}{0}", Environment.NewLine, Me._UploadBoundaryValue)) + requestStream.Write(buffer, 0, buffer.Length) + requestStream.Flush() + requestStream.Close() + End Using + _HttpSend.BeginGetResponse(New AsyncCallback(AddressOf Me.BeginGetResponse), Nothing) + End Sub + + Private Sub BeginGetResponse(ByVal result As IAsyncResult) + Dim response As HttpWebResponse = _HttpSend.EndGetResponse(result) + + Dim responseId As Integer = -1 + + If response.StatusCode = 200 Then + Dim responseString = response.GetResponseStream.StreamToString() + Integer.TryParse(responseString, responseId) + End If + + If _Complete IsNot Nothing Then + _Complete.Invoke(Me, (response.StatusCode = 200), responseId) + End If + End Sub + +End Class diff --git a/Disco.Silverlight.AttachmentUpload/JavascriptNavigator.vb b/Disco.Silverlight.AttachmentUpload/JavascriptNavigator.vb new file mode 100644 index 00000000..ca61333d --- /dev/null +++ b/Disco.Silverlight.AttachmentUpload/JavascriptNavigator.vb @@ -0,0 +1,24 @@ +Imports System.Windows.Browser + + +Public Class JavascriptNavigator + + Private _contentFrame As Controls.Frame + + Public Sub New(contentFrame As Controls.Frame) + _contentFrame = contentFrame + End Sub + + + Public Sub Navigate(uri As String) + _contentFrame.Navigate(New Uri(uri, UriKind.Relative)) + End Sub + + + Public ReadOnly Property Location As String + Get + Return _contentFrame.Source.ToString + End Get + End Property + +End Class diff --git a/Disco.Silverlight.AttachmentUpload/MainPage.xaml b/Disco.Silverlight.AttachmentUpload/MainPage.xaml new file mode 100644 index 00000000..d7a0ff47 --- /dev/null +++ b/Disco.Silverlight.AttachmentUpload/MainPage.xaml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Disco.Silverlight.AttachmentUpload/MainPage.xaml.vb b/Disco.Silverlight.AttachmentUpload/MainPage.xaml.vb new file mode 100644 index 00000000..e6900a89 --- /dev/null +++ b/Disco.Silverlight.AttachmentUpload/MainPage.xaml.vb @@ -0,0 +1,60 @@ +Imports System.Windows.Navigation +Imports System.Windows.Browser + +Partial Public Class MainPage + Inherits UserControl + + Private _Navigator As JavascriptNavigator + + Private _UploadingAttachments As New List(Of FileUploader) + + Public Sub New() + InitializeComponent() + + _Navigator = New JavascriptNavigator(Me.ContentFrame) + HtmlPage.RegisterScriptableObject("Navigator", _Navigator) + End Sub + + Private Sub ContentFrame_NavigationFailed(ByVal sender As Object, ByVal e As NavigationFailedEventArgs) Handles ContentFrame.NavigationFailed + e.Handled = True + Dim errorWindow As ChildWindow = New ErrorWindow(e.Uri) + errorWindow.Show() + End Sub + + Public Sub UploadAttachment(stream As IO.Stream, fileName As String, mimeType As String, comments As String) + + Dim form As New Dictionary(Of String, String) + form.Add("comments", comments) + + Dim ua As New FileUploader(App.UploadUrl, stream, fileName, mimeType, form, New FileUploader.UploadComplete(AddressOf UploadComplete)) + + _UploadingAttachments.Add(ua) + + 'Me.NavigationGrid.Visibility = Windows.Visibility.Visible + + End Sub + + Private Sub UploadComplete(Sender As FileUploader, Success As Boolean, Id As Integer) + If _UploadingAttachments.Contains(Sender) Then + _UploadingAttachments.Remove(Sender) + End If + + 'If _UploadingAttachments.Count = 0 Then + ' Me.Dispatcher.BeginInvoke(Function() + ' Me.NavigationGrid.Visibility = Windows.Visibility.Collapsed + ' Return Nothing + ' End Function) + 'End If + + If Id >= 0 Then + Me.Dispatcher.BeginInvoke(Function() + Dim discoFunctions = System.Windows.Browser.HtmlPage.Document.GetProperty("DiscoFunctions") + If discoFunctions IsNot Nothing Then + discoFunctions.addAttachment(Id) + End If + Return (Nothing) + End Function) + End If + End Sub + +End Class \ No newline at end of file diff --git a/Disco.Silverlight.AttachmentUpload/My Project/AppManifest.xml b/Disco.Silverlight.AttachmentUpload/My Project/AppManifest.xml new file mode 100644 index 00000000..41e2af64 --- /dev/null +++ b/Disco.Silverlight.AttachmentUpload/My Project/AppManifest.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/Disco.Silverlight.AttachmentUpload/My Project/AssemblyInfo.vb b/Disco.Silverlight.AttachmentUpload/My Project/AssemblyInfo.vb new file mode 100644 index 00000000..86ccdb53 --- /dev/null +++ b/Disco.Silverlight.AttachmentUpload/My Project/AssemblyInfo.vb @@ -0,0 +1,34 @@ +Imports System +Imports System.Reflection +Imports 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. + +' Review the values of the assembly attributes + + + + + + + + + +'The following GUID is for the ID of the typelib if this project is exposed to COM + + +' 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: +' + + + diff --git a/Disco.Silverlight.AttachmentUpload/Views/ErrorWindow.xaml b/Disco.Silverlight.AttachmentUpload/Views/ErrorWindow.xaml new file mode 100644 index 00000000..26321cf2 --- /dev/null +++ b/Disco.Silverlight.AttachmentUpload/Views/ErrorWindow.xaml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + @if (false) + { } + + diff --git a/Disco.Web/Areas/Config/Views/Expressions/Editor.generated.cs b/Disco.Web/Areas/Config/Views/Expressions/Editor.generated.cs new file mode 100644 index 00000000..5d26562e --- /dev/null +++ b/Disco.Web/Areas/Config/Views/Expressions/Editor.generated.cs @@ -0,0 +1,169 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.17929 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Disco.Web.Areas.Config.Views.Expressions +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using System.Web; + using System.Web.Helpers; + using System.Web.Mvc; + using System.Web.Mvc.Ajax; + using System.Web.Mvc.Html; + using System.Web.Routing; + using System.Web.Security; + using System.Web.UI; + using System.Web.WebPages; + using Disco.BI.Extensions; + using Disco.Models.Repository; + using Disco.Web; + using Disco.Web.Extensions; + + [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorGenerator", "1.5.0.0")] + [System.Web.WebPages.PageVirtualPathAttribute("~/Areas/Config/Views/Expressions/Editor.cshtml")] + public class Editor : System.Web.Mvc.WebViewPage + { + public Editor() + { + } + public override void Execute() + { + + #line 2 "..\..\Areas\Config\Views\Expressions\Editor.cshtml" + + ViewBag.Title = Html.ToBreadcrumb("Configuration", MVC.Config.Config.Index(), "Expressions"); + Html.BundleDeferred("~/ClientScripts/Modules/Disco-ExpressionEditor"); + + + #line default + #line hidden +WriteLiteral("\r\n\r\n \r\n \r\n \r\n

\r\n Parse Error:

\r\n \r\n \r\n \r\n \r\n Validate\r\n"); + + + #line 17 "..\..\Areas\Config\Views\Expressions\Editor.cshtml" + + + #line default + #line hidden + + #line 17 "..\..\Areas\Config\Views\Expressions\Editor.cshtml" + if (false) + { + + #line default + #line hidden +WriteLiteral(" "); + + + #line 18 "..\..\Areas\Config\Views\Expressions\Editor.cshtml" + } + + + #line default + #line hidden +WriteLiteral(" \r\n $(function () {\r\n var initExpression = \'"); + + + #line 21 "..\..\Areas\Config\Views\Expressions\Editor.cshtml" + Write(Model.Expression); + + + #line default + #line hidden +WriteLiteral("\';\r\n var hostSrcUrl = \'"); + + + #line 22 "..\..\Areas\Config\Views\Expressions\Editor.cshtml" + Write(Links.ClientSource.Style.ExpressionEditor_htm); + + + #line default + #line hidden +WriteLiteral(@"'; + var host = $('' : ''); + inst._keyEvent = false; + return html; + }, + + /* Generate the month and year header. */ + _generateMonthYearHeader: function(inst, drawMonth, drawYear, minDate, maxDate, + secondary, monthNames, monthNamesShort) { + var changeMonth = this._get(inst, 'changeMonth'); + var changeYear = this._get(inst, 'changeYear'); + var showMonthAfterYear = this._get(inst, 'showMonthAfterYear'); + var html = '
'; + var monthHtml = ''; + // month selection + if (secondary || !changeMonth) + monthHtml += '' + monthNames[drawMonth] + ''; + else { + var inMinYear = (minDate && minDate.getFullYear() == drawYear); + var inMaxYear = (maxDate && maxDate.getFullYear() == drawYear); + monthHtml += ''; + } + if (!showMonthAfterYear) + html += monthHtml + (secondary || !(changeMonth && changeYear) ? ' ' : ''); + // year selection + if ( !inst.yearshtml ) { + inst.yearshtml = ''; + if (secondary || !changeYear) + html += '' + drawYear + ''; + else { + // determine range of years to display + var years = this._get(inst, 'yearRange').split(':'); + var thisYear = new Date().getFullYear(); + var determineYear = function(value) { + var year = (value.match(/c[+-].*/) ? drawYear + parseInt(value.substring(1), 10) : + (value.match(/[+-].*/) ? thisYear + parseInt(value, 10) : + parseInt(value, 10))); + return (isNaN(year) ? thisYear : year); + }; + var year = determineYear(years[0]); + var endYear = Math.max(year, determineYear(years[1] || '')); + year = (minDate ? Math.max(year, minDate.getFullYear()) : year); + endYear = (maxDate ? Math.min(endYear, maxDate.getFullYear()) : endYear); + inst.yearshtml += ''; + + html += inst.yearshtml; + inst.yearshtml = null; + } + } + html += this._get(inst, 'yearSuffix'); + if (showMonthAfterYear) + html += (secondary || !(changeMonth && changeYear) ? ' ' : '') + monthHtml; + html += '
'; // Close datepicker_header + return html; + }, + + /* Adjust one of the date sub-fields. */ + _adjustInstDate: function(inst, offset, period) { + var year = inst.drawYear + (period == 'Y' ? offset : 0); + var month = inst.drawMonth + (period == 'M' ? offset : 0); + var day = Math.min(inst.selectedDay, this._getDaysInMonth(year, month)) + + (period == 'D' ? offset : 0); + var date = this._restrictMinMax(inst, + this._daylightSavingAdjust(new Date(year, month, day))); + inst.selectedDay = date.getDate(); + inst.drawMonth = inst.selectedMonth = date.getMonth(); + inst.drawYear = inst.selectedYear = date.getFullYear(); + if (period == 'M' || period == 'Y') + this._notifyChange(inst); + }, + + /* Ensure a date is within any min/max bounds. */ + _restrictMinMax: function(inst, date) { + var minDate = this._getMinMaxDate(inst, 'min'); + var maxDate = this._getMinMaxDate(inst, 'max'); + var newDate = (minDate && date < minDate ? minDate : date); + newDate = (maxDate && newDate > maxDate ? maxDate : newDate); + return newDate; + }, + + /* Notify change of month/year. */ + _notifyChange: function(inst) { + var onChange = this._get(inst, 'onChangeMonthYear'); + if (onChange) + onChange.apply((inst.input ? inst.input[0] : null), + [inst.selectedYear, inst.selectedMonth + 1, inst]); + }, + + /* Determine the number of months to show. */ + _getNumberOfMonths: function(inst) { + var numMonths = this._get(inst, 'numberOfMonths'); + return (numMonths == null ? [1, 1] : (typeof numMonths == 'number' ? [1, numMonths] : numMonths)); + }, + + /* Determine the current maximum date - ensure no time components are set. */ + _getMinMaxDate: function(inst, minMax) { + return this._determineDate(inst, this._get(inst, minMax + 'Date'), null); + }, + + /* Find the number of days in a given month. */ + _getDaysInMonth: function(year, month) { + return 32 - this._daylightSavingAdjust(new Date(year, month, 32)).getDate(); + }, + + /* Find the day of the week of the first of a month. */ + _getFirstDayOfMonth: function(year, month) { + return new Date(year, month, 1).getDay(); + }, + + /* Determines if we should allow a "next/prev" month display change. */ + _canAdjustMonth: function(inst, offset, curYear, curMonth) { + var numMonths = this._getNumberOfMonths(inst); + var date = this._daylightSavingAdjust(new Date(curYear, + curMonth + (offset < 0 ? offset : numMonths[0] * numMonths[1]), 1)); + if (offset < 0) + date.setDate(this._getDaysInMonth(date.getFullYear(), date.getMonth())); + return this._isInRange(inst, date); + }, + + /* Is the given date in the accepted range? */ + _isInRange: function(inst, date) { + var minDate = this._getMinMaxDate(inst, 'min'); + var maxDate = this._getMinMaxDate(inst, 'max'); + return ((!minDate || date.getTime() >= minDate.getTime()) && + (!maxDate || date.getTime() <= maxDate.getTime())); + }, + + /* Provide the configuration settings for formatting/parsing. */ + _getFormatConfig: function(inst) { + var shortYearCutoff = this._get(inst, 'shortYearCutoff'); + shortYearCutoff = (typeof shortYearCutoff != 'string' ? shortYearCutoff : + new Date().getFullYear() % 100 + parseInt(shortYearCutoff, 10)); + return {shortYearCutoff: shortYearCutoff, + dayNamesShort: this._get(inst, 'dayNamesShort'), dayNames: this._get(inst, 'dayNames'), + monthNamesShort: this._get(inst, 'monthNamesShort'), monthNames: this._get(inst, 'monthNames')}; + }, + + /* Format the given date for display. */ + _formatDate: function(inst, day, month, year) { + if (!day) { + inst.currentDay = inst.selectedDay; + inst.currentMonth = inst.selectedMonth; + inst.currentYear = inst.selectedYear; + } + var date = (day ? (typeof day == 'object' ? day : + this._daylightSavingAdjust(new Date(year, month, day))) : + this._daylightSavingAdjust(new Date(inst.currentYear, inst.currentMonth, inst.currentDay))); + return this.formatDate(this._get(inst, 'dateFormat'), date, this._getFormatConfig(inst)); + } +}); + +/* + * Bind hover events for datepicker elements. + * Done via delegate so the binding only occurs once in the lifetime of the parent div. + * Global instActive, set by _updateDatepicker allows the handlers to find their way back to the active picker. + */ +function bindHover(dpDiv) { + var selector = 'button, .ui-datepicker-prev, .ui-datepicker-next, .ui-datepicker-calendar td a'; + return dpDiv.delegate(selector, 'mouseout', function() { + $(this).removeClass('ui-state-hover'); + if (this.className.indexOf('ui-datepicker-prev') != -1) $(this).removeClass('ui-datepicker-prev-hover'); + if (this.className.indexOf('ui-datepicker-next') != -1) $(this).removeClass('ui-datepicker-next-hover'); + }) + .delegate(selector, 'mouseover', function(){ + if (!$.datepicker._isDisabledDatepicker( instActive.inline ? dpDiv.parent()[0] : instActive.input[0])) { + $(this).parents('.ui-datepicker-calendar').find('a').removeClass('ui-state-hover'); + $(this).addClass('ui-state-hover'); + if (this.className.indexOf('ui-datepicker-prev') != -1) $(this).addClass('ui-datepicker-prev-hover'); + if (this.className.indexOf('ui-datepicker-next') != -1) $(this).addClass('ui-datepicker-next-hover'); + } + }); +} + +/* jQuery extend now ignores nulls! */ +function extendRemove(target, props) { + $.extend(target, props); + for (var name in props) + if (props[name] == null || props[name] == undefined) + target[name] = props[name]; + return target; +}; + +/* Invoke the datepicker functionality. + @param options string - a command, optionally followed by additional parameters or + Object - settings for attaching new datepicker functionality + @return jQuery object */ +$.fn.datepicker = function(options){ + + /* Verify an empty collection wasn't passed - Fixes #6976 */ + if ( !this.length ) { + return this; + } + + /* Initialise the date picker. */ + if (!$.datepicker.initialized) { + $(document).mousedown($.datepicker._checkExternalClick). + find(document.body).append($.datepicker.dpDiv); + $.datepicker.initialized = true; + } + + var otherArgs = Array.prototype.slice.call(arguments, 1); + if (typeof options == 'string' && (options == 'isDisabled' || options == 'getDate' || options == 'widget')) + return $.datepicker['_' + options + 'Datepicker']. + apply($.datepicker, [this[0]].concat(otherArgs)); + if (options == 'option' && arguments.length == 2 && typeof arguments[1] == 'string') + return $.datepicker['_' + options + 'Datepicker']. + apply($.datepicker, [this[0]].concat(otherArgs)); + return this.each(function() { + typeof options == 'string' ? + $.datepicker['_' + options + 'Datepicker']. + apply($.datepicker, [this].concat(otherArgs)) : + $.datepicker._attachDatepicker(this, options); + }); +}; + +$.datepicker = new Datepicker(); // singleton instance +$.datepicker.initialized = false; +$.datepicker.uuid = new Date().getTime(); +$.datepicker.version = "1.9.2"; + +// Workaround for #4055 +// Add another global to avoid noConflict issues with inline event handlers +window['DP_jQuery_' + dpuuid] = $; + +})(jQuery); + +(function( $, undefined ) { + +var uiDialogClasses = "ui-dialog ui-widget ui-widget-content ui-corner-all ", + sizeRelatedOptions = { + buttons: true, + height: true, + maxHeight: true, + maxWidth: true, + minHeight: true, + minWidth: true, + width: true + }, + resizableRelatedOptions = { + maxHeight: true, + maxWidth: true, + minHeight: true, + minWidth: true + }; + +$.widget("ui.dialog", { + version: "1.9.2", + options: { + autoOpen: true, + buttons: {}, + closeOnEscape: true, + closeText: "close", + dialogClass: "", + draggable: true, + hide: null, + height: "auto", + maxHeight: false, + maxWidth: false, + minHeight: 150, + minWidth: 150, + modal: false, + position: { + my: "center", + at: "center", + of: window, + collision: "fit", + // ensure that the titlebar is never outside the document + using: function( pos ) { + var topOffset = $( this ).css( pos ).offset().top; + if ( topOffset < 0 ) { + $( this ).css( "top", pos.top - topOffset ); + } + } + }, + resizable: true, + show: null, + stack: true, + title: "", + width: 300, + zIndex: 1000 + }, + + _create: function() { + this.originalTitle = this.element.attr( "title" ); + // #5742 - .attr() might return a DOMElement + if ( typeof this.originalTitle !== "string" ) { + this.originalTitle = ""; + } + this.oldPosition = { + parent: this.element.parent(), + index: this.element.parent().children().index( this.element ) + }; + this.options.title = this.options.title || this.originalTitle; + var that = this, + options = this.options, + + title = options.title || " ", + uiDialog, + uiDialogTitlebar, + uiDialogTitlebarClose, + uiDialogTitle, + uiDialogButtonPane; + + uiDialog = ( this.uiDialog = $( "
" ) ) + .addClass( uiDialogClasses + options.dialogClass ) + .css({ + display: "none", + outline: 0, // TODO: move to stylesheet + zIndex: options.zIndex + }) + // setting tabIndex makes the div focusable + .attr( "tabIndex", -1) + .keydown(function( event ) { + if ( options.closeOnEscape && !event.isDefaultPrevented() && event.keyCode && + event.keyCode === $.ui.keyCode.ESCAPE ) { + that.close( event ); + event.preventDefault(); + } + }) + .mousedown(function( event ) { + that.moveToTop( false, event ); + }) + .appendTo( "body" ); + + this.element + .show() + .removeAttr( "title" ) + .addClass( "ui-dialog-content ui-widget-content" ) + .appendTo( uiDialog ); + + uiDialogTitlebar = ( this.uiDialogTitlebar = $( "
" ) ) + .addClass( "ui-dialog-titlebar ui-widget-header " + + "ui-corner-all ui-helper-clearfix" ) + .bind( "mousedown", function() { + // Dialog isn't getting focus when dragging (#8063) + uiDialog.focus(); + }) + .prependTo( uiDialog ); + + uiDialogTitlebarClose = $( "" ) + .addClass( "ui-dialog-titlebar-close ui-corner-all" ) + .attr( "role", "button" ) + .click(function( event ) { + event.preventDefault(); + that.close( event ); + }) + .appendTo( uiDialogTitlebar ); + + ( this.uiDialogTitlebarCloseText = $( "" ) ) + .addClass( "ui-icon ui-icon-closethick" ) + .text( options.closeText ) + .appendTo( uiDialogTitlebarClose ); + + uiDialogTitle = $( "" ) + .uniqueId() + .addClass( "ui-dialog-title" ) + .html( title ) + .prependTo( uiDialogTitlebar ); + + uiDialogButtonPane = ( this.uiDialogButtonPane = $( "
" ) ) + .addClass( "ui-dialog-buttonpane ui-widget-content ui-helper-clearfix" ); + + ( this.uiButtonSet = $( "
" ) ) + .addClass( "ui-dialog-buttonset" ) + .appendTo( uiDialogButtonPane ); + + uiDialog.attr({ + role: "dialog", + "aria-labelledby": uiDialogTitle.attr( "id" ) + }); + + uiDialogTitlebar.find( "*" ).add( uiDialogTitlebar ).disableSelection(); + this._hoverable( uiDialogTitlebarClose ); + this._focusable( uiDialogTitlebarClose ); + + if ( options.draggable && $.fn.draggable ) { + this._makeDraggable(); + } + if ( options.resizable && $.fn.resizable ) { + this._makeResizable(); + } + + this._createButtons( options.buttons ); + this._isOpen = false; + + if ( $.fn.bgiframe ) { + uiDialog.bgiframe(); + } + + // prevent tabbing out of modal dialogs + this._on( uiDialog, { keydown: function( event ) { + if ( !options.modal || event.keyCode !== $.ui.keyCode.TAB ) { + return; + } + + var tabbables = $( ":tabbable", uiDialog ), + first = tabbables.filter( ":first" ), + last = tabbables.filter( ":last" ); + + if ( event.target === last[0] && !event.shiftKey ) { + first.focus( 1 ); + return false; + } else if ( event.target === first[0] && event.shiftKey ) { + last.focus( 1 ); + return false; + } + }}); + }, + + _init: function() { + if ( this.options.autoOpen ) { + this.open(); + } + }, + + _destroy: function() { + var next, + oldPosition = this.oldPosition; + + if ( this.overlay ) { + this.overlay.destroy(); + } + this.uiDialog.hide(); + this.element + .removeClass( "ui-dialog-content ui-widget-content" ) + .hide() + .appendTo( "body" ); + this.uiDialog.remove(); + + if ( this.originalTitle ) { + this.element.attr( "title", this.originalTitle ); + } + + next = oldPosition.parent.children().eq( oldPosition.index ); + // Don't try to place the dialog next to itself (#8613) + if ( next.length && next[ 0 ] !== this.element[ 0 ] ) { + next.before( this.element ); + } else { + oldPosition.parent.append( this.element ); + } + }, + + widget: function() { + return this.uiDialog; + }, + + close: function( event ) { + var that = this, + maxZ, thisZ; + + if ( !this._isOpen ) { + return; + } + + if ( false === this._trigger( "beforeClose", event ) ) { + return; + } + + this._isOpen = false; + + if ( this.overlay ) { + this.overlay.destroy(); + } + + if ( this.options.hide ) { + this._hide( this.uiDialog, this.options.hide, function() { + that._trigger( "close", event ); + }); + } else { + this.uiDialog.hide(); + this._trigger( "close", event ); + } + + $.ui.dialog.overlay.resize(); + + // adjust the maxZ to allow other modal dialogs to continue to work (see #4309) + if ( this.options.modal ) { + maxZ = 0; + $( ".ui-dialog" ).each(function() { + if ( this !== that.uiDialog[0] ) { + thisZ = $( this ).css( "z-index" ); + if ( !isNaN( thisZ ) ) { + maxZ = Math.max( maxZ, thisZ ); + } + } + }); + $.ui.dialog.maxZ = maxZ; + } + + return this; + }, + + isOpen: function() { + return this._isOpen; + }, + + // the force parameter allows us to move modal dialogs to their correct + // position on open + moveToTop: function( force, event ) { + var options = this.options, + saveScroll; + + if ( ( options.modal && !force ) || + ( !options.stack && !options.modal ) ) { + return this._trigger( "focus", event ); + } + + if ( options.zIndex > $.ui.dialog.maxZ ) { + $.ui.dialog.maxZ = options.zIndex; + } + if ( this.overlay ) { + $.ui.dialog.maxZ += 1; + $.ui.dialog.overlay.maxZ = $.ui.dialog.maxZ; + this.overlay.$el.css( "z-index", $.ui.dialog.overlay.maxZ ); + } + + // Save and then restore scroll + // Opera 9.5+ resets when parent z-index is changed. + // http://bugs.jqueryui.com/ticket/3193 + saveScroll = { + scrollTop: this.element.scrollTop(), + scrollLeft: this.element.scrollLeft() + }; + $.ui.dialog.maxZ += 1; + this.uiDialog.css( "z-index", $.ui.dialog.maxZ ); + this.element.attr( saveScroll ); + this._trigger( "focus", event ); + + return this; + }, + + open: function() { + if ( this._isOpen ) { + return; + } + + var hasFocus, + options = this.options, + uiDialog = this.uiDialog; + + this._size(); + this._position( options.position ); + uiDialog.show( options.show ); + this.overlay = options.modal ? new $.ui.dialog.overlay( this ) : null; + this.moveToTop( true ); + + // set focus to the first tabbable element in the content area or the first button + // if there are no tabbable elements, set focus on the dialog itself + hasFocus = this.element.find( ":tabbable" ); + if ( !hasFocus.length ) { + hasFocus = this.uiDialogButtonPane.find( ":tabbable" ); + if ( !hasFocus.length ) { + hasFocus = uiDialog; + } + } + hasFocus.eq( 0 ).focus(); + + this._isOpen = true; + this._trigger( "open" ); + + return this; + }, + + _createButtons: function( buttons ) { + var that = this, + hasButtons = false; + + // if we already have a button pane, remove it + this.uiDialogButtonPane.remove(); + this.uiButtonSet.empty(); + + if ( typeof buttons === "object" && buttons !== null ) { + $.each( buttons, function() { + return !(hasButtons = true); + }); + } + if ( hasButtons ) { + $.each( buttons, function( name, props ) { + var button, click; + props = $.isFunction( props ) ? + { click: props, text: name } : + props; + // Default to a non-submitting button + props = $.extend( { type: "button" }, props ); + // Change the context for the click callback to be the main element + click = props.click; + props.click = function() { + click.apply( that.element[0], arguments ); + }; + button = $( "", props ) + .appendTo( that.uiButtonSet ); + if ( $.fn.button ) { + button.button(); + } + }); + this.uiDialog.addClass( "ui-dialog-buttons" ); + this.uiDialogButtonPane.appendTo( this.uiDialog ); + } else { + this.uiDialog.removeClass( "ui-dialog-buttons" ); + } + }, + + _makeDraggable: function() { + var that = this, + options = this.options; + + function filteredUi( ui ) { + return { + position: ui.position, + offset: ui.offset + }; + } + + this.uiDialog.draggable({ + cancel: ".ui-dialog-content, .ui-dialog-titlebar-close", + handle: ".ui-dialog-titlebar", + containment: "document", + start: function( event, ui ) { + $( this ) + .addClass( "ui-dialog-dragging" ); + that._trigger( "dragStart", event, filteredUi( ui ) ); + }, + drag: function( event, ui ) { + that._trigger( "drag", event, filteredUi( ui ) ); + }, + stop: function( event, ui ) { + options.position = [ + ui.position.left - that.document.scrollLeft(), + ui.position.top - that.document.scrollTop() + ]; + $( this ) + .removeClass( "ui-dialog-dragging" ); + that._trigger( "dragStop", event, filteredUi( ui ) ); + $.ui.dialog.overlay.resize(); + } + }); + }, + + _makeResizable: function( handles ) { + handles = (handles === undefined ? this.options.resizable : handles); + var that = this, + options = this.options, + // .ui-resizable has position: relative defined in the stylesheet + // but dialogs have to use absolute or fixed positioning + position = this.uiDialog.css( "position" ), + resizeHandles = typeof handles === 'string' ? + handles : + "n,e,s,w,se,sw,ne,nw"; + + function filteredUi( ui ) { + return { + originalPosition: ui.originalPosition, + originalSize: ui.originalSize, + position: ui.position, + size: ui.size + }; + } + + this.uiDialog.resizable({ + cancel: ".ui-dialog-content", + containment: "document", + alsoResize: this.element, + maxWidth: options.maxWidth, + maxHeight: options.maxHeight, + minWidth: options.minWidth, + minHeight: this._minHeight(), + handles: resizeHandles, + start: function( event, ui ) { + $( this ).addClass( "ui-dialog-resizing" ); + that._trigger( "resizeStart", event, filteredUi( ui ) ); + }, + resize: function( event, ui ) { + that._trigger( "resize", event, filteredUi( ui ) ); + }, + stop: function( event, ui ) { + $( this ).removeClass( "ui-dialog-resizing" ); + options.height = $( this ).height(); + options.width = $( this ).width(); + that._trigger( "resizeStop", event, filteredUi( ui ) ); + $.ui.dialog.overlay.resize(); + } + }) + .css( "position", position ) + .find( ".ui-resizable-se" ) + .addClass( "ui-icon ui-icon-grip-diagonal-se" ); + }, + + _minHeight: function() { + var options = this.options; + + if ( options.height === "auto" ) { + return options.minHeight; + } else { + return Math.min( options.minHeight, options.height ); + } + }, + + _position: function( position ) { + var myAt = [], + offset = [ 0, 0 ], + isVisible; + + if ( position ) { + // deep extending converts arrays to objects in jQuery <= 1.3.2 :-( + // if (typeof position == 'string' || $.isArray(position)) { + // myAt = $.isArray(position) ? position : position.split(' '); + + if ( typeof position === "string" || (typeof position === "object" && "0" in position ) ) { + myAt = position.split ? position.split( " " ) : [ position[ 0 ], position[ 1 ] ]; + if ( myAt.length === 1 ) { + myAt[ 1 ] = myAt[ 0 ]; + } + + $.each( [ "left", "top" ], function( i, offsetPosition ) { + if ( +myAt[ i ] === myAt[ i ] ) { + offset[ i ] = myAt[ i ]; + myAt[ i ] = offsetPosition; + } + }); + + position = { + my: myAt[0] + (offset[0] < 0 ? offset[0] : "+" + offset[0]) + " " + + myAt[1] + (offset[1] < 0 ? offset[1] : "+" + offset[1]), + at: myAt.join( " " ) + }; + } + + position = $.extend( {}, $.ui.dialog.prototype.options.position, position ); + } else { + position = $.ui.dialog.prototype.options.position; + } + + // need to show the dialog to get the actual offset in the position plugin + isVisible = this.uiDialog.is( ":visible" ); + if ( !isVisible ) { + this.uiDialog.show(); + } + this.uiDialog.position( position ); + if ( !isVisible ) { + this.uiDialog.hide(); + } + }, + + _setOptions: function( options ) { + var that = this, + resizableOptions = {}, + resize = false; + + $.each( options, function( key, value ) { + that._setOption( key, value ); + + if ( key in sizeRelatedOptions ) { + resize = true; + } + if ( key in resizableRelatedOptions ) { + resizableOptions[ key ] = value; + } + }); + + if ( resize ) { + this._size(); + } + if ( this.uiDialog.is( ":data(resizable)" ) ) { + this.uiDialog.resizable( "option", resizableOptions ); + } + }, + + _setOption: function( key, value ) { + var isDraggable, isResizable, + uiDialog = this.uiDialog; + + switch ( key ) { + case "buttons": + this._createButtons( value ); + break; + case "closeText": + // ensure that we always pass a string + this.uiDialogTitlebarCloseText.text( "" + value ); + break; + case "dialogClass": + uiDialog + .removeClass( this.options.dialogClass ) + .addClass( uiDialogClasses + value ); + break; + case "disabled": + if ( value ) { + uiDialog.addClass( "ui-dialog-disabled" ); + } else { + uiDialog.removeClass( "ui-dialog-disabled" ); + } + break; + case "draggable": + isDraggable = uiDialog.is( ":data(draggable)" ); + if ( isDraggable && !value ) { + uiDialog.draggable( "destroy" ); + } + + if ( !isDraggable && value ) { + this._makeDraggable(); + } + break; + case "position": + this._position( value ); + break; + case "resizable": + // currently resizable, becoming non-resizable + isResizable = uiDialog.is( ":data(resizable)" ); + if ( isResizable && !value ) { + uiDialog.resizable( "destroy" ); + } + + // currently resizable, changing handles + if ( isResizable && typeof value === "string" ) { + uiDialog.resizable( "option", "handles", value ); + } + + // currently non-resizable, becoming resizable + if ( !isResizable && value !== false ) { + this._makeResizable( value ); + } + break; + case "title": + // convert whatever was passed in o a string, for html() to not throw up + $( ".ui-dialog-title", this.uiDialogTitlebar ) + .html( "" + ( value || " " ) ); + break; + } + + this._super( key, value ); + }, + + _size: function() { + /* If the user has resized the dialog, the .ui-dialog and .ui-dialog-content + * divs will both have width and height set, so we need to reset them + */ + var nonContentHeight, minContentHeight, autoHeight, + options = this.options, + isVisible = this.uiDialog.is( ":visible" ); + + // reset content sizing + this.element.show().css({ + width: "auto", + minHeight: 0, + height: 0 + }); + + if ( options.minWidth > options.width ) { + options.width = options.minWidth; + } + + // reset wrapper sizing + // determine the height of all the non-content elements + nonContentHeight = this.uiDialog.css({ + height: "auto", + width: options.width + }) + .outerHeight(); + minContentHeight = Math.max( 0, options.minHeight - nonContentHeight ); + + if ( options.height === "auto" ) { + // only needed for IE6 support + if ( $.support.minHeight ) { + this.element.css({ + minHeight: minContentHeight, + height: "auto" + }); + } else { + this.uiDialog.show(); + autoHeight = this.element.css( "height", "auto" ).height(); + if ( !isVisible ) { + this.uiDialog.hide(); + } + this.element.height( Math.max( autoHeight, minContentHeight ) ); + } + } else { + this.element.height( Math.max( options.height - nonContentHeight, 0 ) ); + } + + if (this.uiDialog.is( ":data(resizable)" ) ) { + this.uiDialog.resizable( "option", "minHeight", this._minHeight() ); + } + } +}); + +$.extend($.ui.dialog, { + uuid: 0, + maxZ: 0, + + getTitleId: function($el) { + var id = $el.attr( "id" ); + if ( !id ) { + this.uuid += 1; + id = this.uuid; + } + return "ui-dialog-title-" + id; + }, + + overlay: function( dialog ) { + this.$el = $.ui.dialog.overlay.create( dialog ); + } +}); + +$.extend( $.ui.dialog.overlay, { + instances: [], + // reuse old instances due to IE memory leak with alpha transparency (see #5185) + oldInstances: [], + maxZ: 0, + events: $.map( + "focus,mousedown,mouseup,keydown,keypress,click".split( "," ), + function( event ) { + return event + ".dialog-overlay"; + } + ).join( " " ), + create: function( dialog ) { + if ( this.instances.length === 0 ) { + // prevent use of anchors and inputs + // we use a setTimeout in case the overlay is created from an + // event that we're going to be cancelling (see #2804) + setTimeout(function() { + // handle $(el).dialog().dialog('close') (see #4065) + if ( $.ui.dialog.overlay.instances.length ) { + $( document ).bind( $.ui.dialog.overlay.events, function( event ) { + // stop events if the z-index of the target is < the z-index of the overlay + // we cannot return true when we don't want to cancel the event (#3523) + if ( $( event.target ).zIndex() < $.ui.dialog.overlay.maxZ ) { + return false; + } + }); + } + }, 1 ); + + // handle window resize + $( window ).bind( "resize.dialog-overlay", $.ui.dialog.overlay.resize ); + } + + var $el = ( this.oldInstances.pop() || $( "
" ).addClass( "ui-widget-overlay" ) ); + + // allow closing by pressing the escape key + $( document ).bind( "keydown.dialog-overlay", function( event ) { + var instances = $.ui.dialog.overlay.instances; + // only react to the event if we're the top overlay + if ( instances.length !== 0 && instances[ instances.length - 1 ] === $el && + dialog.options.closeOnEscape && !event.isDefaultPrevented() && event.keyCode && + event.keyCode === $.ui.keyCode.ESCAPE ) { + + dialog.close( event ); + event.preventDefault(); + } + }); + + $el.appendTo( document.body ).css({ + width: this.width(), + height: this.height() + }); + + if ( $.fn.bgiframe ) { + $el.bgiframe(); + } + + this.instances.push( $el ); + return $el; + }, + + destroy: function( $el ) { + var indexOf = $.inArray( $el, this.instances ), + maxZ = 0; + + if ( indexOf !== -1 ) { + this.oldInstances.push( this.instances.splice( indexOf, 1 )[ 0 ] ); + } + + if ( this.instances.length === 0 ) { + $( [ document, window ] ).unbind( ".dialog-overlay" ); + } + + $el.height( 0 ).width( 0 ).remove(); + + // adjust the maxZ to allow other modal dialogs to continue to work (see #4309) + $.each( this.instances, function() { + maxZ = Math.max( maxZ, this.css( "z-index" ) ); + }); + this.maxZ = maxZ; + }, + + height: function() { + var scrollHeight, + offsetHeight; + // handle IE + if ( $.ui.ie ) { + scrollHeight = Math.max( + document.documentElement.scrollHeight, + document.body.scrollHeight + ); + offsetHeight = Math.max( + document.documentElement.offsetHeight, + document.body.offsetHeight + ); + + if ( scrollHeight < offsetHeight ) { + return $( window ).height() + "px"; + } else { + return scrollHeight + "px"; + } + // handle "good" browsers + } else { + return $( document ).height() + "px"; + } + }, + + width: function() { + var scrollWidth, + offsetWidth; + // handle IE + if ( $.ui.ie ) { + scrollWidth = Math.max( + document.documentElement.scrollWidth, + document.body.scrollWidth + ); + offsetWidth = Math.max( + document.documentElement.offsetWidth, + document.body.offsetWidth + ); + + if ( scrollWidth < offsetWidth ) { + return $( window ).width() + "px"; + } else { + return scrollWidth + "px"; + } + // handle "good" browsers + } else { + return $( document ).width() + "px"; + } + }, + + resize: function() { + /* If the dialog is draggable and the user drags it past the + * right edge of the window, the document becomes wider so we + * need to stretch the overlay. If the user then drags the + * dialog back to the left, the document will become narrower, + * so we need to shrink the overlay to the appropriate size. + * This is handled by shrinking the overlay before setting it + * to the full document size. + */ + var $overlays = $( [] ); + $.each( $.ui.dialog.overlay.instances, function() { + $overlays = $overlays.add( this ); + }); + + $overlays.css({ + width: 0, + height: 0 + }).css({ + width: $.ui.dialog.overlay.width(), + height: $.ui.dialog.overlay.height() + }); + } +}); + +$.extend( $.ui.dialog.overlay.prototype, { + destroy: function() { + $.ui.dialog.overlay.destroy( this.$el ); + } +}); + +}( jQuery ) ); + +(function( $, undefined ) { + +var rvertical = /up|down|vertical/, + rpositivemotion = /up|left|vertical|horizontal/; + +$.effects.effect.blind = function( o, done ) { + // Create element + var el = $( this ), + props = [ "position", "top", "bottom", "left", "right", "height", "width" ], + mode = $.effects.setMode( el, o.mode || "hide" ), + direction = o.direction || "up", + vertical = rvertical.test( direction ), + ref = vertical ? "height" : "width", + ref2 = vertical ? "top" : "left", + motion = rpositivemotion.test( direction ), + animation = {}, + show = mode === "show", + wrapper, distance, margin; + + // if already wrapped, the wrapper's properties are my property. #6245 + if ( el.parent().is( ".ui-effects-wrapper" ) ) { + $.effects.save( el.parent(), props ); + } else { + $.effects.save( el, props ); + } + el.show(); + wrapper = $.effects.createWrapper( el ).css({ + overflow: "hidden" + }); + + distance = wrapper[ ref ](); + margin = parseFloat( wrapper.css( ref2 ) ) || 0; + + animation[ ref ] = show ? distance : 0; + if ( !motion ) { + el + .css( vertical ? "bottom" : "right", 0 ) + .css( vertical ? "top" : "left", "auto" ) + .css({ position: "absolute" }); + + animation[ ref2 ] = show ? margin : distance + margin; + } + + // start at 0 if we are showing + if ( show ) { + wrapper.css( ref, 0 ); + if ( ! motion ) { + wrapper.css( ref2, margin + distance ); + } + } + + // Animate + wrapper.animate( animation, { + duration: o.duration, + easing: o.easing, + queue: false, + complete: function() { + if ( mode === "hide" ) { + el.hide(); + } + $.effects.restore( el, props ); + $.effects.removeWrapper( el ); + done(); + } + }); + +}; + +})(jQuery); + +(function( $, undefined ) { + +$.effects.effect.bounce = function( o, done ) { + var el = $( this ), + props = [ "position", "top", "bottom", "left", "right", "height", "width" ], + + // defaults: + mode = $.effects.setMode( el, o.mode || "effect" ), + hide = mode === "hide", + show = mode === "show", + direction = o.direction || "up", + distance = o.distance, + times = o.times || 5, + + // number of internal animations + anims = times * 2 + ( show || hide ? 1 : 0 ), + speed = o.duration / anims, + easing = o.easing, + + // utility: + ref = ( direction === "up" || direction === "down" ) ? "top" : "left", + motion = ( direction === "up" || direction === "left" ), + i, + upAnim, + downAnim, + + // we will need to re-assemble the queue to stack our animations in place + queue = el.queue(), + queuelen = queue.length; + + // Avoid touching opacity to prevent clearType and PNG issues in IE + if ( show || hide ) { + props.push( "opacity" ); + } + + $.effects.save( el, props ); + el.show(); + $.effects.createWrapper( el ); // Create Wrapper + + // default distance for the BIGGEST bounce is the outer Distance / 3 + if ( !distance ) { + distance = el[ ref === "top" ? "outerHeight" : "outerWidth" ]() / 3; + } + + if ( show ) { + downAnim = { opacity: 1 }; + downAnim[ ref ] = 0; + + // if we are showing, force opacity 0 and set the initial position + // then do the "first" animation + el.css( "opacity", 0 ) + .css( ref, motion ? -distance * 2 : distance * 2 ) + .animate( downAnim, speed, easing ); + } + + // start at the smallest distance if we are hiding + if ( hide ) { + distance = distance / Math.pow( 2, times - 1 ); + } + + downAnim = {}; + downAnim[ ref ] = 0; + // Bounces up/down/left/right then back to 0 -- times * 2 animations happen here + for ( i = 0; i < times; i++ ) { + upAnim = {}; + upAnim[ ref ] = ( motion ? "-=" : "+=" ) + distance; + + el.animate( upAnim, speed, easing ) + .animate( downAnim, speed, easing ); + + distance = hide ? distance * 2 : distance / 2; + } + + // Last Bounce when Hiding + if ( hide ) { + upAnim = { opacity: 0 }; + upAnim[ ref ] = ( motion ? "-=" : "+=" ) + distance; + + el.animate( upAnim, speed, easing ); + } + + el.queue(function() { + if ( hide ) { + el.hide(); + } + $.effects.restore( el, props ); + $.effects.removeWrapper( el ); + done(); + }); + + // inject all the animations we just queued to be first in line (after "inprogress") + if ( queuelen > 1) { + queue.splice.apply( queue, + [ 1, 0 ].concat( queue.splice( queuelen, anims + 1 ) ) ); + } + el.dequeue(); + +}; + +})(jQuery); + +(function( $, undefined ) { + +$.effects.effect.clip = function( o, done ) { + // Create element + var el = $( this ), + props = [ "position", "top", "bottom", "left", "right", "height", "width" ], + mode = $.effects.setMode( el, o.mode || "hide" ), + show = mode === "show", + direction = o.direction || "vertical", + vert = direction === "vertical", + size = vert ? "height" : "width", + position = vert ? "top" : "left", + animation = {}, + wrapper, animate, distance; + + // Save & Show + $.effects.save( el, props ); + el.show(); + + // Create Wrapper + wrapper = $.effects.createWrapper( el ).css({ + overflow: "hidden" + }); + animate = ( el[0].tagName === "IMG" ) ? wrapper : el; + distance = animate[ size ](); + + // Shift + if ( show ) { + animate.css( size, 0 ); + animate.css( position, distance / 2 ); + } + + // Create Animation Object: + animation[ size ] = show ? distance : 0; + animation[ position ] = show ? 0 : distance / 2; + + // Animate + animate.animate( animation, { + queue: false, + duration: o.duration, + easing: o.easing, + complete: function() { + if ( !show ) { + el.hide(); + } + $.effects.restore( el, props ); + $.effects.removeWrapper( el ); + done(); + } + }); + +}; + +})(jQuery); + +(function( $, undefined ) { + +$.effects.effect.drop = function( o, done ) { + + var el = $( this ), + props = [ "position", "top", "bottom", "left", "right", "opacity", "height", "width" ], + mode = $.effects.setMode( el, o.mode || "hide" ), + show = mode === "show", + direction = o.direction || "left", + ref = ( direction === "up" || direction === "down" ) ? "top" : "left", + motion = ( direction === "up" || direction === "left" ) ? "pos" : "neg", + animation = { + opacity: show ? 1 : 0 + }, + distance; + + // Adjust + $.effects.save( el, props ); + el.show(); + $.effects.createWrapper( el ); + + distance = o.distance || el[ ref === "top" ? "outerHeight": "outerWidth" ]( true ) / 2; + + if ( show ) { + el + .css( "opacity", 0 ) + .css( ref, motion === "pos" ? -distance : distance ); + } + + // Animation + animation[ ref ] = ( show ? + ( motion === "pos" ? "+=" : "-=" ) : + ( motion === "pos" ? "-=" : "+=" ) ) + + distance; + + // Animate + el.animate( animation, { + queue: false, + duration: o.duration, + easing: o.easing, + complete: function() { + if ( mode === "hide" ) { + el.hide(); + } + $.effects.restore( el, props ); + $.effects.removeWrapper( el ); + done(); + } + }); +}; + +})(jQuery); + +(function( $, undefined ) { + +$.effects.effect.explode = function( o, done ) { + + var rows = o.pieces ? Math.round( Math.sqrt( o.pieces ) ) : 3, + cells = rows, + el = $( this ), + mode = $.effects.setMode( el, o.mode || "hide" ), + show = mode === "show", + + // show and then visibility:hidden the element before calculating offset + offset = el.show().css( "visibility", "hidden" ).offset(), + + // width and height of a piece + width = Math.ceil( el.outerWidth() / cells ), + height = Math.ceil( el.outerHeight() / rows ), + pieces = [], + + // loop + i, j, left, top, mx, my; + + // children animate complete: + function childComplete() { + pieces.push( this ); + if ( pieces.length === rows * cells ) { + animComplete(); + } + } + + // clone the element for each row and cell. + for( i = 0; i < rows ; i++ ) { // ===> + top = offset.top + i * height; + my = i - ( rows - 1 ) / 2 ; + + for( j = 0; j < cells ; j++ ) { // ||| + left = offset.left + j * width; + mx = j - ( cells - 1 ) / 2 ; + + // Create a clone of the now hidden main element that will be absolute positioned + // within a wrapper div off the -left and -top equal to size of our pieces + el + .clone() + .appendTo( "body" ) + .wrap( "
" ) + .css({ + position: "absolute", + visibility: "visible", + left: -j * width, + top: -i * height + }) + + // select the wrapper - make it overflow: hidden and absolute positioned based on + // where the original was located +left and +top equal to the size of pieces + .parent() + .addClass( "ui-effects-explode" ) + .css({ + position: "absolute", + overflow: "hidden", + width: width, + height: height, + left: left + ( show ? mx * width : 0 ), + top: top + ( show ? my * height : 0 ), + opacity: show ? 0 : 1 + }).animate({ + left: left + ( show ? 0 : mx * width ), + top: top + ( show ? 0 : my * height ), + opacity: show ? 1 : 0 + }, o.duration || 500, o.easing, childComplete ); + } + } + + function animComplete() { + el.css({ + visibility: "visible" + }); + $( pieces ).remove(); + if ( !show ) { + el.hide(); + } + done(); + } +}; + +})(jQuery); + +(function( $, undefined ) { + +$.effects.effect.fade = function( o, done ) { + var el = $( this ), + mode = $.effects.setMode( el, o.mode || "toggle" ); + + el.animate({ + opacity: mode + }, { + queue: false, + duration: o.duration, + easing: o.easing, + complete: done + }); +}; + +})( jQuery ); + +(function( $, undefined ) { + +$.effects.effect.fold = function( o, done ) { + + // Create element + var el = $( this ), + props = [ "position", "top", "bottom", "left", "right", "height", "width" ], + mode = $.effects.setMode( el, o.mode || "hide" ), + show = mode === "show", + hide = mode === "hide", + size = o.size || 15, + percent = /([0-9]+)%/.exec( size ), + horizFirst = !!o.horizFirst, + widthFirst = show !== horizFirst, + ref = widthFirst ? [ "width", "height" ] : [ "height", "width" ], + duration = o.duration / 2, + wrapper, distance, + animation1 = {}, + animation2 = {}; + + $.effects.save( el, props ); + el.show(); + + // Create Wrapper + wrapper = $.effects.createWrapper( el ).css({ + overflow: "hidden" + }); + distance = widthFirst ? + [ wrapper.width(), wrapper.height() ] : + [ wrapper.height(), wrapper.width() ]; + + if ( percent ) { + size = parseInt( percent[ 1 ], 10 ) / 100 * distance[ hide ? 0 : 1 ]; + } + if ( show ) { + wrapper.css( horizFirst ? { + height: 0, + width: size + } : { + height: size, + width: 0 + }); + } + + // Animation + animation1[ ref[ 0 ] ] = show ? distance[ 0 ] : size; + animation2[ ref[ 1 ] ] = show ? distance[ 1 ] : 0; + + // Animate + wrapper + .animate( animation1, duration, o.easing ) + .animate( animation2, duration, o.easing, function() { + if ( hide ) { + el.hide(); + } + $.effects.restore( el, props ); + $.effects.removeWrapper( el ); + done(); + }); + +}; + +})(jQuery); + +(function( $, undefined ) { + +$.effects.effect.highlight = function( o, done ) { + var elem = $( this ), + props = [ "backgroundImage", "backgroundColor", "opacity" ], + mode = $.effects.setMode( elem, o.mode || "show" ), + animation = { + backgroundColor: elem.css( "backgroundColor" ) + }; + + if (mode === "hide") { + animation.opacity = 0; + } + + $.effects.save( elem, props ); + + elem + .show() + .css({ + backgroundImage: "none", + backgroundColor: o.color || "#ffff99" + }) + .animate( animation, { + queue: false, + duration: o.duration, + easing: o.easing, + complete: function() { + if ( mode === "hide" ) { + elem.hide(); + } + $.effects.restore( elem, props ); + done(); + } + }); +}; + +})(jQuery); + +(function( $, undefined ) { + +$.effects.effect.pulsate = function( o, done ) { + var elem = $( this ), + mode = $.effects.setMode( elem, o.mode || "show" ), + show = mode === "show", + hide = mode === "hide", + showhide = ( show || mode === "hide" ), + + // showing or hiding leaves of the "last" animation + anims = ( ( o.times || 5 ) * 2 ) + ( showhide ? 1 : 0 ), + duration = o.duration / anims, + animateTo = 0, + queue = elem.queue(), + queuelen = queue.length, + i; + + if ( show || !elem.is(":visible")) { + elem.css( "opacity", 0 ).show(); + animateTo = 1; + } + + // anims - 1 opacity "toggles" + for ( i = 1; i < anims; i++ ) { + elem.animate({ + opacity: animateTo + }, duration, o.easing ); + animateTo = 1 - animateTo; + } + + elem.animate({ + opacity: animateTo + }, duration, o.easing); + + elem.queue(function() { + if ( hide ) { + elem.hide(); + } + done(); + }); + + // We just queued up "anims" animations, we need to put them next in the queue + if ( queuelen > 1 ) { + queue.splice.apply( queue, + [ 1, 0 ].concat( queue.splice( queuelen, anims + 1 ) ) ); + } + elem.dequeue(); +}; + +})(jQuery); + +(function( $, undefined ) { + +$.effects.effect.puff = function( o, done ) { + var elem = $( this ), + mode = $.effects.setMode( elem, o.mode || "hide" ), + hide = mode === "hide", + percent = parseInt( o.percent, 10 ) || 150, + factor = percent / 100, + original = { + height: elem.height(), + width: elem.width(), + outerHeight: elem.outerHeight(), + outerWidth: elem.outerWidth() + }; + + $.extend( o, { + effect: "scale", + queue: false, + fade: true, + mode: mode, + complete: done, + percent: hide ? percent : 100, + from: hide ? + original : + { + height: original.height * factor, + width: original.width * factor, + outerHeight: original.outerHeight * factor, + outerWidth: original.outerWidth * factor + } + }); + + elem.effect( o ); +}; + +$.effects.effect.scale = function( o, done ) { + + // Create element + var el = $( this ), + options = $.extend( true, {}, o ), + mode = $.effects.setMode( el, o.mode || "effect" ), + percent = parseInt( o.percent, 10 ) || + ( parseInt( o.percent, 10 ) === 0 ? 0 : ( mode === "hide" ? 0 : 100 ) ), + direction = o.direction || "both", + origin = o.origin, + original = { + height: el.height(), + width: el.width(), + outerHeight: el.outerHeight(), + outerWidth: el.outerWidth() + }, + factor = { + y: direction !== "horizontal" ? (percent / 100) : 1, + x: direction !== "vertical" ? (percent / 100) : 1 + }; + + // We are going to pass this effect to the size effect: + options.effect = "size"; + options.queue = false; + options.complete = done; + + // Set default origin and restore for show/hide + if ( mode !== "effect" ) { + options.origin = origin || ["middle","center"]; + options.restore = true; + } + + options.from = o.from || ( mode === "show" ? { + height: 0, + width: 0, + outerHeight: 0, + outerWidth: 0 + } : original ); + options.to = { + height: original.height * factor.y, + width: original.width * factor.x, + outerHeight: original.outerHeight * factor.y, + outerWidth: original.outerWidth * factor.x + }; + + // Fade option to support puff + if ( options.fade ) { + if ( mode === "show" ) { + options.from.opacity = 0; + options.to.opacity = 1; + } + if ( mode === "hide" ) { + options.from.opacity = 1; + options.to.opacity = 0; + } + } + + // Animate + el.effect( options ); + +}; + +$.effects.effect.size = function( o, done ) { + + // Create element + var original, baseline, factor, + el = $( this ), + props0 = [ "position", "top", "bottom", "left", "right", "width", "height", "overflow", "opacity" ], + + // Always restore + props1 = [ "position", "top", "bottom", "left", "right", "overflow", "opacity" ], + + // Copy for children + props2 = [ "width", "height", "overflow" ], + cProps = [ "fontSize" ], + vProps = [ "borderTopWidth", "borderBottomWidth", "paddingTop", "paddingBottom" ], + hProps = [ "borderLeftWidth", "borderRightWidth", "paddingLeft", "paddingRight" ], + + // Set options + mode = $.effects.setMode( el, o.mode || "effect" ), + restore = o.restore || mode !== "effect", + scale = o.scale || "both", + origin = o.origin || [ "middle", "center" ], + position = el.css( "position" ), + props = restore ? props0 : props1, + zero = { + height: 0, + width: 0, + outerHeight: 0, + outerWidth: 0 + }; + + if ( mode === "show" ) { + el.show(); + } + original = { + height: el.height(), + width: el.width(), + outerHeight: el.outerHeight(), + outerWidth: el.outerWidth() + }; + + if ( o.mode === "toggle" && mode === "show" ) { + el.from = o.to || zero; + el.to = o.from || original; + } else { + el.from = o.from || ( mode === "show" ? zero : original ); + el.to = o.to || ( mode === "hide" ? zero : original ); + } + + // Set scaling factor + factor = { + from: { + y: el.from.height / original.height, + x: el.from.width / original.width + }, + to: { + y: el.to.height / original.height, + x: el.to.width / original.width + } + }; + + // Scale the css box + if ( scale === "box" || scale === "both" ) { + + // Vertical props scaling + if ( factor.from.y !== factor.to.y ) { + props = props.concat( vProps ); + el.from = $.effects.setTransition( el, vProps, factor.from.y, el.from ); + el.to = $.effects.setTransition( el, vProps, factor.to.y, el.to ); + } + + // Horizontal props scaling + if ( factor.from.x !== factor.to.x ) { + props = props.concat( hProps ); + el.from = $.effects.setTransition( el, hProps, factor.from.x, el.from ); + el.to = $.effects.setTransition( el, hProps, factor.to.x, el.to ); + } + } + + // Scale the content + if ( scale === "content" || scale === "both" ) { + + // Vertical props scaling + if ( factor.from.y !== factor.to.y ) { + props = props.concat( cProps ).concat( props2 ); + el.from = $.effects.setTransition( el, cProps, factor.from.y, el.from ); + el.to = $.effects.setTransition( el, cProps, factor.to.y, el.to ); + } + } + + $.effects.save( el, props ); + el.show(); + $.effects.createWrapper( el ); + el.css( "overflow", "hidden" ).css( el.from ); + + // Adjust + if (origin) { // Calculate baseline shifts + baseline = $.effects.getBaseline( origin, original ); + el.from.top = ( original.outerHeight - el.outerHeight() ) * baseline.y; + el.from.left = ( original.outerWidth - el.outerWidth() ) * baseline.x; + el.to.top = ( original.outerHeight - el.to.outerHeight ) * baseline.y; + el.to.left = ( original.outerWidth - el.to.outerWidth ) * baseline.x; + } + el.css( el.from ); // set top & left + + // Animate + if ( scale === "content" || scale === "both" ) { // Scale the children + + // Add margins/font-size + vProps = vProps.concat([ "marginTop", "marginBottom" ]).concat(cProps); + hProps = hProps.concat([ "marginLeft", "marginRight" ]); + props2 = props0.concat(vProps).concat(hProps); + + el.find( "*[width]" ).each( function(){ + var child = $( this ), + c_original = { + height: child.height(), + width: child.width(), + outerHeight: child.outerHeight(), + outerWidth: child.outerWidth() + }; + if (restore) { + $.effects.save(child, props2); + } + + child.from = { + height: c_original.height * factor.from.y, + width: c_original.width * factor.from.x, + outerHeight: c_original.outerHeight * factor.from.y, + outerWidth: c_original.outerWidth * factor.from.x + }; + child.to = { + height: c_original.height * factor.to.y, + width: c_original.width * factor.to.x, + outerHeight: c_original.height * factor.to.y, + outerWidth: c_original.width * factor.to.x + }; + + // Vertical props scaling + if ( factor.from.y !== factor.to.y ) { + child.from = $.effects.setTransition( child, vProps, factor.from.y, child.from ); + child.to = $.effects.setTransition( child, vProps, factor.to.y, child.to ); + } + + // Horizontal props scaling + if ( factor.from.x !== factor.to.x ) { + child.from = $.effects.setTransition( child, hProps, factor.from.x, child.from ); + child.to = $.effects.setTransition( child, hProps, factor.to.x, child.to ); + } + + // Animate children + child.css( child.from ); + child.animate( child.to, o.duration, o.easing, function() { + + // Restore children + if ( restore ) { + $.effects.restore( child, props2 ); + } + }); + }); + } + + // Animate + el.animate( el.to, { + queue: false, + duration: o.duration, + easing: o.easing, + complete: function() { + if ( el.to.opacity === 0 ) { + el.css( "opacity", el.from.opacity ); + } + if( mode === "hide" ) { + el.hide(); + } + $.effects.restore( el, props ); + if ( !restore ) { + + // we need to calculate our new positioning based on the scaling + if ( position === "static" ) { + el.css({ + position: "relative", + top: el.to.top, + left: el.to.left + }); + } else { + $.each([ "top", "left" ], function( idx, pos ) { + el.css( pos, function( _, str ) { + var val = parseInt( str, 10 ), + toRef = idx ? el.to.left : el.to.top; + + // if original was "auto", recalculate the new value from wrapper + if ( str === "auto" ) { + return toRef + "px"; + } + + return val + toRef + "px"; + }); + }); + } + } + + $.effects.removeWrapper( el ); + done(); + } + }); + +}; + +})(jQuery); + +(function( $, undefined ) { + +$.effects.effect.shake = function( o, done ) { + + var el = $( this ), + props = [ "position", "top", "bottom", "left", "right", "height", "width" ], + mode = $.effects.setMode( el, o.mode || "effect" ), + direction = o.direction || "left", + distance = o.distance || 20, + times = o.times || 3, + anims = times * 2 + 1, + speed = Math.round(o.duration/anims), + ref = (direction === "up" || direction === "down") ? "top" : "left", + positiveMotion = (direction === "up" || direction === "left"), + animation = {}, + animation1 = {}, + animation2 = {}, + i, + + // we will need to re-assemble the queue to stack our animations in place + queue = el.queue(), + queuelen = queue.length; + + $.effects.save( el, props ); + el.show(); + $.effects.createWrapper( el ); + + // Animation + animation[ ref ] = ( positiveMotion ? "-=" : "+=" ) + distance; + animation1[ ref ] = ( positiveMotion ? "+=" : "-=" ) + distance * 2; + animation2[ ref ] = ( positiveMotion ? "-=" : "+=" ) + distance * 2; + + // Animate + el.animate( animation, speed, o.easing ); + + // Shakes + for ( i = 1; i < times; i++ ) { + el.animate( animation1, speed, o.easing ).animate( animation2, speed, o.easing ); + } + el + .animate( animation1, speed, o.easing ) + .animate( animation, speed / 2, o.easing ) + .queue(function() { + if ( mode === "hide" ) { + el.hide(); + } + $.effects.restore( el, props ); + $.effects.removeWrapper( el ); + done(); + }); + + // inject all the animations we just queued to be first in line (after "inprogress") + if ( queuelen > 1) { + queue.splice.apply( queue, + [ 1, 0 ].concat( queue.splice( queuelen, anims + 1 ) ) ); + } + el.dequeue(); + +}; + +})(jQuery); + +(function( $, undefined ) { + +$.effects.effect.slide = function( o, done ) { + + // Create element + var el = $( this ), + props = [ "position", "top", "bottom", "left", "right", "width", "height" ], + mode = $.effects.setMode( el, o.mode || "show" ), + show = mode === "show", + direction = o.direction || "left", + ref = (direction === "up" || direction === "down") ? "top" : "left", + positiveMotion = (direction === "up" || direction === "left"), + distance, + animation = {}; + + // Adjust + $.effects.save( el, props ); + el.show(); + distance = o.distance || el[ ref === "top" ? "outerHeight" : "outerWidth" ]( true ); + + $.effects.createWrapper( el ).css({ + overflow: "hidden" + }); + + if ( show ) { + el.css( ref, positiveMotion ? (isNaN(distance) ? "-" + distance : -distance) : distance ); + } + + // Animation + animation[ ref ] = ( show ? + ( positiveMotion ? "+=" : "-=") : + ( positiveMotion ? "-=" : "+=")) + + distance; + + // Animate + el.animate( animation, { + queue: false, + duration: o.duration, + easing: o.easing, + complete: function() { + if ( mode === "hide" ) { + el.hide(); + } + $.effects.restore( el, props ); + $.effects.removeWrapper( el ); + done(); + } + }); +}; + +})(jQuery); + +(function( $, undefined ) { + +$.effects.effect.transfer = function( o, done ) { + var elem = $( this ), + target = $( o.to ), + targetFixed = target.css( "position" ) === "fixed", + body = $("body"), + fixTop = targetFixed ? body.scrollTop() : 0, + fixLeft = targetFixed ? body.scrollLeft() : 0, + endPosition = target.offset(), + animation = { + top: endPosition.top - fixTop , + left: endPosition.left - fixLeft , + height: target.innerHeight(), + width: target.innerWidth() + }, + startPosition = elem.offset(), + transfer = $( '
' ) + .appendTo( document.body ) + .addClass( o.className ) + .css({ + top: startPosition.top - fixTop , + left: startPosition.left - fixLeft , + height: elem.innerHeight(), + width: elem.innerWidth(), + position: targetFixed ? "fixed" : "absolute" + }) + .animate( animation, o.duration, o.easing, function() { + transfer.remove(); + done(); + }); +}; + +})(jQuery); + +(function( $, undefined ) { + +var mouseHandled = false; + +$.widget( "ui.menu", { + version: "1.9.2", + defaultElement: "
    ", + delay: 300, + options: { + icons: { + submenu: "ui-icon-carat-1-e" + }, + menus: "ul", + position: { + my: "left top", + at: "right top" + }, + role: "menu", + + // callbacks + blur: null, + focus: null, + select: null + }, + + _create: function() { + this.activeMenu = this.element; + this.element + .uniqueId() + .addClass( "ui-menu ui-widget ui-widget-content ui-corner-all" ) + .toggleClass( "ui-menu-icons", !!this.element.find( ".ui-icon" ).length ) + .attr({ + role: this.options.role, + tabIndex: 0 + }) + // need to catch all clicks on disabled menu + // not possible through _on + .bind( "click" + this.eventNamespace, $.proxy(function( event ) { + if ( this.options.disabled ) { + event.preventDefault(); + } + }, this )); + + if ( this.options.disabled ) { + this.element + .addClass( "ui-state-disabled" ) + .attr( "aria-disabled", "true" ); + } + + this._on({ + // Prevent focus from sticking to links inside menu after clicking + // them (focus should always stay on UL during navigation). + "mousedown .ui-menu-item > a": function( event ) { + event.preventDefault(); + }, + "click .ui-state-disabled > a": function( event ) { + event.preventDefault(); + }, + "click .ui-menu-item:has(a)": function( event ) { + var target = $( event.target ).closest( ".ui-menu-item" ); + if ( !mouseHandled && target.not( ".ui-state-disabled" ).length ) { + mouseHandled = true; + + this.select( event ); + // Open submenu on click + if ( target.has( ".ui-menu" ).length ) { + this.expand( event ); + } else if ( !this.element.is( ":focus" ) ) { + // Redirect focus to the menu + this.element.trigger( "focus", [ true ] ); + + // If the active item is on the top level, let it stay active. + // Otherwise, blur the active item since it is no longer visible. + if ( this.active && this.active.parents( ".ui-menu" ).length === 1 ) { + clearTimeout( this.timer ); + } + } + } + }, + "mouseenter .ui-menu-item": function( event ) { + var target = $( event.currentTarget ); + // Remove ui-state-active class from siblings of the newly focused menu item + // to avoid a jump caused by adjacent elements both having a class with a border + target.siblings().children( ".ui-state-active" ).removeClass( "ui-state-active" ); + this.focus( event, target ); + }, + mouseleave: "collapseAll", + "mouseleave .ui-menu": "collapseAll", + focus: function( event, keepActiveItem ) { + // If there's already an active item, keep it active + // If not, activate the first item + var item = this.active || this.element.children( ".ui-menu-item" ).eq( 0 ); + + if ( !keepActiveItem ) { + this.focus( event, item ); + } + }, + blur: function( event ) { + this._delay(function() { + if ( !$.contains( this.element[0], this.document[0].activeElement ) ) { + this.collapseAll( event ); + } + }); + }, + keydown: "_keydown" + }); + + this.refresh(); + + // Clicks outside of a menu collapse any open menus + this._on( this.document, { + click: function( event ) { + if ( !$( event.target ).closest( ".ui-menu" ).length ) { + this.collapseAll( event ); + } + + // Reset the mouseHandled flag + mouseHandled = false; + } + }); + }, + + _destroy: function() { + // Destroy (sub)menus + this.element + .removeAttr( "aria-activedescendant" ) + .find( ".ui-menu" ).andSelf() + .removeClass( "ui-menu ui-widget ui-widget-content ui-corner-all ui-menu-icons" ) + .removeAttr( "role" ) + .removeAttr( "tabIndex" ) + .removeAttr( "aria-labelledby" ) + .removeAttr( "aria-expanded" ) + .removeAttr( "aria-hidden" ) + .removeAttr( "aria-disabled" ) + .removeUniqueId() + .show(); + + // Destroy menu items + this.element.find( ".ui-menu-item" ) + .removeClass( "ui-menu-item" ) + .removeAttr( "role" ) + .removeAttr( "aria-disabled" ) + .children( "a" ) + .removeUniqueId() + .removeClass( "ui-corner-all ui-state-hover" ) + .removeAttr( "tabIndex" ) + .removeAttr( "role" ) + .removeAttr( "aria-haspopup" ) + .children().each( function() { + var elem = $( this ); + if ( elem.data( "ui-menu-submenu-carat" ) ) { + elem.remove(); + } + }); + + // Destroy menu dividers + this.element.find( ".ui-menu-divider" ).removeClass( "ui-menu-divider ui-widget-content" ); + }, + + _keydown: function( event ) { + var match, prev, character, skip, regex, + preventDefault = true; + + function escape( value ) { + return value.replace( /[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&" ); + } + + switch ( event.keyCode ) { + case $.ui.keyCode.PAGE_UP: + this.previousPage( event ); + break; + case $.ui.keyCode.PAGE_DOWN: + this.nextPage( event ); + break; + case $.ui.keyCode.HOME: + this._move( "first", "first", event ); + break; + case $.ui.keyCode.END: + this._move( "last", "last", event ); + break; + case $.ui.keyCode.UP: + this.previous( event ); + break; + case $.ui.keyCode.DOWN: + this.next( event ); + break; + case $.ui.keyCode.LEFT: + this.collapse( event ); + break; + case $.ui.keyCode.RIGHT: + if ( this.active && !this.active.is( ".ui-state-disabled" ) ) { + this.expand( event ); + } + break; + case $.ui.keyCode.ENTER: + case $.ui.keyCode.SPACE: + this._activate( event ); + break; + case $.ui.keyCode.ESCAPE: + this.collapse( event ); + break; + default: + preventDefault = false; + prev = this.previousFilter || ""; + character = String.fromCharCode( event.keyCode ); + skip = false; + + clearTimeout( this.filterTimer ); + + if ( character === prev ) { + skip = true; + } else { + character = prev + character; + } + + regex = new RegExp( "^" + escape( character ), "i" ); + match = this.activeMenu.children( ".ui-menu-item" ).filter(function() { + return regex.test( $( this ).children( "a" ).text() ); + }); + match = skip && match.index( this.active.next() ) !== -1 ? + this.active.nextAll( ".ui-menu-item" ) : + match; + + // If no matches on the current filter, reset to the last character pressed + // to move down the menu to the first item that starts with that character + if ( !match.length ) { + character = String.fromCharCode( event.keyCode ); + regex = new RegExp( "^" + escape( character ), "i" ); + match = this.activeMenu.children( ".ui-menu-item" ).filter(function() { + return regex.test( $( this ).children( "a" ).text() ); + }); + } + + if ( match.length ) { + this.focus( event, match ); + if ( match.length > 1 ) { + this.previousFilter = character; + this.filterTimer = this._delay(function() { + delete this.previousFilter; + }, 1000 ); + } else { + delete this.previousFilter; + } + } else { + delete this.previousFilter; + } + } + + if ( preventDefault ) { + event.preventDefault(); + } + }, + + _activate: function( event ) { + if ( !this.active.is( ".ui-state-disabled" ) ) { + if ( this.active.children( "a[aria-haspopup='true']" ).length ) { + this.expand( event ); + } else { + this.select( event ); + } + } + }, + + refresh: function() { + var menus, + icon = this.options.icons.submenu, + submenus = this.element.find( this.options.menus ); + + // Initialize nested menus + submenus.filter( ":not(.ui-menu)" ) + .addClass( "ui-menu ui-widget ui-widget-content ui-corner-all" ) + .hide() + .attr({ + role: this.options.role, + "aria-hidden": "true", + "aria-expanded": "false" + }) + .each(function() { + var menu = $( this ), + item = menu.prev( "a" ), + submenuCarat = $( "" ) + .addClass( "ui-menu-icon ui-icon " + icon ) + .data( "ui-menu-submenu-carat", true ); + + item + .attr( "aria-haspopup", "true" ) + .prepend( submenuCarat ); + menu.attr( "aria-labelledby", item.attr( "id" ) ); + }); + + menus = submenus.add( this.element ); + + // Don't refresh list items that are already adapted + menus.children( ":not(.ui-menu-item):has(a)" ) + .addClass( "ui-menu-item" ) + .attr( "role", "presentation" ) + .children( "a" ) + .uniqueId() + .addClass( "ui-corner-all" ) + .attr({ + tabIndex: -1, + role: this._itemRole() + }); + + // Initialize unlinked menu-items containing spaces and/or dashes only as dividers + menus.children( ":not(.ui-menu-item)" ).each(function() { + var item = $( this ); + // hyphen, em dash, en dash + if ( !/[^\-—–\s]/.test( item.text() ) ) { + item.addClass( "ui-widget-content ui-menu-divider" ); + } + }); + + // Add aria-disabled attribute to any disabled menu item + menus.children( ".ui-state-disabled" ).attr( "aria-disabled", "true" ); + + // If the active item has been removed, blur the menu + if ( this.active && !$.contains( this.element[ 0 ], this.active[ 0 ] ) ) { + this.blur(); + } + }, + + _itemRole: function() { + return { + menu: "menuitem", + listbox: "option" + }[ this.options.role ]; + }, + + focus: function( event, item ) { + var nested, focused; + this.blur( event, event && event.type === "focus" ); + + this._scrollIntoView( item ); + + this.active = item.first(); + focused = this.active.children( "a" ).addClass( "ui-state-focus" ); + // Only update aria-activedescendant if there's a role + // otherwise we assume focus is managed elsewhere + if ( this.options.role ) { + this.element.attr( "aria-activedescendant", focused.attr( "id" ) ); + } + + // Highlight active parent menu item, if any + this.active + .parent() + .closest( ".ui-menu-item" ) + .children( "a:first" ) + .addClass( "ui-state-active" ); + + if ( event && event.type === "keydown" ) { + this._close(); + } else { + this.timer = this._delay(function() { + this._close(); + }, this.delay ); + } + + nested = item.children( ".ui-menu" ); + if ( nested.length && ( /^mouse/.test( event.type ) ) ) { + this._startOpening(nested); + } + this.activeMenu = item.parent(); + + this._trigger( "focus", event, { item: item } ); + }, + + _scrollIntoView: function( item ) { + var borderTop, paddingTop, offset, scroll, elementHeight, itemHeight; + if ( this._hasScroll() ) { + borderTop = parseFloat( $.css( this.activeMenu[0], "borderTopWidth" ) ) || 0; + paddingTop = parseFloat( $.css( this.activeMenu[0], "paddingTop" ) ) || 0; + offset = item.offset().top - this.activeMenu.offset().top - borderTop - paddingTop; + scroll = this.activeMenu.scrollTop(); + elementHeight = this.activeMenu.height(); + itemHeight = item.height(); + + if ( offset < 0 ) { + this.activeMenu.scrollTop( scroll + offset ); + } else if ( offset + itemHeight > elementHeight ) { + this.activeMenu.scrollTop( scroll + offset - elementHeight + itemHeight ); + } + } + }, + + blur: function( event, fromFocus ) { + if ( !fromFocus ) { + clearTimeout( this.timer ); + } + + if ( !this.active ) { + return; + } + + this.active.children( "a" ).removeClass( "ui-state-focus" ); + this.active = null; + + this._trigger( "blur", event, { item: this.active } ); + }, + + _startOpening: function( submenu ) { + clearTimeout( this.timer ); + + // Don't open if already open fixes a Firefox bug that caused a .5 pixel + // shift in the submenu position when mousing over the carat icon + if ( submenu.attr( "aria-hidden" ) !== "true" ) { + return; + } + + this.timer = this._delay(function() { + this._close(); + this._open( submenu ); + }, this.delay ); + }, + + _open: function( submenu ) { + var position = $.extend({ + of: this.active + }, this.options.position ); + + clearTimeout( this.timer ); + this.element.find( ".ui-menu" ).not( submenu.parents( ".ui-menu" ) ) + .hide() + .attr( "aria-hidden", "true" ); + + submenu + .show() + .removeAttr( "aria-hidden" ) + .attr( "aria-expanded", "true" ) + .position( position ); + }, + + collapseAll: function( event, all ) { + clearTimeout( this.timer ); + this.timer = this._delay(function() { + // If we were passed an event, look for the submenu that contains the event + var currentMenu = all ? this.element : + $( event && event.target ).closest( this.element.find( ".ui-menu" ) ); + + // If we found no valid submenu ancestor, use the main menu to close all sub menus anyway + if ( !currentMenu.length ) { + currentMenu = this.element; + } + + this._close( currentMenu ); + + this.blur( event ); + this.activeMenu = currentMenu; + }, this.delay ); + }, + + // With no arguments, closes the currently active menu - if nothing is active + // it closes all menus. If passed an argument, it will search for menus BELOW + _close: function( startMenu ) { + if ( !startMenu ) { + startMenu = this.active ? this.active.parent() : this.element; + } + + startMenu + .find( ".ui-menu" ) + .hide() + .attr( "aria-hidden", "true" ) + .attr( "aria-expanded", "false" ) + .end() + .find( "a.ui-state-active" ) + .removeClass( "ui-state-active" ); + }, + + collapse: function( event ) { + var newItem = this.active && + this.active.parent().closest( ".ui-menu-item", this.element ); + if ( newItem && newItem.length ) { + this._close(); + this.focus( event, newItem ); + } + }, + + expand: function( event ) { + var newItem = this.active && + this.active + .children( ".ui-menu " ) + .children( ".ui-menu-item" ) + .first(); + + if ( newItem && newItem.length ) { + this._open( newItem.parent() ); + + // Delay so Firefox will not hide activedescendant change in expanding submenu from AT + this._delay(function() { + this.focus( event, newItem ); + }); + } + }, + + next: function( event ) { + this._move( "next", "first", event ); + }, + + previous: function( event ) { + this._move( "prev", "last", event ); + }, + + isFirstItem: function() { + return this.active && !this.active.prevAll( ".ui-menu-item" ).length; + }, + + isLastItem: function() { + return this.active && !this.active.nextAll( ".ui-menu-item" ).length; + }, + + _move: function( direction, filter, event ) { + var next; + if ( this.active ) { + if ( direction === "first" || direction === "last" ) { + next = this.active + [ direction === "first" ? "prevAll" : "nextAll" ]( ".ui-menu-item" ) + .eq( -1 ); + } else { + next = this.active + [ direction + "All" ]( ".ui-menu-item" ) + .eq( 0 ); + } + } + if ( !next || !next.length || !this.active ) { + next = this.activeMenu.children( ".ui-menu-item" )[ filter ](); + } + + this.focus( event, next ); + }, + + nextPage: function( event ) { + var item, base, height; + + if ( !this.active ) { + this.next( event ); + return; + } + if ( this.isLastItem() ) { + return; + } + if ( this._hasScroll() ) { + base = this.active.offset().top; + height = this.element.height(); + this.active.nextAll( ".ui-menu-item" ).each(function() { + item = $( this ); + return item.offset().top - base - height < 0; + }); + + this.focus( event, item ); + } else { + this.focus( event, this.activeMenu.children( ".ui-menu-item" ) + [ !this.active ? "first" : "last" ]() ); + } + }, + + previousPage: function( event ) { + var item, base, height; + if ( !this.active ) { + this.next( event ); + return; + } + if ( this.isFirstItem() ) { + return; + } + if ( this._hasScroll() ) { + base = this.active.offset().top; + height = this.element.height(); + this.active.prevAll( ".ui-menu-item" ).each(function() { + item = $( this ); + return item.offset().top - base + height > 0; + }); + + this.focus( event, item ); + } else { + this.focus( event, this.activeMenu.children( ".ui-menu-item" ).first() ); + } + }, + + _hasScroll: function() { + return this.element.outerHeight() < this.element.prop( "scrollHeight" ); + }, + + select: function( event ) { + // TODO: It should never be possible to not have an active item at this + // point, but the tests don't trigger mouseenter before click. + this.active = this.active || $( event.target ).closest( ".ui-menu-item" ); + var ui = { item: this.active }; + if ( !this.active.has( ".ui-menu" ).length ) { + this.collapseAll( event, true ); + } + this._trigger( "select", event, ui ); + } +}); + +}( jQuery )); + +(function( $, undefined ) { + +$.ui = $.ui || {}; + +var cachedScrollbarWidth, + max = Math.max, + abs = Math.abs, + round = Math.round, + rhorizontal = /left|center|right/, + rvertical = /top|center|bottom/, + roffset = /[\+\-]\d+%?/, + rposition = /^\w+/, + rpercent = /%$/, + _position = $.fn.position; + +function getOffsets( offsets, width, height ) { + return [ + parseInt( offsets[ 0 ], 10 ) * ( rpercent.test( offsets[ 0 ] ) ? width / 100 : 1 ), + parseInt( offsets[ 1 ], 10 ) * ( rpercent.test( offsets[ 1 ] ) ? height / 100 : 1 ) + ]; +} +function parseCss( element, property ) { + return parseInt( $.css( element, property ), 10 ) || 0; +} + +$.position = { + scrollbarWidth: function() { + if ( cachedScrollbarWidth !== undefined ) { + return cachedScrollbarWidth; + } + var w1, w2, + div = $( "
    " ), + innerDiv = div.children()[0]; + + $( "body" ).append( div ); + w1 = innerDiv.offsetWidth; + div.css( "overflow", "scroll" ); + + w2 = innerDiv.offsetWidth; + + if ( w1 === w2 ) { + w2 = div[0].clientWidth; + } + + div.remove(); + + return (cachedScrollbarWidth = w1 - w2); + }, + getScrollInfo: function( within ) { + var overflowX = within.isWindow ? "" : within.element.css( "overflow-x" ), + overflowY = within.isWindow ? "" : within.element.css( "overflow-y" ), + hasOverflowX = overflowX === "scroll" || + ( overflowX === "auto" && within.width < within.element[0].scrollWidth ), + hasOverflowY = overflowY === "scroll" || + ( overflowY === "auto" && within.height < within.element[0].scrollHeight ); + return { + width: hasOverflowX ? $.position.scrollbarWidth() : 0, + height: hasOverflowY ? $.position.scrollbarWidth() : 0 + }; + }, + getWithinInfo: function( element ) { + var withinElement = $( element || window ), + isWindow = $.isWindow( withinElement[0] ); + return { + element: withinElement, + isWindow: isWindow, + offset: withinElement.offset() || { left: 0, top: 0 }, + scrollLeft: withinElement.scrollLeft(), + scrollTop: withinElement.scrollTop(), + width: isWindow ? withinElement.width() : withinElement.outerWidth(), + height: isWindow ? withinElement.height() : withinElement.outerHeight() + }; + } +}; + +$.fn.position = function( options ) { + if ( !options || !options.of ) { + return _position.apply( this, arguments ); + } + + // make a copy, we don't want to modify arguments + options = $.extend( {}, options ); + + var atOffset, targetWidth, targetHeight, targetOffset, basePosition, + target = $( options.of ), + within = $.position.getWithinInfo( options.within ), + scrollInfo = $.position.getScrollInfo( within ), + targetElem = target[0], + collision = ( options.collision || "flip" ).split( " " ), + offsets = {}; + + if ( targetElem.nodeType === 9 ) { + targetWidth = target.width(); + targetHeight = target.height(); + targetOffset = { top: 0, left: 0 }; + } else if ( $.isWindow( targetElem ) ) { + targetWidth = target.width(); + targetHeight = target.height(); + targetOffset = { top: target.scrollTop(), left: target.scrollLeft() }; + } else if ( targetElem.preventDefault ) { + // force left top to allow flipping + options.at = "left top"; + targetWidth = targetHeight = 0; + targetOffset = { top: targetElem.pageY, left: targetElem.pageX }; + } else { + targetWidth = target.outerWidth(); + targetHeight = target.outerHeight(); + targetOffset = target.offset(); + } + // clone to reuse original targetOffset later + basePosition = $.extend( {}, targetOffset ); + + // force my and at to have valid horizontal and vertical positions + // if a value is missing or invalid, it will be converted to center + $.each( [ "my", "at" ], function() { + var pos = ( options[ this ] || "" ).split( " " ), + horizontalOffset, + verticalOffset; + + if ( pos.length === 1) { + pos = rhorizontal.test( pos[ 0 ] ) ? + pos.concat( [ "center" ] ) : + rvertical.test( pos[ 0 ] ) ? + [ "center" ].concat( pos ) : + [ "center", "center" ]; + } + pos[ 0 ] = rhorizontal.test( pos[ 0 ] ) ? pos[ 0 ] : "center"; + pos[ 1 ] = rvertical.test( pos[ 1 ] ) ? pos[ 1 ] : "center"; + + // calculate offsets + horizontalOffset = roffset.exec( pos[ 0 ] ); + verticalOffset = roffset.exec( pos[ 1 ] ); + offsets[ this ] = [ + horizontalOffset ? horizontalOffset[ 0 ] : 0, + verticalOffset ? verticalOffset[ 0 ] : 0 + ]; + + // reduce to just the positions without the offsets + options[ this ] = [ + rposition.exec( pos[ 0 ] )[ 0 ], + rposition.exec( pos[ 1 ] )[ 0 ] + ]; + }); + + // normalize collision option + if ( collision.length === 1 ) { + collision[ 1 ] = collision[ 0 ]; + } + + if ( options.at[ 0 ] === "right" ) { + basePosition.left += targetWidth; + } else if ( options.at[ 0 ] === "center" ) { + basePosition.left += targetWidth / 2; + } + + if ( options.at[ 1 ] === "bottom" ) { + basePosition.top += targetHeight; + } else if ( options.at[ 1 ] === "center" ) { + basePosition.top += targetHeight / 2; + } + + atOffset = getOffsets( offsets.at, targetWidth, targetHeight ); + basePosition.left += atOffset[ 0 ]; + basePosition.top += atOffset[ 1 ]; + + return this.each(function() { + var collisionPosition, using, + elem = $( this ), + elemWidth = elem.outerWidth(), + elemHeight = elem.outerHeight(), + marginLeft = parseCss( this, "marginLeft" ), + marginTop = parseCss( this, "marginTop" ), + collisionWidth = elemWidth + marginLeft + parseCss( this, "marginRight" ) + scrollInfo.width, + collisionHeight = elemHeight + marginTop + parseCss( this, "marginBottom" ) + scrollInfo.height, + position = $.extend( {}, basePosition ), + myOffset = getOffsets( offsets.my, elem.outerWidth(), elem.outerHeight() ); + + if ( options.my[ 0 ] === "right" ) { + position.left -= elemWidth; + } else if ( options.my[ 0 ] === "center" ) { + position.left -= elemWidth / 2; + } + + if ( options.my[ 1 ] === "bottom" ) { + position.top -= elemHeight; + } else if ( options.my[ 1 ] === "center" ) { + position.top -= elemHeight / 2; + } + + position.left += myOffset[ 0 ]; + position.top += myOffset[ 1 ]; + + // if the browser doesn't support fractions, then round for consistent results + if ( !$.support.offsetFractions ) { + position.left = round( position.left ); + position.top = round( position.top ); + } + + collisionPosition = { + marginLeft: marginLeft, + marginTop: marginTop + }; + + $.each( [ "left", "top" ], function( i, dir ) { + if ( $.ui.position[ collision[ i ] ] ) { + $.ui.position[ collision[ i ] ][ dir ]( position, { + targetWidth: targetWidth, + targetHeight: targetHeight, + elemWidth: elemWidth, + elemHeight: elemHeight, + collisionPosition: collisionPosition, + collisionWidth: collisionWidth, + collisionHeight: collisionHeight, + offset: [ atOffset[ 0 ] + myOffset[ 0 ], atOffset [ 1 ] + myOffset[ 1 ] ], + my: options.my, + at: options.at, + within: within, + elem : elem + }); + } + }); + + if ( $.fn.bgiframe ) { + elem.bgiframe(); + } + + if ( options.using ) { + // adds feedback as second argument to using callback, if present + using = function( props ) { + var left = targetOffset.left - position.left, + right = left + targetWidth - elemWidth, + top = targetOffset.top - position.top, + bottom = top + targetHeight - elemHeight, + feedback = { + target: { + element: target, + left: targetOffset.left, + top: targetOffset.top, + width: targetWidth, + height: targetHeight + }, + element: { + element: elem, + left: position.left, + top: position.top, + width: elemWidth, + height: elemHeight + }, + horizontal: right < 0 ? "left" : left > 0 ? "right" : "center", + vertical: bottom < 0 ? "top" : top > 0 ? "bottom" : "middle" + }; + if ( targetWidth < elemWidth && abs( left + right ) < targetWidth ) { + feedback.horizontal = "center"; + } + if ( targetHeight < elemHeight && abs( top + bottom ) < targetHeight ) { + feedback.vertical = "middle"; + } + if ( max( abs( left ), abs( right ) ) > max( abs( top ), abs( bottom ) ) ) { + feedback.important = "horizontal"; + } else { + feedback.important = "vertical"; + } + options.using.call( this, props, feedback ); + }; + } + + elem.offset( $.extend( position, { using: using } ) ); + }); +}; + +$.ui.position = { + fit: { + left: function( position, data ) { + var within = data.within, + withinOffset = within.isWindow ? within.scrollLeft : within.offset.left, + outerWidth = within.width, + collisionPosLeft = position.left - data.collisionPosition.marginLeft, + overLeft = withinOffset - collisionPosLeft, + overRight = collisionPosLeft + data.collisionWidth - outerWidth - withinOffset, + newOverRight; + + // element is wider than within + if ( data.collisionWidth > outerWidth ) { + // element is initially over the left side of within + if ( overLeft > 0 && overRight <= 0 ) { + newOverRight = position.left + overLeft + data.collisionWidth - outerWidth - withinOffset; + position.left += overLeft - newOverRight; + // element is initially over right side of within + } else if ( overRight > 0 && overLeft <= 0 ) { + position.left = withinOffset; + // element is initially over both left and right sides of within + } else { + if ( overLeft > overRight ) { + position.left = withinOffset + outerWidth - data.collisionWidth; + } else { + position.left = withinOffset; + } + } + // too far left -> align with left edge + } else if ( overLeft > 0 ) { + position.left += overLeft; + // too far right -> align with right edge + } else if ( overRight > 0 ) { + position.left -= overRight; + // adjust based on position and margin + } else { + position.left = max( position.left - collisionPosLeft, position.left ); + } + }, + top: function( position, data ) { + var within = data.within, + withinOffset = within.isWindow ? within.scrollTop : within.offset.top, + outerHeight = data.within.height, + collisionPosTop = position.top - data.collisionPosition.marginTop, + overTop = withinOffset - collisionPosTop, + overBottom = collisionPosTop + data.collisionHeight - outerHeight - withinOffset, + newOverBottom; + + // element is taller than within + if ( data.collisionHeight > outerHeight ) { + // element is initially over the top of within + if ( overTop > 0 && overBottom <= 0 ) { + newOverBottom = position.top + overTop + data.collisionHeight - outerHeight - withinOffset; + position.top += overTop - newOverBottom; + // element is initially over bottom of within + } else if ( overBottom > 0 && overTop <= 0 ) { + position.top = withinOffset; + // element is initially over both top and bottom of within + } else { + if ( overTop > overBottom ) { + position.top = withinOffset + outerHeight - data.collisionHeight; + } else { + position.top = withinOffset; + } + } + // too far up -> align with top + } else if ( overTop > 0 ) { + position.top += overTop; + // too far down -> align with bottom edge + } else if ( overBottom > 0 ) { + position.top -= overBottom; + // adjust based on position and margin + } else { + position.top = max( position.top - collisionPosTop, position.top ); + } + } + }, + flip: { + left: function( position, data ) { + var within = data.within, + withinOffset = within.offset.left + within.scrollLeft, + outerWidth = within.width, + offsetLeft = within.isWindow ? within.scrollLeft : within.offset.left, + collisionPosLeft = position.left - data.collisionPosition.marginLeft, + overLeft = collisionPosLeft - offsetLeft, + overRight = collisionPosLeft + data.collisionWidth - outerWidth - offsetLeft, + myOffset = data.my[ 0 ] === "left" ? + -data.elemWidth : + data.my[ 0 ] === "right" ? + data.elemWidth : + 0, + atOffset = data.at[ 0 ] === "left" ? + data.targetWidth : + data.at[ 0 ] === "right" ? + -data.targetWidth : + 0, + offset = -2 * data.offset[ 0 ], + newOverRight, + newOverLeft; + + if ( overLeft < 0 ) { + newOverRight = position.left + myOffset + atOffset + offset + data.collisionWidth - outerWidth - withinOffset; + if ( newOverRight < 0 || newOverRight < abs( overLeft ) ) { + position.left += myOffset + atOffset + offset; + } + } + else if ( overRight > 0 ) { + newOverLeft = position.left - data.collisionPosition.marginLeft + myOffset + atOffset + offset - offsetLeft; + if ( newOverLeft > 0 || abs( newOverLeft ) < overRight ) { + position.left += myOffset + atOffset + offset; + } + } + }, + top: function( position, data ) { + var within = data.within, + withinOffset = within.offset.top + within.scrollTop, + outerHeight = within.height, + offsetTop = within.isWindow ? within.scrollTop : within.offset.top, + collisionPosTop = position.top - data.collisionPosition.marginTop, + overTop = collisionPosTop - offsetTop, + overBottom = collisionPosTop + data.collisionHeight - outerHeight - offsetTop, + top = data.my[ 1 ] === "top", + myOffset = top ? + -data.elemHeight : + data.my[ 1 ] === "bottom" ? + data.elemHeight : + 0, + atOffset = data.at[ 1 ] === "top" ? + data.targetHeight : + data.at[ 1 ] === "bottom" ? + -data.targetHeight : + 0, + offset = -2 * data.offset[ 1 ], + newOverTop, + newOverBottom; + if ( overTop < 0 ) { + newOverBottom = position.top + myOffset + atOffset + offset + data.collisionHeight - outerHeight - withinOffset; + if ( ( position.top + myOffset + atOffset + offset) > overTop && ( newOverBottom < 0 || newOverBottom < abs( overTop ) ) ) { + position.top += myOffset + atOffset + offset; + } + } + else if ( overBottom > 0 ) { + newOverTop = position.top - data.collisionPosition.marginTop + myOffset + atOffset + offset - offsetTop; + if ( ( position.top + myOffset + atOffset + offset) > overBottom && ( newOverTop > 0 || abs( newOverTop ) < overBottom ) ) { + position.top += myOffset + atOffset + offset; + } + } + } + }, + flipfit: { + left: function() { + $.ui.position.flip.left.apply( this, arguments ); + $.ui.position.fit.left.apply( this, arguments ); + }, + top: function() { + $.ui.position.flip.top.apply( this, arguments ); + $.ui.position.fit.top.apply( this, arguments ); + } + } +}; + +// fraction support test +(function () { + var testElement, testElementParent, testElementStyle, offsetLeft, i, + body = document.getElementsByTagName( "body" )[ 0 ], + div = document.createElement( "div" ); + + //Create a "fake body" for testing based on method used in jQuery.support + testElement = document.createElement( body ? "div" : "body" ); + testElementStyle = { + visibility: "hidden", + width: 0, + height: 0, + border: 0, + margin: 0, + background: "none" + }; + if ( body ) { + $.extend( testElementStyle, { + position: "absolute", + left: "-1000px", + top: "-1000px" + }); + } + for ( i in testElementStyle ) { + testElement.style[ i ] = testElementStyle[ i ]; + } + testElement.appendChild( div ); + testElementParent = body || document.documentElement; + testElementParent.insertBefore( testElement, testElementParent.firstChild ); + + div.style.cssText = "position: absolute; left: 10.7432222px;"; + + offsetLeft = $( div ).offset().left; + $.support.offsetFractions = offsetLeft > 10 && offsetLeft < 11; + + testElement.innerHTML = ""; + testElementParent.removeChild( testElement ); +})(); + +// DEPRECATED +if ( $.uiBackCompat !== false ) { + // offset option + (function( $ ) { + var _position = $.fn.position; + $.fn.position = function( options ) { + if ( !options || !options.offset ) { + return _position.call( this, options ); + } + var offset = options.offset.split( " " ), + at = options.at.split( " " ); + if ( offset.length === 1 ) { + offset[ 1 ] = offset[ 0 ]; + } + if ( /^\d/.test( offset[ 0 ] ) ) { + offset[ 0 ] = "+" + offset[ 0 ]; + } + if ( /^\d/.test( offset[ 1 ] ) ) { + offset[ 1 ] = "+" + offset[ 1 ]; + } + if ( at.length === 1 ) { + if ( /left|center|right/.test( at[ 0 ] ) ) { + at[ 1 ] = "center"; + } else { + at[ 1 ] = at[ 0 ]; + at[ 0 ] = "center"; + } + } + return _position.call( this, $.extend( options, { + at: at[ 0 ] + offset[ 0 ] + " " + at[ 1 ] + offset[ 1 ], + offset: undefined + } ) ); + }; + }( jQuery ) ); +} + +}( jQuery ) ); + +(function( $, undefined ) { + +$.widget( "ui.progressbar", { + version: "1.9.2", + options: { + value: 0, + max: 100 + }, + + min: 0, + + _create: function() { + this.element + .addClass( "ui-progressbar ui-widget ui-widget-content ui-corner-all" ) + .attr({ + role: "progressbar", + "aria-valuemin": this.min, + "aria-valuemax": this.options.max, + "aria-valuenow": this._value() + }); + + this.valueDiv = $( "
    " ) + .appendTo( this.element ); + + this.oldValue = this._value(); + this._refreshValue(); + }, + + _destroy: function() { + this.element + .removeClass( "ui-progressbar ui-widget ui-widget-content ui-corner-all" ) + .removeAttr( "role" ) + .removeAttr( "aria-valuemin" ) + .removeAttr( "aria-valuemax" ) + .removeAttr( "aria-valuenow" ); + + this.valueDiv.remove(); + }, + + value: function( newValue ) { + if ( newValue === undefined ) { + return this._value(); + } + + this._setOption( "value", newValue ); + return this; + }, + + _setOption: function( key, value ) { + if ( key === "value" ) { + this.options.value = value; + this._refreshValue(); + if ( this._value() === this.options.max ) { + this._trigger( "complete" ); + } + } + + this._super( key, value ); + }, + + _value: function() { + var val = this.options.value; + // normalize invalid value + if ( typeof val !== "number" ) { + val = 0; + } + return Math.min( this.options.max, Math.max( this.min, val ) ); + }, + + _percentage: function() { + return 100 * this._value() / this.options.max; + }, + + _refreshValue: function() { + var value = this.value(), + percentage = this._percentage(); + + if ( this.oldValue !== value ) { + this.oldValue = value; + this._trigger( "change" ); + } + + this.valueDiv + .toggle( value > this.min ) + .toggleClass( "ui-corner-right", value === this.options.max ) + .width( percentage.toFixed(0) + "%" ); + this.element.attr( "aria-valuenow", value ); + } +}); + +})( jQuery ); + +(function( $, undefined ) { + +// number of pages in a slider +// (how many times can you page up/down to go through the whole range) +var numPages = 5; + +$.widget( "ui.slider", $.ui.mouse, { + version: "1.9.2", + widgetEventPrefix: "slide", + + options: { + animate: false, + distance: 0, + max: 100, + min: 0, + orientation: "horizontal", + range: false, + step: 1, + value: 0, + values: null + }, + + _create: function() { + var i, handleCount, + o = this.options, + existingHandles = this.element.find( ".ui-slider-handle" ).addClass( "ui-state-default ui-corner-all" ), + handle = "", + handles = []; + + this._keySliding = false; + this._mouseSliding = false; + this._animateOff = true; + this._handleIndex = null; + this._detectOrientation(); + this._mouseInit(); + + this.element + .addClass( "ui-slider" + + " ui-slider-" + this.orientation + + " ui-widget" + + " ui-widget-content" + + " ui-corner-all" + + ( o.disabled ? " ui-slider-disabled ui-disabled" : "" ) ); + + this.range = $([]); + + if ( o.range ) { + if ( o.range === true ) { + if ( !o.values ) { + o.values = [ this._valueMin(), this._valueMin() ]; + } + if ( o.values.length && o.values.length !== 2 ) { + o.values = [ o.values[0], o.values[0] ]; + } + } + + this.range = $( "
    " ) + .appendTo( this.element ) + .addClass( "ui-slider-range" + + // note: this isn't the most fittingly semantic framework class for this element, + // but worked best visually with a variety of themes + " ui-widget-header" + + ( ( o.range === "min" || o.range === "max" ) ? " ui-slider-range-" + o.range : "" ) ); + } + + handleCount = ( o.values && o.values.length ) || 1; + + for ( i = existingHandles.length; i < handleCount; i++ ) { + handles.push( handle ); + } + + this.handles = existingHandles.add( $( handles.join( "" ) ).appendTo( this.element ) ); + + this.handle = this.handles.eq( 0 ); + + this.handles.add( this.range ).filter( "a" ) + .click(function( event ) { + event.preventDefault(); + }) + .mouseenter(function() { + if ( !o.disabled ) { + $( this ).addClass( "ui-state-hover" ); + } + }) + .mouseleave(function() { + $( this ).removeClass( "ui-state-hover" ); + }) + .focus(function() { + if ( !o.disabled ) { + $( ".ui-slider .ui-state-focus" ).removeClass( "ui-state-focus" ); + $( this ).addClass( "ui-state-focus" ); + } else { + $( this ).blur(); + } + }) + .blur(function() { + $( this ).removeClass( "ui-state-focus" ); + }); + + this.handles.each(function( i ) { + $( this ).data( "ui-slider-handle-index", i ); + }); + + this._on( this.handles, { + keydown: function( event ) { + var allowed, curVal, newVal, step, + index = $( event.target ).data( "ui-slider-handle-index" ); + + switch ( event.keyCode ) { + case $.ui.keyCode.HOME: + case $.ui.keyCode.END: + case $.ui.keyCode.PAGE_UP: + case $.ui.keyCode.PAGE_DOWN: + case $.ui.keyCode.UP: + case $.ui.keyCode.RIGHT: + case $.ui.keyCode.DOWN: + case $.ui.keyCode.LEFT: + event.preventDefault(); + if ( !this._keySliding ) { + this._keySliding = true; + $( event.target ).addClass( "ui-state-active" ); + allowed = this._start( event, index ); + if ( allowed === false ) { + return; + } + } + break; + } + + step = this.options.step; + if ( this.options.values && this.options.values.length ) { + curVal = newVal = this.values( index ); + } else { + curVal = newVal = this.value(); + } + + switch ( event.keyCode ) { + case $.ui.keyCode.HOME: + newVal = this._valueMin(); + break; + case $.ui.keyCode.END: + newVal = this._valueMax(); + break; + case $.ui.keyCode.PAGE_UP: + newVal = this._trimAlignValue( curVal + ( (this._valueMax() - this._valueMin()) / numPages ) ); + break; + case $.ui.keyCode.PAGE_DOWN: + newVal = this._trimAlignValue( curVal - ( (this._valueMax() - this._valueMin()) / numPages ) ); + break; + case $.ui.keyCode.UP: + case $.ui.keyCode.RIGHT: + if ( curVal === this._valueMax() ) { + return; + } + newVal = this._trimAlignValue( curVal + step ); + break; + case $.ui.keyCode.DOWN: + case $.ui.keyCode.LEFT: + if ( curVal === this._valueMin() ) { + return; + } + newVal = this._trimAlignValue( curVal - step ); + break; + } + + this._slide( event, index, newVal ); + }, + keyup: function( event ) { + var index = $( event.target ).data( "ui-slider-handle-index" ); + + if ( this._keySliding ) { + this._keySliding = false; + this._stop( event, index ); + this._change( event, index ); + $( event.target ).removeClass( "ui-state-active" ); + } + } + }); + + this._refreshValue(); + + this._animateOff = false; + }, + + _destroy: function() { + this.handles.remove(); + this.range.remove(); + + this.element + .removeClass( "ui-slider" + + " ui-slider-horizontal" + + " ui-slider-vertical" + + " ui-slider-disabled" + + " ui-widget" + + " ui-widget-content" + + " ui-corner-all" ); + + this._mouseDestroy(); + }, + + _mouseCapture: function( event ) { + var position, normValue, distance, closestHandle, index, allowed, offset, mouseOverHandle, + that = this, + o = this.options; + + if ( o.disabled ) { + return false; + } + + this.elementSize = { + width: this.element.outerWidth(), + height: this.element.outerHeight() + }; + this.elementOffset = this.element.offset(); + + position = { x: event.pageX, y: event.pageY }; + normValue = this._normValueFromMouse( position ); + distance = this._valueMax() - this._valueMin() + 1; + this.handles.each(function( i ) { + var thisDistance = Math.abs( normValue - that.values(i) ); + if ( distance > thisDistance ) { + distance = thisDistance; + closestHandle = $( this ); + index = i; + } + }); + + // workaround for bug #3736 (if both handles of a range are at 0, + // the first is always used as the one with least distance, + // and moving it is obviously prevented by preventing negative ranges) + if( o.range === true && this.values(1) === o.min ) { + index += 1; + closestHandle = $( this.handles[index] ); + } + + allowed = this._start( event, index ); + if ( allowed === false ) { + return false; + } + this._mouseSliding = true; + + this._handleIndex = index; + + closestHandle + .addClass( "ui-state-active" ) + .focus(); + + offset = closestHandle.offset(); + mouseOverHandle = !$( event.target ).parents().andSelf().is( ".ui-slider-handle" ); + this._clickOffset = mouseOverHandle ? { left: 0, top: 0 } : { + left: event.pageX - offset.left - ( closestHandle.width() / 2 ), + top: event.pageY - offset.top - + ( closestHandle.height() / 2 ) - + ( parseInt( closestHandle.css("borderTopWidth"), 10 ) || 0 ) - + ( parseInt( closestHandle.css("borderBottomWidth"), 10 ) || 0) + + ( parseInt( closestHandle.css("marginTop"), 10 ) || 0) + }; + + if ( !this.handles.hasClass( "ui-state-hover" ) ) { + this._slide( event, index, normValue ); + } + this._animateOff = true; + return true; + }, + + _mouseStart: function() { + return true; + }, + + _mouseDrag: function( event ) { + var position = { x: event.pageX, y: event.pageY }, + normValue = this._normValueFromMouse( position ); + + this._slide( event, this._handleIndex, normValue ); + + return false; + }, + + _mouseStop: function( event ) { + this.handles.removeClass( "ui-state-active" ); + this._mouseSliding = false; + + this._stop( event, this._handleIndex ); + this._change( event, this._handleIndex ); + + this._handleIndex = null; + this._clickOffset = null; + this._animateOff = false; + + return false; + }, + + _detectOrientation: function() { + this.orientation = ( this.options.orientation === "vertical" ) ? "vertical" : "horizontal"; + }, + + _normValueFromMouse: function( position ) { + var pixelTotal, + pixelMouse, + percentMouse, + valueTotal, + valueMouse; + + if ( this.orientation === "horizontal" ) { + pixelTotal = this.elementSize.width; + pixelMouse = position.x - this.elementOffset.left - ( this._clickOffset ? this._clickOffset.left : 0 ); + } else { + pixelTotal = this.elementSize.height; + pixelMouse = position.y - this.elementOffset.top - ( this._clickOffset ? this._clickOffset.top : 0 ); + } + + percentMouse = ( pixelMouse / pixelTotal ); + if ( percentMouse > 1 ) { + percentMouse = 1; + } + if ( percentMouse < 0 ) { + percentMouse = 0; + } + if ( this.orientation === "vertical" ) { + percentMouse = 1 - percentMouse; + } + + valueTotal = this._valueMax() - this._valueMin(); + valueMouse = this._valueMin() + percentMouse * valueTotal; + + return this._trimAlignValue( valueMouse ); + }, + + _start: function( event, index ) { + var uiHash = { + handle: this.handles[ index ], + value: this.value() + }; + if ( this.options.values && this.options.values.length ) { + uiHash.value = this.values( index ); + uiHash.values = this.values(); + } + return this._trigger( "start", event, uiHash ); + }, + + _slide: function( event, index, newVal ) { + var otherVal, + newValues, + allowed; + + if ( this.options.values && this.options.values.length ) { + otherVal = this.values( index ? 0 : 1 ); + + if ( ( this.options.values.length === 2 && this.options.range === true ) && + ( ( index === 0 && newVal > otherVal) || ( index === 1 && newVal < otherVal ) ) + ) { + newVal = otherVal; + } + + if ( newVal !== this.values( index ) ) { + newValues = this.values(); + newValues[ index ] = newVal; + // A slide can be canceled by returning false from the slide callback + allowed = this._trigger( "slide", event, { + handle: this.handles[ index ], + value: newVal, + values: newValues + } ); + otherVal = this.values( index ? 0 : 1 ); + if ( allowed !== false ) { + this.values( index, newVal, true ); + } + } + } else { + if ( newVal !== this.value() ) { + // A slide can be canceled by returning false from the slide callback + allowed = this._trigger( "slide", event, { + handle: this.handles[ index ], + value: newVal + } ); + if ( allowed !== false ) { + this.value( newVal ); + } + } + } + }, + + _stop: function( event, index ) { + var uiHash = { + handle: this.handles[ index ], + value: this.value() + }; + if ( this.options.values && this.options.values.length ) { + uiHash.value = this.values( index ); + uiHash.values = this.values(); + } + + this._trigger( "stop", event, uiHash ); + }, + + _change: function( event, index ) { + if ( !this._keySliding && !this._mouseSliding ) { + var uiHash = { + handle: this.handles[ index ], + value: this.value() + }; + if ( this.options.values && this.options.values.length ) { + uiHash.value = this.values( index ); + uiHash.values = this.values(); + } + + this._trigger( "change", event, uiHash ); + } + }, + + value: function( newValue ) { + if ( arguments.length ) { + this.options.value = this._trimAlignValue( newValue ); + this._refreshValue(); + this._change( null, 0 ); + return; + } + + return this._value(); + }, + + values: function( index, newValue ) { + var vals, + newValues, + i; + + if ( arguments.length > 1 ) { + this.options.values[ index ] = this._trimAlignValue( newValue ); + this._refreshValue(); + this._change( null, index ); + return; + } + + if ( arguments.length ) { + if ( $.isArray( arguments[ 0 ] ) ) { + vals = this.options.values; + newValues = arguments[ 0 ]; + for ( i = 0; i < vals.length; i += 1 ) { + vals[ i ] = this._trimAlignValue( newValues[ i ] ); + this._change( null, i ); + } + this._refreshValue(); + } else { + if ( this.options.values && this.options.values.length ) { + return this._values( index ); + } else { + return this.value(); + } + } + } else { + return this._values(); + } + }, + + _setOption: function( key, value ) { + var i, + valsLength = 0; + + if ( $.isArray( this.options.values ) ) { + valsLength = this.options.values.length; + } + + $.Widget.prototype._setOption.apply( this, arguments ); + + switch ( key ) { + case "disabled": + if ( value ) { + this.handles.filter( ".ui-state-focus" ).blur(); + this.handles.removeClass( "ui-state-hover" ); + this.handles.prop( "disabled", true ); + this.element.addClass( "ui-disabled" ); + } else { + this.handles.prop( "disabled", false ); + this.element.removeClass( "ui-disabled" ); + } + break; + case "orientation": + this._detectOrientation(); + this.element + .removeClass( "ui-slider-horizontal ui-slider-vertical" ) + .addClass( "ui-slider-" + this.orientation ); + this._refreshValue(); + break; + case "value": + this._animateOff = true; + this._refreshValue(); + this._change( null, 0 ); + this._animateOff = false; + break; + case "values": + this._animateOff = true; + this._refreshValue(); + for ( i = 0; i < valsLength; i += 1 ) { + this._change( null, i ); + } + this._animateOff = false; + break; + case "min": + case "max": + this._animateOff = true; + this._refreshValue(); + this._animateOff = false; + break; + } + }, + + //internal value getter + // _value() returns value trimmed by min and max, aligned by step + _value: function() { + var val = this.options.value; + val = this._trimAlignValue( val ); + + return val; + }, + + //internal values getter + // _values() returns array of values trimmed by min and max, aligned by step + // _values( index ) returns single value trimmed by min and max, aligned by step + _values: function( index ) { + var val, + vals, + i; + + if ( arguments.length ) { + val = this.options.values[ index ]; + val = this._trimAlignValue( val ); + + return val; + } else { + // .slice() creates a copy of the array + // this copy gets trimmed by min and max and then returned + vals = this.options.values.slice(); + for ( i = 0; i < vals.length; i+= 1) { + vals[ i ] = this._trimAlignValue( vals[ i ] ); + } + + return vals; + } + }, + + // returns the step-aligned value that val is closest to, between (inclusive) min and max + _trimAlignValue: function( val ) { + if ( val <= this._valueMin() ) { + return this._valueMin(); + } + if ( val >= this._valueMax() ) { + return this._valueMax(); + } + var step = ( this.options.step > 0 ) ? this.options.step : 1, + valModStep = (val - this._valueMin()) % step, + alignValue = val - valModStep; + + if ( Math.abs(valModStep) * 2 >= step ) { + alignValue += ( valModStep > 0 ) ? step : ( -step ); + } + + // Since JavaScript has problems with large floats, round + // the final value to 5 digits after the decimal point (see #4124) + return parseFloat( alignValue.toFixed(5) ); + }, + + _valueMin: function() { + return this.options.min; + }, + + _valueMax: function() { + return this.options.max; + }, + + _refreshValue: function() { + var lastValPercent, valPercent, value, valueMin, valueMax, + oRange = this.options.range, + o = this.options, + that = this, + animate = ( !this._animateOff ) ? o.animate : false, + _set = {}; + + if ( this.options.values && this.options.values.length ) { + this.handles.each(function( i ) { + valPercent = ( that.values(i) - that._valueMin() ) / ( that._valueMax() - that._valueMin() ) * 100; + _set[ that.orientation === "horizontal" ? "left" : "bottom" ] = valPercent + "%"; + $( this ).stop( 1, 1 )[ animate ? "animate" : "css" ]( _set, o.animate ); + if ( that.options.range === true ) { + if ( that.orientation === "horizontal" ) { + if ( i === 0 ) { + that.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { left: valPercent + "%" }, o.animate ); + } + if ( i === 1 ) { + that.range[ animate ? "animate" : "css" ]( { width: ( valPercent - lastValPercent ) + "%" }, { queue: false, duration: o.animate } ); + } + } else { + if ( i === 0 ) { + that.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { bottom: ( valPercent ) + "%" }, o.animate ); + } + if ( i === 1 ) { + that.range[ animate ? "animate" : "css" ]( { height: ( valPercent - lastValPercent ) + "%" }, { queue: false, duration: o.animate } ); + } + } + } + lastValPercent = valPercent; + }); + } else { + value = this.value(); + valueMin = this._valueMin(); + valueMax = this._valueMax(); + valPercent = ( valueMax !== valueMin ) ? + ( value - valueMin ) / ( valueMax - valueMin ) * 100 : + 0; + _set[ this.orientation === "horizontal" ? "left" : "bottom" ] = valPercent + "%"; + this.handle.stop( 1, 1 )[ animate ? "animate" : "css" ]( _set, o.animate ); + + if ( oRange === "min" && this.orientation === "horizontal" ) { + this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { width: valPercent + "%" }, o.animate ); + } + if ( oRange === "max" && this.orientation === "horizontal" ) { + this.range[ animate ? "animate" : "css" ]( { width: ( 100 - valPercent ) + "%" }, { queue: false, duration: o.animate } ); + } + if ( oRange === "min" && this.orientation === "vertical" ) { + this.range.stop( 1, 1 )[ animate ? "animate" : "css" ]( { height: valPercent + "%" }, o.animate ); + } + if ( oRange === "max" && this.orientation === "vertical" ) { + this.range[ animate ? "animate" : "css" ]( { height: ( 100 - valPercent ) + "%" }, { queue: false, duration: o.animate } ); + } + } + } + +}); + +}(jQuery)); + +(function( $ ) { + +function modifier( fn ) { + return function() { + var previous = this.element.val(); + fn.apply( this, arguments ); + this._refresh(); + if ( previous !== this.element.val() ) { + this._trigger( "change" ); + } + }; +} + +$.widget( "ui.spinner", { + version: "1.9.2", + defaultElement: "", + widgetEventPrefix: "spin", + options: { + culture: null, + icons: { + down: "ui-icon-triangle-1-s", + up: "ui-icon-triangle-1-n" + }, + incremental: true, + max: null, + min: null, + numberFormat: null, + page: 10, + step: 1, + + change: null, + spin: null, + start: null, + stop: null + }, + + _create: function() { + // handle string values that need to be parsed + this._setOption( "max", this.options.max ); + this._setOption( "min", this.options.min ); + this._setOption( "step", this.options.step ); + + // format the value, but don't constrain + this._value( this.element.val(), true ); + + this._draw(); + this._on( this._events ); + this._refresh(); + + // turning off autocomplete prevents the browser from remembering the + // value when navigating through history, so we re-enable autocomplete + // if the page is unloaded before the widget is destroyed. #7790 + this._on( this.window, { + beforeunload: function() { + this.element.removeAttr( "autocomplete" ); + } + }); + }, + + _getCreateOptions: function() { + var options = {}, + element = this.element; + + $.each( [ "min", "max", "step" ], function( i, option ) { + var value = element.attr( option ); + if ( value !== undefined && value.length ) { + options[ option ] = value; + } + }); + + return options; + }, + + _events: { + keydown: function( event ) { + if ( this._start( event ) && this._keydown( event ) ) { + event.preventDefault(); + } + }, + keyup: "_stop", + focus: function() { + this.previous = this.element.val(); + }, + blur: function( event ) { + if ( this.cancelBlur ) { + delete this.cancelBlur; + return; + } + + this._refresh(); + if ( this.previous !== this.element.val() ) { + this._trigger( "change", event ); + } + }, + mousewheel: function( event, delta ) { + if ( !delta ) { + return; + } + if ( !this.spinning && !this._start( event ) ) { + return false; + } + + this._spin( (delta > 0 ? 1 : -1) * this.options.step, event ); + clearTimeout( this.mousewheelTimer ); + this.mousewheelTimer = this._delay(function() { + if ( this.spinning ) { + this._stop( event ); + } + }, 100 ); + event.preventDefault(); + }, + "mousedown .ui-spinner-button": function( event ) { + var previous; + + // We never want the buttons to have focus; whenever the user is + // interacting with the spinner, the focus should be on the input. + // If the input is focused then this.previous is properly set from + // when the input first received focus. If the input is not focused + // then we need to set this.previous based on the value before spinning. + previous = this.element[0] === this.document[0].activeElement ? + this.previous : this.element.val(); + function checkFocus() { + var isActive = this.element[0] === this.document[0].activeElement; + if ( !isActive ) { + this.element.focus(); + this.previous = previous; + // support: IE + // IE sets focus asynchronously, so we need to check if focus + // moved off of the input because the user clicked on the button. + this._delay(function() { + this.previous = previous; + }); + } + } + + // ensure focus is on (or stays on) the text field + event.preventDefault(); + checkFocus.call( this ); + + // support: IE + // IE doesn't prevent moving focus even with event.preventDefault() + // so we set a flag to know when we should ignore the blur event + // and check (again) if focus moved off of the input. + this.cancelBlur = true; + this._delay(function() { + delete this.cancelBlur; + checkFocus.call( this ); + }); + + if ( this._start( event ) === false ) { + return; + } + + this._repeat( null, $( event.currentTarget ).hasClass( "ui-spinner-up" ) ? 1 : -1, event ); + }, + "mouseup .ui-spinner-button": "_stop", + "mouseenter .ui-spinner-button": function( event ) { + // button will add ui-state-active if mouse was down while mouseleave and kept down + if ( !$( event.currentTarget ).hasClass( "ui-state-active" ) ) { + return; + } + + if ( this._start( event ) === false ) { + return false; + } + this._repeat( null, $( event.currentTarget ).hasClass( "ui-spinner-up" ) ? 1 : -1, event ); + }, + // TODO: do we really want to consider this a stop? + // shouldn't we just stop the repeater and wait until mouseup before + // we trigger the stop event? + "mouseleave .ui-spinner-button": "_stop" + }, + + _draw: function() { + var uiSpinner = this.uiSpinner = this.element + .addClass( "ui-spinner-input" ) + .attr( "autocomplete", "off" ) + .wrap( this._uiSpinnerHtml() ) + .parent() + // add buttons + .append( this._buttonHtml() ); + + this.element.attr( "role", "spinbutton" ); + + // button bindings + this.buttons = uiSpinner.find( ".ui-spinner-button" ) + .attr( "tabIndex", -1 ) + .button() + .removeClass( "ui-corner-all" ); + + // IE 6 doesn't understand height: 50% for the buttons + // unless the wrapper has an explicit height + if ( this.buttons.height() > Math.ceil( uiSpinner.height() * 0.5 ) && + uiSpinner.height() > 0 ) { + uiSpinner.height( uiSpinner.height() ); + } + + // disable spinner if element was already disabled + if ( this.options.disabled ) { + this.disable(); + } + }, + + _keydown: function( event ) { + var options = this.options, + keyCode = $.ui.keyCode; + + switch ( event.keyCode ) { + case keyCode.UP: + this._repeat( null, 1, event ); + return true; + case keyCode.DOWN: + this._repeat( null, -1, event ); + return true; + case keyCode.PAGE_UP: + this._repeat( null, options.page, event ); + return true; + case keyCode.PAGE_DOWN: + this._repeat( null, -options.page, event ); + return true; + } + + return false; + }, + + _uiSpinnerHtml: function() { + return ""; + }, + + _buttonHtml: function() { + return "" + + "" + + "" + + "" + + "" + + "" + + ""; + }, + + _start: function( event ) { + if ( !this.spinning && this._trigger( "start", event ) === false ) { + return false; + } + + if ( !this.counter ) { + this.counter = 1; + } + this.spinning = true; + return true; + }, + + _repeat: function( i, steps, event ) { + i = i || 500; + + clearTimeout( this.timer ); + this.timer = this._delay(function() { + this._repeat( 40, steps, event ); + }, i ); + + this._spin( steps * this.options.step, event ); + }, + + _spin: function( step, event ) { + var value = this.value() || 0; + + if ( !this.counter ) { + this.counter = 1; + } + + value = this._adjustValue( value + step * this._increment( this.counter ) ); + + if ( !this.spinning || this._trigger( "spin", event, { value: value } ) !== false) { + this._value( value ); + this.counter++; + } + }, + + _increment: function( i ) { + var incremental = this.options.incremental; + + if ( incremental ) { + return $.isFunction( incremental ) ? + incremental( i ) : + Math.floor( i*i*i/50000 - i*i/500 + 17*i/200 + 1 ); + } + + return 1; + }, + + _precision: function() { + var precision = this._precisionOf( this.options.step ); + if ( this.options.min !== null ) { + precision = Math.max( precision, this._precisionOf( this.options.min ) ); + } + return precision; + }, + + _precisionOf: function( num ) { + var str = num.toString(), + decimal = str.indexOf( "." ); + return decimal === -1 ? 0 : str.length - decimal - 1; + }, + + _adjustValue: function( value ) { + var base, aboveMin, + options = this.options; + + // make sure we're at a valid step + // - find out where we are relative to the base (min or 0) + base = options.min !== null ? options.min : 0; + aboveMin = value - base; + // - round to the nearest step + aboveMin = Math.round(aboveMin / options.step) * options.step; + // - rounding is based on 0, so adjust back to our base + value = base + aboveMin; + + // fix precision from bad JS floating point math + value = parseFloat( value.toFixed( this._precision() ) ); + + // clamp the value + if ( options.max !== null && value > options.max) { + return options.max; + } + if ( options.min !== null && value < options.min ) { + return options.min; + } + + return value; + }, + + _stop: function( event ) { + if ( !this.spinning ) { + return; + } + + clearTimeout( this.timer ); + clearTimeout( this.mousewheelTimer ); + this.counter = 0; + this.spinning = false; + this._trigger( "stop", event ); + }, + + _setOption: function( key, value ) { + if ( key === "culture" || key === "numberFormat" ) { + var prevValue = this._parse( this.element.val() ); + this.options[ key ] = value; + this.element.val( this._format( prevValue ) ); + return; + } + + if ( key === "max" || key === "min" || key === "step" ) { + if ( typeof value === "string" ) { + value = this._parse( value ); + } + } + + this._super( key, value ); + + if ( key === "disabled" ) { + if ( value ) { + this.element.prop( "disabled", true ); + this.buttons.button( "disable" ); + } else { + this.element.prop( "disabled", false ); + this.buttons.button( "enable" ); + } + } + }, + + _setOptions: modifier(function( options ) { + this._super( options ); + this._value( this.element.val() ); + }), + + _parse: function( val ) { + if ( typeof val === "string" && val !== "" ) { + val = window.Globalize && this.options.numberFormat ? + Globalize.parseFloat( val, 10, this.options.culture ) : +val; + } + return val === "" || isNaN( val ) ? null : val; + }, + + _format: function( value ) { + if ( value === "" ) { + return ""; + } + return window.Globalize && this.options.numberFormat ? + Globalize.format( value, this.options.numberFormat, this.options.culture ) : + value; + }, + + _refresh: function() { + this.element.attr({ + "aria-valuemin": this.options.min, + "aria-valuemax": this.options.max, + // TODO: what should we do with values that can't be parsed? + "aria-valuenow": this._parse( this.element.val() ) + }); + }, + + // update the value without triggering change + _value: function( value, allowAny ) { + var parsed; + if ( value !== "" ) { + parsed = this._parse( value ); + if ( parsed !== null ) { + if ( !allowAny ) { + parsed = this._adjustValue( parsed ); + } + value = this._format( parsed ); + } + } + this.element.val( value ); + this._refresh(); + }, + + _destroy: function() { + this.element + .removeClass( "ui-spinner-input" ) + .prop( "disabled", false ) + .removeAttr( "autocomplete" ) + .removeAttr( "role" ) + .removeAttr( "aria-valuemin" ) + .removeAttr( "aria-valuemax" ) + .removeAttr( "aria-valuenow" ); + this.uiSpinner.replaceWith( this.element ); + }, + + stepUp: modifier(function( steps ) { + this._stepUp( steps ); + }), + _stepUp: function( steps ) { + this._spin( (steps || 1) * this.options.step ); + }, + + stepDown: modifier(function( steps ) { + this._stepDown( steps ); + }), + _stepDown: function( steps ) { + this._spin( (steps || 1) * -this.options.step ); + }, + + pageUp: modifier(function( pages ) { + this._stepUp( (pages || 1) * this.options.page ); + }), + + pageDown: modifier(function( pages ) { + this._stepDown( (pages || 1) * this.options.page ); + }), + + value: function( newVal ) { + if ( !arguments.length ) { + return this._parse( this.element.val() ); + } + modifier( this._value ).call( this, newVal ); + }, + + widget: function() { + return this.uiSpinner; + } +}); + +}( jQuery ) ); + +(function( $, undefined ) { + +var tabId = 0, + rhash = /#.*$/; + +function getNextTabId() { + return ++tabId; +} + +function isLocal( anchor ) { + return anchor.hash.length > 1 && + anchor.href.replace( rhash, "" ) === + location.href.replace( rhash, "" ) + // support: Safari 5.1 + // Safari 5.1 doesn't encode spaces in window.location + // but it does encode spaces from anchors (#8777) + .replace( /\s/g, "%20" ); +} + +$.widget( "ui.tabs", { + version: "1.9.2", + delay: 300, + options: { + active: null, + collapsible: false, + event: "click", + heightStyle: "content", + hide: null, + show: null, + + // callbacks + activate: null, + beforeActivate: null, + beforeLoad: null, + load: null + }, + + _create: function() { + var that = this, + options = this.options, + active = options.active, + locationHash = location.hash.substring( 1 ); + + this.running = false; + + this.element + .addClass( "ui-tabs ui-widget ui-widget-content ui-corner-all" ) + .toggleClass( "ui-tabs-collapsible", options.collapsible ) + // Prevent users from focusing disabled tabs via click + .delegate( ".ui-tabs-nav > li", "mousedown" + this.eventNamespace, function( event ) { + if ( $( this ).is( ".ui-state-disabled" ) ) { + event.preventDefault(); + } + }) + // support: IE <9 + // Preventing the default action in mousedown doesn't prevent IE + // from focusing the element, so if the anchor gets focused, blur. + // We don't have to worry about focusing the previously focused + // element since clicking on a non-focusable element should focus + // the body anyway. + .delegate( ".ui-tabs-anchor", "focus" + this.eventNamespace, function() { + if ( $( this ).closest( "li" ).is( ".ui-state-disabled" ) ) { + this.blur(); + } + }); + + this._processTabs(); + + if ( active === null ) { + // check the fragment identifier in the URL + if ( locationHash ) { + this.tabs.each(function( i, tab ) { + if ( $( tab ).attr( "aria-controls" ) === locationHash ) { + active = i; + return false; + } + }); + } + + // check for a tab marked active via a class + if ( active === null ) { + active = this.tabs.index( this.tabs.filter( ".ui-tabs-active" ) ); + } + + // no active tab, set to false + if ( active === null || active === -1 ) { + active = this.tabs.length ? 0 : false; + } + } + + // handle numbers: negative, out of range + if ( active !== false ) { + active = this.tabs.index( this.tabs.eq( active ) ); + if ( active === -1 ) { + active = options.collapsible ? false : 0; + } + } + options.active = active; + + // don't allow collapsible: false and active: false + if ( !options.collapsible && options.active === false && this.anchors.length ) { + options.active = 0; + } + + // Take disabling tabs via class attribute from HTML + // into account and update option properly. + if ( $.isArray( options.disabled ) ) { + options.disabled = $.unique( options.disabled.concat( + $.map( this.tabs.filter( ".ui-state-disabled" ), function( li ) { + return that.tabs.index( li ); + }) + ) ).sort(); + } + + // check for length avoids error when initializing empty list + if ( this.options.active !== false && this.anchors.length ) { + this.active = this._findActive( this.options.active ); + } else { + this.active = $(); + } + + this._refresh(); + + if ( this.active.length ) { + this.load( options.active ); + } + }, + + _getCreateEventData: function() { + return { + tab: this.active, + panel: !this.active.length ? $() : this._getPanelForTab( this.active ) + }; + }, + + _tabKeydown: function( event ) { + var focusedTab = $( this.document[0].activeElement ).closest( "li" ), + selectedIndex = this.tabs.index( focusedTab ), + goingForward = true; + + if ( this._handlePageNav( event ) ) { + return; + } + + switch ( event.keyCode ) { + case $.ui.keyCode.RIGHT: + case $.ui.keyCode.DOWN: + selectedIndex++; + break; + case $.ui.keyCode.UP: + case $.ui.keyCode.LEFT: + goingForward = false; + selectedIndex--; + break; + case $.ui.keyCode.END: + selectedIndex = this.anchors.length - 1; + break; + case $.ui.keyCode.HOME: + selectedIndex = 0; + break; + case $.ui.keyCode.SPACE: + // Activate only, no collapsing + event.preventDefault(); + clearTimeout( this.activating ); + this._activate( selectedIndex ); + return; + case $.ui.keyCode.ENTER: + // Toggle (cancel delayed activation, allow collapsing) + event.preventDefault(); + clearTimeout( this.activating ); + // Determine if we should collapse or activate + this._activate( selectedIndex === this.options.active ? false : selectedIndex ); + return; + default: + return; + } + + // Focus the appropriate tab, based on which key was pressed + event.preventDefault(); + clearTimeout( this.activating ); + selectedIndex = this._focusNextTab( selectedIndex, goingForward ); + + // Navigating with control key will prevent automatic activation + if ( !event.ctrlKey ) { + // Update aria-selected immediately so that AT think the tab is already selected. + // Otherwise AT may confuse the user by stating that they need to activate the tab, + // but the tab will already be activated by the time the announcement finishes. + focusedTab.attr( "aria-selected", "false" ); + this.tabs.eq( selectedIndex ).attr( "aria-selected", "true" ); + + this.activating = this._delay(function() { + this.option( "active", selectedIndex ); + }, this.delay ); + } + }, + + _panelKeydown: function( event ) { + if ( this._handlePageNav( event ) ) { + return; + } + + // Ctrl+up moves focus to the current tab + if ( event.ctrlKey && event.keyCode === $.ui.keyCode.UP ) { + event.preventDefault(); + this.active.focus(); + } + }, + + // Alt+page up/down moves focus to the previous/next tab (and activates) + _handlePageNav: function( event ) { + if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_UP ) { + this._activate( this._focusNextTab( this.options.active - 1, false ) ); + return true; + } + if ( event.altKey && event.keyCode === $.ui.keyCode.PAGE_DOWN ) { + this._activate( this._focusNextTab( this.options.active + 1, true ) ); + return true; + } + }, + + _findNextTab: function( index, goingForward ) { + var lastTabIndex = this.tabs.length - 1; + + function constrain() { + if ( index > lastTabIndex ) { + index = 0; + } + if ( index < 0 ) { + index = lastTabIndex; + } + return index; + } + + while ( $.inArray( constrain(), this.options.disabled ) !== -1 ) { + index = goingForward ? index + 1 : index - 1; + } + + return index; + }, + + _focusNextTab: function( index, goingForward ) { + index = this._findNextTab( index, goingForward ); + this.tabs.eq( index ).focus(); + return index; + }, + + _setOption: function( key, value ) { + if ( key === "active" ) { + // _activate() will handle invalid values and update this.options + this._activate( value ); + return; + } + + if ( key === "disabled" ) { + // don't use the widget factory's disabled handling + this._setupDisabled( value ); + return; + } + + this._super( key, value); + + if ( key === "collapsible" ) { + this.element.toggleClass( "ui-tabs-collapsible", value ); + // Setting collapsible: false while collapsed; open first panel + if ( !value && this.options.active === false ) { + this._activate( 0 ); + } + } + + if ( key === "event" ) { + this._setupEvents( value ); + } + + if ( key === "heightStyle" ) { + this._setupHeightStyle( value ); + } + }, + + _tabId: function( tab ) { + return tab.attr( "aria-controls" ) || "ui-tabs-" + getNextTabId(); + }, + + _sanitizeSelector: function( hash ) { + return hash ? hash.replace( /[!"$%&'()*+,.\/:;<=>?@\[\]\^`{|}~]/g, "\\$&" ) : ""; + }, + + refresh: function() { + var options = this.options, + lis = this.tablist.children( ":has(a[href])" ); + + // get disabled tabs from class attribute from HTML + // this will get converted to a boolean if needed in _refresh() + options.disabled = $.map( lis.filter( ".ui-state-disabled" ), function( tab ) { + return lis.index( tab ); + }); + + this._processTabs(); + + // was collapsed or no tabs + if ( options.active === false || !this.anchors.length ) { + options.active = false; + this.active = $(); + // was active, but active tab is gone + } else if ( this.active.length && !$.contains( this.tablist[ 0 ], this.active[ 0 ] ) ) { + // all remaining tabs are disabled + if ( this.tabs.length === options.disabled.length ) { + options.active = false; + this.active = $(); + // activate previous tab + } else { + this._activate( this._findNextTab( Math.max( 0, options.active - 1 ), false ) ); + } + // was active, active tab still exists + } else { + // make sure active index is correct + options.active = this.tabs.index( this.active ); + } + + this._refresh(); + }, + + _refresh: function() { + this._setupDisabled( this.options.disabled ); + this._setupEvents( this.options.event ); + this._setupHeightStyle( this.options.heightStyle ); + + this.tabs.not( this.active ).attr({ + "aria-selected": "false", + tabIndex: -1 + }); + this.panels.not( this._getPanelForTab( this.active ) ) + .hide() + .attr({ + "aria-expanded": "false", + "aria-hidden": "true" + }); + + // Make sure one tab is in the tab order + if ( !this.active.length ) { + this.tabs.eq( 0 ).attr( "tabIndex", 0 ); + } else { + this.active + .addClass( "ui-tabs-active ui-state-active" ) + .attr({ + "aria-selected": "true", + tabIndex: 0 + }); + this._getPanelForTab( this.active ) + .show() + .attr({ + "aria-expanded": "true", + "aria-hidden": "false" + }); + } + }, + + _processTabs: function() { + var that = this; + + this.tablist = this._getList() + .addClass( "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" ) + .attr( "role", "tablist" ); + + this.tabs = this.tablist.find( "> li:has(a[href])" ) + .addClass( "ui-state-default ui-corner-top" ) + .attr({ + role: "tab", + tabIndex: -1 + }); + + this.anchors = this.tabs.map(function() { + return $( "a", this )[ 0 ]; + }) + .addClass( "ui-tabs-anchor" ) + .attr({ + role: "presentation", + tabIndex: -1 + }); + + this.panels = $(); + + this.anchors.each(function( i, anchor ) { + var selector, panel, panelId, + anchorId = $( anchor ).uniqueId().attr( "id" ), + tab = $( anchor ).closest( "li" ), + originalAriaControls = tab.attr( "aria-controls" ); + + // inline tab + if ( isLocal( anchor ) ) { + selector = anchor.hash; + panel = that.element.find( that._sanitizeSelector( selector ) ); + // remote tab + } else { + panelId = that._tabId( tab ); + selector = "#" + panelId; + panel = that.element.find( selector ); + if ( !panel.length ) { + panel = that._createPanel( panelId ); + panel.insertAfter( that.panels[ i - 1 ] || that.tablist ); + } + panel.attr( "aria-live", "polite" ); + } + + if ( panel.length) { + that.panels = that.panels.add( panel ); + } + if ( originalAriaControls ) { + tab.data( "ui-tabs-aria-controls", originalAriaControls ); + } + tab.attr({ + "aria-controls": selector.substring( 1 ), + "aria-labelledby": anchorId + }); + panel.attr( "aria-labelledby", anchorId ); + }); + + this.panels + .addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" ) + .attr( "role", "tabpanel" ); + }, + + // allow overriding how to find the list for rare usage scenarios (#7715) + _getList: function() { + return this.element.find( "ol,ul" ).eq( 0 ); + }, + + _createPanel: function( id ) { + return $( "
    " ) + .attr( "id", id ) + .addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" ) + .data( "ui-tabs-destroy", true ); + }, + + _setupDisabled: function( disabled ) { + if ( $.isArray( disabled ) ) { + if ( !disabled.length ) { + disabled = false; + } else if ( disabled.length === this.anchors.length ) { + disabled = true; + } + } + + // disable tabs + for ( var i = 0, li; ( li = this.tabs[ i ] ); i++ ) { + if ( disabled === true || $.inArray( i, disabled ) !== -1 ) { + $( li ) + .addClass( "ui-state-disabled" ) + .attr( "aria-disabled", "true" ); + } else { + $( li ) + .removeClass( "ui-state-disabled" ) + .removeAttr( "aria-disabled" ); + } + } + + this.options.disabled = disabled; + }, + + _setupEvents: function( event ) { + var events = { + click: function( event ) { + event.preventDefault(); + } + }; + if ( event ) { + $.each( event.split(" "), function( index, eventName ) { + events[ eventName ] = "_eventHandler"; + }); + } + + this._off( this.anchors.add( this.tabs ).add( this.panels ) ); + this._on( this.anchors, events ); + this._on( this.tabs, { keydown: "_tabKeydown" } ); + this._on( this.panels, { keydown: "_panelKeydown" } ); + + this._focusable( this.tabs ); + this._hoverable( this.tabs ); + }, + + _setupHeightStyle: function( heightStyle ) { + var maxHeight, overflow, + parent = this.element.parent(); + + if ( heightStyle === "fill" ) { + // IE 6 treats height like minHeight, so we need to turn off overflow + // in order to get a reliable height + // we use the minHeight support test because we assume that only + // browsers that don't support minHeight will treat height as minHeight + if ( !$.support.minHeight ) { + overflow = parent.css( "overflow" ); + parent.css( "overflow", "hidden"); + } + maxHeight = parent.height(); + this.element.siblings( ":visible" ).each(function() { + var elem = $( this ), + position = elem.css( "position" ); + + if ( position === "absolute" || position === "fixed" ) { + return; + } + maxHeight -= elem.outerHeight( true ); + }); + if ( overflow ) { + parent.css( "overflow", overflow ); + } + + this.element.children().not( this.panels ).each(function() { + maxHeight -= $( this ).outerHeight( true ); + }); + + this.panels.each(function() { + $( this ).height( Math.max( 0, maxHeight - + $( this ).innerHeight() + $( this ).height() ) ); + }) + .css( "overflow", "auto" ); + } else if ( heightStyle === "auto" ) { + maxHeight = 0; + this.panels.each(function() { + maxHeight = Math.max( maxHeight, $( this ).height( "" ).height() ); + }).height( maxHeight ); + } + }, + + _eventHandler: function( event ) { + var options = this.options, + active = this.active, + anchor = $( event.currentTarget ), + tab = anchor.closest( "li" ), + clickedIsActive = tab[ 0 ] === active[ 0 ], + collapsing = clickedIsActive && options.collapsible, + toShow = collapsing ? $() : this._getPanelForTab( tab ), + toHide = !active.length ? $() : this._getPanelForTab( active ), + eventData = { + oldTab: active, + oldPanel: toHide, + newTab: collapsing ? $() : tab, + newPanel: toShow + }; + + event.preventDefault(); + + if ( tab.hasClass( "ui-state-disabled" ) || + // tab is already loading + tab.hasClass( "ui-tabs-loading" ) || + // can't switch durning an animation + this.running || + // click on active header, but not collapsible + ( clickedIsActive && !options.collapsible ) || + // allow canceling activation + ( this._trigger( "beforeActivate", event, eventData ) === false ) ) { + return; + } + + options.active = collapsing ? false : this.tabs.index( tab ); + + this.active = clickedIsActive ? $() : tab; + if ( this.xhr ) { + this.xhr.abort(); + } + + if ( !toHide.length && !toShow.length ) { + $.error( "jQuery UI Tabs: Mismatching fragment identifier." ); + } + + if ( toShow.length ) { + this.load( this.tabs.index( tab ), event ); + } + this._toggle( event, eventData ); + }, + + // handles show/hide for selecting tabs + _toggle: function( event, eventData ) { + var that = this, + toShow = eventData.newPanel, + toHide = eventData.oldPanel; + + this.running = true; + + function complete() { + that.running = false; + that._trigger( "activate", event, eventData ); + } + + function show() { + eventData.newTab.closest( "li" ).addClass( "ui-tabs-active ui-state-active" ); + + if ( toShow.length && that.options.show ) { + that._show( toShow, that.options.show, complete ); + } else { + toShow.show(); + complete(); + } + } + + // start out by hiding, then showing, then completing + if ( toHide.length && this.options.hide ) { + this._hide( toHide, this.options.hide, function() { + eventData.oldTab.closest( "li" ).removeClass( "ui-tabs-active ui-state-active" ); + show(); + }); + } else { + eventData.oldTab.closest( "li" ).removeClass( "ui-tabs-active ui-state-active" ); + toHide.hide(); + show(); + } + + toHide.attr({ + "aria-expanded": "false", + "aria-hidden": "true" + }); + eventData.oldTab.attr( "aria-selected", "false" ); + // If we're switching tabs, remove the old tab from the tab order. + // If we're opening from collapsed state, remove the previous tab from the tab order. + // If we're collapsing, then keep the collapsing tab in the tab order. + if ( toShow.length && toHide.length ) { + eventData.oldTab.attr( "tabIndex", -1 ); + } else if ( toShow.length ) { + this.tabs.filter(function() { + return $( this ).attr( "tabIndex" ) === 0; + }) + .attr( "tabIndex", -1 ); + } + + toShow.attr({ + "aria-expanded": "true", + "aria-hidden": "false" + }); + eventData.newTab.attr({ + "aria-selected": "true", + tabIndex: 0 + }); + }, + + _activate: function( index ) { + var anchor, + active = this._findActive( index ); + + // trying to activate the already active panel + if ( active[ 0 ] === this.active[ 0 ] ) { + return; + } + + // trying to collapse, simulate a click on the current active header + if ( !active.length ) { + active = this.active; + } + + anchor = active.find( ".ui-tabs-anchor" )[ 0 ]; + this._eventHandler({ + target: anchor, + currentTarget: anchor, + preventDefault: $.noop + }); + }, + + _findActive: function( index ) { + return index === false ? $() : this.tabs.eq( index ); + }, + + _getIndex: function( index ) { + // meta-function to give users option to provide a href string instead of a numerical index. + if ( typeof index === "string" ) { + index = this.anchors.index( this.anchors.filter( "[href$='" + index + "']" ) ); + } + + return index; + }, + + _destroy: function() { + if ( this.xhr ) { + this.xhr.abort(); + } + + this.element.removeClass( "ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-collapsible" ); + + this.tablist + .removeClass( "ui-tabs-nav ui-helper-reset ui-helper-clearfix ui-widget-header ui-corner-all" ) + .removeAttr( "role" ); + + this.anchors + .removeClass( "ui-tabs-anchor" ) + .removeAttr( "role" ) + .removeAttr( "tabIndex" ) + .removeData( "href.tabs" ) + .removeData( "load.tabs" ) + .removeUniqueId(); + + this.tabs.add( this.panels ).each(function() { + if ( $.data( this, "ui-tabs-destroy" ) ) { + $( this ).remove(); + } else { + $( this ) + .removeClass( "ui-state-default ui-state-active ui-state-disabled " + + "ui-corner-top ui-corner-bottom ui-widget-content ui-tabs-active ui-tabs-panel" ) + .removeAttr( "tabIndex" ) + .removeAttr( "aria-live" ) + .removeAttr( "aria-busy" ) + .removeAttr( "aria-selected" ) + .removeAttr( "aria-labelledby" ) + .removeAttr( "aria-hidden" ) + .removeAttr( "aria-expanded" ) + .removeAttr( "role" ); + } + }); + + this.tabs.each(function() { + var li = $( this ), + prev = li.data( "ui-tabs-aria-controls" ); + if ( prev ) { + li.attr( "aria-controls", prev ); + } else { + li.removeAttr( "aria-controls" ); + } + }); + + this.panels.show(); + + if ( this.options.heightStyle !== "content" ) { + this.panels.css( "height", "" ); + } + }, + + enable: function( index ) { + var disabled = this.options.disabled; + if ( disabled === false ) { + return; + } + + if ( index === undefined ) { + disabled = false; + } else { + index = this._getIndex( index ); + if ( $.isArray( disabled ) ) { + disabled = $.map( disabled, function( num ) { + return num !== index ? num : null; + }); + } else { + disabled = $.map( this.tabs, function( li, num ) { + return num !== index ? num : null; + }); + } + } + this._setupDisabled( disabled ); + }, + + disable: function( index ) { + var disabled = this.options.disabled; + if ( disabled === true ) { + return; + } + + if ( index === undefined ) { + disabled = true; + } else { + index = this._getIndex( index ); + if ( $.inArray( index, disabled ) !== -1 ) { + return; + } + if ( $.isArray( disabled ) ) { + disabled = $.merge( [ index ], disabled ).sort(); + } else { + disabled = [ index ]; + } + } + this._setupDisabled( disabled ); + }, + + load: function( index, event ) { + index = this._getIndex( index ); + var that = this, + tab = this.tabs.eq( index ), + anchor = tab.find( ".ui-tabs-anchor" ), + panel = this._getPanelForTab( tab ), + eventData = { + tab: tab, + panel: panel + }; + + // not remote + if ( isLocal( anchor[ 0 ] ) ) { + return; + } + + this.xhr = $.ajax( this._ajaxSettings( anchor, event, eventData ) ); + + // support: jQuery <1.8 + // jQuery <1.8 returns false if the request is canceled in beforeSend, + // but as of 1.8, $.ajax() always returns a jqXHR object. + if ( this.xhr && this.xhr.statusText !== "canceled" ) { + tab.addClass( "ui-tabs-loading" ); + panel.attr( "aria-busy", "true" ); + + this.xhr + .success(function( response ) { + // support: jQuery <1.8 + // http://bugs.jquery.com/ticket/11778 + setTimeout(function() { + panel.html( response ); + that._trigger( "load", event, eventData ); + }, 1 ); + }) + .complete(function( jqXHR, status ) { + // support: jQuery <1.8 + // http://bugs.jquery.com/ticket/11778 + setTimeout(function() { + if ( status === "abort" ) { + that.panels.stop( false, true ); + } + + tab.removeClass( "ui-tabs-loading" ); + panel.removeAttr( "aria-busy" ); + + if ( jqXHR === that.xhr ) { + delete that.xhr; + } + }, 1 ); + }); + } + }, + + // TODO: Remove this function in 1.10 when ajaxOptions is removed + _ajaxSettings: function( anchor, event, eventData ) { + var that = this; + return { + url: anchor.attr( "href" ), + beforeSend: function( jqXHR, settings ) { + return that._trigger( "beforeLoad", event, + $.extend( { jqXHR : jqXHR, ajaxSettings: settings }, eventData ) ); + } + }; + }, + + _getPanelForTab: function( tab ) { + var id = $( tab ).attr( "aria-controls" ); + return this.element.find( this._sanitizeSelector( "#" + id ) ); + } +}); + +// DEPRECATED +if ( $.uiBackCompat !== false ) { + + // helper method for a lot of the back compat extensions + $.ui.tabs.prototype._ui = function( tab, panel ) { + return { + tab: tab, + panel: panel, + index: this.anchors.index( tab ) + }; + }; + + // url method + $.widget( "ui.tabs", $.ui.tabs, { + url: function( index, url ) { + this.anchors.eq( index ).attr( "href", url ); + } + }); + + // TODO: Remove _ajaxSettings() method when removing this extension + // ajaxOptions and cache options + $.widget( "ui.tabs", $.ui.tabs, { + options: { + ajaxOptions: null, + cache: false + }, + + _create: function() { + this._super(); + + var that = this; + + this._on({ tabsbeforeload: function( event, ui ) { + // tab is already cached + if ( $.data( ui.tab[ 0 ], "cache.tabs" ) ) { + event.preventDefault(); + return; + } + + ui.jqXHR.success(function() { + if ( that.options.cache ) { + $.data( ui.tab[ 0 ], "cache.tabs", true ); + } + }); + }}); + }, + + _ajaxSettings: function( anchor, event, ui ) { + var ajaxOptions = this.options.ajaxOptions; + return $.extend( {}, ajaxOptions, { + error: function( xhr, status ) { + try { + // Passing index avoid a race condition when this method is + // called after the user has selected another tab. + // Pass the anchor that initiated this request allows + // loadError to manipulate the tab content panel via $(a.hash) + ajaxOptions.error( + xhr, status, ui.tab.closest( "li" ).index(), ui.tab[ 0 ] ); + } + catch ( error ) {} + } + }, this._superApply( arguments ) ); + }, + + _setOption: function( key, value ) { + // reset cache if switching from cached to not cached + if ( key === "cache" && value === false ) { + this.anchors.removeData( "cache.tabs" ); + } + this._super( key, value ); + }, + + _destroy: function() { + this.anchors.removeData( "cache.tabs" ); + this._super(); + }, + + url: function( index ){ + this.anchors.eq( index ).removeData( "cache.tabs" ); + this._superApply( arguments ); + } + }); + + // abort method + $.widget( "ui.tabs", $.ui.tabs, { + abort: function() { + if ( this.xhr ) { + this.xhr.abort(); + } + } + }); + + // spinner + $.widget( "ui.tabs", $.ui.tabs, { + options: { + spinner: "Loading…" + }, + _create: function() { + this._super(); + this._on({ + tabsbeforeload: function( event, ui ) { + // Don't react to nested tabs or tabs that don't use a spinner + if ( event.target !== this.element[ 0 ] || + !this.options.spinner ) { + return; + } + + var span = ui.tab.find( "span" ), + html = span.html(); + span.html( this.options.spinner ); + ui.jqXHR.complete(function() { + span.html( html ); + }); + } + }); + } + }); + + // enable/disable events + $.widget( "ui.tabs", $.ui.tabs, { + options: { + enable: null, + disable: null + }, + + enable: function( index ) { + var options = this.options, + trigger; + + if ( index && options.disabled === true || + ( $.isArray( options.disabled ) && $.inArray( index, options.disabled ) !== -1 ) ) { + trigger = true; + } + + this._superApply( arguments ); + + if ( trigger ) { + this._trigger( "enable", null, this._ui( this.anchors[ index ], this.panels[ index ] ) ); + } + }, + + disable: function( index ) { + var options = this.options, + trigger; + + if ( index && options.disabled === false || + ( $.isArray( options.disabled ) && $.inArray( index, options.disabled ) === -1 ) ) { + trigger = true; + } + + this._superApply( arguments ); + + if ( trigger ) { + this._trigger( "disable", null, this._ui( this.anchors[ index ], this.panels[ index ] ) ); + } + } + }); + + // add/remove methods and events + $.widget( "ui.tabs", $.ui.tabs, { + options: { + add: null, + remove: null, + tabTemplate: "
  • #{label}
  • " + }, + + add: function( url, label, index ) { + if ( index === undefined ) { + index = this.anchors.length; + } + + var doInsertAfter, panel, + options = this.options, + li = $( options.tabTemplate + .replace( /#\{href\}/g, url ) + .replace( /#\{label\}/g, label ) ), + id = !url.indexOf( "#" ) ? + url.replace( "#", "" ) : + this._tabId( li ); + + li.addClass( "ui-state-default ui-corner-top" ).data( "ui-tabs-destroy", true ); + li.attr( "aria-controls", id ); + + doInsertAfter = index >= this.tabs.length; + + // try to find an existing element before creating a new one + panel = this.element.find( "#" + id ); + if ( !panel.length ) { + panel = this._createPanel( id ); + if ( doInsertAfter ) { + if ( index > 0 ) { + panel.insertAfter( this.panels.eq( -1 ) ); + } else { + panel.appendTo( this.element ); + } + } else { + panel.insertBefore( this.panels[ index ] ); + } + } + panel.addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" ).hide(); + + if ( doInsertAfter ) { + li.appendTo( this.tablist ); + } else { + li.insertBefore( this.tabs[ index ] ); + } + + options.disabled = $.map( options.disabled, function( n ) { + return n >= index ? ++n : n; + }); + + this.refresh(); + if ( this.tabs.length === 1 && options.active === false ) { + this.option( "active", 0 ); + } + + this._trigger( "add", null, this._ui( this.anchors[ index ], this.panels[ index ] ) ); + return this; + }, + + remove: function( index ) { + index = this._getIndex( index ); + var options = this.options, + tab = this.tabs.eq( index ).remove(), + panel = this._getPanelForTab( tab ).remove(); + + // If selected tab was removed focus tab to the right or + // in case the last tab was removed the tab to the left. + // We check for more than 2 tabs, because if there are only 2, + // then when we remove this tab, there will only be one tab left + // so we don't need to detect which tab to activate. + if ( tab.hasClass( "ui-tabs-active" ) && this.anchors.length > 2 ) { + this._activate( index + ( index + 1 < this.anchors.length ? 1 : -1 ) ); + } + + options.disabled = $.map( + $.grep( options.disabled, function( n ) { + return n !== index; + }), + function( n ) { + return n >= index ? --n : n; + }); + + this.refresh(); + + this._trigger( "remove", null, this._ui( tab.find( "a" )[ 0 ], panel[ 0 ] ) ); + return this; + } + }); + + // length method + $.widget( "ui.tabs", $.ui.tabs, { + length: function() { + return this.anchors.length; + } + }); + + // panel ids (idPrefix option + title attribute) + $.widget( "ui.tabs", $.ui.tabs, { + options: { + idPrefix: "ui-tabs-" + }, + + _tabId: function( tab ) { + var a = tab.is( "li" ) ? tab.find( "a[href]" ) : tab; + a = a[0]; + return $( a ).closest( "li" ).attr( "aria-controls" ) || + a.title && a.title.replace( /\s/g, "_" ).replace( /[^\w\u00c0-\uFFFF\-]/g, "" ) || + this.options.idPrefix + getNextTabId(); + } + }); + + // _createPanel method + $.widget( "ui.tabs", $.ui.tabs, { + options: { + panelTemplate: "
    " + }, + + _createPanel: function( id ) { + return $( this.options.panelTemplate ) + .attr( "id", id ) + .addClass( "ui-tabs-panel ui-widget-content ui-corner-bottom" ) + .data( "ui-tabs-destroy", true ); + } + }); + + // selected option + $.widget( "ui.tabs", $.ui.tabs, { + _create: function() { + var options = this.options; + if ( options.active === null && options.selected !== undefined ) { + options.active = options.selected === -1 ? false : options.selected; + } + this._super(); + options.selected = options.active; + if ( options.selected === false ) { + options.selected = -1; + } + }, + + _setOption: function( key, value ) { + if ( key !== "selected" ) { + return this._super( key, value ); + } + + var options = this.options; + this._super( "active", value === -1 ? false : value ); + options.selected = options.active; + if ( options.selected === false ) { + options.selected = -1; + } + }, + + _eventHandler: function() { + this._superApply( arguments ); + this.options.selected = this.options.active; + if ( this.options.selected === false ) { + this.options.selected = -1; + } + } + }); + + // show and select event + $.widget( "ui.tabs", $.ui.tabs, { + options: { + show: null, + select: null + }, + _create: function() { + this._super(); + if ( this.options.active !== false ) { + this._trigger( "show", null, this._ui( + this.active.find( ".ui-tabs-anchor" )[ 0 ], + this._getPanelForTab( this.active )[ 0 ] ) ); + } + }, + _trigger: function( type, event, data ) { + var tab, panel, + ret = this._superApply( arguments ); + + if ( !ret ) { + return false; + } + + if ( type === "beforeActivate" ) { + tab = data.newTab.length ? data.newTab : data.oldTab; + panel = data.newPanel.length ? data.newPanel : data.oldPanel; + ret = this._super( "select", event, { + tab: tab.find( ".ui-tabs-anchor" )[ 0], + panel: panel[ 0 ], + index: tab.closest( "li" ).index() + }); + } else if ( type === "activate" && data.newTab.length ) { + ret = this._super( "show", event, { + tab: data.newTab.find( ".ui-tabs-anchor" )[ 0 ], + panel: data.newPanel[ 0 ], + index: data.newTab.closest( "li" ).index() + }); + } + return ret; + } + }); + + // select method + $.widget( "ui.tabs", $.ui.tabs, { + select: function( index ) { + index = this._getIndex( index ); + if ( index === -1 ) { + if ( this.options.collapsible && this.options.selected !== -1 ) { + index = this.options.selected; + } else { + return; + } + } + this.anchors.eq( index ).trigger( this.options.event + this.eventNamespace ); + } + }); + + // cookie option + (function() { + + var listId = 0; + + $.widget( "ui.tabs", $.ui.tabs, { + options: { + cookie: null // e.g. { expires: 7, path: '/', domain: 'jquery.com', secure: true } + }, + _create: function() { + var options = this.options, + active; + if ( options.active == null && options.cookie ) { + active = parseInt( this._cookie(), 10 ); + if ( active === -1 ) { + active = false; + } + options.active = active; + } + this._super(); + }, + _cookie: function( active ) { + var cookie = [ this.cookie || + ( this.cookie = this.options.cookie.name || "ui-tabs-" + (++listId) ) ]; + if ( arguments.length ) { + cookie.push( active === false ? -1 : active ); + cookie.push( this.options.cookie ); + } + return $.cookie.apply( null, cookie ); + }, + _refresh: function() { + this._super(); + if ( this.options.cookie ) { + this._cookie( this.options.active, this.options.cookie ); + } + }, + _eventHandler: function() { + this._superApply( arguments ); + if ( this.options.cookie ) { + this._cookie( this.options.active, this.options.cookie ); + } + }, + _destroy: function() { + this._super(); + if ( this.options.cookie ) { + this._cookie( null, this.options.cookie ); + } + } + }); + + })(); + + // load event + $.widget( "ui.tabs", $.ui.tabs, { + _trigger: function( type, event, data ) { + var _data = $.extend( {}, data ); + if ( type === "load" ) { + _data.panel = _data.panel[ 0 ]; + _data.tab = _data.tab.find( ".ui-tabs-anchor" )[ 0 ]; + } + return this._super( type, event, _data ); + } + }); + + // fx option + // The new animation options (show, hide) conflict with the old show callback. + // The old fx option wins over show/hide anyway (always favor back-compat). + // If a user wants to use the new animation API, they must give up the old API. + $.widget( "ui.tabs", $.ui.tabs, { + options: { + fx: null // e.g. { height: "toggle", opacity: "toggle", duration: 200 } + }, + + _getFx: function() { + var hide, show, + fx = this.options.fx; + + if ( fx ) { + if ( $.isArray( fx ) ) { + hide = fx[ 0 ]; + show = fx[ 1 ]; + } else { + hide = show = fx; + } + } + + return fx ? { show: show, hide: hide } : null; + }, + + _toggle: function( event, eventData ) { + var that = this, + toShow = eventData.newPanel, + toHide = eventData.oldPanel, + fx = this._getFx(); + + if ( !fx ) { + return this._super( event, eventData ); + } + + that.running = true; + + function complete() { + that.running = false; + that._trigger( "activate", event, eventData ); + } + + function show() { + eventData.newTab.closest( "li" ).addClass( "ui-tabs-active ui-state-active" ); + + if ( toShow.length && fx.show ) { + toShow + .animate( fx.show, fx.show.duration, function() { + complete(); + }); + } else { + toShow.show(); + complete(); + } + } + + // start out by hiding, then showing, then completing + if ( toHide.length && fx.hide ) { + toHide.animate( fx.hide, fx.hide.duration, function() { + eventData.oldTab.closest( "li" ).removeClass( "ui-tabs-active ui-state-active" ); + show(); + }); + } else { + eventData.oldTab.closest( "li" ).removeClass( "ui-tabs-active ui-state-active" ); + toHide.hide(); + show(); + } + } + }); +} + +})( jQuery ); + +(function( $ ) { + +var increments = 0; + +function addDescribedBy( elem, id ) { + var describedby = (elem.attr( "aria-describedby" ) || "").split( /\s+/ ); + describedby.push( id ); + elem + .data( "ui-tooltip-id", id ) + .attr( "aria-describedby", $.trim( describedby.join( " " ) ) ); +} + +function removeDescribedBy( elem ) { + var id = elem.data( "ui-tooltip-id" ), + describedby = (elem.attr( "aria-describedby" ) || "").split( /\s+/ ), + index = $.inArray( id, describedby ); + if ( index !== -1 ) { + describedby.splice( index, 1 ); + } + + elem.removeData( "ui-tooltip-id" ); + describedby = $.trim( describedby.join( " " ) ); + if ( describedby ) { + elem.attr( "aria-describedby", describedby ); + } else { + elem.removeAttr( "aria-describedby" ); + } +} + +$.widget( "ui.tooltip", { + version: "1.9.2", + options: { + content: function() { + return $( this ).attr( "title" ); + }, + hide: true, + // Disabled elements have inconsistent behavior across browsers (#8661) + items: "[title]:not([disabled])", + position: { + my: "left top+15", + at: "left bottom", + collision: "flipfit flip" + }, + show: true, + tooltipClass: null, + track: false, + + // callbacks + close: null, + open: null + }, + + _create: function() { + this._on({ + mouseover: "open", + focusin: "open" + }); + + // IDs of generated tooltips, needed for destroy + this.tooltips = {}; + // IDs of parent tooltips where we removed the title attribute + this.parents = {}; + + if ( this.options.disabled ) { + this._disable(); + } + }, + + _setOption: function( key, value ) { + var that = this; + + if ( key === "disabled" ) { + this[ value ? "_disable" : "_enable" ](); + this.options[ key ] = value; + // disable element style changes + return; + } + + this._super( key, value ); + + if ( key === "content" ) { + $.each( this.tooltips, function( id, element ) { + that._updateContent( element ); + }); + } + }, + + _disable: function() { + var that = this; + + // close open tooltips + $.each( this.tooltips, function( id, element ) { + var event = $.Event( "blur" ); + event.target = event.currentTarget = element[0]; + that.close( event, true ); + }); + + // remove title attributes to prevent native tooltips + this.element.find( this.options.items ).andSelf().each(function() { + var element = $( this ); + if ( element.is( "[title]" ) ) { + element + .data( "ui-tooltip-title", element.attr( "title" ) ) + .attr( "title", "" ); + } + }); + }, + + _enable: function() { + // restore title attributes + this.element.find( this.options.items ).andSelf().each(function() { + var element = $( this ); + if ( element.data( "ui-tooltip-title" ) ) { + element.attr( "title", element.data( "ui-tooltip-title" ) ); + } + }); + }, + + open: function( event ) { + var that = this, + target = $( event ? event.target : this.element ) + // we need closest here due to mouseover bubbling, + // but always pointing at the same event target + .closest( this.options.items ); + + // No element to show a tooltip for or the tooltip is already open + if ( !target.length || target.data( "ui-tooltip-id" ) ) { + return; + } + + if ( target.attr( "title" ) ) { + target.data( "ui-tooltip-title", target.attr( "title" ) ); + } + + target.data( "ui-tooltip-open", true ); + + // kill parent tooltips, custom or native, for hover + if ( event && event.type === "mouseover" ) { + target.parents().each(function() { + var parent = $( this ), + blurEvent; + if ( parent.data( "ui-tooltip-open" ) ) { + blurEvent = $.Event( "blur" ); + blurEvent.target = blurEvent.currentTarget = this; + that.close( blurEvent, true ); + } + if ( parent.attr( "title" ) ) { + parent.uniqueId(); + that.parents[ this.id ] = { + element: this, + title: parent.attr( "title" ) + }; + parent.attr( "title", "" ); + } + }); + } + + this._updateContent( target, event ); + }, + + _updateContent: function( target, event ) { + var content, + contentOption = this.options.content, + that = this, + eventType = event ? event.type : null; + + if ( typeof contentOption === "string" ) { + return this._open( event, target, contentOption ); + } + + content = contentOption.call( target[0], function( response ) { + // ignore async response if tooltip was closed already + if ( !target.data( "ui-tooltip-open" ) ) { + return; + } + // IE may instantly serve a cached response for ajax requests + // delay this call to _open so the other call to _open runs first + that._delay(function() { + // jQuery creates a special event for focusin when it doesn't + // exist natively. To improve performance, the native event + // object is reused and the type is changed. Therefore, we can't + // rely on the type being correct after the event finished + // bubbling, so we set it back to the previous value. (#8740) + if ( event ) { + event.type = eventType; + } + this._open( event, target, response ); + }); + }); + if ( content ) { + this._open( event, target, content ); + } + }, + + _open: function( event, target, content ) { + var tooltip, events, delayedShow, + positionOption = $.extend( {}, this.options.position ); + + if ( !content ) { + return; + } + + // Content can be updated multiple times. If the tooltip already + // exists, then just update the content and bail. + tooltip = this._find( target ); + if ( tooltip.length ) { + tooltip.find( ".ui-tooltip-content" ).html( content ); + return; + } + + // if we have a title, clear it to prevent the native tooltip + // we have to check first to avoid defining a title if none exists + // (we don't want to cause an element to start matching [title]) + // + // We use removeAttr only for key events, to allow IE to export the correct + // accessible attributes. For mouse events, set to empty string to avoid + // native tooltip showing up (happens only when removing inside mouseover). + if ( target.is( "[title]" ) ) { + if ( event && event.type === "mouseover" ) { + target.attr( "title", "" ); + } else { + target.removeAttr( "title" ); + } + } + + tooltip = this._tooltip( target ); + addDescribedBy( target, tooltip.attr( "id" ) ); + tooltip.find( ".ui-tooltip-content" ).html( content ); + + function position( event ) { + positionOption.of = event; + if ( tooltip.is( ":hidden" ) ) { + return; + } + tooltip.position( positionOption ); + } + if ( this.options.track && event && /^mouse/.test( event.type ) ) { + this._on( this.document, { + mousemove: position + }); + // trigger once to override element-relative positioning + position( event ); + } else { + tooltip.position( $.extend({ + of: target + }, this.options.position ) ); + } + + tooltip.hide(); + + this._show( tooltip, this.options.show ); + // Handle tracking tooltips that are shown with a delay (#8644). As soon + // as the tooltip is visible, position the tooltip using the most recent + // event. + if ( this.options.show && this.options.show.delay ) { + delayedShow = setInterval(function() { + if ( tooltip.is( ":visible" ) ) { + position( positionOption.of ); + clearInterval( delayedShow ); + } + }, $.fx.interval ); + } + + this._trigger( "open", event, { tooltip: tooltip } ); + + events = { + keyup: function( event ) { + if ( event.keyCode === $.ui.keyCode.ESCAPE ) { + var fakeEvent = $.Event(event); + fakeEvent.currentTarget = target[0]; + this.close( fakeEvent, true ); + } + }, + remove: function() { + this._removeTooltip( tooltip ); + } + }; + if ( !event || event.type === "mouseover" ) { + events.mouseleave = "close"; + } + if ( !event || event.type === "focusin" ) { + events.focusout = "close"; + } + this._on( true, target, events ); + }, + + close: function( event ) { + var that = this, + target = $( event ? event.currentTarget : this.element ), + tooltip = this._find( target ); + + // disabling closes the tooltip, so we need to track when we're closing + // to avoid an infinite loop in case the tooltip becomes disabled on close + if ( this.closing ) { + return; + } + + // only set title if we had one before (see comment in _open()) + if ( target.data( "ui-tooltip-title" ) ) { + target.attr( "title", target.data( "ui-tooltip-title" ) ); + } + + removeDescribedBy( target ); + + tooltip.stop( true ); + this._hide( tooltip, this.options.hide, function() { + that._removeTooltip( $( this ) ); + }); + + target.removeData( "ui-tooltip-open" ); + this._off( target, "mouseleave focusout keyup" ); + // Remove 'remove' binding only on delegated targets + if ( target[0] !== this.element[0] ) { + this._off( target, "remove" ); + } + this._off( this.document, "mousemove" ); + + if ( event && event.type === "mouseleave" ) { + $.each( this.parents, function( id, parent ) { + $( parent.element ).attr( "title", parent.title ); + delete that.parents[ id ]; + }); + } + + this.closing = true; + this._trigger( "close", event, { tooltip: tooltip } ); + this.closing = false; + }, + + _tooltip: function( element ) { + var id = "ui-tooltip-" + increments++, + tooltip = $( "
    " ) + .attr({ + id: id, + role: "tooltip" + }) + .addClass( "ui-tooltip ui-widget ui-corner-all ui-widget-content " + + ( this.options.tooltipClass || "" ) ); + $( "
    " ) + .addClass( "ui-tooltip-content" ) + .appendTo( tooltip ); + tooltip.appendTo( this.document[0].body ); + if ( $.fn.bgiframe ) { + tooltip.bgiframe(); + } + this.tooltips[ id ] = element; + return tooltip; + }, + + _find: function( target ) { + var id = target.data( "ui-tooltip-id" ); + return id ? $( "#" + id ) : $(); + }, + + _removeTooltip: function( tooltip ) { + tooltip.remove(); + delete this.tooltips[ tooltip.attr( "id" ) ]; + }, + + _destroy: function() { + var that = this; + + // close open tooltips + $.each( this.tooltips, function( id, element ) { + // Delegate to close method to handle common cleanup + var event = $.Event( "blur" ); + event.target = event.currentTarget = element[0]; + that.close( event, true ); + + // Remove immediately; destroying an open tooltip doesn't use the + // hide animation + $( "#" + id ).remove(); + + // Restore the title + if ( element.data( "ui-tooltip-title" ) ) { + element.attr( "title", element.data( "ui-tooltip-title" ) ); + element.removeData( "ui-tooltip-title" ); + } + }); + } +}); + +}( jQuery ) ); + +///#source 1 1 /ClientSource/Scripts/Core/jquery.watermark.js +/* + Watermark plugin for jQuery + Version: 3.1.4 + http://jquery-watermark.googlecode.com/ + + Copyright (c) 2009-2012 Todd Northrop + http://www.speednet.biz/ + + August 13, 2012 + + Requires: jQuery 1.2.3+ + + Dual licensed under the MIT or GPL Version 2 licenses. + See mit-license.txt and gpl2-license.txt in the project root for details. +------------------------------------------------------*/ + +( function ( $, window, undefined ) { + +var + // String constants for data names + dataFlag = "watermark", + dataClass = "watermarkClass", + dataFocus = "watermarkFocus", + dataFormSubmit = "watermarkSubmit", + dataMaxLen = "watermarkMaxLength", + dataPassword = "watermarkPassword", + dataText = "watermarkText", + + // Copy of native jQuery regex use to strip return characters from element value + rreturn = /\r/g, + + // Used to determine if type attribute of input element is a non-text type (invalid) + rInvalidType = /^(button|checkbox|hidden|image|radio|range|reset|submit)$/i, + + // Includes only elements with watermark defined + selWatermarkDefined = "input:data(" + dataFlag + "),textarea:data(" + dataFlag + ")", + + // Includes only elements capable of having watermark + selWatermarkAble = ":watermarkable", + + // triggerFns: + // Array of function names to look for in the global namespace. + // Any such functions found will be hijacked to trigger a call to + // hideAll() any time they are called. The default value is the + // ASP.NET function that validates the controls on the page + // prior to a postback. + // + // Am I missing other important trigger function(s) to look for? + // Please leave me feedback: + // http://code.google.com/p/jquery-watermark/issues/list + triggerFns = [ + "Page_ClientValidate" + ], + + // Holds a value of true if a watermark was displayed since the last + // hideAll() was executed. Avoids repeatedly calling hideAll(). + pageDirty = false, + + // Detects if the browser can handle native placeholders + hasNativePlaceholder = ( "placeholder" in document.createElement( "input" ) ); + +// Best practice: this plugin adds only one method to the jQuery object. +// Also ensures that the watermark code is only added once. +$.watermark = $.watermark || { + + // Current version number of the plugin + version: "3.1.4", + + runOnce: true, + + // Default options used when watermarks are instantiated. + // Can be changed to affect the default behavior for all + // new or updated watermarks. + options: { + + // Default class name for all watermarks + className: "watermark", + + // If true, plugin will detect and use native browser support for + // watermarks, if available. (e.g., WebKit's placeholder attribute.) + useNative: true, + + // If true, all watermarks will be hidden during the window's + // beforeunload event. This is done mainly because WebKit + // browsers remember the watermark text during navigation + // and try to restore the watermark text after the user clicks + // the Back button. We can avoid this by hiding the text before + // the browser has a chance to save it. The regular unload event + // was tried, but it seems the browser saves the text before + // that event kicks off, because it didn't work. + hideBeforeUnload: true + }, + + // Hide one or more watermarks by specifying any selector type + // i.e., DOM element, string selector, jQuery matched set, etc. + hide: function ( selector ) { + $( selector ).filter( selWatermarkDefined ).each( + function () { + $.watermark._hide( $( this ) ); + } + ); + }, + + // Internal use only. + _hide: function ( $input, focus ) { + var elem = $input[ 0 ], + inputVal = ( elem.value || "" ).replace( rreturn, "" ), + inputWm = $input.data( dataText ) || "", + maxLen = $input.data( dataMaxLen ) || 0, + className = $input.data( dataClass ); + + if ( ( inputWm.length ) && ( inputVal == inputWm ) ) { + elem.value = ""; + + // Password type? + if ( $input.data( dataPassword ) ) { + + if ( ( $input.attr( "type" ) || "" ) === "text" ) { + var $pwd = $input.data( dataPassword ) || [], + $wrap = $input.parent() || []; + + if ( ( $pwd.length ) && ( $wrap.length ) ) { + $wrap[ 0 ].removeChild( $input[ 0 ] ); // Can't use jQuery methods, because they destroy data + $wrap[ 0 ].appendChild( $pwd[ 0 ] ); + $input = $pwd; + } + } + } + + if ( maxLen ) { + $input.attr( "maxLength", maxLen ); + $input.removeData( dataMaxLen ); + } + + if ( focus ) { + $input.attr( "autocomplete", "off" ); // Avoid NS_ERROR_XPC_JS_THREW_STRING error in Firefox + + window.setTimeout( + function () { + $input.select(); // Fix missing cursor in IE + } + , 1 ); + } + } + + className && $input.removeClass( className ); + }, + + // Display one or more watermarks by specifying any selector type + // i.e., DOM element, string selector, jQuery matched set, etc. + // If conditions are not right for displaying a watermark, ensures that watermark is not shown. + show: function ( selector ) { + $( selector ).filter( selWatermarkDefined ).each( + function () { + $.watermark._show( $( this ) ); + } + ); + }, + + // Internal use only. + _show: function ( $input ) { + var elem = $input[ 0 ], + val = ( elem.value || "" ).replace( rreturn, "" ), + text = $input.data( dataText ) || "", + type = $input.attr( "type" ) || "", + className = $input.data( dataClass ); + + if ( ( ( val.length == 0 ) || ( val == text ) ) && ( !$input.data( dataFocus ) ) ) { + pageDirty = true; + + // Password type? + if ( $input.data( dataPassword ) ) { + + if ( type === "password" ) { + var $pwd = $input.data( dataPassword ) || [], + $wrap = $input.parent() || []; + + if ( ( $pwd.length ) && ( $wrap.length ) ) { + $wrap[ 0 ].removeChild( $input[ 0 ] ); // Can't use jQuery methods, because they destroy data + $wrap[ 0 ].appendChild( $pwd[ 0 ] ); + $input = $pwd; + $input.attr( "maxLength", text.length ); + elem = $input[ 0 ]; + } + } + } + + // Ensure maxLength big enough to hold watermark (input of type="text" or type="search" only) + if ( ( type === "text" ) || ( type === "search" ) ) { + var maxLen = $input.attr( "maxLength" ) || 0; + + if ( ( maxLen > 0 ) && ( text.length > maxLen ) ) { + $input.data( dataMaxLen, maxLen ); + $input.attr( "maxLength", text.length ); + } + } + + className && $input.addClass( className ); + elem.value = text; + } + else { + $.watermark._hide( $input ); + } + }, + + // Hides all watermarks on the current page. + hideAll: function () { + if ( pageDirty ) { + $.watermark.hide( selWatermarkAble ); + pageDirty = false; + } + }, + + // Displays all watermarks on the current page. + showAll: function () { + $.watermark.show( selWatermarkAble ); + } +}; + +$.fn.watermark = $.fn.watermark || function ( text, options ) { + /// + /// Set watermark text and class name on all input elements of type="text/password/search" and + /// textareas within the matched set. If className is not specified in options, the default is + /// "watermark". Within the matched set, only input elements with type="text/password/search" + /// and textareas are affected; all other elements are ignored. + /// + /// + /// Returns the original jQuery matched set (not just the input and texarea elements). + /// + /// + /// Text to display as a watermark when the input or textarea element has an empty value and does not + /// have focus. The first time watermark() is called on an element, if this argument is empty (or not + /// a String type), then the watermark will have the net effect of only changing the class name when + /// the input or textarea element's value is empty and it does not have focus. + /// + /// + /// Provides the ability to override the default watermark options ($.watermark.options). For backward + /// compatibility, if a string value is supplied, it is used as the class name that overrides the class + /// name in $.watermark.options.className. Properties include: + /// className: When the watermark is visible, the element will be styled using this class name. + /// useNative (Boolean or Function): Specifies if native browser support for watermarks will supersede + /// plugin functionality. If useNative is a function, the return value from the function will + /// determine if native support is used. The function is passed one argument -- a jQuery object + /// containing the element being tested as the only element in its matched set -- and the DOM + /// element being tested is the object on which the function is invoked (the value of "this"). + /// + /// + /// The effect of changing the text and class name on an input element is called a watermark because + /// typically light gray text is used to provide a hint as to what type of input is required. However, + /// the appearance of the watermark can be something completely different: simply change the CSS style + /// pertaining to the supplied class name. + /// + /// The first time watermark() is called on an element, the watermark text and class name are initialized, + /// and the focus and blur events are hooked in order to control the display of the watermark. Also, as + /// of version 3.0, drag and drop events are hooked to guard against dropped text being appended to the + /// watermark. If native watermark support is provided by the browser, it is detected and used, unless + /// the useNative option is set to false. + /// + /// Subsequently, watermark() can be called again on an element in order to change the watermark text + /// and/or class name, and it can also be called without any arguments in order to refresh the display. + /// + /// For example, after changing the value of the input or textarea element programmatically, watermark() + /// should be called without any arguments to refresh the display, because the change event is only + /// triggered by user actions, not by programmatic changes to an input or textarea element's value. + /// + /// The one exception to programmatic updates is for password input elements: you are strongly cautioned + /// against changing the value of a password input element programmatically (after the page loads). + /// The reason is that some fairly hairy code is required behind the scenes to make the watermarks bypass + /// IE security and switch back and forth between clear text (for watermarks) and obscured text (for + /// passwords). It is *possible* to make programmatic changes, but it must be done in a certain way, and + /// overall it is not recommended. + /// + + if ( !this.length ) { + return this; + } + + var hasClass = false, + hasText = ( typeof( text ) === "string" ); + + if ( hasText ) { + text = text.replace( rreturn, "" ); + } + + if ( typeof( options ) === "object" ) { + hasClass = ( typeof( options.className ) === "string" ); + options = $.extend( {}, $.watermark.options, options ); + } + else if ( typeof( options ) === "string" ) { + hasClass = true; + options = $.extend( {}, $.watermark.options, { className: options } ); + } + else { + options = $.watermark.options; + } + + if ( typeof( options.useNative ) !== "function" ) { + options.useNative = options.useNative? function () { return true; } : function () { return false; }; + } + + return this.each( + function () { + var $input = $( this ); + + if ( !$input.is( selWatermarkAble ) ) { + return; + } + + // Watermark already initialized? + if ( $input.data( dataFlag ) ) { + + // If re-defining text or class, first remove existing watermark, then make changes + if ( hasText || hasClass ) { + $.watermark._hide( $input ); + + if ( hasText ) { + $input.data( dataText, text ); + } + + if ( hasClass ) { + $input.data( dataClass, options.className ); + } + } + } + else { + + // Detect and use native browser support, if enabled in options + if ( + ( hasNativePlaceholder ) + && ( options.useNative.call( this, $input ) ) + && ( ( $input.attr( "tagName" ) || "" ) !== "TEXTAREA" ) + ) { + // className is not set because current placeholder standard doesn't + // have a separate class name property for placeholders (watermarks). + if ( hasText ) { + $input.attr( "placeholder", text ); + } + + // Only set data flag for non-native watermarks + // [purposely commented-out] -> $input.data(dataFlag, 1); + return; + } + + $input.data( dataText, hasText? text : "" ); + $input.data( dataClass, options.className ); + $input.data( dataFlag, 1 ); // Flag indicates watermark was initialized + + // Special processing for password type + if ( ( $input.attr( "type" ) || "" ) === "password" ) { + var $wrap = $input.wrap( "" ).parent(), + $wm = $( $wrap.html().replace( /type=["']?password["']?/i, 'type="text"' ) ); + + $wm.data( dataText, $input.data( dataText ) ); + $wm.data( dataClass, $input.data( dataClass ) ); + $wm.data( dataFlag, 1 ); + $wm.attr( "maxLength", text.length ); + + $wm.focus( + function () { + $.watermark._hide( $wm, true ); + } + ).bind( "dragenter", + function () { + $.watermark._hide( $wm ); + } + ).bind( "dragend", + function () { + window.setTimeout( function () { $wm.blur(); }, 1 ); + } + ); + + $input.blur( + function () { + $.watermark._show( $input ); + } + ).bind( "dragleave", + function () { + $.watermark._show( $input ); + } + ); + + $wm.data( dataPassword, $input ); + $input.data( dataPassword, $wm ); + } + else { + + $input.focus( + function () { + $input.data( dataFocus, 1 ); + $.watermark._hide( $input, true ); + } + ).blur( + function () { + $input.data( dataFocus, 0 ); + $.watermark._show( $input ); + } + ).bind( "dragenter", + function () { + $.watermark._hide( $input ); + } + ).bind( "dragleave", + function () { + $.watermark._show( $input ); + } + ).bind( "dragend", + function () { + window.setTimeout( function () { $.watermark._show($input); }, 1 ); + } + ).bind( "drop", + // Firefox makes this lovely function necessary because the dropped text + // is merged with the watermark before the drop event is called. + function ( evt ) { + var elem = $input[ 0 ], + dropText = evt.originalEvent.dataTransfer.getData( "Text" ); + + if ( ( elem.value || "" ).replace( rreturn, "" ).replace( dropText, "" ) === $input.data( dataText ) ) { + elem.value = dropText; + } + + $input.focus(); + } + ); + } + + // In order to reliably clear all watermarks before form submission, + // we need to replace the form's submit function with our own + // function. Otherwise watermarks won't be cleared when the form + // is submitted programmatically. + if ( this.form ) { + var form = this.form, + $form = $( form ); + + if ( !$form.data( dataFormSubmit ) ) { + $form.submit( $.watermark.hideAll ); + + // form.submit exists for all browsers except Google Chrome + // (see "else" below for explanation) + if ( form.submit ) { + $form.data( dataFormSubmit, form.submit ); + + form.submit = ( function ( f, $f ) { + return function () { + var nativeSubmit = $f.data( dataFormSubmit ); + + $.watermark.hideAll(); + + if ( nativeSubmit.apply ) { + nativeSubmit.apply( f, Array.prototype.slice.call( arguments ) ); + } + else { + nativeSubmit(); + } + }; + })( form, $form ); + } + else { + $form.data( dataFormSubmit, 1 ); + + // This strangeness is due to the fact that Google Chrome's + // form.submit function is not visible to JavaScript (identifies + // as "undefined"). I had to invent a solution here because hours + // of Googling (ironically) for an answer did not turn up anything + // useful. Within my own form.submit function I delete the form's + // submit function, and then call the non-existent function -- + // which, in the world of Google Chrome, still exists. + form.submit = ( function ( f ) { + return function () { + $.watermark.hideAll(); + delete f.submit; + f.submit(); + }; + })( form ); + } + } + } + } + + $.watermark._show( $input ); + } + ); +}; + +// The code included within the following if structure is guaranteed to only run once, +// even if the watermark script file is included multiple times in the page. +if ( $.watermark.runOnce ) { + $.watermark.runOnce = false; + + $.extend( $.expr[ ":" ], { + + // Extends jQuery with a custom selector - ":data(...)" + // :data() Includes elements that have a specific name defined in the jQuery data + // collection. (Only the existence of the name is checked; the value is ignored.) + // A more sophisticated version of the :data() custom selector originally part of this plugin + // was removed for compatibility with jQuery UI. The original code can be found in the SVN + // source listing in the file, "jquery.data.js". + data: $.expr.createPseudo ? + $.expr.createPseudo( function( dataName ) { + return function( elem ) { + return !!$.data( elem, dataName ); + }; + }) : + // support: jQuery <1.8 + function( elem, i, match ) { + return !!$.data( elem, match[ 3 ] ); + }, + + // Extends jQuery with a custom selector - ":watermarkable" + // Includes elements that can be watermarked, including textareas and most input elements + // that accept text input. It uses a "negative" test (i.e., testing for input types that DON'T + // work) because the HTML spec states that you can basically use any type, and if it doesn't + // recognize the type it will default to type=text. So if we only looked for certain type attributes + // we would fail to recognize non-standard types, which are still valid and watermarkable. + watermarkable: function ( elem ) { + var type, + name = elem.nodeName; + + if ( name === "TEXTAREA" ) { + return true; + } + + if ( name !== "INPUT" ) { + return false; + } + + type = elem.getAttribute( "type" ); + + return ( ( !type ) || ( !rInvalidType.test( type ) ) ); + } + }); + + // Overloads the jQuery .val() function to return the underlying input value on + // watermarked input elements. When .val() is being used to set values, this + // function ensures watermarks are properly set/removed after the values are set. + // Uses self-executing function to override the default jQuery function. + ( function ( valOld ) { + + $.fn.val = function () { + var args = Array.prototype.slice.call( arguments ); + + // Best practice: return immediately if empty matched set + if ( !this.length ) { + return args.length? this : undefined; + } + + // If no args, then we're getting the value of the first element; + // else we're setting values for all elements in matched set + if ( !args.length ) { + + // If element is watermarked, get the underlying value; + // else use native jQuery .val() + if ( this.data( dataFlag ) ) { + var v = ( this[ 0 ].value || "" ).replace( rreturn, "" ); + return ( v === ( this.data( dataText ) || "" ) )? "" : v; + } + else { + return valOld.apply( this ); + } + } + else { + valOld.apply( this, args ); + $.watermark.show( this ); + return this; + } + }; + + })( $.fn.val ); + + // Hijack any functions found in the triggerFns list + if ( triggerFns.length ) { + + // Wait until DOM is ready before searching + $( function () { + var i, name, fn; + + for ( i = triggerFns.length - 1; i >= 0; i-- ) { + name = triggerFns[ i ]; + fn = window[ name ]; + + if ( typeof( fn ) === "function" ) { + window[ name ] = ( function ( origFn ) { + return function () { + $.watermark.hideAll(); + return origFn.apply( null, Array.prototype.slice.call( arguments ) ); + }; + })( fn ); + } + } + }); + } + + $( window ).bind( "beforeunload", function () { + if ( $.watermark.options.hideBeforeUnload ) { + $.watermark.hideAll(); + } + }); +} + +})( jQuery, window ); + +///#source 1 1 /ClientSource/Scripts/Core/jquery.dataTables.js +/** + * @summary DataTables + * @description Paginate, search and sort HTML tables + * @version 1.9.3 + * @file jquery.dataTables.js + * @author Allan Jardine (www.sprymedia.co.uk) + * @contact www.sprymedia.co.uk/contact + * + * @copyright Copyright 2008-2012 Allan Jardine, all rights reserved. + * + * This source file is free software, under either the GPL v2 license or a + * BSD style license, available at: + * http://datatables.net/license_gpl2 + * http://datatables.net/license_bsd + * + * This source file is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details. + * + * For details please refer to: http://www.datatables.net + */ + +/*jslint evil: true, undef: true, browser: true */ +/*globals $, jQuery,_fnExternApiFunc,_fnInitialise,_fnInitComplete,_fnLanguageCompat,_fnAddColumn,_fnColumnOptions,_fnAddData,_fnCreateTr,_fnGatherData,_fnBuildHead,_fnDrawHead,_fnDraw,_fnReDraw,_fnAjaxUpdate,_fnAjaxParameters,_fnAjaxUpdateDraw,_fnServerParams,_fnAddOptionsHtml,_fnFeatureHtmlTable,_fnScrollDraw,_fnAdjustColumnSizing,_fnFeatureHtmlFilter,_fnFilterComplete,_fnFilterCustom,_fnFilterColumn,_fnFilter,_fnBuildSearchArray,_fnBuildSearchRow,_fnFilterCreateSearch,_fnDataToSearch,_fnSort,_fnSortAttachListener,_fnSortingClasses,_fnFeatureHtmlPaginate,_fnPageChange,_fnFeatureHtmlInfo,_fnUpdateInfo,_fnFeatureHtmlLength,_fnFeatureHtmlProcessing,_fnProcessingDisplay,_fnVisibleToColumnIndex,_fnColumnIndexToVisible,_fnNodeToDataIndex,_fnVisbleColumns,_fnCalculateEnd,_fnConvertToWidth,_fnCalculateColumnWidths,_fnScrollingWidthAdjust,_fnGetWidestNode,_fnGetMaxLenString,_fnStringToCss,_fnDetectType,_fnSettingsFromNode,_fnGetDataMaster,_fnGetTrNodes,_fnGetTdNodes,_fnEscapeRegex,_fnDeleteIndex,_fnReOrderIndex,_fnColumnOrdering,_fnLog,_fnClearTable,_fnSaveState,_fnLoadState,_fnCreateCookie,_fnReadCookie,_fnDetectHeader,_fnGetUniqueThs,_fnScrollBarWidth,_fnApplyToChildren,_fnMap,_fnGetRowData,_fnGetCellData,_fnSetCellData,_fnGetObjectDataFn,_fnSetObjectDataFn,_fnApplyColumnDefs,_fnBindAction,_fnCallbackReg,_fnCallbackFire,_fnJsonString,_fnRender,_fnNodeToColumnIndex,_fnInfoMacros,_fnBrowserDetect,_fnGetColumns*/ + +(/** @lends */function($, window, document, undefined) { + /** + * DataTables is a plug-in for the jQuery Javascript library. It is a + * highly flexible tool, based upon the foundations of progressive + * enhancement, which will add advanced interaction controls to any + * HTML table. For a full list of features please refer to + * DataTables.net. + * + * Note that the DataTable object is not a global variable but is + * aliased to jQuery.fn.DataTable and jQuery.fn.dataTable through which + * it may be accessed. + * + * @class + * @param {object} [oInit={}] Configuration object for DataTables. Options + * are defined by {@link DataTable.defaults} + * @requires jQuery 1.3+ + * + * @example + * // Basic initialisation + * $(document).ready( function { + * $('#example').dataTable(); + * } ); + * + * @example + * // Initialisation with configuration options - in this case, disable + * // pagination and sorting. + * $(document).ready( function { + * $('#example').dataTable( { + * "bPaginate": false, + * "bSort": false + * } ); + * } ); + */ + var DataTable = function( oInit ) + { + + + /** + * Add a column to the list used for the table with default values + * @param {object} oSettings dataTables settings object + * @param {node} nTh The th element for this column + * @memberof DataTable#oApi + */ + function _fnAddColumn( oSettings, nTh ) + { + var oDefaults = DataTable.defaults.columns; + var iCol = oSettings.aoColumns.length; + var oCol = $.extend( {}, DataTable.models.oColumn, oDefaults, { + "sSortingClass": oSettings.oClasses.sSortable, + "sSortingClassJUI": oSettings.oClasses.sSortJUI, + "nTh": nTh ? nTh : document.createElement('th'), + "sTitle": oDefaults.sTitle ? oDefaults.sTitle : nTh ? nTh.innerHTML : '', + "aDataSort": oDefaults.aDataSort ? oDefaults.aDataSort : [iCol], + "mData": oDefaults.mData ? oDefaults.oDefaults : iCol + } ); + oSettings.aoColumns.push( oCol ); + + /* Add a column specific filter */ + if ( oSettings.aoPreSearchCols[ iCol ] === undefined || oSettings.aoPreSearchCols[ iCol ] === null ) + { + oSettings.aoPreSearchCols[ iCol ] = $.extend( {}, DataTable.models.oSearch ); + } + else + { + var oPre = oSettings.aoPreSearchCols[ iCol ]; + + /* Don't require that the user must specify bRegex, bSmart or bCaseInsensitive */ + if ( oPre.bRegex === undefined ) + { + oPre.bRegex = true; + } + + if ( oPre.bSmart === undefined ) + { + oPre.bSmart = true; + } + + if ( oPre.bCaseInsensitive === undefined ) + { + oPre.bCaseInsensitive = true; + } + } + + /* Use the column options function to initialise classes etc */ + _fnColumnOptions( oSettings, iCol, null ); + } + + + /** + * Apply options for a column + * @param {object} oSettings dataTables settings object + * @param {int} iCol column index to consider + * @param {object} oOptions object with sType, bVisible and bSearchable etc + * @memberof DataTable#oApi + */ + function _fnColumnOptions( oSettings, iCol, oOptions ) + { + var oCol = oSettings.aoColumns[ iCol ]; + + /* User specified column options */ + if ( oOptions !== undefined && oOptions !== null ) + { + /* Backwards compatibility for mDataProp */ + if ( oOptions.mDataProp && !oOptions.mData ) + { + oOptions.mData = oOptions.mDataProp; + } + + if ( oOptions.sType !== undefined ) + { + oCol.sType = oOptions.sType; + oCol._bAutoType = false; + } + + $.extend( oCol, oOptions ); + _fnMap( oCol, oOptions, "sWidth", "sWidthOrig" ); + + /* iDataSort to be applied (backwards compatibility), but aDataSort will take + * priority if defined + */ + if ( oOptions.iDataSort !== undefined ) + { + oCol.aDataSort = [ oOptions.iDataSort ]; + } + _fnMap( oCol, oOptions, "aDataSort" ); + } + + /* Cache the data get and set functions for speed */ + var mRender = oCol.mRender ? _fnGetObjectDataFn( oCol.mRender ) : null; + var mData = _fnGetObjectDataFn( oCol.mData ); + + oCol.fnGetData = function (oData, sSpecific) { + var innerData = mData( oData, sSpecific ); + + if ( oCol.mRender && (sSpecific && sSpecific !== '') ) + { + return mRender( innerData, sSpecific, oData ); + } + return innerData; + }; + oCol.fnSetData = _fnSetObjectDataFn( oCol.mData ); + + /* Feature sorting overrides column specific when off */ + if ( !oSettings.oFeatures.bSort ) + { + oCol.bSortable = false; + } + + /* Check that the class assignment is correct for sorting */ + if ( !oCol.bSortable || + ($.inArray('asc', oCol.asSorting) == -1 && $.inArray('desc', oCol.asSorting) == -1) ) + { + oCol.sSortingClass = oSettings.oClasses.sSortableNone; + oCol.sSortingClassJUI = ""; + } + else if ( oCol.bSortable || + ($.inArray('asc', oCol.asSorting) == -1 && $.inArray('desc', oCol.asSorting) == -1) ) + { + oCol.sSortingClass = oSettings.oClasses.sSortable; + oCol.sSortingClassJUI = oSettings.oClasses.sSortJUI; + } + else if ( $.inArray('asc', oCol.asSorting) != -1 && $.inArray('desc', oCol.asSorting) == -1 ) + { + oCol.sSortingClass = oSettings.oClasses.sSortableAsc; + oCol.sSortingClassJUI = oSettings.oClasses.sSortJUIAscAllowed; + } + else if ( $.inArray('asc', oCol.asSorting) == -1 && $.inArray('desc', oCol.asSorting) != -1 ) + { + oCol.sSortingClass = oSettings.oClasses.sSortableDesc; + oCol.sSortingClassJUI = oSettings.oClasses.sSortJUIDescAllowed; + } + } + + + /** + * Adjust the table column widths for new data. Note: you would probably want to + * do a redraw after calling this function! + * @param {object} oSettings dataTables settings object + * @memberof DataTable#oApi + */ + function _fnAdjustColumnSizing ( oSettings ) + { + /* Not interested in doing column width calculation if auto-width is disabled */ + if ( oSettings.oFeatures.bAutoWidth === false ) + { + return false; + } + + _fnCalculateColumnWidths( oSettings ); + for ( var i=0 , iLen=oSettings.aoColumns.length ; i
    ')[0]; + oSettings.nTable.parentNode.insertBefore( nHolding, oSettings.nTable ); + + /* + * All DataTables are wrapped in a div + */ + oSettings.nTableWrapper = $('
    ')[0]; + oSettings.nTableReinsertBefore = oSettings.nTable.nextSibling; + + /* Track where we want to insert the option */ + var nInsertNode = oSettings.nTableWrapper; + + /* Loop over the user set positioning and place the elements as needed */ + var aDom = oSettings.sDom.split(''); + var nTmp, iPushFeature, cOption, nNewNode, cNext, sAttr, j; + for ( var i=0 ; i
    ')[0]; + + /* Check to see if we should append an id and/or a class name to the container */ + cNext = aDom[i+1]; + if ( cNext == "'" || cNext == '"' ) + { + sAttr = ""; + j = 2; + while ( aDom[i+j] != cNext ) + { + sAttr += aDom[i+j]; + j++; + } + + /* Replace jQuery UI constants */ + if ( sAttr == "H" ) + { + sAttr = oSettings.oClasses.sJUIHeader; + } + else if ( sAttr == "F" ) + { + sAttr = oSettings.oClasses.sJUIFooter; + } + + /* The attribute can be in the format of "#id.class", "#id" or "class" This logic + * breaks the string into parts and applies them as needed + */ + if ( sAttr.indexOf('.') != -1 ) + { + var aSplit = sAttr.split('.'); + nNewNode.id = aSplit[0].substr(1, aSplit[0].length-1); + nNewNode.className = aSplit[1]; + } + else if ( sAttr.charAt(0) == "#" ) + { + nNewNode.id = sAttr.substr(1, sAttr.length-1); + } + else + { + nNewNode.className = sAttr; + } + + i += j; /* Move along the position array */ + } + + nInsertNode.appendChild( nNewNode ); + nInsertNode = nNewNode; + } + else if ( cOption == '>' ) + { + /* End container div */ + nInsertNode = nInsertNode.parentNode; + } + else if ( cOption == 'l' && oSettings.oFeatures.bPaginate && oSettings.oFeatures.bLengthChange ) + { + /* Length */ + nTmp = _fnFeatureHtmlLength( oSettings ); + iPushFeature = 1; + } + else if ( cOption == 'f' && oSettings.oFeatures.bFilter ) + { + /* Filter */ + nTmp = _fnFeatureHtmlFilter( oSettings ); + iPushFeature = 1; + } + else if ( cOption == 'r' && oSettings.oFeatures.bProcessing ) + { + /* pRocessing */ + nTmp = _fnFeatureHtmlProcessing( oSettings ); + iPushFeature = 1; + } + else if ( cOption == 't' ) + { + /* Table */ + nTmp = _fnFeatureHtmlTable( oSettings ); + iPushFeature = 1; + } + else if ( cOption == 'i' && oSettings.oFeatures.bInfo ) + { + /* Info */ + nTmp = _fnFeatureHtmlInfo( oSettings ); + iPushFeature = 1; + } + else if ( cOption == 'p' && oSettings.oFeatures.bPaginate ) + { + /* Pagination */ + nTmp = _fnFeatureHtmlPaginate( oSettings ); + iPushFeature = 1; + } + else if ( DataTable.ext.aoFeatures.length !== 0 ) + { + /* Plug-in features */ + var aoFeatures = DataTable.ext.aoFeatures; + for ( var k=0, kLen=aoFeatures.length ; k') : + sSearchStr==="" ? '' : sSearchStr+' '; + + var nFilter = document.createElement( 'div' ); + nFilter.className = oSettings.oClasses.sFilter; + nFilter.innerHTML = ''; + if ( !oSettings.aanFeatures.f ) + { + nFilter.id = oSettings.sTableId+'_filter'; + } + + var jqFilter = $('input[type="text"]', nFilter); + + // Store a reference to the input element, so other input elements could be + // added to the filter wrapper if needed (submit button for example) + nFilter._DT_Input = jqFilter[0]; + + jqFilter.val( oPreviousSearch.sSearch.replace('"','"') ); + jqFilter.bind( 'keyup.DT', function(e) { + /* Update all other filter input elements for the new display */ + var n = oSettings.aanFeatures.f; + var val = this.value==="" ? "" : this.value; // mental IE8 fix :-( + + for ( var i=0, iLen=n.length ; i=0 ; i-- ) + { + var sData = _fnDataToSearch( _fnGetCellData( oSettings, oSettings.aiDisplay[i], iColumn, 'filter' ), + oSettings.aoColumns[iColumn].sType ); + if ( ! rpSearch.test( sData ) ) + { + oSettings.aiDisplay.splice( i, 1 ); + iIndexCorrector++; + } + } + } + + + /** + * Filter the data table based on user input and draw the table + * @param {object} oSettings dataTables settings object + * @param {string} sInput string to filter on + * @param {int} iForce optional - force a research of the master array (1) or not (undefined or 0) + * @param {bool} bRegex treat as a regular expression or not + * @param {bool} bSmart perform smart filtering or not + * @param {bool} bCaseInsensitive Do case insenstive matching or not + * @memberof DataTable#oApi + */ + function _fnFilter( oSettings, sInput, iForce, bRegex, bSmart, bCaseInsensitive ) + { + var i; + var rpSearch = _fnFilterCreateSearch( sInput, bRegex, bSmart, bCaseInsensitive ); + var oPrevSearch = oSettings.oPreviousSearch; + + /* Check if we are forcing or not - optional parameter */ + if ( !iForce ) + { + iForce = 0; + } + + /* Need to take account of custom filtering functions - always filter */ + if ( DataTable.ext.afnFiltering.length !== 0 ) + { + iForce = 1; + } + + /* + * If the input is blank - we want the full data set + */ + if ( sInput.length <= 0 ) + { + oSettings.aiDisplay.splice( 0, oSettings.aiDisplay.length); + oSettings.aiDisplay = oSettings.aiDisplayMaster.slice(); + } + else + { + /* + * We are starting a new search or the new search string is smaller + * then the old one (i.e. delete). Search from the master array + */ + if ( oSettings.aiDisplay.length == oSettings.aiDisplayMaster.length || + oPrevSearch.sSearch.length > sInput.length || iForce == 1 || + sInput.indexOf(oPrevSearch.sSearch) !== 0 ) + { + /* Nuke the old display array - we are going to rebuild it */ + oSettings.aiDisplay.splice( 0, oSettings.aiDisplay.length); + + /* Force a rebuild of the search array */ + _fnBuildSearchArray( oSettings, 1 ); + + /* Search through all records to populate the search array + * The the oSettings.aiDisplayMaster and asDataSearch arrays have 1 to 1 + * mapping + */ + for ( i=0 ; i').html(sSearch).text(); + } + + // Strip newline characters + return sSearch.replace( /[\n\r]/g, " " ); + } + + /** + * Build a regular expression object suitable for searching a table + * @param {string} sSearch string to search for + * @param {bool} bRegex treat as a regular expression or not + * @param {bool} bSmart perform smart filtering or not + * @param {bool} bCaseInsensitive Do case insensitive matching or not + * @returns {RegExp} constructed object + * @memberof DataTable#oApi + */ + function _fnFilterCreateSearch( sSearch, bRegex, bSmart, bCaseInsensitive ) + { + var asSearch, sRegExpString; + + if ( bSmart ) + { + /* Generate the regular expression to use. Something along the lines of: + * ^(?=.*?\bone\b)(?=.*?\btwo\b)(?=.*?\bthree\b).*$ + */ + asSearch = bRegex ? sSearch.split( ' ' ) : _fnEscapeRegex( sSearch ).split( ' ' ); + sRegExpString = '^(?=.*?'+asSearch.join( ')(?=.*?' )+').*$'; + return new RegExp( sRegExpString, bCaseInsensitive ? "i" : "" ); + } + else + { + sSearch = bRegex ? sSearch : _fnEscapeRegex( sSearch ); + return new RegExp( sSearch, bCaseInsensitive ? "i" : "" ); + } + } + + + /** + * Convert raw data into something that the user can search on + * @param {string} sData data to be modified + * @param {string} sType data type + * @returns {string} search string + * @memberof DataTable#oApi + */ + function _fnDataToSearch ( sData, sType ) + { + if ( typeof DataTable.ext.ofnSearch[sType] === "function" ) + { + return DataTable.ext.ofnSearch[sType]( sData ); + } + else if ( sData === null ) + { + return ''; + } + else if ( sType == "html" ) + { + return sData.replace(/[\r\n]/g," ").replace( /<.*?>/g, "" ); + } + else if ( typeof sData === "string" ) + { + return sData.replace(/[\r\n]/g," "); + } + return sData; + } + + + /** + * scape a string such that it can be used in a regular expression + * @param {string} sVal string to escape + * @returns {string} escaped string + * @memberof DataTable#oApi + */ + function _fnEscapeRegex ( sVal ) + { + var acEscape = [ '/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\', '$', '^', '-' ]; + var reReplace = new RegExp( '(\\' + acEscape.join('|\\') + ')', 'g' ); + return sVal.replace(reReplace, '\\$1'); + } + + + + /** + * Generate the node required for the info display + * @param {object} oSettings dataTables settings object + * @returns {node} Information element + * @memberof DataTable#oApi + */ + function _fnFeatureHtmlInfo ( oSettings ) + { + var nInfo = document.createElement( 'div' ); + nInfo.className = oSettings.oClasses.sInfo; + + /* Actions that are to be taken once only for this feature */ + if ( !oSettings.aanFeatures.i ) + { + /* Add draw callback */ + oSettings.aoDrawCallback.push( { + "fn": _fnUpdateInfo, + "sName": "information" + } ); + + /* Add id */ + nInfo.id = oSettings.sTableId+'_info'; + } + oSettings.nTable.setAttribute( 'aria-describedby', oSettings.sTableId+'_info' ); + + return nInfo; + } + + + /** + * Update the information elements in the display + * @param {object} oSettings dataTables settings object + * @memberof DataTable#oApi + */ + function _fnUpdateInfo ( oSettings ) + { + /* Show information about the table */ + if ( !oSettings.oFeatures.bInfo || oSettings.aanFeatures.i.length === 0 ) + { + return; + } + + var + oLang = oSettings.oLanguage, + iStart = oSettings._iDisplayStart+1, + iEnd = oSettings.fnDisplayEnd(), + iMax = oSettings.fnRecordsTotal(), + iTotal = oSettings.fnRecordsDisplay(), + sOut; + + if ( iTotal === 0 && iTotal == iMax ) + { + /* Empty record set */ + sOut = oLang.sInfoEmpty; + } + else if ( iTotal === 0 ) + { + /* Empty record set after filtering */ + sOut = oLang.sInfoEmpty +' '+ oLang.sInfoFiltered; + } + else if ( iTotal == iMax ) + { + /* Normal record set */ + sOut = oLang.sInfo; + } + else + { + /* Record set after filtering */ + sOut = oLang.sInfo +' '+ oLang.sInfoFiltered; + } + + // Convert the macros + sOut += oLang.sInfoPostFix; + sOut = _fnInfoMacros( oSettings, sOut ); + + if ( oLang.fnInfoCallback !== null ) + { + sOut = oLang.fnInfoCallback.call( oSettings.oInstance, + oSettings, iStart, iEnd, iMax, iTotal, sOut ); + } + + var n = oSettings.aanFeatures.i; + for ( var i=0, iLen=n.length ; i'; + var i, iLen; + var aLengthMenu = oSettings.aLengthMenu; + + if ( aLengthMenu.length == 2 && typeof aLengthMenu[0] === 'object' && + typeof aLengthMenu[1] === 'object' ) + { + for ( i=0, iLen=aLengthMenu[0].length ; i'+aLengthMenu[1][i]+''; + } + } + else + { + for ( i=0, iLen=aLengthMenu.length ; i'+aLengthMenu[i]+''; + } + } + sStdMenu += ''; + + var nLength = document.createElement( 'div' ); + if ( !oSettings.aanFeatures.l ) + { + nLength.id = oSettings.sTableId+'_length'; + } + nLength.className = oSettings.oClasses.sLength; + nLength.innerHTML = ''; + + /* + * Set the length to the current display length - thanks to Andrea Pavlovic for this fix, + * and Stefan Skopnik for fixing the fix! + */ + $('select option[value="'+oSettings._iDisplayLength+'"]', nLength).attr("selected", true); + + $('select', nLength).bind( 'change.DT', function(e) { + var iVal = $(this).val(); + + /* Update all other length options for the new display */ + var n = oSettings.aanFeatures.l; + for ( i=0, iLen=n.length ; i oSettings.aiDisplay.length || + oSettings._iDisplayLength == -1 ) + { + oSettings._iDisplayEnd = oSettings.aiDisplay.length; + } + else + { + oSettings._iDisplayEnd = oSettings._iDisplayStart + oSettings._iDisplayLength; + } + } + } + + + + /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * Note that most of the paging logic is done in + * DataTable.ext.oPagination + */ + + /** + * Generate the node required for default pagination + * @param {object} oSettings dataTables settings object + * @returns {node} Pagination feature node + * @memberof DataTable#oApi + */ + function _fnFeatureHtmlPaginate ( oSettings ) + { + if ( oSettings.oScroll.bInfinite ) + { + return null; + } + + var nPaginate = document.createElement( 'div' ); + nPaginate.className = oSettings.oClasses.sPaging+oSettings.sPaginationType; + + DataTable.ext.oPagination[ oSettings.sPaginationType ].fnInit( oSettings, nPaginate, + function( oSettings ) { + _fnCalculateEnd( oSettings ); + _fnDraw( oSettings ); + } + ); + + /* Add a draw callback for the pagination on first instance, to update the paging display */ + if ( !oSettings.aanFeatures.p ) + { + oSettings.aoDrawCallback.push( { + "fn": function( oSettings ) { + DataTable.ext.oPagination[ oSettings.sPaginationType ].fnUpdate( oSettings, function( oSettings ) { + _fnCalculateEnd( oSettings ); + _fnDraw( oSettings ); + } ); + }, + "sName": "pagination" + } ); + } + return nPaginate; + } + + + /** + * Alter the display settings to change the page + * @param {object} oSettings dataTables settings object + * @param {string|int} mAction Paging action to take: "first", "previous", "next" or "last" + * or page number to jump to (integer) + * @returns {bool} true page has changed, false - no change (no effect) eg 'first' on page 1 + * @memberof DataTable#oApi + */ + function _fnPageChange ( oSettings, mAction ) + { + var iOldStart = oSettings._iDisplayStart; + + if ( typeof mAction === "number" ) + { + oSettings._iDisplayStart = mAction * oSettings._iDisplayLength; + if ( oSettings._iDisplayStart > oSettings.fnRecordsDisplay() ) + { + oSettings._iDisplayStart = 0; + } + } + else if ( mAction == "first" ) + { + oSettings._iDisplayStart = 0; + } + else if ( mAction == "previous" ) + { + oSettings._iDisplayStart = oSettings._iDisplayLength>=0 ? + oSettings._iDisplayStart - oSettings._iDisplayLength : + 0; + + /* Correct for under-run */ + if ( oSettings._iDisplayStart < 0 ) + { + oSettings._iDisplayStart = 0; + } + } + else if ( mAction == "next" ) + { + if ( oSettings._iDisplayLength >= 0 ) + { + /* Make sure we are not over running the display array */ + if ( oSettings._iDisplayStart + oSettings._iDisplayLength < oSettings.fnRecordsDisplay() ) + { + oSettings._iDisplayStart += oSettings._iDisplayLength; + } + } + else + { + oSettings._iDisplayStart = 0; + } + } + else if ( mAction == "last" ) + { + if ( oSettings._iDisplayLength >= 0 ) + { + var iPages = parseInt( (oSettings.fnRecordsDisplay()-1) / oSettings._iDisplayLength, 10 ) + 1; + oSettings._iDisplayStart = (iPages-1) * oSettings._iDisplayLength; + } + else + { + oSettings._iDisplayStart = 0; + } + } + else + { + _fnLog( oSettings, 0, "Unknown paging action: "+mAction ); + } + $(oSettings.oInstance).trigger('page', oSettings); + + return iOldStart != oSettings._iDisplayStart; + } + + + + /** + * Generate the node required for the processing node + * @param {object} oSettings dataTables settings object + * @returns {node} Processing element + * @memberof DataTable#oApi + */ + function _fnFeatureHtmlProcessing ( oSettings ) + { + var nProcessing = document.createElement( 'div' ); + + if ( !oSettings.aanFeatures.r ) + { + nProcessing.id = oSettings.sTableId+'_processing'; + } + nProcessing.innerHTML = oSettings.oLanguage.sProcessing; + nProcessing.className = oSettings.oClasses.sProcessing; + oSettings.nTable.parentNode.insertBefore( nProcessing, oSettings.nTable ); + + return nProcessing; + } + + + /** + * Display or hide the processing indicator + * @param {object} oSettings dataTables settings object + * @param {bool} bShow Show the processing indicator (true) or not (false) + * @memberof DataTable#oApi + */ + function _fnProcessingDisplay ( oSettings, bShow ) + { + if ( oSettings.oFeatures.bProcessing ) + { + var an = oSettings.aanFeatures.r; + for ( var i=0, iLen=an.length ; i 0 ) + { + nCaption = nCaption[0]; + if ( nCaption._captionSide === "top" ) + { + nScrollHeadTable.appendChild( nCaption ); + } + else if ( nCaption._captionSide === "bottom" && nTfoot ) + { + nScrollFootTable.appendChild( nCaption ); + } + } + + /* + * Sizing + */ + /* When x-scrolling add the width and a scroller to move the header with the body */ + if ( oSettings.oScroll.sX !== "" ) + { + nScrollHead.style.width = _fnStringToCss( oSettings.oScroll.sX ); + nScrollBody.style.width = _fnStringToCss( oSettings.oScroll.sX ); + + if ( nTfoot !== null ) + { + nScrollFoot.style.width = _fnStringToCss( oSettings.oScroll.sX ); + } + + /* When the body is scrolled, then we also want to scroll the headers */ + $(nScrollBody).scroll( function (e) { + nScrollHead.scrollLeft = this.scrollLeft; + + if ( nTfoot !== null ) + { + nScrollFoot.scrollLeft = this.scrollLeft; + } + } ); + } + + /* When yscrolling, add the height */ + if ( oSettings.oScroll.sY !== "" ) + { + nScrollBody.style.height = _fnStringToCss( oSettings.oScroll.sY ); + } + + /* Redraw - align columns across the tables */ + oSettings.aoDrawCallback.push( { + "fn": _fnScrollDraw, + "sName": "scrolling" + } ); + + /* Infinite scrolling event handlers */ + if ( oSettings.oScroll.bInfinite ) + { + $(nScrollBody).scroll( function() { + /* Use a blocker to stop scrolling from loading more data while other data is still loading */ + if ( !oSettings.bDrawing && $(this).scrollTop() !== 0 ) + { + /* Check if we should load the next data set */ + if ( $(this).scrollTop() + $(this).height() > + $(oSettings.nTable).height() - oSettings.oScroll.iLoadGap ) + { + /* Only do the redraw if we have to - we might be at the end of the data */ + if ( oSettings.fnDisplayEnd() < oSettings.fnRecordsDisplay() ) + { + _fnPageChange( oSettings, 'next' ); + _fnCalculateEnd( oSettings ); + _fnDraw( oSettings ); + } + } + } + } ); + } + + oSettings.nScrollHead = nScrollHead; + oSettings.nScrollFoot = nScrollFoot; + + return nScroller; + } + + + /** + * Update the various tables for resizing. It's a bit of a pig this function, but + * basically the idea to: + * 1. Re-create the table inside the scrolling div + * 2. Take live measurements from the DOM + * 3. Apply the measurements + * 4. Clean up + * @param {object} o dataTables settings object + * @returns {node} Node to add to the DOM + * @memberof DataTable#oApi + */ + function _fnScrollDraw ( o ) + { + var + nScrollHeadInner = o.nScrollHead.getElementsByTagName('div')[0], + nScrollHeadTable = nScrollHeadInner.getElementsByTagName('table')[0], + nScrollBody = o.nTable.parentNode, + i, iLen, j, jLen, anHeadToSize, anHeadSizers, anFootSizers, anFootToSize, oStyle, iVis, + nTheadSize, nTfootSize, + iWidth, aApplied=[], iSanityWidth, + nScrollFootInner = (o.nTFoot !== null) ? o.nScrollFoot.getElementsByTagName('div')[0] : null, + nScrollFootTable = (o.nTFoot !== null) ? nScrollFootInner.getElementsByTagName('table')[0] : null, + ie67 = o.oBrowser.bScrollOversize; + + /* + * 1. Re-create the table inside the scrolling div + */ + + /* Remove the old minimised thead and tfoot elements in the inner table */ + $(o.nTable).children('thead, tfoot').remove(); + + /* Clone the current header and footer elements and then place it into the inner table */ + nTheadSize = $(o.nTHead).clone()[0]; + o.nTable.insertBefore( nTheadSize, o.nTable.childNodes[0] ); + + if ( o.nTFoot !== null ) + { + nTfootSize = $(o.nTFoot).clone()[0]; + o.nTable.insertBefore( nTfootSize, o.nTable.childNodes[1] ); + } + + /* + * 2. Take live measurements from the DOM - do not alter the DOM itself! + */ + + /* Remove old sizing and apply the calculated column widths + * Get the unique column headers in the newly created (cloned) header. We want to apply the + * calculated sizes to this header + */ + if ( o.oScroll.sX === "" ) + { + nScrollBody.style.width = '100%'; + nScrollHeadInner.parentNode.style.width = '100%'; + } + + var nThs = _fnGetUniqueThs( o, nTheadSize ); + for ( i=0, iLen=nThs.length ; i nScrollBody.offsetHeight || + $(nScrollBody).css('overflow-y') == "scroll") ) + { + o.nTable.style.width = _fnStringToCss( $(o.nTable).outerWidth() - o.oScroll.iBarWidth); + } + } + else + { + if ( o.oScroll.sXInner !== "" ) + { + /* x scroll inner has been given - use it */ + o.nTable.style.width = _fnStringToCss(o.oScroll.sXInner); + } + else if ( iSanityWidth == $(nScrollBody).width() && + $(nScrollBody).height() < $(o.nTable).height() ) + { + /* There is y-scrolling - try to take account of the y scroll bar */ + o.nTable.style.width = _fnStringToCss( iSanityWidth-o.oScroll.iBarWidth ); + if ( $(o.nTable).outerWidth() > iSanityWidth-o.oScroll.iBarWidth ) + { + /* Not possible to take account of it */ + o.nTable.style.width = _fnStringToCss( iSanityWidth ); + } + } + else + { + /* All else fails */ + o.nTable.style.width = _fnStringToCss( iSanityWidth ); + } + } + + /* Recalculate the sanity width - now that we've applied the required width, before it was + * a temporary variable. This is required because the column width calculation is done + * before this table DOM is created. + */ + iSanityWidth = $(o.nTable).outerWidth(); + + /* We want the hidden header to have zero height, so remove padding and borders. Then + * set the width based on the real headers + */ + anHeadToSize = o.nTHead.getElementsByTagName('tr'); + anHeadSizers = nTheadSize.getElementsByTagName('tr'); + + _fnApplyToChildren( function(nSizer, nToSize) { + oStyle = nSizer.style; + oStyle.paddingTop = "0"; + oStyle.paddingBottom = "0"; + oStyle.borderTopWidth = "0"; + oStyle.borderBottomWidth = "0"; + oStyle.height = 0; + + iWidth = $(nSizer).width(); + nToSize.style.width = _fnStringToCss( iWidth ); + aApplied.push( iWidth ); + }, anHeadSizers, anHeadToSize ); + $(anHeadSizers).height(0); + + if ( o.nTFoot !== null ) + { + /* Clone the current footer and then place it into the body table as a "hidden header" */ + anFootSizers = nTfootSize.getElementsByTagName('tr'); + anFootToSize = o.nTFoot.getElementsByTagName('tr'); + + _fnApplyToChildren( function(nSizer, nToSize) { + oStyle = nSizer.style; + oStyle.paddingTop = "0"; + oStyle.paddingBottom = "0"; + oStyle.borderTopWidth = "0"; + oStyle.borderBottomWidth = "0"; + oStyle.height = 0; + + iWidth = $(nSizer).width(); + nToSize.style.width = _fnStringToCss( iWidth ); + aApplied.push( iWidth ); + }, anFootSizers, anFootToSize ); + $(anFootSizers).height(0); + } + + /* + * 3. Apply the measurements + */ + + /* "Hide" the header and footer that we used for the sizing. We want to also fix their width + * to what they currently are + */ + _fnApplyToChildren( function(nSizer) { + nSizer.innerHTML = ""; + nSizer.style.width = _fnStringToCss( aApplied.shift() ); + }, anHeadSizers ); + + if ( o.nTFoot !== null ) + { + _fnApplyToChildren( function(nSizer) { + nSizer.innerHTML = ""; + nSizer.style.width = _fnStringToCss( aApplied.shift() ); + }, anFootSizers ); + } + + /* Sanity check that the table is of a sensible width. If not then we are going to get + * misalignment - try to prevent this by not allowing the table to shrink below its min width + */ + if ( $(o.nTable).outerWidth() < iSanityWidth ) + { + /* The min width depends upon if we have a vertical scrollbar visible or not */ + var iCorrection = ((nScrollBody.scrollHeight > nScrollBody.offsetHeight || + $(nScrollBody).css('overflow-y') == "scroll")) ? + iSanityWidth+o.oScroll.iBarWidth : iSanityWidth; + + /* IE6/7 are a law unto themselves... */ + if ( ie67 && (nScrollBody.scrollHeight > + nScrollBody.offsetHeight || $(nScrollBody).css('overflow-y') == "scroll") ) + { + o.nTable.style.width = _fnStringToCss( iCorrection-o.oScroll.iBarWidth ); + } + + /* Apply the calculated minimum width to the table wrappers */ + nScrollBody.style.width = _fnStringToCss( iCorrection ); + nScrollHeadInner.parentNode.style.width = _fnStringToCss( iCorrection ); + + if ( o.nTFoot !== null ) + { + nScrollFootInner.parentNode.style.width = _fnStringToCss( iCorrection ); + } + + /* And give the user a warning that we've stopped the table getting too small */ + if ( o.oScroll.sX === "" ) + { + _fnLog( o, 1, "The table cannot fit into the current element which will cause column"+ + " misalignment. The table has been drawn at its minimum possible width." ); + } + else if ( o.oScroll.sXInner !== "" ) + { + _fnLog( o, 1, "The table cannot fit into the current element which will cause column"+ + " misalignment. Increase the sScrollXInner value or remove it to allow automatic"+ + " calculation" ); + } + } + else + { + nScrollBody.style.width = _fnStringToCss( '100%' ); + nScrollHeadInner.parentNode.style.width = _fnStringToCss( '100%' ); + + if ( o.nTFoot !== null ) + { + nScrollFootInner.parentNode.style.width = _fnStringToCss( '100%' ); + } + } + + + /* + * 4. Clean up + */ + if ( o.oScroll.sY === "" ) + { + /* IE7< puts a vertical scrollbar in place (when it shouldn't be) due to subtracting + * the scrollbar height from the visible display, rather than adding it on. We need to + * set the height in order to sort this. Don't want to do it in any other browsers. + */ + if ( ie67 ) + { + nScrollBody.style.height = _fnStringToCss( o.nTable.offsetHeight+o.oScroll.iBarWidth ); + } + } + + if ( o.oScroll.sY !== "" && o.oScroll.bCollapse ) + { + nScrollBody.style.height = _fnStringToCss( o.oScroll.sY ); + + var iExtra = (o.oScroll.sX !== "" && o.nTable.offsetWidth > nScrollBody.offsetWidth) ? + o.oScroll.iBarWidth : 0; + if ( o.nTable.offsetHeight < nScrollBody.offsetHeight ) + { + nScrollBody.style.height = _fnStringToCss( o.nTable.offsetHeight+iExtra ); + } + } + + /* Finally set the width's of the header and footer tables */ + var iOuterWidth = $(o.nTable).outerWidth(); + nScrollHeadTable.style.width = _fnStringToCss( iOuterWidth ); + nScrollHeadInner.style.width = _fnStringToCss( iOuterWidth ); + + // Figure out if there are scrollbar present - if so then we need a the header and footer to + // provide a bit more space to allow "overflow" scrolling (i.e. past the scrollbar) + var bScrolling = $(o.nTable).height() > nScrollBody.clientHeight || $(nScrollBody).css('overflow-y') == "scroll"; + nScrollHeadInner.style.paddingRight = bScrolling ? o.oScroll.iBarWidth+"px" : "0px"; + + if ( o.nTFoot !== null ) + { + nScrollFootTable.style.width = _fnStringToCss( iOuterWidth ); + nScrollFootInner.style.width = _fnStringToCss( iOuterWidth ); + nScrollFootInner.style.paddingRight = bScrolling ? o.oScroll.iBarWidth+"px" : "0px"; + } + + /* Adjust the position of the header in case we loose the y-scrollbar */ + $(nScrollBody).scroll(); + + /* If sorting or filtering has occurred, jump the scrolling back to the top */ + if ( o.bSorted || o.bFiltered ) + { + nScrollBody.scrollTop = 0; + } + } + + + /** + * Apply a given function to the display child nodes of an element array (typically + * TD children of TR rows + * @param {function} fn Method to apply to the objects + * @param array {nodes} an1 List of elements to look through for display children + * @param array {nodes} an2 Another list (identical structure to the first) - optional + * @memberof DataTable#oApi + */ + function _fnApplyToChildren( fn, an1, an2 ) + { + for ( var i=0, iLen=an1.length ; itd', nCalcTmp); + } + + /* Apply custom sizing to the cloned header */ + var nThs = _fnGetUniqueThs( oSettings, nTheadClone ); + iCorrector = 0; + for ( i=0 ; i 0 ) + { + oSettings.aoColumns[i].sWidth = _fnStringToCss( iWidth ); + } + iCorrector++; + } + } + + var cssWidth = $(nCalcTmp).css('width'); + oSettings.nTable.style.width = (cssWidth.indexOf('%') !== -1) ? + cssWidth : _fnStringToCss( $(nCalcTmp).outerWidth() ); + nCalcTmp.parentNode.removeChild( nCalcTmp ); + } + + if ( widthAttr ) + { + oSettings.nTable.style.width = _fnStringToCss( widthAttr ); + } + } + + + /** + * Adjust a table's width to take account of scrolling + * @param {object} oSettings dataTables settings object + * @param {node} n table node + * @memberof DataTable#oApi + */ + function _fnScrollingWidthAdjust ( oSettings, n ) + { + if ( oSettings.oScroll.sX === "" && oSettings.oScroll.sY !== "" ) + { + /* When y-scrolling only, we want to remove the width of the scroll bar so the table + * + scroll bar will fit into the area avaialble. + */ + var iOrigWidth = $(n).width(); + n.style.width = _fnStringToCss( $(n).outerWidth()-oSettings.oScroll.iBarWidth ); + } + else if ( oSettings.oScroll.sX !== "" ) + { + /* When x-scrolling both ways, fix the table at it's current size, without adjusting */ + n.style.width = _fnStringToCss( $(n).outerWidth() ); + } + } + + + /** + * Get the widest node + * @param {object} oSettings dataTables settings object + * @param {int} iCol column of interest + * @returns {string} max string length for each column + * @memberof DataTable#oApi + */ + function _fnGetWidestNode( oSettings, iCol ) + { + var iMaxIndex = _fnGetMaxLenString( oSettings, iCol ); + if ( iMaxIndex < 0 ) + { + return null; + } + + if ( oSettings.aoData[iMaxIndex].nTr === null ) + { + var n = document.createElement('td'); + n.innerHTML = _fnGetCellData( oSettings, iMaxIndex, iCol, '' ); + return n; + } + return _fnGetTdNodes(oSettings, iMaxIndex)[iCol]; + } + + + /** + * Get the maximum strlen for each data column + * @param {object} oSettings dataTables settings object + * @param {int} iCol column of interest + * @returns {string} max string length for each column + * @memberof DataTable#oApi + */ + function _fnGetMaxLenString( oSettings, iCol ) + { + var iMax = -1; + var iMaxIndex = -1; + + for ( var i=0 ; i/g, "" ); + if ( s.length > iMax ) + { + iMax = s.length; + iMaxIndex = i; + } + } + + return iMaxIndex; + } + + + /** + * Append a CSS unit (only if required) to a string + * @param {array} aArray1 first array + * @param {array} aArray2 second array + * @returns {int} 0 if match, 1 if length is different, 2 if no match + * @memberof DataTable#oApi + */ + function _fnStringToCss( s ) + { + if ( s === null ) + { + return "0px"; + } + + if ( typeof s == 'number' ) + { + if ( s < 0 ) + { + return "0px"; + } + return s+"px"; + } + + /* Check if the last character is not 0-9 */ + var c = s.charCodeAt( s.length-1 ); + if (c < 0x30 || c > 0x39) + { + return s; + } + return s+"px"; + } + + + /** + * Get the width of a scroll bar in this browser being used + * @returns {int} width in pixels + * @memberof DataTable#oApi + */ + function _fnScrollBarWidth () + { + var inner = document.createElement('p'); + var style = inner.style; + style.width = "100%"; + style.height = "200px"; + style.padding = "0px"; + + var outer = document.createElement('div'); + style = outer.style; + style.position = "absolute"; + style.top = "0px"; + style.left = "0px"; + style.visibility = "hidden"; + style.width = "200px"; + style.height = "150px"; + style.padding = "0px"; + style.overflow = "hidden"; + outer.appendChild(inner); + + document.body.appendChild(outer); + var w1 = inner.offsetWidth; + outer.style.overflow = 'scroll'; + var w2 = inner.offsetWidth; + if ( w1 == w2 ) + { + w2 = outer.clientWidth; + } + + document.body.removeChild(outer); + return (w1 - w2); + } + + + + /** + * Change the order of the table + * @param {object} oSettings dataTables settings object + * @param {bool} bApplyClasses optional - should we apply classes or not + * @memberof DataTable#oApi + */ + function _fnSort ( oSettings, bApplyClasses ) + { + var + i, iLen, j, jLen, k, kLen, + sDataType, nTh, + aaSort = [], + aiOrig = [], + oSort = DataTable.ext.oSort, + aoData = oSettings.aoData, + aoColumns = oSettings.aoColumns, + oAria = oSettings.oLanguage.oAria; + + /* No sorting required if server-side or no sorting array */ + if ( !oSettings.oFeatures.bServerSide && + (oSettings.aaSorting.length !== 0 || oSettings.aaSortingFixed !== null) ) + { + aaSort = ( oSettings.aaSortingFixed !== null ) ? + oSettings.aaSortingFixed.concat( oSettings.aaSorting ) : + oSettings.aaSorting.slice(); + + /* If there is a sorting data type, and a function belonging to it, then we need to + * get the data from the developer's function and apply it for this column + */ + for ( i=0 ; i/g, "" ); + nTh = aoColumns[i].nTh; + nTh.removeAttribute('aria-sort'); + nTh.removeAttribute('aria-label'); + + /* In ARIA only the first sorting column can be marked as sorting - no multi-sort option */ + if ( aoColumns[i].bSortable ) + { + if ( aaSort.length > 0 && aaSort[0][0] == i ) + { + nTh.setAttribute('aria-sort', aaSort[0][1]=="asc" ? "ascending" : "descending" ); + + var nextSort = (aoColumns[i].asSorting[ aaSort[0][2]+1 ]) ? + aoColumns[i].asSorting[ aaSort[0][2]+1 ] : aoColumns[i].asSorting[0]; + nTh.setAttribute('aria-label', sTitle+ + (nextSort=="asc" ? oAria.sSortAscending : oAria.sSortDescending) ); + } + else + { + nTh.setAttribute('aria-label', sTitle+ + (aoColumns[i].asSorting[0]=="asc" ? oAria.sSortAscending : oAria.sSortDescending) ); + } + } + else + { + nTh.setAttribute('aria-label', sTitle); + } + } + + /* Tell the draw function that we have sorted the data */ + oSettings.bSorted = true; + $(oSettings.oInstance).trigger('sort', oSettings); + + /* Copy the master data into the draw array and re-draw */ + if ( oSettings.oFeatures.bFilter ) + { + /* _fnFilter() will redraw the table for us */ + _fnFilterComplete( oSettings, oSettings.oPreviousSearch, 1 ); + } + else + { + oSettings.aiDisplay = oSettings.aiDisplayMaster.slice(); + oSettings._iDisplayStart = 0; /* reset display back to page 0 */ + _fnCalculateEnd( oSettings ); + _fnDraw( oSettings ); + } + } + + + /** + * Attach a sort handler (click) to a node + * @param {object} oSettings dataTables settings object + * @param {node} nNode node to attach the handler to + * @param {int} iDataIndex column sorting index + * @param {function} [fnCallback] callback function + * @memberof DataTable#oApi + */ + function _fnSortAttachListener ( oSettings, nNode, iDataIndex, fnCallback ) + { + _fnBindAction( nNode, {}, function (e) { + /* If the column is not sortable - don't to anything */ + if ( oSettings.aoColumns[iDataIndex].bSortable === false ) + { + return; + } + + /* + * This is a little bit odd I admit... I declare a temporary function inside the scope of + * _fnBuildHead and the click handler in order that the code presented here can be used + * twice - once for when bProcessing is enabled, and another time for when it is + * disabled, as we need to perform slightly different actions. + * Basically the issue here is that the Javascript engine in modern browsers don't + * appear to allow the rendering engine to update the display while it is still executing + * it's thread (well - it does but only after long intervals). This means that the + * 'processing' display doesn't appear for a table sort. To break the js thread up a bit + * I force an execution break by using setTimeout - but this breaks the expected + * thread continuation for the end-developer's point of view (their code would execute + * too early), so we only do it when we absolutely have to. + */ + var fnInnerSorting = function () { + var iColumn, iNextSort; + + /* If the shift key is pressed then we are multiple column sorting */ + if ( e.shiftKey ) + { + /* Are we already doing some kind of sort on this column? */ + var bFound = false; + for ( var i=0 ; i= iColumns ) + { + for ( i=0 ; i 4096 ) /* Magic 10 for padding */ + { + var aCookies =document.cookie.split(';'); + for ( var i=0, iLen=aCookies.length ; i=0 ; i-- ) + { + aRet.push( aoStore[i].fn.apply( oSettings.oInstance, aArgs ) ); + } + + if ( sTrigger !== null ) + { + $(oSettings.oInstance).trigger(sTrigger, aArgs); + } + + return aRet; + } + + + /** + * JSON stringify. If JSON.stringify it provided by the browser, json2.js or any other + * library, then we use that as it is fast, safe and accurate. If the function isn't + * available then we need to built it ourselves - the inspiration for this function comes + * from Craig Buckler ( http://www.sitepoint.com/javascript-json-serialization/ ). It is + * not perfect and absolutely should not be used as a replacement to json2.js - but it does + * do what we need, without requiring a dependency for DataTables. + * @param {object} o JSON object to be converted + * @returns {string} JSON string + * @memberof DataTable#oApi + */ + var _fnJsonString = (window.JSON) ? JSON.stringify : function( o ) + { + /* Not an object or array */ + var sType = typeof o; + if (sType !== "object" || o === null) + { + // simple data type + if (sType === "string") + { + o = '"'+o+'"'; + } + return o+""; + } + + /* If object or array, need to recurse over it */ + var + sProp, mValue, + json = [], + bArr = $.isArray(o); + + for (sProp in o) + { + mValue = o[sProp]; + sType = typeof mValue; + + if (sType === "string") + { + mValue = '"'+mValue+'"'; + } + else if (sType === "object" && mValue !== null) + { + mValue = _fnJsonString(mValue); + } + + json.push((bArr ? "" : '"'+sProp+'":') + mValue); + } + + return (bArr ? "[" : "{") + json + (bArr ? "]" : "}"); + }; + + + /** + * From some browsers (specifically IE6/7) we need special handling to work around browser + * bugs - this function is used to detect when these workarounds are needed. + * @param {object} oSettings dataTables settings object + * @memberof DataTable#oApi + */ + function _fnBrowserDetect( oSettings ) + { + /* IE6/7 will oversize a width 100% element inside a scrolling element, to include the + * width of the scrollbar, while other browsers ensure the inner element is contained + * without forcing scrolling + */ + var n = $( + '
    '+ + '
    '+ + '
    '+ + '
    '+ + '
    ')[0]; + + document.body.appendChild( n ); + oSettings.oBrowser.bScrollOversize = $('#DT_BrowserTest', n)[0].offsetWidth === 100 ? true : false; + document.body.removeChild( n ); + } + + + + + /** + * Perform a jQuery selector action on the table's TR elements (from the tbody) and + * return the resulting jQuery object. + * @param {string|node|jQuery} sSelector jQuery selector or node collection to act on + * @param {object} [oOpts] Optional parameters for modifying the rows to be included + * @param {string} [oOpts.filter=none] Select TR elements that meet the current filter + * criterion ("applied") or all TR elements (i.e. no filter). + * @param {string} [oOpts.order=current] Order of the TR elements in the processed array. + * Can be either 'current', whereby the current sorting of the table is used, or + * 'original' whereby the original order the data was read into the table is used. + * @param {string} [oOpts.page=all] Limit the selection to the currently displayed page + * ("current") or not ("all"). If 'current' is given, then order is assumed to be + * 'current' and filter is 'applied', regardless of what they might be given as. + * @returns {object} jQuery object, filtered by the given selector. + * @dtopt API + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Highlight every second row + * oTable.$('tr:odd').css('backgroundColor', 'blue'); + * } ); + * + * @example + * $(document).ready(function() { + * var oTable = $('#example').dataTable(); + * + * // Filter to rows with 'Webkit' in them, add a background colour and then + * // remove the filter, thus highlighting the 'Webkit' rows only. + * oTable.fnFilter('Webkit'); + * oTable.$('tr', {"filter": "applied"}).css('backgroundColor', 'blue'); + * oTable.fnFilter(''); + * } ); + */ + this.$ = function ( sSelector, oOpts ) + { + var i, iLen, a = [], tr; + var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] ); + var aoData = oSettings.aoData; + var aiDisplay = oSettings.aiDisplay; + var aiDisplayMaster = oSettings.aiDisplayMaster; + + if ( !oOpts ) + { + oOpts = {}; + } + + oOpts = $.extend( {}, { + "filter": "none", // applied + "order": "current", // "original" + "page": "all" // current + }, oOpts ); + + // Current page implies that order=current and fitler=applied, since it is fairly + // senseless otherwise + if ( oOpts.page == 'current' ) + { + for ( i=oSettings._iDisplayStart, iLen=oSettings.fnDisplayEnd() ; i + *
  • 1D array of data - add a single row with the data provided
  • + *
  • 2D array of arrays - add multiple rows in a single call
  • + *
  • object - data object when using mData
  • + *
  • array of objects - multiple data objects when using mData
  • + *
+ * @param {bool} [bRedraw=true] redraw the table or not + * @returns {array} An array of integers, representing the list of indexes in + * aoData ({@link DataTable.models.oSettings}) that have been added to + * the table. + * @dtopt API + * + * @example + * // Global var for counter + * var giCount = 2; + * + * $(document).ready(function() { + * $('#example').dataTable(); + * } ); + * + * function fnClickAddRow() { + * $('#example').dataTable().fnAddData( [ + * giCount+".1", + * giCount+".2", + * giCount+".3", + * giCount+".4" ] + * ); + * + * giCount++; + * } + */ + this.fnAddData = function( mData, bRedraw ) + { + if ( mData.length === 0 ) + { + return []; + } + + var aiReturn = []; + var iTest; + + /* Find settings from table node */ + var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] ); + + /* Check if we want to add multiple rows or not */ + if ( typeof mData[0] === "object" && mData[0] !== null ) + { + for ( var i=0 ; i= oSettings.fnRecordsDisplay() ) + { + oSettings._iDisplayStart -= oSettings._iDisplayLength; + if ( oSettings._iDisplayStart < 0 ) + { + oSettings._iDisplayStart = 0; + } + } + + if ( bRedraw === undefined || bRedraw ) + { + _fnCalculateEnd( oSettings ); + _fnDraw( oSettings ); + } + + return oData; + }; + + + /** + * Restore the table to it's original state in the DOM by removing all of DataTables + * enhancements, alterations to the DOM structure of the table and event listeners. + * @param {boolean} [bRemove=false] Completely remove the table from the DOM + * @dtopt API + * + * @example + * $(document).ready(function() { + * // This example is fairly pointless in reality, but shows how fnDestroy can be used + * var oTable = $('#example').dataTable(); + * oTable.fnDestroy(); + * } ); + */ + this.fnDestroy = function ( bRemove ) + { + var oSettings = _fnSettingsFromNode( this[DataTable.ext.iApiIndex] ); + var nOrig = oSettings.nTableWrapper.parentNode; + var nBody = oSettings.nTBody; + var i, iLen; + + bRemove = (bRemove===undefined) ? false : true; + + /* Flag to note that the table is currently being destroyed - no action should be taken */ + oSettings.bDestroying = true; + + /* Fire off the destroy callbacks for plug-ins etc */ + _fnCallbackFire( oSettings, "aoDestroyCallback", "destroy", [oSettings] ); + + /* Restore hidden columns */ + for ( i=0, iLen=oSettings.aoColumns.length ; itr>td.'+oSettings.oClasses.sRowEmpty, oSettings.nTable).parent().remove(); + + /* When scrolling we had to break the table up - restore it */ + if ( oSettings.nTable != oSettings.nTHead.parentNode ) + { + $(oSettings.nTable).children('thead').remove(); + oSettings.nTable.appendChild( oSettings.nTHead ); + } + + if ( oSettings.nTFoot && oSettings.nTable != oSettings.nTFoot.parentNode ) + { + $(oSettings.nTable).children('tfoot').remove(); + oSettings.nTable.appendChild( oSettings.nTFoot ); + } + + /* Remove the DataTables generated nodes, events and classes */ + oSettings.nTable.parentNode.removeChild( oSettings.nTable ); + $(oSettings.nTableWrapper).remove(); + + oSettings.aaSorting = []; + oSettings.aaSortingFixed = []; + _fnSortingClasses( oSettings ); + + $(_fnGetTrNodes( oSettings )).removeClass( oSettings.asStripeClasses.join(' ') ); + + $('th, td', oSettings.nTHead).removeClass( [ + oSettings.oClasses.sSortable, + oSettings.oClasses.sSortableAsc, + oSettings.oClasses.sSortableDesc, + oSettings.oClasses.sSortableNone ].join(' ') + ); + if ( oSettings.bJUI ) + { + $('th span.'+oSettings.oClasses.sSortIcon + + ', td span.'+oSettings.oClasses.sSortIcon, oSettings.nTHead).remove(); + + $('th, td', oSettings.nTHead).each( function () { + var jqWrapper = $('div.'+oSettings.oClasses.sSortJUIWrapper, this); + var kids = jqWrapper.contents(); + $(this).append( kids ); + jqWrapper.remove(); + } ); + } + + /* Add the TR elements back into the table in their original order */ + if ( !bRemove && oSettings.nTableReinsertBefore ) + { + nOrig.insertBefore( oSettings.nTable, oSettings.nTableReinsertBefore ); + } + else if ( !bRemove ) + { + nOrig.appendChild( oSettings.nTable ); + } + + for ( i=0, iLen=oSettings.aoData.length ; i
\r\n"); + + } + } +} +#pragma warning restore 1591 diff --git a/Disco.Web/Views/Device/_DeviceActions.cshtml b/Disco.Web/Views/Device/_DeviceActions.cshtml new file mode 100644 index 00000000..2839d063 --- /dev/null +++ b/Disco.Web/Views/Device/_DeviceActions.cshtml @@ -0,0 +1,124 @@ +@model Disco.Models.Repository.Device +
+ @if (Model.CanDecommission()) + { + @Html.ActionLinkButton("Decommission", MVC.API.Device.Decommission(Model.SerialNumber, true), "buttonDeviceDecommission") +
+

+ + Are you sure?

+
+ + } + @if (Model.CanRecommission()) + { + @Html.ActionLinkButton("Recommission", MVC.API.Device.Recommission(Model.SerialNumber, true), "buttonDeviceRecommission") +
+

+ + Are you sure?

+
+ + } + @if (Model.CanDelete()) + { + @Html.ActionLinkButton("Delete Device", MVC.API.Device.Delete(Model.SerialNumber, true), "buttonDeviceDelete") +
+

+ + This item will be permanently deleted and cannot be recovered.
+ Jobs linked to this Device (but not to a User) will be deleted also.
+ Are you sure?

+
+ + } + @if (Model.CanCreateJob()) + { + Html.BundleDeferred("~/ClientScripts/Modules/Disco-CreateJob"); + @Html.ActionLinkButton("Create Job", MVC.Job.Create(Model.SerialNumber, Model.AssignedUserId), "buttonCreateJob") + } +
\ No newline at end of file diff --git a/Disco.Web/Views/Device/_DeviceActions.generated.cs b/Disco.Web/Views/Device/_DeviceActions.generated.cs new file mode 100644 index 00000000..fddf4f8e --- /dev/null +++ b/Disco.Web/Views/Device/_DeviceActions.generated.cs @@ -0,0 +1,326 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.17929 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Disco.Web.Views.Device +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using System.Web; + using System.Web.Helpers; + using System.Web.Mvc; + using System.Web.Mvc.Ajax; + using System.Web.Mvc.Html; + using System.Web.Routing; + using System.Web.Security; + using System.Web.UI; + using System.Web.WebPages; + using Disco.BI.Extensions; + using Disco.Models.Repository; + using Disco.Web; + using Disco.Web.Extensions; + + [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorGenerator", "1.5.0.0")] + [System.Web.WebPages.PageVirtualPathAttribute("~/Views/Device/_DeviceActions.cshtml")] + public class DeviceActions : System.Web.Mvc.WebViewPage + { + public DeviceActions() + { + } + public override void Execute() + { +WriteLiteral("\r\n"); + + + #line 3 "..\..\Views\Device\_DeviceActions.cshtml" + + + #line default + #line hidden + + #line 3 "..\..\Views\Device\_DeviceActions.cshtml" + if (Model.CanDecommission()) + { + + + #line default + #line hidden + + #line 5 "..\..\Views\Device\_DeviceActions.cshtml" + Write(Html.ActionLinkButton("Decommission", MVC.API.Device.Decommission(Model.SerialNumber, true), "buttonDeviceDecommission")); + + + #line default + #line hidden + + #line 5 "..\..\Views\Device\_DeviceActions.cshtml" + + + + #line default + #line hidden +WriteLiteral(" \r\n

\r\n \r\n Are you sure?

\r\n
\r\n"); + +WriteLiteral(" + $(function () { + var button = $('#buttonDeviceDecommission'); + var buttonDialog = $('#dialogConfirmDecommission'); + var buttonLink = button.attr('href'); + button.attr('href', '#'); + button.click(function () { + buttonDialog.dialog('open'); + return false; + }); + buttonDialog.dialog({ + resizable: false, + height: 140, + modal: true, + autoOpen: false, + buttons: { + ""Decommission"": function () { + var $this = $(this); + $this.dialog(""disable""); + $this.dialog(""option"", ""buttons"", null); + window.location.href = buttonLink; + }, + Cancel: function () { + $(this).dialog(""close""); + } + } + }); + }); + +"); + + + #line 40 "..\..\Views\Device\_DeviceActions.cshtml" + } + + + #line default + #line hidden +WriteLiteral(" "); + + + #line 41 "..\..\Views\Device\_DeviceActions.cshtml" + if (Model.CanRecommission()) + { + + + #line default + #line hidden + + #line 43 "..\..\Views\Device\_DeviceActions.cshtml" + Write(Html.ActionLinkButton("Recommission", MVC.API.Device.Recommission(Model.SerialNumber, true), "buttonDeviceRecommission")); + + + #line default + #line hidden + + #line 43 "..\..\Views\Device\_DeviceActions.cshtml" + + + + #line default + #line hidden +WriteLiteral(" \r\n

\r\n \r\n Are you sure?

\r\n
\r\n"); + +WriteLiteral(" + $(function () { + var button = $('#buttonDeviceRecommission'); + var buttonDialog = $('#dialogConfirmRecommission'); + var buttonLink = button.attr('href'); + button.attr('href', '#'); + button.click(function () { + buttonDialog.dialog('open'); + return false; + }); + buttonDialog.dialog({ + resizable: false, + height: 140, + modal: true, + autoOpen: false, + buttons: { + ""Recommission"": function () { + var $this = $(this); + $this.dialog(""disable""); + $this.dialog(""option"", ""buttons"", null); + window.location.href = buttonLink; + }, + Cancel: function () { + $(this).dialog(""close""); + } + } + }); + }); + +"); + + + #line 78 "..\..\Views\Device\_DeviceActions.cshtml" + } + + + #line default + #line hidden +WriteLiteral(" "); + + + #line 79 "..\..\Views\Device\_DeviceActions.cshtml" + if (Model.CanDelete()) + { + + + #line default + #line hidden + + #line 81 "..\..\Views\Device\_DeviceActions.cshtml" + Write(Html.ActionLinkButton("Delete Device", MVC.API.Device.Delete(Model.SerialNumber, true), "buttonDeviceDelete")); + + + #line default + #line hidden + + #line 81 "..\..\Views\Device\_DeviceActions.cshtml" + + + + #line default + #line hidden +WriteLiteral(" \r\n

\r\n \r\n This item will be permanently deleted and cannot be rec" + +"overed.
\r\n Jobs linked to this Device (but not to a User) wi" + +"ll be deleted also.
\r\n Are you sure?

\r\n
\r\n"); + +WriteLiteral(" + $(function () { + var button = $('#buttonDeviceDelete'); + var buttonDialog = $('#dialogConfirmDelete'); + var buttonLink = button.attr('href'); + button.attr('href', '#'); + button.click(function () { + buttonDialog.dialog('open'); + return false; + }); + buttonDialog.dialog({ + resizable: false, + height: 200, + modal: true, + autoOpen: false, + buttons: { + ""Delete"": function () { + var $this = $(this); + $this.dialog(""disable""); + $this.dialog(""option"", ""buttons"", null); + window.location.href = buttonLink; + }, + Cancel: function () { + $(this).dialog(""close""); + } + } + }); + }); + +"); + + + #line 118 "..\..\Views\Device\_DeviceActions.cshtml" + } + + + #line default + #line hidden +WriteLiteral(" "); + + + #line 119 "..\..\Views\Device\_DeviceActions.cshtml" + if (Model.CanCreateJob()) + { + Html.BundleDeferred("~/ClientScripts/Modules/Disco-CreateJob"); + + + #line default + #line hidden + + #line 122 "..\..\Views\Device\_DeviceActions.cshtml" + Write(Html.ActionLinkButton("Create Job", MVC.Job.Create(Model.SerialNumber, Model.AssignedUserId), "buttonCreateJob")); + + + #line default + #line hidden + + #line 122 "..\..\Views\Device\_DeviceActions.cshtml" + + } + + + #line default + #line hidden +WriteLiteral("
"); + + } + } +} +#pragma warning restore 1591 diff --git a/Disco.Web/Views/Device/_DeviceTable.cshtml b/Disco.Web/Views/Device/_DeviceTable.cshtml new file mode 100644 index 00000000..d4a96b04 --- /dev/null +++ b/Disco.Web/Views/Device/_DeviceTable.cshtml @@ -0,0 +1,89 @@ +@model IEnumerable +
+ @if (Model != null && Model.Count() > 0) + { + + + + + + + + + + + + + + @foreach (var item in Model) + { + + + + + + + + + + } + +
+ Serial# + + Asset# + + Name + + Model + + Profile + + Assigned User + + Jobs +
+ @Html.ActionLink(item.SerialNumber, MVC.Device.Show(item.SerialNumber)) + + @item.AssetNumber + @if (item.DecommissionedDate.HasValue) + { (Decommissioned + @CommonHelpers.FriendlyDate(item.DecommissionedDate.Value)) } + + @if (string.IsNullOrWhiteSpace(item.ComputerName)) + { + Unknown + } + else + { + @item.ComputerName + } + + @if (item.DeviceModelDescription != null) + { + @item.DeviceModelDescription + } + else + { + Unknown + } + + @item.DeviceProfileDescription + + @if (string.IsNullOrEmpty(item.AssignedUserId)) + { + N/A + } + else + { + @item.AssignedUserDescription + } + + @item.JobCount +
+ } + else + { + No Devices Found + } +
diff --git a/Disco.Web/Views/Device/_DeviceTable.generated.cs b/Disco.Web/Views/Device/_DeviceTable.generated.cs new file mode 100644 index 00000000..4f2968b0 --- /dev/null +++ b/Disco.Web/Views/Device/_DeviceTable.generated.cs @@ -0,0 +1,387 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.17929 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Disco.Web.Views.Device +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using System.Web; + using System.Web.Helpers; + using System.Web.Mvc; + using System.Web.Mvc.Ajax; + using System.Web.Mvc.Html; + using System.Web.Routing; + using System.Web.Security; + using System.Web.UI; + using System.Web.WebPages; + using Disco.BI.Extensions; + using Disco.Models.Repository; + using Disco.Web; + using Disco.Web.Extensions; + + [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorGenerator", "1.5.0.0")] + [System.Web.WebPages.PageVirtualPathAttribute("~/Views/Device/_DeviceTable.cshtml")] + public class DeviceTable : System.Web.Mvc.WebViewPage> + { + public DeviceTable() + { + } + public override void Execute() + { +WriteLiteral("\r\n"); + + + #line 3 "..\..\Views\Device\_DeviceTable.cshtml" + + + #line default + #line hidden + + #line 3 "..\..\Views\Device\_DeviceTable.cshtml" + if (Model != null && Model.Count() > 0) + { + + + #line default + #line hidden +WriteLiteral(" + + + + Serial# + + + Asset# + + + Name + + + Model + + + Profile + + + Assigned User + + + Jobs + + + + +"); + + + #line 32 "..\..\Views\Device\_DeviceTable.cshtml" + + + #line default + #line hidden + + #line 32 "..\..\Views\Device\_DeviceTable.cshtml" + foreach (var item in Model) + { + + + #line default + #line hidden +WriteLiteral(" (item.DecommissionedDate.HasValue ? "decommissioned" : string.Empty + + #line default + #line hidden +, 1013), false) +); + +WriteLiteral(">\r\n \r\n"); + +WriteLiteral(" "); + + + #line 36 "..\..\Views\Device\_DeviceTable.cshtml" + Write(Html.ActionLink(item.SerialNumber, MVC.Device.Show(item.SerialNumber))); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n \r\n"); + +WriteLiteral(" "); + + + #line 39 "..\..\Views\Device\_DeviceTable.cshtml" + Write(item.AssetNumber); + + + #line default + #line hidden +WriteLiteral("\r\n"); + + + #line 40 "..\..\Views\Device\_DeviceTable.cshtml" + + + #line default + #line hidden + + #line 40 "..\..\Views\Device\_DeviceTable.cshtml" + if (item.DecommissionedDate.HasValue) + { + + #line default + #line hidden +WriteLiteral(" (Decommissioned\r\n"); + +WriteLiteral(" "); + + + #line 42 "..\..\Views\Device\_DeviceTable.cshtml" + Write(CommonHelpers.FriendlyDate(item.DecommissionedDate.Value)); + + + #line default + #line hidden +WriteLiteral(") "); + + + #line 42 "..\..\Views\Device\_DeviceTable.cshtml" + } + + + #line default + #line hidden +WriteLiteral(" \r\n \r\n"); + + + #line 45 "..\..\Views\Device\_DeviceTable.cshtml" + + + #line default + #line hidden + + #line 45 "..\..\Views\Device\_DeviceTable.cshtml" + if (string.IsNullOrWhiteSpace(item.ComputerName)) + { + + + #line default + #line hidden +WriteLiteral(" Unknown\r\n"); + + + #line 48 "..\..\Views\Device\_DeviceTable.cshtml" + } + else + { + + + #line default + #line hidden + + #line 51 "..\..\Views\Device\_DeviceTable.cshtml" + Write(item.ComputerName); + + + #line default + #line hidden + + #line 51 "..\..\Views\Device\_DeviceTable.cshtml" + + } + + + #line default + #line hidden +WriteLiteral(" \r\n \r\n"); + + + #line 55 "..\..\Views\Device\_DeviceTable.cshtml" + + + #line default + #line hidden + + #line 55 "..\..\Views\Device\_DeviceTable.cshtml" + if (item.DeviceModelDescription != null) + { + + + #line default + #line hidden +WriteLiteral(" "); + + + #line 57 "..\..\Views\Device\_DeviceTable.cshtml" + Write(item.DeviceModelDescription); + + + #line default + #line hidden +WriteLiteral("\r\n"); + + + #line 58 "..\..\Views\Device\_DeviceTable.cshtml" + } + else + { + + + #line default + #line hidden +WriteLiteral(" Unknown \r\n"); + + + #line 62 "..\..\Views\Device\_DeviceTable.cshtml" + } + + + #line default + #line hidden +WriteLiteral(" \r\n \r\n"); + +WriteLiteral(" "); + + + #line 65 "..\..\Views\Device\_DeviceTable.cshtml" + Write(item.DeviceProfileDescription); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n \r\n"); + + + #line 68 "..\..\Views\Device\_DeviceTable.cshtml" + + + #line default + #line hidden + + #line 68 "..\..\Views\Device\_DeviceTable.cshtml" + if (string.IsNullOrEmpty(item.AssignedUserId)) + { + + + #line default + #line hidden +WriteLiteral(" N/A \r\n"); + + + #line 71 "..\..\Views\Device\_DeviceTable.cshtml" + } + else + { + + + #line default + #line hidden +WriteLiteral(" "); + + + #line 74 "..\..\Views\Device\_DeviceTable.cshtml" + Write(item.AssignedUserDescription); + + + #line default + #line hidden +WriteLiteral("\r\n"); + + + #line 75 "..\..\Views\Device\_DeviceTable.cshtml" + } + + + #line default + #line hidden +WriteLiteral(" \r\n \r\n"); + +WriteLiteral(" "); + + + #line 78 "..\..\Views\Device\_DeviceTable.cshtml" + Write(item.JobCount); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n \r\n"); + + + #line 81 "..\..\Views\Device\_DeviceTable.cshtml" + } + + + #line default + #line hidden +WriteLiteral(" \r\n \r\n"); + + + #line 84 "..\..\Views\Device\_DeviceTable.cshtml" + } + else + { + + + #line default + #line hidden +WriteLiteral(" No Devices Found\r\n"); + + + #line 88 "..\..\Views\Device\_DeviceTable.cshtml" + } + + + #line default + #line hidden +WriteLiteral("\r\n"); + + } + } +} +#pragma warning restore 1591 diff --git a/Disco.Web/Views/Device/_DeviceUserAssignmentHistoryTable.cshtml b/Disco.Web/Views/Device/_DeviceUserAssignmentHistoryTable.cshtml new file mode 100644 index 00000000..081843ac --- /dev/null +++ b/Disco.Web/Views/Device/_DeviceUserAssignmentHistoryTable.cshtml @@ -0,0 +1,35 @@ +@model Disco.Models.Repository.Device +@if (Model.DeviceUserAssignments.Count > 0) +{ + + + + + + + @foreach (var dua in Model.DeviceUserAssignments.OrderByDescending(m => m.AssignedDate)) + { + + + + + + } +
+ User + + Assigned + + Unassigned +
+ @Html.ActionLink(dua.AssignedUser.ToString(), MVC.User.Show(dua.AssignedUserId)) + + @CommonHelpers.FriendlyDate(dua.AssignedDate) + + @CommonHelpers.FriendlyDate(dua.UnassignedDate, "Current") +
+} +else +{ + No Assignment History Available +} diff --git a/Disco.Web/Views/Device/_DeviceUserAssignmentHistoryTable.generated.cs b/Disco.Web/Views/Device/_DeviceUserAssignmentHistoryTable.generated.cs new file mode 100644 index 00000000..56033157 --- /dev/null +++ b/Disco.Web/Views/Device/_DeviceUserAssignmentHistoryTable.generated.cs @@ -0,0 +1,142 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.17929 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Disco.Web.Views.Device +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using System.Web; + using System.Web.Helpers; + using System.Web.Mvc; + using System.Web.Mvc.Ajax; + using System.Web.Mvc.Html; + using System.Web.Routing; + using System.Web.Security; + using System.Web.UI; + using System.Web.WebPages; + using Disco.BI.Extensions; + using Disco.Models.Repository; + using Disco.Web; + using Disco.Web.Extensions; + + [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorGenerator", "1.5.0.0")] + [System.Web.WebPages.PageVirtualPathAttribute("~/Views/Device/_DeviceUserAssignmentHistoryTable.cshtml")] + public class DeviceUserAssignmentHistoryTable : System.Web.Mvc.WebViewPage + { + public DeviceUserAssignmentHistoryTable() + { + } + public override void Execute() + { + + #line 2 "..\..\Views\Device\_DeviceUserAssignmentHistoryTable.cshtml" + if (Model.DeviceUserAssignments.Count > 0) +{ + + + #line default + #line hidden +WriteLiteral(" \r\n \r\n \r\n User\r\n \r\n " + +" \r\n Assigned\r\n \r\n \r\n " + +" Unassigned\r\n \r\n \r\n"); + + + #line 16 "..\..\Views\Device\_DeviceUserAssignmentHistoryTable.cshtml" + + + #line default + #line hidden + + #line 16 "..\..\Views\Device\_DeviceUserAssignmentHistoryTable.cshtml" + foreach (var dua in Model.DeviceUserAssignments.OrderByDescending(m => m.AssignedDate)) + { + + + #line default + #line hidden +WriteLiteral(" \r\n \r\n"); + +WriteLiteral(" "); + + + #line 20 "..\..\Views\Device\_DeviceUserAssignmentHistoryTable.cshtml" + Write(Html.ActionLink(dua.AssignedUser.ToString(), MVC.User.Show(dua.AssignedUserId))); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n \r\n"); + +WriteLiteral(" "); + + + #line 23 "..\..\Views\Device\_DeviceUserAssignmentHistoryTable.cshtml" + Write(CommonHelpers.FriendlyDate(dua.AssignedDate)); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n \r\n"); + +WriteLiteral(" "); + + + #line 26 "..\..\Views\Device\_DeviceUserAssignmentHistoryTable.cshtml" + Write(CommonHelpers.FriendlyDate(dua.UnassignedDate, "Current")); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n \r\n"); + + + #line 29 "..\..\Views\Device\_DeviceUserAssignmentHistoryTable.cshtml" + } + + + #line default + #line hidden +WriteLiteral(" \r\n"); + + + #line 31 "..\..\Views\Device\_DeviceUserAssignmentHistoryTable.cshtml" +} +else +{ + + + #line default + #line hidden +WriteLiteral(" No Assignment History Available\r\n"); + + + #line 35 "..\..\Views\Device\_DeviceUserAssignmentHistoryTable.cshtml" +} + + + #line default + #line hidden + } + } +} +#pragma warning restore 1591 diff --git a/Disco.Web/Views/Device/_ViewStart.cshtml b/Disco.Web/Views/Device/_ViewStart.cshtml new file mode 100644 index 00000000..181e92b8 --- /dev/null +++ b/Disco.Web/Views/Device/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Html.BundleDeferred("~/Style/Device"); +} \ No newline at end of file diff --git a/Disco.Web/Views/Device/_ViewStart.generated.cs b/Disco.Web/Views/Device/_ViewStart.generated.cs new file mode 100644 index 00000000..2b282bee --- /dev/null +++ b/Disco.Web/Views/Device/_ViewStart.generated.cs @@ -0,0 +1,54 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.17929 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Disco.Web.Views.Device +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using System.Web; + using System.Web.Helpers; + using System.Web.Mvc; + using System.Web.Mvc.Ajax; + using System.Web.Mvc.Html; + using System.Web.Routing; + using System.Web.Security; + using System.Web.UI; + using System.Web.WebPages; + using Disco.BI.Extensions; + using Disco.Models.Repository; + using Disco.Web; + using Disco.Web.Extensions; + + [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorGenerator", "1.5.0.0")] + [System.Web.WebPages.PageVirtualPathAttribute("~/Views/Device/_ViewStart.cshtml")] + public class ViewStart : System.Web.Mvc.ViewStartPage + { + public ViewStart() + { + } + public override void Execute() + { + + #line 1 "..\..\Views\Device\_ViewStart.cshtml" + + Html.BundleDeferred("~/Style/Device"); + + + #line default + #line hidden + } + } +} +#pragma warning restore 1591 diff --git a/Disco.Web/Views/InitialConfig/Complete.cshtml b/Disco.Web/Views/InitialConfig/Complete.cshtml new file mode 100644 index 00000000..305237c4 --- /dev/null +++ b/Disco.Web/Views/InitialConfig/Complete.cshtml @@ -0,0 +1,200 @@ +@model Disco.Web.Models.InitialConfig.CompleteModel +@{ + ViewBag.Title = null; +} +

@CommonHelpers.Breadcrumbs(Html.ToBreadcrumb("Initial Configuration", MVC.InitialConfig.Index(), "Complete"))

+
+
+

Verification Results

+ + + + + + + + + + + + + + + + +
+

Database

+
+ @{ + if (Model.RegistryDatabaseResult == null) + { + The database connection string was correctly configured and saved. + } + else + { + There was an error saving the database connection string configuration. +
+
+ @{var ex = Model.RegistryDatabaseResult; + do + { +
+

[@ex.GetType().Name]

+
+ @ex.Message +
+
+ if (ex.InnerException == null) { break; } + else { ex = ex.InnerException; } + } while (true); + } +
+ } + } +
+
+

Disco DNS Entry

+
+ @{ + if (Model.DiscoDnsTestResult.Item1 != null) + { + +
The following 'disco' DNS entry was found:
+
+ @Model.DiscoDnsTestResult.Item1.HostName + @{ + if (Model.DiscoDnsTestResult.Item1.Aliases.Length > 0) + { +
+ Aliases: +
    + @foreach (var a in Model.DiscoDnsTestResult.Item1.Aliases) + { +
  • @a
  • + } +
+
+ } + if (Model.DiscoDnsTestResult.Item1.AddressList.Length > 0) + { +
+ IP Addresses: +
    + @foreach (var a in Model.DiscoDnsTestResult.Item1.AddressList) + { +
  • @a.ToString()
  • + } +
+
+ } + } +
+ } + else + { + There was an error determining a DNS entry for Disco. +
+
+ @{var ex = Model.DiscoDnsTestResult.Item2; + do + { +
+

[@ex.GetType().Name]

+
+ @ex.Message +
+
+ if (ex.InnerException == null) { break; } + else { ex = ex.InnerException; } + } while (true); + } +
+ } + } +
+
+

Connectivity to http://discoict.com.au

+
+ @{ + if (Model.DiscoIctComAuWebResult == null) + { + A connection was successfully established to http://discoict.com.au. + } + else + { + There was an error establishing a connection to http://discoict.com.au. This may be caused by missing proxy settings - after starting Disco check these settings in the 'System' configuration area. +
+
+ @{var ex = Model.DiscoIctComAuWebResult; + do + { +
+

[@ex.GetType().Name]

+
+ @ex.Message +
+
+ if (ex.InnerException == null) { break; } + else { ex = ex.InnerException; } + } while (true); + } +
+ } + } +
+
+

Unblock ICMP (Ping) for the Disco server

+
+ The Disco Client Bootstrapper requires the Disco server to respond to ICMP Echo requests (Ping) to function correctly. Please insure any firewall rules are updated accordingly. +
+
+

Configure a regularly scheduled Backup

+
+ Please ensure both the SQL Database and File Store are backed up regularly. +
+
+
+
+ @{ + if (Model.LaunchAllowed) + { + Start Disco + } + else + { + Try Again + } + } +
+
+
+

Re-running Verification Tests

+
Please wait while the verification tests are performed.
+
+ diff --git a/Disco.Web/Views/InitialConfig/Complete.generated.cs b/Disco.Web/Views/InitialConfig/Complete.generated.cs new file mode 100644 index 00000000..2fbe38a5 --- /dev/null +++ b/Disco.Web/Views/InitialConfig/Complete.generated.cs @@ -0,0 +1,726 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.17929 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Disco.Web.Views.InitialConfig +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using System.Web; + using System.Web.Helpers; + using System.Web.Mvc; + using System.Web.Mvc.Ajax; + using System.Web.Mvc.Html; + using System.Web.Routing; + using System.Web.Security; + using System.Web.UI; + using System.Web.WebPages; + using Disco.BI.Extensions; + using Disco.Models.Repository; + using Disco.Web; + using Disco.Web.Extensions; + + [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorGenerator", "1.5.0.0")] + [System.Web.WebPages.PageVirtualPathAttribute("~/Views/InitialConfig/Complete.cshtml")] + public class Complete : System.Web.Mvc.WebViewPage + { + public Complete() + { + } + public override void Execute() + { + + #line 2 "..\..\Views\InitialConfig\Complete.cshtml" + + ViewBag.Title = null; + + + #line default + #line hidden +WriteLiteral("\r\n

"); + + + #line 5 "..\..\Views\InitialConfig\Complete.cshtml" +Write(CommonHelpers.Breadcrumbs(Html.ToBreadcrumb("Initial Configuration", MVC.InitialConfig.Index(), "Complete"))); + + + #line default + #line hidden +WriteLiteral("

\r\n\r\n \r\n

Verification Results

\r\n \r\n \r\n " + +" \r\n \r\n " + +" \r\n \r\n \r\n " + +" \r\n \r\n \r\n " + +" \r\n + + + \r\n " + +" \r\n
\r\n

(Model.RegistryDatabaseResult == null ? "success" : "error" + + #line default + #line hidden +, 425), false) +); + +WriteLiteral(">Database

\r\n \r\n"); + + + #line 14 "..\..\Views\InitialConfig\Complete.cshtml" + + + #line default + #line hidden + + #line 14 "..\..\Views\InitialConfig\Complete.cshtml" + + if (Model.RegistryDatabaseResult == null) + { + + + #line default + #line hidden +WriteLiteral(" "); + +WriteLiteral("The database connection string was correctly configured and saved."); + +WriteLiteral("\r\n"); + + + #line 18 "..\..\Views\InitialConfig\Complete.cshtml" + } + else + { + + + #line default + #line hidden +WriteLiteral(" "); + +WriteLiteral("There was an error saving the database connection string configuration."); + +WriteLiteral("\r\n"); + +WriteLiteral("
\r\n"); + +WriteLiteral(" \r\n"); + + + #line 24 "..\..\Views\InitialConfig\Complete.cshtml" + + + #line default + #line hidden + + #line 24 "..\..\Views\InitialConfig\Complete.cshtml" + var ex = Model.RegistryDatabaseResult; + do + { + + + #line default + #line hidden +WriteLiteral("
\r\n " + +"

["); + + + #line 28 "..\..\Views\InitialConfig\Complete.cshtml" + Write(ex.GetType().Name); + + + #line default + #line hidden +WriteLiteral("]

\r\n \r\n"); + +WriteLiteral(" "); + + + #line 30 "..\..\Views\InitialConfig\Complete.cshtml" + Write(ex.Message); + + + #line default + #line hidden +WriteLiteral("\r\n
\r\n " + +" \r\n"); + + + #line 33 "..\..\Views\InitialConfig\Complete.cshtml" + if (ex.InnerException == null) { break; } + else { ex = ex.InnerException; } + } while (true); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n"); + + + #line 38 "..\..\Views\InitialConfig\Complete.cshtml" + } + + + #line default + #line hidden +WriteLiteral("\r\n \r\n
\r\n

(Model.DiscoDnsTestResult.Item2 == null ? "success" : "warning" + + #line default + #line hidden +, 2053), false) +); + +WriteLiteral(">Disco DNS Entry

\r\n \r\n"); + + + #line 47 "..\..\Views\InitialConfig\Complete.cshtml" + + + #line default + #line hidden + + #line 47 "..\..\Views\InitialConfig\Complete.cshtml" + + if (Model.DiscoDnsTestResult.Item1 != null) + { + + + + #line default + #line hidden +WriteLiteral("
The following \'disco\' DNS entry was found:
" + +"\r\n"); + +WriteLiteral(" \r\n "); + + + #line 53 "..\..\Views\InitialConfig\Complete.cshtml" + Write(Model.DiscoDnsTestResult.Item1.HostName); + + + #line default + #line hidden +WriteLiteral("\r\n"); + + + #line 54 "..\..\Views\InitialConfig\Complete.cshtml" + + + #line default + #line hidden + + #line 54 "..\..\Views\InitialConfig\Complete.cshtml" + + if (Model.DiscoDnsTestResult.Item1.Aliases.Length > 0) + { + + + #line default + #line hidden +WriteLiteral("
\r\n " + +" Aliases:\r\n
    \r\n"); + + + #line 60 "..\..\Views\InitialConfig\Complete.cshtml" + + + #line default + #line hidden + + #line 60 "..\..\Views\InitialConfig\Complete.cshtml" + foreach (var a in Model.DiscoDnsTestResult.Item1.Aliases) + { + + + #line default + #line hidden +WriteLiteral("
  • "); + + + #line 62 "..\..\Views\InitialConfig\Complete.cshtml" + Write(a); + + + #line default + #line hidden +WriteLiteral("
  • \r\n"); + + + #line 63 "..\..\Views\InitialConfig\Complete.cshtml" + } + + + #line default + #line hidden +WriteLiteral("
\r\n
\r\n"); + + + #line 66 "..\..\Views\InitialConfig\Complete.cshtml" + } + if (Model.DiscoDnsTestResult.Item1.AddressList.Length > 0) + { + + + #line default + #line hidden +WriteLiteral("
\r\n " + +" IP Addresses:\r\n
    \r\n"); + + + #line 72 "..\..\Views\InitialConfig\Complete.cshtml" + + + #line default + #line hidden + + #line 72 "..\..\Views\InitialConfig\Complete.cshtml" + foreach (var a in Model.DiscoDnsTestResult.Item1.AddressList) + { + + + #line default + #line hidden +WriteLiteral("
  • "); + + + #line 74 "..\..\Views\InitialConfig\Complete.cshtml" + Write(a.ToString()); + + + #line default + #line hidden +WriteLiteral("
  • \r\n"); + + + #line 75 "..\..\Views\InitialConfig\Complete.cshtml" + } + + + #line default + #line hidden +WriteLiteral("
\r\n
\r\n"); + + + #line 78 "..\..\Views\InitialConfig\Complete.cshtml" + } + + + #line default + #line hidden +WriteLiteral("\r\n \r\n"); + + + #line 81 "..\..\Views\InitialConfig\Complete.cshtml" + } + else + { + + + #line default + #line hidden +WriteLiteral(" "); + +WriteLiteral("There was an error determining a DNS entry for Disco."); + +WriteLiteral("\r\n"); + +WriteLiteral("
\r\n"); + +WriteLiteral(" \r\n"); + + + #line 87 "..\..\Views\InitialConfig\Complete.cshtml" + + + #line default + #line hidden + + #line 87 "..\..\Views\InitialConfig\Complete.cshtml" + var ex = Model.DiscoDnsTestResult.Item2; + do + { + + + #line default + #line hidden +WriteLiteral("
\r\n " + +"

["); + + + #line 91 "..\..\Views\InitialConfig\Complete.cshtml" + Write(ex.GetType().Name); + + + #line default + #line hidden +WriteLiteral("]

\r\n \r\n"); + +WriteLiteral(" "); + + + #line 93 "..\..\Views\InitialConfig\Complete.cshtml" + Write(ex.Message); + + + #line default + #line hidden +WriteLiteral("\r\n
\r\n " + +" \r\n"); + + + #line 96 "..\..\Views\InitialConfig\Complete.cshtml" + if (ex.InnerException == null) { break; } + else { ex = ex.InnerException; } + } while (true); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n"); + + + #line 101 "..\..\Views\InitialConfig\Complete.cshtml" + } + + + #line default + #line hidden +WriteLiteral("\r\n \r\n
\r\n

(Model.DiscoIctComAuWebResult == null ? "success" : "warning" + + #line default + #line hidden +, 4958), false) +); + +WriteLiteral(">Connectivity to http://discoict.com.au

\r\n \r\n"); + + + #line 110 "..\..\Views\InitialConfig\Complete.cshtml" + + + #line default + #line hidden + + #line 110 "..\..\Views\InitialConfig\Complete.cshtml" + + if (Model.DiscoIctComAuWebResult == null) + { + + + #line default + #line hidden +WriteLiteral(" "); + +WriteLiteral("A connection was successfully established to http://discoict.com.au."); + +WriteLiteral("\r\n"); + + + #line 114 "..\..\Views\InitialConfig\Complete.cshtml" + } + else + { + + + #line default + #line hidden +WriteLiteral(" "); + +WriteLiteral("There was an error establishing a connection to http://discoict.com.au. This may be caused by missing proxy settings - after" + +" starting Disco check these settings in the \'System\' configuration area."); + +WriteLiteral("\r\n"); + +WriteLiteral("
\r\n"); + +WriteLiteral(" \r\n"); + + + #line 120 "..\..\Views\InitialConfig\Complete.cshtml" + + + #line default + #line hidden + + #line 120 "..\..\Views\InitialConfig\Complete.cshtml" + var ex = Model.DiscoIctComAuWebResult; + do + { + + + #line default + #line hidden +WriteLiteral("
\r\n " + +"

["); + + + #line 124 "..\..\Views\InitialConfig\Complete.cshtml" + Write(ex.GetType().Name); + + + #line default + #line hidden +WriteLiteral("]

\r\n \r\n"); + +WriteLiteral(" "); + + + #line 126 "..\..\Views\InitialConfig\Complete.cshtml" + Write(ex.Message); + + + #line default + #line hidden +WriteLiteral("\r\n
\r\n " + +" \r\n"); + + + #line 129 "..\..\Views\InitialConfig\Complete.cshtml" + if (ex.InnerException == null) { break; } + else { ex = ex.InnerException; } + } while (true); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n"); + + + #line 134 "..\..\Views\InitialConfig\Complete.cshtml" + } + + + #line default + #line hidden +WriteLiteral("\r\n \r\n
\r\n

Unblock ICMP (Ping) for the Disco server

\r\n + The Disco Client Bootstrapper requires the Disco server to respond to ICMP Echo requests (Ping) to function correctly. Please insure any firewall rules are updated accordingly. + +
+

Configure a regularly scheduled Backup

\r\n \r\n Please ensure both the SQL Database and File Store are" + +" backed up regularly.\r\n \r\n
\r\n \r\n \r\n"); + + + #line 158 "..\..\Views\InitialConfig\Complete.cshtml" + + + #line default + #line hidden + + #line 158 "..\..\Views\InitialConfig\Complete.cshtml" + + if (Model.LaunchAllowed) + { + + + #line default + #line hidden +WriteLiteral(" (Url.Action(MVC.InitialConfig.RestartWebApp()) + + #line default + #line hidden +, 7798), false) +); + +WriteLiteral(" class=\"button\""); + +WriteLiteral(">Start Disco\r\n"); + + + #line 162 "..\..\Views\InitialConfig\Complete.cshtml" + } + else + { + + + #line default + #line hidden +WriteLiteral(" (Url.Action(MVC.InitialConfig.Complete()) + + #line default + #line hidden +, 7963), false) +); + +WriteLiteral(" class=\"button\""); + +WriteLiteral(">Try Again\r\n"); + + + #line 166 "..\..\Views\InitialConfig\Complete.cshtml" + } + + + #line default + #line hidden +WriteLiteral("\r\n \r\n\r\n\r\n

Re-running Verification Tests

+
Please wait while the verification tests are performed.
+ + +"); + + } + } +} +#pragma warning restore 1591 diff --git a/Disco.Web/Views/InitialConfig/Database.cshtml b/Disco.Web/Views/InitialConfig/Database.cshtml new file mode 100644 index 00000000..4a57edc4 --- /dev/null +++ b/Disco.Web/Views/InitialConfig/Database.cshtml @@ -0,0 +1,119 @@ +@model Disco.Web.Models.InitialConfig.DatabaseModel +@{ + ViewBag.Title = null; +} +

@CommonHelpers.Breadcrumbs(Html.ToBreadcrumb("Initial Configuration", MVC.InitialConfig.Index(), "Database"))

+@using (Html.BeginForm()) +{ + @Html.ValidationSummary(true) + +
+

SQL Server Connection

+ + + + + + + + + + + + + +
Server: + + @Html.EditorFor(m => m.Server) @Html.ValidationMessageFor(m => m.Server) +
+ If the default instance of SQL Server is not being used, include the instance name.
+ For example: "SERVER_NAME\INSTANCE_NAME" +
+
Database Name: + + @Html.EditorFor(m => m.DatabaseName) @Html.ValidationMessageFor(m => m.DatabaseName) +
+ An attempt will be made to create a database with this name if it does not exist. +
+
Authentication Method: + + @Html.DropDownListFor(m => m.AuthMethod, Model.AuthMethods) @Html.ValidationMessageFor(m => m.AuthMethod) +
+ Integrated Authentication is recommended.
+ To use Integrated Authentication ensure the DiscoServiceAccount domain user has the db_owner role over the database, or sysadmin role if creating a new database. +
+ +
+
+
+ +
+} +
+

Building and Validating Database

+
Please wait while the Disco database is created and/or validated
+
+ diff --git a/Disco.Web/Views/InitialConfig/Database.generated.cs b/Disco.Web/Views/InitialConfig/Database.generated.cs new file mode 100644 index 00000000..0c81f853 --- /dev/null +++ b/Disco.Web/Views/InitialConfig/Database.generated.cs @@ -0,0 +1,317 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.17929 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Disco.Web.Views.InitialConfig +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using System.Web; + using System.Web.Helpers; + using System.Web.Mvc; + using System.Web.Mvc.Ajax; + using System.Web.Mvc.Html; + using System.Web.Routing; + using System.Web.Security; + using System.Web.UI; + using System.Web.WebPages; + using Disco.BI.Extensions; + using Disco.Models.Repository; + using Disco.Web; + using Disco.Web.Extensions; + + [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorGenerator", "1.5.0.0")] + [System.Web.WebPages.PageVirtualPathAttribute("~/Views/InitialConfig/Database.cshtml")] + public class Database : System.Web.Mvc.WebViewPage + { + public Database() + { + } + public override void Execute() + { + + #line 2 "..\..\Views\InitialConfig\Database.cshtml" + + ViewBag.Title = null; + + + #line default + #line hidden +WriteLiteral("\r\n

"); + + + #line 5 "..\..\Views\InitialConfig\Database.cshtml" +Write(CommonHelpers.Breadcrumbs(Html.ToBreadcrumb("Initial Configuration", MVC.InitialConfig.Index(), "Database"))); + + + #line default + #line hidden +WriteLiteral("

\r\n"); + + + #line 6 "..\..\Views\InitialConfig\Database.cshtml" + using (Html.BeginForm()) +{ + + + #line default + #line hidden + + #line 8 "..\..\Views\InitialConfig\Database.cshtml" +Write(Html.ValidationSummary(true)); + + + #line default + #line hidden + + #line 8 "..\..\Views\InitialConfig\Database.cshtml" + + + + + #line default + #line hidden +WriteLiteral(" \r\n

SQL Server Connection

\r\n \r\n \r\n " + +" Server:\r\n \r\n \r\n \r\n \r\n \r\n + + + + \r\n \r\n \r\n \r\n \r\n " + +" \r\n <" + +"td>"); + + + #line 55 "..\..\Views\InitialConfig\Database.cshtml" + Write(Html.EditorFor(m => m.Auth_SQL_Password)); + + + #line default + #line hidden +WriteLiteral(" "); + + + #line 55 "..\..\Views\InitialConfig\Database.cshtml" + Write(Html.ValidationMessageFor(m => m.Auth_SQL_Password)); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n
\r\n"); + +WriteLiteral(" "); + + + #line 17 "..\..\Views\InitialConfig\Database.cshtml" + Write(Html.EditorFor(m => m.Server)); + + + #line default + #line hidden +WriteLiteral(" "); + + + #line 17 "..\..\Views\InitialConfig\Database.cshtml" + Write(Html.ValidationMessageFor(m => m.Server)); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n If the default instance of SQL Server is not being use" + +"d, include the instance name.
\r\n For example: \"SERVER_NAME\\INSTANCE_NAME\"\r\n \r\n " + +"
Database Name:\r\n" + +" \r\n"); + +WriteLiteral(" "); + + + #line 28 "..\..\Views\InitialConfig\Database.cshtml" + Write(Html.EditorFor(m => m.DatabaseName)); + + + #line default + #line hidden +WriteLiteral(" "); + + + #line 28 "..\..\Views\InitialConfig\Database.cshtml" + Write(Html.ValidationMessageFor(m => m.DatabaseName)); + + + #line default + #line hidden +WriteLiteral("\r\n + An attempt will be made to create a database with this name if it does not exist. + +
Authentication Method: + +"); + +WriteLiteral(" "); + + + #line 38 "..\..\Views\InitialConfig\Database.cshtml" + Write(Html.DropDownListFor(m => m.AuthMethod, Model.AuthMethods)); + + + #line default + #line hidden +WriteLiteral(" "); + + + #line 38 "..\..\Views\InitialConfig\Database.cshtml" + Write(Html.ValidationMessageFor(m => m.AuthMethod)); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n Integrated Authentication is recommended.
\r\n " + +" To use Integrated Authentication ensure the DiscoServiceAccount domain user has the db_owner role over the database, or sysadmin role if creating a new database.\r\n \r\n " + +" \r\n

SQL Authentication Credentials

\r\n " + +" \r\n The following credentials will be stored in clear-" + +"text.\r\n \r\n \r\n
Username" + +":"); + + + #line 51 "..\..\Views\InitialConfig\Database.cshtml" + Write(Html.EditorFor(m => m.Auth_SQL_Username)); + + + #line default + #line hidden +WriteLiteral(" "); + + + #line 51 "..\..\Views\InitialConfig\Database.cshtml" + Write(Html.ValidationMessageFor(m => m.Auth_SQL_Username)); + + + #line default + #line hidden +WriteLiteral("
Password:
\r\n " + +" \r\n \r\n \r\n \r\n \r\n"); + +WriteLiteral(" \r\n \r\n \r\n"); + + + #line 66 "..\..\Views\InitialConfig\Database.cshtml" +} + + + #line default + #line hidden +WriteLiteral("\r\n

Building and Validating Database

\r\n
Please wait while the Dis" + +"co database is created and/or validated
\r\n\r\n\r\n"); + + } + } +} +#pragma warning restore 1591 diff --git a/Disco.Web/Views/InitialConfig/FileStore.cshtml b/Disco.Web/Views/InitialConfig/FileStore.cshtml new file mode 100644 index 00000000..6c1e9058 --- /dev/null +++ b/Disco.Web/Views/InitialConfig/FileStore.cshtml @@ -0,0 +1,249 @@ +@model Disco.Web.Models.InitialConfig.FileStoreModel +@{ + ViewBag.Title = null; + Html.BundleDeferred("~/Style/jQueryUI/dynatree"); + Html.BundleDeferred("~/ClientScripts/Modules/jQueryUI-DynaTree"); +} +

@CommonHelpers.Breadcrumbs(Html.ToBreadcrumb("Initial Configuration", MVC.InitialConfig.Index(), "File Store"))

+
+ @Html.ValidationSummary(false) + +
+

File Store Location

+ + + + + + + +
+
+
+ +
+
+ Selected Location: <None> (Invalid DataStore Location) +
+
+
+ @using (Html.BeginForm()) + { + @Html.HiddenFor(m => m.FileStoreLocation) +
+ +
+ } +
+
+

Building and Validating File Store

+
Please wait while the Disco File Store is created and/or validated
+
+
+

Create Directory

+ +
Parent:
+
+ diff --git a/Disco.Web/Views/InitialConfig/FileStore.generated.cs b/Disco.Web/Views/InitialConfig/FileStore.generated.cs new file mode 100644 index 00000000..19d1d33a --- /dev/null +++ b/Disco.Web/Views/InitialConfig/FileStore.generated.cs @@ -0,0 +1,329 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.17929 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Disco.Web.Views.InitialConfig +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using System.Web; + using System.Web.Helpers; + using System.Web.Mvc; + using System.Web.Mvc.Ajax; + using System.Web.Mvc.Html; + using System.Web.Routing; + using System.Web.Security; + using System.Web.UI; + using System.Web.WebPages; + using Disco.BI.Extensions; + using Disco.Models.Repository; + using Disco.Web; + using Disco.Web.Extensions; + + [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorGenerator", "1.5.0.0")] + [System.Web.WebPages.PageVirtualPathAttribute("~/Views/InitialConfig/FileStore.cshtml")] + public class FileStore : System.Web.Mvc.WebViewPage + { + public FileStore() + { + } + public override void Execute() + { + + #line 2 "..\..\Views\InitialConfig\FileStore.cshtml" + + ViewBag.Title = null; + Html.BundleDeferred("~/Style/jQueryUI/dynatree"); + Html.BundleDeferred("~/ClientScripts/Modules/jQueryUI-DynaTree"); + + + #line default + #line hidden +WriteLiteral("\r\n

"); + + + #line 7 "..\..\Views\InitialConfig\FileStore.cshtml" +Write(CommonHelpers.Breadcrumbs(Html.ToBreadcrumb("Initial Configuration", MVC.InitialConfig.Index(), "File Store"))); + + + #line default + #line hidden +WriteLiteral("

\r\n\r\n"); + +WriteLiteral(" "); + + + #line 9 "..\..\Views\InitialConfig\FileStore.cshtml" +Write(Html.ValidationSummary(false)); + + + #line default + #line hidden +WriteLiteral("\r\n\r\n \r\n

File Store Location

\r\n \r\n \r\n " + +" \r\n " + +" \r\n \r\n \r\n \r\n
\r\n \r\n \r\n \r\n Create Directory\r\n \r\n
\r\n
\r\n " + +" Selected Location: <None> (Invalid DataStore Location)\r\n
\r\n " + +"
\r\n \r\n"); + + + #line 32 "..\..\Views\InitialConfig\FileStore.cshtml" + + + #line default + #line hidden + + #line 32 "..\..\Views\InitialConfig\FileStore.cshtml" + using (Html.BeginForm()) + { + + + #line default + #line hidden + + #line 34 "..\..\Views\InitialConfig\FileStore.cshtml" + Write(Html.HiddenFor(m => m.FileStoreLocation)); + + + #line default + #line hidden + + #line 34 "..\..\Views\InitialConfig\FileStore.cshtml" + + + + #line default + #line hidden +WriteLiteral(" \r\n \r\n \r\n"); + + + #line 38 "..\..\Views\InitialConfig\FileStore.cshtml" + } + + + #line default + #line hidden +WriteLiteral("\r\n\r\n

Building and Validating File Store

\r\n
Please wait while the D" + +"isco File Store is created and/or validated
\r\n\r\n\r\n

Create Directory

\r\n \r\n
Parent:
\r\n\r\n\r\n"); + + } + } +} +#pragma warning restore 1591 diff --git a/Disco.Web/Views/InitialConfig/RestartWebApp.cshtml b/Disco.Web/Views/InitialConfig/RestartWebApp.cshtml new file mode 100644 index 00000000..305f92e2 --- /dev/null +++ b/Disco.Web/Views/InitialConfig/RestartWebApp.cshtml @@ -0,0 +1,34 @@ +@{ + ViewBag.Title = null; +} +

@CommonHelpers.Breadcrumbs("Initial Configuration > Starting Disco")

+
+

Starting Disco

+
Please wait while the Disco environment is initialized
+
+ diff --git a/Disco.Web/Views/InitialConfig/RestartWebApp.generated.cs b/Disco.Web/Views/InitialConfig/RestartWebApp.generated.cs new file mode 100644 index 00000000..f07ce553 --- /dev/null +++ b/Disco.Web/Views/InitialConfig/RestartWebApp.generated.cs @@ -0,0 +1,111 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.17929 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Disco.Web.Views.InitialConfig +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using System.Web; + using System.Web.Helpers; + using System.Web.Mvc; + using System.Web.Mvc.Ajax; + using System.Web.Mvc.Html; + using System.Web.Routing; + using System.Web.Security; + using System.Web.UI; + using System.Web.WebPages; + using Disco.BI.Extensions; + using Disco.Models.Repository; + using Disco.Web; + using Disco.Web.Extensions; + + [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorGenerator", "1.5.0.0")] + [System.Web.WebPages.PageVirtualPathAttribute("~/Views/InitialConfig/RestartWebApp.cshtml")] + public class RestartWebApp : System.Web.Mvc.WebViewPage + { + public RestartWebApp() + { + } + public override void Execute() + { + + #line 1 "..\..\Views\InitialConfig\RestartWebApp.cshtml" + + ViewBag.Title = null; + + + #line default + #line hidden +WriteLiteral("\r\n

"); + + + #line 4 "..\..\Views\InitialConfig\RestartWebApp.cshtml" +Write(CommonHelpers.Breadcrumbs("Initial Configuration > Starting Disco")); + + + #line default + #line hidden +WriteLiteral("

\r\n\r\n

Starting Disco

\r\n Please wait while the Disco environment is initialized + + +"); + + } + } +} +#pragma warning restore 1591 diff --git a/Disco.Web/Views/InitialConfig/Welcome.cshtml b/Disco.Web/Views/InitialConfig/Welcome.cshtml new file mode 100644 index 00000000..c22cbc06 --- /dev/null +++ b/Disco.Web/Views/InitialConfig/Welcome.cshtml @@ -0,0 +1,34 @@ +@model Disco.Web.Models.InitialConfig.WelcomeModel +@{ + ViewBag.Title = null; +} +

Welcome to Disco ICT Management!

+
+

The installation is complete, but a few things need to be configured before Disco can be started.

+ @using (Html.BeginForm()) + { +
+

Organisation Name

+ + + + +
+
+ @Html.EditorFor(m => m.OrganisationName)
+ @Html.ValidationMessageFor(m => m.OrganisationName) +
+
+
+
+ +
+ } +
+ diff --git a/Disco.Web/Views/InitialConfig/Welcome.generated.cs b/Disco.Web/Views/InitialConfig/Welcome.generated.cs new file mode 100644 index 00000000..b0503f19 --- /dev/null +++ b/Disco.Web/Views/InitialConfig/Welcome.generated.cs @@ -0,0 +1,133 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.17929 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Disco.Web.Views.InitialConfig +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using System.Web; + using System.Web.Helpers; + using System.Web.Mvc; + using System.Web.Mvc.Ajax; + using System.Web.Mvc.Html; + using System.Web.Routing; + using System.Web.Security; + using System.Web.UI; + using System.Web.WebPages; + using Disco.BI.Extensions; + using Disco.Models.Repository; + using Disco.Web; + using Disco.Web.Extensions; + + [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorGenerator", "1.5.0.0")] + [System.Web.WebPages.PageVirtualPathAttribute("~/Views/InitialConfig/Welcome.cshtml")] + public class Welcome : System.Web.Mvc.WebViewPage + { + public Welcome() + { + } + public override void Execute() + { + + #line 2 "..\..\Views\InitialConfig\Welcome.cshtml" + + ViewBag.Title = null; + + + #line default + #line hidden +WriteLiteral("\r\n

Welcome to Disco ICT Management!

\r\n\r\n

The installation is complete, but a few things need to be configured be" + +"fore Disco can be started.

\r\n"); + + + #line 8 "..\..\Views\InitialConfig\Welcome.cshtml" + + + #line default + #line hidden + + #line 8 "..\..\Views\InitialConfig\Welcome.cshtml" + using (Html.BeginForm()) + { + + + #line default + #line hidden +WriteLiteral(" \r\n

Organisation Name

\r\n \r\n <" + +"tr>\r\n \r\n \r\n
\r\n
\r\n"); + +WriteLiteral(" "); + + + #line 16 "..\..\Views\InitialConfig\Welcome.cshtml" + Write(Html.EditorFor(m => m.OrganisationName)); + + + #line default + #line hidden +WriteLiteral("
\r\n"); + +WriteLiteral(" "); + + + #line 17 "..\..\Views\InitialConfig\Welcome.cshtml" + Write(Html.ValidationMessageFor(m => m.OrganisationName)); + + + #line default + #line hidden +WriteLiteral("\r\n
\r\n
\r\n \r\n"); + +WriteLiteral(" \r\n \r\n \r\n"); + + + #line 26 "..\..\Views\InitialConfig\Welcome.cshtml" + } + + + #line default + #line hidden +WriteLiteral("\r\n\r\n"); + + } + } +} +#pragma warning restore 1591 diff --git a/Disco.Web/Views/InitialConfig/_Layout.cshtml b/Disco.Web/Views/InitialConfig/_Layout.cshtml new file mode 100644 index 00000000..c0c3594b --- /dev/null +++ b/Disco.Web/Views/InitialConfig/_Layout.cshtml @@ -0,0 +1,35 @@ +@{ + Html.BundleDeferred("~/Style/Site"); + Html.BundleDeferred("~/ClientScripts/Core"); +} + + + + + + Disco@{if(ViewBag.Title != null){<text> - @CommonHelpers.BreadcrumbsTitle(ViewBag.Title)</text>}} + @Html.BundleRenderDeferred() + @RenderSection("head", false) + + +
+
+
+
+ + DISCO - ICT Asset Management +
+
+
+
+
+ @if(ViewBag.Title != null){
@CommonHelpers.Breadcrumbs(ViewBag.Title)
} +
+ @RenderBody() +
+
+ Disco v@(Disco.Web.DiscoApplication.Version) +
+
+ + \ No newline at end of file diff --git a/Disco.Web/Views/InitialConfig/_Layout.generated.cs b/Disco.Web/Views/InitialConfig/_Layout.generated.cs new file mode 100644 index 00000000..b06793de --- /dev/null +++ b/Disco.Web/Views/InitialConfig/_Layout.generated.cs @@ -0,0 +1,218 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.17929 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Disco.Web.Views.InitialConfig +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using System.Web; + using System.Web.Helpers; + using System.Web.Mvc; + using System.Web.Mvc.Ajax; + using System.Web.Mvc.Html; + using System.Web.Routing; + using System.Web.Security; + using System.Web.UI; + using System.Web.WebPages; + using Disco.BI.Extensions; + using Disco.Models.Repository; + using Disco.Web; + using Disco.Web.Extensions; + + [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorGenerator", "1.5.0.0")] + [System.Web.WebPages.PageVirtualPathAttribute("~/Views/InitialConfig/_Layout.cshtml")] + public class Layout : System.Web.Mvc.WebViewPage + { + public Layout() + { + } + public override void Execute() + { + + #line 1 "..\..\Views\InitialConfig\_Layout.cshtml" + + Html.BundleDeferred("~/Style/Site"); + Html.BundleDeferred("~/ClientScripts/Core"); + + + #line default + #line hidden +WriteLiteral("\r\n\r\n\r\n\r\n \r\n \r\n Disco"); + + + #line 10 "..\..\Views\InitialConfig\_Layout.cshtml" + if(ViewBag.Title != null){ + + #line default + #line hidden +WriteLiteral(" - "); + + + #line 10 "..\..\Views\InitialConfig\_Layout.cshtml" + Write(CommonHelpers.BreadcrumbsTitle(ViewBag.Title)); + + + #line default + #line hidden + + #line 10 "..\..\Views\InitialConfig\_Layout.cshtml" + } + + #line default + #line hidden +WriteLiteral("\r\n"); + +WriteLiteral(" "); + + + #line 11 "..\..\Views\InitialConfig\_Layout.cshtml" +Write(Html.BundleRenderDeferred()); + + + #line default + #line hidden +WriteLiteral("\r\n"); + +WriteLiteral(" "); + + + #line 12 "..\..\Views\InitialConfig\_Layout.cshtml" +Write(RenderSection("head", false)); + + + #line default + #line hidden +WriteLiteral("\r\n\r\n\r\n \r\n
\r\n \r\n \r\n (Url.Action(MVC.Public.Public.Index()) + + #line default + #line hidden +, 585), false) +); + +WriteLiteral(">\r\n (Links.ClientSource.Style.Images.Heading_png + + #line default + #line hidden +, 661), false) +); + +WriteLiteral(" alt=\"DISCO - ICT Asset Management\""); + +WriteLiteral(" />\r\n \r\n \r\n \r\n \r\n
\r\n"); + + + #line 26 "..\..\Views\InitialConfig\_Layout.cshtml" + + + #line default + #line hidden + + #line 26 "..\..\Views\InitialConfig\_Layout.cshtml" + if(ViewBag.Title != null){ + + #line default + #line hidden +WriteLiteral(""); + + + #line 26 "..\..\Views\InitialConfig\_Layout.cshtml" + Write(CommonHelpers.Breadcrumbs(ViewBag.Title)); + + + #line default + #line hidden +WriteLiteral(""); + + + #line 26 "..\..\Views\InitialConfig\_Layout.cshtml" + } + + + #line default + #line hidden +WriteLiteral(" \r\n"); + +WriteLiteral(" "); + + + #line 28 "..\..\Views\InitialConfig\_Layout.cshtml" + Write(RenderBody()); + + + #line default + #line hidden +WriteLiteral("\r\n
\r\n
\r\n Disco v"); + + + #line 31 "..\..\Views\InitialConfig\_Layout.cshtml" + Write(Disco.Web.DiscoApplication.Version); + + + #line default + #line hidden +WriteLiteral("\r\n
\r\n \r\n\r\n"); + + } + } +} +#pragma warning restore 1591 diff --git a/Disco.Web/Views/InitialConfig/_ViewStart.cshtml b/Disco.Web/Views/InitialConfig/_ViewStart.cshtml new file mode 100644 index 00000000..4cca93cc --- /dev/null +++ b/Disco.Web/Views/InitialConfig/_ViewStart.cshtml @@ -0,0 +1,4 @@ +@{ + Layout = "~/Views/InitialConfig/_Layout.cshtml"; + Html.BundleDeferred("~/Style/InitialConfig"); +} \ No newline at end of file diff --git a/Disco.Web/Views/InitialConfig/_ViewStart.generated.cs b/Disco.Web/Views/InitialConfig/_ViewStart.generated.cs new file mode 100644 index 00000000..1414f176 --- /dev/null +++ b/Disco.Web/Views/InitialConfig/_ViewStart.generated.cs @@ -0,0 +1,55 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.17929 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Disco.Web.Views.InitialConfig +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using System.Web; + using System.Web.Helpers; + using System.Web.Mvc; + using System.Web.Mvc.Ajax; + using System.Web.Mvc.Html; + using System.Web.Routing; + using System.Web.Security; + using System.Web.UI; + using System.Web.WebPages; + using Disco.BI.Extensions; + using Disco.Models.Repository; + using Disco.Web; + using Disco.Web.Extensions; + + [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorGenerator", "1.5.0.0")] + [System.Web.WebPages.PageVirtualPathAttribute("~/Views/InitialConfig/_ViewStart.cshtml")] + public class ViewStart : System.Web.Mvc.ViewStartPage + { + public ViewStart() + { + } + public override void Execute() + { + + #line 1 "..\..\Views\InitialConfig\_ViewStart.cshtml" + + Layout = "~/Views/InitialConfig/_Layout.cshtml"; + Html.BundleDeferred("~/Style/InitialConfig"); + + + #line default + #line hidden + } + } +} +#pragma warning restore 1591 diff --git a/Disco.Web/Views/Job/Create-Old.cshtml b/Disco.Web/Views/Job/Create-Old.cshtml new file mode 100644 index 00000000..4936128a --- /dev/null +++ b/Disco.Web/Views/Job/Create-Old.cshtml @@ -0,0 +1,58 @@ +@model Disco.Web.Models.Job.CreateModel +@{ + ViewBag.Title = Html.ToBreadcrumb("Jobs", MVC.Job.Index(), "Create"); +} +@using (Html.BeginForm(MVC.Job.Create(), FormMethod.Post)) +{ + @Html.ValidationSummary(false) + @Html.HiddenFor(m => m.DeviceSerialNumber) + @Html.HiddenFor(m => m.UserId) +
+ + + + + + + + + @foreach (var jt in Model.JobTypes) + { + + + + + } +
+ @Html.Partial(MVC.Job.Views._CreateSubject, Model) +
+ Type: + + @CommonHelpers.RadioButtonList("Type", Model.JobTypes.ToSelectListItems(Model.Type), 2) +
+ @jt.Description
+ Sub Types +
+ @CommonHelpers.CheckBoxList("SubTypes", Model.JobSubTypes.Where(jst => jst.JobTypeId == jt.Id).ToList().ToSelectListItems(Model.SubTypes), 2) +
+

+ +

+ +
+} \ No newline at end of file diff --git a/Disco.Web/Views/Job/Create.cshtml b/Disco.Web/Views/Job/Create.cshtml new file mode 100644 index 00000000..c30239e7 --- /dev/null +++ b/Disco.Web/Views/Job/Create.cshtml @@ -0,0 +1,301 @@ +@model Disco.Web.Models.Job.CreateModel +@{ + Layout = "~/Views/Shared/_DialogLayout.cshtml"; + ViewBag.Title = Html.ToBreadcrumb("Jobs", MVC.Job.Index(), "Create"); +} +
+ @using (Html.BeginForm(MVC.Job.Create(), FormMethod.Post)) + { + @Html.HiddenFor(m => m.DeviceSerialNumber) + @Html.HiddenFor(m => m.UserId) + @Html.HiddenFor(m => m.QuickLogDestinationUrl) + + @Html.Partial(MVC.Job.Views._CreateSubject, Model) + @Html.ValidationSummary(true) + +
+
+

Type

+ @Html.ValidationMessageFor(m => m.Type) + @CommonHelpers.RadioButtonList("Type", Model.JobTypes.ToSelectListItems(Model.Type), 3) + @Html.ValidationMessageFor(m => m.SubTypes) +
+
+ @foreach (var jt in Model.JobTypes) + { +
+
+ @CommonHelpers.CheckBoxList("SubTypes", Model.JobSubTypes.Where(jst => jst.JobTypeId == jt.Id).ToList().ToSelectListItems(Model.SubTypes), 3) +
+
+ } +
+
+
+ @Html.ValidationMessageFor(m => m.DeviceHeld) + @Html.HiddenFor(m => m.DeviceHeld) + + + + + + +
+

Device Held

+
+ + + +
+
+
+ + + + + +
+

Comments

+
+ @Html.EditorFor(m => m.Comments) +
+
+
+
+

Quick Log

+ +
+
+

Task Time

+ @Html.ValidationMessageFor(m => m.QuickLogTaskTimeMinutes) + + + + + + + + Minutes + +
+
+ } + +
diff --git a/Disco.Web/Views/Job/Create.generated.cs b/Disco.Web/Views/Job/Create.generated.cs new file mode 100644 index 00000000..9457c343 --- /dev/null +++ b/Disco.Web/Views/Job/Create.generated.cs @@ -0,0 +1,598 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.17929 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Disco.Web.Views.Job +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using System.Web; + using System.Web.Helpers; + using System.Web.Mvc; + using System.Web.Mvc.Ajax; + using System.Web.Mvc.Html; + using System.Web.Routing; + using System.Web.Security; + using System.Web.UI; + using System.Web.WebPages; + using Disco.BI.Extensions; + using Disco.Models.Repository; + using Disco.Web; + using Disco.Web.Extensions; + + [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorGenerator", "1.5.0.0")] + [System.Web.WebPages.PageVirtualPathAttribute("~/Views/Job/Create.cshtml")] + public class Create : System.Web.Mvc.WebViewPage + { + public Create() + { + } + public override void Execute() + { + + #line 2 "..\..\Views\Job\Create.cshtml" + + Layout = "~/Views/Shared/_DialogLayout.cshtml"; + ViewBag.Title = Html.ToBreadcrumb("Jobs", MVC.Job.Index(), "Create"); + + + #line default + #line hidden +WriteLiteral("\r\n\r\n"); + + + #line 7 "..\..\Views\Job\Create.cshtml" + + + #line default + #line hidden + + #line 7 "..\..\Views\Job\Create.cshtml" + using (Html.BeginForm(MVC.Job.Create(), FormMethod.Post)) + { + + + #line default + #line hidden + + #line 9 "..\..\Views\Job\Create.cshtml" + Write(Html.HiddenFor(m => m.DeviceSerialNumber)); + + + #line default + #line hidden + + #line 9 "..\..\Views\Job\Create.cshtml" + + + + #line default + #line hidden + + #line 10 "..\..\Views\Job\Create.cshtml" + Write(Html.HiddenFor(m => m.UserId)); + + + #line default + #line hidden + + #line 10 "..\..\Views\Job\Create.cshtml" + + + + #line default + #line hidden + + #line 11 "..\..\Views\Job\Create.cshtml" + Write(Html.HiddenFor(m => m.QuickLogDestinationUrl)); + + + #line default + #line hidden + + #line 11 "..\..\Views\Job\Create.cshtml" + + + + + #line default + #line hidden + + #line 13 "..\..\Views\Job\Create.cshtml" + Write(Html.Partial(MVC.Job.Views._CreateSubject, Model)); + + + #line default + #line hidden + + #line 13 "..\..\Views\Job\Create.cshtml" + + + + #line default + #line hidden + + #line 14 "..\..\Views\Job\Create.cshtml" + Write(Html.ValidationSummary(true)); + + + #line default + #line hidden + + #line 14 "..\..\Views\Job\Create.cshtml" + + + + + #line default + #line hidden +WriteLiteral(" \r\n \r\n

Type

\r\n"); + +WriteLiteral(" "); + + + #line 19 "..\..\Views\Job\Create.cshtml" + Write(Html.ValidationMessageFor(m => m.Type)); + + + #line default + #line hidden +WriteLiteral("\r\n"); + +WriteLiteral(" "); + + + #line 20 "..\..\Views\Job\Create.cshtml" + Write(CommonHelpers.RadioButtonList("Type", Model.JobTypes.ToSelectListItems(Model.Type), 3)); + + + #line default + #line hidden +WriteLiteral("\r\n"); + +WriteLiteral(" "); + + + #line 21 "..\..\Views\Job\Create.cshtml" + Write(Html.ValidationMessageFor(m => m.SubTypes)); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n \r\n"); + + + #line 24 "..\..\Views\Job\Create.cshtml" + + + #line default + #line hidden + + #line 24 "..\..\Views\Job\Create.cshtml" + foreach (var jt in Model.JobTypes) + { + + + #line default + #line hidden +WriteLiteral(" (jt.Id + + #line default + #line hidden +, 1060), false) +); + +WriteLiteral(" class=\"createJob_SubType\""); + +WriteLiteral(">\r\n \r\n"); + +WriteLiteral(" "); + + + #line 28 "..\..\Views\Job\Create.cshtml" + Write(CommonHelpers.CheckBoxList("SubTypes", Model.JobSubTypes.Where(jst => jst.JobTypeId == jt.Id).ToList().ToSelectListItems(Model.SubTypes), 3)); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n \r\n"); + + + #line 31 "..\..\Views\Job\Create.cshtml" + } + + + #line default + #line hidden +WriteLiteral(" \r\n \r\n"); + +WriteLiteral(" \r\n"); + +WriteLiteral(" "); + + + #line 35 "..\..\Views\Job\Create.cshtml" + Write(Html.ValidationMessageFor(m => m.DeviceHeld)); + + + #line default + #line hidden +WriteLiteral("\r\n"); + +WriteLiteral(" "); + + + #line 36 "..\..\Views\Job\Create.cshtml" + Write(Html.HiddenFor(m => m.DeviceHeld)); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n \r\n \r\n " + +" \r\n \r\n \r\n " + +"
\r\n " + +"

Device Held

\r\n
\r\n Held\r\n \r\n " + +" Not Held\r\n
\r\n \r\n"); + +WriteLiteral(" \r\n \r\n \r\n \r\n " + +" \r\n \r\n
\r\n " + +"

Comments

\r\n
\r\n"); + +WriteLiteral(" "); + + + #line 58 "..\..\Views\Job\Create.cshtml" + Write(Html.EditorFor(m => m.Comments)); + + + #line default + #line hidden +WriteLiteral("\r\n
\r\n " + +" \r\n"); + +WriteLiteral(" \r\n \r\n

Quick Log

\r\n Automatically close this job\r\n \r\n \r\n

Task Time

\r\n"); + +WriteLiteral(" "); + + + #line 70 "..\..\Views\Job\Create.cshtml" + Write(Html.ValidationMessageFor(m => m.QuickLogTaskTimeMinutes)); + + + #line default + #line hidden +WriteLiteral("\r\n 10 Minutes\r\n 30 Minutes\r\n 1 Hour\r\n 2 Hours\r\n Other\r\n \r\n \r\n Minutes\r\n \r\n \r\n " + +" \r\n"); + + + #line 82 "..\..\Views\Job\Create.cshtml" + } + + + #line default + #line hidden +WriteLiteral(" \r\n $(function () {\r\n var discoDialogMethods;\r\n var " + +"init = true;\r\n //#region Parent Dialog\r\n if (window.parent" + +" && window.parent.document) {\r\n $(\'#QuickLogDestinationUrl\').val(" + +"window.parent.window.location.href);\r\n\r\n var parentDialog = $(\'#c" + +"reateJobDialog\', window.parent.document);\r\n if (parentDialog.leng" + +"th > 0) {\r\n discoDialogMethods = parentDialog[0].discoDialogM" + +"ethods;\r\n var buttons = {\r\n \"Create Jo" + +"b\": function () {\r\n createJobForm.submit()\r\n " + +" },\r\n Cancel: function () {\r\n " + +" discoDialogMethods.close();\r\n }\r\n " + +" }\r\n\r\n discoDialogMethods.setButtons(buttons);\r\n " + +" }\r\n }\r\n //#endregion\r\n\r\n\r\n var crea" + +"teJobForm = $(\'form\');\r\n var validator = createJobForm.data(\'validato" + +"r\');\r\n var unobtrusiveValidation = createJobForm.data(\'unobtrusiveVal" + +"idation\');\r\n\r\n // Validate all Fields\r\n validator.settings" + +".ignore = \'\';\r\n\r\n //#region Job Type/SubTypes\r\n var $jobTy" + +"peContainer = $(\'#createJob_Type\');\r\n var $typeValidationMessage = $(" + +"\'[data-valmsg-for=\"Type\"]\', $jobTypeContainer)\r\n var $subTypesValidat" + +"ionMessage = $(\'[data-valmsg-for=\"SubTypes\"]\', $jobTypeContainer)\r\n v" + +"ar $jobTypes = $jobTypeContainer.find(\'input[type=\"radio\"]\').change(jobTypeChang" + +"e);\r\n $(\'#createJob_SubTypes\').find(\'input[type=\"checkbox\"]\').change(" + +"jobSubTypeHighlight).each(jobSubTypeHighlight);\r\n jobTypeChange();\r\n " + +" function jobSubTypeHighlight() {\r\n var $this = $(this)" + +";\r\n if ($this.is(\':checked\'))\r\n $this.closest(" + +"\'li\').addClass(\'highlight\');\r\n else\r\n $this.cl" + +"osest(\'li\').removeClass(\'highlight\');\r\n }\r\n function jobTy" + +"peChange() {\r\n var $checkedItem = $jobTypes.filter(\':checked\');\r\n" + +"\r\n $jobTypes.closest(\'li\').removeClass(\'highlight\');\r\n\r\n " + +" $checkedItem.closest(\'li\').addClass(\'highlight\');\r\n\r\n if (" + +"init) {\r\n var jobType = $checkedItem.val();\r\n " + +" $(\'#createJob_SubType_\' + jobType).show();\r\n } else {\r\n " + +" $(\'#createJob_SubTypes\').find(\'.createJob_SubType:visible\').slideU" + +"p();\r\n var jobType = $checkedItem.val();\r\n " + +" $(\'#createJob_SubType_\' + jobType).slideDown();\r\n }\r\n " + +" }\r\n\r\n\r\n var additionalValidation = function (form) {\r\n " + +" var isValid = true;\r\n\r\n // Validate Type\r\n var t" + +"ypeValue = $jobTypes.filter(\':checked\').val();\r\n if (typeValue) {" + +"\r\n $typeValidationMessage.removeClass(\'field-validation-error" + +"\').addClass(\'field-validation-valid\');\r\n // Validate SubTypes" + +"\r\n if ($(\'#createJob_SubType_\' + typeValue).find(\'input:check" + +"ed\').length > 0) {\r\n $subTypesValidationMessage.removeCla" + +"ss(\'field-validation-error\').addClass(\'field-validation-valid\');\r\n " + +" } else {\r\n $subTypesValidationMessage.text(\'At leas" + +"t one Job Sub Type is required\').removeClass(\'field-validation-valid\').addClass(" + +"\'field-validation-error\');\r\n isValid = false;\r\n " + +" }\r\n } else {\r\n $typeValidationMessag" + +"e.text(\'A Job Type is required\').removeClass(\'field-validation-valid\').addClass(" + +"\'field-validation-error\');\r\n isValid = false;\r\n " + +" }\r\n\r\n // Validate QuickLog Task Time\r\n if ($quic" + +"kLog.is(\':checked\')) {\r\n var selectedTime = $quickLogTaskTime" + +"s.filter(\':checked\');\r\n if (selectedTime.length > 0) {\r\n " + +" if (selectedTime.val() === \'\') {\r\n " + +" // Handle \'Other\'\r\n var otherTime = parseInt($quickL" + +"ogTaskTimeOtherMinutes.val());\r\n if (!otherTime || ot" + +"herTime <= 0) {\r\n $quickLogTaskTimeValidationMess" + +"age.text(\'A Task Time is required\').removeClass(\'field-validation-valid\').addCla" + +"ss(\'field-validation-error\');\r\n isValid = false;\r" + +"\n }\r\n } else {\r\n " + +" $quickLogTaskTimeValidationMessage.removeClass(\'field-validation-v" + +"alid\').addClass(\'field-validation-error\');\r\n }\r\n " + +" } else {\r\n $quickLogTaskTimeValidationMessage." + +"text(\'A Task Time is required\').removeClass(\'field-validation-valid\').addClass(\'" + +"field-validation-error\');\r\n isValid = false;\r\n " + +" }\r\n } else {\r\n $quickLogTaskTimeValid" + +"ationMessage.removeClass(\'field-validation-valid\').addClass(\'field-validation-er" + +"ror\');\r\n }\r\n\r\n return isValid;\r\n }\r\n\r\n " + +" validator.settings.submitHandler = function (form) {\r\n " + +" if (additionalValidation()) {\r\n discoDialogMethods.setButton" + +"s({});\r\n form.submit();\r\n }\r\n }\r\n " + +" //#endregion\r\n\r\n //#region DeviceHeld\r\n var $dev" + +"iceHeld = $(\'#DeviceHeld\');\r\n\r\n if ($(\'#DeviceSerialNumber\').val()) {" + +"\r\n switch ($deviceHeld.val()) {\r\n case \'True\':" + +"\r\n $(\'#createJob_DeviceHeld\').attr(\'checked\', \'checked\');" + +"\r\n $(\'#createJob_DeviceNotHeld\').attr(\'checked\', null);\r\n" + +" break;\r\n case \'False\':\r\n " + +" $(\'#createJob_DeviceHeld\').attr(\'checked\', null);\r\n " + +" $(\'#createJob_DeviceNotHeld\').attr(\'checked\', \'checked\');\r\n " + +" break;\r\n default:\r\n $(\'#creat" + +"eJob_DeviceHeld\').attr(\'checked\', null);\r\n $(\'#createJob_" + +"DeviceNotHeld\').attr(\'checked\', null);\r\n break;\r\n " + +" }\r\n $(\'#createJob_DeviceHeldContainer\').find(\'input[type=" + +"\"radio\"]\').change(function () {\r\n // Update Hidden Field with" + +" Boolean Value\r\n // Set DeviceHeld\r\n var d" + +"eviceHeldValue = \'\';\r\n if ($(\'#createJob_DeviceHeld\').is(\':ch" + +"ecked\'))\r\n deviceHeldValue = \'True\';\r\n " + +" if ($(\'#createJob_DeviceNotHeld\').is(\':checked\'))\r\n devi" + +"ceHeldValue = \'False\';\r\n $deviceHeld.val(deviceHeldValue).cha" + +"nge();\r\n });\r\n } else {\r\n // No Device " + +"Associated\r\n $deviceHeld.val(\'False\');\r\n $(\'#creat" + +"eJob_DeviceHeldContainer\').hide();\r\n }\r\n //#endregion\r\n\r\n " + +" //#region QuickLog\r\n var $quickLog = $(\'#createJob_QuickLo" + +"g\');\r\n var $quickLogContainer = $(\'#createJob_QuickLogContainer\');\r\n " + +" var $quickLogTaskTimeContainer = $(\'#createJob_QuickLogTaskTimeContai" + +"ner\');\r\n var $quickLogTaskTimes = $quickLogTaskTimeContainer.find(\'in" + +"put[type=\"radio\"]\');\r\n var $quickLogTaskTimeOtherMinutes = $(\'#create" + +"Job_TaskTimeOtherMinutes\');\r\n var $quickLogTaskTimeValidationMessage " + +"= $quickLogTaskTimeContainer.find(\'[data-valmsg-for=\"QuickLogTaskTimeMinutes\"]\')" + +";\r\n\r\n $deviceHeld.change(validateQuickLog);\r\n $jobTypes.ch" + +"ange(validateQuickLog);\r\n validateQuickLog();\r\n\r\n function" + +" validateQuickLog() {\r\n var quickLogAllowed = false;\r\n\r\n " + +" if ($deviceHeld.val() === \'True\') {\r\n quickLogAllowed " + +"= false;\r\n } else {\r\n var selectedType = $jobT" + +"ypes.filter(\':checked\').val();\r\n switch (selectedType) {\r\n " + +" case \'HMisc\':\r\n case \'SApp\':\r\n " + +" case \'SImg\':\r\n case \'SOS\':\r\n " + +" case \'UMgmt\':\r\n quickLogAllowed = true;\r" + +"\n break;\r\n default:\r\n " + +" quickLogAllowed = false;\r\n break;\r" + +"\n }\r\n }\r\n\r\n if (quickLogAllowed" + +") {\r\n $quickLogContainer.slideDown();\r\n } else" + +" {\r\n if (init)\r\n $quickLogContainer.hi" + +"de();\r\n else\r\n $quickLogContainer.slid" + +"eUp();\r\n $quickLog.attr(\'checked\', null).change();\r\n " + +" }\r\n }\r\n\r\n $quickLog.change(function () {\r\n " + +" if ($(this).is(\':checked\')) {\r\n $quickLogTaskTimeConta" + +"iner.slideDown();\r\n } else {\r\n $quickLogTaskTi" + +"meContainer.slideUp();\r\n }\r\n });\r\n\r\n $quick" + +"LogTaskTimes.change(function () {\r\n if ($quickLogTaskTimes.filter" + +"(\':checked\').val() === \"\") {\r\n $(\'#createJob_TaskTimeOtherMin" + +"utesContainer\').show();\r\n $quickLogTaskTimeOtherMinutes.attr(" + +"\'disabled\', null).focus().select();\r\n } else {\r\n " + +" $(\'#createJob_TaskTimeOtherMinutesContainer\').hide();\r\n $q" + +"uickLogTaskTimeOtherMinutes.attr(\'disabled\', \'disabled\');\r\n }\r\n " + +" });\r\n //#endregion\r\n\r\n init = false;\r\n })" + +";\r\n \r\n\r\n"); + + } + } +} +#pragma warning restore 1591 diff --git a/Disco.Web/Views/Job/Create_Redirect.cshtml b/Disco.Web/Views/Job/Create_Redirect.cshtml new file mode 100644 index 00000000..0603bbbb --- /dev/null +++ b/Disco.Web/Views/Job/Create_Redirect.cshtml @@ -0,0 +1,21 @@ +@model Disco.Web.Models.Job.CreateRedirectModel +@{ + Layout = null; + ViewBag.Title = Html.ToBreadcrumb("Jobs", MVC.Job.Index(), "Create - Redirecting..."); +} +Redirecting... + diff --git a/Disco.Web/Views/Job/Create_Redirect.generated.cs b/Disco.Web/Views/Job/Create_Redirect.generated.cs new file mode 100644 index 00000000..6b7a7188 --- /dev/null +++ b/Disco.Web/Views/Job/Create_Redirect.generated.cs @@ -0,0 +1,87 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.17929 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Disco.Web.Views.Job +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using System.Web; + using System.Web.Helpers; + using System.Web.Mvc; + using System.Web.Mvc.Ajax; + using System.Web.Mvc.Html; + using System.Web.Routing; + using System.Web.Security; + using System.Web.UI; + using System.Web.WebPages; + using Disco.BI.Extensions; + using Disco.Models.Repository; + using Disco.Web; + using Disco.Web.Extensions; + + [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorGenerator", "1.5.0.0")] + [System.Web.WebPages.PageVirtualPathAttribute("~/Views/Job/Create_Redirect.cshtml")] + public class Create_Redirect : System.Web.Mvc.WebViewPage + { + public Create_Redirect() + { + } + public override void Execute() + { + + #line 2 "..\..\Views\Job\Create_Redirect.cshtml" + + Layout = null; + ViewBag.Title = Html.ToBreadcrumb("Jobs", MVC.Job.Index(), "Create - Redirecting..."); + + + #line default + #line hidden +WriteLiteral("\r\n(Model.RedirectLink + + #line default + #line hidden +, 195), false) +); + +WriteLiteral(@">Redirecting... + +"); + + } + } +} +#pragma warning restore 1591 diff --git a/Disco.Web/Views/Job/Index.cshtml b/Disco.Web/Views/Job/Index.cshtml new file mode 100644 index 00000000..0d3fc80c --- /dev/null +++ b/Disco.Web/Views/Job/Index.cshtml @@ -0,0 +1,131 @@ +@model Disco.Web.Models.Job.IndexModel +@{ + ViewBag.Title = "Jobs"; + Html.BundleDeferred("~/ClientScripts/Modules/Highcharts"); +} +
+
+

Search Jobs

+ @Html.Partial(MVC.Shared.Views._SearchDialog, "jobs") +
+
+

Daily Opened & Closed Jobs

+
+
+ +
+
+

Open Jobs Awaiting Technician Action (@Model.OpenJobs.Items.Count)

+@Html.Partial(MVC.Shared.Views._JobTable, Model.OpenJobs, new ViewDataDictionary()) +

Long Running Jobs (@Model.LongRunningJobs.Items.Count)

+@Html.Partial(MVC.Shared.Views._JobTable, Model.LongRunningJobs, new ViewDataDictionary()) +@*

+ Jobs with Devices Ready for Return (@Model.ReadyForReturnJobs.Items.Count)

+@Html.Partial(MVC.Shared.Views._JobTable, Model.ReadyForReturnJobs, new ViewDataDictionary()) +

+ Jobs Waiting for User Action (@Model.WaitingForUserActionJobs.Items.Count)

+@Html.Partial(MVC.Shared.Views._JobTable, Model.WaitingForUserActionJobs, new ViewDataDictionary()) +

+ Recently Closed Jobs (@Model.RecentlyClosedJobs.Items.Count)

+@Html.Partial(MVC.Shared.Views._JobTable, Model.RecentlyClosedJobs, new ViewDataDictionary())*@ diff --git a/Disco.Web/Views/Job/Index.generated.cs b/Disco.Web/Views/Job/Index.generated.cs new file mode 100644 index 00000000..8b9d4728 --- /dev/null +++ b/Disco.Web/Views/Job/Index.generated.cs @@ -0,0 +1,191 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.17929 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Disco.Web.Views.Job +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using System.Web; + using System.Web.Helpers; + using System.Web.Mvc; + using System.Web.Mvc.Ajax; + using System.Web.Mvc.Html; + using System.Web.Routing; + using System.Web.Security; + using System.Web.UI; + using System.Web.WebPages; + using Disco.BI.Extensions; + using Disco.Models.Repository; + using Disco.Web; + using Disco.Web.Extensions; + + [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorGenerator", "1.5.0.0")] + [System.Web.WebPages.PageVirtualPathAttribute("~/Views/Job/Index.cshtml")] + public class Index : System.Web.Mvc.WebViewPage + { + public Index() + { + } + public override void Execute() + { + + #line 2 "..\..\Views\Job\Index.cshtml" + + ViewBag.Title = "Jobs"; + Html.BundleDeferred("~/ClientScripts/Modules/Highcharts"); + + + #line default + #line hidden +WriteLiteral("\r\n\r\n \r\n

Search Jobs

\r\n"); + +WriteLiteral(" "); + + + #line 9 "..\..\Views\Job\Index.cshtml" + Write(Html.Partial(MVC.Shared.Views._SearchDialog, "jobs")); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n \r\n

Daily Opened & Closed Jobs

\r\n \r\n \r\n \r\n (function () {\r\n\r\n var chartData;\r\n\r\n " + +" function buildChart() {\r\n $(function () {\r\n\r\n " + +" var data = chartData;\r\n\r\n var dataTotalOpenJobs" + +" = [];\r\n var dataOpenedJobs = [];\r\n " + +" var dataClosedJobs = [];\r\n for (var i = 0; i < data.len" + +"gth; i++) {\r\n var dataItem = data[i];\r\n " + +" var dataItemDate = new Date(parseInt(dataItem.Timestamp.substr(6, " + +"dataItem.Timestamp.length - 8))).getTime(); // $.datepicker.parseDate(\'yy-mm-dd\'" + +", dataItem.Timestamp.substr(0, 10)).getTime();\r\n data" + +"TotalOpenJobs.push([dataItemDate, dataItem.TotalJobs]);\r\n " + +" dataOpenedJobs.push([dataItemDate, dataItem.OpenedJobs]);\r\n " + +" dataClosedJobs.push([dataItemDate, dataItem.ClosedJobs]);\r\n " + +" }\r\n Highcharts.setOptions({\r\n " + +" global: {\r\n useUTC: false\r\n " + +" }\r\n });\r\n new" + +" Highcharts.Chart({\r\n chart: {\r\n " + +" renderTo: \'chartHostJobDailyOpenedClosed\',\r\n " + +" height: 175,\r\n animation: false\r\n " + +" },\r\n colors: [\'#BBBBBB\', \'#005fab\'" + +", \'#DB761D\'],\r\n title: {\r\n " + +" text: null\r\n },\r\n plo" + +"tOptions: {\r\n series: {\r\n " + +" marker: {\r\n radius: 3\r\n " + +" },\r\n animation:" + +" false\r\n }\r\n },\r\n " + +" legend: {\r\n align: \'left\'," + +"\r\n verticalAlign: \'top\',\r\n " + +" y: 0,\r\n floating: true,\r\n " + +" borderWidth: 0\r\n },\r\n " + +" xAxis: {\r\n type: \'datetime\',\r\n " + +" tickInterval: 7 * 24 * 3600 * 1000, // week\r\n " + +" tickWidth: 1,\r\n gridLineWi" + +"dth: 1,\r\n dateTimeLabelFormats: {\r\n " + +" week: \'%e %b\'\r\n }\r\n " + +" },\r\n yAxis: [{\r\n " + +" title: {\r\n text: null\r\n " + +" },\r\n labels: {\r\n " + +" enabled: false\r\n },\r\n" + +" min: 0\r\n }, {\r\n " + +" title: {\r\n text: nu" + +"ll\r\n },\r\n labels: " + +"{\r\n enabled: false\r\n " + +" },\r\n min: 0\r\n }" + +"],\r\n series: [{\r\n name" + +": \'Total Open Jobs\',\r\n data: dataTotalOpenJobs,\r\n" + +" yAxis: 1\r\n }, {\r\n " + +" name: \'Closed Jobs\',\r\n " + +" data: dataClosedJobs\r\n }, {\r\n " + +" name: \'Opened Jobs\',\r\n data: dataOpened" + +"Jobs\r\n }],\r\n credits: {\r\n " + +" enabled: false\r\n }\r\n " + +" });\r\n });\r\n }\r\n\r\n\r\n " + +" $.getJSON(\'"); + + + #line 110 "..\..\Views\Job\Index.cshtml" + Write(Url.Action(MVC.API.Job.StatisticsDailyOpenedClosed())); + + + #line default + #line hidden +WriteLiteral("\', function (data) {\r\n chartData = data;\r\n " + +"buildChart();\r\n });\r\n }());\r\n\r\n \r\n " + +"\r\n\r\n

Open Jobs Awaiting Technician Action ("); + + + #line 119 "..\..\Views\Job\Index.cshtml" + Write(Model.OpenJobs.Items.Count); + + + #line default + #line hidden +WriteLiteral(")

\r\n"); + + + #line 120 "..\..\Views\Job\Index.cshtml" +Write(Html.Partial(MVC.Shared.Views._JobTable, Model.OpenJobs, new ViewDataDictionary())); + + + #line default + #line hidden +WriteLiteral("\r\n

Long Running Jobs ("); + + + #line 121 "..\..\Views\Job\Index.cshtml" + Write(Model.LongRunningJobs.Items.Count); + + + #line default + #line hidden +WriteLiteral(")

\r\n"); + + + #line 122 "..\..\Views\Job\Index.cshtml" +Write(Html.Partial(MVC.Shared.Views._JobTable, Model.LongRunningJobs, new ViewDataDictionary())); + + + #line default + #line hidden +WriteLiteral("\r\n"); + +WriteLiteral("\r\n"); + + } + } +} +#pragma warning restore 1591 diff --git a/Disco.Web/Views/Job/JobParts/Components.cshtml b/Disco.Web/Views/Job/JobParts/Components.cshtml new file mode 100644 index 00000000..8add3f6c --- /dev/null +++ b/Disco.Web/Views/Job/JobParts/Components.cshtml @@ -0,0 +1,184 @@ +@model Disco.Web.Models.Job.ShowModel +@{ + Html.BundleDeferred("~/ClientScripts/Modules/jQuery-NumberFormatter"); + } + + + + + + + @foreach (var jc in Model.Job.JobComponents) + { + + + + + + } + + + + +
+ Description + + Cost + +   +
+ + + + + +
+ Add Component + + Total: +
+
+

+ + Are you sure?

+
+ diff --git a/Disco.Web/Views/Job/JobParts/Components.generated.cs b/Disco.Web/Views/Job/JobParts/Components.generated.cs new file mode 100644 index 00000000..d6390244 --- /dev/null +++ b/Disco.Web/Views/Job/JobParts/Components.generated.cs @@ -0,0 +1,294 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.17929 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Disco.Web.Views.Job.JobParts +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using System.Web; + using System.Web.Helpers; + using System.Web.Mvc; + using System.Web.Mvc.Ajax; + using System.Web.Mvc.Html; + using System.Web.Routing; + using System.Web.Security; + using System.Web.UI; + using System.Web.WebPages; + using Disco.BI.Extensions; + using Disco.Models.Repository; + using Disco.Web; + using Disco.Web.Extensions; + + [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorGenerator", "1.5.0.0")] + [System.Web.WebPages.PageVirtualPathAttribute("~/Views/Job/JobParts/Components.cshtml")] + public class Components : System.Web.Mvc.WebViewPage + { + public Components() + { + } + public override void Execute() + { + + #line 2 "..\..\Views\Job\JobParts\Components.cshtml" + + Html.BundleDeferred("~/ClientScripts/Modules/jQuery-NumberFormatter"); + + + #line default + #line hidden +WriteLiteral("\r\n\r\n \r\n \r\n Description\r\n \r\n \r\n" + +" Cost\r\n \r\n \r\n  \r\n \r\n \r\n"); + + + #line 17 "..\..\Views\Job\JobParts\Components.cshtml" + + + #line default + #line hidden + + #line 17 "..\..\Views\Job\JobParts\Components.cshtml" + foreach (var jc in Model.Job.JobComponents) + { + + + #line default + #line hidden +WriteLiteral("
\r\n\r\n"); + + } + } +} +#pragma warning restore 1591 diff --git a/Disco.Web/Views/Shared/_EmptyLayout.cshtml b/Disco.Web/Views/Shared/_EmptyLayout.cshtml new file mode 100644 index 00000000..aac3831a --- /dev/null +++ b/Disco.Web/Views/Shared/_EmptyLayout.cshtml @@ -0,0 +1,21 @@ +@{ + Html.BundleDeferred("~/Style/Site"); + Html.BundleDeferred("~/ClientScripts/Core"); +} + + + + Disco - @CommonHelpers.BreadcrumbsTitle(ViewBag.Title) + + + + + @Html.BundleRenderDeferred() + @RenderSection("head", false) + + +
+ @RenderBody() +
+ + diff --git a/Disco.Web/Views/Shared/_EmptyLayout.generated.cs b/Disco.Web/Views/Shared/_EmptyLayout.generated.cs new file mode 100644 index 00000000..bea714f3 --- /dev/null +++ b/Disco.Web/Views/Shared/_EmptyLayout.generated.cs @@ -0,0 +1,131 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.17929 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Disco.Web.Views.Shared +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using System.Web; + using System.Web.Helpers; + using System.Web.Mvc; + using System.Web.Mvc.Ajax; + using System.Web.Mvc.Html; + using System.Web.Routing; + using System.Web.Security; + using System.Web.UI; + using System.Web.WebPages; + using Disco.BI.Extensions; + using Disco.Models.Repository; + using Disco.Web; + using Disco.Web.Extensions; + + [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorGenerator", "1.5.0.0")] + [System.Web.WebPages.PageVirtualPathAttribute("~/Views/Shared/_EmptyLayout.cshtml")] + public class EmptyLayout : System.Web.Mvc.WebViewPage + { + public EmptyLayout() + { + } + public override void Execute() + { + + #line 1 "..\..\Views\Shared\_EmptyLayout.cshtml" + + Html.BundleDeferred("~/Style/Site"); + Html.BundleDeferred("~/ClientScripts/Core"); + + + #line default + #line hidden +WriteLiteral("\r\n\r\n\r\n\r\n Disco - "); + + + #line 8 "..\..\Views\Shared\_EmptyLayout.cshtml" + Write(CommonHelpers.BreadcrumbsTitle(ViewBag.Title)); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n \r\n \r\n \r\n"); + +WriteLiteral(" "); + + + #line 13 "..\..\Views\Shared\_EmptyLayout.cshtml" +Write(Html.BundleRenderDeferred()); + + + #line default + #line hidden +WriteLiteral("\r\n"); + +WriteLiteral(" "); + + + #line 14 "..\..\Views\Shared\_EmptyLayout.cshtml" +Write(RenderSection("head", false)); + + + #line default + #line hidden +WriteLiteral("\r\n\r\n\r\n \r\n"); + +WriteLiteral(" "); + + + #line 18 "..\..\Views\Shared\_EmptyLayout.cshtml" + Write(RenderBody()); + + + #line default + #line hidden +WriteLiteral("\r\n
\r\n\r\n\r\n"); + + } + } +} +#pragma warning restore 1591 diff --git a/Disco.Web/Views/Shared/_JobTable.cshtml b/Disco.Web/Views/Shared/_JobTable.cshtml new file mode 100644 index 00000000..eb8e2a95 --- /dev/null +++ b/Disco.Web/Views/Shared/_JobTable.cshtml @@ -0,0 +1,27 @@ +@model Disco.Models.BI.Job.JobTableModel +@if (DiscoApplication.MultiSiteMode) +{ + if (Model == null || Model.Items == null || Model.Items.Count == 0) + { + No Jobs Found + } + else + { + var modelItems = Model.Items; + var modelItemsGrouped = modelItems.OrderBy(i => i.DeviceAddress).GroupBy(i => i.DeviceAddress); + foreach (var modelItemsGroup in modelItemsGrouped) + { + Model.Items = modelItemsGroup.ToList(); + if (modelItemsGroup.Key != null) + { +

+ @modelItemsGroup.Key

+ } + @Html.Partial(MVC.Shared.Views._JobTableRender, Model, new ViewDataDictionary()) + } + } +} +else +{ + @Html.Partial(MVC.Shared.Views._JobTableRender, Model, new ViewDataDictionary()) +} \ No newline at end of file diff --git a/Disco.Web/Views/Shared/_JobTable.generated.cs b/Disco.Web/Views/Shared/_JobTable.generated.cs new file mode 100644 index 00000000..6a2528df --- /dev/null +++ b/Disco.Web/Views/Shared/_JobTable.generated.cs @@ -0,0 +1,131 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.17929 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Disco.Web.Views.Shared +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using System.Web; + using System.Web.Helpers; + using System.Web.Mvc; + using System.Web.Mvc.Ajax; + using System.Web.Mvc.Html; + using System.Web.Routing; + using System.Web.Security; + using System.Web.UI; + using System.Web.WebPages; + using Disco.BI.Extensions; + using Disco.Models.Repository; + using Disco.Web; + using Disco.Web.Extensions; + + [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorGenerator", "1.5.0.0")] + [System.Web.WebPages.PageVirtualPathAttribute("~/Views/Shared/_JobTable.cshtml")] + public class JobTable : System.Web.Mvc.WebViewPage + { + public JobTable() + { + } + public override void Execute() + { + + #line 2 "..\..\Views\Shared\_JobTable.cshtml" + if (DiscoApplication.MultiSiteMode) +{ + if (Model == null || Model.Items == null || Model.Items.Count == 0) + { + + + #line default + #line hidden +WriteLiteral(" No Jobs Found\r\n"); + + + #line 7 "..\..\Views\Shared\_JobTable.cshtml" + } + else + { + var modelItems = Model.Items; + var modelItemsGrouped = modelItems.OrderBy(i => i.DeviceAddress).GroupBy(i => i.DeviceAddress); + foreach (var modelItemsGroup in modelItemsGrouped) + { + Model.Items = modelItemsGroup.ToList(); + if (modelItemsGroup.Key != null) + { + + + #line default + #line hidden +WriteLiteral("

\r\n"); + +WriteLiteral(" "); + + + #line 18 "..\..\Views\Shared\_JobTable.cshtml" + Write(modelItemsGroup.Key); + + + #line default + #line hidden +WriteLiteral("

\r\n"); + + + #line 19 "..\..\Views\Shared\_JobTable.cshtml" + } + + + #line default + #line hidden + + #line 20 "..\..\Views\Shared\_JobTable.cshtml" +Write(Html.Partial(MVC.Shared.Views._JobTableRender, Model, new ViewDataDictionary())); + + + #line default + #line hidden + + #line 20 "..\..\Views\Shared\_JobTable.cshtml" + + } + } +} +else +{ + + + #line default + #line hidden + + #line 26 "..\..\Views\Shared\_JobTable.cshtml" +Write(Html.Partial(MVC.Shared.Views._JobTableRender, Model, new ViewDataDictionary())); + + + #line default + #line hidden + + #line 26 "..\..\Views\Shared\_JobTable.cshtml" + +} + + #line default + #line hidden + } + } +} +#pragma warning restore 1591 diff --git a/Disco.Web/Views/Shared/_JobTableRender.cshtml b/Disco.Web/Views/Shared/_JobTableRender.cshtml new file mode 100644 index 00000000..81bb7c9a --- /dev/null +++ b/Disco.Web/Views/Shared/_JobTableRender.cshtml @@ -0,0 +1,108 @@ +@model Disco.Models.BI.Job.JobTableModel +@{ + Html.BundleDeferred("~/ClientScripts/Modules/Disco-DataTableHelpers"); +} +
+ @if (Model != null && Model.Items.Count() > 0) + { + + + + @if (Model.ShowId) + { } + @if (Model.ShowStatus) + { } + @if (Model.ShowDates) + { } + @if (Model.ShowType) + { } + @if (Model.ShowDevice) + { } + @if (Model.ShowUser) + { } + @if (Model.ShowTechnician) + { } + @if (Model.ShowLocation) + { } + + + + @foreach (var item in Model.Items) + { + + @if (Model.ShowId) + { } + @if (Model.ShowStatus) + { } + @if (Model.ShowDates) + { } + @if (Model.ShowType) + { } + @if (Model.ShowDevice) + { } + @if (Model.ShowUser) + {} + @if (Model.ShowTechnician) + { } + @if (Model.ShowLocation) + { } + + } + +
+ Ref + + Status + + Dates + + Type + + Device + + User + + Technician + + Location +
+ @Html.ActionLink(item.Id.ToString(), MVC.Job.Show(item.Id)) + + + @item.StatusDescription + + @CommonHelpers.FriendlyDate(item.OpenedDate) + - @CommonHelpers.FriendlyDate(item.ClosedDate) + + @item.TypeId + + @if (item.DeviceSerialNumber != null) + { + @Html.ActionLink(item.DeviceSerialNumber, MVC.Device.Show(item.DeviceSerialNumber), new { Title = item.DeviceModelDescription }) + } + else + { + N/A + } + + @if (item.UserId != null) + { + @Html.ActionLink(string.Format("{0} ({1})", item.UserDisplayName, item.UserId), MVC.User.Show(item.UserId)) + } + else + { + N/A + } + + @item.OpenedTechUserId + + + @(item.Location ?? "Unknown") + +
+ } + else + { + No Jobs Found + } +
diff --git a/Disco.Web/Views/Shared/_JobTableRender.generated.cs b/Disco.Web/Views/Shared/_JobTableRender.generated.cs new file mode 100644 index 00000000..d0b906a3 --- /dev/null +++ b/Disco.Web/Views/Shared/_JobTableRender.generated.cs @@ -0,0 +1,732 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.17929 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Disco.Web.Views.Shared +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using System.Web; + using System.Web.Helpers; + using System.Web.Mvc; + using System.Web.Mvc.Ajax; + using System.Web.Mvc.Html; + using System.Web.Routing; + using System.Web.Security; + using System.Web.UI; + using System.Web.WebPages; + using Disco.BI.Extensions; + using Disco.Models.Repository; + using Disco.Web; + using Disco.Web.Extensions; + + [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorGenerator", "1.5.0.0")] + [System.Web.WebPages.PageVirtualPathAttribute("~/Views/Shared/_JobTableRender.cshtml")] + public class JobTableRender : System.Web.Mvc.WebViewPage + { + public JobTableRender() + { + } + public override void Execute() + { + + #line 2 "..\..\Views\Shared\_JobTableRender.cshtml" + + Html.BundleDeferred("~/ClientScripts/Modules/Disco-DataTableHelpers"); + + + #line default + #line hidden +WriteLiteral("\r\n\r\n"); + + + #line 6 "..\..\Views\Shared\_JobTableRender.cshtml" + + + #line default + #line hidden + + #line 6 "..\..\Views\Shared\_JobTableRender.cshtml" + if (Model != null && Model.Items.Count() > 0) + { + + + #line default + #line hidden +WriteLiteral(" (Model.IsSmallTable ? " smallTable" : string.Empty + + #line default + #line hidden +, 239), false) + + #line 8 "..\..\Views\Shared\_JobTableRender.cshtml" + , Tuple.Create(Tuple.Create("", 291), Tuple.Create(Model.HideClosedJobs ? " hideStatusClosed" : string.Empty + + #line default + #line hidden +, 291), false) +); + +WriteLiteral(">\r\n \r\n \r\n"); + + + #line 11 "..\..\Views\Shared\_JobTableRender.cshtml" + + + #line default + #line hidden + + #line 11 "..\..\Views\Shared\_JobTableRender.cshtml" + if (Model.ShowId) + { + + #line default + #line hidden +WriteLiteral(" \r\n Ref\r\n "); + + + #line 14 "..\..\Views\Shared\_JobTableRender.cshtml" + } + + + #line default + #line hidden +WriteLiteral(" "); + + + #line 15 "..\..\Views\Shared\_JobTableRender.cshtml" + if (Model.ShowStatus) + { + + #line default + #line hidden +WriteLiteral(" \r\n Status\r\n "); + + + #line 18 "..\..\Views\Shared\_JobTableRender.cshtml" + } + + + #line default + #line hidden +WriteLiteral(" "); + + + #line 19 "..\..\Views\Shared\_JobTableRender.cshtml" + if (Model.ShowDates) + { + + #line default + #line hidden +WriteLiteral(" \r\n Dates\r\n "); + + + #line 22 "..\..\Views\Shared\_JobTableRender.cshtml" + } + + + #line default + #line hidden +WriteLiteral(" "); + + + #line 23 "..\..\Views\Shared\_JobTableRender.cshtml" + if (Model.ShowType) + { + + #line default + #line hidden +WriteLiteral(" \r\n Type\r\n "); + + + #line 26 "..\..\Views\Shared\_JobTableRender.cshtml" + } + + + #line default + #line hidden +WriteLiteral(" "); + + + #line 27 "..\..\Views\Shared\_JobTableRender.cshtml" + if (Model.ShowDevice) + { + + #line default + #line hidden +WriteLiteral("\r\n Device\r\n "); + + + #line 30 "..\..\Views\Shared\_JobTableRender.cshtml" + } + + + #line default + #line hidden +WriteLiteral(" "); + + + #line 31 "..\..\Views\Shared\_JobTableRender.cshtml" + if (Model.ShowUser) + { + + #line default + #line hidden +WriteLiteral(" \r\n User\r\n "); + + + #line 34 "..\..\Views\Shared\_JobTableRender.cshtml" + } + + + #line default + #line hidden +WriteLiteral(" "); + + + #line 35 "..\..\Views\Shared\_JobTableRender.cshtml" + if (Model.ShowTechnician) + { + + #line default + #line hidden +WriteLiteral(" \r\n Technician\r\n "); + + + #line 38 "..\..\Views\Shared\_JobTableRender.cshtml" + } + + + #line default + #line hidden +WriteLiteral(" "); + + + #line 39 "..\..\Views\Shared\_JobTableRender.cshtml" + if (Model.ShowLocation) + { + + #line default + #line hidden +WriteLiteral(" \r\n Location\r\n "); + + + #line 42 "..\..\Views\Shared\_JobTableRender.cshtml" + } + + + #line default + #line hidden +WriteLiteral(" \r\n \r\n \r\n"); + + + #line 46 "..\..\Views\Shared\_JobTableRender.cshtml" + + + #line default + #line hidden + + #line 46 "..\..\Views\Shared\_JobTableRender.cshtml" + foreach (var item in Model.Items) + { + + + #line default + #line hidden +WriteLiteral("
\r\n
\r\n Disco v"); + + + #line 103 "..\..\Views\Shared\_Layout.cshtml" + Write(Disco.Web.DiscoApplication.Version); + + + #line default + #line hidden +WriteLiteral(" "); + +WriteLiteral("@ "); + + + #line 103 "..\..\Views\Shared\_Layout.cshtml" + Write(Disco.Web.DiscoApplication.OrganisationName); + + + #line default + #line hidden +WriteLiteral(" | discoict.co" + +"m.au | "); + + + #line 104 "..\..\Views\Shared\_Layout.cshtml" + Write(Html.ActionLink("Credits", MVC.Public.Public.Credits())); + + + #line default + #line hidden +WriteLiteral(" | "); + + + #line 104 "..\..\Views\Shared\_Layout.cshtml" + Write(Html.ActionLink("Licence", MVC.Public.Public.Licence())); + + + #line default + #line hidden +WriteLiteral("\r\n
\r\n \r\n\r\n\r\n"); + + } + } +} +#pragma warning restore 1591 diff --git a/Disco.Web/Views/Shared/_SearchDialog.cshtml b/Disco.Web/Views/Shared/_SearchDialog.cshtml new file mode 100644 index 00000000..6f6450cb --- /dev/null +++ b/Disco.Web/Views/Shared/_SearchDialog.cshtml @@ -0,0 +1,35 @@ +@model string +@using (Html.BeginForm(MVC.Search.Query(), FormMethod.Get)) +{ + @Html.ValidationSummary(true) +
+ + + + + +
+ Query: + + @Html.TextBox("term") + @if (Model != null) + { + @Html.Hidden("limit", Model) + if (Model == "jobs") + { +
+ + } + } +
+

+ +

+ +
+} \ No newline at end of file diff --git a/Disco.Web/Views/Shared/_SearchDialog.generated.cs b/Disco.Web/Views/Shared/_SearchDialog.generated.cs new file mode 100644 index 00000000..7821beff --- /dev/null +++ b/Disco.Web/Views/Shared/_SearchDialog.generated.cs @@ -0,0 +1,169 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.17929 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Disco.Web.Views.Shared +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using System.Web; + using System.Web.Helpers; + using System.Web.Mvc; + using System.Web.Mvc.Ajax; + using System.Web.Mvc.Html; + using System.Web.Routing; + using System.Web.Security; + using System.Web.UI; + using System.Web.WebPages; + using Disco.BI.Extensions; + using Disco.Models.Repository; + using Disco.Web; + using Disco.Web.Extensions; + + [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorGenerator", "1.5.0.0")] + [System.Web.WebPages.PageVirtualPathAttribute("~/Views/Shared/_SearchDialog.cshtml")] + public class SearchDialog : System.Web.Mvc.WebViewPage + { + public SearchDialog() + { + } + public override void Execute() + { + + #line 2 "..\..\Views\Shared\_SearchDialog.cshtml" + using (Html.BeginForm(MVC.Search.Query(), FormMethod.Get)) +{ + + + #line default + #line hidden + + #line 4 "..\..\Views\Shared\_SearchDialog.cshtml" +Write(Html.ValidationSummary(true)); + + + #line default + #line hidden + + #line 4 "..\..\Views\Shared\_SearchDialog.cshtml" + + + + #line default + #line hidden +WriteLiteral(" \r\n \r\n \r\n \r\n \r\n \r\n
\r\n Q" + +"uery:\r\n \r\n"); + +WriteLiteral(" "); + + + #line 12 "..\..\Views\Shared\_SearchDialog.cshtml" + Write(Html.TextBox("term")); + + + #line default + #line hidden +WriteLiteral("\r\n"); + + + #line 13 "..\..\Views\Shared\_SearchDialog.cshtml" + + + #line default + #line hidden + + #line 13 "..\..\Views\Shared\_SearchDialog.cshtml" + if (Model != null) + { + + + #line default + #line hidden + + #line 15 "..\..\Views\Shared\_SearchDialog.cshtml" + Write(Html.Hidden("limit", Model)); + + + #line default + #line hidden + + #line 15 "..\..\Views\Shared\_SearchDialog.cshtml" + + if (Model == "jobs") + { + + + #line default + #line hidden +WriteLiteral("
\r\n"); + +WriteLiteral(" "); + +WriteLiteral("Search Details\r\n"); + + + #line 21 "..\..\Views\Shared\_SearchDialog.cshtml" + } + } + + + #line default + #line hidden +WriteLiteral("
\r\n \r\n \r\n

\r\n \r\n $(function () {\r\n $(\'#searchDialog\').find(\'#term\')." + +"watermark(\'Search\').focus();\r\n });\r\n \r\n \r\n"); + + + #line 35 "..\..\Views\Shared\_SearchDialog.cshtml" +} + + #line default + #line hidden + } + } +} +#pragma warning restore 1591 diff --git a/Disco.Web/Views/User/Index.cshtml b/Disco.Web/Views/User/Index.cshtml new file mode 100644 index 00000000..2fe96a85 --- /dev/null +++ b/Disco.Web/Views/User/Index.cshtml @@ -0,0 +1,6 @@ +@{ + ViewBag.Title = "Users"; +} +

+ Search for a User

+@Html.Partial(MVC.Shared.Views._SearchDialog, "users") diff --git a/Disco.Web/Views/User/Index.generated.cs b/Disco.Web/Views/User/Index.generated.cs new file mode 100644 index 00000000..9df96d91 --- /dev/null +++ b/Disco.Web/Views/User/Index.generated.cs @@ -0,0 +1,65 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.17929 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Disco.Web.Views.User +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using System.Web; + using System.Web.Helpers; + using System.Web.Mvc; + using System.Web.Mvc.Ajax; + using System.Web.Mvc.Html; + using System.Web.Routing; + using System.Web.Security; + using System.Web.UI; + using System.Web.WebPages; + using Disco.BI.Extensions; + using Disco.Models.Repository; + using Disco.Web; + using Disco.Web.Extensions; + + [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorGenerator", "1.5.0.0")] + [System.Web.WebPages.PageVirtualPathAttribute("~/Views/User/Index.cshtml")] + public class Index : System.Web.Mvc.WebViewPage + { + public Index() + { + } + public override void Execute() + { + + #line 1 "..\..\Views\User\Index.cshtml" + + ViewBag.Title = "Users"; + + + #line default + #line hidden +WriteLiteral("\r\n

\r\n Search for a User

\r\n"); + + + #line 6 "..\..\Views\User\Index.cshtml" +Write(Html.Partial(MVC.Shared.Views._SearchDialog, "users")); + + + #line default + #line hidden +WriteLiteral("\r\n"); + + } + } +} +#pragma warning restore 1591 diff --git a/Disco.Web/Views/User/Show.cshtml b/Disco.Web/Views/User/Show.cshtml new file mode 100644 index 00000000..82a6fb71 --- /dev/null +++ b/Disco.Web/Views/User/Show.cshtml @@ -0,0 +1,85 @@ +@model Disco.Web.Models.User.ShowModel +@{ + ViewBag.Title = Html.ToBreadcrumb("Users", MVC.User.Index(), string.Format("{0} ({1})", Model.User.DisplayName, Model.User.Id)); + Html.BundleDeferred("~/ClientScripts/Modules/Silverlight"); + Html.BundleDeferred("~/ClientScripts/Modules/Disco-CreateJob"); +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Id: + + @Model.User.Id + Given Name: + + @Model.User.GivenName +
Type: + + @Model.User.Type + Surname: + + @Model.User.Surname +
Display Name: + + @Model.User.DisplayName +
Email Address: + + @Model.User.EmailAddress + Phone Number: + + @Model.User.PhoneNumber +
Assigned Devices: + + @Html.Partial(MVC.User.Views._UserDeviceAssignmentHistoryTable, Model.User) +
Generate Documents: + + @Html.DropDownList("DocumentTemplates", Model.DocumentTemplatesSelectListItems) + +
+

Jobs

+@Html.Partial(MVC.Shared.Views._JobTable, Model.Jobs) +

Attachments

+@Html.Partial(MVC.User.Views.UserParts.Resources, Model) +
+ @Html.ActionLinkButton("Create Job", MVC.Job.Create(Model.PrimaryDeviceSerialNumber, Model.User.Id), "buttonCreateJob") +
diff --git a/Disco.Web/Views/User/Show.generated.cs b/Disco.Web/Views/User/Show.generated.cs new file mode 100644 index 00000000..92aa4cdc --- /dev/null +++ b/Disco.Web/Views/User/Show.generated.cs @@ -0,0 +1,300 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.17929 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Disco.Web.Views.User +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using System.Web; + using System.Web.Helpers; + using System.Web.Mvc; + using System.Web.Mvc.Ajax; + using System.Web.Mvc.Html; + using System.Web.Routing; + using System.Web.Security; + using System.Web.UI; + using System.Web.WebPages; + using Disco.BI.Extensions; + using Disco.Models.Repository; + using Disco.Web; + using Disco.Web.Extensions; + + [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorGenerator", "1.5.0.0")] + [System.Web.WebPages.PageVirtualPathAttribute("~/Views/User/Show.cshtml")] + public class Show : System.Web.Mvc.WebViewPage + { + public Show() + { + } + public override void Execute() + { + + #line 2 "..\..\Views\User\Show.cshtml" + + ViewBag.Title = Html.ToBreadcrumb("Users", MVC.User.Index(), string.Format("{0} ({1})", Model.User.DisplayName, Model.User.Id)); + Html.BundleDeferred("~/ClientScripts/Modules/Silverlight"); + Html.BundleDeferred("~/ClientScripts/Modules/Disco-CreateJob"); + + + #line default + #line hidden +WriteLiteral("\r\n\r\n \r\n Id:\r\n \r\n \r\n"); + +WriteLiteral(" "); + + + #line 12 "..\..\Views\User\Show.cshtml" + Write(Model.User.Id); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n Given Name:\r\n \r\n \r\n"); + +WriteLiteral(" "); + + + #line 17 "..\..\Views\User\Show.cshtml" + Write(Model.User.GivenName); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n \r\n \r\n Type:\r\n \r\n \r\n"); + +WriteLiteral(" "); + + + #line 24 "..\..\Views\User\Show.cshtml" + Write(Model.User.Type); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n Surname:\r\n \r\n \r\n"); + +WriteLiteral(" "); + + + #line 29 "..\..\Views\User\Show.cshtml" + Write(Model.User.Surname); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n \r\n \r\n Display Name:\r\n \r\n \r\n"); + +WriteLiteral(" "); + + + #line 36 "..\..\Views\User\Show.cshtml" + Write(Model.User.DisplayName); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n \r\n \r\n Email Address:\r\n \r\n \r\n"); + +WriteLiteral(" "); + + + #line 43 "..\..\Views\User\Show.cshtml" + Write(Model.User.EmailAddress); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n Phone Number:\r\n \r\n \r\n"); + +WriteLiteral(" "); + + + #line 48 "..\..\Views\User\Show.cshtml" + Write(Model.User.PhoneNumber); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n \r\n \r\n Assigned Devices:\r\n \r\n \r\n"); + +WriteLiteral(" "); + + + #line 55 "..\..\Views\User\Show.cshtml" + Write(Html.Partial(MVC.User.Views._UserDeviceAssignmentHistoryTable, Model.User)); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n \r\n \r\n Generate Documents:\r\n \r\n \r\n"); + +WriteLiteral(" "); + + + #line 62 "..\..\Views\User\Show.cshtml" + Write(Html.DropDownList("DocumentTemplates", Model.DocumentTemplatesSelectListItems)); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n $(function () {\r\n var generatePdfUrl = \'"); + + + #line 65 "..\..\Views\User\Show.cshtml" + Write(Url.Action(MVC.API.User.GeneratePdf(Model.User.Id, null))); + + + #line default + #line hidden +WriteLiteral(@"?DocumentTemplateId='; + var $documentTemplates = $('#DocumentTemplates'); + $documentTemplates.change(function () { + var v = $documentTemplates.val(); + if (v) { + window.location.href = generatePdfUrl + v; + $documentTemplates.val(''); + } + }); + }); + + + + +

Jobs

+"); + + + #line 80 "..\..\Views\User\Show.cshtml" +Write(Html.Partial(MVC.Shared.Views._JobTable, Model.Jobs)); + + + #line default + #line hidden +WriteLiteral("\r\n

Attachments

\r\n"); + + + #line 82 "..\..\Views\User\Show.cshtml" +Write(Html.Partial(MVC.User.Views.UserParts.Resources, Model)); + + + #line default + #line hidden +WriteLiteral("\r\n\r\n"); + +WriteLiteral(" "); + + + #line 84 "..\..\Views\User\Show.cshtml" +Write(Html.ActionLinkButton("Create Job", MVC.Job.Create(Model.PrimaryDeviceSerialNumber, Model.User.Id), "buttonCreateJob")); + + + #line default + #line hidden +WriteLiteral("\r\n\r\n"); + + } + } +} +#pragma warning restore 1591 diff --git a/Disco.Web/Views/User/UserParts/Resources.cshtml b/Disco.Web/Views/User/UserParts/Resources.cshtml new file mode 100644 index 00000000..a09f2d7a --- /dev/null +++ b/Disco.Web/Views/User/UserParts/Resources.cshtml @@ -0,0 +1,193 @@ +@model Disco.Web.Models.User.ShowModel +@{ + Html.BundleDeferred("~/Style/Shadowbox"); + Html.BundleDeferred("~/ClientScripts/Modules/Shadowbox"); +} + + + + +
+ +
+ +
+ +
+
+
+
+
+
+

+ + Are you sure?

+
diff --git a/Disco.Web/Views/User/UserParts/Resources.generated.cs b/Disco.Web/Views/User/UserParts/Resources.generated.cs new file mode 100644 index 00000000..d85a1ccf --- /dev/null +++ b/Disco.Web/Views/User/UserParts/Resources.generated.cs @@ -0,0 +1,479 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.17929 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Disco.Web.Views.User.UserParts +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using System.Web; + using System.Web.Helpers; + using System.Web.Mvc; + using System.Web.Mvc.Ajax; + using System.Web.Mvc.Html; + using System.Web.Routing; + using System.Web.Security; + using System.Web.UI; + using System.Web.WebPages; + using Disco.BI.Extensions; + using Disco.Models.Repository; + using Disco.Web; + using Disco.Web.Extensions; + + [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorGenerator", "1.5.0.0")] + [System.Web.WebPages.PageVirtualPathAttribute("~/Views/User/UserParts/Resources.cshtml")] + public class Resources : System.Web.Mvc.WebViewPage + { + public Resources() + { + } + public override void Execute() + { + + #line 2 "..\..\Views\User\UserParts\Resources.cshtml" + + Html.BundleDeferred("~/Style/Shadowbox"); + Html.BundleDeferred("~/ClientScripts/Modules/Shadowbox"); + + + #line default + #line hidden +WriteLiteral("\r\n\r\n \r\n \r\n \r\n"); + + + #line 10 "..\..\Views\User\UserParts\Resources.cshtml" + + + #line default + #line hidden + + #line 10 "..\..\Views\User\UserParts\Resources.cshtml" + if (Model.User.UserAttachments != null) + { + foreach (var ua in Model.User.UserAttachments) + { + + + #line default + #line hidden +WriteLiteral(" (Url.Action(MVC.API.User.AttachmentDownload(ua.Id)) + + #line default + #line hidden +, 471), false) +); + +WriteLiteral(" data-attachmentid=\""); + + + #line 14 "..\..\Views\User\UserParts\Resources.cshtml" + Write(ua.Id); + + + #line default + #line hidden +WriteLiteral("\""); + +WriteLiteral(" data-mimetype=\""); + + + #line 14 "..\..\Views\User\UserParts\Resources.cshtml" + Write(ua.MimeType); + + + #line default + #line hidden +WriteLiteral("\""); + +WriteLiteral(">\r\n (ua.Filename + + #line default + #line hidden +, 632), false) +); + +WriteLiteral(">\r\n (Url.Action(MVC.API.User.AttachmentThumbnail(ua.Id)) + + #line default + #line hidden +, 713), false) +); + +WriteLiteral(" />\r\n (ua.Comments + + #line default + #line hidden +, 834), false) +); + +WriteLiteral(">\r\n"); + + + #line 18 "..\..\Views\User\UserParts\Resources.cshtml" + + + #line default + #line hidden + + #line 18 "..\..\Views\User\UserParts\Resources.cshtml" + if (!string.IsNullOrEmpty(ua.DocumentTemplateId)) + { + + #line default + #line hidden + + #line 19 "..\..\Views\User\UserParts\Resources.cshtml" + Write(ua.DocumentTemplate.Description); + + + #line default + #line hidden + + #line 19 "..\..\Views\User\UserParts\Resources.cshtml" + } + else + { + + #line default + #line hidden + + #line 21 "..\..\Views\User\UserParts\Resources.cshtml" + Write(ua.Comments); + + + #line default + #line hidden + + #line 21 "..\..\Views\User\UserParts\Resources.cshtml" + } + + #line default + #line hidden +WriteLiteral("\r\n "); + + + #line 22 "..\..\Views\User\UserParts\Resources.cshtml" + Write(ua.TechUser.ToString()); + + + #line default + #line hidden +WriteLiteral("\r\n (ua.Timestamp.ToFullDateTime() + + #line default + #line hidden +, 1250), false) +); + +WriteLiteral(">"); + + + #line 23 "..\..\Views\User\UserParts\Resources.cshtml" + Write(ua.Timestamp.ToFuzzy()); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n"); + + + #line 25 "..\..\Views\User\UserParts\Resources.cshtml" + } + } + + + #line default + #line hidden +WriteLiteral(" \r\n \r\n \r\n \r\n + Shadowbox.init({ + skipSetup: true, + modal: true + }); + $(function () { + if (!document.DiscoFunctions) { + document.DiscoFunctions = {}; + } + document.DiscoFunctions.addAttachment = addAttachment; + + $Attachments = $('#Attachments'); + $attachmentOutput = $Attachments.find('.attachmentOutput'); + + $attachmentOutput.find('span.remove').click(removeAttachment); + + $('#dialogUpload').dialog({ autoOpen: false, + draggable: false, + modal: true, + resizable: false, + width: 860, + height: 550, + close: function () { + silverlightUploadAttachment.content.Navigator.Navigate('/Hidden'); + } + }); + + $('#dialogRemoveAttachment').dialog({ + resizable: false, + height: 140, + modal: true, + autoOpen: false + }); + + var onLoadNavigation = null; + var isLoaded = null; + Silverlight.createObject( + '"); + + + #line 68 "..\..\Views\User\UserParts\Resources.cshtml" + Write(Links.ClientBin.Disco_Silverlight_AttachmentUpload_xap); + + + #line default + #line hidden +WriteLiteral(@"', + $('#silverlightHostUploadAttachment').get(0), + 'silverlightUploadAttachment', + { width: '840px', height: '500px', background: 'white', version: '4.0.60310.0' }, + { onLoad: function () { + if (onLoadNavigation) { + silverlightUploadAttachment.content.Navigator.Navigate(onLoadNavigation); + isLoaded = true; + } + } + }, + 'UploadUrl="); + + + #line 79 "..\..\Views\User\UserParts\Resources.cshtml" + Write(Url.Action(MVC.API.User.AttachmentUpload(Model.User.Id, null))); + + + #line default + #line hidden +WriteLiteral(@"' + ); + + $attachmentInput = $Attachments.find('.attachmentInput'); + $attachmentInput.find('.photo').click(function () { + showDialog('/WebCam'); + }); + $attachmentInput.find('.upload').click(function () { + showDialog('/File'); + }); + + silverlightUploadAttachment = $('#silverlightUploadAttachment').get(0); + function showDialog(navigationPath) { + $('#dialogUpload').dialog('open'); + if (isLoaded) { + silverlightUploadAttachment.content.Navigator.Navigate(navigationPath); + } else { + onLoadNavigation = navigationPath; + } + }; + function addAttachment(id, quick) { + var data = { id: id }; + $.ajax({ + url: '"); + + + #line 102 "..\..\Views\User\UserParts\Resources.cshtml" + Write(Url.Action(MVC.API.User.Attachment())); + + + #line default + #line hidden +WriteLiteral(@"', + dataType: 'json', + data: data, + success: function (d) { + if (d.Result == 'OK') { + var a = d.Attachment; + + var e = $(''); + + e.attr('data-attachmentid', a.Id).attr('data-mimetype', a.MimeType).attr('href', '"); + + + #line 111 "..\..\Views\User\UserParts\Resources.cshtml" + Write(Url.Action(MVC.API.User.AttachmentDownload())); + + + #line default + #line hidden +WriteLiteral("/\' + a.Id);\r\n e.find(\'.icon img\').attr(\'src\', " + +"\'"); + + + #line 112 "..\..\Views\User\UserParts\Resources.cshtml" + Write(Url.Action(MVC.API.User.AttachmentThumbnail())); + + + #line default + #line hidden +WriteLiteral("/\' + a.Id);\r\n e.find(\'.comments\').text(a.Comme" + +"nts);\r\n e.find(\'.author\').text(a.Author);\r\n " + +" e.find(\'.timestamp\').text(a.TimestampFuzzy).at" + +"tr(\'title\', a.TimestampFull);\r\n e.find(\'.remo" + +"ve\').click(removeAttachment);\r\n if (!quick)\r\n" + +" e.hide();\r\n " + +" $attachmentOutput.append(e);\r\n if (!qu" + +"ick)\r\n e.show(\'slow\');\r\n " + +" if (a.MimeType.toLowerCase().indexOf(\'image/\') == 0)\r\n " + +" e.shadowbox({ gallery: \'attachments\', player: \'" + +"img\', title: a.Comments });\r\n } else {\r\n " + +" alert(\'Unable to add attachment: \' + d.Result);\r\n " + +" }\r\n },\r\n " + +" error: function (jqXHR, textStatus, errorThrown) {\r\n " + +" alert(\'Unable to add attachment: \' + textStatus);\r\n " + +" }\r\n });\r\n }\r\n " + +" function removeAttachment() {\r\n $this = $(this)." + +"closest(\'a\');\r\n\r\n var data = { id: $this.attr(\'data-attac" + +"hmentid\') };\r\n var $dialogRemoveAttachment = $(\'#dialogRe" + +"moveAttachment\');\r\n $dialogRemoveAttachment.dialog(\"enabl" + +"e\");\r\n $dialogRemoveAttachment.dialog(\'option\', \'buttons\'" + +", {\r\n \"Remove\": function () {\r\n " + +" $dialogRemoveAttachment.dialog(\"disable\");\r\n " + +" $dialogRemoveAttachment.dialog(\"option\", \"buttons\", null);\r\n " + +" $.ajax({\r\n url: \'"); + + + #line 144 "..\..\Views\User\UserParts\Resources.cshtml" + Write(Url.Action(MVC.API.User.AttachmentRemove())); + + + #line default + #line hidden +WriteLiteral("\',\r\n dataType: \'json\',\r\n " + +" data: data,\r\n success: function" + +" (d) {\r\n if (d == \'OK\') {\r\n " + +" $this.hide(300).delay(300).queue(function () {\r\n " + +" var $this = $(this);\r\n " + +" if ($this.attr(\'data-mimetype\').toLowerCase(" + +").indexOf(\'image/\') == 0)\r\n S" + +"hadowbox.removeCache(this);\r\n $th" + +"is.remove();\r\n });\r\n " + +" } else {\r\n ale" + +"rt(\'Unable to remove attachment: \' + d);\r\n " + +" }\r\n $dialogRemoveAttachment.dialog(\"clo" + +"se\");\r\n },\r\n " + +" error: function (jqXHR, textStatus, errorThrown) {\r\n " + +" alert(\'Unable to remove attachment: \' + textStatus);\r\n " + +" $dialogRemoveAttachment.dialog(\"close\");\r\n " + +" }\r\n });\r\n " + +" },\r\n Cancel: function () {\r\n " + +" $dialogRemoveAttachment.dialog(\"close\");\r\n " + +" }\r\n });\r\n\r\n $dialogRem" + +"oveAttachment.dialog(\'open\');\r\n\r\n return false;\r\n " + +" }\r\n $attachmentOutput.children(\'a\').each(function" + +" () {\r\n $this = $(this);\r\n if ($th" + +"is.attr(\'data-mimetype\').toLowerCase().indexOf(\'image/\') == 0)\r\n " + +" $this.shadowbox({ gallery: \'attachments\', player: \'img\', title: $thi" + +"s.find(\'.comments\').text() });\r\n });\r\n });\r\n " + +" \r\n \r\n \r\n\r\n\r\n \r\n \r\n\r\n\r\n

\r\n \r\n Are you sure?

\r\n\r\n"); + + } + } +} +#pragma warning restore 1591 diff --git a/Disco.Web/Views/User/_UserDeviceAssignmentHistoryTable.cshtml b/Disco.Web/Views/User/_UserDeviceAssignmentHistoryTable.cshtml new file mode 100644 index 00000000..799744c6 --- /dev/null +++ b/Disco.Web/Views/User/_UserDeviceAssignmentHistoryTable.cshtml @@ -0,0 +1,80 @@ +@model Disco.Models.Repository.User +@{ + var userId = Model.Id; +} +@if (Model.DeviceUserAssignments.Count > 0) +{ + + + + + + + + + @foreach (var dua in Model.DeviceUserAssignments.OrderByDescending(m => m.AssignedDate)) + { + + + + + + + + } + @if (Model.DeviceUserAssignments.Count(m => !m.UnassignedDate.HasValue) == 0) + { + + + + } +
+ Device Serial # + + Device Asset # + + Device Model + + Assigned + + Unassigned +
+ @Html.ActionLink(dua.Device.SerialNumber, MVC.Device.Show(dua.DeviceSerialNumber)) + + @dua.Device.AssetNumber + + @dua.Device.DeviceModel.ToString() + + @CommonHelpers.FriendlyDate(dua.AssignedDate) + + @CommonHelpers.FriendlyDate(dua.UnassignedDate, "Current") +
+ No Active Assignments +
+ Show + All Assignment History () + +} +else +{ + No Assignment History Available +} diff --git a/Disco.Web/Views/User/_UserDeviceAssignmentHistoryTable.generated.cs b/Disco.Web/Views/User/_UserDeviceAssignmentHistoryTable.generated.cs new file mode 100644 index 00000000..43fd50d3 --- /dev/null +++ b/Disco.Web/Views/User/_UserDeviceAssignmentHistoryTable.generated.cs @@ -0,0 +1,334 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.17929 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Disco.Web.Views.User +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using System.Web; + using System.Web.Helpers; + using System.Web.Mvc; + using System.Web.Mvc.Ajax; + using System.Web.Mvc.Html; + using System.Web.Routing; + using System.Web.Security; + using System.Web.UI; + using System.Web.WebPages; + using Disco.BI.Extensions; + using Disco.Models.Repository; + using Disco.Web; + using Disco.Web.Extensions; + + [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorGenerator", "1.5.0.0")] + [System.Web.WebPages.PageVirtualPathAttribute("~/Views/User/_UserDeviceAssignmentHistoryTable.cshtml")] + public class UserDeviceAssignmentHistoryTable : System.Web.Mvc.WebViewPage + { + public UserDeviceAssignmentHistoryTable() + { + } + public override void Execute() + { + + #line 2 "..\..\Views\User\_UserDeviceAssignmentHistoryTable.cshtml" + + var userId = Model.Id; + + + #line default + #line hidden +WriteLiteral("\r\n"); + + + #line 5 "..\..\Views\User\_UserDeviceAssignmentHistoryTable.cshtml" + if (Model.DeviceUserAssignments.Count > 0) +{ + + + #line default + #line hidden +WriteLiteral(" (userId + + #line default + #line hidden +, 195), false) +); + +WriteLiteral(@"> + + + Device Serial # + + + Device Asset # + + + Device Model + + + Assigned + + + Unassigned + + +"); + + + #line 25 "..\..\Views\User\_UserDeviceAssignmentHistoryTable.cshtml" + + + #line default + #line hidden + + #line 25 "..\..\Views\User\_UserDeviceAssignmentHistoryTable.cshtml" + foreach (var dua in Model.DeviceUserAssignments.OrderByDescending(m => m.AssignedDate)) + { + + + #line default + #line hidden +WriteLiteral(" ((!dua.UnassignedDate.HasValue).ToString() + + #line default + #line hidden +, 719), false) +); + +WriteLiteral(">\r\n \r\n"); + +WriteLiteral(" "); + + + #line 29 "..\..\Views\User\_UserDeviceAssignmentHistoryTable.cshtml" + Write(Html.ActionLink(dua.Device.SerialNumber, MVC.Device.Show(dua.DeviceSerialNumber))); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n \r\n"); + +WriteLiteral(" "); + + + #line 32 "..\..\Views\User\_UserDeviceAssignmentHistoryTable.cshtml" + Write(dua.Device.AssetNumber); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n \r\n"); + +WriteLiteral(" "); + + + #line 35 "..\..\Views\User\_UserDeviceAssignmentHistoryTable.cshtml" + Write(dua.Device.DeviceModel.ToString()); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n \r\n"); + +WriteLiteral(" "); + + + #line 38 "..\..\Views\User\_UserDeviceAssignmentHistoryTable.cshtml" + Write(CommonHelpers.FriendlyDate(dua.AssignedDate)); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n \r\n"); + +WriteLiteral(" "); + + + #line 41 "..\..\Views\User\_UserDeviceAssignmentHistoryTable.cshtml" + Write(CommonHelpers.FriendlyDate(dua.UnassignedDate, "Current")); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n \r\n"); + + + #line 44 "..\..\Views\User\_UserDeviceAssignmentHistoryTable.cshtml" + } + + + #line default + #line hidden +WriteLiteral(" "); + + + #line 45 "..\..\Views\User\_UserDeviceAssignmentHistoryTable.cshtml" + if (Model.DeviceUserAssignments.Count(m => !m.UnassignedDate.HasValue) == 0) + { + + + #line default + #line hidden +WriteLiteral(" \r\n \r\n No Active Assignments\r\n \r\n \r\n"); + + + #line 52 "..\..\Views\User\_UserDeviceAssignmentHistoryTable.cshtml" + } + + + #line default + #line hidden +WriteLiteral(" \r\n"); + +WriteLiteral(" (userId + + #line default + #line hidden +, 1752), false) +); + +WriteLiteral(" class=\"smallLink\""); + +WriteLiteral(">Show\r\n All Assignment History ((userId + + #line default + #line hidden +, 1869), false) +); + +WriteLiteral(">)\r\n"); + +WriteLiteral(" \r\n $(function () {\r\n var $table = $(\'#User_AssignedDevice_Hist" + +"ory_"); + + + #line 58 "..\..\Views\User\_UserDeviceAssignmentHistoryTable.cshtml" + Write(userId); + + + #line default + #line hidden +WriteLiteral(@"'); + var $inactiveRecords = $table.find('tr.assignmentActiveFalse').hide(); + if ($inactiveRecords.length != 0) { + var recordCountText = $inactiveRecords.length + ' record'; + if ($inactiveRecords.length != 1) + recordCountText += 's'; + $('#User_AssignedDevice_History_RecordCount_"); + + + #line 64 "..\..\Views\User\_UserDeviceAssignmentHistoryTable.cshtml" + Write(userId); + + + #line default + #line hidden +WriteLiteral("\').text(recordCountText);\r\n $(\'#User_AssignedDevice_History_Trigge" + +"r_"); + + + #line 65 "..\..\Views\User\_UserDeviceAssignmentHistoryTable.cshtml" + Write(userId); + + + #line default + #line hidden +WriteLiteral(@"').click(function () { + $(this).hide(); + $table.find('tr.noActiveAssignments').hide(); + $inactiveRecords.show(); + return false; + }); + } else { + $('#User_AssignedDevice_History_Trigger_"); + + + #line 72 "..\..\Views\User\_UserDeviceAssignmentHistoryTable.cshtml" + Write(userId); + + + #line default + #line hidden +WriteLiteral("\').hide();\r\n }\r\n });\r\n \r\n"); + + + #line 76 "..\..\Views\User\_UserDeviceAssignmentHistoryTable.cshtml" +} +else +{ + + + #line default + #line hidden +WriteLiteral(" No Assignment History Available\r\n"); + + + #line 80 "..\..\Views\User\_UserDeviceAssignmentHistoryTable.cshtml" +} + + + #line default + #line hidden + } + } +} +#pragma warning restore 1591 diff --git a/Disco.Web/Views/User/_UserTable.cshtml b/Disco.Web/Views/User/_UserTable.cshtml new file mode 100644 index 00000000..1c2fc845 --- /dev/null +++ b/Disco.Web/Views/User/_UserTable.cshtml @@ -0,0 +1,59 @@ +@model IEnumerable +
+ @if (Model != null && Model.Count() > 0) + { + + + + + + + + + + + + + @foreach (var item in Model) + { + + + + + + + + + } + +
+ Id + + Surname + + Given Name + + Display Name + + Assigned Devices + + Jobs +
+ @Html.ActionLink(item.Id, MVC.User.Show(item.Id)) + + @item.Surname + + @item.GivenName + + @item.DisplayName + + @item.AssignedDevicesCount + + @item.JobCount +
+ } + else + { + No Users Found + } +
diff --git a/Disco.Web/Views/User/_UserTable.generated.cs b/Disco.Web/Views/User/_UserTable.generated.cs new file mode 100644 index 00000000..c00b999a --- /dev/null +++ b/Disco.Web/Views/User/_UserTable.generated.cs @@ -0,0 +1,211 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.17929 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Disco.Web.Views.User +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using System.Web; + using System.Web.Helpers; + using System.Web.Mvc; + using System.Web.Mvc.Ajax; + using System.Web.Mvc.Html; + using System.Web.Routing; + using System.Web.Security; + using System.Web.UI; + using System.Web.WebPages; + using Disco.BI.Extensions; + using Disco.Models.Repository; + using Disco.Web; + using Disco.Web.Extensions; + + [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorGenerator", "1.5.0.0")] + [System.Web.WebPages.PageVirtualPathAttribute("~/Views/User/_UserTable.cshtml")] + public class UserTable : System.Web.Mvc.WebViewPage> + { + public UserTable() + { + } + public override void Execute() + { +WriteLiteral("\r\n"); + + + #line 3 "..\..\Views\User\_UserTable.cshtml" + + + #line default + #line hidden + + #line 3 "..\..\Views\User\_UserTable.cshtml" + if (Model != null && Model.Count() > 0) + { + + + #line default + #line hidden +WriteLiteral(" + + + + Id + + + Surname + + + Given Name + + + Display Name + + + Assigned Devices + + + Jobs + + + + +"); + + + #line 29 "..\..\Views\User\_UserTable.cshtml" + + + #line default + #line hidden + + #line 29 "..\..\Views\User\_UserTable.cshtml" + foreach (var item in Model) + { + + + #line default + #line hidden +WriteLiteral(" \r\n \r\n"); + +WriteLiteral(" "); + + + #line 33 "..\..\Views\User\_UserTable.cshtml" + Write(Html.ActionLink(item.Id, MVC.User.Show(item.Id))); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n \r\n"); + +WriteLiteral(" "); + + + #line 36 "..\..\Views\User\_UserTable.cshtml" + Write(item.Surname); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n \r\n"); + +WriteLiteral(" "); + + + #line 39 "..\..\Views\User\_UserTable.cshtml" + Write(item.GivenName); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n \r\n"); + +WriteLiteral(" "); + + + #line 42 "..\..\Views\User\_UserTable.cshtml" + Write(item.DisplayName); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n \r\n"); + +WriteLiteral(" "); + + + #line 45 "..\..\Views\User\_UserTable.cshtml" + Write(item.AssignedDevicesCount); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n \r\n"); + +WriteLiteral(" "); + + + #line 48 "..\..\Views\User\_UserTable.cshtml" + Write(item.JobCount); + + + #line default + #line hidden +WriteLiteral("\r\n \r\n \r\n"); + + + #line 51 "..\..\Views\User\_UserTable.cshtml" + } + + + #line default + #line hidden +WriteLiteral(" \r\n \r\n"); + + + #line 54 "..\..\Views\User\_UserTable.cshtml" + } + else + { + + + #line default + #line hidden +WriteLiteral(" No Users Found\r\n"); + + + #line 58 "..\..\Views\User\_UserTable.cshtml" + } + + + #line default + #line hidden +WriteLiteral("\r\n"); + + } + } +} +#pragma warning restore 1591 diff --git a/Disco.Web/Views/User/_ViewStart.cshtml b/Disco.Web/Views/User/_ViewStart.cshtml new file mode 100644 index 00000000..709935b5 --- /dev/null +++ b/Disco.Web/Views/User/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Html.BundleDeferred("~/Style/User"); +} \ No newline at end of file diff --git a/Disco.Web/Views/User/_ViewStart.generated.cs b/Disco.Web/Views/User/_ViewStart.generated.cs new file mode 100644 index 00000000..0cca7b1e --- /dev/null +++ b/Disco.Web/Views/User/_ViewStart.generated.cs @@ -0,0 +1,54 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.17929 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Disco.Web.Views.User +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using System.Web; + using System.Web.Helpers; + using System.Web.Mvc; + using System.Web.Mvc.Ajax; + using System.Web.Mvc.Html; + using System.Web.Routing; + using System.Web.Security; + using System.Web.UI; + using System.Web.WebPages; + using Disco.BI.Extensions; + using Disco.Models.Repository; + using Disco.Web; + using Disco.Web.Extensions; + + [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorGenerator", "1.5.0.0")] + [System.Web.WebPages.PageVirtualPathAttribute("~/Views/User/_ViewStart.cshtml")] + public class ViewStart : System.Web.Mvc.ViewStartPage + { + public ViewStart() + { + } + public override void Execute() + { + + #line 1 "..\..\Views\User\_ViewStart.cshtml" + + Html.BundleDeferred("~/Style/User"); + + + #line default + #line hidden + } + } +} +#pragma warning restore 1591 diff --git a/Disco.Web/Views/Web.config b/Disco.Web/Views/Web.config new file mode 100644 index 00000000..fbc5bd75 --- /dev/null +++ b/Disco.Web/Views/Web.config @@ -0,0 +1,62 @@ + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Disco.Web/Views/_ViewStart.cshtml b/Disco.Web/Views/_ViewStart.cshtml new file mode 100644 index 00000000..9c30ccf2 --- /dev/null +++ b/Disco.Web/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "~/Views/Shared/_Layout.cshtml"; +} \ No newline at end of file diff --git a/Disco.Web/Views/_ViewStart.generated.cs b/Disco.Web/Views/_ViewStart.generated.cs new file mode 100644 index 00000000..3e56c2f8 --- /dev/null +++ b/Disco.Web/Views/_ViewStart.generated.cs @@ -0,0 +1,54 @@ +#pragma warning disable 1591 +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.17929 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Disco.Web.Views +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Linq; + using System.Net; + using System.Text; + using System.Web; + using System.Web.Helpers; + using System.Web.Mvc; + using System.Web.Mvc.Ajax; + using System.Web.Mvc.Html; + using System.Web.Routing; + using System.Web.Security; + using System.Web.UI; + using System.Web.WebPages; + using Disco.BI.Extensions; + using Disco.Models.Repository; + using Disco.Web; + using Disco.Web.Extensions; + + [System.CodeDom.Compiler.GeneratedCodeAttribute("RazorGenerator", "1.5.0.0")] + [System.Web.WebPages.PageVirtualPathAttribute("~/Views/_ViewStart.cshtml")] + public class ViewStart : System.Web.Mvc.ViewStartPage + { + public ViewStart() + { + } + public override void Execute() + { + + #line 1 "..\..\Views\_ViewStart.cshtml" + + Layout = "~/Views/Shared/_Layout.cshtml"; + + + #line default + #line hidden + } + } +} +#pragma warning restore 1591 diff --git a/Disco.Web/Web.Debug.config b/Disco.Web/Web.Debug.config new file mode 100644 index 00000000..2f743e60 --- /dev/null +++ b/Disco.Web/Web.Debug.config @@ -0,0 +1,31 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Disco.Web/Web.Release.config b/Disco.Web/Web.Release.config new file mode 100644 index 00000000..9ebb53d6 --- /dev/null +++ b/Disco.Web/Web.Release.config @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Disco.Web/Web.config b/Disco.Web/Web.config new file mode 100644 index 00000000..762bd6a1 --- /dev/null +++ b/Disco.Web/Web.config @@ -0,0 +1,83 @@ + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Disco.Web/_bin_deployableAssemblies/amd64/Microsoft.VC90.CRT/Microsoft.VC90.CRT.manifest b/Disco.Web/_bin_deployableAssemblies/amd64/Microsoft.VC90.CRT/Microsoft.VC90.CRT.manifest new file mode 100644 index 00000000..a7a7026c --- /dev/null +++ b/Disco.Web/_bin_deployableAssemblies/amd64/Microsoft.VC90.CRT/Microsoft.VC90.CRT.manifest @@ -0,0 +1,6 @@ + + + + + Vy8CgQgbu3qH5JHTK0op4kR8114= QTJu3Gttpt8hhCktGelNeXj4Yp8= 1ruqF7/L+m1tqnJVscaOtNRNHIE= + \ No newline at end of file diff --git a/Disco.Web/_bin_deployableAssemblies/amd64/Microsoft.VC90.CRT/README_ENU.txt b/Disco.Web/_bin_deployableAssemblies/amd64/Microsoft.VC90.CRT/README_ENU.txt new file mode 100644 index 00000000..fc38b368 Binary files /dev/null and b/Disco.Web/_bin_deployableAssemblies/amd64/Microsoft.VC90.CRT/README_ENU.txt differ diff --git a/Disco.Web/_bin_deployableAssemblies/amd64/Microsoft.VC90.CRT/msvcr90.dll b/Disco.Web/_bin_deployableAssemblies/amd64/Microsoft.VC90.CRT/msvcr90.dll new file mode 100644 index 00000000..c95e1bf2 Binary files /dev/null and b/Disco.Web/_bin_deployableAssemblies/amd64/Microsoft.VC90.CRT/msvcr90.dll differ diff --git a/Disco.Web/_bin_deployableAssemblies/amd64/sqlceca40.dll b/Disco.Web/_bin_deployableAssemblies/amd64/sqlceca40.dll new file mode 100644 index 00000000..d5d4c204 Binary files /dev/null and b/Disco.Web/_bin_deployableAssemblies/amd64/sqlceca40.dll differ diff --git a/Disco.Web/_bin_deployableAssemblies/amd64/sqlcecompact40.dll b/Disco.Web/_bin_deployableAssemblies/amd64/sqlcecompact40.dll new file mode 100644 index 00000000..ed061ade Binary files /dev/null and b/Disco.Web/_bin_deployableAssemblies/amd64/sqlcecompact40.dll differ diff --git a/Disco.Web/_bin_deployableAssemblies/amd64/sqlceer40EN.dll b/Disco.Web/_bin_deployableAssemblies/amd64/sqlceer40EN.dll new file mode 100644 index 00000000..e19eed9b Binary files /dev/null and b/Disco.Web/_bin_deployableAssemblies/amd64/sqlceer40EN.dll differ diff --git a/Disco.Web/_bin_deployableAssemblies/amd64/sqlceme40.dll b/Disco.Web/_bin_deployableAssemblies/amd64/sqlceme40.dll new file mode 100644 index 00000000..c67fc9e6 Binary files /dev/null and b/Disco.Web/_bin_deployableAssemblies/amd64/sqlceme40.dll differ diff --git a/Disco.Web/_bin_deployableAssemblies/amd64/sqlceqp40.dll b/Disco.Web/_bin_deployableAssemblies/amd64/sqlceqp40.dll new file mode 100644 index 00000000..df444033 Binary files /dev/null and b/Disco.Web/_bin_deployableAssemblies/amd64/sqlceqp40.dll differ diff --git a/Disco.Web/_bin_deployableAssemblies/amd64/sqlcese40.dll b/Disco.Web/_bin_deployableAssemblies/amd64/sqlcese40.dll new file mode 100644 index 00000000..af2de5ec Binary files /dev/null and b/Disco.Web/_bin_deployableAssemblies/amd64/sqlcese40.dll differ diff --git a/Disco.Web/_bin_deployableAssemblies/x86/Microsoft.VC90.CRT/Microsoft.VC90.CRT.manifest b/Disco.Web/_bin_deployableAssemblies/x86/Microsoft.VC90.CRT/Microsoft.VC90.CRT.manifest new file mode 100644 index 00000000..9090f295 --- /dev/null +++ b/Disco.Web/_bin_deployableAssemblies/x86/Microsoft.VC90.CRT/Microsoft.VC90.CRT.manifest @@ -0,0 +1,6 @@ + + + + + +CXED+6HzJlSphyMNOn27ujadC0= MyKED+9DyS+1XcMeaC0Zlw2vFZ0= EeyDE7og6WoPd2oBhYbMEnpFHhY= + \ No newline at end of file diff --git a/Disco.Web/_bin_deployableAssemblies/x86/Microsoft.VC90.CRT/README_ENU.txt b/Disco.Web/_bin_deployableAssemblies/x86/Microsoft.VC90.CRT/README_ENU.txt new file mode 100644 index 00000000..fc38b368 Binary files /dev/null and b/Disco.Web/_bin_deployableAssemblies/x86/Microsoft.VC90.CRT/README_ENU.txt differ diff --git a/Disco.Web/_bin_deployableAssemblies/x86/Microsoft.VC90.CRT/msvcr90.dll b/Disco.Web/_bin_deployableAssemblies/x86/Microsoft.VC90.CRT/msvcr90.dll new file mode 100644 index 00000000..e2e66019 Binary files /dev/null and b/Disco.Web/_bin_deployableAssemblies/x86/Microsoft.VC90.CRT/msvcr90.dll differ diff --git a/Disco.Web/_bin_deployableAssemblies/x86/sqlceca40.dll b/Disco.Web/_bin_deployableAssemblies/x86/sqlceca40.dll new file mode 100644 index 00000000..92596101 Binary files /dev/null and b/Disco.Web/_bin_deployableAssemblies/x86/sqlceca40.dll differ diff --git a/Disco.Web/_bin_deployableAssemblies/x86/sqlcecompact40.dll b/Disco.Web/_bin_deployableAssemblies/x86/sqlcecompact40.dll new file mode 100644 index 00000000..41c69ecc Binary files /dev/null and b/Disco.Web/_bin_deployableAssemblies/x86/sqlcecompact40.dll differ diff --git a/Disco.Web/_bin_deployableAssemblies/x86/sqlceer40EN.dll b/Disco.Web/_bin_deployableAssemblies/x86/sqlceer40EN.dll new file mode 100644 index 00000000..a40154fd Binary files /dev/null and b/Disco.Web/_bin_deployableAssemblies/x86/sqlceer40EN.dll differ diff --git a/Disco.Web/_bin_deployableAssemblies/x86/sqlceme40.dll b/Disco.Web/_bin_deployableAssemblies/x86/sqlceme40.dll new file mode 100644 index 00000000..d737119f Binary files /dev/null and b/Disco.Web/_bin_deployableAssemblies/x86/sqlceme40.dll differ diff --git a/Disco.Web/_bin_deployableAssemblies/x86/sqlceqp40.dll b/Disco.Web/_bin_deployableAssemblies/x86/sqlceqp40.dll new file mode 100644 index 00000000..dedfc9a3 Binary files /dev/null and b/Disco.Web/_bin_deployableAssemblies/x86/sqlceqp40.dll differ diff --git a/Disco.Web/_bin_deployableAssemblies/x86/sqlcese40.dll b/Disco.Web/_bin_deployableAssemblies/x86/sqlcese40.dll new file mode 100644 index 00000000..cc37e3b5 Binary files /dev/null and b/Disco.Web/_bin_deployableAssemblies/x86/sqlcese40.dll differ diff --git a/Disco.Web/favicon.ico b/Disco.Web/favicon.ico new file mode 100644 index 00000000..53d271d4 Binary files /dev/null and b/Disco.Web/favicon.ico differ diff --git a/Disco.Web/packages.config b/Disco.Web/packages.config new file mode 100644 index 00000000..5c51710d --- /dev/null +++ b/Disco.Web/packages.config @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Disco.sln b/Disco.sln new file mode 100644 index 00000000..6c894fca --- /dev/null +++ b/Disco.sln @@ -0,0 +1,125 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2012 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Disco.ClientBootstrapper", "Disco.ClientBootstrapper\Disco.ClientBootstrapper.csproj", "{15BD9561-A3C7-4608-9F7E-F1A1CFB60055}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Disco.BI", "Disco.BI\Disco.BI.csproj", "{095E6F94-3C34-47AE-BB83-46203535E0F6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Disco.Models", "Disco.Models\Disco.Models.csproj", "{FBC05512-FCCA-4B16-9E76-8C413C5DE6C9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Disco.Web.Extensions", "Disco.Web.Extensions\Disco.Web.Extensions.csproj", "{C433EFBA-8608-4451-874B-AF32C8536792}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Disco.Data", "Disco.Data\Disco.Data.csproj", "{85A6BD19-2C64-4746-8F2C-A68A86E8C2D7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Disco.Services", "Disco.Services\Disco.Services.csproj", "{B80A737F-BD6A-4986-9182-DD7B932BD950}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Disco.Web", "Disco.Web\Disco.Web.csproj", "{241F4F43-6ACB-482D-8CBF-8F4E4B4DB0FE}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Disco.Client", "Disco.Client\Disco.Client.csproj", "{D6B85A86-0FAA-4B04-BC9E-D01A08C03387}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|Mixed Platforms = Debug|Mixed Platforms + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|Mixed Platforms = Release|Mixed Platforms + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {15BD9561-A3C7-4608-9F7E-F1A1CFB60055}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {15BD9561-A3C7-4608-9F7E-F1A1CFB60055}.Debug|Any CPU.Build.0 = Debug|Any CPU + {15BD9561-A3C7-4608-9F7E-F1A1CFB60055}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {15BD9561-A3C7-4608-9F7E-F1A1CFB60055}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {15BD9561-A3C7-4608-9F7E-F1A1CFB60055}.Debug|x86.ActiveCfg = Debug|x86 + {15BD9561-A3C7-4608-9F7E-F1A1CFB60055}.Debug|x86.Build.0 = Debug|x86 + {15BD9561-A3C7-4608-9F7E-F1A1CFB60055}.Release|Any CPU.ActiveCfg = Release|Any CPU + {15BD9561-A3C7-4608-9F7E-F1A1CFB60055}.Release|Any CPU.Build.0 = Release|Any CPU + {15BD9561-A3C7-4608-9F7E-F1A1CFB60055}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {15BD9561-A3C7-4608-9F7E-F1A1CFB60055}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {15BD9561-A3C7-4608-9F7E-F1A1CFB60055}.Release|x86.ActiveCfg = Release|x86 + {15BD9561-A3C7-4608-9F7E-F1A1CFB60055}.Release|x86.Build.0 = Release|x86 + {095E6F94-3C34-47AE-BB83-46203535E0F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {095E6F94-3C34-47AE-BB83-46203535E0F6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {095E6F94-3C34-47AE-BB83-46203535E0F6}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {095E6F94-3C34-47AE-BB83-46203535E0F6}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {095E6F94-3C34-47AE-BB83-46203535E0F6}.Debug|x86.ActiveCfg = Debug|Any CPU + {095E6F94-3C34-47AE-BB83-46203535E0F6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {095E6F94-3C34-47AE-BB83-46203535E0F6}.Release|Any CPU.Build.0 = Release|Any CPU + {095E6F94-3C34-47AE-BB83-46203535E0F6}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {095E6F94-3C34-47AE-BB83-46203535E0F6}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {095E6F94-3C34-47AE-BB83-46203535E0F6}.Release|x86.ActiveCfg = Release|Any CPU + {FBC05512-FCCA-4B16-9E76-8C413C5DE6C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FBC05512-FCCA-4B16-9E76-8C413C5DE6C9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FBC05512-FCCA-4B16-9E76-8C413C5DE6C9}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {FBC05512-FCCA-4B16-9E76-8C413C5DE6C9}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {FBC05512-FCCA-4B16-9E76-8C413C5DE6C9}.Debug|x86.ActiveCfg = Debug|Any CPU + {FBC05512-FCCA-4B16-9E76-8C413C5DE6C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FBC05512-FCCA-4B16-9E76-8C413C5DE6C9}.Release|Any CPU.Build.0 = Release|Any CPU + {FBC05512-FCCA-4B16-9E76-8C413C5DE6C9}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {FBC05512-FCCA-4B16-9E76-8C413C5DE6C9}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {FBC05512-FCCA-4B16-9E76-8C413C5DE6C9}.Release|x86.ActiveCfg = Release|Any CPU + {C433EFBA-8608-4451-874B-AF32C8536792}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C433EFBA-8608-4451-874B-AF32C8536792}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C433EFBA-8608-4451-874B-AF32C8536792}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {C433EFBA-8608-4451-874B-AF32C8536792}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {C433EFBA-8608-4451-874B-AF32C8536792}.Debug|x86.ActiveCfg = Debug|Any CPU + {C433EFBA-8608-4451-874B-AF32C8536792}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C433EFBA-8608-4451-874B-AF32C8536792}.Release|Any CPU.Build.0 = Release|Any CPU + {C433EFBA-8608-4451-874B-AF32C8536792}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {C433EFBA-8608-4451-874B-AF32C8536792}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {C433EFBA-8608-4451-874B-AF32C8536792}.Release|x86.ActiveCfg = Release|Any CPU + {85A6BD19-2C64-4746-8F2C-A68A86E8C2D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {85A6BD19-2C64-4746-8F2C-A68A86E8C2D7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {85A6BD19-2C64-4746-8F2C-A68A86E8C2D7}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {85A6BD19-2C64-4746-8F2C-A68A86E8C2D7}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {85A6BD19-2C64-4746-8F2C-A68A86E8C2D7}.Debug|x86.ActiveCfg = Debug|Any CPU + {85A6BD19-2C64-4746-8F2C-A68A86E8C2D7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {85A6BD19-2C64-4746-8F2C-A68A86E8C2D7}.Release|Any CPU.Build.0 = Release|Any CPU + {85A6BD19-2C64-4746-8F2C-A68A86E8C2D7}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {85A6BD19-2C64-4746-8F2C-A68A86E8C2D7}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {85A6BD19-2C64-4746-8F2C-A68A86E8C2D7}.Release|x86.ActiveCfg = Release|Any CPU + {B80A737F-BD6A-4986-9182-DD7B932BD950}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B80A737F-BD6A-4986-9182-DD7B932BD950}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B80A737F-BD6A-4986-9182-DD7B932BD950}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {B80A737F-BD6A-4986-9182-DD7B932BD950}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {B80A737F-BD6A-4986-9182-DD7B932BD950}.Debug|x86.ActiveCfg = Debug|Any CPU + {B80A737F-BD6A-4986-9182-DD7B932BD950}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B80A737F-BD6A-4986-9182-DD7B932BD950}.Release|Any CPU.Build.0 = Release|Any CPU + {B80A737F-BD6A-4986-9182-DD7B932BD950}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {B80A737F-BD6A-4986-9182-DD7B932BD950}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {B80A737F-BD6A-4986-9182-DD7B932BD950}.Release|x86.ActiveCfg = Release|Any CPU + {241F4F43-6ACB-482D-8CBF-8F4E4B4DB0FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {241F4F43-6ACB-482D-8CBF-8F4E4B4DB0FE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {241F4F43-6ACB-482D-8CBF-8F4E4B4DB0FE}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {241F4F43-6ACB-482D-8CBF-8F4E4B4DB0FE}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {241F4F43-6ACB-482D-8CBF-8F4E4B4DB0FE}.Debug|x86.ActiveCfg = Debug|Any CPU + {241F4F43-6ACB-482D-8CBF-8F4E4B4DB0FE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {241F4F43-6ACB-482D-8CBF-8F4E4B4DB0FE}.Release|Any CPU.Build.0 = Release|Any CPU + {241F4F43-6ACB-482D-8CBF-8F4E4B4DB0FE}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {241F4F43-6ACB-482D-8CBF-8F4E4B4DB0FE}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {241F4F43-6ACB-482D-8CBF-8F4E4B4DB0FE}.Release|x86.ActiveCfg = Release|Any CPU + {D6B85A86-0FAA-4B04-BC9E-D01A08C03387}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D6B85A86-0FAA-4B04-BC9E-D01A08C03387}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D6B85A86-0FAA-4B04-BC9E-D01A08C03387}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {D6B85A86-0FAA-4B04-BC9E-D01A08C03387}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {D6B85A86-0FAA-4B04-BC9E-D01A08C03387}.Debug|x86.ActiveCfg = Debug|Any CPU + {D6B85A86-0FAA-4B04-BC9E-D01A08C03387}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D6B85A86-0FAA-4B04-BC9E-D01A08C03387}.Release|Any CPU.Build.0 = Release|Any CPU + {D6B85A86-0FAA-4B04-BC9E-D01A08C03387}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {D6B85A86-0FAA-4B04-BC9E-D01A08C03387}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {D6B85A86-0FAA-4B04-BC9E-D01A08C03387}.Release|x86.ActiveCfg = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + BuildVersion_BuildAction = ReBuild + BuildVersion_StartDate = 2001/1/1 + BuildVersion_BuildVersioningStyle = None.None.None.None + BuildVersion_UpdateAssemblyVersion = False + BuildVersion_UpdateFileVersion = False + BuildVersion_DetectChanges = False + BuildVersion_UseGlobalSettings = False + EndGlobalSection +EndGlobal