1/* -*- Mode: c++; tab-width: 2; indent-tabs-mode: nil; -*- */ 2/* This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6#import <Cocoa/Cocoa.h> 7 8#include "nsMacSharingService.h" 9 10#include "jsapi.h" 11#include "js/Array.h" // JS::NewArrayObject 12#include "js/PropertyAndElement.h" // JS_SetElement, JS_SetProperty 13#include "nsCocoaUtils.h" 14#include "mozilla/MacStringHelpers.h" 15 16NS_IMPL_ISUPPORTS(nsMacSharingService, nsIMacSharingService) 17 18NSString* const remindersServiceName = @"com.apple.reminders.RemindersShareExtension"; 19 20// These are some undocumented constants also used by Safari 21// to let us open the preferences window 22NSString* const extensionPrefPanePath = @"/System/Library/PreferencePanes/Extensions.prefPane"; 23const UInt32 openSharingSubpaneDescriptorType = 'ptru'; 24NSString* const openSharingSubpaneActionKey = @"action"; 25NSString* const openSharingSubpaneActionValue = @"revealExtensionPoint"; 26NSString* const openSharingSubpaneProtocolKey = @"protocol"; 27NSString* const openSharingSubpaneProtocolValue = @"com.apple.share-services"; 28 29// Expose the id so we can pass reference through to JS and back 30@interface NSSharingService (ExposeName) 31- (id)name; 32@end 33 34// Filter providers that we do not want to expose to the user, because they are duplicates or do not 35// work correctly within the context 36static bool ShouldIgnoreProvider(NSString* aProviderName) { 37 return [aProviderName isEqualToString:@"com.apple.share.System.add-to-safari-reading-list"]; 38} 39 40// Clean up the activity once the share is complete 41@interface SharingServiceDelegate : NSObject <NSSharingServiceDelegate> { 42 NSUserActivity* mShareActivity; 43} 44 45- (void)cleanup; 46 47@end 48 49@implementation SharingServiceDelegate 50 51- (id)initWithActivity:(NSUserActivity*)activity { 52 self = [super init]; 53 mShareActivity = [activity retain]; 54 return self; 55} 56 57- (void)cleanup { 58 [mShareActivity resignCurrent]; 59 [mShareActivity invalidate]; 60 [mShareActivity release]; 61 mShareActivity = nil; 62} 63 64- (void)sharingService:(NSSharingService*)sharingService didShareItems:(NSArray*)items { 65 [self cleanup]; 66} 67 68- (void)sharingService:(NSSharingService*)service 69 didFailToShareItems:(NSArray*)items 70 error:(NSError*)error { 71 [self cleanup]; 72} 73 74- (void)dealloc { 75 [mShareActivity release]; 76 [super dealloc]; 77} 78 79@end 80 81static NSString* NSImageToBase64(const NSImage* aImage) { 82 CGImageRef cgRef = [aImage CGImageForProposedRect:nil context:nil hints:nil]; 83 NSBitmapImageRep* bitmapRep = [[NSBitmapImageRep alloc] initWithCGImage:cgRef]; 84 [bitmapRep setSize:[aImage size]]; 85 NSData* imageData = [bitmapRep representationUsingType:NSPNGFileType properties:@{}]; 86 NSString* base64Encoded = [imageData base64EncodedStringWithOptions:0]; 87 [bitmapRep release]; 88 return [NSString stringWithFormat:@"data:image/png;base64,%@", base64Encoded]; 89} 90 91static void SetStrAttribute(JSContext* aCx, JS::Rooted<JSObject*>& aObj, const char* aKey, 92 NSString* aVal) { 93 nsAutoString strVal; 94 mozilla::CopyCocoaStringToXPCOMString(aVal, strVal); 95 JS::Rooted<JSString*> title(aCx, JS_NewUCStringCopyZ(aCx, strVal.get())); 96 JS::Rooted<JS::Value> attVal(aCx, JS::StringValue(title)); 97 JS_SetProperty(aCx, aObj, aKey, attVal); 98} 99 100nsresult nsMacSharingService::GetSharingProviders(const nsAString& aPageUrl, JSContext* aCx, 101 JS::MutableHandleValue aResult) { 102 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 103 104 NSURL* url = nsCocoaUtils::ToNSURL(aPageUrl); 105 if (!url) { 106 // aPageUrl is not a valid URL. 107 return NS_ERROR_FAILURE; 108 } 109 110 NSArray* sharingService = [NSSharingService sharingServicesForItems:@[ url ]]; 111 int32_t serviceCount = 0; 112 JS::Rooted<JSObject*> array(aCx, JS::NewArrayObject(aCx, 0)); 113 114 for (NSSharingService* currentService in sharingService) { 115 if (ShouldIgnoreProvider([currentService name])) { 116 continue; 117 } 118 JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx)); 119 120 SetStrAttribute(aCx, obj, "name", [currentService name]); 121 SetStrAttribute(aCx, obj, "menuItemTitle", currentService.menuItemTitle); 122 SetStrAttribute(aCx, obj, "image", NSImageToBase64(currentService.image)); 123 124 JS::Rooted<JS::Value> element(aCx, JS::ObjectValue(*obj)); 125 JS_SetElement(aCx, array, serviceCount++, element); 126 } 127 128 aResult.setObject(*array); 129 130 return NS_OK; 131 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE); 132} 133 134NS_IMETHODIMP 135nsMacSharingService::OpenSharingPreferences() { 136 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 137 138 NSURL* prefPaneURL = [NSURL fileURLWithPath:extensionPrefPanePath isDirectory:YES]; 139 NSDictionary* args = @{ 140 openSharingSubpaneActionKey : openSharingSubpaneActionValue, 141 openSharingSubpaneProtocolKey : openSharingSubpaneProtocolValue 142 }; 143 NSData* data = [NSPropertyListSerialization dataWithPropertyList:args 144 format:NSPropertyListXMLFormat_v1_0 145 options:0 146 error:nil]; 147 NSAppleEventDescriptor* descriptor = 148 [[NSAppleEventDescriptor alloc] initWithDescriptorType:openSharingSubpaneDescriptorType 149 data:data]; 150 151 [[NSWorkspace sharedWorkspace] openURLs:@[ prefPaneURL ] 152 withAppBundleIdentifier:nil 153 options:NSWorkspaceLaunchAsync 154 additionalEventParamDescriptor:descriptor 155 launchIdentifiers:NULL]; 156 157 [descriptor release]; 158 159 return NS_OK; 160 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE); 161} 162 163NS_IMETHODIMP 164nsMacSharingService::ShareUrl(const nsAString& aServiceName, const nsAString& aPageUrl, 165 const nsAString& aPageTitle) { 166 NS_OBJC_BEGIN_TRY_BLOCK_RETURN; 167 168 NSString* serviceName = nsCocoaUtils::ToNSString(aServiceName); 169 NSURL* pageUrl = nsCocoaUtils::ToNSURL(aPageUrl); 170 NSString* pageTitle = nsCocoaUtils::ToNSString(aPageTitle); 171 NSSharingService* service = [NSSharingService sharingServiceNamed:serviceName]; 172 173 // Reminders fetch its data from an activity, not the share data 174 if ([[service name] isEqual:remindersServiceName]) { 175 NSUserActivity* shareActivity = 176 [[NSUserActivity alloc] initWithActivityType:NSUserActivityTypeBrowsingWeb]; 177 178 if ([pageUrl.scheme hasPrefix:@"http"]) { 179 [shareActivity setWebpageURL:pageUrl]; 180 } 181 [shareActivity setEligibleForHandoff:NO]; 182 [shareActivity setTitle:pageTitle]; 183 [shareActivity becomeCurrent]; 184 185 // Pass ownership of shareActivity to shareDelegate, which will release the 186 // activity once sharing has completed. 187 SharingServiceDelegate* shareDelegate = 188 [[SharingServiceDelegate alloc] initWithActivity:shareActivity]; 189 [shareActivity release]; 190 191 [service setDelegate:shareDelegate]; 192 [shareDelegate release]; 193 } 194 195 // Twitter likes the the title as an additional share item 196 NSArray* toShare = [[service name] isEqual:NSSharingServiceNamePostOnTwitter] 197 ? @[ pageUrl, pageTitle ] 198 : @[ pageUrl ]; 199 200 [service setSubject:pageTitle]; 201 [service performWithItems:toShare]; 202 203 return NS_OK; 204 205 NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE); 206} 207