diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e489bd067..9d1b923e7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -190,6 +190,7 @@ jobs: -DCMAKE_TOOLCHAIN_FILE=${{ env.RUNVCPKG_VCPKG_ROOT }}/scripts/buildsystems/vcpkg.cmake ` "-DLibDigiDocpp_ROOT=libs/libdigidocpp" cmake --build build --target msi + cmake --build build --target msishellext cmake --build build --target appx - name: Archive artifacts uses: actions/upload-artifact@v3 diff --git a/.gitmodules b/.gitmodules index 7cbda08be..fff60d64e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,6 +4,3 @@ [submodule "common"] path = common url = ../qt-common -[submodule "extensions"] - path = extensions - url = ../digidoc-extensions diff --git a/CMakeLists.txt b/CMakeLists.txt index e29adabd3..7e037671e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,7 +39,7 @@ include(CPack) if(APPLE) add_subdirectory(extensions/DigiDocQL) elseif(WIN32) - add_subdirectory(extensions/windows EXCLUDE_FROM_ALL) + add_subdirectory(extensions/windows) elseif(UNIX) option(ENABLE_KDE "Install KDE service menu (default: TRUE)" TRUE) option(ENABLE_NAUTILUS_EXTENSION "Build Nautilus extension (default: TRUE)" TRUE) diff --git a/extensions b/extensions deleted file mode 160000 index d428a88c8..000000000 --- a/extensions +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d428a88c8acdd2f89dbefd4c4c5691761518160a diff --git a/extensions/DigiDocQL/CMakeLists.txt b/extensions/DigiDocQL/CMakeLists.txt new file mode 100644 index 000000000..8856ad0d3 --- /dev/null +++ b/extensions/DigiDocQL/CMakeLists.txt @@ -0,0 +1,24 @@ +set( RESOURCES ../../client/mac/Resources/asic.icns ) +add_library( DigiDocQL MODULE + ${RESOURCES} + Info.plist + main.c + GenerateThumbnailForURL.c + GeneratePreviewForURL.mm +) +set_source_files_properties( GeneratePreviewForURL.mm PROPERTIES COMPILE_FLAGS "-fobjc-arc" ) +set_source_files_properties( Info.plist PROPERTIES MACOSX_PACKAGE_LOCATION . ) +set_target_properties( DigiDocQL PROPERTIES + BUNDLE YES + BUNDLE_EXTENSION qlgenerator + RESOURCE "${RESOURCES}" + XCODE_ATTRIBUTE_WRAPPER_EXTENSION qlgenerator + MACOSX_BUNDLE_INFO_PLIST ${CMAKE_CURRENT_SOURCE_DIR}/Info.plist + AUTOMOC OFF + INCLUDE_DIRECTORIES "${LIBDIGIDOCPP_INCLUDE_DIR}" + COMPILE_FLAGS "-Wno-unused-parameter" + LINK_LIBRARIES "-framework QuickLook;-framework digidocpp;-framework Cocoa" + LINK_FLAGS "-F/Library/Frameworks -fobjc-arc" + BUILD_WITH_INSTALL_RPATH YES + INSTALL_RPATH "@loader_path/../../../../../Frameworks" +) diff --git a/extensions/DigiDocQL/GeneratePreviewForURL.mm b/extensions/DigiDocQL/GeneratePreviewForURL.mm new file mode 100644 index 000000000..c1b07476b --- /dev/null +++ b/extensions/DigiDocQL/GeneratePreviewForURL.mm @@ -0,0 +1,201 @@ +/* + * QEstEidClient + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +// https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/Quicklook_Programming_Guide/Introduction/Introduction.html + +#include +#include +#include +#include +#include + +#include +#include + +using namespace digidoc; + +QL_EXTERN_C_BEGIN +OSStatus GeneratePreviewForURL(void *thisInterface, QLPreviewRequestRef preview, + CFURLRef url, CFStringRef contentTypeUTI, CFDictionaryRef options); +void CancelPreviewGeneration(void * /*thisInterface*/, QLPreviewRequestRef /*preview*/) {} +QL_EXTERN_C_END + +@interface NSString (Digidoc) ++ (NSString*)stdstring:(const std::string&)str; ++ (NSString*)fileSize:(unsigned long)bytes; ++ (void)parseException:(const Exception&)e result:(NSMutableArray *)result; +@end + +@implementation NSString (Digidoc) ++ (NSString*)stdstring:(const std::string&)str +{ + return str.empty() ? [NSString string] : [NSString stringWithUTF8String:str.c_str()]; +} + ++ (NSString*)htmlEntityEncode:(NSString*)str +{ + str = [str stringByReplacingOccurrencesOfString:@"&" withString:@"&"]; + str = [str stringByReplacingOccurrencesOfString:@"\"" withString:@"""]; + str = [str stringByReplacingOccurrencesOfString:@"'" withString:@"'"]; + str = [str stringByReplacingOccurrencesOfString:@"<" withString:@"<"]; + str = [str stringByReplacingOccurrencesOfString:@">" withString:@">"]; + return str; +} + ++ (NSString*)fileSize:(unsigned long)bytes +{ + enum { + kb = 1UL << 1, + mb = 1UL << 2, + gb = 1UL << 3 + }; + if (bytes >= gb) + return [NSString stringWithFormat:@"%1.2f GB", double(bytes) / gb]; + if (bytes >= mb) + return [NSString stringWithFormat:@"%1.2f MB", double(bytes) / mb]; + if (bytes >= kb) + return [NSString stringWithFormat:@"%1.1f KB", double(bytes) / kb]; + return [NSString stringWithFormat:@"%lu bytes", bytes]; +} + ++ (void)parseException:(const Exception&)e result:(NSMutableArray *)result +{ + [result addObject:[self stdstring:e.msg()]]; + for (const Exception &i : e.causes()) { + [self parseException:i result:result]; + } +} +@end + +class DigidocConf: public digidoc::XmlConfCurrent +{ +public: + bool TSLAutoUpdate() const final { return false; } + bool TSLOnlineDigest() const final { return false; } + std::string TSLCache() const final + { + std::string home = "~"; + if(char *var = getenv("HOME")) + home = var; + return home + "/Library/Containers/ee.ria.qdigidoc4/Data/Library/Application Support/RIA/qdigidoc4/"; + } +}; + +OSStatus GeneratePreviewForURL(void */*thisInterface*/, QLPreviewRequestRef preview, + CFURLRef url, CFStringRef /*contentTypeUTI*/, CFDictionaryRef /*options*/) +{ + @autoreleasepool { + NSMutableString *h = [NSMutableString string]; + [h appendString:@""]; + [h appendFormat:@"

%@

", [NSString htmlEntityEncode:[(__bridge NSURL*)url lastPathComponent]]]; + try + { + digidoc::Conf::init( new DigidocConf ); + digidoc::initialize(); + std::unique_ptr d(Container::openPtr([(__bridge NSURL*)url path].UTF8String)); + + [h appendString:@"Files
    "]; + for (const DataFile *doc : d->dataFiles()) { + [h appendFormat:@"
  1. %@
  2. ", [NSString htmlEntityEncode:[NSString stdstring:doc->fileName()]]]; + } + [h appendString:@"
"]; + + [h appendString:@"Signatures"]; + for (const Signature *s : d->signatures()) { + [h appendFormat:@"
Signer
%@
", [NSString htmlEntityEncode:[NSString stdstring:s->signedBy()]]]; + + NSString *date = [NSString stdstring:s->trustedSigningTime()]; + [date stringByReplacingOccurrencesOfString:@"Z" withString:@"-0000"]; + NSDateFormatter *df = [[NSDateFormatter alloc] init]; + [df setDateFormat:@"yyyy-MM-dd'T'HH:mm:ssZ"]; + NSDate *formateddate = [df dateFromString:date]; + [df setTimeZone: [NSTimeZone defaultTimeZone]]; + [df setDateFormat:@"YYYY-MM-dd HH:mm:ss z"]; + [h appendFormat:@"
Time
%@
", [df stringFromDate:formateddate]]; + + Signature::Validator v(s); + NSString *status = @"not valid"; + switch(v.status()) + { + case Signature::Validator::Valid: status = @"valid"; break; + case Signature::Validator::Warning: status = @"valid with warnings"; break; + case Signature::Validator::NonQSCD: status = @"valid with limitations"; break; + case Signature::Validator::Test: status = @"valid test signature"; break; + case Signature::Validator::Invalid: status = @"invalid"; break; + case Signature::Validator::Unknown: status = @"unknown"; break; + } + [h appendFormat:@"
Validity
Signature is %@
", status]; + + NSMutableArray *roles = [NSMutableArray array]; + for (const std::string &role : s->signerRoles()) { + if( !role.empty() ) { + [roles addObject:[NSString htmlEntityEncode:[NSString stdstring:role]]]; + } + } + if( [roles count] > 0 ) { + [h appendFormat:@"
Role
%@ 
", [NSString htmlEntityEncode:[roles componentsJoinedByString:@" / "]]]; + } + if (!s->countryName().empty()) { + [h appendFormat:@"
Country
%@ 
", [NSString htmlEntityEncode:[NSString stdstring:s->countryName()]]]; + } + if (!s->city().empty()) { + [h appendFormat:@"
City
%@ 
", [NSString htmlEntityEncode:[NSString stdstring:s->city()]]]; + } + if (!s->stateOrProvince().empty()) { + [h appendFormat:@"
State
%@ 
", [NSString htmlEntityEncode:[NSString stdstring:s->stateOrProvince()]]]; + } + if (!s->postalCode().empty()) { + [h appendFormat:@"
Postal code
%@ 
", [NSString htmlEntityEncode:[NSString stdstring:s->postalCode()]]]; + } + [h appendString:@"
"]; + } + digidoc::terminate(); + } catch (const Exception &e) { + NSMutableArray *err = [NSMutableArray array]; + [NSString parseException:e result:err]; + [h appendFormat:@"Failed to load document:
%@", [err componentsJoinedByString:@"
"]]; + } + [h appendString:@""]; + + NSBundle *bundle = [NSBundle bundleWithIdentifier:@"ee.ria.DigiDocQL"]; + NSData *image = [NSData dataWithContentsOfFile:[bundle pathForResource:@"asic" ofType:@"icns"]]; + NSDictionary *props = @{ + (__bridge id)kQLPreviewPropertyTextEncodingNameKey : @"UTF-8", + (__bridge id)kQLPreviewPropertyMIMETypeKey : @"text/html", + (__bridge id)kQLPreviewPropertyWidthKey : [[bundle infoDictionary] valueForKey:@"QLPreviewWidth"], + (__bridge id)kQLPreviewPropertyHeightKey : [[bundle infoDictionary] valueForKey:@"QLPreviewHeight"], + (__bridge id)kQLPreviewPropertyAttachmentsKey : @{ + @"asic.icns" : @{ + (__bridge id)kQLPreviewPropertyMIMETypeKey : @"image/icns", + (__bridge id)kQLPreviewPropertyAttachmentDataKey : image + } + } + }; + QLPreviewRequestSetDataRepresentation(preview, + (__bridge CFDataRef)[h dataUsingEncoding:NSUTF8StringEncoding], kUTTypeHTML, (__bridge CFDictionaryRef)props); + } + return noErr; +} diff --git a/extensions/DigiDocQL/GenerateThumbnailForURL.c b/extensions/DigiDocQL/GenerateThumbnailForURL.c new file mode 100644 index 000000000..6f93ad2cd --- /dev/null +++ b/extensions/DigiDocQL/GenerateThumbnailForURL.c @@ -0,0 +1,30 @@ +/* + * QEstEidClient + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#include + +OSStatus GenerateThumbnailForURL(void *thisInterface, QLThumbnailRequestRef thumbnail, + CFURLRef url, CFStringRef contentTypeUTI, CFDictionaryRef options, CGSize maxSize) +{ + return noErr; +} + +void CancelThumbnailGeneration(void *thisInterface, QLThumbnailRequestRef thumbnail) +{ +} diff --git a/extensions/DigiDocQL/Info.plist b/extensions/DigiDocQL/Info.plist new file mode 100644 index 000000000..1789ee05d --- /dev/null +++ b/extensions/DigiDocQL/Info.plist @@ -0,0 +1,62 @@ + + + + + CFBundleDevelopmentRegion + English + CFBundleDocumentTypes + + + CFBundleTypeRole + QLGenerator + LSItemContentTypes + + ee.ria.bdoc + ee.ria.asics + + + + CFBundleExecutable + DigiDocQL + CFBundleIdentifier + ee.ria.DigiDocQL + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + DigiDocQL + CFBundleShortVersionString + 1 + CFBundleVersion + 1.0 + CFPlugInDynamicRegisterFunction + + CFPlugInDynamicRegistration + NO + CFPlugInFactories + + 60213CB1-91F9-43A6-BEF5-9DE58A2B6C52 + QuickLookGeneratorPluginFactory + + CFPlugInTypes + + 5E2D9680-5022-40FA-B806-43349622E5B9 + + 60213CB1-91F9-43A6-BEF5-9DE58A2B6C52 + + + CFPlugInUnloadFunction + + NSHumanReadableCopyright + Copyright © 2012-2023 Estonian Information System's Authority. All rights reserved. + QLNeedsToBeRunInMainThread + + QLPreviewHeight + 600 + QLPreviewWidth + 500 + QLSupportsConcurrentRequests + + QLThumbnailMinimumSize + 17 + + diff --git a/extensions/DigiDocQL/en.lproj/InfoPlist.strings b/extensions/DigiDocQL/en.lproj/InfoPlist.strings new file mode 100644 index 000000000..477b28ff8 --- /dev/null +++ b/extensions/DigiDocQL/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +/* Localized versions of Info.plist keys */ + diff --git a/extensions/DigiDocQL/main.c b/extensions/DigiDocQL/main.c new file mode 100644 index 000000000..4288f17d2 --- /dev/null +++ b/extensions/DigiDocQL/main.c @@ -0,0 +1,220 @@ +//============================================================================== +// +// DO NO MODIFY THE CONTENT OF THIS FILE +// +// This file contains the generic CFPlug-in code necessary for your generator +// To complete your generator implement the function in GenerateThumbnailForURL/GeneratePreviewForURL.c +// +//============================================================================== + + + + + + +#include +#include +#include +#include + +#define EXP __attribute__ ((visibility("default"))) + +// ----------------------------------------------------------------------------- +// constants +// ----------------------------------------------------------------------------- + +// Don't modify this line +#define PLUGIN_ID "60213CB1-91F9-43A6-BEF5-9DE58A2B6C52" + +// +// Below is the generic glue code for all plug-ins. +// +// You should not have to modify this code aside from changing +// names if you decide to change the names defined in the Info.plist +// + + +// ----------------------------------------------------------------------------- +// typedefs +// ----------------------------------------------------------------------------- + +// The thumbnail generation function to be implemented in GenerateThumbnailForURL.c +EXP OSStatus GenerateThumbnailForURL(void *thisInterface, QLThumbnailRequestRef thumbnail, CFURLRef url, CFStringRef contentTypeUTI, CFDictionaryRef options, CGSize maxSize); +EXP void CancelThumbnailGeneration(void* thisInterface, QLThumbnailRequestRef thumbnail); + +// The preview generation function to be implemented in GeneratePreviewForURL.c +EXP OSStatus GeneratePreviewForURL(void *thisInterface, QLPreviewRequestRef preview, CFURLRef url, CFStringRef contentTypeUTI, CFDictionaryRef options); +EXP void CancelPreviewGeneration(void *thisInterface, QLPreviewRequestRef preview); + +// The layout for an instance of QuickLookGeneratorPlugIn +typedef struct __QuickLookGeneratorPluginType +{ + void *conduitInterface; + CFUUIDRef factoryID; + UInt32 refCount; +} QuickLookGeneratorPluginType; + +// ----------------------------------------------------------------------------- +// prototypes +// ----------------------------------------------------------------------------- +// Forward declaration for the IUnknown implementation. +// + +EXP QuickLookGeneratorPluginType *AllocQuickLookGeneratorPluginType(CFUUIDRef inFactoryID); +EXP void DeallocQuickLookGeneratorPluginType(QuickLookGeneratorPluginType *thisInstance); +EXP HRESULT QuickLookGeneratorQueryInterface(void *thisInstance,REFIID iid,LPVOID *ppv); +EXP void *QuickLookGeneratorPluginFactory(CFAllocatorRef allocator,CFUUIDRef typeID); +EXP ULONG QuickLookGeneratorPluginAddRef(void *thisInstance); +EXP ULONG QuickLookGeneratorPluginRelease(void *thisInstance); + +// ----------------------------------------------------------------------------- +// myInterfaceFtbl definition +// ----------------------------------------------------------------------------- +// The QLGeneratorInterfaceStruct function table. +// +static QLGeneratorInterfaceStruct myInterfaceFtbl = { + NULL, + QuickLookGeneratorQueryInterface, + QuickLookGeneratorPluginAddRef, + QuickLookGeneratorPluginRelease, + NULL, + NULL, + NULL, + NULL +}; + + +// ----------------------------------------------------------------------------- +// AllocQuickLookGeneratorPluginType +// ----------------------------------------------------------------------------- +// Utility function that allocates a new instance. +// You can do some initial setup for the generator here if you wish +// like allocating globals etc... +// +QuickLookGeneratorPluginType *AllocQuickLookGeneratorPluginType(CFUUIDRef inFactoryID) +{ + QuickLookGeneratorPluginType *theNewInstance; + + theNewInstance = (QuickLookGeneratorPluginType *)malloc(sizeof(QuickLookGeneratorPluginType)); + memset(theNewInstance,0,sizeof(QuickLookGeneratorPluginType)); + + /* Point to the function table Malloc enough to store the stuff and copy the filler from myInterfaceFtbl over */ + theNewInstance->conduitInterface = malloc(sizeof(QLGeneratorInterfaceStruct)); + memcpy(theNewInstance->conduitInterface,&myInterfaceFtbl,sizeof(QLGeneratorInterfaceStruct)); + + /* Retain and keep an open instance refcount for each factory. */ + theNewInstance->factoryID = CFRetain(inFactoryID); + CFPlugInAddInstanceForFactory(inFactoryID); + + /* This function returns the IUnknown interface so set the refCount to one. */ + theNewInstance->refCount = 1; + return theNewInstance; +} + +// ----------------------------------------------------------------------------- +// DeallocQuickLookGeneratorPluginType +// ----------------------------------------------------------------------------- +// Utility function that deallocates the instance when +// the refCount goes to zero. +// In the current implementation generator interfaces are never deallocated +// but implement this as this might change in the future +// +void DeallocQuickLookGeneratorPluginType(QuickLookGeneratorPluginType *thisInstance) +{ + CFUUIDRef theFactoryID; + + theFactoryID = thisInstance->factoryID; + /* Free the conduitInterface table up */ + free(thisInstance->conduitInterface); + + /* Free the instance structure */ + free(thisInstance); + if (theFactoryID){ + CFPlugInRemoveInstanceForFactory(theFactoryID); + CFRelease(theFactoryID); + } +} + +// ----------------------------------------------------------------------------- +// QuickLookGeneratorQueryInterface +// ----------------------------------------------------------------------------- +// Implementation of the IUnknown QueryInterface function. +// +HRESULT QuickLookGeneratorQueryInterface(void *thisInstance,REFIID iid,LPVOID *ppv) +{ + CFUUIDRef interfaceID; + + interfaceID = CFUUIDCreateFromUUIDBytes(kCFAllocatorDefault,iid); + + if (CFEqual(interfaceID,kQLGeneratorCallbacksInterfaceID)){ + /* If the Right interface was requested, bump the ref count, + * set the ppv parameter equal to the instance, and + * return good status. + */ + ((QLGeneratorInterfaceStruct *)((QuickLookGeneratorPluginType *)thisInstance)->conduitInterface)->GenerateThumbnailForURL = GenerateThumbnailForURL; + ((QLGeneratorInterfaceStruct *)((QuickLookGeneratorPluginType *)thisInstance)->conduitInterface)->CancelThumbnailGeneration = CancelThumbnailGeneration; + ((QLGeneratorInterfaceStruct *)((QuickLookGeneratorPluginType *)thisInstance)->conduitInterface)->GeneratePreviewForURL = GeneratePreviewForURL; + ((QLGeneratorInterfaceStruct *)((QuickLookGeneratorPluginType *)thisInstance)->conduitInterface)->CancelPreviewGeneration = CancelPreviewGeneration; + ((QLGeneratorInterfaceStruct *)((QuickLookGeneratorPluginType*)thisInstance)->conduitInterface)->AddRef(thisInstance); + *ppv = thisInstance; + CFRelease(interfaceID); + return S_OK; + }else{ + /* Requested interface unknown, bail with error. */ + *ppv = NULL; + CFRelease(interfaceID); + return E_NOINTERFACE; + } +} + +// ----------------------------------------------------------------------------- +// QuickLookGeneratorPluginAddRef +// ----------------------------------------------------------------------------- +// Implementation of reference counting for this type. Whenever an interface +// is requested, bump the refCount for the instance. NOTE: returning the +// refcount is a convention but is not required so don't rely on it. +// +ULONG QuickLookGeneratorPluginAddRef(void *thisInstance) +{ + ((QuickLookGeneratorPluginType *)thisInstance )->refCount += 1; + return ((QuickLookGeneratorPluginType*) thisInstance)->refCount; +} + +// ----------------------------------------------------------------------------- +// QuickLookGeneratorPluginRelease +// ----------------------------------------------------------------------------- +// When an interface is released, decrement the refCount. +// If the refCount goes to zero, deallocate the instance. +// +ULONG QuickLookGeneratorPluginRelease(void *thisInstance) +{ + ((QuickLookGeneratorPluginType*)thisInstance)->refCount -= 1; + if (((QuickLookGeneratorPluginType*)thisInstance)->refCount == 0){ + DeallocQuickLookGeneratorPluginType((QuickLookGeneratorPluginType*)thisInstance ); + return 0; + }else{ + return ((QuickLookGeneratorPluginType*) thisInstance )->refCount; + } +} + +// ----------------------------------------------------------------------------- +// QuickLookGeneratorPluginFactory +// ----------------------------------------------------------------------------- +void *QuickLookGeneratorPluginFactory(CFAllocatorRef allocator,CFUUIDRef typeID) +{ + QuickLookGeneratorPluginType *result; + CFUUIDRef uuid; + + /* If correct type is being requested, allocate an + * instance of kQLGeneratorTypeID and return the IUnknown interface. + */ + if (CFEqual(typeID,kQLGeneratorTypeID)){ + uuid = CFUUIDCreateFromString(kCFAllocatorDefault,CFSTR(PLUGIN_ID)); + result = AllocQuickLookGeneratorPluginType(uuid); + CFRelease(uuid); + return result; + } + /* If the requested type is incorrect, return NULL. */ + return NULL; +} + diff --git a/extensions/kde/CMakeLists.txt b/extensions/kde/CMakeLists.txt new file mode 100644 index 000000000..33984ed30 --- /dev/null +++ b/extensions/kde/CMakeLists.txt @@ -0,0 +1,2 @@ +set(SERVICES_INSTALL_DIR "${CMAKE_INSTALL_DATADIR}/kservices5" CACHE PATH "Install dir for KDE service (desktop, protocol, ...) files") +install(FILES qdigidoc-signer.desktop DESTINATION "${SERVICES_INSTALL_DIR}") diff --git a/extensions/kde/qdigidoc-signer.desktop b/extensions/kde/qdigidoc-signer.desktop new file mode 100644 index 000000000..070cc3563 --- /dev/null +++ b/extensions/kde/qdigidoc-signer.desktop @@ -0,0 +1,17 @@ +[Desktop Entry] +X-KDE-ServiceTypes=KonqPopupMenu/Plugin,all/allfiles +Actions=sign; +Encoding=UTF-8 +Type=Service +X-KDE-Priority=TopLevel +X-KDE-PluginInfo-Name=digidoc-signer +X-KDE-PluginInfo-Author=Erkko Kebbinau +X-KDE-PluginInfo-Email=erkko@smartlink.ee +X-KDE-PluginInfo-Version=1.0 + +[Desktop Action sign] +Name=Sign digitally +Name[et]=Allkirjasta digitaalselt +Name[ru]=Подписать дигитально +Icon=qdigidoc4 +Exec=qdigidoc4 -sign %U diff --git a/extensions/nautilus/CMakeLists.txt b/extensions/nautilus/CMakeLists.txt new file mode 100644 index 000000000..b7c0e1ef3 --- /dev/null +++ b/extensions/nautilus/CMakeLists.txt @@ -0,0 +1,6 @@ +find_package(Gettext) + +file(GLOB PO_FILES po/*.po) +gettext_create_translations(po/nautilus-qdigidoc.pot ALL ${PO_FILES}) + +install(FILES nautilus-qdigidoc.py DESTINATION ${CMAKE_INSTALL_DATADIR}/nautilus-python/extensions/) diff --git a/extensions/nautilus/nautilus-qdigidoc.py b/extensions/nautilus/nautilus-qdigidoc.py new file mode 100644 index 000000000..dd510ab68 --- /dev/null +++ b/extensions/nautilus/nautilus-qdigidoc.py @@ -0,0 +1,117 @@ +# +# QDigiDoc Nautilus Extension +# +# Copyright (C) 2010 Erkko Kebbinau +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# +import os +import gettext +import locale +import gi + +try: + # Python 3. + from urllib.parse import unquote +except ImportError: + from urllib import unquote + +from gi.repository import Nautilus, GObject, Gio +if hasattr(Nautilus, "LocationWidgetProvider"): + gi.require_version('Nautilus', '3.0') + +APP = 'nautilus-qdigidoc' + +locale.setlocale(locale.LC_ALL, '') +gettext.bindtextdomain(APP) +gettext.textdomain(APP) + +class OpenDigidocExtension(GObject.GObject, Nautilus.MenuProvider): + def __init__(self): + super().__init__() + + def menu_activate_cb(self, menu, paths): + args = "-sign " + for path in paths: + args += "\"%s\" " % path + cmd = ("qdigidoc4 " + args + "&") + os.system(cmd) + + def valid_file(self, file): + return file.get_file_type() == Gio.FileType.REGULAR and file.get_uri_scheme() == 'file' + + def get_file_items(self, *args): + paths = [] + files = args[-1] + for file in files: + if self.valid_file(file): + path = unquote(file.get_uri()[7:]) + paths.append(path) + + if len(paths) < 1: + return [] + + item = Nautilus.MenuItem( + name="OpenDigidocExtension::DigidocSigner", + label=gettext.gettext('Sign digitally'), + tip=gettext.ngettext('Sign selected file with DigiDoc4 Client', + 'Sign selected files with DigiDoc4 Client', + len(paths)), + icon='qdigidoc4' + ) + item.connect('activate', self.menu_activate_cb, paths) + return [item] + + def get_background_items(self, current_folder): + return [] + +class OpenCryptoExtension(GObject.GObject, Nautilus.MenuProvider): + def __init__(self): + super().__init__() + + def menu_activate_cb(self, menu, paths): + args = "-crypto " + for path in paths: + args += "\"%s\" " % path + cmd = ("qdigidoc4 " + args + "&") + os.system(cmd) + + def valid_file(self, file): + return file.get_file_type() == Gio.FileType.REGULAR and file.get_uri_scheme() == 'file' + + def get_file_items(self, *args): + paths = [] + files = args[-1] + for file in files: + if self.valid_file(file): + path = unquote(file.get_uri()[7:]) + paths.append(path) + + if len(paths) < 1: + return [] + + item = Nautilus.MenuItem( + name="OpenCryptoExtension::DigidocEncrypter", + label=gettext.gettext('Encrypt files'), + tip=gettext.ngettext('Encrypt selected file with DigiDoc4 Client', + 'Encrypt selected files with DigiDoc4 Client', + len(paths)), + icon='qdigidoc4' + ) + item.connect('activate', self.menu_activate_cb, paths) + return [item] + + def get_background_items(self, current_folder): + return [] diff --git a/extensions/nautilus/po/et.po b/extensions/nautilus/po/et.po new file mode 100644 index 000000000..8f9161ced --- /dev/null +++ b/extensions/nautilus/po/et.po @@ -0,0 +1,36 @@ +# Estonian translations for qdigidoc package. +# This file is distributed under the same license as the qdigidoc package. +# Erkko Kebbinau , 2010. +# Sander Lepik , 2014. +# +msgid "" +msgstr "" +"Project-Id-Version: qdigidoc-3.9.1\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-08-25 15:51+0300\n" +"PO-Revision-Date: 2014-11-11 14:23+0200\n" +"Last-Translator: Sander Lepik \n" +"Language-Team: Estonian\n" +"Language: et\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Encrypt files" +msgstr "Krüpteeri failid" + +msgid "Encrypt selected file with DigiDoc4 Client" +msgid_plural "Encrypt selected files with DigiDoc4 Client" +msgstr[0] "Krüpteeri valitud fail DigiDoc4 kliendiga" +msgstr[1] "Krüpteeri valitud failid DigiDoc4 kliendiga" + +#: nautilus-qdigidoc.py:77 +msgid "Sign digitally" +msgstr "Allkirjasta digitaalselt" + +#: nautilus-qdigidoc.py:74 +msgid "Sign selected file with DigiDoc4 Client" +msgid_plural "Sign selected files with DigiDoc4 Client" +msgstr[0] "Allkirjasta valitud fail DigiDoc4 kliendiga" +msgstr[1] "Allkirjasta valitud failid DigiDoc4 kliendiga" diff --git a/extensions/nautilus/po/nautilus-qdigidoc.pot b/extensions/nautilus/po/nautilus-qdigidoc.pot new file mode 100644 index 000000000..bba16a696 --- /dev/null +++ b/extensions/nautilus/po/nautilus-qdigidoc.pot @@ -0,0 +1,37 @@ +# Translation template for qdigidoc package. +# This file is distributed under the same license as the qdigidoc package. +# Erkko Kebbinau , 2010. +# Sander Lepik , 2014. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-08-25 15:51+0300\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" + +#: nautilus-qdigidoc.py:74 +msgid "Sign selected file with DigiDoc4 Client" +msgid_plural "Sign selected files with DigiDoc4 Client" +msgstr[0] "" +msgstr[1] "" + +#: nautilus-qdigidoc.py:77 +msgid "Sign digitally" +msgstr "" + +msgid "Encrypt selected file with DigiDoc4 Client" +msgid_plural "Encrypt selected files with DigiDoc4 Client" +msgstr[0] "" +msgstr[1] "" + +msgid "Encrypt files" +msgstr "" \ No newline at end of file diff --git a/extensions/nautilus/po/ru.po b/extensions/nautilus/po/ru.po new file mode 100644 index 000000000..4117a19fb --- /dev/null +++ b/extensions/nautilus/po/ru.po @@ -0,0 +1,40 @@ +# Russian translations for qdigidoc package. +# This file is distributed under the same license as the qdigidoc package. +# Erkko Kebbinau , 2010. +# Sander Lepik , 2014. +# Edmund Laugasson , 2020. +# +msgid "" +msgstr "" +"Project-Id-Version: qdigidoc-3.9.1\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2010-08-25 15:51+0300\n" +"PO-Revision-Date: 2020-08-18 00:14+0300\n" +"Last-Translator: Edmund Laugasson \n" +"Language-Team: Russian\n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +msgid "Encrypt files" +msgstr "Зашифруйте файлы" + +msgid "Encrypt selected file with DigiDoc4 Client" +msgid_plural "Encrypt selected files with DigiDoc4 Client" +msgstr[0] "Зашифруйте выбранный файл с клиентом DigiDoc4" +msgstr[1] "Зашифруйте выбранныйые файлы с клиентом DigiDoc4" +msgstr[2] "Зашифруйте выбранныйые файлы с клиентом DigiDoc4" + +#: nautilus-qdigidoc.py:77 +msgid "Sign digitally" +msgstr "Подпишите дигитально" + +#: nautilus-qdigidoc.py:74 +msgid "Sign selected file with DigiDoc4 Client" +msgid_plural "Sign selected files with DigiDoc4 Client" +msgstr[0] "Подпишите выбранный файл с клиентом DigiDoc4" +msgstr[1] "Подпишите выбранныйые файлы с клиентом DigiDoc4" +msgstr[2] "Подпишите выбранныйые файлы с клиентом DigiDoc4" diff --git a/extensions/nautilus/po/update-po.sh b/extensions/nautilus/po/update-po.sh new file mode 100644 index 000000000..8d5c1df3c --- /dev/null +++ b/extensions/nautilus/po/update-po.sh @@ -0,0 +1,30 @@ +#!/bin/sh + +# Copyright (C) 2010 Erkko Kebbinau +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc, 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +# Generate .pot file: +pushd .. +xgettext -k_ -kN_ nautilus-qdigidoc.py --output=po/nautilus-qdigidoc.pot +popd + +# Fix up charset +sed -i -e '/Content-Type/ s/CHARSET/UTF-8/' nautilus-qdigidoc.pot + +# Update po files: +for f in *.po ; do + msgmerge -U "$f" nautilus-qdigidoc.pot +done diff --git a/extensions/windows/CMakeLists.txt b/extensions/windows/CMakeLists.txt new file mode 100644 index 000000000..7845191e5 --- /dev/null +++ b/extensions/windows/CMakeLists.txt @@ -0,0 +1,84 @@ +cmake_minimum_required(VERSION 3.16) +project(digidocshellextension VERSION 3.13.9) + +set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/modules) +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +include( VersionInfo ) + +if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(MIDL_TARGET "x64") + set(PLATFORM "x64") +else() + set(MIDL_TARGET "win32") + set(PLATFORM "x86") +endif() + +add_custom_command( + OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/EsteidShellExtension_i.h + ${CMAKE_CURRENT_BINARY_DIR}/EsteidShellExtension_i.c + ${CMAKE_CURRENT_BINARY_DIR}/EsteidShellExtension_p.c + COMMAND Midl.Exe ${CMAKE_CURRENT_SOURCE_DIR}/EsteidShellExtension.idl + /nologo /no_robust /char signed /Oicf /env ${MIDL_TARGET} + /I ${CMAKE_CURRENT_SOURCE_DIR} + /tlb EsteidShellExtension.tlb + /h EsteidShellExtension_i.h + /iid EsteidShellExtension_i.c + /proxy EsteidShellExtension_p.c 2> nul + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} + VERBATIM +) + +add_library(EsteidShellExtension SHARED + ${CMAKE_CURRENT_BINARY_DIR}/EsteidShellExtension_i.c + dllmain.cpp + EsteidShellExtension.cpp + EsteidShellExtension.def + EsteidShlExt.cpp + stdafx.cpp + EsteidShellExtension.rc + EsteidShellExtension.rgs + EsteidShlExt_x86.rgs + EsteidShlExt_x64.rgs +) +set_target_properties(EsteidShellExtension PROPERTIES + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>" + COMPILE_DEFINITIONS "_UNICODE;UNICODE;_MERGE_PROXYSTUB;_WINDLL" + COMPILE_OPTIONS "/guard:cf" + INCLUDE_DIRECTORIES ${CMAKE_CURRENT_BINARY_DIR} + INTERPROCEDURAL_OPTIMIZATION YES + LINK_OPTIONS "/guard:cf" + LINK_LIBRARIES "uxtheme.lib" + SKIP_AUTOMOC ON +) + +add_custom_target(msishellext DEPENDS EsteidShellExtension + COMMAND "$ENV{WIX}bin\\candle.exe" -nologo -arch ${PLATFORM} + -dMSI_VERSION=${VERSION} -dShellExt=$ + ${CMAKE_CURRENT_SOURCE_DIR}/EsteidShellExtension.wxs + ${CMAKE_MODULE_PATH}/WelcomeDlg2.wxs + ${CMAKE_MODULE_PATH}/WixUI_Minimal2.wxs + COMMAND "$ENV{WIX}bin\\light.exe" -nologo -ext WixUIExtension + EsteidShellExtension.wixobj WelcomeDlg2.wixobj WixUI_Minimal2.wixobj + -dWixUIDialogBmp=${CMAKE_MODULE_PATH}/dlgbmp.bmp + -dWixUIBannerBmp=${CMAKE_MODULE_PATH}/banner.bmp + -o "Digidoc_ShellExt-${VERSION}$ENV{VER_SUFFIX}.${PLATFORM}.msi" + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} +) + +if(SIGNCERT) + if(CROSSSIGNCERT) + target_link_options(EsteidShellExtension PRIVATE "/INTEGRITYCHECK") + endif() + add_custom_command(TARGET EsteidShellExtension POST_BUILD + COMMAND signtool.exe sign /a /v /s MY /n "${SIGNCERT}" /fd SHA256 /du http://installer.id.ee + "$<$:/ph;/ac;${CROSSSIGNCERT}>" + /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp /td SHA256 $ + COMMAND_EXPAND_LISTS + ) + add_custom_command(TARGET msishellext POST_BUILD + COMMAND signtool.exe sign /a /v /s MY /n "${SIGNCERT}" /fd SHA256 /du http://installer.id.ee + /tr http://sha256timestamp.ws.symantec.com/sha256/timestamp /td SHA256 + "${CMAKE_BINARY_DIR}/Digidoc_ShellExt-${VERSION}$ENV{VER_SUFFIX}.${PLATFORM}.msi" + ) +endif() diff --git a/extensions/windows/EsteidShellExtension.cpp b/extensions/windows/EsteidShellExtension.cpp new file mode 100644 index 000000000..3de907dd0 --- /dev/null +++ b/extensions/windows/EsteidShellExtension.cpp @@ -0,0 +1,59 @@ +// EsteidShellExtension.cpp : Implementation of DLL Exports. + + +#include "stdafx.h" +#include "EsteidShellExtension_i.h" +#include "dllmain.h" + +// Used to determine whether the DLL can be unloaded by OLE +STDAPI DllCanUnloadNow(void) +{ + return _AtlModule.DllCanUnloadNow(); +} + + +// Returns a class factory to create an object of the requested type +STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv) +{ + return _AtlModule.DllGetClassObject(rclsid, riid, ppv); +} + + +// DllRegisterServer - Adds entries to the system registry +STDAPI DllRegisterServer(void) +{ + // registers object, typelib and all interfaces in typelib + HRESULT hr = _AtlModule.DllRegisterServer(); + return hr; +} + + +// DllUnregisterServer - Removes entries from the system registry +STDAPI DllUnregisterServer(void) +{ + HRESULT hr = _AtlModule.DllUnregisterServer(); + return hr; +} + +// DllInstall - Adds/Removes entries to the system registry per user +// per machine. +STDAPI DllInstall(BOOL bInstall, LPCWSTR pszCmdLine) +{ + HRESULT hr = E_FAIL; + static const wchar_t szUserSwitch[] = _T("user"); + + if (pszCmdLine != NULL) { + if (_wcsnicmp(pszCmdLine, szUserSwitch, _countof(szUserSwitch)) == 0) + AtlSetPerUserRegistration(true); + } + + if (bInstall) { + hr = DllRegisterServer(); + if (FAILED(hr)) + DllUnregisterServer(); + } else { + hr = DllUnregisterServer(); + } + + return hr; +} diff --git a/extensions/windows/EsteidShellExtension.def b/extensions/windows/EsteidShellExtension.def new file mode 100644 index 000000000..d606fda47 --- /dev/null +++ b/extensions/windows/EsteidShellExtension.def @@ -0,0 +1,10 @@ +; EsteidShellExtension.def : Declares the module parameters. + +LIBRARY "EsteidShellExtension.DLL" + +EXPORTS + DllCanUnloadNow PRIVATE + DllGetClassObject PRIVATE + DllRegisterServer PRIVATE + DllUnregisterServer PRIVATE + DllInstall PRIVATE diff --git a/extensions/windows/EsteidShellExtension.idl b/extensions/windows/EsteidShellExtension.idl new file mode 100644 index 000000000..92361f614 --- /dev/null +++ b/extensions/windows/EsteidShellExtension.idl @@ -0,0 +1,40 @@ +// EsteidShellExtension.idl : IDL source for EsteidShellExtension +// + +// This file will be processed by the MIDL tool to +// produce the type library (EsteidShellExtension.tlb) and marshalling code. + +import "oaidl.idl"; +import "ocidl.idl"; + +[ + object, + uuid(8BD7CE13-2DB7-4268-8201-CED0626CB94E), + dual, + nonextensible, + helpstring("IEsteidShlExt Interface"), + pointer_default(unique) +] +interface IEsteidShlExt : IDispatch{ +}; +[ + uuid(F4748FA8-B59E-43FA-9D53-2380EB141AED), + version(1.0), + helpstring("EsteidShellExtension 1.0 Type Library") +] +library EsteidShellExtensionLib +{ + importlib("stdole2.tlb"); + [ +#ifdef _WIN64 + uuid(5606A547-759D-43DA-AEEB-D3BF1D1E816D), +#else + uuid(310AAB39-76FE-401B-8A7F-0F578C5F6AB5), +#endif + helpstring("EsteidShlExt Class") + ] + coclass EsteidShlExt + { + [default] interface IEsteidShlExt; + }; +}; diff --git a/extensions/windows/EsteidShellExtension.rc b/extensions/windows/EsteidShellExtension.rc new file mode 100644 index 000000000..1dc252cb9 --- /dev/null +++ b/extensions/windows/EsteidShellExtension.rc @@ -0,0 +1,155 @@ +// Microsoft Visual C++ generated resource script. +// +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#ifndef APSTUDIO_INVOKED +#include "targetver.h" +#endif +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#ifndef APSTUDIO_INVOKED\r\n" + "#include ""targetver.h""\r\n" + "#endif\r\n" + "#include ""winres.h""\r\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#define VER_STR_HELPER(x) #x +#define VER_STR(x) VER_STR_HELPER(x) + +VS_VERSION_INFO VERSIONINFO + FILEVERSION MAJOR_VER,MINOR_VER,RELEASE_VER,BUILD_VER + PRODUCTVERSION MAJOR_VER,MINOR_VER,RELEASE_VER,BUILD_VER + FILEFLAGSMASK 0x3fL +#ifdef _DEBUG + FILEFLAGS 0x1L +#else + FILEFLAGS 0x0L +#endif + FILEOS 0x4L + FILETYPE 0x2L + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "RIA" + VALUE "FileDescription", "Estonian ID Card Software explorer integration" + VALUE "FileVersion", VER_STR(MAJOR_VER.MINOR_VER.RELEASE_VER.BUILD_VER) + VALUE "InternalName", "EsteidShellExtension.dll" + VALUE "LegalCopyright", "(c) RIA. All rights reserved." + VALUE "OriginalFilename", "EsteidShellExtension.dll" + VALUE "ProductName", "Estonian ID Card Software" + VALUE "ProductVersion", VER_STR(MAJOR_VER.MINOR_VER.RELEASE_VER.BUILD_VER) + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + + +///////////////////////////////////////////////////////////////////////////// +// +// REGISTRY +// + +IDR_ESTEIDEXT REGISTRY "EsteidShellExtension.rgs" + + +///////////////////////////////////////////////////////////////////////////// +// +// String Table +// + +STRINGTABLE +BEGIN + IDS_PROJNAME "EsteidShellExtension" +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + +///////////////////////////////////////////////////////////////////////////// +// Estonian (Estonia) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ETI) +LANGUAGE LANG_ESTONIAN, SUBLANG_DEFAULT +#pragma code_page(1257) + +///////////////////////////////////////////////////////////////////////////// +// +// REGISTRY +// + +#ifdef _WIN64 +IDR_ESTEIDSHLEXT REGISTRY "EsteidShlExt_x64.rgs" +#else +IDR_ESTEIDSHLEXT REGISTRY "EsteidShlExt_x86.rgs" +#endif + +///////////////////////////////////////////////////////////////////////////// +// +// Bitmap +// + +IDB_DIGIDOCICO ICON "resources\\digidoc.ico" + +#endif // Estonian (Estonia) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/extensions/windows/EsteidShellExtension.rgs b/extensions/windows/EsteidShellExtension.rgs new file mode 100644 index 000000000..1da405ca5 --- /dev/null +++ b/extensions/windows/EsteidShellExtension.rgs @@ -0,0 +1,11 @@ +HKCR +{ + NoRemove AppID + { + '%APPID%' = s 'EsteidShellExtension' + 'EsteidShellExtension.DLL' + { + val AppID = s '%APPID%' + } + } +} diff --git a/extensions/windows/EsteidShellExtension.wxs b/extensions/windows/EsteidShellExtension.wxs new file mode 100644 index 000000000..5fbe4068f --- /dev/null +++ b/extensions/windows/EsteidShellExtension.wxs @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + = 601)]]> + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/extensions/windows/EsteidShlExt.cpp b/extensions/windows/EsteidShlExt.cpp new file mode 100644 index 000000000..78e9c38e5 --- /dev/null +++ b/extensions/windows/EsteidShlExt.cpp @@ -0,0 +1,324 @@ +// EsteidShlExt.cpp : Implementation of CEsteidShlExt +// http://msdn.microsoft.com/en-us/library/bb757020.aspx + +#include "stdafx.h" +#include "EsteidShlExt.h" + +#include + +typedef DWORD ARGB; + +bool HasAlpha(ARGB *pargb, SIZE &sizeImage, int cxRow) +{ + ULONG cxDelta = cxRow - sizeImage.cx; + for(ULONG y = sizeImage.cy; y; --y) + { + for(ULONG x = sizeImage.cx; x; --x) + { + if(*pargb++ & 0xFF000000) + return true; + } + pargb += cxDelta; + } + return false; +} + +BITMAPINFO InitBitmapInfo(const SIZE &sizeImage) +{ + BITMAPINFO pbmi = {}; + pbmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + pbmi.bmiHeader.biPlanes = 1; + pbmi.bmiHeader.biCompression = BI_RGB; + + pbmi.bmiHeader.biWidth = sizeImage.cx; + pbmi.bmiHeader.biHeight = sizeImage.cy; + pbmi.bmiHeader.biBitCount = 32; + return pbmi; +} + +HBITMAP Create32BitHBITMAP(HDC hdc, const SIZE &sizeImage, void **ppvBits) +{ + BITMAPINFO bmi = InitBitmapInfo(sizeImage); + if (HDC hdcUsed = hdc ? hdc : GetDC(nullptr)) + { + HBITMAP phBmp = CreateDIBSection(hdcUsed, &bmi, DIB_RGB_COLORS, ppvBits, nullptr, 0); + if (hdc != hdcUsed) + ReleaseDC(NULL, hdcUsed); + return phBmp; + } + return nullptr; +} + +HRESULT ConvertToPARGB32(HDC hdc, ARGB *pargb, HBITMAP hbmp, SIZE &sizeImage, int cxRow) +{ + BITMAPINFO bmi = InitBitmapInfo(sizeImage); + HRESULT hr = E_OUTOFMEMORY; + HANDLE hHeap = GetProcessHeap(); + if (void *pvBits = HeapAlloc(hHeap, 0, bmi.bmiHeader.biWidth * 4 * bmi.bmiHeader.biHeight)) + { + hr = E_UNEXPECTED; + if (GetDIBits(hdc, hbmp, 0, bmi.bmiHeader.biHeight, pvBits, &bmi, DIB_RGB_COLORS) == bmi.bmiHeader.biHeight) + { + ULONG cxDelta = cxRow - bmi.bmiHeader.biWidth; + ARGB *pargbMask = static_cast(pvBits); + for (ULONG y = bmi.bmiHeader.biHeight; y; --y) + { + for (ULONG x = bmi.bmiHeader.biWidth; x; --x) + { + if (*pargbMask++) // transparent pixel + *pargb++ = 0; + else // opaque pixel + *pargb++ |= 0xFF000000; + } + pargb += cxDelta; + } + hr = S_OK; + } + HeapFree(hHeap, 0, pvBits); + } + return hr; +} + +HRESULT ConvertBufferToPARGB32(HPAINTBUFFER hPaintBuffer, HDC hdc, HICON hicon, SIZE &sizeIcon) +{ + RGBQUAD *prgbQuad; + int cxRow = 0; + HRESULT hr = GetBufferedPaintBits(hPaintBuffer, &prgbQuad, &cxRow); + if (SUCCEEDED(hr)) + { + ARGB *pargb = reinterpret_cast(prgbQuad); + if (!HasAlpha(pargb, sizeIcon, cxRow)) + { + ICONINFO info = {}; + if (GetIconInfo(hicon, &info)) + { + if (info.hbmMask) + hr = ConvertToPARGB32(hdc, pargb, info.hbmMask, sizeIcon, cxRow); + DeleteObject(info.hbmColor); + DeleteObject(info.hbmMask); + } + } + } + return hr; +} + +CEsteidShlExt::CEsteidShlExt() +{ + SIZE sizeIcon = { GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON) }; + if(HICON hIcon = (HICON)LoadImage(_AtlBaseModule.GetModuleInstance(), MAKEINTRESOURCE(IDB_DIGIDOCICO), IMAGE_ICON, sizeIcon.cx, sizeIcon.cy, LR_DEFAULTCOLOR|LR_CREATEDIBSECTION)) + { + if(HDC hdcDest = CreateCompatibleDC(nullptr)) { + if((m_DigidocBmp = Create32BitHBITMAP(hdcDest, sizeIcon, nullptr))) { + if(HBITMAP hbmpOld = (HBITMAP)SelectObject(hdcDest, m_DigidocBmp)) { + RECT rcIcon = { 0, 0, sizeIcon.cx, sizeIcon.cy }; + BLENDFUNCTION bfAlpha = { AC_SRC_OVER, 0, 255, AC_SRC_ALPHA }; + BP_PAINTPARAMS paintParams = { sizeof(paintParams), BPPF_ERASE, nullptr, &bfAlpha }; + HDC hdcBuffer; + if(HPAINTBUFFER hPaintBuffer = BeginBufferedPaint(hdcDest, &rcIcon, BPBF_DIB, &paintParams, &hdcBuffer)) { + if(DrawIconEx(hdcBuffer, 0, 0, hIcon, sizeIcon.cx, sizeIcon.cy, 0, nullptr, DI_NORMAL)) { + // If icon did not have an alpha channel, we need to convert buffer to PARGB. + ConvertBufferToPARGB32(hPaintBuffer, hdcDest, hIcon, sizeIcon); + } + EndBufferedPaint(hPaintBuffer, TRUE); + } + SelectObject(hdcDest, hbmpOld); + } + } + DeleteDC(hdcDest); + } + DestroyIcon(hIcon); + } +} + +CEsteidShlExt::~CEsteidShlExt() +{ + DeleteObject(m_DigidocBmp); +} + +STDMETHODIMP CEsteidShlExt::Initialize( + LPCITEMIDLIST /* pidlFolder */, LPDATAOBJECT pDataObj, HKEY /* hProgID */) +{ + FORMATETC fmt = { CF_HDROP, nullptr, DVASPECT_CONTENT, -1, TYMED_HGLOBAL }; + STGMEDIUM stg = { TYMED_HGLOBAL }; + m_Files.clear(); + + // Look for CF_HDROP data in the data object. + if (FAILED(pDataObj->GetData(&fmt, &stg))) { + // Nope! Return an "invalid argument" error back to Explorer. + return E_INVALIDARG; + } + + // Get a pointer to the actual data. + HDROP hDrop = HDROP(GlobalLock(stg.hGlobal)); + if (!hDrop) { + ReleaseStgMedium(&stg); + return E_INVALIDARG; + } + + // Sanity check - make sure there is at least one filename. + UINT nFiles = DragQueryFile(hDrop, 0xFFFFFFFF, nullptr, 0); + if (nFiles == 0) { + GlobalUnlock(stg.hGlobal); + ReleaseStgMedium(&stg); + return E_INVALIDARG; + } + + for (UINT i = 0; i < nFiles; i++) { + // Get path length in chars + UINT len = DragQueryFile(hDrop, i, nullptr, 0); + if (len == 0 || len >= MAX_PATH) + continue; + + // Get the name of the file + TCHAR szFile[MAX_PATH]; + if (DragQueryFile(hDrop, i, szFile, len+1) == 0) + continue; + + tstring str = tstring(szFile); + if (str.empty()) + continue; + + m_Files.push_back(str); + } + + GlobalUnlock(stg.hGlobal); + ReleaseStgMedium(&stg); + + // Don't show menu if no items were found + return m_Files.empty() ? E_INVALIDARG : S_OK; +} + +STDMETHODIMP CEsteidShlExt::QueryContextMenu( + HMENU hmenu, UINT uMenuIndex, UINT uidFirstCmd, + UINT /* uidLastCmd */, UINT uFlags) +{ + // If the flags include CMF_DEFAULTONLY then we shouldn't do anything. + if (uFlags & CMF_DEFAULTONLY) + return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 0); + + PCTCH sign = _T("Sign digitally"); + PCTCH encrypt = _T("Encrypt"); + switch (PRIMARYLANGID(GetUserDefaultUILanguage())) + { + case LANG_ESTONIAN: + sign = _T("Allkirjasta digitaalselt"); + encrypt = _T("Krüpteeri"); + break; + case LANG_RUSSIAN: + sign = _T("Подписать дигитально"); + encrypt = _T("Зашифровать"); + break; + default: break; + } + + InsertMenu(hmenu, uMenuIndex, MF_STRING | MF_BYPOSITION, uidFirstCmd, sign); + if (m_DigidocBmp) + SetMenuItemBitmaps(hmenu, uMenuIndex, MF_BYPOSITION, m_DigidocBmp, nullptr); + InsertMenu(hmenu, uMenuIndex + MENU_ENCRYPT, MF_STRING | MF_BYPOSITION, uidFirstCmd + MENU_ENCRYPT, encrypt); + if (m_DigidocBmp) + SetMenuItemBitmaps(hmenu, uMenuIndex + MENU_ENCRYPT, MF_BYPOSITION, m_DigidocBmp, nullptr); + + return MAKE_HRESULT(SEVERITY_SUCCESS, FACILITY_NULL, 2); +} + +STDMETHODIMP CEsteidShlExt::GetCommandString( + UINT_PTR idCmd, UINT uFlags, UINT * /* pwReserved */, LPSTR pszName, UINT cchMax) +{ + USES_CONVERSION; + + // Check idCmd, it must be 0 or 1 since we have only two menu items. + if (idCmd > MENU_ENCRYPT) + return E_INVALIDARG; + + // If Explorer is asking for a help string, copy our string into the + // supplied buffer. + if (uFlags & GCS_HELPTEXT) { + LPCTSTR szText = idCmd == MENU_SIGN ? _T("Allkirjasta valitud failid digitaalselt") : _T("Krüpteeri valitud failid"); + + if (uFlags & GCS_UNICODE) { + // We need to cast pszName to a Unicode string, and then use the + // Unicode string copy API. + lstrcpynW(LPWSTR(pszName), T2CW(szText), int(cchMax)); + } else { + // Use the ANSI string copy API to return the help string. + lstrcpynA(pszName, T2CA(szText), int(cchMax)); + } + + return S_OK; + } + + return E_INVALIDARG; +} + +bool WINAPI CEsteidShlExt::FindRegistryInstallPath(tstring* path) +{ + static PCTCH IDCARD_REGKEY = _T("SOFTWARE\\RIA\\Open-EID"); + static PCTCH IDCARD_REGVALUE = _T("Installed"); + HKEY hkey; + DWORD dwSize = MAX_PATH * sizeof(TCHAR); + TCHAR szInstalldir[MAX_PATH]; + LSTATUS dwRet = RegOpenKeyEx(HKEY_LOCAL_MACHINE, IDCARD_REGKEY, 0, KEY_QUERY_VALUE, &hkey); + if (dwRet == ERROR_SUCCESS) { + dwRet = RegQueryValueEx(hkey, IDCARD_REGVALUE, nullptr, nullptr, LPBYTE(szInstalldir), &dwSize); + RegCloseKey(hkey); + *path = tstring(szInstalldir); + return true; + } + dwRet = RegOpenKeyEx(HKEY_CURRENT_USER, IDCARD_REGKEY, 0, KEY_QUERY_VALUE, &hkey); + if (dwRet == ERROR_SUCCESS) { + RegCloseKey(hkey); + *path = tstring(szInstalldir); + return true; + } + return false; +} + +STDMETHODIMP CEsteidShlExt::ExecuteDigidocclient(LPCMINVOKECOMMANDINFO /* pCmdInfo */, bool crypto) +{ + if (m_Files.empty()) + return E_INVALIDARG; + + tstring path(MAX_PATH, 0); + tstring command(MAX_PATH, 0); + + // Read the location of the installation from registry + if (!FindRegistryInstallPath(&path)) { + // .. and fall back to directory where shellext resides if not found from registry + GetModuleFileName(_AtlBaseModule.m_hInst, &path[0], MAX_PATH); + path.resize(path.find_last_of(_T('\\')) + 1); + } + + command = path + _T("qdigidoc4.exe"); + if(PathFileExists(command.c_str()) != 1) { + // Replace "c:\Program Files\" with "c:\Program Files (x86)\" + command.insert(16, _T(" (x86)")); + } + + // Construct command line arguments to pass to qdigidocclient.exe + tstring parameters = crypto ? _T("\"-crypto\" ") : _T("\"-sign\" "); + for (const tstring &file: m_Files) + parameters += _T("\"") + file + _T("\" "); + + SHELLEXECUTEINFO seInfo = { sizeof(SHELLEXECUTEINFO) }; + seInfo.lpFile = command.c_str(); + seInfo.lpParameters = parameters.c_str(); + seInfo.nShow = SW_SHOW; + return ShellExecuteEx(&seInfo) ? S_OK : S_FALSE; +} + +STDMETHODIMP CEsteidShlExt::InvokeCommand(LPCMINVOKECOMMANDINFO pCmdInfo) +{ + // If lpVerb really points to a string, ignore this function call and bail out. + if (HIWORD(pCmdInfo->lpVerb) != 0) + return E_INVALIDARG; + + // Get the command index - the valid ones are 0 and 1. + switch (LOWORD(pCmdInfo->lpVerb)) { + case MENU_SIGN: + return ExecuteDigidocclient(pCmdInfo); + case MENU_ENCRYPT: + return ExecuteDigidocclient(pCmdInfo, true); + default: + return E_INVALIDARG; + } +} diff --git a/extensions/windows/EsteidShlExt.h b/extensions/windows/EsteidShlExt.h new file mode 100644 index 000000000..b4caba6a9 --- /dev/null +++ b/extensions/windows/EsteidShlExt.h @@ -0,0 +1,54 @@ +// EsteidShlExt.h : Declaration of the CEsteidShlExt + +#pragma once +#include "resource.h" // main symbols + +#include "EsteidShellExtension_i.h" + +#if defined(_WIN32_WCE) && !defined(_CE_DCOM) && !defined(_CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA) +#error "Single-threaded COM objects are not properly supported on Windows CE platform, such as the Windows Mobile platforms that do not include full DCOM support. Define _CE_ALLOW_SINGLE_THREADED_OBJECTS_IN_MTA to force ATL to support creating single-thread COM object's and allow use of it's single-threaded COM object implementations. The threading model in your rgs file was set to 'Free' as that is the only threading model supported in non DCOM Windows CE platforms." +#endif + +class ATL_NO_VTABLE CEsteidShlExt : + public CComObjectRootEx, + public CComCoClass, + public IShellExtInit, + public IContextMenu +{ +public: + CEsteidShlExt(); + ~CEsteidShlExt(); + + DECLARE_REGISTRY_RESOURCEID(IDR_ESTEIDSHLEXT) + DECLARE_NOT_AGGREGATABLE(CEsteidShlExt) + + BEGIN_COM_MAP(CEsteidShlExt) + COM_INTERFACE_ENTRY(IShellExtInit) + COM_INTERFACE_ENTRY(IContextMenu) + END_COM_MAP() + + DECLARE_PROTECT_FINAL_CONSTRUCT() + + // IShellExtInit + STDMETHODIMP Initialize(LPCITEMIDLIST, LPDATAOBJECT, HKEY) override; + + // IContextMenu + STDMETHODIMP QueryContextMenu(HMENU, UINT, UINT, UINT, UINT) override; + STDMETHODIMP InvokeCommand(LPCMINVOKECOMMANDINFO) override; + STDMETHODIMP GetCommandString(UINT_PTR, UINT, UINT*, LPSTR, UINT) override; + +private: + enum { + MENU_SIGN = 0, + MENU_ENCRYPT = 1, + }; + + using tstring = std::basic_string; + bool WINAPI FindRegistryInstallPath(tstring* path); + STDMETHODIMP ExecuteDigidocclient(LPCMINVOKECOMMANDINFO pCmdInfo, bool crypto = false); + + HBITMAP m_DigidocBmp = nullptr; + std::vector m_Files; +}; + +OBJECT_ENTRY_AUTO(__uuidof(EsteidShlExt), CEsteidShlExt) diff --git a/extensions/windows/EsteidShlExt_x64.rgs b/extensions/windows/EsteidShlExt_x64.rgs new file mode 100644 index 000000000..a77b39827 --- /dev/null +++ b/extensions/windows/EsteidShlExt_x64.rgs @@ -0,0 +1,23 @@ +HKCR +{ + NoRemove CLSID + { + ForceRemove {5606A547-759D-43DA-AEEB-D3BF1D1E816D} = s 'EsteidShlExt Class' + { + InprocServer32 = s '%MODULE%' + { + val ThreadingModel = s 'Apartment' + } + } + } + NoRemove * + { + NoRemove ShellEx + { + NoRemove ContextMenuHandlers + { + ForceRemove EsteidShlExt = s '{5606A547-759D-43DA-AEEB-D3BF1D1E816D}' + } + } + } +} diff --git a/extensions/windows/EsteidShlExt_x86.rgs b/extensions/windows/EsteidShlExt_x86.rgs new file mode 100644 index 000000000..21b81d78c --- /dev/null +++ b/extensions/windows/EsteidShlExt_x86.rgs @@ -0,0 +1,23 @@ +HKCR +{ + NoRemove CLSID + { + ForceRemove {310AAB39-76FE-401B-8A7F-0F578C5F6AB5} = s 'EsteidShlExt Class' + { + InprocServer32 = s '%MODULE%' + { + val ThreadingModel = s 'Apartment' + } + } + } + NoRemove * + { + NoRemove ShellEx + { + NoRemove ContextMenuHandlers + { + ForceRemove EsteidShlExt = s '{310AAB39-76FE-401B-8A7F-0F578C5F6AB5}' + } + } + } +} diff --git a/extensions/windows/dllmain.cpp b/extensions/windows/dllmain.cpp new file mode 100644 index 000000000..c1ac50115 --- /dev/null +++ b/extensions/windows/dllmain.cpp @@ -0,0 +1,15 @@ +// dllmain.cpp : Implementation of DllMain. + +#include "stdafx.h" +#include "resource.h" +#include "EsteidShellExtension_i.h" +#include "dllmain.h" + +CEsteidShellExtensionModule _AtlModule; + +// DLL Entry Point +extern "C" BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved) +{ + hInstance; + return _AtlModule.DllMain(dwReason, lpReserved); +} diff --git a/extensions/windows/dllmain.h b/extensions/windows/dllmain.h new file mode 100644 index 000000000..05f6a30d6 --- /dev/null +++ b/extensions/windows/dllmain.h @@ -0,0 +1,10 @@ +// dllmain.h : Declaration of module class. + +class CEsteidShellExtensionModule : public CAtlDllModuleT< CEsteidShellExtensionModule > +{ +public : + DECLARE_LIBID(LIBID_EsteidShellExtensionLib) + DECLARE_REGISTRY_APPID_RESOURCEID(IDR_ESTEIDEXT, "{08BD50C2-0AFC-4ED0-BC78-78488EDF9E58}") +}; + +extern class CEsteidShellExtensionModule _AtlModule; diff --git a/extensions/windows/resource.h b/extensions/windows/resource.h new file mode 100644 index 000000000..6d15be586 --- /dev/null +++ b/extensions/windows/resource.h @@ -0,0 +1,19 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by EsteidShellExtension.rc +// +#define IDS_PROJNAME 100 +#define IDR_ESTEIDEXT 101 +#define IDR_ESTEIDSHLEXT 102 +#define IDB_DIGIDOCICO 201 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 203 +#define _APS_NEXT_COMMAND_VALUE 32768 +#define _APS_NEXT_CONTROL_VALUE 201 +#define _APS_NEXT_SYMED_VALUE 103 +#endif +#endif diff --git a/extensions/windows/resources/digidoc.ico b/extensions/windows/resources/digidoc.ico new file mode 100644 index 000000000..658ea2e93 Binary files /dev/null and b/extensions/windows/resources/digidoc.ico differ diff --git a/extensions/windows/stdafx.cpp b/extensions/windows/stdafx.cpp new file mode 100644 index 000000000..c77aa0f6c --- /dev/null +++ b/extensions/windows/stdafx.cpp @@ -0,0 +1,5 @@ +// stdafx.cpp : source file that includes just the standard includes +// EsteidShellExtension.pch will be the pre-compiled header +// stdafx.obj will contain the pre-compiled type information + +#include "stdafx.h" diff --git a/extensions/windows/stdafx.h b/extensions/windows/stdafx.h new file mode 100644 index 000000000..fa0489713 --- /dev/null +++ b/extensions/windows/stdafx.h @@ -0,0 +1,26 @@ +// stdafx.h : include file for standard system include files, +// or project specific include files that are used frequently, +// but are changed infrequently + +#pragma once + +#ifndef STRICT +#define STRICT +#endif + +#include "targetver.h" + +#define _ATL_APARTMENT_THREADED +#define _ATL_NO_AUTOMATIC_NAMESPACE + +#define _ATL_CSTRING_EXPLICIT_CONSTRUCTORS // some CString constructors will be explicit + +#include "resource.h" +#include +#include +#include +#include +#include +#include + +using namespace ATL; diff --git a/extensions/windows/targetver.h b/extensions/windows/targetver.h new file mode 100644 index 000000000..5f1a19420 --- /dev/null +++ b/extensions/windows/targetver.h @@ -0,0 +1,23 @@ +#pragma once + +// The following macros define the minimum required platform. The minimum required platform +// is the earliest version of Windows, Internet Explorer etc. that has the necessary features to run +// your application. The macros work by enabling all features available on platform versions up to and +// including the version specified. + +#ifndef WINVER +#define WINVER 0x0A00 +#endif + +#ifndef _WIN32_WINNT +#define _WIN32_WINNT 0x0A00 +#endif + +#ifndef _WIN32_WINDOWS +#define _WIN32_WINDOWS 0x0A00 +#endif + +#ifndef _WIN32_IE +#define _WIN32_IE 0x0A00 +#endif +