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