1
0
mirror of https://github.com/Kopano-dev/kopano-ol-extension.git synced 2023-10-10 13:37:40 +02:00

[KOE-17] Basic infrastructure is working, still need replacement of placeholders

This commit is contained in:
Patrick Simpson 2017-02-23 15:29:53 +01:00
parent 629bfd371b
commit e735aafc72
24 changed files with 777 additions and 81 deletions

View File

@ -274,16 +274,17 @@
</Compile>
<Compile Include="Features\SendAs\FeatureSendAs.cs" />
<Compile Include="Features\SharedFolders\FolderTreeNode.cs" />
<Compile Include="Features\Signatures\FeatureSignatures.cs" />
<Compile Include="GlobalOptions.cs" />
<Compile Include="Logging.cs" />
<Compile Include="Native\IOleWindow.cs" />
<Compile Include="Native\IOlkAccount.cs" />
<Compile Include="Native\MAPI\Binary.cs" />
<Compile Include="Native\MAPI\IMAPIContainer.cs" />
<Compile Include="Native\MAPI\IMAPIFolder.cs" />
<Compile Include="Native\MAPI\IMAPIProp.cs" />
<Compile Include="Native\MAPI\Property.cs" />
<Compile Include="Native\MAPI\Restriction.cs" />
<Compile Include="Native\NativeEncoder.cs" />
<Compile Include="OutlookConstants.cs" />
<Compile Include="SearchQuery.cs" />
<Compile Include="Stubs\Enums.cs" />
@ -299,6 +300,8 @@
<Compile Include="Stubs\IOutlookWindow.cs" />
<Compile Include="Stubs\IPicture.cs" />
<Compile Include="Stubs\IRecipient.cs" />
<Compile Include="Stubs\ISignature.cs" />
<Compile Include="Stubs\ISignatures.cs" />
<Compile Include="Stubs\IStores.cs" />
<Compile Include="Stubs\ISyncObject.cs" />
<Compile Include="Stubs\OutlookWrappers\AccountWrapper.cs" />
@ -312,6 +315,8 @@
<Compile Include="Stubs\OutlookWrappers\OutlookItemWrapper.cs" />
<Compile Include="Stubs\OutlookWrappers\PictureWrapper.cs" />
<Compile Include="Stubs\OutlookWrappers\RecipientWrapper.cs" />
<Compile Include="Stubs\OutlookWrappers\SignaturesWrapper.cs" />
<Compile Include="Stubs\OutlookWrappers\SignatureWrapper.cs" />
<Compile Include="Stubs\OutlookWrappers\StoresWrapper.cs" />
<Compile Include="Stubs\OutlookWrappers\SyncObjectWrapper.cs" />
<Compile Include="Stubs\Wrappers.cs" />

View File

@ -74,6 +74,7 @@ namespace Acacia
public const string ZPUSH_HEADER_CLIENT_CAPABILITIES = "X-Push-Plugin-Capabilities";
public const string ZPUSH_HEADER_PLUGIN = "X-Push-Plugin";
public const string ZPUSH_HEADER_VERSION = "X-Z-Push-Version";
public const string ZPUSH_HEADER_SIGNATURES_HASH = "X-Push-Signatures-Hash";
#endregion

View File

@ -142,6 +142,38 @@ namespace Acacia
}
public class StringOption : Option<string>
{
private readonly string _defaultValue;
public StringOption(string token, string defaultValue)
:
base(token)
{
this._defaultValue = defaultValue;
}
public override string GetToken(string value)
{
if (value.Equals(_defaultValue))
return null;
return Token + "=" + value.ToString();
}
public override string GetValue(string value)
{
if (string.IsNullOrEmpty(value))
return _defaultValue;
else
{
if (value.ToLower().StartsWith(Token.ToLower() + "="))
value = value.Substring(Token.Length + 1);
return value;
}
}
}
// General
public static readonly BoolOption ENABLED = new BoolOption("", true);
public static readonly BoolOption FEATURE_DISABLED_DEFAULT = new BoolOption("", false);

View File

@ -87,7 +87,6 @@ namespace Acacia.Features.SecondaryContacts
// So, when the folder is detected, we make it invisible and perform steps 1 and 2. We issue a warning
// that Outlook must be restarted. When the folder is detected again and is invisible, that means we've restarted
// At this point the name is patched and the folder is made visible.
if (!folder.AttrHidden)
{
// Stage 1
@ -113,12 +112,12 @@ namespace Acacia.Features.SecondaryContacts
// Stage 2
// Patch the name
Logger.Instance.Trace(this, "Patching name");
folder.Name = strippedName;
Logger.Instance.Trace(this, "Patching name");
folder.Name = strippedName;
// Show it
folder.AttrHidden = false;
Logger.Instance.Debug(this, "Shown secondary contacts folder: {0}", strippedName);
// Show it
folder.AttrHidden = false;
Logger.Instance.Debug(this, "Shown secondary contacts folder: {0}", strippedName);
}
Logger.Instance.Debug(this, "Patching done: {0}: {1}", strippedName, folder.AttrHidden);
}

View File

@ -0,0 +1,186 @@
/// Copyright 2017 Kopano b.v.
///
/// This program is free software: you can redistribute it and/or modify
/// it under the terms of the GNU Affero General Public License, version 3,
/// as published by the Free Software Foundation.
///
/// This program is distributed in the hope that it will be useful,
/// but WITHOUT ANY WARRANTY; without even the implied warranty of
/// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
/// GNU Affero General Public License for more details.
///
/// You should have received a copy of the GNU Affero General Public License
/// along with this program.If not, see<http://www.gnu.org/licenses/>.
///
/// Consult LICENSE file for details
using Acacia.Stubs;
using Acacia.Stubs.OutlookWrappers;
using Acacia.Utils;
using Acacia.ZPush;
using Acacia.ZPush.Connect;
using Acacia.ZPush.Connect.Soap;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using static Acacia.DebugOptions;
namespace Acacia.Features.Signatures
{
[AcaciaOption("Provides the possibility to synchronise signatures from the server.")]
public class FeatureSignatures : Feature
{
#region Debug options
[AcaciaOption("The format for local names of synchronised signatures, to prevent overwriting local signatures. May contain %account% and %name%.")]
public string SignatureLocalName
{
get { return GetOption(OPTION_SIGNATURE_LOCAL_NAME); }
set { SetOption(OPTION_SIGNATURE_LOCAL_NAME, value); }
}
private static readonly StringOption OPTION_SIGNATURE_LOCAL_NAME = new StringOption("SignatureLocalName", "%name% (KOE-%account%)");
#endregion
public override void Startup()
{
Watcher.AccountDiscovered += Watcher_AccountDiscovered;
}
private void Watcher_AccountDiscovered(ZPushAccount account)
{
account.ConfirmedChanged += Account_ConfirmedChanged;
}
private void Account_ConfirmedChanged(ZPushAccount account)
{
// TODO: make a helper to register for all zpush accounts with specific capabilities, best even
// the feature's capabilities
if (account.Confirmed == ZPushAccount.ConfirmationType.IsZPush &&
account.Capabilities.Has("signatures"))
{
Logger.Instance.Trace(this, "Checking signature hash for account {0}: {1}", account, account.ServerSignaturesHash);
// Fetch signatures if there is a change
if (account.ServerSignaturesHash != account.Account.LocalSignaturesHash)
{
try
{
Logger.Instance.Debug(this, "Updating signatures: {0}", account);
FetchSignatures(account);
// Store updated hash
account.Account.LocalSignaturesHash = account.ServerSignaturesHash;
Logger.Instance.Debug(this, "Updated signatures: {0}", account);
}
catch (Exception e)
{
Logger.Instance.Error(this, "Error fetching signatures: {0}: {1}", account, e);
}
}
}
}
// Prevent field assignment warnings
#pragma warning disable 0649
private class Signature
{
public string id;
public string name;
public string content;
public bool isHTML;
}
private class GetSignatures
{
public Dictionary<string, Signature> all;
public string new_message;
public string replyforward_message;
public string hash;
}
#pragma warning restore 0649
private class GetSignaturesRequest : SoapRequest<GetSignatures>
{
}
private void FetchSignatures(ZPushAccount account)
{
Logger.Instance.Debug(this, "Fetching signatures for account {0}", account);
using (ZPushConnection connection = account.Connect())
using (ZPushWebServiceInfo infoService = connection.InfoService)
{
GetSignatures result = infoService.Execute(new GetSignaturesRequest());
// Store the signatures
Dictionary<object, string> fullNames = new Dictionary<object, string>();
using (ISignatures signatures = ThisAddIn.Instance.GetSignatures())
{
foreach (Signature signature in result.all.Values)
{
string name = StoreSignature(signatures, account, signature);
fullNames.Add(signature.id, name);
}
}
// Set default signatures if available and none are set
if (!string.IsNullOrEmpty(result.new_message) && string.IsNullOrEmpty(account.Account.SignatureNewMessage))
{
account.Account.SignatureNewMessage = fullNames[result.new_message];
}
if (!string.IsNullOrEmpty(result.replyforward_message) && string.IsNullOrEmpty(account.Account.SignatureReplyForwardMessage))
{
account.Account.SignatureReplyForwardMessage = fullNames[result.replyforward_message];
}
}
}
private string StoreSignature(ISignatures signatures, ZPushAccount account, Signature signatureInfo)
{
string name = SignatureLocalName.ReplacePercentStrings(new Dictionary<string, string>
{
{ "account", account.DisplayName },
{ "name", signatureInfo.name }
});
// Remove any existing signature
try
{
ISignature signature = signatures.Get(name);
if (signature != null)
{
try
{
signature.Delete();
}
finally
{
signature.Dispose();
}
}
}
catch(Exception e)
{
Logger.Instance.Error(this, "Unable to delete signature {0}: {1}", name, e);
}
// Create the new signature
using (ISignature signature = signatures.Add(name))
{
signature.SetContent(signatureInfo.content, signatureInfo.isHTML ? ISignatureFormat.HTML : ISignatureFormat.Text);
// TODO: generate text version if we get an HTML?
}
return name;
}
}
}

View File

@ -0,0 +1,53 @@
using Acacia.Native.MAPI;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace Acacia.Native
{
[StructLayout(LayoutKind.Explicit)]
unsafe public struct ACCT_VARIANT
{
[FieldOffset(0)]
public uint dwType;
[FieldOffset(4)]
public uint dwAlignPad;
[FieldOffset(8)]
public char* lpszW;
}
[ComImport]
[Guid("9240a6d2-af41-11d2-8c3b-00104b2a6676")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
unsafe public interface IOlkAccount
{
// IOlkErrorUnknown
void IOlkErrorUnknown_GetLastError();
void IOlkAccount_Placeholder1();
void IOlkAccount_Placeholder2();
void IOlkAccount_Placeholder3();
void IOlkAccount_Placeholder4();
void IOlkAccount_Placeholder5();
void IOlkAccount_Placeholder6();
void GetAccountInfo(Guid* pclsidType, int* pcCategories, Guid** prgclsidCategory);
void GetProp(PropTag dwProp, ACCT_VARIANT *pVar);
void SetProp(PropTag dwProp, ACCT_VARIANT *pVar);
void IOlkAccount_Placeholder7();
void IOlkAccount_Placeholder8();
void IOlkAccount_Placeholder9();
void FreeMemory(byte* pv);
void IOlkAccount_Placeholder10();
void SaveChanges(uint dwFlags);
}
}

View File

@ -54,6 +54,15 @@ namespace Acacia.Native.MAPI
{
return SearchQuery.PropertyIdentifier.FromTag(prop, (ushort)type);
}
public static PropTag FromInt(int v)
{
return new PropTag()
{
prop = (ushort)((v & 0xFFFF0000) >> 16),
type = (PropType)(v & 0xFFFF)
};
}
}

View File

@ -41,6 +41,10 @@ namespace Acacia
public const string REG_VAL_DELIVERY_STORE = "Delivery Store EntryID";
public const string REG_VAL_DELIVERY_FOLDER = "Delivery Folder EntryID";
public const string REG_VAL_NEW_SIGNATURE = "New Signature";
public const string REG_VAL_REPLY_FORWARD_SIGNATURE = "Reply-Forward Signature";
public const string REG_VAL_CURRENT_SIGNATURE = "KOE Signature Digest";
public const string REG_VAL_NEXT_ACCOUNT_ID = "NextAccountID";
#endregion

View File

@ -49,5 +49,23 @@ namespace Acacia.Stubs
string DomainName { get; }
// TODO: this is really a Z-Push thing, but it's here to store it in the registry
string LocalSignaturesHash
{
get;
set;
}
string SignatureNewMessage
{
get;
set;
}
string SignatureReplyForwardMessage
{
get;
set;
}
}
}

View File

@ -86,5 +86,9 @@ namespace Acacia.Stubs
}
#endregion
ISignatures GetSignatures();
void InUI(Action action);
}
}

View File

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Acacia.Stubs
{
public enum ISignatureFormat
{
HTML,
Text
}
public interface ISignature : IDisposable
{
void Delete();
void SetContent(string content, ISignatureFormat format);
}
}

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Acacia.Stubs
{
public interface ISignatures : IDisposable
{
ISignature Get(string name);
ISignature Add(string name);
}
}

View File

@ -1,4 +1,7 @@
/// Copyright 2017 Kopano b.v.

using Acacia.Native;
using Acacia.Native.MAPI;
/// Copyright 2017 Kopano b.v.
///
/// This program is free software: you can redistribute it and/or modify
/// it under the terms of the GNU Affero General Public License, version 3,
@ -13,26 +16,29 @@
/// along with this program.If not, see<http://www.gnu.org/licenses/>.
///
/// Consult LICENSE file for details
using Acacia.Utils;
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using NSOutlook = Microsoft.Office.Interop.Outlook;
namespace Acacia.Stubs.OutlookWrappers
{
[TypeConverter(typeof(ExpandableObjectConverter))]
class AccountWrapper : DisposableWrapper, IAccount, LogContext
class AccountWrapper : ComWrapper<NSOutlook.Application>, IAccount, LogContext
{
private readonly string _regPath;
private readonly IStore _store;
internal AccountWrapper(string regPath, IStore store)
internal AccountWrapper(NSOutlook.Application item, string regPath, IStore store) : base(item)
{
this._regPath = regPath;
this._store = store;
@ -44,6 +50,7 @@ namespace Acacia.Stubs.OutlookWrappers
protected override void DoRelease()
{
_store.Dispose();
base.DoRelease();
}
[Browsable(false)]
@ -163,6 +170,83 @@ namespace Acacia.Stubs.OutlookWrappers
}
}
public string LocalSignaturesHash
{
get
{
return RegistryUtil.GetValueString(_regPath, OutlookConstants.REG_VAL_CURRENT_SIGNATURE, null);
}
set
{
RegistryUtil.SetValueString(_regPath, OutlookConstants.REG_VAL_CURRENT_SIGNATURE, value);
}
}
public string SignatureNewMessage
{
get
{
return RegistryUtil.GetValueString(_regPath, OutlookConstants.REG_VAL_NEW_SIGNATURE, null);
}
set
{
// TODO: constant for account
SetAccountProp(PropTag.FromInt(0x0016001F), value);
}
}
unsafe private void SetAccountProp(PropTag propTag, string value)
{
// Use IOlkAccount to notify while we're running
// IOlkAccount can only be accessed on main thread
ThisAddIn.Instance.InUI(() =>
{
using (ComRelease com = new ComRelease())
{
NSOutlook.Account account = com.Add(FindAccountObject());
IOlkAccount olk = com.Add(account.IOlkAccount);
fixed (char* ptr = value.ToCharArray())
{
ACCT_VARIANT val = new ACCT_VARIANT()
{
dwType = (uint)PropType.UNICODE,
lpszW = ptr
};
olk.SetProp(propTag, &val);
olk.SaveChanges(0);
}
}
});
}
private NSOutlook.Account FindAccountObject()
{
using (ComRelease com = new ComRelease())
{
NSOutlook.NameSpace session = com.Add(_item.Session);
foreach(NSOutlook.Account account in session.Accounts.ComEnum(false))
{
if (account.SmtpAddress == this.SmtpAddress)
return account;
else
com.Add(account);
}
}
return null;
}
public string SignatureReplyForwardMessage
{
get
{
return RegistryUtil.GetValueString(_regPath, OutlookConstants.REG_VAL_REPLY_FORWARD_SIGNATURE, null);
}
set
{
SetAccountProp(PropTag.FromInt(0x0017001F), value);
}
}
#endregion
}
}

View File

@ -25,17 +25,19 @@ using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using NSOutlook = Microsoft.Office.Interop.Outlook;
namespace Acacia.Stubs.OutlookWrappers
{
public class AddInWrapper : IAddIn
class AddInWrapper : IAddIn
{
private readonly NSOutlook.Application _app;
private readonly ThisAddIn _thisAddIn;
private readonly StoresWrapper _stores;
private readonly SynchronizationContext _sync;
public AddInWrapper(ThisAddIn thisAddIn)
{
@ -51,6 +53,38 @@ namespace Acacia.Stubs.OutlookWrappers
{
ComRelease.Release(session);
}
// The synchronization context is needed to allow background tasks to jump back to the UI thread.
// It's null in older versions of .Net, this fixes that
if (SynchronizationContext.Current == null)
{
SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
}
_sync = SynchronizationContext.Current;
}
public ISignatures GetSignatures()
{
return new SignaturesWrapper();
}
public void InUI(Action action)
{
Exception x = null;
_sync.Send((_) =>
{
try
{
action();
}
catch(Exception e)
{
x = e;
}
}, null);
if (x != null)
throw x;
}
public void SendReceive(IAccount account)

View File

@ -0,0 +1,80 @@
using Acacia.Utils;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Acacia.Stubs.OutlookWrappers
{
class SignatureWrapper : DisposableWrapper, ISignature
{
private static readonly string[] SUFFIXES =
{
"htm", "html", "rtf", "txt"
};
private static string BasePath
{
get
{
return Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "\\Microsoft\\Signatures";
}
}
private readonly string _name;
public SignatureWrapper(string name)
{
this._name = name;
}
protected override void DoRelease()
{
}
internal static ISignature FindExisting(string name)
{
foreach(string suffix in SUFFIXES)
{
string path = GetPath(name, suffix);
if (new FileInfo(path).Exists)
return new SignatureWrapper(name);
}
return null;
}
private static string GetPath(string name, string suffix)
{
return Path.ChangeExtension(Path.Combine(BasePath, name), suffix);
}
public void Delete()
{
foreach (string suffix in SUFFIXES)
{
string path = GetPath(_name, suffix);
FileInfo file = new FileInfo(path);
if (file.Exists)
file.Delete();
}
// TODO: additional files folder? We never create it
}
public void SetContent(string content, ISignatureFormat format)
{
// Determine suffix
string suffix = "txt";
switch(format)
{
case ISignatureFormat.HTML: suffix = "htm"; break;
}
// Write
string path = GetPath(_name, suffix);
File.WriteAllText(path, content);
}
}
}

View File

@ -0,0 +1,38 @@
using Acacia.Utils;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Acacia.Stubs.OutlookWrappers
{
class SignaturesWrapper : DisposableWrapper, ISignatures
{
public SignaturesWrapper()
{
}
protected override void DoRelease()
{
}
public ISignature Get(string name)
{
return SignatureWrapper.FindExisting(name);
}
public ISignature Add(string name)
{
// Check if exists
using (ISignature existing = SignatureWrapper.FindExisting(name))
{
if (existing != null)
{
throw new ArgumentException("Signature " + name + " already exists");
}
}
return new SignatureWrapper(name);
}
}
}

View File

@ -247,7 +247,7 @@ namespace Acacia.Stubs.OutlookWrappers
{
if (baseKey == null)
return null;
AccountWrapper account = new AccountWrapper(baseKey.Name, store);
AccountWrapper account = new AccountWrapper(_item.Application, baseKey.Name, store);
Register(account);
return account;
}

View File

@ -101,13 +101,6 @@ namespace Acacia
int lcid = Application.LanguageSettings.get_LanguageID(Microsoft.Office.Core.MsoAppLanguageID.msoLanguageIDUI);
Thread.CurrentThread.CurrentUICulture = new CultureInfo(lcid);
// The synchronization context is needed to allow background tasks to jump back to the UI thread.
// It's null in older versions of .Net, this fixes that
if (SynchronizationContext.Current == null)
{
SynchronizationContext.SetSynchronizationContext(new WindowsFormsSynchronizationContext());
}
// Create the watcher
Watcher = new ZPushWatcher(Instance);
OutlookUI.Watcher = Watcher;

View File

@ -43,8 +43,13 @@ namespace Acacia.Utils
public static object Convert(this Type type, object value)
{
// For value types, null becomes a default instance
if (value == null && type.IsValueType)
return Activator.CreateInstance(type);
if (value == null)
{
if (type.IsValueType)
return Activator.CreateInstance(type);
else
return null;
}
// Check if we need a conversion
if (!type.IsAssignableFrom(value.GetType()))
@ -89,11 +94,27 @@ namespace Acacia.Utils
public static Type[] GetGenericArguments(this Type type, Type _base)
{
if (type.IsGenericType && type.GetGenericTypeDefinition() == _base)
return type.GetGenericArguments();
IEnumerable<Type> bases = _base.IsInterface ? type.GetInterfaces() : type.AllBaseTypes();
return bases.Select(x =>
(x.IsGenericType && x.GetGenericTypeDefinition() == _base) ? x.GetGenericArguments() : null
).FirstOrDefault();
}
public static IEnumerable<string> DebugComTypeNames(object o)
{
foreach (System.Reflection.Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
{
foreach (Type type in assembly.GetTypes())
{
if (type.IsInstanceOfType(o))
yield return type.FullName;
}
}
}
}
}

View File

@ -34,13 +34,17 @@ namespace Acacia.Utils
return RegToString(o);
}
// TODO: remove, also above
public static string GetValueString(string keyPath, string valueName, string defaultValue)
{
object o = Registry.GetValue(keyPath, valueName, defaultValue);
return RegToString(o);
}
public static void SetValueString(string keyPath, string valueName, string value)
{
Registry.SetValue(keyPath, valueName, value);
}
public static string RegToString(object o)
{
if (o is byte[])

View File

@ -19,6 +19,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml;
@ -105,5 +106,25 @@ namespace Acacia.Utils
#endregion
#region Formatting / Replacement
public static string ReplacePercentStrings(this string s, Dictionary<string, string> replacements)
{
return Regex.Replace(s, @"%(\w+)%", (m) =>
{
string replacement;
var key = m.Groups[1].Value;
if (replacements.TryGetValue(key, out replacement))
{
return Convert.ToString(replacement);
}
else
{
return m.Groups[0].Value;
}
});
}
#endregion
}
}

View File

@ -81,7 +81,7 @@ namespace Acacia.ZPush.Connect.Soap
return null;
// Type-specific parsing
TypeHandler type = LookupType(part);
TypeHandler type = LookupType(part, expectedType);
return type.Deserialize(part, expectedType);
}
@ -109,24 +109,43 @@ namespace Acacia.ZPush.Connect.Soap
public object Deserialize(XmlNode node, Type expectedType)
{
if (expectedType != null && _baseType != null)
{
// Check if the expected type matches the type
if (!_baseType.IsAssignableFrom(expectedType))
throw new InvalidOperationException("Expected " + expectedType + ", found " + _baseType);
}
object value = DeserializeContents(node, expectedType);
if (expectedType != null)
{
// Check if it's the expected type
return expectedType.Cast(value);
// Try to convert it to the expected type
return SoapConvert(expectedType, value);
}
return value;
}
protected object SoapConvert(Type type, object value)
{
// Check if any conversion is needed
if (value != null && type.IsAssignableFrom(value.GetType()))
return value;
if (value != null)
{
// Try Soap conversion
if (typeof(ISoapSerializable<>).IsGenericAssignableFrom(type))
{
// Get the serialization type
Type serializationType = type.GetGenericArguments(typeof(ISoapSerializable<>))[0];
if (serializationType.IsAssignableFrom(value.GetType()))
{
// Create the instance
return Activator.CreateInstance(type, value);
}
}
}
// Or standard conversions
return type.Convert(value);
}
abstract protected object DeserializeContents(XmlNode node, Type expectedType);
abstract public void Serialize(string name, object value, StringBuilder s);
@ -150,7 +169,7 @@ namespace Acacia.ZPush.Connect.Soap
}
private class TypeHandlerInt : TypeHandler
{
public TypeHandlerInt() : base(SoapConstants.XMLNS_XSD, "int", typeof(int)) { }
public TypeHandlerInt() : base(SoapConstants.XMLNS_XSD, "int", typeof(long)) { }
public override void Serialize(string name, object value, StringBuilder s)
{
@ -159,7 +178,7 @@ namespace Acacia.ZPush.Connect.Soap
protected override object DeserializeContents(XmlNode node, Type expectedType)
{
return int.Parse(node.InnerText);
return long.Parse(node.InnerText);
}
}
@ -230,7 +249,10 @@ namespace Acacia.ZPush.Connect.Soap
{
// Determine if the expected type is an ISoapSerializable
if (!typeof(ISoapSerializable<>).IsGenericAssignableFrom(expectedType))
throw new InvalidOperationException("Cannot parse type " + expectedType);
{
// Nope, try simple assignment
return DeserializeContentsRaw(node, expectedType);
}
// Get the serialization type
Type serializationType = expectedType.GetGenericArguments(typeof(ISoapSerializable<>))[0];
@ -243,6 +265,32 @@ namespace Acacia.ZPush.Connect.Soap
return CreateCustomInstance(values, serializationType, expectedType);
}
private object DeserializeContentsRaw(XmlNode node, Type expectedType)
{
// TODO: better error on failure
// Get the values as a dictionary
Dictionary<string, object> values = new Dictionary<string, object>();
DeserializeMembers(node, expectedType, values);
// And assign them to a new instance
return DeserializeContentsRaw(values, expectedType);
}
private object DeserializeContentsRaw(Dictionary<string, object> node, Type serializationType)
{
object instance = Activator.CreateInstance(serializationType);
foreach (FieldInfo field in serializationType.GetFields())
{
object value = null;
if (node.TryGetValue(field.Name.ToLower(), out value))
{
value = SoapConvert(field.FieldType, value);
field.SetValue(instance, value);
}
}
return instance;
}
abstract protected void DeserializeMembers(XmlNode node, Type serializationType, Dictionary<string, object> values);
private object CreateCustomInstance(Dictionary<string, object> node, Type serializationType, Type finalType)
@ -255,47 +303,13 @@ namespace Acacia.ZPush.Connect.Soap
else
{
// Initialise the serialization type
instance = Activator.CreateInstance(serializationType);
foreach (FieldInfo field in serializationType.GetFields())
{
object value = null;
if (node.TryGetValue(field.Name.ToLower(), out value))
{
value = SoapConvert(field.FieldType, value);
field.SetValue(instance, value);
}
}
instance = DeserializeContentsRaw(node, serializationType);
}
// Return the final type
return Activator.CreateInstance(finalType, instance);
}
private object SoapConvert(Type type, object value)
{
// Check if any conversion is needed
if (value != null && type.IsAssignableFrom(value.GetType()))
return value;
if (value != null)
{
// Try Soap conversion
if (typeof(ISoapSerializable<>).IsGenericAssignableFrom(type))
{
// Get the serialization type
Type serializationType = type.GetGenericArguments(typeof(ISoapSerializable<>))[0];
if (serializationType.IsAssignableFrom(value.GetType()))
{
// Create the instance
return Activator.CreateInstance(type, value);
}
}
}
// Or standard conversions
return type.Convert(value);
}
public override void Serialize(string name, object value, StringBuilder s)
{
Dictionary<string, object> dict;
@ -319,6 +333,18 @@ namespace Acacia.ZPush.Connect.Soap
}
protected abstract void SerializeMembers(string name, Dictionary<string, object> fields, StringBuilder s);
protected virtual Type DetermineChildType(Type type, string field)
{
if (type == null)
return null;
FieldInfo prop = type.GetField(field);
if (prop == null)
return null;
return prop.FieldType;
}
}
private class TypeHandlerStruct : TypeHandlerObject
@ -333,7 +359,7 @@ namespace Acacia.ZPush.Connect.Soap
foreach (XmlNode child in node.ChildNodes)
{
string key = child.Name.ToLower();
object value = DeserializeNode(child, null);
object value = DeserializeNode(child, DetermineChildType(expectedType, key));
dict.Add(key, value);
}
}
@ -345,9 +371,9 @@ namespace Acacia.ZPush.Connect.Soap
}
private class TypeHandlerMap : TypeHandlerObject
private class TypeHandlerObjectMap : TypeHandlerObject
{
public TypeHandlerMap() : base(SoapConstants.XMLNS_APACHE, "Map")
public TypeHandlerObjectMap() : base(SoapConstants.XMLNS_APACHE, "Map")
{
}
@ -357,7 +383,7 @@ namespace Acacia.ZPush.Connect.Soap
foreach (XmlNode child in node.ChildNodes)
{
string key = (string)DeserializeNode(child.SelectSingleNode("key"), typeof(string));
object value = DeserializeNode(child.SelectSingleNode("value"), null);
object value = DeserializeNode(child.SelectSingleNode("value"), DetermineChildType(expectedType, key));
dict.Add(key, value);
}
}
@ -377,10 +403,38 @@ namespace Acacia.ZPush.Connect.Soap
#endregion
#region Map
private class TypeHandlerMap<KeyType,ValueType> : TypeHandler
{
public TypeHandlerMap() : base(SoapConstants.XMLNS_SOAP_ENC, "Array", typeof(System.Collections.ICollection)) { }
protected override object DeserializeContents(XmlNode node, Type expectedType)
{
Dictionary<KeyType, ValueType> map = new Dictionary<KeyType, ValueType>();
foreach (XmlNode child in node.ChildNodes)
{
KeyType key = (KeyType)DeserializeNode(child.SelectSingleNode("key"), typeof(KeyType));
ValueType value = (ValueType)DeserializeNode(child.SelectSingleNode("value"), typeof(ValueType));
map.Add(key, value);
}
return map;
}
public override void Serialize(string name, object value, StringBuilder s)
{
throw new NotImplementedException();
}
}
#endregion
private readonly static Dictionary<string, TypeHandler> TYPES_BY_FULL_NAME = new Dictionary<string, TypeHandler>();
private readonly static Dictionary<Type, TypeHandler> TYPES_BY_TYPE = new Dictionary<Type, TypeHandler>();
private readonly static TypeHandler TYPE_HANDLER_OBJECT = new TypeHandlerMap();
private readonly static TypeHandler TYPE_HANDLER_OBJECT = new TypeHandlerObjectMap();
static SoapSerializer()
{
@ -415,8 +469,14 @@ namespace Acacia.ZPush.Connect.Soap
return null;
}
private static TypeHandler LookupType(XmlNode part)
private static TypeHandler LookupType(XmlNode part, Type expectedType)
{
if (expectedType != null && typeof(IDictionary<,>).IsGenericAssignableFrom(expectedType))
{
Type bound = typeof(TypeHandlerMap<,>).MakeGenericType(expectedType.GetGenericArguments(typeof(IDictionary<,>)));
return (TypeHandler)Activator.CreateInstance(bound);
}
XmlAttribute typeAttr = part.Attributes["type", SoapConstants.XMLNS_XSI];
if (typeAttr == null)
throw new Exception("Missing type");
@ -431,6 +491,7 @@ namespace Acacia.ZPush.Connect.Soap
TypeHandler type;
if (!TYPES_BY_FULL_NAME.TryGetValue(fullName, out type))
throw new Exception("Unknown type: " + fullName);
return type;
}

View File

@ -204,6 +204,12 @@ namespace Acacia.ZPush.Connect
private set;
}
public string SignaturesHash
{
get;
private set;
}
private string GetStringHeader(HttpResponseMessage response, string name)
{
IEnumerable<string> values;
@ -227,6 +233,7 @@ namespace Acacia.ZPush.Connect
Capabilities = ZPushCapabilities.Parse(GetStringHeader(response, Constants.ZPUSH_HEADER_CAPABILITIES));
ZPushVersion = GetStringHeader(response, Constants.ZPUSH_HEADER_VERSION);
SignaturesHash = GetStringHeader(response, Constants.ZPUSH_HEADER_SIGNATURES_HASH);
// Check for success
Success = response.IsSuccessStatusCode;

View File

@ -120,6 +120,13 @@ namespace Acacia.ZPush
private set;
}
public string ServerSignaturesHash
{
get;
private set;
}
public void LinkedGABFolder(IFolder folder)
{
GABFolderLinked = folder.EntryID;
@ -128,13 +135,14 @@ namespace Acacia.ZPush
internal void OnConfirmationResponse(ZPushConnection.Response response)
{
Capabilities = response.Capabilities;
// TODO: move these properties to the features? Though it's nice to have them here for the debug dialog
GABFolder = response.GABName;
ZPushVersion = response.ZPushVersion;
ServerSignaturesHash = response.SignaturesHash;
Confirmed = Capabilities == null ? ConfirmationType.IsNotZPush : ConfirmationType.IsZPush;
Logger.Instance.Info(this, "ZPush confirmation: {0} -> {1}, {2}", Confirmed, Capabilities, GABFolder);
if (_confirmedChanged != null)
_confirmedChanged(this);
_confirmedChanged?.Invoke(this);
}
#endregion