Commits (16)
using System; using pEp.Extensions;
using System;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
......
...@@ -28,14 +28,15 @@ namespace pEp.DPE ...@@ -28,14 +28,15 @@ namespace pEp.DPE
if (context.Request.HttpMethod != "POST") if (context.Request.HttpMethod != "POST")
{ {
Log.Warning("Process request: Ignoring request of type " + context.Request.HttpMethod); Log.Warning("Process request: Ignoring request of type " + context.Request.HttpMethod);
return;
} }
string request = null; string request = null;
try try
{ {
using (var stream = context.Request.InputStream) using (Stream stream = context.Request.InputStream)
using (var sr = new StreamReader(stream)) using (StreamReader sr = new StreamReader(stream))
{ {
request = sr.ReadToEnd(); request = sr.ReadToEnd();
} }
...@@ -43,6 +44,8 @@ namespace pEp.DPE ...@@ -43,6 +44,8 @@ namespace pEp.DPE
catch (Exception ex) catch (Exception ex)
{ {
Log.Error("ProcessRequest: Error getting request. " + ex.ToString()); Log.Error("ProcessRequest: Error getting request. " + ex.ToString());
context.Response.StatusCode = (int)HttpStatusCode.BadRequest;
context.Response.Close();
return; return;
} }
......
using pEp.DPE.Interfaces; using pEp.DPE.Interfaces;
using pEp.Extensions;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using Outlook = Microsoft.Office.Interop.Outlook;
namespace pEp.DPE namespace pEp.DPE
{ {
internal class DistributedPolicyEngine : IDistributedPolicyEngine internal class DistributedPolicyEngine : IDistributedPolicyEngine
{ {
private readonly static string DPE_FOLDER = Path.Combine(Globals.PEPUserFolder, "DPE"); private static readonly string DPE_FOLDER = Path.Combine(Globals.PEPUserFolder, "DPE");
private readonly static string PATCH_EXTENSION = ".patch"; public static readonly string DPE_TEMP_LOCATION = Path.Combine(DistributedPolicyEngine.DPE_FOLDER, "temp");
private const string PATCH_EXTENSION = ".patch";
private const string PATCH_MESSAGE_SUBJECT = "Configuration changes";
public const string DPE_MESSAGE_CLASS = "IPM.Note.DPE";
private readonly PEPIdentity ownIdentity;
private readonly PatchEvents patchEvents; private readonly PatchEvents patchEvents;
/// <summary> /// <summary>
...@@ -19,16 +26,17 @@ namespace pEp.DPE ...@@ -19,16 +26,17 @@ namespace pEp.DPE
{ {
this.patchEvents = new PatchEvents(); this.patchEvents = new PatchEvents();
this.Subscribe(this.patchEvents); this.Subscribe(this.patchEvents);
this.ownIdentity = new PEPIdentity(Globals.ThisAddIn.Settings.AccountSettingsList.First().SmtpAddress);
} }
#region Event handlers #region Event handlers
/// <summary> /// <summary>
/// Event handler for when a patch has been accepted. /// Event handler for when a patch has been accepted.
/// </summary> /// </summary>
private void PatchEvents_PatchAccepted(object sender, PatchEventArgs e) private void PatchEvents_PatchAccepted(object sender, PatchEventArgs e)
{ {
ThisAddIn.PEPEngine.ShowNotification("pEp Distributed Policy Engine", $"Patch with id {e.Patch.Id} was accepted."); ThisAddIn.PEPEngine.ShowNotification("pEp Distributed Policy Engine", $"Patch with id { e.Patch.Id } was accepted.");
} }
/// <summary> /// <summary>
...@@ -36,7 +44,7 @@ namespace pEp.DPE ...@@ -36,7 +44,7 @@ namespace pEp.DPE
/// </summary> /// </summary>
private void PatchEvents_PatchRejected(object sender, PatchEventArgs e) private void PatchEvents_PatchRejected(object sender, PatchEventArgs e)
{ {
ThisAddIn.PEPEngine.ShowNotification("pEp Distributed Policy Engine", $"Patch with id {e.Patch.Id} was rejected."); ThisAddIn.PEPEngine.ShowNotification("pEp Distributed Policy Engine", $"Patch with id { e.Patch.Id } was rejected.");
} }
/// <summary> /// <summary>
...@@ -46,10 +54,58 @@ namespace pEp.DPE ...@@ -46,10 +54,58 @@ namespace pEp.DPE
{ {
// Save the patch and show a notification // Save the patch and show a notification
Patch patch = e.Patch; Patch patch = e.Patch;
this.SavePatch(patch); this.CreatePatchMailItem(patch, e.Submitter);
ThisAddIn.PEPEngine.ShowNotification("New patch suggested", patch.CommitMessage + " " + patch.Diff); ThisAddIn.PEPEngine.ShowNotification("New patch suggested", patch.CommitMessage + " " + patch.Diff);
} }
/// <summary>
/// Creates a mail item that shows a suggested patch.
/// </summary>
/// <param name="patch">The patch to show.</param>
/// <param name="submitter">The submitter of the patch.</param>
private void CreatePatchMailItem(Patch patch, PEPIdentity submitter)
{
Outlook.MailItem omi = null;
Outlook.MailItem mi = null;
try
{
// Use a PEPMessage to define the message to create
PEPMessage patchMessage = new PEPMessage
{
From = submitter,
LongMsgFormattedHtml = patch.Serialize(),
ShortMsg = DistributedPolicyEngine.PATCH_MESSAGE_SUBJECT,
};
patchMessage.To.Add(this.ownIdentity);
// Create the mail item and apply the PEPMessage
omi = Globals.ThisAddIn.Application.CreateItem(Outlook.OlItemType.olMailItem) as Outlook.MailItem;
patchMessage.ApplyTo(omi, false, true, true);
// Add custom message class, set time, and mark as Sent (so that it won't appear as draft)
MapiHelper.SetProperties(omi, new MAPIProperties
{
{ MapiProperty.PidTagMessageClass, DistributedPolicyEngine.DPE_MESSAGE_CLASS },
{ MapiProperty.PidTagMessageDeliveryTime, DateTime.UtcNow }
});
omi.SetMessageFlag(MapiPropertyValue.EnumPidTagMessageFlags.mfUnsent, false);
// Move to inbox and set Received time
mi = omi.Move(Globals.ThisAddIn.Application.Session.GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox));
mi.Save();
}
catch (Exception ex)
{
Log.Error("CreatePatchMailItem: Error creating patch mail item. " + ex);
}
finally
{
omi = null;
mi = null;
}
}
#endregion #endregion
#region Methods #region Methods
...@@ -71,7 +127,6 @@ namespace pEp.DPE ...@@ -71,7 +127,6 @@ namespace pEp.DPE
public void Reject(Patch patch, PEPIdentity me) public void Reject(Patch patch, PEPIdentity me)
{ {
DPEWebClient.RejectPatch(patch, me); DPEWebClient.RejectPatch(patch, me);
this.DeletePatch(patch);
AdapterExtensions.ShowNotification("Patch rejected", patch.CommitMessage); AdapterExtensions.ShowNotification("Patch rejected", patch.CommitMessage);
} }
...@@ -81,9 +136,9 @@ namespace pEp.DPE ...@@ -81,9 +136,9 @@ namespace pEp.DPE
/// <param name="patchEvents">The patch events to subscribe to.</param> /// <param name="patchEvents">The patch events to subscribe to.</param>
public void Subscribe(PatchEvents patchEvents) public void Subscribe(PatchEvents patchEvents)
{ {
patchEvents.PatchAccepted += PatchEvents_PatchAccepted; patchEvents.PatchAccepted += this.PatchEvents_PatchAccepted;
patchEvents.PatchRejected += PatchEvents_PatchRejected; patchEvents.PatchRejected += this.PatchEvents_PatchRejected;
patchEvents.PatchSuggested += PatchEvents_PatchSuggested; patchEvents.PatchSuggested += this.PatchEvents_PatchSuggested;
} }
/// <summary> /// <summary>
...@@ -105,7 +160,6 @@ namespace pEp.DPE ...@@ -105,7 +160,6 @@ namespace pEp.DPE
public void Support(Patch patch, PEPIdentity me) public void Support(Patch patch, PEPIdentity me)
{ {
DPEWebClient.SupportPatch(patch, me); DPEWebClient.SupportPatch(patch, me);
this.DeletePatch(patch);
AdapterExtensions.ShowNotification("Patch supported", patch.CommitMessage); AdapterExtensions.ShowNotification("Patch supported", patch.CommitMessage);
} }
...@@ -115,88 +169,9 @@ namespace pEp.DPE ...@@ -115,88 +169,9 @@ namespace pEp.DPE
/// <param name="patchEvents">The patch events to unsubscribe from.</param> /// <param name="patchEvents">The patch events to unsubscribe from.</param>
public void Unsubscribe(PatchEvents patchEvents) public void Unsubscribe(PatchEvents patchEvents)
{ {
patchEvents.PatchAccepted += PatchEvents_PatchAccepted; patchEvents.PatchAccepted -= this.PatchEvents_PatchAccepted;
patchEvents.PatchRejected += PatchEvents_PatchRejected; patchEvents.PatchRejected -= this.PatchEvents_PatchRejected;
patchEvents.PatchSuggested += PatchEvents_PatchSuggested; patchEvents.PatchSuggested -= this.PatchEvents_PatchSuggested;
}
/// <summary>
/// Deletes the patch from the disk.
/// </summary>
/// <param name="patch">The patch to delete.</param>
/// <returns>True if the patch was deleted or not found, otherwise false.</returns>
private bool DeletePatch(Patch patch)
{
try
{
string fileName = Path.Combine(DistributedPolicyEngine.DPE_FOLDER, patch.Id + DistributedPolicyEngine.PATCH_EXTENSION);
if (File.Exists(fileName))
{
File.Delete(fileName);
}
return true;
}
catch (Exception ex)
{
Log.Error("DeletePatch: Error occured. " + ex.ToString());
}
return false;
}
/// <summary>
/// Saves the patch to disk.
/// </summary>
/// <param name="patch">The patch to save.</param>
/// <returns>True if the patch was saved successfully, otherwise false.</returns>
private bool SavePatch(Patch patch)
{
try
{
string fileName = Path.Combine(DistributedPolicyEngine.DPE_FOLDER, patch.Id + DistributedPolicyEngine.PATCH_EXTENSION);
string xml = patch.Serialize();
File.WriteAllText(fileName, xml);
return File.Exists(fileName);
}
catch (Exception ex)
{
Log.Error("SavePatch: Error saving patch. " + ex.ToString());
}
return false;
}
#endregion
#region Static methods
/// <summary>
/// Gets the patches that are currently saved to disk.
/// </summary>
/// <returns>The list of open patches or an empty list if none was found.</returns>
internal static List<Tuple<Patch, bool>> GetOpenPatches()
{
List<Tuple<Patch, bool>> patches = new List<Tuple<Patch, bool>>();
try
{
foreach (string file in Directory.GetFiles(DistributedPolicyEngine.DPE_FOLDER, "*" + DistributedPolicyEngine.PATCH_EXTENSION))
{
string xml = File.ReadAllText(file);
if ((Patch.Deserialize(xml) is Patch patch) &&
(patch != null))
{
patches.Add(new Tuple<Patch, bool>(patch, false));
}
}
}
catch (Exception ex)
{
Log.Error("GetPatches: Error getting patches. " + ex.ToString());
}
return patches;
} }
#endregion #endregion
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
{ {
internal interface IPatchEvents internal interface IPatchEvents
{ {
void Suggested(Patch patch); void Suggested(Patch patch, PEPIdentity submitter);
void Rejected(Patch patch, Patch.RejectReason rejectReason); void Rejected(Patch patch, Patch.RejectReason rejectReason);
void Accepted(Patch patch); void Accepted(Patch patch);
} }
......
using System; using pEp.Extensions;
using System.IO; using System;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
namespace pEp.DPE namespace pEp.DPE
{ {
...@@ -62,54 +59,13 @@ namespace pEp.DPE ...@@ -62,54 +59,13 @@ namespace pEp.DPE
} }
/// <summary> /// <summary>
/// Serializes this patch into XML format. /// Deserializes a JSON string into a Patch object.
/// </summary> /// </summary>
/// <returns>The serialized object as XML string.</returns> /// <param name="json">The JSON string to process.</param>
public string Serialize()
{
XmlWriterSettings xmlWriterSettings = new XmlWriterSettings
{
//Indent = true,
OmitXmlDeclaration = true,
Encoding = Encoding.UTF8,
CheckCharacters = false
};
using (StringWriter stringWriter = new StringWriter())
using (XmlWriter writer = XmlWriter.Create(stringWriter, xmlWriterSettings))
{
XmlSerializer serializer = new XmlSerializer(typeof(Patch));
serializer.Serialize(writer, this, new XmlSerializerNamespaces(new[] { XmlQualifiedName.Empty }));
return stringWriter.ToString();
}
}
/// <summary>
/// Deserializes an XML string into a Patch object.
/// </summary>
/// <param name="xml">The XML to process.</param>
/// <returns>The Patch object or null if an error occured.</returns> /// <returns>The Patch object or null if an error occured.</returns>
public static Patch Deserialize(string xml) public static Patch Deserialize(string json)
{ {
if (string.IsNullOrEmpty(xml)) return json.Deserialize<Patch>();
{
Log.Info("Deserialize: XML is empty.");
return null;
}
XmlSerializer xmlSerializer = new XmlSerializer(typeof(Patch));
using (StringReader stringReader = new StringReader(xml))
{
try
{
return xmlSerializer.Deserialize(stringReader) as Patch;
}
catch (Exception ex)
{
Log.Error("Deserialize: Error deserializing Patch object: " + ex.ToString());
return null;
}
}
} }
} }
} }
...@@ -4,7 +4,8 @@ namespace pEp.DPE ...@@ -4,7 +4,8 @@ namespace pEp.DPE
{ {
public class PatchEventArgs : EventArgs public class PatchEventArgs : EventArgs
{ {
public Patch Patch { get; set; } public Patch Patch { get; set; }
public Patch.RejectReason Reason { get; set; } = Patch.RejectReason.None; internal PEPIdentity Submitter { get; set; }
public Patch.RejectReason Reason { get; set; } = Patch.RejectReason.None;
} }
} }
...@@ -33,7 +33,7 @@ namespace pEp.DPE ...@@ -33,7 +33,7 @@ namespace pEp.DPE
Patch patch; Patch patch;
if ((patch = Patch.Deserialize(e.Request)) != null) if ((patch = Patch.Deserialize(e.Request)) != null)
{ {
this.Suggested(patch); this.Suggested(patch, new PEPIdentity("patchadmin@pep.security"));
} }
} }
...@@ -72,9 +72,9 @@ namespace pEp.DPE ...@@ -72,9 +72,9 @@ namespace pEp.DPE
/// Raises the Patch suggested event. /// Raises the Patch suggested event.
/// </summary> /// </summary>
/// <param name="patch">The patch that has been suggested.</param> /// <param name="patch">The patch that has been suggested.</param>
public void Suggested(Patch patch) public void Suggested(Patch patch, PEPIdentity submitter)
{ {
this.PatchSuggested?.Invoke(this, new PatchEventArgs { Patch = patch }); this.PatchSuggested?.Invoke(this, new PatchEventArgs { Patch = patch, Submitter = submitter });
} }
#endregion #endregion
......
...@@ -17,16 +17,18 @@ namespace pEp ...@@ -17,16 +17,18 @@ namespace pEp
/// </summary> /// </summary>
internal static class MailItemExtensions internal static class MailItemExtensions
{ {
public const string USER_PROPERTY_KEY_ORIG_ENTRY_ID = "origEntryID"; public const string USER_PROPERTY_KEY_DPE_PATCH_STATUS = "patchStatus";
public const string USER_PROPERTY_KEY_DPE_PATCH_EDIT_DATE = "patchEditDate";
public const string USER_PROPERTY_KEY_INSPECTOR_CLOSED = "inspectorClosed"; public const string USER_PROPERTY_KEY_INSPECTOR_CLOSED = "inspectorClosed";
public const string USER_PROPERTY_KEY_IS_INCOMING = "isIncoming"; public const string USER_PROPERTY_KEY_IS_INCOMING = "isIncoming";
public const string USER_PROPERTY_KEY_IS_MIRROR = "isMirror"; public const string USER_PROPERTY_KEY_IS_MIRROR = "isMirror";
public const string USER_PROPERTY_KEY_ORIG_ENTRY_ID = "origEntryID";
public const string USER_PROPERTY_KEY_PROCESSING_STATE = "processingState"; public const string USER_PROPERTY_KEY_PROCESSING_STATE = "processingState";
public const string USER_PROPERTY_KEY_REPLY_ACTION = "replyAction"; public const string USER_PROPERTY_KEY_REPLY_ACTION = "replyAction";
public const string UNKNOWN_SENDER = "unknown"; public const string UNKNOWN_SENDER = "unknown";
private static readonly object mutexCopiedItemsList = new object(); private static readonly object mutexCopiedItemsList = new object();
private static List<string> copiedItemsList = new List<string>(); private static readonly List<string> copiedItemsList = new List<string>();
/// <summary> /// <summary>
/// Enumeration defining the standard, setable pEp properties of an extended MailItem. /// Enumeration defining the standard, setable pEp properties of an extended MailItem.
...@@ -1190,7 +1192,6 @@ namespace pEp ...@@ -1190,7 +1192,6 @@ namespace pEp
/// <returns>True if it is an attached mail, otherwise false.</returns> /// <returns>True if it is an attached mail, otherwise false.</returns>
public static bool GetIsAttachedMail(this Outlook.MailItem omi, out string messageId) public static bool GetIsAttachedMail(this Outlook.MailItem omi, out string messageId)
{ {
messageId = null;
HeaderList headers = omi.GetParsedTransportMessageHeaders(); HeaderList headers = omi.GetParsedTransportMessageHeaders();
try try
......
using System; using Newtonsoft.Json;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
namespace pEp.Extensions namespace pEp.Extensions
{ {
internal static class TypeExtensions internal static class TypeExtensions
{ {
/// <summary> /// <summary>
/// Converts an object to its serialized XML format. /// Deserializes a JSON string into a given object.
/// </summary> /// </summary>
/// <typeparam name="T">The type of object we are operating on</typeparam> /// <param name="json">The json string to process.</param>
/// <param name="value">The object we are operating on</param> /// <returns>The deserialized object or null if an error occured.</returns>
/// <returns>The XML string representation of the object</returns> public static T Deserialize<T>(this string json) where T : class
public static string Serialize<T>(this T value, bool indent = true) where T : class
{ {
XmlWriterSettings xmlWriterSettings = new XmlWriterSettings if (string.IsNullOrEmpty(json))
{
Indent = indent,
OmitXmlDeclaration = true,
CheckCharacters = false,
Encoding = Encoding.UTF8
};
using (StringWriter stringWriter = new StringWriter())
using (XmlWriter xmlWriter = XmlWriter.Create(stringWriter, xmlWriterSettings))
{ {
XmlSerializer serializer = new XmlSerializer(value.GetType()); Log.Info("Deserialize: json is empty.");
return null;
try
{
serializer.Serialize(xmlWriter, value, new XmlSerializerNamespaces(new[] { XmlQualifiedName.Empty }));
}
catch (Exception ex)
{
Log.Error("Serialize: Error occured. " + ex.ToString());
return null;
}
return stringWriter.ToString();
} }
return JsonConvert.DeserializeObject<T>(json);
} }
/// <summary> /// <summary>
/// Deserializes an XML string into a given object. /// Serializes a given object into a´JSON string.
/// </summary> /// </summary>
/// <param name="xml">The XML to process.</param> /// <typeparam name="T">The type of the object to serialize.</typeparam>
/// <returns>The deserialized object or null if an error occured.</returns> /// <param name="obj">The object to serialize.</param>
public static object Deserialize(Type type, string xml) /// <returns>The serialized string or null if an error occured.</returns>
public static string Serialize<T>(this T obj)
{ {
if (string.IsNullOrEmpty(xml)) return JsonConvert.SerializeObject(obj);
{
Log.Info("Deserialize: XML is empty.");
return null;
}
XmlSerializer xmlSerializer = new XmlSerializer(type);
using (StringReader stringReader = new StringReader(xml))
{
try
{
return xmlSerializer.Deserialize(stringReader);
}
catch (Exception ex)
{
Log.Error("Deserialize: Error deserializing Patch object: " + ex.ToString());
return null;
}
}
} }
} }
} }
...@@ -578,6 +578,8 @@ namespace pEp ...@@ -578,6 +578,8 @@ namespace pEp
[FieldOffset(0)] [FieldOffset(0)]
public double at; public double at;
[FieldOffset(0)] [FieldOffset(0)]
public System.Runtime.InteropServices.ComTypes.FILETIME ft;
[FieldOffset(0)]
public IntPtr lpszA; public IntPtr lpszA;
[FieldOffset(0)] [FieldOffset(0)]
public IntPtr lpszW; public IntPtr lpszW;
...@@ -1455,6 +1457,23 @@ namespace pEp ...@@ -1455,6 +1457,23 @@ namespace pEp
} }
} }
break; break;
case MapiProperty.MapiDataType.PtypTime:
{
try
{
long fileTime = ((DateTime)value).ToFileTimeUtc();
sPropValue.Value.ft = new System.Runtime.InteropServices.ComTypes.FILETIME
{
dwLowDateTime = (int)(fileTime & 0xFFFFFFFF),
dwHighDateTime = (int)(fileTime >> 32)
};
}
catch (Exception ex)
{
throw new Exception(string.Format("Error converting to Filetime. Property tag: {0}. Value: {1}. Exception: {2}.", property.DaslName, value?.ToString(), ex.ToString()));
}
}
break;
default: default:
{ {
throw new Exception(string.Format("Error creating SPropValue. Data type {0} not supported.", Enum.GetName(typeof(MapiProperty.MapiDataType), property.DataType))); throw new Exception(string.Format("Error creating SPropValue. Data type {0} not supported.", Enum.GetName(typeof(MapiProperty.MapiDataType), property.DataType)));
...@@ -1769,6 +1788,23 @@ namespace pEp ...@@ -1769,6 +1788,23 @@ namespace pEp
} }
} }
break; break;
case MapiProperty.MapiDataType.PtypTime:
{
try
{
long fileTime = ((DateTime)value).ToFileTimeUtc();
sPropValue.Value.ft = new System.Runtime.InteropServices.ComTypes.FILETIME
{
dwLowDateTime = (int)(fileTime & 0xFFFFFFFF),
dwHighDateTime = (int)(fileTime >> 32)
};
}
catch (Exception ex)
{
throw new Exception(string.Format("Error converting to Filetime. Property tag: {0}. Value: {1}. Exception: {2}.", mapiProperty.DaslName, value?.ToString(), ex.ToString()));
}
}
break;
default: default:
{ {
throw new Exception(string.Format("Error creating SPropValue. Data type {0} not supported.", Enum.GetName(typeof(MapiProperty.MapiDataType), mapiProperty.DataType))); throw new Exception(string.Format("Error creating SPropValue. Data type {0} not supported.", Enum.GetName(typeof(MapiProperty.MapiDataType), mapiProperty.DataType)));
......
...@@ -1614,7 +1614,6 @@ namespace pEp ...@@ -1614,7 +1614,6 @@ namespace pEp
// TODO: Check whether this method should be part of the engine? // TODO: Check whether this method should be part of the engine?
int unencryptedCount = 0; int unencryptedCount = 0;
pEpRating rating = pEpRating.pEpRatingUndefined; pEpRating rating = pEpRating.pEpRatingUndefined;
List<PEPIdentity> forceUnencryptedList = new List<PEPIdentity>();
PEPIdentity[] recipients; PEPIdentity[] recipients;
PEPMessage workingMessage; PEPMessage workingMessage;
......

namespace pEp
{
[System.ComponentModel.ToolboxItemAttribute(false)]
partial class FormRegionDPE : Microsoft.Office.Tools.Outlook.FormRegionBase
{
public FormRegionDPE(Microsoft.Office.Interop.Outlook.FormRegion formRegion)
: base(Globals.Factory, formRegion)
{
this.InitializeComponent();
}
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Form Region Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private static void InitializeManifest(Microsoft.Office.Tools.Outlook.FormRegionManifest manifest, Microsoft.Office.Tools.Outlook.Factory factory)
{
manifest.FormRegionName = "FormRegionDPE";
manifest.FormRegionType = Microsoft.Office.Tools.Outlook.FormRegionType.Replacement;
manifest.Title = "FormRegionDPE";
}
#endregion
#region Component Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.ElementHostFormControl = new System.Windows.Forms.Integration.ElementHost();
this.FormControlPatchView = new pEp.UI.Views.FormControlPatchView();
this.SuspendLayout();
//
// ElementHostFormControl
//
this.ElementHostFormControl.Dock = System.Windows.Forms.DockStyle.Fill;
this.ElementHostFormControl.Location = new System.Drawing.Point(0, 0);
this.ElementHostFormControl.Name = "ElementHostFormControl";
this.ElementHostFormControl.Size = new System.Drawing.Size(500, 350);
this.ElementHostFormControl.TabIndex = 0;
this.ElementHostFormControl.Child = this.FormControlPatchView;
//
// PatchView
//
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.Controls.Add(this.ElementHostFormControl);
this.Name = "PatchView";
this.Size = new System.Drawing.Size(500, 350);
this.FormRegionShowing += new System.EventHandler(this.FormRegionDPE_FormRegionShowing);
this.FormRegionClosed += new System.EventHandler(this.FormRegionDPE_FormRegionClosed);
this.ResumeLayout(false);
}
#endregion
private System.Windows.Forms.Integration.ElementHost ElementHostFormControl;
private UI.Views.FormControlPatchView FormControlPatchView;
public partial class FormRegionDPEFactory : Microsoft.Office.Tools.Outlook.IFormRegionFactory
{
public event Microsoft.Office.Tools.Outlook.FormRegionInitializingEventHandler FormRegionInitializing;
private Microsoft.Office.Tools.Outlook.FormRegionManifest _Manifest;
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
public FormRegionDPEFactory()
{
this._Manifest = Globals.Factory.CreateFormRegionManifest();
FormRegionDPE.InitializeManifest(this._Manifest, Globals.Factory);
this.FormRegionInitializing += new Microsoft.Office.Tools.Outlook.FormRegionInitializingEventHandler(this.FormRegionDPEFactory_FormRegionInitializing);
}
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
public Microsoft.Office.Tools.Outlook.FormRegionManifest Manifest
{
get
{
return this._Manifest;
}
}
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
Microsoft.Office.Tools.Outlook.IFormRegion Microsoft.Office.Tools.Outlook.IFormRegionFactory.CreateFormRegion(Microsoft.Office.Interop.Outlook.FormRegion formRegion)
{
FormRegionDPE form = new FormRegionDPE(formRegion);
form.Factory = this;
return form;
}
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
byte[] Microsoft.Office.Tools.Outlook.IFormRegionFactory.GetFormRegionStorage(object outlookItem, Microsoft.Office.Interop.Outlook.OlFormRegionMode formRegionMode, Microsoft.Office.Interop.Outlook.OlFormRegionSize formRegionSize)
{
throw new System.NotSupportedException();
}
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
bool Microsoft.Office.Tools.Outlook.IFormRegionFactory.IsDisplayedForItem(object outlookItem, Microsoft.Office.Interop.Outlook.OlFormRegionMode formRegionMode, Microsoft.Office.Interop.Outlook.OlFormRegionSize formRegionSize)
{
if (this.FormRegionInitializing != null)
{
Microsoft.Office.Tools.Outlook.FormRegionInitializingEventArgs cancelArgs = Globals.Factory.CreateFormRegionInitializingEventArgs(outlookItem, formRegionMode, formRegionSize, false);
this.FormRegionInitializing(this, cancelArgs);
return !cancelArgs.Cancel;
}
else
{
return true;
}
}
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
Microsoft.Office.Tools.Outlook.FormRegionKindConstants Microsoft.Office.Tools.Outlook.IFormRegionFactory.Kind
{
get
{
return Microsoft.Office.Tools.Outlook.FormRegionKindConstants.WindowsForms;
}
}
}
}
partial class WindowFormRegionCollection
{
internal FormRegionDPE FormRegionDPE
{
get
{
foreach (var item in this)
{
if (item.GetType() == typeof(FormRegionDPE))
return (FormRegionDPE)item;
}
return null;
}
}
}
}
using pEp.DPE;
using pEp.UI.ViewModels;
using System;
using System.ComponentModel;
using Outlook = Microsoft.Office.Interop.Outlook;
namespace pEp
{
internal partial class FormRegionDPE
{
#region Form Region Factory
[Microsoft.Office.Tools.Outlook.FormRegionMessageClass(DistributedPolicyEngine.DPE_MESSAGE_CLASS)]
[Microsoft.Office.Tools.Outlook.FormRegionName("pEpForOutlook.FormRegionDPE")]
public partial class FormRegionDPEFactory
{
// Occurs before the form region is initialized.
// To prevent the form region from appearing, set e.Cancel to true.
// Use e.OutlookItem to get a reference to the current Outlook item.
private void FormRegionDPEFactory_FormRegionInitializing(object sender, Microsoft.Office.Tools.Outlook.FormRegionInitializingEventArgs e)
{
}
}
#endregion
// Occurs before the form region is displayed.
// Use this.OutlookItem to get a reference to the current Outlook item.
// Use this.OutlookFormRegion to get a reference to the form region.
private void FormRegionDPE_FormRegionShowing(object sender, EventArgs e)
{
Outlook.MailItem omi = null;
try
{
omi = this.OutlookItem as Outlook.MailItem;
string xml = omi.HTMLBody;
Patch patch = Patch.Deserialize(xml);
FormControlPatchViewModel.PatchStatus patchStatus = FormControlPatchViewModel.PatchStatus.Open;
DateTime? editDate = null;
// Get patch status if available
if ((omi.GetUserProperty(MailItemExtensions.USER_PROPERTY_KEY_DPE_PATCH_STATUS, FormControlPatchViewModel.PatchStatus.Open) is string patchStatusString) &&
Enum.TryParse(patchStatusString, out FormControlPatchViewModel.PatchStatus status))
{
patchStatus = status;
}
// Get last edit date if available
if ((omi.GetUserProperty(MailItemExtensions.USER_PROPERTY_KEY_DPE_PATCH_EDIT_DATE) is string editDateString) &&
DateTime.TryParse(editDateString, out DateTime savedEditDate))
{
editDate = savedEditDate;
}
// Get the patch submitter
if (PEPIdentity.GetFromIdentity(omi, out PEPIdentity submitter) == Globals.ReturnStatus.Success)
{
this.FormControlPatchView.DataContext = new FormControlPatchViewModel(patch, submitter, patchStatus, editDate);
// Subscribe to property changed event
if (this.FormControlPatchView.DataContext is FormControlPatchViewModel formControlPatchViewModel)
{
formControlPatchViewModel.PropertyChanged += this.FormRegionDPE_PropertyChanged;
}
}
else
{
throw new Exception("FormRegionDPE_FormRegionShowing: Error getting patch submitter.");
}
}
catch (Exception ex)
{
Log.Error("FormRegionDPE_FormRegionShowing: Error getting Outlook item. " + ex);
}
finally
{
omi = null;
}
}
/// <summary>
/// Event handler for when a property in the associated view model changes.
/// </summary>
private void FormRegionDPE_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
// Set patch status and edit date as user properties if being changed
if ((e.PropertyName == nameof(FormControlPatchViewModel.Status)) ||
(e.PropertyName == nameof(FormControlPatchViewModel.EditDate)))
{
Outlook.MailItem omi = null;
try
{
omi = this.OutlookItem as Outlook.MailItem;
FormControlPatchViewModel.PatchStatus patchStatus = (sender as FormControlPatchViewModel).Status;
DateTime? editDate = (sender as FormControlPatchViewModel).EditDate;
if (patchStatus != FormControlPatchViewModel.PatchStatus.Open)
{
omi.SetUserProperty(MailItemExtensions.USER_PROPERTY_KEY_DPE_PATCH_STATUS, Enum.GetName(typeof(FormControlPatchViewModel.PatchStatus), patchStatus));
}
if (editDate != null)
{
omi.SetUserProperty(MailItemExtensions.USER_PROPERTY_KEY_DPE_PATCH_EDIT_DATE, editDate?.ToString("f"));
}
omi.Save();
}
catch (Exception ex)
{
Log.Error("FormRegionDPE_PropertyChanged: Error setting user property. " + ex);
}
finally
{
omi = null;
}
}
}
// Occurs when the form region is closed.
// Use this.OutlookItem to get a reference to the current Outlook item.
// Use this.OutlookFormRegion to get a reference to the form region.
private void FormRegionDPE_FormRegionClosed(object sender, EventArgs e)
{
// Unsubscribe from property changed event
if (this.FormControlPatchView.DataContext is FormControlPatchViewModel formControlPatchViewModel)
{
formControlPatchViewModel.PropertyChanged -= this.FormRegionDPE_PropertyChanged;
}
}
}
}
namespace pEp.UI.Models
{
public class ConfigFile
{
public string FileName { get; }
public string TempFileName { get; }
public string Diff { get; }
public ConfigFile(string fileName, string tempFileName, string diff)
{
this.FileName = fileName;
this.TempFileName = tempFileName;
this.Diff = diff;
}
}
}
...@@ -62,11 +62,11 @@ namespace pEp.UI.Models ...@@ -62,11 +62,11 @@ namespace pEp.UI.Models
{ {
// Get group manager // Get group manager
pEpIdentity groupManager = (pEpIdentity)AdapterExtensions.ExecuteWithPassphraseCheck(() => ThisAddIn.PEPEngine.GroupQueryManager(groupIdentity)); pEpIdentity groupManager = (pEpIdentity)AdapterExtensions.ExecuteWithPassphraseCheck(() => ThisAddIn.PEPEngine.GroupQueryManager(groupIdentity));
Log.Verbose("GetManagedGroupe: Retrieved group manager " + groupManager.Address); Log.Verbose("GetManagedGroups: Retrieved group manager " + groupManager.Address);
// Get group members // Get group members
pEpIdentity[] members = (pEpIdentity[])AdapterExtensions.ExecuteWithPassphraseCheck(() => ThisAddIn.PEPEngine.GroupQueryMembers(groupIdentity)); pEpIdentity[] members = (pEpIdentity[])AdapterExtensions.ExecuteWithPassphraseCheck(() => ThisAddIn.PEPEngine.GroupQueryMembers(groupIdentity));
Log.Verbose("GetManagedGroupe: Retrieved group {0} group members", members.Length); Log.Verbose("GetManagedGroups: Retrieved group {0} group members", members.Length);
// Add group // Add group
messageGroups.Add(new MessageGroup(groupIdentity, groupManager, members)); messageGroups.Add(new MessageGroup(groupIdentity, groupManager, members));
......
...@@ -97,7 +97,6 @@ namespace pEp.UI.Models ...@@ -97,7 +97,6 @@ namespace pEp.UI.Models
{ {
if (dialogResult == System.Windows.Forms.DialogResult.OK) if (dialogResult == System.Windows.Forms.DialogResult.OK)
{ {
patchDialog.Patch.CreationDate = DateTime.UtcNow;
Globals.ThisAddIn.DistributedPolicyEngine.Suggest(patchDialog.Patch, me); Globals.ThisAddIn.DistributedPolicyEngine.Suggest(patchDialog.Patch, me);
} }
} }
......
using pEp.DPE;
using System;
using System.Windows.Documents;
namespace pEp.UI.ViewModels
{
internal class FormControlPatchViewModel : ViewModelBase
{
public enum PatchStatus
{
Open,
Accepted,
Supported,
Rejected
}
#region Fields
private readonly Patch patch;
private PatchStatus _Status = PatchStatus.Open;
private DateTime? _EditDate = null;
private string _Explanation = null;
#endregion
#region Properties
/// <summary>
/// The commit message of the patch.
/// </summary>
public string CommitMessage
{
get => this.patch?.CommitMessage;
set
{
this.patch.CommitMessage = value;
this.OnPropertyChanged();
}
}
/// <summary>
/// Gets the creation date as string.
/// </summary>
public string CreationDateString => this.patch?.CreationDate.ToString("F");
/// <summary>
/// The diff of this patch.
/// </summary>
public string Diff
{
get => this.patch?.Diff;
set
{
this.patch.Diff = value;
this.OnPropertyChanged();
}
}
/// <summary>
/// Gets the diff of this patch as formatted flow document.
/// </summary>
public FlowDocument DisplayDiff => PatchDialogViewModel.FormatDiff(this.Diff);
/// <summary>
/// Gets or sets the last date the patch was edited.
/// </summary>
public DateTime? EditDate { get => this._EditDate; set => this.SetProperty(ref this._EditDate, value); }
/// <summary>
/// The explanation shown in the UI regarding this patch.
/// </summary>
public string Explanation { get => this._Explanation; set => this.SetProperty(ref this._Explanation, value); }
/// <summary>
/// Gets whether the OK button is visible.
/// </summary>
public bool IsOKButtonVisible { get; } = true;
/// <summary>
/// Gets whether the Reject button is visible.
/// </summary>
public bool IsRejectButtonVisible { get; } = false;
/// <summary>
/// The command to accept the patch dialog.
/// </summary>
public RelayCommand OKButtonCommand => new RelayCommand(this.SupportPatch, p => (this.Status == PatchStatus.Open));
/// <summary>
/// Gets the OK button text.
/// </summary>
public string OKButtonText { get; } = Properties.Resources.Options_OKText;
/// <summary>
/// The command to reject the dialog.
/// </summary>
public RelayCommand RejectButtonCommand => new RelayCommand(this.RejectPatch, p => (this.Status == PatchStatus.Open));
/// <summary>
/// Gets the Reject button text.
/// </summary>
public string RejectButtonText { get; } = Properties.Resources.SyncWizard_RejectButton;
/// <summary>
/// Gets the submitter of this patch.
/// </summary>
public PEPIdentity Submitter { get; }
/// <summary>
/// Gets or sets the status of this patch.
/// </summary>
public PatchStatus Status { get => this._Status; set => this.SetProperty(ref this._Status, value); }
/// <summary>
/// Gets or sets the tag of the patch.
/// </summary>
public string Tag
{
get => this.patch?.Tag;
set
{
this.patch.Tag = value;
this.OnPropertyChanged();
}
}
/// <summary>
/// Gets or sets the URI of the patch.
/// </summary>
public string Uri
{
get => this.patch?.Uri;
set
{
this.patch.Uri = value;
this.OnPropertyChanged();
}
}
#endregion
#region Constructors
/// <summary>
/// Primary constructor.
/// </summary>
/// <param name="patch">The patch to create the form region with.</param>
/// <param name="submitter">The submitter of the patch.</param>
/// <param name="status">The status of the patch.</param>
/// <param name="editDate">The last time this patch was edited (accepted/supported/rejected).</param>
public FormControlPatchViewModel(Patch patch, PEPIdentity submitter, PatchStatus status = PatchStatus.Open, DateTime? editDate = null)
{
this.EditDate = editDate;
this.patch = patch;
this.Submitter = submitter;
this.Status = status;
this.IsRejectButtonVisible = true;
this.OKButtonText = "Support";
this.SetExplanation();
}
#endregion
#region Methods
/// <summary>
/// Rejects this patch.
/// </summary>
/// <param name="parameter">The command parameter.</param>
private void RejectPatch(object parameter)
{
Globals.ThisAddIn.DistributedPolicyEngine.Reject(this.patch, new PEPIdentity());
this.Status = PatchStatus.Rejected;
this.EditDate = DateTime.UtcNow;
this.SetExplanation();
}
/// <summary>
/// Sets the explanation text.
/// </summary>
private void SetExplanation()
{
string editDate = "<n/a>";
if (this.EditDate != null)
{
editDate = ((DateTime)this.EditDate).ToLocalTime().ToString("F");
}
switch (this.Status)
{
case PatchStatus.Accepted:
this.Explanation = $"Accepted on { editDate }";
break;
case PatchStatus.Supported:
this.Explanation = $"Supported on { editDate }";
break;
case PatchStatus.Rejected:
this.Explanation = $"Rejected on { editDate }";
break;
case PatchStatus.Open:
default:
this.Explanation = "New configuration changes pending approval";
break;
}
}
/// <summary>
/// Supports this patch.
/// </summary>
/// <param name="parameter">The command parameter.</param>
private void SupportPatch(object parameter)
{
Globals.ThisAddIn.DistributedPolicyEngine.Support(this.patch, new PEPIdentity());
this.Status = PatchStatus.Supported;
this.EditDate = DateTime.UtcNow;
this.SetExplanation();
}
#endregion
}
}
...@@ -31,7 +31,7 @@ namespace pEp.UI.ViewModels ...@@ -31,7 +31,7 @@ namespace pEp.UI.ViewModels
MessageGroups = 5 MessageGroups = 5
} }
private PEPSettings pEpSettings; private readonly PEPSettings pEpSettings;
private int _AccountSettingsListSelectedIndex = -1; private int _AccountSettingsListSelectedIndex = -1;
private RelayCommand _CommandButtonAddGroup = null; private RelayCommand _CommandButtonAddGroup = null;
...@@ -45,9 +45,6 @@ namespace pEp.UI.ViewModels ...@@ -45,9 +45,6 @@ namespace pEp.UI.ViewModels
private RelayCommand _CommandButtonRefreshLogs = null; private RelayCommand _CommandButtonRefreshLogs = null;
private RelayCommand _CommandButtonResetPEPStore = null; private RelayCommand _CommandButtonResetPEPStore = null;
private RelayCommand _CommandButtonResetAllOwnKeys = null; private RelayCommand _CommandButtonResetAllOwnKeys = null;
private RelayCommand<IList<object>> _CommandOpenPatch = null;
private RelayCommand<IList<object>> _CommandRejectPatch = null;
private RelayCommand<IList<object>> _CommandSupportPatch = null;
private bool _IsAdvancedEnabled = false; private bool _IsAdvancedEnabled = false;
private string _LogEngine = null; private string _LogEngine = null;
private string _LogOutlook = null; private string _LogOutlook = null;
...@@ -78,12 +75,6 @@ namespace pEp.UI.ViewModels ...@@ -78,12 +75,6 @@ namespace pEp.UI.ViewModels
this.AccountSettingsList.Add(new AccountViewModel(accountSettings, this.CalculateDependentProperties)); this.AccountSettingsList.Add(new AccountViewModel(accountSettings, this.CalculateDependentProperties));
}); });
// Add patches
DistributedPolicyEngine.GetOpenPatches()?.ForEach((tuple) =>
{
this.Patches.Add(new PatchViewModel(tuple.Item1, tuple.Item2));
});
// Calculate dependent properties // Calculate dependent properties
this.calcDepPropIsEnabled = true; this.calcDepPropIsEnabled = true;
this.CalculateDependentProperties(); this.CalculateDependentProperties();
...@@ -305,55 +296,6 @@ namespace pEp.UI.ViewModels ...@@ -305,55 +296,6 @@ namespace pEp.UI.ViewModels
} }
} }
/// <summary>
/// Gets the command to open a patch.
/// </summary>
public RelayCommand<IList<object>> CommandOpenPatch
{
get
{
if (this._CommandOpenPatch == null)
{
this._CommandOpenPatch = new RelayCommand<IList<object>>(p => (p?.First() as PatchViewModel)?.OpenPatchDialog(PatchDialog.PatchAction.SupportOrRejectPatch));
}
return this._CommandOpenPatch;
}
}
/// <summary>
/// Gets the command to reject a patch.
/// </summary>
public RelayCommand<IList<object>> CommandRejectPatch
{
get
{
if (this._CommandRejectPatch == null)
{
this._CommandRejectPatch = new RelayCommand<IList<object>>(p => this.RejectPatch(p?.First() as PatchViewModel));
}
return this._CommandRejectPatch;
}
}
/// <summary>
/// Gets the command to support a patch.
/// </summary>
public RelayCommand<IList<object>> CommandSupportPatch
{
get
{
if (this._CommandSupportPatch == null)
{
this._CommandSupportPatch = new RelayCommand<IList<object>>(p => this.SupportPatch(p?.First() as PatchViewModel));
}
return this._CommandSupportPatch;
}
}
/// <summary> /// <summary>
/// Gets the list of projects that are used in pEp for Outlook and that /// Gets the list of projects that are used in pEp for Outlook and that
/// are being credited. /// are being credited.
...@@ -1113,37 +1055,6 @@ namespace pEp.UI.ViewModels ...@@ -1113,37 +1055,6 @@ namespace pEp.UI.ViewModels
} while (retry); } while (retry);
} }
/// <summary>
/// Opens the Patch dialog.
/// </summary>
/// <param name="patchViewModel">The patch view model that </param>
/// <param name="patchAction">The patch action to execute.</param>
/// <returns>The dialog result.</returns>
public System.Windows.Forms.DialogResult OpenPatchDialog(PatchViewModel patchViewModel, PatchDialog.PatchAction patchAction)
{
if (patchViewModel == null)
{
Log.ErrorAndFailInDebugMode("OpenPatchDialog: patch view model is null.");
}
System.Windows.Forms.DialogResult dialogResult = patchViewModel?.OpenPatchDialog(patchAction) ?? System.Windows.Forms.DialogResult.Cancel;
if (dialogResult != System.Windows.Forms.DialogResult.Cancel)
{
this.Patches.Clear();
// Add patches
DistributedPolicyEngine.GetOpenPatches()?.ForEach((tuple) =>
{
this.Patches.Add(new PatchViewModel(tuple.Item1, tuple.Item2));
});
this.OnPropertyChanged(nameof(this.Patches));
}
return dialogResult;
}
/// <summary> /// <summary>
/// Resets the logs. /// Resets the logs.
/// </summary> /// </summary>
...@@ -1179,43 +1090,6 @@ namespace pEp.UI.ViewModels ...@@ -1179,43 +1090,6 @@ namespace pEp.UI.ViewModels
} }
} }
/// <summary>
/// Removes the given patch from the list of patches.
/// </summary>
/// <param name="patchViewModel">The patch view model to remove.</param>
private void RemovePatch(PatchViewModel patchViewModel)
{
if (patchViewModel == null)
{
Log.ErrorAndFailInDebugMode("RemovePatch: patch view model is null.");
}
if (!this.Patches.Remove(patchViewModel))
{
Log.Error("RemovePatch: Could not remove patch.");
}
}
/// <summary>
/// Rejects the patch.
/// </summary>
/// <param name="patchViewModel">The patch's view model.</param>
private void RejectPatch(PatchViewModel patchViewModel)
{
patchViewModel?.RejectPatch();
this.RemovePatch(patchViewModel);
}
/// <summary>
/// Supports the patch.
/// </summary>
/// <param name="patchViewModel">The patch's view model.</param>
private void SupportPatch(PatchViewModel patchViewModel)
{
patchViewModel?.SupportPatch();
this.RemovePatch(patchViewModel);
}
#endregion #endregion
} }
} }
using Microsoft.Win32; using DiffPlex.DiffBuilder;
using DiffPlex.DiffBuilder.Model;
using Microsoft.Win32;
using pEp.DPE;
using pEp.UI.Models; using pEp.UI.Models;
using System; using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents; using System.Windows.Documents;
using System.Windows.Media; using System.Windows.Media;
...@@ -11,17 +20,36 @@ namespace pEp.UI.ViewModels ...@@ -11,17 +20,36 @@ namespace pEp.UI.ViewModels
{ {
#region Fields #region Fields
private FlowDocument _DisplayDiff = null; private RelayCommand _AddOrEditFileCommand = null;
private bool _IsCommitMessageValid = false; private bool _IsCommitMessageValid = false;
private bool _IsDiffValid = false; private bool _IsDiffValid = false;
private bool _IsUriValid = false; private bool _IsValid = false;
private bool _IsValid = false; private ConfigFile _SelectedFile = null;
private RelayCommand _LoadFromFileCommand = null; private FlowDocument _VisibleDiff = null;
private const string FILE_A_PREFIX = "---a";
private const string FILE_B_PREFIX = "+++b";
#endregion #endregion
#region Properties #region Properties
/// <summary>
/// Gets the command to load the diff from file.
/// </summary>
public RelayCommand AddOrEditFileCommand
{
get
{
if (this._AddOrEditFileCommand == null)
{
this._AddOrEditFileCommand = new RelayCommand(this.AddOrEditFile);
}
return this._AddOrEditFileCommand;
}
}
/// <summary> /// <summary>
/// Command to cancel the dialog. /// Command to cancel the dialog.
/// </summary> /// </summary>
...@@ -37,7 +65,7 @@ namespace pEp.UI.ViewModels ...@@ -37,7 +65,7 @@ namespace pEp.UI.ViewModels
/// </summary> /// </summary>
public string CommitMessage public string CommitMessage
{ {
get => this.Dialog.Patch.CommitMessage; get => this.Dialog.Patch?.CommitMessage;
set set
{ {
this.Dialog.Patch.CommitMessage = value; this.Dialog.Patch.CommitMessage = value;
...@@ -51,25 +79,6 @@ namespace pEp.UI.ViewModels ...@@ -51,25 +79,6 @@ namespace pEp.UI.ViewModels
/// </summary> /// </summary>
public new PatchDialog Dialog => base.Dialog as PatchDialog; public new PatchDialog Dialog => base.Dialog as PatchDialog;
/// <summary>
/// The diff of this patch.
/// </summary>
public string Diff
{
get => this.Dialog.Patch.Diff;
set
{
this.Dialog.Patch.Diff = value;
this.ValidatePatch();
this.OnPropertyChanged();
}
}
/// <summary>
/// Gets the diff of this patch as formatted flow document.
/// </summary>
public FlowDocument DisplayDiff { get => this._DisplayDiff; set => SetProperty(ref this._DisplayDiff, value); }
/// <summary> /// <summary>
/// The explanation shown in the UI regarding this patch. /// The explanation shown in the UI regarding this patch.
/// </summary> /// </summary>
...@@ -127,22 +136,6 @@ namespace pEp.UI.ViewModels ...@@ -127,22 +136,6 @@ namespace pEp.UI.ViewModels
/// </summary> /// </summary>
public bool IsRejectButtonVisible { get; } = false; public bool IsRejectButtonVisible { get; } = false;
/// <summary>
/// Gets or sets whether the URI is valid.
/// </summary>
public bool IsUriValid
{
get => this._IsUriValid;
set
{
if (value != this._IsUriValid)
{
this._IsUriValid = value;
this.OnPropertyChanged();
}
}
}
/// <summary> /// <summary>
/// Gets or sets whether this patch is valid. /// Gets or sets whether this patch is valid.
/// </summary> /// </summary>
...@@ -160,25 +153,14 @@ namespace pEp.UI.ViewModels ...@@ -160,25 +153,14 @@ namespace pEp.UI.ViewModels
} }
/// <summary> /// <summary>
/// Gets the command to load the diff from file. /// Gets the collection of config files that are being modified.
/// </summary> /// </summary>
public RelayCommand LoadFromFileCommand public ObservableCollection<ConfigFile> ConfigFiles { get; } = new ObservableCollection<ConfigFile>();
{
get
{
if (this._LoadFromFileCommand == null)
{
this._LoadFromFileCommand = new RelayCommand(LoadFromFile);
}
return this._LoadFromFileCommand;
}
}
/// <summary> /// <summary>
/// The command to accept the patch dialog. /// The command to accept the patch dialog.
/// </summary> /// </summary>
public RelayCommand OKButtonCommand => new RelayCommand(p => this.Close(true), p => this.IsValid); public RelayCommand OKButtonCommand => new RelayCommand(this.CreateAndSuggestPatch, p => this.IsValid);
/// <summary> /// <summary>
/// Gets the OK button text. /// Gets the OK button text.
...@@ -186,21 +168,29 @@ namespace pEp.UI.ViewModels ...@@ -186,21 +168,29 @@ namespace pEp.UI.ViewModels
public string OKButtonText { get; } = Properties.Resources.Options_OKText; public string OKButtonText { get; } = Properties.Resources.Options_OKText;
/// <summary> /// <summary>
/// The command to reject the dialog. /// Gets the Remove files button command
/// </summary> /// </summary>
public RelayCommand RejectButtonCommand => new RelayCommand(p => this.Close(false)); public RelayCommand RemoveButtonCommand => new RelayCommand(this.RemoveFile);
/// <summary> /// <summary>
/// Gets the Reject button text. /// Gets the currently selected file.
/// </summary> /// </summary>
public string RejectButtonText { get; } = Properties.Resources.SyncWizard_RejectButton; public ConfigFile SelectedFile
{
get => this._SelectedFile;
set
{
this._SelectedFile = value;
this.UpdateVisibleDiff();
}
}
/// <summary> /// <summary>
/// Gets or sets the tag of the patch. /// Gets or sets the tag of the patch.
/// </summary> /// </summary>
public string Tag public string Tag
{ {
get => this.Dialog.Patch.Tag; get => this.Dialog.Patch?.Tag;
set set
{ {
this.Dialog.Patch.Tag = value; this.Dialog.Patch.Tag = value;
...@@ -209,18 +199,9 @@ namespace pEp.UI.ViewModels ...@@ -209,18 +199,9 @@ namespace pEp.UI.ViewModels
} }
/// <summary> /// <summary>
/// Gets or sets the URI of the patch. /// Gets the diff of the selected file as formatted flow document.
/// </summary> /// </summary>
public string Uri public FlowDocument VisibleDiff { get => this._VisibleDiff; set => this.SetProperty(ref this._VisibleDiff, value); }
{
get => this.Dialog.Patch.Uri;
set
{
this.Dialog.Patch.Uri = value;
this.ValidatePatch();
this.OnPropertyChanged();
}
}
#endregion #endregion
...@@ -235,19 +216,11 @@ namespace pEp.UI.ViewModels ...@@ -235,19 +216,11 @@ namespace pEp.UI.ViewModels
{ {
switch (dialog.PatchActionType) switch (dialog.PatchActionType)
{ {
case PatchDialog.PatchAction.EditPatch:
break;
case PatchDialog.PatchAction.NewPatch: case PatchDialog.PatchAction.NewPatch:
{ {
this.Explanation = "New patch"; this.Explanation = "New patch";
} }
break; break;
case PatchDialog.PatchAction.ShowPatch:
{
this.Explanation = "Patch " + dialog.Patch.Id;
this.IsCancelButtonVisible = false;
}
break;
case PatchDialog.PatchAction.SupportOrRejectPatch: case PatchDialog.PatchAction.SupportOrRejectPatch:
{ {
this.Explanation = "Support or reject patch"; this.Explanation = "Support or reject patch";
...@@ -255,11 +228,12 @@ namespace pEp.UI.ViewModels ...@@ -255,11 +228,12 @@ namespace pEp.UI.ViewModels
this.OKButtonText = "Support"; this.OKButtonText = "Support";
} }
break; break;
case PatchDialog.PatchAction.EditPatch:
case PatchDialog.PatchAction.ShowPatch:
default: default:
break; break;
} }
this.DisplayDiff = this.FormatDiff();
this.ValidatePatch(); this.ValidatePatch();
} }
...@@ -268,75 +242,349 @@ namespace pEp.UI.ViewModels ...@@ -268,75 +242,349 @@ namespace pEp.UI.ViewModels
#region Methods #region Methods
/// <summary> /// <summary>
/// Formats the diff displaying colors of changes. /// Adds a new file to the list of modified config files.
/// </summary> /// </summary>
/// <returns>The formatted diff or null if no diff is available.</returns> /// <param name="parameter">The command parameter.</param>
private FlowDocument FormatDiff() private void AddOrEditFile(object parameter)
{ {
if (string.IsNullOrEmpty(this.Diff)) string fileName = parameter as string;
if (string.IsNullOrEmpty(fileName))
{ {
return null; // Create the dialog to select the key file
OpenFileDialog openFileDialog = new OpenFileDialog
{
CheckFileExists = true,
CheckPathExists = true,
Filter = "All files|*.*",
Multiselect = false
};
// Import the key file and set the key as default
if (openFileDialog.ShowDialog() == true)
{
fileName = openFileDialog.FileName;
}
} }
FlowDocument document = new FlowDocument if (!string.IsNullOrEmpty(fileName))
{ {
FontFamily = new FontFamily("Courier New"), this.EditFileAndCreateDiff(fileName);
FontSize = 12.0, }
Background = Brushes.White,
};
string[] lines = this.Diff?.Replace("\r\n", "\n")?.Split('\n') ?? new string[] { }; this.ValidatePatch();
foreach (string line in lines) }
/// <summary>
/// Closes this window.
/// </summary>
/// <param name="dialogResult">The dialog result.</param>
private new void Close(bool? dialogResult)
{
// Delete temporary directory and close
this.DeleteTempDirectory();
base.Close(dialogResult);
}
/// <summary>
/// Creates a patch from the view model and suggests it.
/// </summary>
/// <param name="parameter">The command parameter.</param>
private void CreateAndSuggestPatch(object parameter)
{
// The patch was created right now
this.Dialog.Patch.CreationDate = DateTime.UtcNow;
// Get a common root URI of all diff files
this.Dialog.Patch.Uri = this.GetRootUri();
// Concatenate all diffs from the selected files
this.Dialog.Patch.Diff = this.UnifyDiffs();
// Close with result True.
this.Close(true);
}
/// <summary>
/// Deletes the temporary directory.
/// </summary>
private void DeleteTempDirectory()
{
try
{ {
Paragraph paragraph = new Paragraph(new Run(line)) if (Directory.Exists(DistributedPolicyEngine.DPE_TEMP_LOCATION))
{ {
Margin = new System.Windows.Thickness(1), Directory.Delete(DistributedPolicyEngine.DPE_TEMP_LOCATION, true);
LineHeight = 14.0 }
}; }
catch (Exception ex)
{
Log.Error("DeleteTempDirectory: Error deleting temporary directory. " + ex);
}
}
if (line.StartsWith("-")) /// <summary>
/// Creates a diff from two files.
/// </summary>
/// <param name="fileNameA">The old file.</param>
/// <param name="fileNameB">The new file.</param>
/// <returns>The diff between the two files.</returns>
private string CreateDiff(string fileNameA, string fileNameB)
{
if (!(File.Exists(fileNameA) && File.Exists(fileNameB)))
{
Log.ErrorAndFailInDebugMode("CreateDiff: Input file doesn't exist.");
return null;
}
string fileA, fileB;
try
{
fileA = File.ReadAllText(fileNameA);
fileB = File.ReadAllText(fileNameB);
}
catch (Exception ex)
{
Log.Error("CreateDiff: Error reading input file. " + ex);
return null;
}
/* Create a diff string with sections of the following format:
*
* --- a/path/to/fileA
* +++ b/path/to/fileB
* @@ -1,7 +1,7 @@
* string a = "A";
* string b = "B";
* string c = "C"
* -string d = "D";
* +string e = "E";
*
* if (string.IsNullOrEmpty(a))
* {
*
* => Line number and line count of affected lines in file A.
* => Line number and line count of affected lines in file B.
* => Three unchanged lines (if available) before the modified part.
* => The modified part.
* => Three unchanged lines (if available) after the modified part.
*/
string diff = null;
SideBySideDiffModel sideBySideDiffModel = SideBySideDiffBuilder.Diff(fileA, fileB);
List<DiffPiece> newLines = sideBySideDiffModel.NewText.Lines;
List<DiffPiece> oldLines = sideBySideDiffModel.OldText.Lines;
List<string> block = new List<string>();
bool newBlock = true;
int oldFileIndex = 0, newFileIndex = 0;
for (int i = 0; i < newLines.Count; i++)
{
// Add lines as necessary
if (newLines[i].Type == ChangeType.Imaginary)
{ {
paragraph.Background = Globals.ResourceDict["BrushRedBackground"] as Brush; block.Add("-" + oldLines[i].Text);
} }
else if (line.StartsWith("+")) else if (newLines[i].Type == ChangeType.Inserted)
{ {
paragraph.Background = Globals.ResourceDict["BrushGreenBackground"] as Brush; block.Add("+" + newLines[i].Text);
} }
else if (line.StartsWith("@@")) else if (newLines[i].Type == ChangeType.Modified)
{ {
paragraph.Background = Brushes.LightGray; block.Add("-" + oldLines[i].Text);
block.Add("+" + newLines[i].Text);
} }
document.Blocks.Add(paragraph); // If a block is being constructed, add additional parts as necessary
if (block.Count > 0)
{
// First, add the three preceding unchanged lines (if available)
if (newBlock)
{
newBlock = false;
int index = i;
for (int k = 0; k < 3; k++)
{
if (index-- > 0)
{
block.Insert(0, " " + newLines[index].Text);
}
else
{
break;
}
}
index++;
newFileIndex = newLines[index].Position ?? 1;
oldFileIndex = oldLines[index].Position ?? 1;
}
// Then, add the three following lines (if available)
int counter = 0;
while ((++i < newLines.Count) &&
(newLines[i].Type == ChangeType.Unchanged))
{
if (counter++ < 3)
{
block.Add(" " + newLines[i].Text);
}
else
{
break;
}
}
// Close block
if ((counter > 3) ||
(i >= newLines.Count))
{
// Once a block is complete, add descriptor at the beginning and add lines to diff
int oldFileLineCount = block.Count(s => !s.StartsWith("+"));
int newFileLineCount = block.Count(s => !s.StartsWith("-"));
diff += $"@@ -{ oldFileIndex },{ oldFileLineCount } +{ newFileIndex },{ newFileLineCount } @@\n";
foreach (string line in block)
{
diff += line + '\n';
}
block = new List<string>();
newBlock = true;
}
i--;
}
} }
return document; if (string.IsNullOrEmpty(diff))
{
return null;
}
return diff.Insert(0, $"{ PatchDialogViewModel.FILE_A_PREFIX }/{ fileNameB }\n{ PatchDialogViewModel.FILE_B_PREFIX }/{ fileNameB }\n").TrimEnd('\n');
} }
private void LoadFromFile(object parameter) /// <summary>
/// Opens the given file to edit and creates a diff once the editor is closed.
/// </summary>
/// <param name="fileName">The file name to edit and create a diff for.</param>
private void EditFileAndCreateDiff(string fileName)
{ {
// Create the dialog to select the key file try
OpenFileDialog openFileDialog = new OpenFileDialog
{ {
CheckFileExists = true, // Get the temp file that will actually be edited
CheckPathExists = true, string tempFileName, originalFileName;
Filter = "Diff files|*.diff;|Patch files|*.patch;|All files|*.*", if ((this.ConfigFiles.FirstOrDefault(cf => cf.FileName.Equals(fileName)) is ConfigFile configFile) &&
Multiselect = false (string.IsNullOrEmpty(configFile.TempFileName) == false))
}; {
tempFileName = configFile.TempFileName;
originalFileName = tempFileName + ".orig";
File.Copy(tempFileName, fileName, true);
}
else
{
tempFileName = Path.Combine(DistributedPolicyEngine.DPE_TEMP_LOCATION, Path.GetFileName(fileName));
originalFileName = tempFileName + ".orig";
Directory.CreateDirectory(DistributedPolicyEngine.DPE_TEMP_LOCATION);
File.Copy(fileName, tempFileName, true);
File.Copy(fileName, originalFileName, true);
}
// Import the key file and set the key as default // Open temp file in Notepad for the user to edit
if (openFileDialog.ShowDialog() == true) using (Process modifyFileProcess = Process.Start(new ProcessStartInfo
{
try
{ {
string fileContent = File.ReadAllText(openFileDialog.FileName); FileName = "Notepad.exe",
this.Diff = fileContent; Arguments = fileName,
UseShellExecute = false,
CreateNoWindow = true
}))
{
modifyFileProcess.WaitForExit();
}
// If changes were made, add or update file, otherwise show notification
if (this.CreateDiff(originalFileName, fileName) is string diff)
{
this.ConfigFiles.Remove(this.ConfigFiles.Where(f => (f.FileName?.Equals(fileName) == true) && f.TempFileName?.Equals(tempFileName) == true).FirstOrDefault());
this.ConfigFiles.Add(new ConfigFile(fileName, tempFileName, diff));
// Select the created diff in the UI
this.SelectedFile = this.ConfigFiles.Last();
} }
catch (Exception ex) else
{ {
Log.Error($"LoadFromFile: Error reading file content from { openFileDialog.FileName }: { ex }"); MessageBox.Show("No changes were made. Ignoring selected file.");
} }
// Always update the respective files
File.Copy(fileName, tempFileName, true);
File.Copy(originalFileName, fileName, true);
}
catch (Exception ex)
{
Log.Error($"EditFileAndCreateDiff: Error reading file content from { fileName }: { ex }");
}
}
/// <summary>
/// Gets a common root URI from all selected files in the dialog.
/// </summary>
/// <returns>The root URI</returns>
private string GetRootUri()
{
string rootUri = this.ConfigFiles.First()?.FileName;
foreach (ConfigFile modifiedFile in this.ConfigFiles)
{
for (int i = 0; i < modifiedFile.FileName.Length; i++)
{
if ((rootUri.Length <= i) ||
(modifiedFile.FileName[i] == rootUri[i]))
{
continue;
}
rootUri = rootUri.Substring(0, i);
break;
}
}
return rootUri;
}
/// <summary>
/// Removes a file from the list of changed files
/// </summary>
/// <param name="parameter">The command parameter.</param>
private void RemoveFile(object parameter)
{
this.ConfigFiles.Remove(this.SelectedFile);
}
/// <summary>
/// Concatenates all diffs from the selected files into one string.
/// </summary>
/// <returns>The unified diff.</returns>
private string UnifyDiffs()
{
string diff = null;
// Concatenate diffs
foreach (ConfigFile modifiedFile in this.ConfigFiles)
{
diff += modifiedFile.Diff + "\n";
} }
// Remove root parts of URI
diff = diff.Replace(PatchDialogViewModel.FILE_A_PREFIX + "/" + this.Dialog.Patch.Uri, PatchDialogViewModel.FILE_A_PREFIX + "/");
diff = diff.Replace(PatchDialogViewModel.FILE_B_PREFIX + "/" + this.Dialog.Patch.Uri, PatchDialogViewModel.FILE_B_PREFIX + "/");
return diff.TrimEnd('\n');
}
/// <summary>
/// Updates the diff that is visible in the view pane.
/// </summary>
private void UpdateVisibleDiff()
{
this.VisibleDiff = PatchDialogViewModel.FormatDiff(this.SelectedFile?.Diff);
} }
/// <summary> /// <summary>
...@@ -345,12 +593,79 @@ namespace pEp.UI.ViewModels ...@@ -345,12 +593,79 @@ namespace pEp.UI.ViewModels
private void ValidatePatch() private void ValidatePatch()
{ {
this.IsCommitMessageValid = !string.IsNullOrEmpty(this.CommitMessage); this.IsCommitMessageValid = !string.IsNullOrEmpty(this.CommitMessage);
this.IsDiffValid = !string.IsNullOrEmpty(this.Diff); this.IsDiffValid = this.ConfigFiles?.Count > 0;
this.IsUriValid = !string.IsNullOrEmpty(this.Uri);
this.IsValid = this.IsCommitMessageValid && this.IsDiffValid && this.IsUriValid; this.IsValid = this.IsCommitMessageValid && this.IsDiffValid;
} }
#endregion #endregion
#region Static methods
/// <summary>
/// Formats the diff displaying colors of changes.
/// </summary>
/// <returns>The formatted diff or null if no diff is available.</returns>
public static FlowDocument FormatDiff(string diff)
{
if (string.IsNullOrEmpty(diff))
{
return null;
}
// Initialize the document with zero width to set the real width later
FlowDocument document = new FlowDocument
{
Background = Brushes.White,
PageWidth = 0
};
string[] lines = diff.Replace("\r\n", "\n")?.Split('\n') ?? new string[] { };
foreach (string line in lines)
{
Paragraph paragraph = new Paragraph()
{
Margin = new Thickness(1),
};
TextBlock textBlock = new TextBlock()
{
Text = line,
FontFamily = new FontFamily("Courier New"),
LineHeight = 14.0,
FontSize = 12.0,
TextWrapping = TextWrapping.NoWrap
};
// Set the document width to the biggest textblock width plus some margin
// to account for padding inside the scroll viewer
textBlock.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
if (textBlock.DesiredSize.Width > document.PageWidth)
{
document.PageWidth = textBlock.DesiredSize.Width + 50;
}
paragraph.Inlines.Add(textBlock);
// Color the added and removed lines accordingly
if (line.StartsWith("-"))
{
paragraph.Background = Globals.ResourceDict["BrushRedBackground"] as Brush;
}
else if (line.StartsWith("+"))
{
paragraph.Background = Globals.ResourceDict["BrushGreenBackground"] as Brush;
}
else if (line.StartsWith("@@"))
{
paragraph.Background = Brushes.LightGray;
}
document.Blocks.Add(paragraph);
}
return document;
}
#endregion
} }
} }
<UserControl x:Class="pEp.UI.Views.FormControlPatchView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:p="clr-namespace:pEp.Properties"
xmlns:ui="clr-namespace:pEp.UI"
xmlns:vm="clr-namespace:pEp.UI.ViewModels"
mc:Ignorable="d"
FontFamily="Segoe UI"
FontSize="12"
d:DataContext="{d:DesignInstance Type=vm:FormControlPatchViewModel}"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/pEp;component/Resources/Dictionary.xaml" />
</ResourceDictionary.MergedDictionaries>
<ui:InvertBoolConverter x:Key="InvertBool"/>
<BooleanToVisibilityConverter x:Key="BoolToVisibility" />
<ui:ValueConverterGroup x:Key="InvertBoolToVisibility">
<ui:InvertBoolConverter />
<BooleanToVisibilityConverter />
</ui:ValueConverterGroup>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Label Grid.Column="0"
Grid.Row="0"
Grid.ColumnSpan="2"
Content="{Binding Explanation}"
FontWeight="Bold"
Margin="10,5"/>
<Label Grid.Column="0"
Grid.Row="1"
Grid.ColumnSpan="2"
Content="{Binding Submitter.DisplayString}"
Margin="10,1" />
<Label Grid.Column="0"
Grid.Row="2"
Grid.ColumnSpan="2"
Content="{Binding CreationDateString}"
Margin="10,1" />
<Image Grid.Column="2"
Grid.Row="0"
Grid.RowSpan="3"
Width="100"
Height="41"
Source="pack://application:,,,/pEp;component/Resources/ImageLogoMedium.png"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Margin="5,5,10,5"/>
<Label Grid.Column="0"
Grid.Row="3"
Content="Diff"
Margin="10,10,5,10"/>
<FlowDocumentScrollViewer Grid.Column="1"
Grid.ColumnSpan="2"
Grid.Row="3"
HorizontalScrollBarVisibility="Auto"
BorderBrush="Black"
BorderThickness="1"
Margin="5,10,10,10"
Document="{Binding DisplayDiff}" />
<Label Grid.Column="0"
Grid.Row="4"
Content="Uri"
Margin="10,10,5,10"/>
<TextBox Grid.Column="1"
Grid.ColumnSpan="2"
Grid.Row="4"
VerticalAlignment="Center"
Padding="2"
Text="{Binding Uri}"
IsReadOnly="True"
MinWidth="400"
Margin="5,10,10,10"/>
<Label Grid.Column="0"
Grid.Row="5"
Content="Commit message"
Margin="10,10,5,10"/>
<TextBox Grid.Column="1"
Grid.ColumnSpan="2"
Grid.Row="5"
VerticalAlignment="Center"
Padding="2"
Text="{Binding CommitMessage}"
IsReadOnly="True"
MinWidth="400"
Margin="5,10,10,10"/>
<Label Grid.Column="0"
Grid.Row="6"
Content="Tag"
Margin="10,10,5,10"/>
<TextBox Grid.Column="1"
Grid.ColumnSpan="2"
Grid.Row="6"
VerticalAlignment="Center"
Padding="2"
Text="{Binding Tag}"
IsReadOnly="True"
MinWidth="400"
Margin="5,10,10,10"/>
<StackPanel Grid.Column="0"
Grid.ColumnSpan="3"
Grid.Row="7"
Orientation="Horizontal"
HorizontalAlignment="Center">
<Button Content="{Binding OKButtonText}"
Command="{Binding OKButtonCommand}"
IsDefault="True"
Visibility="{Binding IsOKButtonVisible, Converter={StaticResource BoolToVisibility}}"
Style="{StaticResource StyleButtonGray}"
Margin="10" />
<Button Content="{Binding RejectButtonText}"
Command="{Binding RejectButtonCommand}"
Visibility="{Binding IsRejectButtonVisible, Converter={StaticResource BoolToVisibility}}"
Style="{StaticResource StyleButtonGray}"
Margin="10" />
</StackPanel>
</Grid>
</UserControl>