From be85aeff52259b2c56bcef062cc7a249258ab8d8 Mon Sep 17 00:00:00 2001 From: Disassembler Date: Thu, 30 May 2019 15:29:46 +0200 Subject: [PATCH] Initial commit --- RCM.sln | 25 + RCM/Config.cs | 82 ++++ RCM/Config.resx | 124 +++++ RCM/ConfigForm.Designer.cs | 167 +++++++ RCM/ConfigForm.cs | 182 +++++++ RCM/ConfigForm.resx | 120 +++++ RCM/ConfigFormTab.Designer.cs | 67 +++ RCM/ConfigFormTab.cs | 27 + RCM/ConfigFormTab.resx | 120 +++++ RCM/ConfigSerializer.cs | 90 ++++ RCM/DataSource/MySQL/MySQL.cs | 146 ++++++ RCM/DataSource/MySQL/MySQL.resx | 124 +++++ RCM/DataSource/MySQL/MySQLControl.Designer.cs | 206 ++++++++ RCM/DataSource/MySQL/MySQLControl.cs | 43 ++ RCM/DataSource/SQLite/SQLite.cs | 137 ++++++ RCM/DataSource/SQLite/SQLite.resx | 124 +++++ .../SQLite/SQLiteControl.Designer.cs | 146 ++++++ RCM/DataSource/SQLite/SQLiteControl.cs | 46 ++ RCM/DisplayName.cs | 13 + RCM/Group.cs | 20 + RCM/Group.resx | 124 +++++ RCM/GroupPath.cs | 23 + RCM/IAction.cs | 5 + RCM/IConfigSection.cs | 9 + RCM/IDataSource.cs | 19 + RCM/IRecord.cs | 15 + RCM/MainForm.Designer.cs | 249 ++++++++++ RCM/MainForm.cs | 463 ++++++++++++++++++ RCM/MainForm.resx | 129 +++++ RCM/Program.cs | 21 + RCM/Properties/AssemblyInfo.cs | 36 ++ RCM/Properties/Resources.Designer.cs | 73 +++ RCM/Properties/Resources.resx | 124 +++++ RCM/RCM.csproj | 229 +++++++++ RCM/Record/RDP/RDP.cs | 49 ++ RCM/Record/RDP/RDP.resx | 124 +++++ RCM/Record/RDP/RDPAction.cs | 55 +++ RCM/Record/RDP/SMBAction.cs | 27 + RCM/Record/SSH/SCPAction.cs | 67 +++ RCM/Record/SSH/SSH.cs | 44 ++ RCM/Record/SSH/SSH.resx | 124 +++++ RCM/Record/SSH/SSHAction.cs | 72 +++ RCM/Record/SSH/SSHConfig.cs | 33 ++ RCM/Record/SSH/SSHConfigControl.Designer.cs | 222 +++++++++ RCM/Record/SSH/SSHConfigControl.cs | 37 ++ RCM/Record/WebSite/BrowserAction.cs | 24 + RCM/Record/WebSite/WebSite.cs | 40 ++ RCM/Record/WebSite/WebSite.resx | 124 +++++ RCM/Record/WebSite/WebsiteControl.Designer.cs | 122 +++++ RCM/Record/WebSite/WebsiteControl.cs | 17 + RCM/Record/WinBox/WinBox.cs | 42 ++ RCM/Record/WinBox/WinBox.resx | 124 +++++ RCM/Record/WinBox/WinBoxAction.cs | 40 ++ RCM/Record/WinBox/WinBoxConfig.cs | 27 + .../WinBox/WinBoxConfigControl.Designer.cs | 83 ++++ RCM/Record/WinBox/WinBoxConfigControl.cs | 16 + RCM/Record/WinBox/WinBoxConfigControl.resx | 123 +++++ RCM/RecordNodeSorter.cs | 32 ++ RCM/SimpleHostControl.Designer.cs | 122 +++++ RCM/SimpleHostControl.cs | 17 + RCM/SimpleHostControl.resx | 120 +++++ RCM/TypeExtensions.cs | 14 + RCM/packages.config | 9 + Resources/Config.png | Bin 0 -> 512 bytes Resources/Empty.ico | Bin 0 -> 1742 bytes Resources/Group.ico | Bin 0 -> 1742 bytes Resources/MySQL.ico | Bin 0 -> 1742 bytes Resources/RDP.ico | Bin 0 -> 1742 bytes Resources/SQLite.ico | Bin 0 -> 1742 bytes Resources/SSH.ico | Bin 0 -> 1742 bytes Resources/Website.ico | Bin 0 -> 1742 bytes Resources/WinBox.ico | Bin 0 -> 1742 bytes 72 files changed, 5478 insertions(+) create mode 100644 RCM.sln create mode 100644 RCM/Config.cs create mode 100644 RCM/Config.resx create mode 100644 RCM/ConfigForm.Designer.cs create mode 100644 RCM/ConfigForm.cs create mode 100644 RCM/ConfigForm.resx create mode 100644 RCM/ConfigFormTab.Designer.cs create mode 100644 RCM/ConfigFormTab.cs create mode 100644 RCM/ConfigFormTab.resx create mode 100644 RCM/ConfigSerializer.cs create mode 100644 RCM/DataSource/MySQL/MySQL.cs create mode 100644 RCM/DataSource/MySQL/MySQL.resx create mode 100644 RCM/DataSource/MySQL/MySQLControl.Designer.cs create mode 100644 RCM/DataSource/MySQL/MySQLControl.cs create mode 100644 RCM/DataSource/SQLite/SQLite.cs create mode 100644 RCM/DataSource/SQLite/SQLite.resx create mode 100644 RCM/DataSource/SQLite/SQLiteControl.Designer.cs create mode 100644 RCM/DataSource/SQLite/SQLiteControl.cs create mode 100644 RCM/DisplayName.cs create mode 100644 RCM/Group.cs create mode 100644 RCM/Group.resx create mode 100644 RCM/GroupPath.cs create mode 100644 RCM/IAction.cs create mode 100644 RCM/IConfigSection.cs create mode 100644 RCM/IDataSource.cs create mode 100644 RCM/IRecord.cs create mode 100644 RCM/MainForm.Designer.cs create mode 100644 RCM/MainForm.cs create mode 100644 RCM/MainForm.resx create mode 100644 RCM/Program.cs create mode 100644 RCM/Properties/AssemblyInfo.cs create mode 100644 RCM/Properties/Resources.Designer.cs create mode 100644 RCM/Properties/Resources.resx create mode 100644 RCM/RCM.csproj create mode 100644 RCM/Record/RDP/RDP.cs create mode 100644 RCM/Record/RDP/RDP.resx create mode 100644 RCM/Record/RDP/RDPAction.cs create mode 100644 RCM/Record/RDP/SMBAction.cs create mode 100644 RCM/Record/SSH/SCPAction.cs create mode 100644 RCM/Record/SSH/SSH.cs create mode 100644 RCM/Record/SSH/SSH.resx create mode 100644 RCM/Record/SSH/SSHAction.cs create mode 100644 RCM/Record/SSH/SSHConfig.cs create mode 100644 RCM/Record/SSH/SSHConfigControl.Designer.cs create mode 100644 RCM/Record/SSH/SSHConfigControl.cs create mode 100644 RCM/Record/WebSite/BrowserAction.cs create mode 100644 RCM/Record/WebSite/WebSite.cs create mode 100644 RCM/Record/WebSite/WebSite.resx create mode 100644 RCM/Record/WebSite/WebsiteControl.Designer.cs create mode 100644 RCM/Record/WebSite/WebsiteControl.cs create mode 100644 RCM/Record/WinBox/WinBox.cs create mode 100644 RCM/Record/WinBox/WinBox.resx create mode 100644 RCM/Record/WinBox/WinBoxAction.cs create mode 100644 RCM/Record/WinBox/WinBoxConfig.cs create mode 100644 RCM/Record/WinBox/WinBoxConfigControl.Designer.cs create mode 100644 RCM/Record/WinBox/WinBoxConfigControl.cs create mode 100644 RCM/Record/WinBox/WinBoxConfigControl.resx create mode 100644 RCM/RecordNodeSorter.cs create mode 100644 RCM/SimpleHostControl.Designer.cs create mode 100644 RCM/SimpleHostControl.cs create mode 100644 RCM/SimpleHostControl.resx create mode 100644 RCM/TypeExtensions.cs create mode 100644 RCM/packages.config create mode 100644 Resources/Config.png create mode 100644 Resources/Empty.ico create mode 100644 Resources/Group.ico create mode 100644 Resources/MySQL.ico create mode 100644 Resources/RDP.ico create mode 100644 Resources/SQLite.ico create mode 100644 Resources/SSH.ico create mode 100644 Resources/Website.ico create mode 100644 Resources/WinBox.ico diff --git a/RCM.sln b/RCM.sln new file mode 100644 index 0000000..5503077 --- /dev/null +++ b/RCM.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.28307.572 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RCM", "RCM\RCM.csproj", "{F5912EF4-EDC9-421D-B474-4CDADB121E55}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {F5912EF4-EDC9-421D-B474-4CDADB121E55}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F5912EF4-EDC9-421D-B474-4CDADB121E55}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F5912EF4-EDC9-421D-B474-4CDADB121E55}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F5912EF4-EDC9-421D-B474-4CDADB121E55}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {144EB07E-AE7D-4681-B9C9-DB12547017F3} + EndGlobalSection +EndGlobal diff --git a/RCM/Config.cs b/RCM/Config.cs new file mode 100644 index 0000000..beda2f5 --- /dev/null +++ b/RCM/Config.cs @@ -0,0 +1,82 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Windows.Forms; + +namespace RCM { + public static class Config { + private static readonly char[] _equals = new char[] { '=' }; + private static readonly char[] _brackets = new char[] { '[', ']' }; + private static readonly string _fileName = "RCM.conf"; + + private static Icon _emptyIcon; + public static ImageList IconList { get; } = new ImageList() { ColorDepth = ColorDepth.Depth32Bit, ImageSize = new Size(20, 20) }; + + public static List DataSourceTypes { get; } = new List(); + public static List RecordTypes { get; } = new List(); + + public static List DataSources { get; } = new List(); + public static Dictionary ConfigSections { get; } = new Dictionary(); + private static List _uknownSections = new List(); + + public static void LoadModules() { + _emptyIcon = (Icon)new ComponentResourceManager(typeof(Config)).GetObject("EmptyIcon"); + + IEnumerable types = Assembly.GetExecutingAssembly().GetTypes().Where(type => !type.IsInterface); + foreach (Type type in types) { + if (typeof(IDataSource).IsAssignableFrom(type)) { + DataSourceTypes.Add(type); + AddIconForType(type); + } else if (typeof(IRecord).IsAssignableFrom(type)) { + RecordTypes.Add(type); + AddIconForType(type); + } else if (typeof(IConfigSection).IsAssignableFrom(type)) { + ConfigSections[type] = (IConfigSection)Activator.CreateInstance(type); + } + } + } + + public static void AddIconForType(Type type) { + try { + IconList.Images.Add(type.FullName, (Icon)new ComponentResourceManager(type).GetObject("Icon")); + } catch { + IconList.Images.Add(type.FullName, _emptyIcon); + } + } + + public static void LoadConfiguration() { + if (File.Exists(_fileName)) { + JArray json = JArray.Parse(File.ReadAllText(_fileName)); + foreach (JObject section in json) { + try { + LoadSection(section); + } catch { + _uknownSections.Add(section); + } + } + } + } + + private static void LoadSection(JObject section) { + object obj = ConfigSerializer.JsonToObj(section); + if (obj is IDataSource dataSource) { + DataSources.Add(dataSource); + } else if (obj is IConfigSection configSection) { + ConfigSections[configSection.GetType()] = configSection; + } + } + + public static void Save() { + JArray json = new JArray(DataSources.Select(dataSource => ConfigSerializer.ObjToJson(dataSource)) + .Union(ConfigSections.Values.Select(configSection => ConfigSerializer.ObjToJson(configSection))) + .Union(_uknownSections)); + File.WriteAllText(_fileName, ConfigSerializer.JsonToString(json)); + } + } +} diff --git a/RCM/Config.resx b/RCM/Config.resx new file mode 100644 index 0000000..c20e3e5 --- /dev/null +++ b/RCM/Config.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\Empty.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/RCM/ConfigForm.Designer.cs b/RCM/ConfigForm.Designer.cs new file mode 100644 index 0000000..e87dfde --- /dev/null +++ b/RCM/ConfigForm.Designer.cs @@ -0,0 +1,167 @@ +namespace RCM { + partial class ConfigForm { + /// + /// 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.treeView = new System.Windows.Forms.TreeView(); + this.tabControl = new System.Windows.Forms.TabControl(); + this.tabPage1 = new System.Windows.Forms.TabPage(); + this.splitContainer1 = new System.Windows.Forms.SplitContainer(); + this.panelDataSource = new System.Windows.Forms.Panel(); + this.buttonSave = new System.Windows.Forms.Button(); + this.textBoxTitle = new System.Windows.Forms.TextBox(); + this.labelTitle = new System.Windows.Forms.Label(); + this.tabControl.SuspendLayout(); + this.tabPage1.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).BeginInit(); + this.splitContainer1.Panel1.SuspendLayout(); + this.splitContainer1.Panel2.SuspendLayout(); + this.splitContainer1.SuspendLayout(); + this.SuspendLayout(); + // + // treeView + // + this.treeView.Dock = System.Windows.Forms.DockStyle.Fill; + this.treeView.LabelEdit = true; + this.treeView.Location = new System.Drawing.Point(0, 0); + this.treeView.Name = "treeView"; + this.treeView.ShowRootLines = false; + this.treeView.Size = new System.Drawing.Size(239, 464); + this.treeView.TabIndex = 0; + this.treeView.AfterLabelEdit += new System.Windows.Forms.NodeLabelEditEventHandler(this.TreeView_AfterLabelEdit); + this.treeView.AfterSelect += new System.Windows.Forms.TreeViewEventHandler(this.TreeView_AfterSelect); + this.treeView.NodeMouseClick += new System.Windows.Forms.TreeNodeMouseClickEventHandler(this.TreeView_NodeMouseClick); + this.treeView.KeyDown += new System.Windows.Forms.KeyEventHandler(this.TreeView_KeyDown); + this.treeView.MouseUp += new System.Windows.Forms.MouseEventHandler(this.TreeView_MouseUp); + // + // tabControl + // + this.tabControl.Controls.Add(this.tabPage1); + this.tabControl.Dock = System.Windows.Forms.DockStyle.Fill; + this.tabControl.Location = new System.Drawing.Point(0, 0); + this.tabControl.Name = "tabControl"; + this.tabControl.SelectedIndex = 0; + this.tabControl.Size = new System.Drawing.Size(574, 496); + this.tabControl.TabIndex = 1; + // + // tabPage1 + // + this.tabPage1.BackColor = System.Drawing.SystemColors.Control; + this.tabPage1.Controls.Add(this.splitContainer1); + this.tabPage1.Location = new System.Drawing.Point(4, 22); + this.tabPage1.Name = "tabPage1"; + this.tabPage1.Padding = new System.Windows.Forms.Padding(3); + this.tabPage1.Size = new System.Drawing.Size(566, 470); + this.tabPage1.TabIndex = 0; + this.tabPage1.Text = "Data Sources"; + // + // splitContainer1 + // + this.splitContainer1.Dock = System.Windows.Forms.DockStyle.Fill; + this.splitContainer1.Location = new System.Drawing.Point(3, 3); + this.splitContainer1.Name = "splitContainer1"; + // + // splitContainer1.Panel1 + // + this.splitContainer1.Panel1.Controls.Add(this.treeView); + // + // splitContainer1.Panel2 + // + this.splitContainer1.Panel2.Controls.Add(this.panelDataSource); + this.splitContainer1.Panel2.Controls.Add(this.buttonSave); + this.splitContainer1.Panel2.Controls.Add(this.textBoxTitle); + this.splitContainer1.Panel2.Controls.Add(this.labelTitle); + this.splitContainer1.Size = new System.Drawing.Size(560, 464); + this.splitContainer1.SplitterDistance = 239; + this.splitContainer1.TabIndex = 1; + // + // panelDataSource + // + this.panelDataSource.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.panelDataSource.Location = new System.Drawing.Point(10, 53); + this.panelDataSource.Name = "panelDataSource"; + this.panelDataSource.Size = new System.Drawing.Size(307, 373); + this.panelDataSource.TabIndex = 19; + // + // buttonSave + // + this.buttonSave.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.buttonSave.Enabled = false; + this.buttonSave.Location = new System.Drawing.Point(131, 432); + this.buttonSave.Name = "buttonSave"; + this.buttonSave.Size = new System.Drawing.Size(175, 23); + this.buttonSave.TabIndex = 18; + this.buttonSave.Text = "Save changes"; + this.buttonSave.UseVisualStyleBackColor = true; + this.buttonSave.Click += new System.EventHandler(this.ButtonSave_Click); + // + // textBoxTitle + // + this.textBoxTitle.Enabled = false; + this.textBoxTitle.Location = new System.Drawing.Point(130, 9); + this.textBoxTitle.Name = "textBoxTitle"; + this.textBoxTitle.Size = new System.Drawing.Size(175, 20); + this.textBoxTitle.TabIndex = 17; + // + // labelTitle + // + this.labelTitle.AutoSize = true; + this.labelTitle.Location = new System.Drawing.Point(10, 12); + this.labelTitle.Name = "labelTitle"; + this.labelTitle.Size = new System.Drawing.Size(30, 13); + this.labelTitle.TabIndex = 16; + this.labelTitle.Text = "Title:"; + // + // ConfigForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(574, 496); + this.Controls.Add(this.tabControl); + this.Name = "ConfigForm"; + this.Text = "Configuration - Remote Connection Manager"; + this.tabControl.ResumeLayout(false); + this.tabPage1.ResumeLayout(false); + this.splitContainer1.Panel1.ResumeLayout(false); + this.splitContainer1.Panel2.ResumeLayout(false); + this.splitContainer1.Panel2.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)(this.splitContainer1)).EndInit(); + this.splitContainer1.ResumeLayout(false); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.TreeView treeView; + private System.Windows.Forms.TabControl tabControl; + private System.Windows.Forms.TabPage tabPage1; + private System.Windows.Forms.SplitContainer splitContainer1; + private System.Windows.Forms.Panel panelDataSource; + private System.Windows.Forms.Button buttonSave; + private System.Windows.Forms.TextBox textBoxTitle; + private System.Windows.Forms.Label labelTitle; + } +} \ No newline at end of file diff --git a/RCM/ConfigForm.cs b/RCM/ConfigForm.cs new file mode 100644 index 0000000..c18fb9e --- /dev/null +++ b/RCM/ConfigForm.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace RCM { + public partial class ConfigForm : Form { + #region Form + protected override CreateParams CreateParams { // Workaround for control flickering reduction + get { + CreateParams cp = base.CreateParams; + cp.ExStyle |= 0x02000000; // WS_EX_COMPOSITED + return cp; + } + } + + public ConfigForm() { + InitializeComponent(); + treeView.ImageList = Config.IconList; + treeView.ContextMenuStrip = GetNewDataSourceContextMenu(); + foreach (IDataSource dataSource in Config.DataSources) { + string dataSourceType = dataSource.GetType().FullName; + treeView.Nodes.Add(CreateTreeNodeFromDataSource(dataSource)); + SortTreeAndSelectNode(null); + } + foreach (IConfigSection configSection in Config.ConfigSections.Values) { + TabPage tabPage = new TabPage(configSection.GetType().GetDisplayName()); + tabPage.Controls.Add(new ConfigFormTab(configSection)); + tabControl.TabPages.Add(tabPage); + } + } + #endregion + + #region TreeView display + private TreeNode CreateTreeNodeFromDataSource(IDataSource dataSource) { + string imageKey = dataSource.GetType().FullName; + return new TreeNode(dataSource.Title) { Tag = dataSource, ImageKey = imageKey, SelectedImageKey = imageKey, ContextMenuStrip = GetContextMenu(dataSource) }; + } + + private void SortTreeAndSelectNode(TreeNode node) { + treeView.Sort(); + if (treeView.SelectedNode == node) { + ChangeSelectedDataSource(); + } else { + treeView.SelectedNode = node; + } + } + #endregion + + #region TreeView navigation & selection + private void TreeView_AfterSelect(object sender, TreeViewEventArgs e) => ChangeSelectedDataSource(); + + private void TreeView_NodeMouseClick(object sender, TreeNodeMouseClickEventArgs e) => treeView.SelectedNode = e.Node; + + private void TreeView_MouseUp(object sender, MouseEventArgs e) { + if (treeView.HitTest(e.X, e.Y).Node == null) { + treeView.SelectedNode = null; + ChangeSelectedDataSource(); + } + } + + private void TreeView_KeyDown(object sender, KeyEventArgs e) { + if (treeView.SelectedNode == null) { + return; + } + if (e.KeyCode == Keys.F2) { + treeView.SelectedNode.BeginEdit(); + } else if (e.KeyCode == Keys.Delete) { + DeleteSelectedDataSource(); + } + } + + private void ChangeSelectedDataSource() { + bool isSelected = treeView.SelectedNode != null; + textBoxTitle.Enabled = isSelected; + textBoxTitle.Text = ""; + buttonSave.Enabled = isSelected; + LoadDataSourceControl(); + } + + private void LoadDataSourceControl() { + foreach (Control control in panelDataSource.Controls) { + panelDataSource.Controls.Remove(control); + control.Dispose(); + } + if (treeView.SelectedNode != null) { + IDataSource dataSource = (IDataSource)treeView.SelectedNode.Tag; + textBoxTitle.Text = dataSource.Title; + panelDataSource.Controls.Add(dataSource.GetControl()); + } + } + #endregion + + #region DataSource create/update/delete + public delegate void AfterLabelEditWorkaround(TreeNode node); + + private void TreeView_AfterLabelEdit(object sender, NodeLabelEditEventArgs e) { + if (e.Label == null) { + e.CancelEdit = true; + return; + } + IDataSource dataSource = (IDataSource)e.Node.Tag; + dataSource.Title = e.Label; + Config.Save(); + BeginInvoke(new AfterLabelEditWorkaround(SortTreeAndSelectNode), new object[] { e.Node }); // Workaround for treeView.Sort() triggering relabel on every sorted node. See note in https://docs.microsoft.com/en-us/dotnet/api/system.windows.forms.listview.afterlabeledit?view=netframework-4.6 + } + + private void ButtonSave_Click(object sender, EventArgs e) { + TreeNode selectedNode = treeView.SelectedNode; + IDataSource dataSource = (IDataSource)selectedNode.Tag; + dataSource.UpdateFromControl(); + dataSource.Title = selectedNode.Text = textBoxTitle.Text; + Config.Save(); + SortTreeAndSelectNode(selectedNode); + } + + private void NewDataSourceContextMenuItem_Click(object sender, EventArgs e) { + Type type = (Type)((ToolStripMenuItem)sender).Tag; + IDataSource dataSource = (IDataSource)Activator.CreateInstance(type); + dataSource.Title = string.Format("New {0}", type.GetDisplayName()); + InsertNewDataSource(dataSource); + } + + private void DuplicateDataSourceContextMenuItem_Click(object sender, EventArgs e) { + IDataSource source = (IDataSource)treeView.SelectedNode.Tag; + IDataSource dataSource = (IDataSource)ConfigSerializer.JsonToObj(ConfigSerializer.ObjToJson(source)); + dataSource.Title = string.Format("{0} - Copy", dataSource.Title); + InsertNewDataSource(dataSource); + } + + private void InsertNewDataSource(IDataSource dataSource) { + Config.DataSources.Add(dataSource); + Config.Save(); + TreeNode node = CreateTreeNodeFromDataSource(dataSource); + treeView.Nodes.Add(node); + SortTreeAndSelectNode(node); + } + + private void DeleteSelectedDataSource() { + IDataSource dataSource = (IDataSource)treeView.SelectedNode.Tag; + if (MessageBox.Show("Do you really want to delete selected data source?", "Confirmation", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) { + Config.DataSources.Remove(dataSource); + Config.Save(); + treeView.SelectedNode.Remove(); + SortTreeAndSelectNode(null); + } + } + + private void DeleteDataSourceContextMenuItem_Click(object sender, EventArgs e) => DeleteSelectedDataSource(); + #endregion + + #region DataSource ContextMenu + private ContextMenuStrip GetNewDataSourceContextMenu() { + ContextMenuStrip menu = new ContextMenuStrip(); + ToolStripMenuItem menuItem = new ToolStripMenuItem("New item"); + foreach (Type type in Config.DataSourceTypes) { + ToolStripMenuItem item = new ToolStripMenuItem(type.GetDisplayName()) { Tag = type }; + item.Click += NewDataSourceContextMenuItem_Click; + menuItem.DropDownItems.Add(item); + } + menu.Items.Add(menuItem); + return menu; + } + + private ContextMenuStrip GetContextMenu(IDataSource dataSource) { + ContextMenuStrip menu = new ContextMenuStrip(); + ToolStripMenuItem duplicateItem = new ToolStripMenuItem("Duplicate item"); + duplicateItem.Click += DuplicateDataSourceContextMenuItem_Click; + menu.Items.Add(duplicateItem); + ToolStripMenuItem deleteItem = new ToolStripMenuItem("Delete item"); + deleteItem.Click += DeleteDataSourceContextMenuItem_Click; + menu.Items.Add(deleteItem); + return menu; + } + #endregion + } +} diff --git a/RCM/ConfigForm.resx b/RCM/ConfigForm.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/RCM/ConfigForm.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/RCM/ConfigFormTab.Designer.cs b/RCM/ConfigFormTab.Designer.cs new file mode 100644 index 0000000..407f336 --- /dev/null +++ b/RCM/ConfigFormTab.Designer.cs @@ -0,0 +1,67 @@ +namespace RCM { + partial class ConfigFormTab { + /// + /// 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 Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() { + this.buttonSave = new System.Windows.Forms.Button(); + this.panel = new System.Windows.Forms.Panel(); + this.SuspendLayout(); + // + // buttonSave + // + this.buttonSave.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.buttonSave.Location = new System.Drawing.Point(371, 429); + this.buttonSave.Name = "buttonSave"; + this.buttonSave.Size = new System.Drawing.Size(175, 23); + this.buttonSave.TabIndex = 19; + this.buttonSave.Text = "Save changes"; + this.buttonSave.Click += new System.EventHandler(this.ButtonSave_Click); + // + // panel + // + this.panel.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.panel.Location = new System.Drawing.Point(3, 3); + this.panel.Name = "panel"; + this.panel.Size = new System.Drawing.Size(554, 412); + this.panel.TabIndex = 20; + // + // ConfigFormTab + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.panel); + this.Controls.Add(this.buttonSave); + this.Name = "ConfigFormTab"; + this.Size = new System.Drawing.Size(560, 464); + this.ResumeLayout(false); + + } + + #endregion + + private System.Windows.Forms.Button buttonSave; + public System.Windows.Forms.Panel panel; + } +} diff --git a/RCM/ConfigFormTab.cs b/RCM/ConfigFormTab.cs new file mode 100644 index 0000000..9b3d8d1 --- /dev/null +++ b/RCM/ConfigFormTab.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace RCM { + public partial class ConfigFormTab : UserControl { + private IConfigSection _configSection; + + public ConfigFormTab(IConfigSection configSection) { + InitializeComponent(); + Dock = DockStyle.Fill; + _configSection = configSection; + panel.Controls.Add(configSection.GetControl()); + } + + private void ButtonSave_Click(object sender, EventArgs e) { + _configSection.UpdateFromControl(); + Config.Save(); + } + } +} diff --git a/RCM/ConfigFormTab.resx b/RCM/ConfigFormTab.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/RCM/ConfigFormTab.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/RCM/ConfigSerializer.cs b/RCM/ConfigSerializer.cs new file mode 100644 index 0000000..854df2f --- /dev/null +++ b/RCM/ConfigSerializer.cs @@ -0,0 +1,90 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; + +namespace RCM { + public static class ConfigSerializer { + private static readonly string _typeProp = "SerializedType"; + + public static JObject ObjToJson(object obj) { + JObject json = JObject.FromObject(obj); + json.Add(_typeProp, obj.GetType().FullName); + return json; + } + + public static object JsonToObj(JObject json) { + Type type = Type.GetType(json.GetValue(_typeProp).ToString(), true); + json.Remove(_typeProp); + return json.ToObject(type); + } + + public static string JsonToString(JToken json) { + var stringWriter = new StringWriter(); + var jsonWriter = new JsonTextWriter(stringWriter) { StringEscapeHandling = StringEscapeHandling.EscapeNonAscii }; + new JsonSerializer().Serialize(jsonWriter, json); + return stringWriter.ToString(); + } + + public static JObject StringToJson(string str) { + return JObject.Parse(str); + } + + public static string ObjToString(object obj) { + return JsonToString(ObjToJson(obj)); + } + + public static object StringToObj(string str) { + return JsonToObj(StringToJson(str)); + } + + public static byte[] Decrypt(byte[] cipherbytes, string password) { + if (string.IsNullOrEmpty(password)) { + return cipherbytes; + } + byte[] salt = new byte[16]; + byte[] iv = new byte[16]; + Array.Copy(cipherbytes, salt, 16); + Array.Copy(cipherbytes, 16, iv, 0, 16); + using (AesManaged aes = new AesManaged() { KeySize = 128 }) + using (Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(password, salt, 1024)) + using (ICryptoTransform decryptor = aes.CreateDecryptor(pbkdf2.GetBytes(16), iv)) + using (MemoryStream memoryStream = new MemoryStream()) + using (CryptoStream cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Write)) + using (BinaryWriter writer = new BinaryWriter(cryptoStream)) { + writer.Write(cipherbytes, 32, cipherbytes.Length - 32); + cryptoStream.FlushFinalBlock(); + return memoryStream.ToArray(); + } + } + + public static object DecryptObject(byte[] cipherbytes, string password) { + return StringToObj(Encoding.ASCII.GetString(Decrypt(cipherbytes, password))); + } + + public static byte[] Encrypt(byte[] plaintext, string password) { + if (string.IsNullOrEmpty(password)) { + return plaintext; + } + using (AesManaged aes = new AesManaged() { KeySize = 128 }) { + aes.GenerateIV(); + using (Rfc2898DeriveBytes pbkdf2 = new Rfc2898DeriveBytes(password, 16, 1024)) + using (ICryptoTransform encryptor = aes.CreateEncryptor(pbkdf2.GetBytes(16), aes.IV)) + using (MemoryStream memoryStream = new MemoryStream()) + using (CryptoStream cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write)) + using (BinaryWriter writer = new BinaryWriter(cryptoStream)) { + writer.Write(plaintext); + cryptoStream.FlushFinalBlock(); + return pbkdf2.Salt.Concat(aes.IV).Concat(memoryStream.ToArray()).ToArray(); + } + } + } + + public static byte[] EncryptObject(object obj, string password) { + return Encrypt(Encoding.ASCII.GetBytes(ObjToString(obj)), password); + } + } +} diff --git a/RCM/DataSource/MySQL/MySQL.cs b/RCM/DataSource/MySQL/MySQL.cs new file mode 100644 index 0000000..e5d31a8 --- /dev/null +++ b/RCM/DataSource/MySQL/MySQL.cs @@ -0,0 +1,146 @@ +using MySql.Data.MySqlClient; +using System; +using System.Collections.Generic; +using System.Windows.Forms; + +namespace RCM { + [DisplayName("MySQL/MariaDB")] + [Serializable] + public class MySQL : IDataSource { + public string Title { get; set; } + public HashSet ExpandedNodes { get; } = new HashSet(); + + public string Host { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public string Database { get; set; } + public string AESPassword { get; set; } + + [NonSerialized] + private MySqlConnection _connection; + [NonSerialized] + private MySQLControl _control; + + public override string ToString() => Title; + + private MySqlConnection CreateConnection(string host, string username, string password, string database) { + string[] hostport = host.Split(':'); + string port = hostport.Length > 1 ? hostport[1] : "3306"; + return new MySqlConnection(string.Format("server={0};port={1};user={2};password={3};database={4}", hostport[0], port, username, password, database)); + } + + private void OpenConnection() { + if (_connection == null) { + _connection = CreateConnection(Host, Username, Password, Database); + } + _connection.Open(); + } + + public Dictionary SelectAll() { + Dictionary records = new Dictionary(); + OpenConnection(); + using (MySqlCommand command = new MySqlCommand("SELECT `data`, LENGTH(`data`) AS `datalength` FROM `data`", _connection)) + using (MySqlDataReader reader = command.ExecuteReader()) { + while (reader.Read()) { + int dataBytesLength = reader.GetInt32(1); + byte[] dataBytes = new byte[dataBytesLength]; + reader.GetBytes(0, 0, dataBytes, 0, dataBytesLength); + IRecord record = (IRecord)ConfigSerializer.DecryptObject(dataBytes, AESPassword); + records.Add(record.Id, record); + } + } + _connection.Close(); + return records; + } + + public void Insert(IRecord record) { + OpenConnection(); + using (MySqlCommand command = new MySqlCommand("INSERT INTO `data` (`id`, `group`, `data`) VALUES (@id, @group, @data)", _connection)) { + command.Parameters.AddWithValue("@id", record.Id.ToByteArray()); + if (record.GroupId == Guid.Empty) { + command.Parameters.AddWithValue("@group", null); + } else { + command.Parameters.AddWithValue("@group", record.GroupId.ToByteArray()); + } + command.Parameters.AddWithValue("@data", ConfigSerializer.EncryptObject(record, AESPassword)); + command.ExecuteNonQuery(); + } + _connection.Close(); + } + + public void Update(IRecord record) { + OpenConnection(); + using (MySqlCommand command = new MySqlCommand("UPDATE `data` SET `group` = @group, `data` = @data WHERE `id` = @id", _connection)) { + if (record.GroupId == Guid.Empty) { + command.Parameters.AddWithValue("@group", null); + } else { + command.Parameters.AddWithValue("@group", record.GroupId.ToByteArray()); + } + command.Parameters.AddWithValue("@data", ConfigSerializer.EncryptObject(record, AESPassword)); + command.Parameters.AddWithValue("@id", record.Id.ToByteArray()); + command.ExecuteNonQuery(); + } + _connection.Close(); + } + + public void Delete(IRecord record) { + OpenConnection(); + using (MySqlCommand command = new MySqlCommand("DELETE FROM `data` WHERE `id` = @id", _connection)) { + command.Parameters.AddWithValue("@id", record.Id.ToByteArray()); + command.ExecuteNonQuery(); + } + _connection.Close(); + } + + public UserControl GetControl() { + if (_control == null || _control.IsDisposed) { + _control = new MySQLControl(this); + } + return _control; + } + + public void UpdateFromControl() { + Host = _control.textBoxHostAddress.Text; + Username = _control.textBoxUsername.Text; + Password = _control.textBoxPassword.Text; + Database = _control.textBoxDatabase.Text; + AESPassword = _control.textBoxEncPassword.Text; + _connection = null; + } + + public bool SchemaExists(string host, string username, string password, string database) { + using (MySqlConnection tempConnection = CreateConnection(host, username, password, database)) { + tempConnection.Open(); + using (MySqlCommand command = new MySqlCommand("SHOW TABLES LIKE \"data\"", tempConnection)) + using (MySqlDataReader reader = command.ExecuteReader()) { + return reader.HasRows; + } + } + } + + public void CreateSchema(string host, string username, string password, string database) { + using (MySqlConnection tempConnection = CreateConnection(host, username, password, database)) { + tempConnection.Open(); + using (MySqlCommand command = new MySqlCommand(@"DROP TABLE IF EXISTS `data`; +CREATE TABLE `data` ( + `id` binary(16) NOT NULL, + `group` binary(16), + `data` blob NOT NULL, + PRIMARY KEY (`id`), + FOREIGN KEY (`group`) REFERENCES `data`(`id`) ON DELETE CASCADE ON UPDATE CASCADE +) ENGINE=InnoDB;", tempConnection)) { + command.ExecuteNonQuery(); + } + } + } + + public string TestConnection(string host, string username, string password, string database) { + using (MySqlConnection tempConnection = CreateConnection(host, username, password, database)) { + tempConnection.Open(); + using (MySqlCommand command = new MySqlCommand("SELECT VERSION()", tempConnection)) { + return (string)command.ExecuteScalar(); + } + } + } + } +} diff --git a/RCM/DataSource/MySQL/MySQL.resx b/RCM/DataSource/MySQL/MySQL.resx new file mode 100644 index 0000000..76f67cf --- /dev/null +++ b/RCM/DataSource/MySQL/MySQL.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\MySQL.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/RCM/DataSource/MySQL/MySQLControl.Designer.cs b/RCM/DataSource/MySQL/MySQLControl.Designer.cs new file mode 100644 index 0000000..a266287 --- /dev/null +++ b/RCM/DataSource/MySQL/MySQLControl.Designer.cs @@ -0,0 +1,206 @@ +namespace RCM { + partial class MySQLControl { + /// + /// 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 Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() { + this.buttonUnmaskPassword = new System.Windows.Forms.Button(); + this.textBoxPassword = new System.Windows.Forms.TextBox(); + this.textBoxUsername = new System.Windows.Forms.TextBox(); + this.textBoxHostAddress = new System.Windows.Forms.TextBox(); + this.labelPassword = new System.Windows.Forms.Label(); + this.labelUsername = new System.Windows.Forms.Label(); + this.labelHostAddress = new System.Windows.Forms.Label(); + this.buttonUnmaskEncPassword = new System.Windows.Forms.Button(); + this.textBoxEncPassword = new System.Windows.Forms.TextBox(); + this.textBoxDatabase = new System.Windows.Forms.TextBox(); + this.label1 = new System.Windows.Forms.Label(); + this.label2 = new System.Windows.Forms.Label(); + this.buttonTestConnection = new System.Windows.Forms.Button(); + this.buttonCreateSchema = new System.Windows.Forms.Button(); + this.SuspendLayout(); + // + // buttonUnmaskPassword + // + this.buttonUnmaskPassword.Location = new System.Drawing.Point(273, 51); + this.buttonUnmaskPassword.Name = "buttonUnmaskPassword"; + this.buttonUnmaskPassword.Size = new System.Drawing.Size(22, 22); + this.buttonUnmaskPassword.TabIndex = 4; + this.buttonUnmaskPassword.Text = "●"; + this.buttonUnmaskPassword.UseVisualStyleBackColor = true; + this.buttonUnmaskPassword.Click += new System.EventHandler(this.ButtonUnmaskPassword_Click); + // + // textBoxPassword + // + this.textBoxPassword.Location = new System.Drawing.Point(120, 52); + this.textBoxPassword.Name = "textBoxPassword"; + this.textBoxPassword.Size = new System.Drawing.Size(147, 20); + this.textBoxPassword.TabIndex = 3; + this.textBoxPassword.UseSystemPasswordChar = true; + // + // textBoxUsername + // + this.textBoxUsername.Location = new System.Drawing.Point(120, 26); + this.textBoxUsername.Name = "textBoxUsername"; + this.textBoxUsername.Size = new System.Drawing.Size(175, 20); + this.textBoxUsername.TabIndex = 2; + // + // textBoxHostAddress + // + this.textBoxHostAddress.Location = new System.Drawing.Point(120, 0); + this.textBoxHostAddress.Name = "textBoxHostAddress"; + this.textBoxHostAddress.Size = new System.Drawing.Size(175, 20); + this.textBoxHostAddress.TabIndex = 1; + // + // labelPassword + // + this.labelPassword.AutoSize = true; + this.labelPassword.Location = new System.Drawing.Point(0, 55); + this.labelPassword.Name = "labelPassword"; + this.labelPassword.Size = new System.Drawing.Size(56, 13); + this.labelPassword.TabIndex = 14; + this.labelPassword.Text = "Password:"; + // + // labelUsername + // + this.labelUsername.AutoSize = true; + this.labelUsername.Location = new System.Drawing.Point(0, 29); + this.labelUsername.Name = "labelUsername"; + this.labelUsername.Size = new System.Drawing.Size(58, 13); + this.labelUsername.TabIndex = 13; + this.labelUsername.Text = "Username:"; + // + // labelHostAddress + // + this.labelHostAddress.AutoSize = true; + this.labelHostAddress.Location = new System.Drawing.Point(0, 3); + this.labelHostAddress.Name = "labelHostAddress"; + this.labelHostAddress.Size = new System.Drawing.Size(72, 13); + this.labelHostAddress.TabIndex = 12; + this.labelHostAddress.Text = "Host address:"; + // + // buttonUnmaskEncPassword + // + this.buttonUnmaskEncPassword.Location = new System.Drawing.Point(273, 104); + this.buttonUnmaskEncPassword.Name = "buttonUnmaskEncPassword"; + this.buttonUnmaskEncPassword.Size = new System.Drawing.Size(22, 22); + this.buttonUnmaskEncPassword.TabIndex = 17; + this.buttonUnmaskEncPassword.Text = "●"; + this.buttonUnmaskEncPassword.UseVisualStyleBackColor = true; + this.buttonUnmaskEncPassword.Click += new System.EventHandler(this.ButtonUnmaskEncPassword_Click); + // + // textBoxEncPassword + // + this.textBoxEncPassword.Location = new System.Drawing.Point(120, 105); + this.textBoxEncPassword.Name = "textBoxEncPassword"; + this.textBoxEncPassword.Size = new System.Drawing.Size(147, 20); + this.textBoxEncPassword.TabIndex = 16; + this.textBoxEncPassword.UseSystemPasswordChar = true; + // + // textBoxDatabase + // + this.textBoxDatabase.Location = new System.Drawing.Point(120, 79); + this.textBoxDatabase.Name = "textBoxDatabase"; + this.textBoxDatabase.Size = new System.Drawing.Size(175, 20); + this.textBoxDatabase.TabIndex = 15; + // + // label1 + // + this.label1.AutoSize = true; + this.label1.Location = new System.Drawing.Point(0, 108); + this.label1.Name = "label1"; + this.label1.Size = new System.Drawing.Size(108, 13); + this.label1.TabIndex = 19; + this.label1.Text = "Encryption password:"; + // + // label2 + // + this.label2.AutoSize = true; + this.label2.Location = new System.Drawing.Point(0, 82); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(56, 13); + this.label2.TabIndex = 18; + this.label2.Text = "Database:"; + // + // buttonTestConnection + // + this.buttonTestConnection.Location = new System.Drawing.Point(120, 132); + this.buttonTestConnection.Name = "buttonTestConnection"; + this.buttonTestConnection.Size = new System.Drawing.Size(175, 23); + this.buttonTestConnection.TabIndex = 20; + this.buttonTestConnection.Text = "Test connection"; + this.buttonTestConnection.UseVisualStyleBackColor = true; + this.buttonTestConnection.Click += new System.EventHandler(this.ButtonTestConnection_Click); + // + // buttonCreateSchema + // + this.buttonCreateSchema.Location = new System.Drawing.Point(120, 161); + this.buttonCreateSchema.Name = "buttonCreateSchema"; + this.buttonCreateSchema.Size = new System.Drawing.Size(175, 23); + this.buttonCreateSchema.TabIndex = 21; + this.buttonCreateSchema.Text = "Create schema"; + this.buttonCreateSchema.UseVisualStyleBackColor = true; + this.buttonCreateSchema.Click += new System.EventHandler(this.ButtonCreateSchema_Click); + // + // MySQLControl + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.buttonCreateSchema); + this.Controls.Add(this.buttonTestConnection); + this.Controls.Add(this.buttonUnmaskEncPassword); + this.Controls.Add(this.textBoxEncPassword); + this.Controls.Add(this.textBoxDatabase); + this.Controls.Add(this.label1); + this.Controls.Add(this.label2); + this.Controls.Add(this.buttonUnmaskPassword); + this.Controls.Add(this.textBoxPassword); + this.Controls.Add(this.textBoxUsername); + this.Controls.Add(this.textBoxHostAddress); + this.Controls.Add(this.labelPassword); + this.Controls.Add(this.labelUsername); + this.Controls.Add(this.labelHostAddress); + this.Name = "MySQLControl"; + this.Size = new System.Drawing.Size(300, 190); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.Button buttonUnmaskPassword; + private System.Windows.Forms.Label labelPassword; + private System.Windows.Forms.Label labelUsername; + private System.Windows.Forms.Label labelHostAddress; + public System.Windows.Forms.TextBox textBoxHostAddress; + public System.Windows.Forms.TextBox textBoxUsername; + public System.Windows.Forms.TextBox textBoxPassword; + private System.Windows.Forms.Button buttonUnmaskEncPassword; + public System.Windows.Forms.TextBox textBoxEncPassword; + public System.Windows.Forms.TextBox textBoxDatabase; + private System.Windows.Forms.Label label1; + private System.Windows.Forms.Label label2; + private System.Windows.Forms.Button buttonTestConnection; + private System.Windows.Forms.Button buttonCreateSchema; + } +} diff --git a/RCM/DataSource/MySQL/MySQLControl.cs b/RCM/DataSource/MySQL/MySQLControl.cs new file mode 100644 index 0000000..a21afee --- /dev/null +++ b/RCM/DataSource/MySQL/MySQLControl.cs @@ -0,0 +1,43 @@ +using System; +using System.Windows.Forms; + +namespace RCM { + public partial class MySQLControl : UserControl { + private MySQL _mySQL; + + public MySQLControl(MySQL mySQL) { + InitializeComponent(); + _mySQL = mySQL; + textBoxHostAddress.Text = mySQL.Host; + textBoxUsername.Text = mySQL.Username; + textBoxPassword.Text = mySQL.Password; + textBoxDatabase.Text = mySQL.Database; + textBoxEncPassword.Text = mySQL.AESPassword; + } + + private void ButtonUnmaskPassword_Click(object sender, EventArgs e) { + textBoxPassword.UseSystemPasswordChar = !textBoxPassword.UseSystemPasswordChar; + } + + private void ButtonTestConnection_Click(object sender, EventArgs e) { + try { + string version = _mySQL.TestConnection(textBoxHostAddress.Text, textBoxUsername.Text, textBoxPassword.Text, textBoxDatabase.Text); + MessageBox.Show(version, "Test successful", MessageBoxButtons.OK, MessageBoxIcon.Information); + } catch (Exception ex) { + MessageBox.Show(ex.Message, "Test failed", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + private void ButtonCreateSchema_Click(object sender, EventArgs e) { + bool schemaExists = _mySQL.SchemaExists(textBoxHostAddress.Text, textBoxUsername.Text, textBoxPassword.Text, textBoxDatabase.Text); + if (!schemaExists || MessageBox.Show("Do you want to drop existing schema and create a new one?", "Schema already exists", MessageBoxButtons.YesNo, MessageBoxIcon.Exclamation) == DialogResult.Yes) { + _mySQL.CreateSchema(textBoxHostAddress.Text, textBoxUsername.Text, textBoxPassword.Text, textBoxDatabase.Text); + MessageBox.Show("New schema successfully created", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + } + + private void ButtonUnmaskEncPassword_Click(object sender, EventArgs e) { + textBoxEncPassword.UseSystemPasswordChar = !textBoxEncPassword.UseSystemPasswordChar; + } + } +} diff --git a/RCM/DataSource/SQLite/SQLite.cs b/RCM/DataSource/SQLite/SQLite.cs new file mode 100644 index 0000000..e0e8424 --- /dev/null +++ b/RCM/DataSource/SQLite/SQLite.cs @@ -0,0 +1,137 @@ +using System; +using System.Collections.Generic; +using System.Data.SQLite; +using System.Windows.Forms; + +namespace RCM { + [Serializable] + public class SQLite : IDataSource { + public string Title { get; set; } + public HashSet ExpandedNodes { get; } = new HashSet(); + + public string FileName { get; set; } + public string AESPassword { get; set; } + + [NonSerialized] + private SQLiteConnection _connection; + [NonSerialized] + private SQLiteControl _control; + + public override string ToString() => Title; + + private SQLiteConnection CreateConnection(string fileName, bool failIfMissing) { + return new SQLiteConnection(string.Format("Data Source={0};Version=3;FailIfMissing={1}", fileName, failIfMissing ? "True" : "False")); + } + + private void OpenConnection() { + if (_connection == null) { + _connection = CreateConnection(FileName, true); + } + _connection.Open(); + } + + public Dictionary SelectAll() { + Dictionary records = new Dictionary(); + OpenConnection(); + using (SQLiteCommand command = new SQLiteCommand("SELECT \"data\", LENGTH(\"data\") AS \"datalength\" FROM \"data\"", _connection)) + using (SQLiteDataReader reader = command.ExecuteReader()) { + while (reader.Read()) { + int dataBytesLength = reader.GetInt32(1); + byte[] dataBytes = new byte[dataBytesLength]; + reader.GetBytes(0, 0, dataBytes, 0, dataBytesLength); + IRecord record = (IRecord)ConfigSerializer.DecryptObject(dataBytes, AESPassword); + records.Add(record.Id, record); + } + } + _connection.Close(); + return records; + } + + public void Insert(IRecord record) { + OpenConnection(); + EnableForeignKeys(); + using (SQLiteCommand command = new SQLiteCommand("INSERT INTO \"data\" (\"id\", \"group\", \"data\") VALUES (@id, @group, @data)", _connection)) { + command.Parameters.AddWithValue("@id", record.Id.ToByteArray()); + if (record.GroupId == Guid.Empty) { + command.Parameters.AddWithValue("@group", null); + } else { + command.Parameters.AddWithValue("@group", record.GroupId.ToByteArray()); + } + command.Parameters.AddWithValue("@data", ConfigSerializer.EncryptObject(record, AESPassword)); + command.ExecuteNonQuery(); + } + _connection.Close(); + } + + public void Update(IRecord record) { + OpenConnection(); + EnableForeignKeys(); + using (SQLiteCommand command = new SQLiteCommand("UPDATE \"data\" SET \"group\" = @group, \"data\" = @data WHERE \"id\" = @id", _connection)) { + if (record.GroupId == Guid.Empty) { + command.Parameters.AddWithValue("@group", null); + } else { + command.Parameters.AddWithValue("@group", record.GroupId.ToByteArray()); + } + command.Parameters.AddWithValue("@data", ConfigSerializer.EncryptObject(record, AESPassword)); + command.Parameters.AddWithValue("@id", record.Id); + command.ExecuteNonQuery(); + } + _connection.Close(); + } + + public void Delete(IRecord record) { + OpenConnection(); + EnableForeignKeys(); + using (SQLiteCommand command = new SQLiteCommand("DELETE FROM \"data\" WHERE \"id\" = @id", _connection)) { + command.Parameters.AddWithValue("@id", record.Id.ToByteArray()); + command.ExecuteNonQuery(); + } + _connection.Close(); + } + + private void EnableForeignKeys() { + using (SQLiteCommand command = new SQLiteCommand("PRAGMA foreign_keys = ON", _connection)) { + command.ExecuteNonQuery(); + } + } + + public UserControl GetControl() { + if (_control == null || _control.IsDisposed) { + _control = new SQLiteControl(this); + } + return _control; + } + + public void UpdateFromControl() { + FileName = _control.textBoxDatabase.Text; + AESPassword = _control.textBoxEncPassword.Text; + _connection = null; + } + + public void CreateSchema(string fileName) { + using (SQLiteConnection tempConnection = CreateConnection(fileName, false)) { + tempConnection.Open(); + SQLiteCommand command = new SQLiteCommand(@" +DROP TABLE IF EXISTS ""data""; +CREATE TABLE ""data"" ( + ""id"" binary(16) NOT NULL, + ""group"" binary(16), + ""data"" blob NOT NULL, + PRIMARY KEY (""id""), + FOREIGN KEY (""group"") REFERENCES ""data""(""id"") ON DELETE CASCADE ON UPDATE CASCADE +);", tempConnection); + command.ExecuteNonQuery(); + tempConnection.Close(); + } + } + + public string TestConnection(string fileName) { + using (SQLiteConnection tempConnection = CreateConnection(fileName, true)) { + tempConnection.Open(); + using (SQLiteCommand command = new SQLiteCommand("PRAGMA quick_check", tempConnection)) { + return (string)command.ExecuteScalar(); + } + } + } + } +} diff --git a/RCM/DataSource/SQLite/SQLite.resx b/RCM/DataSource/SQLite/SQLite.resx new file mode 100644 index 0000000..18231a3 --- /dev/null +++ b/RCM/DataSource/SQLite/SQLite.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\SQLite.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/RCM/DataSource/SQLite/SQLiteControl.Designer.cs b/RCM/DataSource/SQLite/SQLiteControl.Designer.cs new file mode 100644 index 0000000..664563d --- /dev/null +++ b/RCM/DataSource/SQLite/SQLiteControl.Designer.cs @@ -0,0 +1,146 @@ +namespace RCM { + partial class SQLiteControl { + /// + /// 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 Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() { + this.buttonSelectDatabase = new System.Windows.Forms.Button(); + this.buttonUnmaskEncPassword = new System.Windows.Forms.Button(); + this.textBoxEncPassword = new System.Windows.Forms.TextBox(); + this.textBoxDatabase = new System.Windows.Forms.TextBox(); + this.labelEncPassword = new System.Windows.Forms.Label(); + this.labelDatabase = new System.Windows.Forms.Label(); + this.buttonTestConnection = new System.Windows.Forms.Button(); + this.buttonCreateSchema = new System.Windows.Forms.Button(); + this.saveFileDialog = new System.Windows.Forms.SaveFileDialog(); + this.SuspendLayout(); + // + // buttonSelectDatabase + // + this.buttonSelectDatabase.Location = new System.Drawing.Point(273, -1); + this.buttonSelectDatabase.Name = "buttonSelectDatabase"; + this.buttonSelectDatabase.Size = new System.Drawing.Size(22, 22); + this.buttonSelectDatabase.TabIndex = 4; + this.buttonSelectDatabase.Text = "⋯"; + this.buttonSelectDatabase.UseVisualStyleBackColor = true; + this.buttonSelectDatabase.Click += new System.EventHandler(this.ButtonSelectDatabase_Click); + // + // buttonUnmaskEncPassword + // + this.buttonUnmaskEncPassword.Location = new System.Drawing.Point(273, 25); + this.buttonUnmaskEncPassword.Name = "buttonUnmaskEncPassword"; + this.buttonUnmaskEncPassword.Size = new System.Drawing.Size(22, 22); + this.buttonUnmaskEncPassword.TabIndex = 17; + this.buttonUnmaskEncPassword.Text = "●"; + this.buttonUnmaskEncPassword.UseVisualStyleBackColor = true; + this.buttonUnmaskEncPassword.Click += new System.EventHandler(this.ButtonUnmaskEncPassword_Click); + // + // textBoxEncPassword + // + this.textBoxEncPassword.Location = new System.Drawing.Point(120, 26); + this.textBoxEncPassword.Name = "textBoxEncPassword"; + this.textBoxEncPassword.Size = new System.Drawing.Size(147, 20); + this.textBoxEncPassword.TabIndex = 16; + this.textBoxEncPassword.UseSystemPasswordChar = true; + // + // textBoxDatabase + // + this.textBoxDatabase.Location = new System.Drawing.Point(120, 0); + this.textBoxDatabase.Name = "textBoxDatabase"; + this.textBoxDatabase.Size = new System.Drawing.Size(147, 20); + this.textBoxDatabase.TabIndex = 15; + // + // labelEncPassword + // + this.labelEncPassword.AutoSize = true; + this.labelEncPassword.Location = new System.Drawing.Point(0, 29); + this.labelEncPassword.Name = "labelEncPassword"; + this.labelEncPassword.Size = new System.Drawing.Size(108, 13); + this.labelEncPassword.TabIndex = 19; + this.labelEncPassword.Text = "Encryption password:"; + // + // labelDatabase + // + this.labelDatabase.AutoSize = true; + this.labelDatabase.Location = new System.Drawing.Point(0, 3); + this.labelDatabase.Name = "labelDatabase"; + this.labelDatabase.Size = new System.Drawing.Size(72, 13); + this.labelDatabase.TabIndex = 18; + this.labelDatabase.Text = "Database file:"; + // + // buttonTestConnection + // + this.buttonTestConnection.Location = new System.Drawing.Point(120, 53); + this.buttonTestConnection.Name = "buttonTestConnection"; + this.buttonTestConnection.Size = new System.Drawing.Size(175, 23); + this.buttonTestConnection.TabIndex = 20; + this.buttonTestConnection.Text = "Test connection"; + this.buttonTestConnection.UseVisualStyleBackColor = true; + this.buttonTestConnection.Click += new System.EventHandler(this.ButtonTestConnection_Click); + // + // buttonCreateSchema + // + this.buttonCreateSchema.Location = new System.Drawing.Point(120, 82); + this.buttonCreateSchema.Name = "buttonCreateSchema"; + this.buttonCreateSchema.Size = new System.Drawing.Size(175, 23); + this.buttonCreateSchema.TabIndex = 21; + this.buttonCreateSchema.Text = "Create schema"; + this.buttonCreateSchema.UseVisualStyleBackColor = true; + this.buttonCreateSchema.Click += new System.EventHandler(this.ButtonCreateSchema_Click); + // + // saveFileDialog + // + this.saveFileDialog.DefaultExt = "sqlite"; + this.saveFileDialog.Filter = "SQLite database|*.sqlite"; + // + // SQLiteControl + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.buttonCreateSchema); + this.Controls.Add(this.buttonTestConnection); + this.Controls.Add(this.buttonUnmaskEncPassword); + this.Controls.Add(this.textBoxEncPassword); + this.Controls.Add(this.textBoxDatabase); + this.Controls.Add(this.labelEncPassword); + this.Controls.Add(this.labelDatabase); + this.Controls.Add(this.buttonSelectDatabase); + this.Name = "SQLiteControl"; + this.Size = new System.Drawing.Size(300, 110); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.Button buttonSelectDatabase; + private System.Windows.Forms.Button buttonUnmaskEncPassword; + public System.Windows.Forms.TextBox textBoxEncPassword; + public System.Windows.Forms.TextBox textBoxDatabase; + private System.Windows.Forms.Label labelEncPassword; + private System.Windows.Forms.Label labelDatabase; + private System.Windows.Forms.Button buttonTestConnection; + private System.Windows.Forms.Button buttonCreateSchema; + private System.Windows.Forms.SaveFileDialog saveFileDialog; + } +} diff --git a/RCM/DataSource/SQLite/SQLiteControl.cs b/RCM/DataSource/SQLite/SQLiteControl.cs new file mode 100644 index 0000000..d4bac8a --- /dev/null +++ b/RCM/DataSource/SQLite/SQLiteControl.cs @@ -0,0 +1,46 @@ +using System; +using System.IO; +using System.Windows.Forms; + +namespace RCM { + public partial class SQLiteControl : UserControl { + private SQLite _sqLite; + + public SQLiteControl(SQLite sqLite) { + InitializeComponent(); + _sqLite = sqLite; + textBoxDatabase.Text = _sqLite.FileName; + textBoxEncPassword.Text = _sqLite.AESPassword; + } + + private void ButtonSelectDatabase_Click(object sender, EventArgs e) { + if (saveFileDialog.ShowDialog() == DialogResult.OK) { + textBoxDatabase.Text = saveFileDialog.FileName; + } + } + + private void ButtonTestConnection_Click(object sender, EventArgs e) { + try { + string version = _sqLite.TestConnection(textBoxDatabase.Text); + MessageBox.Show(version, "Test successful", MessageBoxButtons.OK, MessageBoxIcon.Information); + } catch (Exception ex) { + MessageBox.Show(ex.Message, "Test failed", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + private void ButtonCreateSchema_Click(object sender, EventArgs e) { + if (File.Exists(textBoxDatabase.Text)) { + if (MessageBox.Show("Do you want to delete existing database and create a new one?", "Database already exists", MessageBoxButtons.YesNo, MessageBoxIcon.Exclamation) != DialogResult.Yes) { + return; + } + File.Delete(textBoxDatabase.Text); + } + _sqLite.CreateSchema(textBoxDatabase.Text); + MessageBox.Show("New database successfully created", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information); + } + + private void ButtonUnmaskEncPassword_Click(object sender, EventArgs e) { + textBoxEncPassword.UseSystemPasswordChar = !textBoxEncPassword.UseSystemPasswordChar; + } + } +} diff --git a/RCM/DisplayName.cs b/RCM/DisplayName.cs new file mode 100644 index 0000000..6e03a83 --- /dev/null +++ b/RCM/DisplayName.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace RCM { + public class DisplayName : Attribute { + public string Text { get; private set; } + + public DisplayName(string text) { + Text = text; + } + } +} diff --git a/RCM/Group.cs b/RCM/Group.cs new file mode 100644 index 0000000..56fcb10 --- /dev/null +++ b/RCM/Group.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; +using System; +using System.Windows.Forms; + +namespace RCM { + [Serializable] + [DisplayName("Group")] + public class Group : IRecord { // TODO: Prvni v poradi new items + public Guid Id { get; set; } + public string Title { get; set; } + public Guid GroupId { get; set; } + + [NonSerialized] + private readonly IAction[] _actions = new IAction[0]; + + public IAction[] GetActions() => _actions; + public UserControl GetControl() => new UserControl(); + public void UpdateFromControl() { } + } +} diff --git a/RCM/Group.resx b/RCM/Group.resx new file mode 100644 index 0000000..1f5f928 --- /dev/null +++ b/RCM/Group.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\Group.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/RCM/GroupPath.cs b/RCM/GroupPath.cs new file mode 100644 index 0000000..e386a18 --- /dev/null +++ b/RCM/GroupPath.cs @@ -0,0 +1,23 @@ +using System; +using System.Windows.Forms; + +namespace RCM { + public class GroupPath { + public Guid Id { get; private set; } + public TreeNode Node { get; private set; } + public string FullPath { get; private set; } + + public GroupPath(TreeNode node) { + Node = node; + if (node == null) { + Id = Guid.Empty; + FullPath = "Root"; + } else { + Id = ((IRecord)node.Tag).Id; + FullPath = node.FullPath; + } + } + + public override string ToString() => FullPath; + } +} diff --git a/RCM/IAction.cs b/RCM/IAction.cs new file mode 100644 index 0000000..ce5952f --- /dev/null +++ b/RCM/IAction.cs @@ -0,0 +1,5 @@ +namespace RCM { + public interface IAction { + void Run(); + } +} diff --git a/RCM/IConfigSection.cs b/RCM/IConfigSection.cs new file mode 100644 index 0000000..592458d --- /dev/null +++ b/RCM/IConfigSection.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using System.Windows.Forms; + +namespace RCM { + public interface IConfigSection { + UserControl GetControl(); + void UpdateFromControl(); + } +} diff --git a/RCM/IDataSource.cs b/RCM/IDataSource.cs new file mode 100644 index 0000000..74a55b1 --- /dev/null +++ b/RCM/IDataSource.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Windows.Forms; + +namespace RCM { + public interface IDataSource { + string Title { get; set; } + HashSet ExpandedNodes { get; } + + Dictionary SelectAll(); + void Insert(IRecord record); + void Update(IRecord record); + void Delete(IRecord record); + UserControl GetControl(); + void UpdateFromControl(); + + string ToString(); + } +} diff --git a/RCM/IRecord.cs b/RCM/IRecord.cs new file mode 100644 index 0000000..65aed6f --- /dev/null +++ b/RCM/IRecord.cs @@ -0,0 +1,15 @@ +using System; +using System.Windows.Forms; + +namespace RCM { + public interface IRecord { + Guid Id { get; set; } + string Title { get; set; } + Guid GroupId { get; set; } + // DateTime LastUpdate { get; set; } + + IAction[] GetActions(); + UserControl GetControl(); + void UpdateFromControl(); + } +} diff --git a/RCM/MainForm.Designer.cs b/RCM/MainForm.Designer.cs new file mode 100644 index 0000000..cb4117d --- /dev/null +++ b/RCM/MainForm.Designer.cs @@ -0,0 +1,249 @@ +namespace RCM +{ + partial class MainForm + { + /// + /// 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.components = new System.ComponentModel.Container(); + this.treeView = new System.Windows.Forms.TreeView(); + this.splitContainer = new System.Windows.Forms.SplitContainer(); + this.buttonConfig = new System.Windows.Forms.Button(); + this.comboBoxDatabase = new System.Windows.Forms.ComboBox(); + this.panelRecord = new System.Windows.Forms.Panel(); + this.buttonSave = new System.Windows.Forms.Button(); + this.labelGroup = new System.Windows.Forms.Label(); + this.comboBoxGroup = new System.Windows.Forms.ComboBox(); + this.textBoxTitle = new System.Windows.Forms.TextBox(); + this.labelTitle = new System.Windows.Forms.Label(); + this.timerDataSourceRefresh = new System.Windows.Forms.Timer(this.components); + this.statusStrip = new System.Windows.Forms.StatusStrip(); + this.toolStripStatusLabel = new System.Windows.Forms.ToolStripStatusLabel(); + this.toolStripStatusLastRefresh = new System.Windows.Forms.ToolStripStatusLabel(); + ((System.ComponentModel.ISupportInitialize)(this.splitContainer)).BeginInit(); + this.splitContainer.Panel1.SuspendLayout(); + this.splitContainer.Panel2.SuspendLayout(); + this.splitContainer.SuspendLayout(); + this.statusStrip.SuspendLayout(); + this.SuspendLayout(); + // + // treeView + // + this.treeView.AllowDrop = true; + this.treeView.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.treeView.Enabled = false; + this.treeView.Indent = 24; + this.treeView.LabelEdit = true; + this.treeView.Location = new System.Drawing.Point(3, 30); + this.treeView.Name = "treeView"; + this.treeView.Size = new System.Drawing.Size(244, 440); + this.treeView.TabIndex = 0; + this.treeView.AfterLabelEdit += new System.Windows.Forms.NodeLabelEditEventHandler(this.TreeView_AfterLabelEdit); + this.treeView.AfterCollapse += new System.Windows.Forms.TreeViewEventHandler(this.TreeView_AfterCollapse); + this.treeView.AfterExpand += new System.Windows.Forms.TreeViewEventHandler(this.TreeView_AfterExpand); + this.treeView.ItemDrag += new System.Windows.Forms.ItemDragEventHandler(this.TreeView_ItemDrag); + this.treeView.AfterSelect += new System.Windows.Forms.TreeViewEventHandler(this.TreeView_AfterSelect); + this.treeView.NodeMouseClick += new System.Windows.Forms.TreeNodeMouseClickEventHandler(this.TreeView_NodeMouseClick); + this.treeView.NodeMouseDoubleClick += new System.Windows.Forms.TreeNodeMouseClickEventHandler(this.TreeView_NodeMouseDoubleClick); + this.treeView.DragDrop += new System.Windows.Forms.DragEventHandler(this.TreeView_DragDrop); + this.treeView.DragEnter += new System.Windows.Forms.DragEventHandler(this.TreeView_DragEnter); + this.treeView.KeyDown += new System.Windows.Forms.KeyEventHandler(this.TreeView_KeyDown); + this.treeView.MouseUp += new System.Windows.Forms.MouseEventHandler(this.TreeView_MouseUp); + // + // splitContainer + // + this.splitContainer.Dock = System.Windows.Forms.DockStyle.Fill; + this.splitContainer.Location = new System.Drawing.Point(0, 0); + this.splitContainer.Name = "splitContainer"; + // + // splitContainer.Panel1 + // + this.splitContainer.Panel1.Controls.Add(this.buttonConfig); + this.splitContainer.Panel1.Controls.Add(this.treeView); + this.splitContainer.Panel1.Controls.Add(this.comboBoxDatabase); + // + // splitContainer.Panel2 + // + this.splitContainer.Panel2.Controls.Add(this.panelRecord); + this.splitContainer.Panel2.Controls.Add(this.buttonSave); + this.splitContainer.Panel2.Controls.Add(this.labelGroup); + this.splitContainer.Panel2.Controls.Add(this.comboBoxGroup); + this.splitContainer.Panel2.Controls.Add(this.textBoxTitle); + this.splitContainer.Panel2.Controls.Add(this.labelTitle); + this.splitContainer.Size = new System.Drawing.Size(574, 496); + this.splitContainer.SplitterDistance = 250; + this.splitContainer.TabIndex = 1; + // + // buttonConfig + // + this.buttonConfig.Image = global::RCM.Properties.Resources.ConfigIcon; + this.buttonConfig.Location = new System.Drawing.Point(3, 1); + this.buttonConfig.Name = "buttonConfig"; + this.buttonConfig.Size = new System.Drawing.Size(24, 24); + this.buttonConfig.TabIndex = 2; + this.buttonConfig.UseVisualStyleBackColor = true; + this.buttonConfig.Click += new System.EventHandler(this.ButtonConfig_Click); + // + // comboBoxDatabase + // + this.comboBoxDatabase.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.comboBoxDatabase.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.comboBoxDatabase.Location = new System.Drawing.Point(33, 3); + this.comboBoxDatabase.Name = "comboBoxDatabase"; + this.comboBoxDatabase.Size = new System.Drawing.Size(214, 21); + this.comboBoxDatabase.TabIndex = 1; + this.comboBoxDatabase.SelectedIndexChanged += new System.EventHandler(this.ComboBoxDatabase_SelectedIndexChanged); + // + // panelRecord + // + this.panelRecord.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.panelRecord.Location = new System.Drawing.Point(11, 82); + this.panelRecord.Name = "panelRecord"; + this.panelRecord.Size = new System.Drawing.Size(306, 350); + this.panelRecord.TabIndex = 15; + // + // buttonSave + // + this.buttonSave.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Right))); + this.buttonSave.Enabled = false; + this.buttonSave.Location = new System.Drawing.Point(131, 438); + this.buttonSave.Name = "buttonSave"; + this.buttonSave.Size = new System.Drawing.Size(175, 23); + this.buttonSave.TabIndex = 14; + this.buttonSave.Text = "Save changes"; + this.buttonSave.UseVisualStyleBackColor = true; + this.buttonSave.Click += new System.EventHandler(this.ButtonSave_Click); + // + // labelGroup + // + this.labelGroup.AutoSize = true; + this.labelGroup.Location = new System.Drawing.Point(11, 42); + this.labelGroup.Name = "labelGroup"; + this.labelGroup.Size = new System.Drawing.Size(39, 13); + this.labelGroup.TabIndex = 13; + this.labelGroup.Text = "Group:"; + // + // comboBoxGroup + // + this.comboBoxGroup.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.comboBoxGroup.Enabled = false; + this.comboBoxGroup.FormattingEnabled = true; + this.comboBoxGroup.Location = new System.Drawing.Point(131, 39); + this.comboBoxGroup.Name = "comboBoxGroup"; + this.comboBoxGroup.Size = new System.Drawing.Size(175, 21); + this.comboBoxGroup.TabIndex = 12; + // + // textBoxTitle + // + this.textBoxTitle.Enabled = false; + this.textBoxTitle.Location = new System.Drawing.Point(131, 13); + this.textBoxTitle.Name = "textBoxTitle"; + this.textBoxTitle.Size = new System.Drawing.Size(175, 20); + this.textBoxTitle.TabIndex = 1; + // + // labelTitle + // + this.labelTitle.AutoSize = true; + this.labelTitle.Location = new System.Drawing.Point(11, 16); + this.labelTitle.Name = "labelTitle"; + this.labelTitle.Size = new System.Drawing.Size(30, 13); + this.labelTitle.TabIndex = 0; + this.labelTitle.Text = "Title:"; + // + // timerDataSourceRefresh + // + this.timerDataSourceRefresh.Interval = 30000; + this.timerDataSourceRefresh.Tick += new System.EventHandler(this.TimerDataSourceRefresh_Tick); + // + // statusStrip + // + this.statusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + this.toolStripStatusLabel, + this.toolStripStatusLastRefresh}); + this.statusStrip.Location = new System.Drawing.Point(0, 474); + this.statusStrip.Name = "statusStrip"; + this.statusStrip.Size = new System.Drawing.Size(574, 22); + this.statusStrip.TabIndex = 0; + this.statusStrip.Text = "statusStrip1"; + // + // toolStripStatusLabel + // + this.toolStripStatusLabel.Name = "toolStripStatusLabel"; + this.toolStripStatusLabel.Size = new System.Drawing.Size(134, 17); + this.toolStripStatusLabel.Text = "Last data source refresh:"; + // + // toolStripStatusLastRefresh + // + this.toolStripStatusLastRefresh.Name = "toolStripStatusLastRefresh"; + this.toolStripStatusLastRefresh.Size = new System.Drawing.Size(36, 17); + this.toolStripStatusLastRefresh.Text = "never"; + // + // MainForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(574, 496); + this.Controls.Add(this.statusStrip); + this.Controls.Add(this.splitContainer); + this.Name = "MainForm"; + this.Text = "Remote Connection Manager"; + this.splitContainer.Panel1.ResumeLayout(false); + this.splitContainer.Panel2.ResumeLayout(false); + this.splitContainer.Panel2.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)(this.splitContainer)).EndInit(); + this.splitContainer.ResumeLayout(false); + this.statusStrip.ResumeLayout(false); + this.statusStrip.PerformLayout(); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.TreeView treeView; + private System.Windows.Forms.SplitContainer splitContainer; + private System.Windows.Forms.ComboBox comboBoxDatabase; + private System.Windows.Forms.TextBox textBoxTitle; + private System.Windows.Forms.Label labelTitle; + private System.Windows.Forms.Label labelGroup; + private System.Windows.Forms.ComboBox comboBoxGroup; + private System.Windows.Forms.Button buttonSave; + private System.Windows.Forms.Panel panelRecord; + private System.Windows.Forms.Timer timerDataSourceRefresh; + private System.Windows.Forms.Button buttonConfig; + private System.Windows.Forms.StatusStrip statusStrip; + private System.Windows.Forms.ToolStripStatusLabel toolStripStatusLabel; + private System.Windows.Forms.ToolStripStatusLabel toolStripStatusLastRefresh; + } +} + diff --git a/RCM/MainForm.cs b/RCM/MainForm.cs new file mode 100644 index 0000000..9b3f854 --- /dev/null +++ b/RCM/MainForm.cs @@ -0,0 +1,463 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Windows.Forms; +using System.Linq; +using System.Collections.Specialized; + +namespace RCM { + public partial class MainForm : Form { + #region Form + protected override CreateParams CreateParams { // Workaround for control flickering reduction + get { + CreateParams cp = base.CreateParams; + cp.ExStyle |= 0x02000000; // WS_EX_COMPOSITED + return cp; + } + } + + public MainForm() { + InitializeComponent(); + treeView.ImageList = Config.IconList; + treeView.ContextMenuStrip = GetNewRecordContextMenu(); + treeView.TreeViewNodeSorter = new RecordNodeSorter(); + LoadDataSources(0); + } + + private void LoadDataSources(int selectedIndex) { + comboBoxDatabase.Items.Clear(); + comboBoxDatabase.Items.AddRange(Config.DataSources.ToArray()); + if (comboBoxDatabase.Items.Count == 0) { + treeView.Enabled = false; + treeView.Nodes.Clear(); + } else { + treeView.Enabled = true; + comboBoxDatabase.SelectedIndex = selectedIndex != -1 ? selectedIndex : 0; + } + } + + private void ButtonConfig_Click(object sender, EventArgs e) { + int selectedIndex = comboBoxDatabase.SelectedIndex; + ConfigForm configForm = new ConfigForm(); + configForm.ShowDialog(); + configForm.Dispose(); + LoadDataSources(selectedIndex); + } + #endregion + + #region ComboBox & DataSource + private void RebuildGroupCombobox(IRecord record) { + List groupPaths = new List() { new GroupPath(null) }; + groupPaths.AddRange(RebuildGroupCombobox(treeView.Nodes)); + comboBoxGroup.Items.Clear(); + comboBoxGroup.Items.AddRange(groupPaths.ToArray()); + comboBoxGroup.SelectedItem = groupPaths.First(gp => gp.Id == record.GroupId); + } + + private List RebuildGroupCombobox(TreeNodeCollection nodes) { + List groupPaths = new List(); + foreach (TreeNode node in nodes) { + if ((IRecord)node.Tag is Group && !ReferenceEquals(node, treeView.SelectedNode)) { + groupPaths.Add(new GroupPath(node)); + groupPaths.AddRange(RebuildGroupCombobox(node.Nodes)); + } + } + return groupPaths; + } + + private void ReloadDataSource() { + RebuildTreeView(); + timerDataSourceRefresh.Stop(); + timerDataSourceRefresh.Start(); + } + + private void ComboBoxDatabase_SelectedIndexChanged(object sender, EventArgs e) => ReloadDataSource(); + + private void RefreshContextMenuItem_Click(object sender, EventArgs e) => ReloadDataSource(); + + private void TimerDataSourceRefresh_Tick(object sender, EventArgs e) { + RebuildTreeView(); + } + #endregion + + #region TreeView display + + /* test */ + private void RebuildTreeView() { + // Select all records from database + IDataSource dataSource = (IDataSource)comboBoxDatabase.SelectedItem; + Dictionary dataSourceRecords = dataSource.SelectAll(); + Guid selectedGuid = treeView.SelectedNode == null ? Guid.Empty : ((IRecord)treeView.SelectedNode.Tag).Id; + + // Flatten tree nodes + List flattenedTreeList = new List(); + int lastProcessPosition = 0; + flattenedTreeList.AddRange(treeView.Nodes.Cast()); + while (lastProcessPosition < flattenedTreeList.Count) { + flattenedTreeList.AddRange(flattenedTreeList[lastProcessPosition++].Nodes.Cast()); + } + Dictionary flattenedTree = flattenedTreeList.ToDictionary(n => ((IRecord)n.Tag).Id, n => n); + + // Iterate over existing nodes + foreach (TreeNode node in flattenedTree.Values) { + IRecord record = (IRecord)node.Tag; + // Delete node if record no longer present in database + if (!dataSourceRecords.ContainsKey(record.Id)) { + node.Remove(); + continue; + } + // Update existing record if it has changed since the last refresh + IRecord updatedRecord = dataSourceRecords[record.Id]; + if (ConfigSerializer.ObjToString(record) != ConfigSerializer.ObjToString(updatedRecord)) { + node.Tag = updatedRecord; + node.Text = updatedRecord.Title; + if (updatedRecord.GroupId != record.GroupId) { + node.Remove(); + if (updatedRecord.GroupId == Guid.Empty) { + treeView.Nodes.Add(node); + } else { + flattenedTree[updatedRecord.GroupId].Nodes.Add(node); + } + } + } + // Remove processed record + dataSourceRecords.Remove(record.Id); + } + + // Create nodes from unprocessed (new) records + foreach (IRecord record in dataSourceRecords.Values) { + TreeNode node = CreateTreeNodeFromRecord(record); + if (dataSource.ExpandedNodes.Contains(record.Id)) { + node.Expand(); + } + flattenedTree.Add(record.Id, node); + } + + // Assign nodes to tree structure + foreach (IRecord record in dataSourceRecords.Values) { + TreeNode node = flattenedTree[record.Id]; + if (record.GroupId == Guid.Empty) { + treeView.Nodes.Add(node); + } else { + flattenedTree[record.GroupId].Nodes.Add(node); + } + } + + TreeNode selectedNode = flattenedTree.ContainsKey(selectedGuid) ? flattenedTree[selectedGuid] : null; + SortTreeAndSelectNode(selectedNode); + toolStripStatusLastRefresh.Text = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + } + /* test end */ + + /*private void RebuildTreeView() { + IDataSource dataSource = (IDataSource)comboBoxDatabase.SelectedItem; + Dictionary nodes = new Dictionary(); + + try { + foreach (IRecord record in dataSource.SelectAll()) { + nodes.Add(record.Id, CreateTreeNodeFromRecord(record)); + } + } catch { + MessageBox.Show("Failed to retieve records from data source", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + + Guid selectedGuid = treeView.SelectedNode == null ? Guid.Empty : ((IRecord)treeView.SelectedNode.Tag).Id; + TreeNode selectedNode = null; + treeView.Nodes.Clear(); + foreach (TreeNode node in nodes.Values) { + IRecord record = (IRecord)node.Tag; + if (record.GroupId == Guid.Empty) { + treeView.Nodes.Add(node); + } else { + nodes[record.GroupId].Nodes.Add(node); + } + if (record.Id == selectedGuid) { + selectedNode = node; + } + } + SortTreeAndSelectNode(selectedNode); + foreach (TreeNode node in nodes.Values) { + if (dataSource.ExpandedNodes.Contains(((IRecord)node.Tag).Id)) { + node.Expand(); + } + } + toolStripStatusLastRefresh.Text = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); + }*/ + + private TreeNode CreateTreeNodeFromRecord(IRecord record) { + string imageKey = record.GetType().FullName; + return new TreeNode(record.Title) { Tag = record, ImageKey = imageKey, SelectedImageKey = imageKey, ContextMenuStrip = GetContextMenu(record) }; + } + + private void SortTreeAndSelectNode(TreeNode node) { + treeView.Sort(); + if (treeView.SelectedNode == node) { + ChangeSelectedRecord(); + } else { + treeView.SelectedNode = node; + } + } + #endregion + + #region TreeView drag & drop + private void TreeView_ItemDrag(object sender, ItemDragEventArgs e) => DoDragDrop(e.Item, DragDropEffects.Move); + + private void TreeView_DragEnter(object sender, DragEventArgs e) => e.Effect = DragDropEffects.Move; + + private void TreeView_DragDrop(object sender, DragEventArgs e) { + TreeNode draggedNode = (TreeNode)e.Data.GetData(typeof(TreeNode)); + if (draggedNode == null) { + return; + } + TreeNode targetNode = treeView.GetNodeAt(treeView.PointToClient(new Point(e.X, e.Y))); + bool canDrop = true; + if (targetNode != null) { + TreeNode parentNode = targetNode; + canDrop = !draggedNode.Equals(targetNode) && targetNode != null && (IRecord)targetNode.Tag is Group; + while (canDrop && (parentNode != null)) { + canDrop = !ReferenceEquals(draggedNode, parentNode); + parentNode = parentNode.Parent; + } + } + if (canDrop) { + IRecord record = (IRecord)draggedNode.Tag; + record.GroupId = targetNode == null ? Guid.Empty : ((IRecord)targetNode.Tag).Id; + try { + ((IDataSource)comboBoxDatabase.SelectedItem).Update(record); + } catch { + MessageBox.Show("Failed to update record in data source", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + draggedNode.Remove(); + if (targetNode == null) { + treeView.Nodes.Add(draggedNode); + } else { + targetNode.Nodes.Add(draggedNode); + } + SortTreeAndSelectNode(draggedNode); + } + } + #endregion + + #region TreeView navigation & selection + private void TreeView_AfterSelect(object sender, TreeViewEventArgs e) => ChangeSelectedRecord(); + + private void TreeView_NodeMouseClick(object sender, TreeNodeMouseClickEventArgs e) => treeView.SelectedNode = e.Node; + + private void TreeView_MouseUp(object sender, MouseEventArgs e) { + if (treeView.HitTest(e.X, e.Y).Node == null) { + treeView.SelectedNode = null; + ChangeSelectedRecord(); + } + } + + private void TreeView_KeyDown(object sender, KeyEventArgs e) { + if (e.KeyCode == Keys.Delete) { + if (treeView.SelectedNode != null) { + DeleteSelectedNode(); + } + } else if (e.KeyCode == Keys.F2) { + if (treeView.SelectedNode != null) { + treeView.SelectedNode.BeginEdit(); + } + } else if (e.KeyCode == Keys.F5) { + ReloadDataSource(); + } + } + + private void TreeView_NodeMouseDoubleClick(object sender, TreeNodeMouseClickEventArgs e) { + IAction[] actions = ((IRecord)e.Node.Tag).GetActions(); + if (actions.Length == 0) { + return; + } + actions[0].Run(); + } + + private void ChangeSelectedRecord() { + bool isSelected = treeView.SelectedNode != null; + textBoxTitle.Enabled = isSelected; + textBoxTitle.Text = ""; + comboBoxGroup.Enabled = isSelected; + comboBoxGroup.SelectedItem = null; + buttonSave.Enabled = isSelected; + LoadRecordControl(); + } + + private void LoadRecordControl() { + foreach (Control control in panelRecord.Controls) { + panelRecord.Controls.Remove(control); + control.Dispose(); + } + if (treeView.SelectedNode != null) { + IRecord record = (IRecord)treeView.SelectedNode.Tag; + textBoxTitle.Text = record.Title; + RebuildGroupCombobox(record); + panelRecord.Controls.Add(record.GetControl()); + } + } + + private void TreeView_AfterExpand(object sender, TreeViewEventArgs e) { + ((IDataSource)comboBoxDatabase.SelectedItem).ExpandedNodes.Add(((IRecord)e.Node.Tag).Id); + } + + private void TreeView_AfterCollapse(object sender, TreeViewEventArgs e) { + ((IDataSource)comboBoxDatabase.SelectedItem).ExpandedNodes.Remove(((IRecord)e.Node.Tag).Id); + } + #endregion + + #region Record create/update/delete + public delegate void AfterLabelEditWorkaround(TreeNode node); + + private void TreeView_AfterLabelEdit(object sender, NodeLabelEditEventArgs e) { + if (e.Label != null) { + IRecord record = (IRecord)e.Node.Tag; + record.Title = e.Label; + try { + ((IDataSource)comboBoxDatabase.SelectedItem).Update(record); + BeginInvoke(new AfterLabelEditWorkaround(SortTreeAndSelectNode), new object[] { e.Node }); // Workaround for treeView.Sort() triggering relabel on every sorted node. See note in https://docs.microsoft.com/en-us/dotnet/api/system.windows.forms.listview.afterlabeledit?view=netframework-4.6 + } catch { + MessageBox.Show("Failed to update record in data source", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } else { + e.CancelEdit = true; + } + } + + private void ButtonSave_Click(object sender, EventArgs e) { + TreeNode selectedNode = treeView.SelectedNode; + IRecord record = (IRecord)selectedNode.Tag; + record.UpdateFromControl(); + record.Title = selectedNode.Text = textBoxTitle.Text; + GroupPath groupPath = (GroupPath)comboBoxGroup.SelectedItem; + bool groupChanged = record.GroupId != groupPath.Id; + record.GroupId = groupPath.Id; + try { + ((IDataSource)comboBoxDatabase.SelectedItem).Update(record); + } catch { + MessageBox.Show("Failed to update record in data source", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + if (groupChanged) { + selectedNode.Remove(); + if (groupPath.Node != null) { + groupPath.Node.Nodes.Add(selectedNode); + } else { + treeView.Nodes.Add(selectedNode); + } + } + SortTreeAndSelectNode(selectedNode); + } + + private void NewRecordContextMenuItem_Click(object sender, EventArgs e) { + Type type = (Type)((ToolStripMenuItem)sender).Tag; + TreeNode parentNode = treeView.SelectedNode; + IRecord record = (IRecord)Activator.CreateInstance(type); + record.Id = Guid.NewGuid(); + record.Title = string.Format("New {0}", type.GetDisplayName()); + record.GroupId = parentNode == null ? Guid.Empty : ((IRecord)parentNode.Tag).Id; + try { + TreeNode node = InsertNewRecord(record, parentNode); + SortTreeAndSelectNode(node); + } catch { + MessageBox.Show("Failed to insert record into data source", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + private void DuplicateRecordContextMenuItem_Click(object sender, EventArgs e) { + try { + TreeNode node = DuplicateRecord(treeView.SelectedNode, treeView.SelectedNode.Parent); + SortTreeAndSelectNode(node); + } catch { + MessageBox.Show("Failed to insert record into data source", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + private TreeNode DuplicateRecord(TreeNode sourceNode, TreeNode parentNode) { + IRecord source = (IRecord)sourceNode.Tag; + IRecord record = (IRecord)ConfigSerializer.JsonToObj(ConfigSerializer.ObjToJson(source)); + record.Id = Guid.NewGuid(); + record.Title = string.Format("{0} - Copy", record.Title); + record.GroupId = parentNode == null ? Guid.Empty : ((IRecord)parentNode.Tag).Id; + TreeNode node = InsertNewRecord(record, parentNode); + if (record is Group) { + foreach (TreeNode sourceChildNode in sourceNode.Nodes) { + DuplicateRecord(sourceChildNode, node); + } + } + return node; + } + + private TreeNode InsertNewRecord(IRecord record, TreeNode parentNode) { + ((IDataSource)comboBoxDatabase.SelectedItem).Insert(record); + TreeNode node = CreateTreeNodeFromRecord(record); + if (parentNode != null) { + parentNode.Nodes.Add(node); + } else { + treeView.Nodes.Add(node); + } + return node; + } + + private void DeleteSelectedNode() { + IRecord record = (IRecord)treeView.SelectedNode.Tag; + string message = string.Format("Do you really want to delete selected item{0}?", record is Group ? " and all its subitems" : ""); + if (MessageBox.Show(message, "Confirmation", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.Yes) { + try { + ((IDataSource)comboBoxDatabase.SelectedItem).Delete(record); + } catch { + MessageBox.Show("Failed to remove record from data source", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + treeView.SelectedNode.Remove(); + SortTreeAndSelectNode(null); + } + } + + private void DeleteRecordContextMenuItem_Click(object sender, EventArgs e) => DeleteSelectedNode(); + #endregion + + #region Record ContextMenu + private ContextMenuStrip GetNewRecordContextMenu() { + ContextMenuStrip menu = new ContextMenuStrip(); + ToolStripMenuItem menuItemNew = new ToolStripMenuItem("New item"); + foreach (Type type in Config.RecordTypes) { + ToolStripMenuItem item = new ToolStripMenuItem(type.GetDisplayName()) { Tag = type }; + item.Click += NewRecordContextMenuItem_Click; + menuItemNew.DropDownItems.Add(item); + } + menu.Items.Add(menuItemNew); + ToolStripMenuItem menuItemRefresh = new ToolStripMenuItem("Refresh data source"); + menuItemRefresh.Click += RefreshContextMenuItem_Click; + menu.Items.Add(menuItemRefresh); + return menu; + } + + private ContextMenuStrip GetContextMenu(IRecord record) { + ContextMenuStrip menu = record is Group ? GetNewRecordContextMenu() : new ContextMenuStrip(); + bool isDefaultAction = true; + foreach (IAction action in record.GetActions()) { + ToolStripMenuItem actionItem = new ToolStripMenuItem(action.GetType().GetDisplayName()) { Tag = action }; + if (isDefaultAction) { + actionItem.Font = actionItem.Font = new Font(actionItem.Font, actionItem.Font.Style | FontStyle.Bold); + isDefaultAction = false; + } + actionItem.Click += ActionItem_Click; + menu.Items.Add(actionItem); + } + menu.Items.Add(new ToolStripSeparator()); + ToolStripMenuItem duplicateItem = new ToolStripMenuItem("Duplicate item"); + duplicateItem.Click += DuplicateRecordContextMenuItem_Click; + menu.Items.Add(duplicateItem); + ToolStripMenuItem deleteItem = new ToolStripMenuItem("Delete item"); + deleteItem.Click += DeleteRecordContextMenuItem_Click; + menu.Items.Add(deleteItem); + return menu; + } + + private void ActionItem_Click(object sender, EventArgs e) { + ((IAction)((ToolStripMenuItem)sender).Tag).Run(); + } + #endregion + } +} diff --git a/RCM/MainForm.resx b/RCM/MainForm.resx new file mode 100644 index 0000000..30722eb --- /dev/null +++ b/RCM/MainForm.resx @@ -0,0 +1,129 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + 17, 17 + + + 201, 17 + + + 39 + + \ No newline at end of file diff --git a/RCM/Program.cs b/RCM/Program.cs new file mode 100644 index 0000000..def660c --- /dev/null +++ b/RCM/Program.cs @@ -0,0 +1,21 @@ +using Newtonsoft.Json.Linq; +using System; +using System.ComponentModel; +using System.Drawing; +using System.IO; +using System.Reflection; +using System.Windows.Forms; + +namespace RCM { + static class Program { + [STAThread] + static void Main() { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + + Config.LoadModules(); + Config.LoadConfiguration(); + Application.Run(new MainForm()); + } + } +} diff --git a/RCM/Properties/AssemblyInfo.cs b/RCM/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..3776473 --- /dev/null +++ b/RCM/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("RCM")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("RCM")] +[assembly: AssemblyCopyright("Copyright © Disassembler 2019")] +[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("f5912ef4-edc9-421d-b474-4cdadb121e55")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/RCM/Properties/Resources.Designer.cs b/RCM/Properties/Resources.Designer.cs new file mode 100644 index 0000000..5f103f9 --- /dev/null +++ b/RCM/Properties/Resources.Designer.cs @@ -0,0 +1,73 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace RCM.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", "15.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("RCM.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 ConfigIcon { + get { + object obj = ResourceManager.GetObject("ConfigIcon", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + } +} diff --git a/RCM/Properties/Resources.resx b/RCM/Properties/Resources.resx new file mode 100644 index 0000000..6739b80 --- /dev/null +++ b/RCM/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\Config.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/RCM/RCM.csproj b/RCM/RCM.csproj new file mode 100644 index 0000000..504a591 --- /dev/null +++ b/RCM/RCM.csproj @@ -0,0 +1,229 @@ + + + + + Debug + AnyCPU + {F5912EF4-EDC9-421D-B474-4CDADB121E55} + WinExe + RCM + RCM + v4.6.1 + 512 + true + true + + + + + AnyCPU + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + AnyCPU + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + true + bin\Debug\ + DEBUG;TRACE + full + x64 + prompt + MinimumRecommendedRules.ruleset + true + + + bin\x64\Release\ + TRACE + true + pdbonly + x64 + prompt + MinimumRecommendedRules.ruleset + true + + + + ..\packages\CredentialManagement.1.0.2\lib\net35\CredentialManagement.dll + + + + ..\packages\MySql.Data.8.0.16\lib\net452\MySql.Data.dll + + + ..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll + + + + + ..\packages\System.Data.SQLite.Core.1.0.110.0\lib\net46\System.Data.SQLite.dll + + + + + + + + UserControl + + + ConfigFormTab.cs + + + + + UserControl + + + MySQLControl.cs + + + + UserControl + + + SQLiteControl.cs + + + + + + + + + + UserControl + + + SSHConfigControl.cs + + + + + UserControl + + + WebSiteControl.cs + + + + + + UserControl + + + WinBoxConfigControl.cs + + + + ConfigForm.cs + + + ConfigFormTab.cs + + + MySQL.cs + Designer + + + SQLite.cs + Designer + + + MainForm.cs + + + Config.cs + Designer + + + Form + + + ConfigForm.cs + + + + + + + + Form + + + MainForm.cs + + + True + True + Resources.resx + + + + + + + UserControl + + + SimpleHostControl.cs + + + + + ResXFileCodeGenerator + Designer + Resources.Designer.cs + + + RDP.cs + Designer + + + SSH.cs + Designer + + + WebSite.cs + Designer + + + WinBox.cs + Designer + + + WinBoxConfigControl.cs + + + SimpleHostControl.cs + + + Group.cs + Designer + + + + + + + + + + + This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. + + + + \ No newline at end of file diff --git a/RCM/Record/RDP/RDP.cs b/RCM/Record/RDP/RDP.cs new file mode 100644 index 0000000..446a022 --- /dev/null +++ b/RCM/Record/RDP/RDP.cs @@ -0,0 +1,49 @@ +using System; +using System.Windows.Forms; +using System.Diagnostics; +using CredentialManagement; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace RCM { + [Serializable] + [DisplayName("Remote Desktop (RDP)")] + public class RDP : IRecord { + public Guid Id { get; set; } + public string Title { get; set; } + public Guid GroupId { get; set; } + + public string Host { get; set; } + public string Username { get; set; } + public string Password { get; set; } + [NonSerialized] + private SimpleHostControl _control; + [NonSerialized] + private IAction[] _actions; + + public IAction[] GetActions() { + if (_actions == null) { + _actions = new IAction[] { new RDPAction(this), new SMBAction(this) }; + } + return _actions; + } + + public UserControl GetControl() { + if (_control == null || _control.IsDisposed) { + _control = new SimpleHostControl(Host, Username, Password); + } + return _control; + } + + public void UpdateFromControl() { + Host = _control.textBoxHostAddress.Text; + Username = _control.textBoxUsername.Text; + Password = _control.textBoxPassword.Text; + } + + public static void Init() { + + } + } +} diff --git a/RCM/Record/RDP/RDP.resx b/RCM/Record/RDP/RDP.resx new file mode 100644 index 0000000..c3625eb --- /dev/null +++ b/RCM/Record/RDP/RDP.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\RDP.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/RCM/Record/RDP/RDPAction.cs b/RCM/Record/RDP/RDPAction.cs new file mode 100644 index 0000000..ea70d21 --- /dev/null +++ b/RCM/Record/RDP/RDPAction.cs @@ -0,0 +1,55 @@ +using CredentialManagement; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace RCM { + [DisplayName("Start Remote Desktop")] + public class RDPAction : IAction { + private RDP _rdp; + + public RDPAction(RDP rdp) { + _rdp = rdp; + } + + public async void Run() { + await Task.Run(() => { + if (Environment.OSVersion.Platform == PlatformID.Unix) { + RunUnix(); + } else { + RunWin(); + } + }); + } + + private void RunWin() { + string[] host = _rdp.Host.Split(new char[] { ':' }); + string[] username = _rdp.Username.Split(new char[] { '\\' }); + + // https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-server-2008-R2-and-2008/ff393699(v%3dws.10) + string rdpFile = Path.GetTempPath() + Guid.NewGuid().ToString() + ".rdp"; + using (StreamWriter sw = new StreamWriter(rdpFile)) { + sw.Write("drivestoredirect:s:*\r\nredirectprinters:i:0\r\nredirectcomports:i:0\r\nredirectsmartcards:i:0\r\nredirectposdevices:i:0\r\nnetworkautodetect:i:0\r\nauthentication level:i:0\r\nprompt for credentials:i:0\r\nnegotiate security layer:i:1\r\npromptcredentialonce:i:0\r\nsmart sizing:i:0\r\nuse multimon:i:0\r\nbandwidthautodetect:i:1\r\n"); + sw.Write(string.Format("full address:s:{0}\r\n", _rdp.Host)); + sw.Write(string.Format("username:s:{0}\r\n", _rdp.Username)); + if (username.Length == 2) { + sw.Write(string.Format("domain:s:{0}\r\n", username[0])); + } + } + Credential credential = new Credential(_rdp.Username, _rdp.Password, string.Format("TERMSRV/{0}", host[0])); + credential.Save(); + Process p = Process.Start("mstsc", string.Format("\"{0}\"", rdpFile)); + Thread.Sleep(1000); + File.Delete(rdpFile); + } + + private void RunUnix() { + // TODO + } + } +} diff --git a/RCM/Record/RDP/SMBAction.cs b/RCM/Record/RDP/SMBAction.cs new file mode 100644 index 0000000..51dd495 --- /dev/null +++ b/RCM/Record/RDP/SMBAction.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RCM { + [DisplayName("Browse via SMB")] + public class SMBAction : IAction { + private RDP _rdp; + + public SMBAction(RDP rdp) { + _rdp = rdp; + } + + public void Run() { + string[] host = _rdp.Host.Split(new char[] { ':' }); + + if (Environment.OSVersion.Platform == PlatformID.Unix) { + // TODO + } else { + Process.Start("explorer.exe", string.Format("\\\\{0}", host[0])); // TODO: net use credentials? + } + } + } +} diff --git a/RCM/Record/SSH/SCPAction.cs b/RCM/Record/SSH/SCPAction.cs new file mode 100644 index 0000000..c275a1e --- /dev/null +++ b/RCM/Record/SSH/SCPAction.cs @@ -0,0 +1,67 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace RCM { + [DisplayName("Browse via SCP")] + public class SCPAction : IAction { + private SSH _ssh; + private SSHConfig _config; + + public SCPAction(SSH ssh) { + _ssh = ssh; + _config = (SSHConfig)Config.ConfigSections[typeof(SSHConfig)]; + } + + public async void Run() { + await Task.Run(() => { + if (Environment.OSVersion.Platform == PlatformID.Unix) { + RunUnix(); + } else { + RunWin(); + } + }); + } + + private void RunWin() { + if (string.IsNullOrWhiteSpace(_config.WinSCPPath)) { + MessageBox.Show("WinSCP path is not set", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + if (!File.Exists(_config.WinSCPPath)) { + MessageBox.Show("WinSCP path does not exist", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + if (string.IsNullOrWhiteSpace(_config.SSHKeys) && string.IsNullOrWhiteSpace(_ssh.Password)) { + MessageBox.Show("No authentication method was set", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + if (!string.IsNullOrWhiteSpace(_config.SSHKeys)) { + if (string.IsNullOrWhiteSpace(_config.PageantPath)) { + MessageBox.Show("Pageant path is not set", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + if (!File.Exists(_config.PageantPath)) { + MessageBox.Show("Pageant path does not exist", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + } + + StringBuilder scpParams = new StringBuilder(); + if (!string.IsNullOrWhiteSpace(_config.SSHKeys)) { + Process.Start(_config.PageantPath, _config.SSHKeys); + scpParams.Append(string.Format("/privatekey={0} ", _config.SSHKeys)); //TODO: Zvlada vic klicu? + } + scpParams.AppendFormat("scp://{0}@{1}", _ssh.Username, _ssh.Host); // TODO: password? + Process.Start(_config.WinSCPPath, scpParams.ToString()); + } + + private void RunUnix() { + // TOOD + } + } +} diff --git a/RCM/Record/SSH/SSH.cs b/RCM/Record/SSH/SSH.cs new file mode 100644 index 0000000..b8144cb --- /dev/null +++ b/RCM/Record/SSH/SSH.cs @@ -0,0 +1,44 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace RCM { + [Serializable] + public class SSH : IRecord { + public Guid Id { get; set; } + public string Title { get; set; } + public Guid GroupId { get; set; } + + public string Host { get; set; } + public string Username { get; set; } + public string Password { get; set; } + [NonSerialized] + private SimpleHostControl _control; + [NonSerialized] + private IAction[] _actions; + + public IAction[] GetActions() { + if (_actions == null) { + _actions = new IAction[] { new SSHAction(this), new SCPAction(this) }; + } + return _actions; + } + + public UserControl GetControl() { + if (_control == null || _control.IsDisposed) { + _control = new SimpleHostControl(Host, Username, Password); + } + return _control; + } + + public void UpdateFromControl() { + Host = _control.textBoxHostAddress.Text; + Username = _control.textBoxUsername.Text; + Password = _control.textBoxPassword.Text; + } + } +} diff --git a/RCM/Record/SSH/SSH.resx b/RCM/Record/SSH/SSH.resx new file mode 100644 index 0000000..74dacdd --- /dev/null +++ b/RCM/Record/SSH/SSH.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\SSH.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/RCM/Record/SSH/SSHAction.cs b/RCM/Record/SSH/SSHAction.cs new file mode 100644 index 0000000..966a5ad --- /dev/null +++ b/RCM/Record/SSH/SSHAction.cs @@ -0,0 +1,72 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace RCM { + [DisplayName("Start SSH")] + public class SSHAction : IAction { + private SSH _ssh; + private SSHConfig _config; + + public SSHAction(SSH ssh) { + _ssh = ssh; + _config = (SSHConfig)Config.ConfigSections[typeof(SSHConfig)]; + } + + public async void Run() { + await Task.Run(() => { + if (Environment.OSVersion.Platform == PlatformID.Unix) { + RunUnix(); + } else { + RunWin(); + } + }); + } + + private void RunWin() { + if (string.IsNullOrWhiteSpace(_config.PuttyPath)) { + MessageBox.Show("PuTTY path is not set", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + if (!File.Exists(_config.PuttyPath)) { + MessageBox.Show("PuTTY path does not exist", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + if (string.IsNullOrWhiteSpace(_config.SSHKeys) && string.IsNullOrWhiteSpace(_ssh.Password)) { + MessageBox.Show("No authentication method was set", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + if (!string.IsNullOrWhiteSpace(_config.SSHKeys)) { + if (string.IsNullOrWhiteSpace(_config.PageantPath)) { + MessageBox.Show("Pageant path is not set", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + if (!File.Exists(_config.PageantPath)) { + MessageBox.Show("Pageant path does not exist", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); + return; + } + } + + string[] host = _ssh.Host.Split(new char[] { ':' }); + StringBuilder puttyParams = new StringBuilder(); + if (!string.IsNullOrWhiteSpace(_config.SSHKeys)) { + Process.Start(_config.PageantPath, _config.SSHKeys); + puttyParams.AppendFormat("-i {0} ", _config.SSHKeys); + } else { + puttyParams.AppendFormat("-pw {0} ", _ssh.Password); + } + if (host.Length > 1) { + puttyParams.AppendFormat("-P {0} ", host[1]); + } + puttyParams.AppendFormat("{0}@{1}", _ssh.Username, host[0]); + Process.Start(_config.PuttyPath, puttyParams.ToString()); + } + + private void RunUnix() { + // TODO + } + } +} diff --git a/RCM/Record/SSH/SSHConfig.cs b/RCM/Record/SSH/SSHConfig.cs new file mode 100644 index 0000000..f12da7c --- /dev/null +++ b/RCM/Record/SSH/SSHConfig.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace RCM { + [DisplayName("SSH")] + [Serializable] + public class SSHConfig : IConfigSection { + public string SSHKeys { get; set; } + public string PuttyPath { get; set; } + public string PageantPath { get; set; } + public string WinSCPPath { get; set; } + [NonSerialized] + private SSHConfigControl _control; + + public UserControl GetControl() { + if (_control == null || _control.IsDisposed) { + _control = new SSHConfigControl(this); + } + return _control; + } + + public void UpdateFromControl() { + SSHKeys = _control.textBoxSshKeys.Text; + PuttyPath = _control.textBoxPutty.Text; + PageantPath = _control.textBoxPageant.Text; + WinSCPPath = _control.textBoxWinSCP.Text; + } + } +} diff --git a/RCM/Record/SSH/SSHConfigControl.Designer.cs b/RCM/Record/SSH/SSHConfigControl.Designer.cs new file mode 100644 index 0000000..b6fd4eb --- /dev/null +++ b/RCM/Record/SSH/SSHConfigControl.Designer.cs @@ -0,0 +1,222 @@ +namespace RCM { + partial class SSHConfigControl { + /// + /// 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 Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() { + this.labelPutty = new System.Windows.Forms.Label(); + this.groupBoxWindows = new System.Windows.Forms.GroupBox(); + this.buttonSelectPageant = new System.Windows.Forms.Button(); + this.buttonSelectPutty = new System.Windows.Forms.Button(); + this.textBoxPageant = new System.Windows.Forms.TextBox(); + this.textBoxPutty = new System.Windows.Forms.TextBox(); + this.labelPageant = new System.Windows.Forms.Label(); + this.labelSshKey = new System.Windows.Forms.Label(); + this.textBoxSshKeys = new System.Windows.Forms.TextBox(); + this.buttonSelectSshKeys = new System.Windows.Forms.Button(); + this.openFileDialogSshKeys = new System.Windows.Forms.OpenFileDialog(); + this.openFileDialogPutty = new System.Windows.Forms.OpenFileDialog(); + this.openFileDialogPageant = new System.Windows.Forms.OpenFileDialog(); + this.buttonSelectWinSCP = new System.Windows.Forms.Button(); + this.textBoxWinSCP = new System.Windows.Forms.TextBox(); + this.labelWinSCP = new System.Windows.Forms.Label(); + this.openFileDialogWinSCP = new System.Windows.Forms.OpenFileDialog(); + this.groupBoxWindows.SuspendLayout(); + this.SuspendLayout(); + // + // labelPutty + // + this.labelPutty.AutoSize = true; + this.labelPutty.Location = new System.Drawing.Point(19, 22); + this.labelPutty.Name = "labelPutty"; + this.labelPutty.Size = new System.Drawing.Size(101, 13); + this.labelPutty.TabIndex = 0; + this.labelPutty.Text = "Path to PuTTY.exe:"; + // + // groupBoxWindows + // + this.groupBoxWindows.Controls.Add(this.buttonSelectWinSCP); + this.groupBoxWindows.Controls.Add(this.textBoxWinSCP); + this.groupBoxWindows.Controls.Add(this.labelWinSCP); + this.groupBoxWindows.Controls.Add(this.buttonSelectPageant); + this.groupBoxWindows.Controls.Add(this.buttonSelectPutty); + this.groupBoxWindows.Controls.Add(this.textBoxPageant); + this.groupBoxWindows.Controls.Add(this.textBoxPutty); + this.groupBoxWindows.Controls.Add(this.labelPageant); + this.groupBoxWindows.Controls.Add(this.labelPutty); + this.groupBoxWindows.Location = new System.Drawing.Point(3, 39); + this.groupBoxWindows.Name = "groupBoxWindows"; + this.groupBoxWindows.Size = new System.Drawing.Size(487, 108); + this.groupBoxWindows.TabIndex = 1; + this.groupBoxWindows.TabStop = false; + this.groupBoxWindows.Text = "Windows"; + // + // buttonSelectPageant + // + this.buttonSelectPageant.Location = new System.Drawing.Point(452, 44); + this.buttonSelectPageant.Name = "buttonSelectPageant"; + this.buttonSelectPageant.Size = new System.Drawing.Size(22, 22); + this.buttonSelectPageant.TabIndex = 6; + this.buttonSelectPageant.Text = "⋯"; + this.buttonSelectPageant.UseVisualStyleBackColor = true; + this.buttonSelectPageant.Click += new System.EventHandler(this.ButtonSelectPageant_Click); + // + // buttonSelectPutty + // + this.buttonSelectPutty.Location = new System.Drawing.Point(452, 18); + this.buttonSelectPutty.Name = "buttonSelectPutty"; + this.buttonSelectPutty.Size = new System.Drawing.Size(22, 22); + this.buttonSelectPutty.TabIndex = 6; + this.buttonSelectPutty.Text = "⋯"; + this.buttonSelectPutty.UseVisualStyleBackColor = true; + this.buttonSelectPutty.Click += new System.EventHandler(this.ButtonSelectPutty_Click); + // + // textBoxPageant + // + this.textBoxPageant.Location = new System.Drawing.Point(132, 45); + this.textBoxPageant.Name = "textBoxPageant"; + this.textBoxPageant.Size = new System.Drawing.Size(314, 20); + this.textBoxPageant.TabIndex = 3; + // + // textBoxPutty + // + this.textBoxPutty.Location = new System.Drawing.Point(132, 19); + this.textBoxPutty.Name = "textBoxPutty"; + this.textBoxPutty.Size = new System.Drawing.Size(314, 20); + this.textBoxPutty.TabIndex = 2; + // + // labelPageant + // + this.labelPageant.AutoSize = true; + this.labelPageant.Location = new System.Drawing.Point(19, 48); + this.labelPageant.Name = "labelPageant"; + this.labelPageant.Size = new System.Drawing.Size(107, 13); + this.labelPageant.TabIndex = 1; + this.labelPageant.Text = "Path to Pageant.exe:"; + // + // labelSshKey + // + this.labelSshKey.AutoSize = true; + this.labelSshKey.Location = new System.Drawing.Point(8, 10); + this.labelSshKey.Name = "labelSshKey"; + this.labelSshKey.Size = new System.Drawing.Size(99, 13); + this.labelSshKey.TabIndex = 4; + this.labelSshKey.Text = "Paths to SSH keys:"; + // + // textBoxSshKeys + // + this.textBoxSshKeys.Location = new System.Drawing.Point(135, 7); + this.textBoxSshKeys.Name = "textBoxSshKeys"; + this.textBoxSshKeys.Size = new System.Drawing.Size(314, 20); + this.textBoxSshKeys.TabIndex = 4; + // + // buttonSelectSshKeys + // + this.buttonSelectSshKeys.Location = new System.Drawing.Point(455, 6); + this.buttonSelectSshKeys.Name = "buttonSelectSshKeys"; + this.buttonSelectSshKeys.Size = new System.Drawing.Size(22, 22); + this.buttonSelectSshKeys.TabIndex = 5; + this.buttonSelectSshKeys.Text = "⋯"; + this.buttonSelectSshKeys.UseVisualStyleBackColor = true; + this.buttonSelectSshKeys.Click += new System.EventHandler(this.ButtonSelectSshKeys_Click); + // + // openFileDialogSshKeys + // + this.openFileDialogSshKeys.Filter = "OpenSSH keys (*.pub)|*.pub|PuTTY keys (*.ppk)|*.ppk|All files (*.*)|*.*"; + this.openFileDialogSshKeys.Multiselect = true; + // + // openFileDialogPutty + // + this.openFileDialogPutty.Filter = "PuTTY.exe|PuTTY.exe"; + // + // openFileDialogPageant + // + this.openFileDialogPageant.Filter = "Pageant.exe|Pageant.exe"; + // + // buttonSelectWinSCP + // + this.buttonSelectWinSCP.Location = new System.Drawing.Point(452, 70); + this.buttonSelectWinSCP.Name = "buttonSelectWinSCP"; + this.buttonSelectWinSCP.Size = new System.Drawing.Size(22, 22); + this.buttonSelectWinSCP.TabIndex = 9; + this.buttonSelectWinSCP.Text = "⋯"; + this.buttonSelectWinSCP.UseVisualStyleBackColor = true; + this.buttonSelectWinSCP.Click += new System.EventHandler(this.ButtonSelectWinSCP_Click); + // + // textBoxWinSCP + // + this.textBoxWinSCP.Location = new System.Drawing.Point(132, 71); + this.textBoxWinSCP.Name = "textBoxWinSCP"; + this.textBoxWinSCP.Size = new System.Drawing.Size(314, 20); + this.textBoxWinSCP.TabIndex = 8; + // + // labelWinSCP + // + this.labelWinSCP.AutoSize = true; + this.labelWinSCP.Location = new System.Drawing.Point(19, 74); + this.labelWinSCP.Name = "labelWinSCP"; + this.labelWinSCP.Size = new System.Drawing.Size(107, 13); + this.labelWinSCP.TabIndex = 7; + this.labelWinSCP.Text = "Path to WinSCP.exe:"; + // + // openFileDialogWinSCP + // + this.openFileDialogWinSCP.Filter = "WinSCP.exe|WinSCP.exe"; + // + // SSHConfigControl + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.buttonSelectSshKeys); + this.Controls.Add(this.textBoxSshKeys); + this.Controls.Add(this.labelSshKey); + this.Controls.Add(this.groupBoxWindows); + this.Name = "SSHConfigControl"; + this.Size = new System.Drawing.Size(499, 172); + this.groupBoxWindows.ResumeLayout(false); + this.groupBoxWindows.PerformLayout(); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.Label labelPutty; + private System.Windows.Forms.GroupBox groupBoxWindows; + public System.Windows.Forms.TextBox textBoxPageant; + public System.Windows.Forms.TextBox textBoxPutty; + private System.Windows.Forms.Label labelPageant; + private System.Windows.Forms.Label labelSshKey; + public System.Windows.Forms.TextBox textBoxSshKeys; + private System.Windows.Forms.Button buttonSelectSshKeys; + private System.Windows.Forms.Button buttonSelectPageant; + private System.Windows.Forms.Button buttonSelectPutty; + private System.Windows.Forms.OpenFileDialog openFileDialogSshKeys; + private System.Windows.Forms.OpenFileDialog openFileDialogPutty; + private System.Windows.Forms.OpenFileDialog openFileDialogPageant; + private System.Windows.Forms.Button buttonSelectWinSCP; + public System.Windows.Forms.TextBox textBoxWinSCP; + private System.Windows.Forms.Label labelWinSCP; + private System.Windows.Forms.OpenFileDialog openFileDialogWinSCP; + } +} diff --git a/RCM/Record/SSH/SSHConfigControl.cs b/RCM/Record/SSH/SSHConfigControl.cs new file mode 100644 index 0000000..5957b9c --- /dev/null +++ b/RCM/Record/SSH/SSHConfigControl.cs @@ -0,0 +1,37 @@ +using System; +using System.Windows.Forms; + +namespace RCM { + public partial class SSHConfigControl : UserControl { + public SSHConfigControl(SSHConfig sshConfig) { + InitializeComponent(); + textBoxPutty.Text = sshConfig.PuttyPath; + textBoxPageant.Text = sshConfig.PageantPath; + textBoxSshKeys.Text = sshConfig.SSHKeys; + } + + private void ButtonSelectSshKeys_Click(object sender, EventArgs e) { + if (openFileDialogSshKeys.ShowDialog() == DialogResult.OK) { + textBoxSshKeys.Text = string.Join(",", openFileDialogSshKeys.FileNames); + } + } + + private void ButtonSelectPutty_Click(object sender, EventArgs e) { + if (openFileDialogPutty.ShowDialog() == DialogResult.OK) { + textBoxPutty.Text = openFileDialogPutty.FileName; + } + } + + private void ButtonSelectPageant_Click(object sender, EventArgs e) { + if (openFileDialogPageant.ShowDialog() == DialogResult.OK) { + textBoxPageant.Text = openFileDialogPageant.FileName; + } + } + + private void ButtonSelectWinSCP_Click(object sender, EventArgs e) { + if (openFileDialogWinSCP.ShowDialog() == DialogResult.OK) { + textBoxWinSCP.Text = openFileDialogWinSCP.FileName; + } + } + } +} diff --git a/RCM/Record/WebSite/BrowserAction.cs b/RCM/Record/WebSite/BrowserAction.cs new file mode 100644 index 0000000..008de85 --- /dev/null +++ b/RCM/Record/WebSite/BrowserAction.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RCM { + [DisplayName("Open in browser")] + public class BrowserAction : IAction { + private WebSite _website; + + public BrowserAction(WebSite website) { + _website = website; + } + + public async void Run() { + await Task.Run(() => { + if (_website.URL.StartsWith("http://") || _website.URL.StartsWith("https://")) + Process.Start(_website.URL); // TODO: vyplnovat hesla + }); + } + } +} diff --git a/RCM/Record/WebSite/WebSite.cs b/RCM/Record/WebSite/WebSite.cs new file mode 100644 index 0000000..4855693 --- /dev/null +++ b/RCM/Record/WebSite/WebSite.cs @@ -0,0 +1,40 @@ +using System; +using System.Diagnostics; +using System.Windows.Forms; + +namespace RCM { + [Serializable] + public class WebSite : IRecord { + public Guid Id { get; set; } + public string Title { get; set; } + public Guid GroupId { get; set; } + + public string URL { get; set; } + public string Username { get; set; } + public string Password { get; set; } + [NonSerialized] + private WebSiteControl _control; + [NonSerialized] + private IAction[] _actions; + + public IAction[] GetActions() { + if (_actions == null) { + _actions = new IAction[] { new BrowserAction(this) }; + } + return _actions; + } + + public UserControl GetControl() { + if (_control == null || _control.IsDisposed) { + _control = new WebSiteControl(this); + } + return _control; + } + + public void UpdateFromControl() { + URL = _control.textBoxURL.Text; + Username = _control.textBoxUsername.Text; + Password = _control.textBoxPassword.Text; + } + } +} diff --git a/RCM/Record/WebSite/WebSite.resx b/RCM/Record/WebSite/WebSite.resx new file mode 100644 index 0000000..a753065 --- /dev/null +++ b/RCM/Record/WebSite/WebSite.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\Website.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/RCM/Record/WebSite/WebsiteControl.Designer.cs b/RCM/Record/WebSite/WebsiteControl.Designer.cs new file mode 100644 index 0000000..b8ab580 --- /dev/null +++ b/RCM/Record/WebSite/WebsiteControl.Designer.cs @@ -0,0 +1,122 @@ +namespace RCM { + partial class WebSiteControl { + /// + /// 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 Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() { + this.buttonUnmaskPassword = new System.Windows.Forms.Button(); + this.textBoxPassword = new System.Windows.Forms.TextBox(); + this.textBoxUsername = new System.Windows.Forms.TextBox(); + this.textBoxURL = new System.Windows.Forms.TextBox(); + this.labelPassword = new System.Windows.Forms.Label(); + this.labelUsername = new System.Windows.Forms.Label(); + this.labelURL = new System.Windows.Forms.Label(); + this.SuspendLayout(); + // + // buttonUnmaskPassword + // + this.buttonUnmaskPassword.Location = new System.Drawing.Point(273, 51); + this.buttonUnmaskPassword.Name = "buttonUnmaskPassword"; + this.buttonUnmaskPassword.Size = new System.Drawing.Size(22, 22); + this.buttonUnmaskPassword.TabIndex = 4; + this.buttonUnmaskPassword.Text = "●"; + this.buttonUnmaskPassword.UseVisualStyleBackColor = true; + this.buttonUnmaskPassword.Click += new System.EventHandler(this.ButtonUnmaskPassword_Click); + // + // textBoxPassword + // + this.textBoxPassword.Location = new System.Drawing.Point(120, 52); + this.textBoxPassword.Name = "textBoxPassword"; + this.textBoxPassword.Size = new System.Drawing.Size(147, 20); + this.textBoxPassword.TabIndex = 3; + this.textBoxPassword.UseSystemPasswordChar = true; + // + // textBoxUsername + // + this.textBoxUsername.Location = new System.Drawing.Point(120, 26); + this.textBoxUsername.Name = "textBoxUsername"; + this.textBoxUsername.Size = new System.Drawing.Size(175, 20); + this.textBoxUsername.TabIndex = 2; + // + // textBoxURL + // + this.textBoxURL.Location = new System.Drawing.Point(120, 0); + this.textBoxURL.Name = "textBoxURL"; + this.textBoxURL.Size = new System.Drawing.Size(175, 20); + this.textBoxURL.TabIndex = 1; + // + // labelPassword + // + this.labelPassword.AutoSize = true; + this.labelPassword.Location = new System.Drawing.Point(0, 55); + this.labelPassword.Name = "labelPassword"; + this.labelPassword.Size = new System.Drawing.Size(56, 13); + this.labelPassword.TabIndex = 14; + this.labelPassword.Text = "Password:"; + // + // labelUsername + // + this.labelUsername.AutoSize = true; + this.labelUsername.Location = new System.Drawing.Point(0, 29); + this.labelUsername.Name = "labelUsername"; + this.labelUsername.Size = new System.Drawing.Size(58, 13); + this.labelUsername.TabIndex = 13; + this.labelUsername.Text = "Username:"; + // + // labelURL + // + this.labelURL.AutoSize = true; + this.labelURL.Location = new System.Drawing.Point(0, 3); + this.labelURL.Name = "labelURL"; + this.labelURL.Size = new System.Drawing.Size(32, 13); + this.labelURL.TabIndex = 12; + this.labelURL.Text = "URL:"; + // + // WebsiteControl + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.buttonUnmaskPassword); + this.Controls.Add(this.textBoxPassword); + this.Controls.Add(this.textBoxUsername); + this.Controls.Add(this.textBoxURL); + this.Controls.Add(this.labelPassword); + this.Controls.Add(this.labelUsername); + this.Controls.Add(this.labelURL); + this.Name = "WebsiteControl"; + this.Size = new System.Drawing.Size(300, 77); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.Button buttonUnmaskPassword; + private System.Windows.Forms.Label labelPassword; + private System.Windows.Forms.Label labelUsername; + private System.Windows.Forms.Label labelURL; + public System.Windows.Forms.TextBox textBoxURL; + public System.Windows.Forms.TextBox textBoxUsername; + public System.Windows.Forms.TextBox textBoxPassword; + } +} diff --git a/RCM/Record/WebSite/WebsiteControl.cs b/RCM/Record/WebSite/WebsiteControl.cs new file mode 100644 index 0000000..15bd0c8 --- /dev/null +++ b/RCM/Record/WebSite/WebsiteControl.cs @@ -0,0 +1,17 @@ +using System; +using System.Windows.Forms; + +namespace RCM { + public partial class WebSiteControl : UserControl { + public WebSiteControl(WebSite website) { + InitializeComponent(); + textBoxURL.Text = website.URL; + textBoxUsername.Text = website.Username; + textBoxPassword.Text = website.Password; + } + + private void ButtonUnmaskPassword_Click(object sender, EventArgs e) { + textBoxPassword.UseSystemPasswordChar = !textBoxPassword.UseSystemPasswordChar; + } + } +} diff --git a/RCM/Record/WinBox/WinBox.cs b/RCM/Record/WinBox/WinBox.cs new file mode 100644 index 0000000..675124c --- /dev/null +++ b/RCM/Record/WinBox/WinBox.cs @@ -0,0 +1,42 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace RCM { + [Serializable] + public class WinBox : IRecord { + public Guid Id { get; set; } + public string Title { get; set; } + public Guid GroupId { get; set; } + + public string Host { get; set; } + public string Username { get; set; } + public string Password { get; set; } + [NonSerialized] + private SimpleHostControl _control; + [NonSerialized] + private IAction[] _actions; + + public IAction[] GetActions() { + if (_actions == null) { + _actions = new IAction[] { new WinBoxAction(this) }; + } + return _actions; + } + + public UserControl GetControl() { + if (_control == null || _control.IsDisposed) { + _control = new SimpleHostControl(Host, Username, Password); + } + return _control; + } + + public void UpdateFromControl() { + Host = _control.textBoxHostAddress.Text; + Username = _control.textBoxUsername.Text; + Password = _control.textBoxPassword.Text; + } + } +} diff --git a/RCM/Record/WinBox/WinBox.resx b/RCM/Record/WinBox/WinBox.resx new file mode 100644 index 0000000..61f5149 --- /dev/null +++ b/RCM/Record/WinBox/WinBox.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\WinBox.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/RCM/Record/WinBox/WinBoxAction.cs b/RCM/Record/WinBox/WinBoxAction.cs new file mode 100644 index 0000000..37ff5eb --- /dev/null +++ b/RCM/Record/WinBox/WinBoxAction.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RCM { + [DisplayName("Start WinBox")] + public class WinBoxAction : IAction { + private WinBox _winbox; + private WinBoxConfig _config; + + public WinBoxAction(WinBox winbox) { + _winbox = winbox; + _config = (WinBoxConfig)Config.ConfigSections[typeof(WinBoxConfig)]; + } + + public async void Run() { + await Task.Run(() => { + if (Environment.OSVersion.Platform == PlatformID.Unix) { + RunUnix(); + } else { + RunWin(); + } + }); + } + + private void RunWin() { + string winboxParam = string.Format("{0} \"{1}\" \"{2}\"", _winbox.Host, _winbox.Username, _winbox.Password); + Process.Start(_config.WinBoxPath, winboxParam); + } + + private void RunUnix() { + string winboxParam = string.Format("{0} {1} \"{2}\" \"{3}\"", _config.WinBoxPath, _winbox.Host, _winbox.Username, _winbox.Password); + Process.Start("wine", winboxParam); + } + } +} diff --git a/RCM/Record/WinBox/WinBoxConfig.cs b/RCM/Record/WinBox/WinBoxConfig.cs new file mode 100644 index 0000000..6b89c7d --- /dev/null +++ b/RCM/Record/WinBox/WinBoxConfig.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace RCM { + [DisplayName("WinBox")] + [Serializable] + public class WinBoxConfig : IConfigSection { + public string WinBoxPath { get; set; } + [NonSerialized] + private WinBoxConfigControl _control; + + public UserControl GetControl() { + if (_control == null || _control.IsDisposed) { + _control = new WinBoxConfigControl(this); + } + return _control; + } + + public void UpdateFromControl() { + WinBoxPath = _control.textBoxWinBox.Text; + } + } +} diff --git a/RCM/Record/WinBox/WinBoxConfigControl.Designer.cs b/RCM/Record/WinBox/WinBoxConfigControl.Designer.cs new file mode 100644 index 0000000..83b1b4c --- /dev/null +++ b/RCM/Record/WinBox/WinBoxConfigControl.Designer.cs @@ -0,0 +1,83 @@ +namespace RCM { + partial class WinBoxConfigControl { + /// + /// 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 Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() { + this.buttonSelectWinBox = new System.Windows.Forms.Button(); + this.textBoxWinBox = new System.Windows.Forms.TextBox(); + this.labelWinBox = new System.Windows.Forms.Label(); + this.openFileDialogWinBox = new System.Windows.Forms.OpenFileDialog(); + this.SuspendLayout(); + // + // buttonSelectWinBox + // + this.buttonSelectWinBox.Location = new System.Drawing.Point(455, 6); + this.buttonSelectWinBox.Name = "buttonSelectWinBox"; + this.buttonSelectWinBox.Size = new System.Drawing.Size(22, 22); + this.buttonSelectWinBox.TabIndex = 9; + this.buttonSelectWinBox.Text = "⋯"; + this.buttonSelectWinBox.UseVisualStyleBackColor = true; + this.buttonSelectWinBox.Click += new System.EventHandler(this.ButtonSelectWinBox_Click); + // + // textBoxWinBox + // + this.textBoxWinBox.Location = new System.Drawing.Point(135, 7); + this.textBoxWinBox.Name = "textBoxWinBox"; + this.textBoxWinBox.Size = new System.Drawing.Size(314, 20); + this.textBoxWinBox.TabIndex = 8; + // + // labelWinBox + // + this.labelWinBox.AutoSize = true; + this.labelWinBox.Location = new System.Drawing.Point(6, 10); + this.labelWinBox.Name = "labelWinBox"; + this.labelWinBox.Size = new System.Drawing.Size(104, 13); + this.labelWinBox.TabIndex = 7; + this.labelWinBox.Text = "Path to WinBox.exe:"; + // + // openFileDialogWinBox + // + this.openFileDialogWinBox.Filter = "WinBox.exe|WinBox.exe"; + // + // WinBoxConfigControl + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.buttonSelectWinBox); + this.Controls.Add(this.textBoxWinBox); + this.Controls.Add(this.labelWinBox); + this.Name = "WinBoxConfigControl"; + this.Size = new System.Drawing.Size(494, 116); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.Button buttonSelectWinBox; + public System.Windows.Forms.TextBox textBoxWinBox; + private System.Windows.Forms.Label labelWinBox; + private System.Windows.Forms.OpenFileDialog openFileDialogWinBox; + } +} diff --git a/RCM/Record/WinBox/WinBoxConfigControl.cs b/RCM/Record/WinBox/WinBoxConfigControl.cs new file mode 100644 index 0000000..4e3fa90 --- /dev/null +++ b/RCM/Record/WinBox/WinBoxConfigControl.cs @@ -0,0 +1,16 @@ +using System.Windows.Forms; + +namespace RCM { + public partial class WinBoxConfigControl : UserControl { + public WinBoxConfigControl(WinBoxConfig winBoxConfig) { + InitializeComponent(); + textBoxWinBox.Text = winBoxConfig.WinBoxPath; + } + + private void ButtonSelectWinBox_Click(object sender, System.EventArgs e) { + if (openFileDialogWinBox.ShowDialog() == DialogResult.OK) { + textBoxWinBox.Text = openFileDialogWinBox.FileName; + } + } + } +} diff --git a/RCM/Record/WinBox/WinBoxConfigControl.resx b/RCM/Record/WinBox/WinBoxConfigControl.resx new file mode 100644 index 0000000..d141af1 --- /dev/null +++ b/RCM/Record/WinBox/WinBoxConfigControl.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 + + + 17, 21 + + \ No newline at end of file diff --git a/RCM/RecordNodeSorter.cs b/RCM/RecordNodeSorter.cs new file mode 100644 index 0000000..234066b --- /dev/null +++ b/RCM/RecordNodeSorter.cs @@ -0,0 +1,32 @@ +using System.Collections; +using System.Windows.Forms; + +namespace RCM { + public class RecordNodeSorter : IComparer { + public int Compare(object o1, object o2) { + IRecord record1 = (IRecord)((TreeNode)o1).Tag; + IRecord record2 = (IRecord)((TreeNode)o2).Tag; + + int result = 0; + + if (record1 is Group) { + if (record2 is Group) { + result = string.Compare(record1.Title, record2.Title); + if (result == 0) { + result = string.Compare(record1.Id.ToString(), record2.Id.ToString()); + } + } else { + result = -1; + } + } else if (record2 is Group) { + result = 1; + } else { + result = string.Compare(record1.Title, record2.Title); + if (result == 0) { + result = string.Compare(record1.Id.ToString(), record2.Id.ToString()); + } + } + return result; + } + } +} diff --git a/RCM/SimpleHostControl.Designer.cs b/RCM/SimpleHostControl.Designer.cs new file mode 100644 index 0000000..2689fef --- /dev/null +++ b/RCM/SimpleHostControl.Designer.cs @@ -0,0 +1,122 @@ +namespace RCM { + partial class SimpleHostControl { + /// + /// 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 Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() { + this.buttonUnmaskPassword = new System.Windows.Forms.Button(); + this.textBoxPassword = new System.Windows.Forms.TextBox(); + this.textBoxUsername = new System.Windows.Forms.TextBox(); + this.textBoxHostAddress = new System.Windows.Forms.TextBox(); + this.labelPassword = new System.Windows.Forms.Label(); + this.labelUsername = new System.Windows.Forms.Label(); + this.labelHostAddress = new System.Windows.Forms.Label(); + this.SuspendLayout(); + // + // buttonUnmaskPassword + // + this.buttonUnmaskPassword.Location = new System.Drawing.Point(273, 51); + this.buttonUnmaskPassword.Name = "buttonUnmaskPassword"; + this.buttonUnmaskPassword.Size = new System.Drawing.Size(22, 22); + this.buttonUnmaskPassword.TabIndex = 4; + this.buttonUnmaskPassword.Text = "●"; + this.buttonUnmaskPassword.UseVisualStyleBackColor = true; + this.buttonUnmaskPassword.Click += new System.EventHandler(this.ButtonUnmaskPassword_Click); + // + // textBoxPassword + // + this.textBoxPassword.Location = new System.Drawing.Point(120, 52); + this.textBoxPassword.Name = "textBoxPassword"; + this.textBoxPassword.Size = new System.Drawing.Size(147, 20); + this.textBoxPassword.TabIndex = 3; + this.textBoxPassword.UseSystemPasswordChar = true; + // + // textBoxUsername + // + this.textBoxUsername.Location = new System.Drawing.Point(120, 26); + this.textBoxUsername.Name = "textBoxUsername"; + this.textBoxUsername.Size = new System.Drawing.Size(175, 20); + this.textBoxUsername.TabIndex = 2; + // + // textBoxHostAddress + // + this.textBoxHostAddress.Location = new System.Drawing.Point(120, 0); + this.textBoxHostAddress.Name = "textBoxHostAddress"; + this.textBoxHostAddress.Size = new System.Drawing.Size(175, 20); + this.textBoxHostAddress.TabIndex = 1; + // + // labelPassword + // + this.labelPassword.AutoSize = true; + this.labelPassword.Location = new System.Drawing.Point(0, 55); + this.labelPassword.Name = "labelPassword"; + this.labelPassword.Size = new System.Drawing.Size(56, 13); + this.labelPassword.TabIndex = 14; + this.labelPassword.Text = "Password:"; + // + // labelUsername + // + this.labelUsername.AutoSize = true; + this.labelUsername.Location = new System.Drawing.Point(0, 29); + this.labelUsername.Name = "labelUsername"; + this.labelUsername.Size = new System.Drawing.Size(58, 13); + this.labelUsername.TabIndex = 13; + this.labelUsername.Text = "Username:"; + // + // labelHostAddress + // + this.labelHostAddress.AutoSize = true; + this.labelHostAddress.Location = new System.Drawing.Point(0, 3); + this.labelHostAddress.Name = "labelHostAddress"; + this.labelHostAddress.Size = new System.Drawing.Size(72, 13); + this.labelHostAddress.TabIndex = 12; + this.labelHostAddress.Text = "Host address:"; + // + // SimpleHostControl + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.Controls.Add(this.buttonUnmaskPassword); + this.Controls.Add(this.textBoxPassword); + this.Controls.Add(this.textBoxUsername); + this.Controls.Add(this.textBoxHostAddress); + this.Controls.Add(this.labelPassword); + this.Controls.Add(this.labelUsername); + this.Controls.Add(this.labelHostAddress); + this.Name = "SimpleHostControl"; + this.Size = new System.Drawing.Size(300, 75); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.Button buttonUnmaskPassword; + private System.Windows.Forms.Label labelPassword; + private System.Windows.Forms.Label labelUsername; + private System.Windows.Forms.Label labelHostAddress; + public System.Windows.Forms.TextBox textBoxHostAddress; + public System.Windows.Forms.TextBox textBoxUsername; + public System.Windows.Forms.TextBox textBoxPassword; + } +} diff --git a/RCM/SimpleHostControl.cs b/RCM/SimpleHostControl.cs new file mode 100644 index 0000000..ad30da6 --- /dev/null +++ b/RCM/SimpleHostControl.cs @@ -0,0 +1,17 @@ +using System; +using System.Windows.Forms; + +namespace RCM { + public partial class SimpleHostControl : UserControl { + public SimpleHostControl(string host, string username, string password) { + InitializeComponent(); + textBoxHostAddress.Text = host; + textBoxUsername.Text = username; + textBoxPassword.Text = password; + } + + private void ButtonUnmaskPassword_Click(object sender, EventArgs e) { + textBoxPassword.UseSystemPasswordChar = !textBoxPassword.UseSystemPasswordChar; + } + } +} diff --git a/RCM/SimpleHostControl.resx b/RCM/SimpleHostControl.resx new file mode 100644 index 0000000..1af7de1 --- /dev/null +++ b/RCM/SimpleHostControl.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/RCM/TypeExtensions.cs b/RCM/TypeExtensions.cs new file mode 100644 index 0000000..0c7102a --- /dev/null +++ b/RCM/TypeExtensions.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RCM { + public static class TypeExtensions { + public static string GetDisplayName(this Type type) { + DisplayName displayName = (DisplayName)type.GetCustomAttributes(typeof(DisplayName), false).FirstOrDefault(); + return displayName != null ? displayName.Text : type.Name; + } + } +} diff --git a/RCM/packages.config b/RCM/packages.config new file mode 100644 index 0000000..e6fef98 --- /dev/null +++ b/RCM/packages.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/Resources/Config.png b/Resources/Config.png new file mode 100644 index 0000000000000000000000000000000000000000..67de2c6ccbeac17742f56cf7391e72b2bf5033ba GIT binary patch literal 512 zcmV+b0{{JqP)CQDsH?WF>AIFt zQuJ}i;w2$ZUU#3SZ6RY0Gw;kZ&ol1~2ky^QZ(fom$=jNJZt!z7w_pH~wdQ;R)Gh%BbQFCx+Nm!4SuS-vkr`vhhrX zM*>w%e+v~?m@q~ImPAgtLkR_3U<2F8LP3W5=LJ*ZN|S5p#sf4YFr$p~Q~Z*0Ngxf2 zjk#J#<7EAlhzlrV53~GF&pIzcCN_lz9@05UeoUXiK%N z#x+4o*i_c|6_Uu1+&TIho?3@y4k-#b8Y_o94zW*B3a1ne2-Y5s0uke$$|@=}OP-i= zNYZQA=>PrZu0MfSL=b8UhD_={W4IY1{b{)U)*gc45xtL%IYLY&hF;d`@GzI&7H&D# zh;z_BX$#hqh@q?AY3sJTod2%*Yd)_>YM0#q&ixGuh+PQsneK)F00007{s`4_9AgrbU#ENoY ztf~N{6=dOjm^cazlvkFMa#WI)bX1g)bhOadQkxm1^LlQi>GwHMJUar&CIQb1H~kLM zGdtAayCe^5#r16q{{Q&@ACA8Q+23LOfB*jd1hNTXkp3r^kN%hDWvjZeV-Z&MU^|F3 z4`kodE64xK@Ud3i-nH=mFCZU=k=^ox1oNI>J@#J~Xx_cu^TB2z`x#^=$Sg1hv5CMh zt{wd^$InvraL>GdzhQbQpkG}-@=soXx$5EGx&QwF?ML<}*nLnpqKo6=zrKFtKP}9A z1a#kTD!A_r)P2wP&j$M!6u-!RAtk+l-1Gj%;r|MPOjRKBKzvHf1BL&Gn}`1^3Ncr| zJUH|JUn0#zw-c6TLGJl@^U!}yNzR7Xhh~7yL-s3(O{#xC-#YkTM~1uc_Ns#akFV~< ziXUCw`wy2Ky7>LQ6aQ;VbJg!lWBk7#j2Zs#PiKPQ43_`2m@3+fC*~(B_6-Wbd zyW-0SZL%-#w@3ro3NP=s$-R8gq5SfEi_yz-wF);TW;+ E0F5+VjQ{`u literal 0 HcmV?d00001 diff --git a/Resources/Group.ico b/Resources/Group.ico new file mode 100644 index 0000000000000000000000000000000000000000..d2a3d3382f3a3256ddc7ebfdc7fac74236b086af GIT binary patch literal 1742 zcmciBIWI&}90l;J@d-q&^hM0>J1CVz>_I^gVh@54#D`E?#=Z=NLS-b(ZWfWKCdWB% zO!@9@#G9PC$(zY9=iPVz#x(HL)MWVGVh$UPX*R~RGQ8!jy02@Izy0SGy2vARp7Rns z%OQA{MKI2sW)M(Llr#dzX|x@s>TAibqr8PQFrNbEm|14#9hhdOn8`O_f*EII)>l6- zd|AK}y`Gn<(^OB{T7A8%0+#9Z(sT9Hv#YOGo41mwdFp91wV&Dpt$tqQ`U9)<`q)oB zZKmFl_EoFRTcg)cPd#mBl;@!H=}Yh5_x(n0idfGA1N79>W_CY+*UnNr+u_@f-UhwF zWEJY^GIc)vEZvXZ7O_chh@N`7Or3eFcUQs|ond#3)n-DWzyL>YT|V3hsT(`9Pjt^E=e?9&^gr=Bh|%z3wY?GF_k z(37d2F4O5fenMu9J!5=!?PpZsIr75usTuOZS?9bbZ2kV?H=t~irppoi#~oQe9W6+H literal 0 HcmV?d00001 diff --git a/Resources/MySQL.ico b/Resources/MySQL.ico new file mode 100644 index 0000000000000000000000000000000000000000..ec9e9e0147d3b4376421748feec76e553ee14656 GIT binary patch literal 1742 zcmdUt&nrYx6vxjBZ)zw2u1l(l7+0SWyfDYvXEq^!~!+bl#QrKiLHe~Sy@@l zzhJVM?TxKGpEJ$WG;`nc!h-MX^X`4$bMAfj-g8AlJmIkLj>v75NVACa16Hn8UzH&C zPt?RZ=YCVD*wx^(Xc>_k_JE~@o?)3!pzWgbwj-}mN1o|t hjFHS6yco%5^Q4WW<3=7kjU;D`#G*z*%+UZk*C!+IPuc(g literal 0 HcmV?d00001 diff --git a/Resources/RDP.ico b/Resources/RDP.ico new file mode 100644 index 0000000000000000000000000000000000000000..9b1c0afe53d4a862120040ee8a977f0645a31397 GIT binary patch literal 1742 zcmb7^YfO_@7{}kYsM`lcMa1D{D%*_PaC7sLm@J!a%PuZUHn+qNYFsw6&C7Jlh=PIx zi&K$ntMK+hDW#=C?HziVl(sCXkDlIL2SXNs0kjLW%3kwP|1OmZ|i+OqEzxmbG)rl-C zi)D1NF&PernVy~=@OV5(rBX4f+FHh9H9?>~0JC+k*Ne1T?F664E1P9L%ahCHbBspg z38&LJ;dZ-Y9UUErc9bqZ2{yM4!hK;hFgSqvZ}u}t2=d(P6 z!SEd0FTbIo;jzQvm|-=dU@!>m2~w#Pb#-?_Uw;@f)w!@odJvp#PU!9FhEOPkSPh8P z7<`r|l~s{ywfZ%UMk6zsOyhNRbu+A9V0D9lq98gNh1-9Q0M~LJ_E!G{b#^^mQk@5L zgO24j0sE(uvphZ}CM9|eX)u||N^$v|Q|al+$J5dd=3mUSl@yme=5o&>KA#U`V`I<~ zZiWm~E;!rmaI)qUWGYU8!)Zf}jg3!vB_)^VaJlCav$M~vOHV)bF8A#DZ+Mrfe1TY6 zM+qg7it>sXMHz*J#YJ!<62bQ$-p8Z&qIhWZ796TQ4qR;xc>Of8*=$eyl}KbS7nWSE z;7iJ{%N5G|My-CzD6O2inv;W+-|YwaryqlS)9X;aYbTgAYCQSi0iKCXO25cf%s9FB><_?#(DUC&%xnao^VyZ{x?j|0I10;&?{ zg7LkLpiKJ-TXY&ctMksCI}nXVkvHIB{h}FXeOM|GP%Aooft2d}!oY>(L?%Bu0p%@= zM|lh9qLO$LM2ksKEFnO@dI8X1?EwGxUt{ZuBlyaNTwKX30%_?bBoqm!ii?VhS??vY zUg!9oO$%!&VIyzrn@s73b?DOi)xcl!90=oyrz~wE0ru1v;QG;hFqrWJ3}+pJ;hbY| zE9WTm6`euXEapdIk?62qA?HvOMOuH)oujEzsHvSFG148|kYv-VKrM;`c>;m8O9}kP zG6DzQdkshPGvO}p3?9Fniyu(u@FU>`7#HOs+HRYcNXj#8@(PYfEG7lp-s040^TSj7x(8zI4XEGK#8fMl9w9}oQfsv&wS60>&S=`9 zIG*;Pd`%LnT|&aOWCDCI640MQ!ua-=fN|CXigY6A?*tg^0T>Dax*Y-3A4JhEFQe1x z!z}M5ds8z>efb%w*BMjgS(#&ke!nPXC8Jv$$2gMWnBa?Z z(XDk0(Y>R)(O<0%%w3l?HcH!KQLlYwgl?F++2*)A;A@Etgt~%sTbru3re^iPtszn^ zm62MFVYOCS$Z{%WmpzS;7}vBsw6@+LQ(?eciPZ>2rE+-~~AHky9h z1pdhp+Ju39@4EoZ&FwrV?>j_jfhaxYtcaAx(&klT1Z zo5Lb_iF?u*=9&nE0@3A`t}R@LNm=xG%dz}o6&co>rTkmIP+xpb2|bpC8p3JHAJIO$Z&0-r1;peR-NmaO5-wT5ZDj0rw^Vm%4wI zzP~brXlJQ1J$Q*paO5<;ylP};_pM534m)`;<8M9x!t-Mm&Zu{@@*~*HBsg+L9<@~^ zeKwkg>#^p;jM)20;T!X9Rc1o1OoAh4STDlXq_1$L1!w z1spjm(HB<}4SREwXJ@A=IMs9Y319H0xn$D6^XaFDl|xIV?{e`QIOf&a;EUrdtA+1> oYrTWY;p+jl!og=|iAQG1V_HNmej9WMm9FWOp{f+5!C4vo0XcA14FCWD literal 0 HcmV?d00001 diff --git a/Resources/SSH.ico b/Resources/SSH.ico new file mode 100644 index 0000000000000000000000000000000000000000..17090e72fac511837dd27fe587b3568efe1a5cdc GIT binary patch literal 1742 zcmd^yvAGN{ce*HwGt5|f*>LY%2-II3D_OM zS~8Vw1_DXBX8jx1;Ur8kGu<#-mfN$>+28rszt&c%5BxbgQv7|azJ65dvr_7WgO~J- zb&M~3?_V47c$`wH)L%!jSTw9TClU$DWHNMpeNE|fnnIxvg~MTr#bOkRMD%qq7$lF! zLr$lYE-o&};c$@4uoy@$g=GoPQI zsn_e#`T4o&C-2dDf(agKz&YE5$v!=n%jGvrTt7TKnE7os8?xK&ZU`GQ$0m9&?VJA9Wh7z}hDMWa#O zQ|KdMvWJhtW6d_yzgDZgVa{eVLD6I~p=2^i=zsJPn9wLp)PVON3zP3Zv1ZLg-oxS0 zWR1sTv)`>uYro?ioZp%7*wcIVl)&uw`}F$ys(FTC=suD&1RcL(LM!XI6&}|;k8=U$ zVzJPy?RKkcF1=}GqDOGnkhkdMUNg@!^G?xw;58Zzoj3SW^WR~j2BHr$Yc~GmzXo|* j1NyDJ|KT^$4~1a&IBf6YlgcY=DD{n>u*P@376#^@0aMT} literal 0 HcmV?d00001 diff --git a/Resources/Website.ico b/Resources/Website.ico new file mode 100644 index 0000000000000000000000000000000000000000..1b561a0deb4710157e0a38e797b7ad35391d106b GIT binary patch literal 1742 zcmZvd3sg;67{|X`NcZOIR=3idUMN(Q(nB>pq#3=FJbI`QnsmLUF(sCo$so!*kAxzb z5(%|ZQ`F=gku}jUUSlMWH4|6+Kl>VtvFx+{=ljn7&id_d|Ia$-5DEC9rA7Q~LRI~U zw25da7oU0?74wRb!)u?8O;PHCL@$xv)N!JSnDOdMCy&*P4Yik!P4Lo}e(Pu0C)LYP zd7-OepCkwUK6C8!l;dr5ls;vAe4Q4krc)3n+Iw}C3sQ=&uYM ztrhNNIUwBLKrL#t-hgOFL$zt{gN2^K!!!({MrwYhJEHPrjTv2=B=WQ%?E={Zso89I z4YN&&wrgwBZT~0;mp&;Aw|rR7fE-6U_d#8b00?={V|M0JEcE1BVrQ(>b~JH-i&QD2}Xej>^P zYtLE_Bw$&%-#K04UrJOKIQgltiW z5w$!`d1!W~dM%?01tN}jtJiZ=OMh>hNR*tWqY?)5=C>`t=kZb?$@ z+3Bc^9S($oafH3&iFrr%hi`a46uJ_2IT5zoE21h&eT{H3!Uao5>2$dmiew?y5|M&= zb%XuTzf0X-zSwKe_PZ1Bf%v_IqmzhL1reJNOQ1P~l*xp0f5Jg8zLv9sD?r{{U zs>6vL@h8RFEFsF0t%~A!Gi1<~KK3uVK}Zcv}1wn&OmkVK$+4KB0tj z1V0c|G6}M!gt~cz<~TzB3_?a2VR*b2A``5U6=eCwXNYLyq%i~46?^W;(kObE?+MnJ zf?JuUxVeZ>wVaTXO>D_J?qfFLK?b2Sm3tv0^yhT}XR9PA!zMuovItWaY%V13?Sb~sOFvy=1gVoDa|DW$12G~Mv77cJcX(XUS)>vnA_=n|5}qP zg=Mu5D%!WA_kKN&{I-t;Ui$`R_qGDRW&tNW!1DWJ|9ZlAD+vyqCr%f7#9kqXvo#4MvfD~`ht_TlJ#b@oqDA_sGBB#juv3o-}5--k2?(bXh;KgOu`|2s=Z#ody zmWsfi!;n*KgpRdJxU-C~ELFg==ZY?W9WZ!ea*(ZK>?C912Ov*Q?=%nkWru$E`no{W zUQWi{j@9fzcLU@-Pq3r25VnnxNG~!$R~F$`ssJ@9YVCtt zZI#8r`g*&QN94}^a*YKI=wwax5*<;puVxPs+wyheETZ_EfpVu#Y zRJPLhZj^^b_@uF4i9lQLZ~0EUm2!HK{ERGOR50 zlT_^qHoj04o!nj+cC<2M?DbNad24-!(Q%o-x|=LOzb~nD^B)fvC3D%llllkza-Mx& P>U5gdI^U^>N5=IJt(Lp| literal 0 HcmV?d00001 diff --git a/Resources/WinBox.ico b/Resources/WinBox.ico new file mode 100644 index 0000000000000000000000000000000000000000..ed44ef5408575f6f16dd19b0d32be99aeb99007f GIT binary patch literal 1742 zcma))4K$Ql7{^~ZVT*+7gw$@EB8$%M*|W7Rg__bSAEm@eCA2jRvPr0nZ%e!FPGx+o zv|$>d+2o^PFk*AWCLh^87Fml@n=zq5^FRCE$(&8cId$)O-sgSqd++Z&|NnC-N`;>D z=27(XW9o()MJ=Ex%7hMjNs-Aq89&kgK6E5+pEHBwsAU*ot8N`>r{Na0LBpMGr(tv6 zS$lEXPF>Xx<&b>|n`c>tuT$eE+Ry6$W!GFe&qEKF>Bw-Mi=-{H2Ts~(2-q7n8Si7B zbJEfbUOO}CtdrJgq0a)`IJ|I52$}jQ^EE^%(+KIV^JSqnnyHxwEp(>i64r6+nfW<; zbmi}I$hVMbpx_iSm!Q;t8N|V+C}u4Oo1rbt@-fqWmr3fHj#YYCAN)frAWHcT%@urf zHP@o6r50^fxwyqW1kv#?5oW8E!8>H3qGS@kh{m_;64D%j?{ZY(u#5zutIcfU5x(c-UA0$mJO9 zY(!g{CmJHx;7;^f+=*L{mZXh%z}bpU?hZUkc0u%(`9eX^S88XSv<*ogl^&J*8KEe^ z6b~CjkWY-`pPEZ(jIlyp#2Sd=wxd4pN3{Qb9S`bC&{mp`huMMX&-Mb}?~9>}-k%vq zZurPL!&OIK=C=qH0ZUMM18w|J4~hlbP6;Ot)N9L<>Rmb-r;Dg&3I`sfN22U$Y;;u)kpZD>wnpeAM$I+|`` zVsr>kMI7{G?nCdd`|vc!2hXoCA-NiWmxaOTxZsVn{bsD#&06jSK8sNsvJ!RSmZ(h& z!0T}t9`S;3H<5vR;(P_Gw;%+)7x&^xju)Qi9>!pSKcv@=Vx%|}vKuGRoyA0km$`q; zra3m34=lm$Q=g+Tay9OiWkTBBjQbo1)F*pEHu@6Js?MW7>j0h+ZTTz={T77bq7b|) z4aZnn1YTD}p*__TTn|&13ww+fpWCTF&=mU(niFi$b-M)pm0WbDxZqEkJ<`*Hr+g;< z&GW^OAP~ca$1qwFhB4tOOo*Z&uZ#hz;!zy7aX2qc*!f9Sb)2I*xq}JY7BKStEM<>V zccDYbMIWv03GX2KTB{)`3ZK+=0^^g~Q1ymG>LSr_#%-Ltcja!vQjSv$z0^NWZ$p1B zOVNLc;0Suf7yj2qdnB)<@6jYfnfr5t5a(ccMI7rmL&+k!;HZ^Oyn}(TJ=tB+|53Vc zBeaeQQREbDN)E|{#c^MLi)Jl+o27iecFbBgVv}BmIA-gZv?!GBTQptIRBePq{304B z>zbCOWEO|ns&L$l9TJ=t2@6APhuXNF=(^;qi1t)3MIVXx#4mBI(-e(L#&o%yZLh{> ztu{{fFm+DzG-stBF!$%~GjqugvNE|D@2u$Kbmv~HN+_9%gnCQ`QetWq9dju*9q$`t O57|r3kh2nc64rl2c1v3T literal 0 HcmV?d00001