1// Copyright 2010-2018, Google Inc.
2// All rights reserved.
3//
4// Redistribution and use in source and binary forms, with or without
5// modification, are permitted provided that the following conditions are
6// met:
7//
8//     * Redistributions of source code must retain the above copyright
9// notice, this list of conditions and the following disclaimer.
10//     * Redistributions in binary form must reproduce the above
11// copyright notice, this list of conditions and the following disclaimer
12// in the documentation and/or other materials provided with the
13// distribution.
14//     * Neither the name of Google Inc. nor the names of its
15// contributors may be used to endorse or promote products derived from
16// this software without specific prior written permission.
17//
18// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30#import "base/mac_util.h"
31
32#import <Foundation/Foundation.h>
33
34#include <launch.h>
35#include <CoreFoundation/CoreFoundation.h>
36#include <IOKit/IOKitLib.h>
37
38#include "base/const.h"
39#include "base/logging.h"
40#include "base/scoped_cftyperef.h"
41#include "base/singleton.h"
42#include "base/util.h"
43
44namespace mozc {
45namespace {
46const char kServerDirectory[] =
47    "/Library/Input Methods/" kProductPrefix ".app/Contents/Resources";
48const unsigned char kPrelauncherPath[] =
49    "/Library/Input Methods/" kProductPrefix ".app/Contents/Resources/"
50    kProductPrefix "Prelauncher.app";
51
52#ifdef GOOGLE_JAPANESE_INPUT_BUILD
53const char kProjectPrefix[] =
54    "com.google.inputmethod.Japanese.";
55#elif defined(MOZC_BUILD)
56const char kProjectPrefix[] =
57    "org.mozc.inputmethod.Japanese.";
58#else
59#error Unknown branding
60#endif
61
62// Returns the reference of prelauncher login item.
63// If the prelauncher login item does not exist this function returns nullptr.
64// Otherwise you must release the reference.
65LSSharedFileListItemRef GetPrelauncherLoginItem() {
66  LSSharedFileListItemRef prelauncher_item = nullptr;
67  scoped_cftyperef<CFURLRef> url(
68    CFURLCreateFromFileSystemRepresentation(
69        kCFAllocatorDefault, kPrelauncherPath,
70        strlen((const char *)kPrelauncherPath), true));
71  if (!url.get()) {
72    LOG(ERROR) << "CFURLCreateFromFileSystemRepresentation error:"
73               << " Cannot create CFURL object.";
74    return nullptr;
75  }
76
77  scoped_cftyperef<LSSharedFileListRef> login_items(
78      LSSharedFileListCreate(
79          kCFAllocatorDefault, kLSSharedFileListSessionLoginItems, nullptr));
80  if (!login_items.get()) {
81    LOG(ERROR) << "LSSharedFileListCreate error: Cannot get the login items.";
82    return nullptr;
83  }
84
85  scoped_cftyperef<CFArrayRef> login_items_array(
86      LSSharedFileListCopySnapshot(login_items.get(), nullptr));
87  if (!login_items_array.get()) {
88    LOG(ERROR) << "LSSharedFileListCopySnapshot error:"
89               << " Cannot get the login items.";
90    return nullptr;
91  }
92
93  for(CFIndex i = 0; i < CFArrayGetCount(login_items_array.get()); ++i) {
94    LSSharedFileListItemRef item =
95        reinterpret_cast<LSSharedFileListItemRef>(const_cast<void *>(
96            CFArrayGetValueAtIndex(login_items_array.get(), i)));
97    if (!item) {
98      LOG(ERROR) << "CFArrayGetValueAtIndex error:"
99                 << " Cannot get the login item.";
100      return nullptr;
101    }
102
103    CFURLRef item_url_ref = nullptr;
104    if (LSSharedFileListItemResolve(item, 0, &item_url_ref, nullptr) == noErr) {
105      if (!item_url_ref) {
106        LOG(ERROR) << "LSSharedFileListItemResolve error:"
107                   << " Cannot get the login item url.";
108        return nullptr;
109      }
110      if (CFEqual(item_url_ref, url.get())) {
111        prelauncher_item = item;
112        CFRetain(prelauncher_item);
113      }
114    }
115  }
116
117  return prelauncher_item;
118}
119
120string GetSearchPathForDirectoriesInDomains(NSSearchPathDirectory directory) {
121  string dir;
122  @autoreleasepool {
123    NSArray *paths = NSSearchPathForDirectoriesInDomains(
124        directory, NSUserDomainMask, YES);
125    if ([paths count] > 0) {
126      dir.assign([[paths objectAtIndex:0] fileSystemRepresentation]);
127    }
128  }
129  return dir;
130}
131
132}  // namespace
133
134string MacUtil::GetLabelForSuffix(const string &suffix) {
135  return string(kProjectPrefix) + suffix;
136}
137
138string MacUtil::GetApplicationSupportDirectory() {
139  return GetSearchPathForDirectoriesInDomains(NSApplicationSupportDirectory);
140}
141
142string MacUtil::GetCachesDirectory() {
143  return GetSearchPathForDirectoriesInDomains(NSCachesDirectory);
144}
145
146string MacUtil::GetLoggingDirectory() {
147  string dir;
148  @autoreleasepool {
149    NSArray *paths = NSSearchPathForDirectoriesInDomains(
150        NSLibraryDirectory, NSUserDomainMask, YES);
151    if ([paths count] > 0) {
152      dir.assign(
153          [[[[paths objectAtIndex:0] stringByAppendingPathComponent:@"Logs"]
154             stringByAppendingPathComponent:@kProductPrefix]
155            fileSystemRepresentation]);
156    }
157  }
158  return dir;
159}
160
161string MacUtil::GetOSVersionString() {
162  string version;
163  @autoreleasepool {
164    version.assign([[[NSProcessInfo processInfo] operatingSystemVersionString]
165                    cStringUsingEncoding:NSUTF8StringEncoding]);
166  }
167  return version;
168}
169
170string MacUtil::GetServerDirectory() {
171  return kServerDirectory;
172}
173
174string MacUtil::GetResourcesDirectory() {
175  string result;
176  @autoreleasepool {
177    NSBundle *mainBundle = [NSBundle mainBundle];
178    if (mainBundle) {
179      NSString *resourcePath = [mainBundle resourcePath];
180      if (resourcePath) {
181        result.assign([resourcePath fileSystemRepresentation]);
182      }
183    }
184  }
185  return result;
186}
187
188string MacUtil::GetSerialNumber() {
189  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
190  // Please refer to TN1103 for the details
191  // http://developer.apple.com/library/mac/#technotes/tn/tn1103.html
192  string result;
193  io_service_t platformExpert = IOServiceGetMatchingService(
194      kIOMasterPortDefault, IOServiceMatching("IOPlatformExpertDevice"));
195
196  if (platformExpert) {
197    CFTypeRef serialNumberAsCFString =
198        IORegistryEntryCreateCFProperty(
199            platformExpert, CFSTR(kIOPlatformSerialNumberKey),
200            kCFAllocatorDefault, 0);
201    if (serialNumberAsCFString) {
202      const NSString *serialNumberNSString = reinterpret_cast<const NSString *>(
203          serialNumberAsCFString);
204      result.assign([serialNumberNSString UTF8String]);
205    }
206
207    IOObjectRelease(platformExpert);
208  }
209
210  [pool drain];
211  // Return the empty string if failed.
212  return result;
213}
214
215bool MacUtil::StartLaunchdService(const string &service_name,
216                                  pid_t *pid) {
217  int dummy_pid = 0;
218  if (pid == nullptr) {
219    pid = &dummy_pid;
220  }
221  const string label = GetLabelForSuffix(service_name);
222
223  launch_data_t start_renderer_command =
224      launch_data_alloc(LAUNCH_DATA_DICTIONARY);
225  launch_data_dict_insert(start_renderer_command,
226                          launch_data_new_string(label.c_str()),
227                          LAUNCH_KEY_STARTJOB);
228  launch_data_t result_data = launch_msg(start_renderer_command);
229  launch_data_free(start_renderer_command);
230  if (result_data == nullptr) {
231    LOG(ERROR) << "Failed to launch the specified service";
232    return false;
233  }
234  launch_data_free(result_data);
235
236  // Getting PID by using launch_msg API.
237  launch_data_t get_renderer_info =
238      launch_data_alloc(LAUNCH_DATA_DICTIONARY);
239  launch_data_dict_insert(get_renderer_info,
240                          launch_data_new_string(label.c_str()),
241                          LAUNCH_KEY_GETJOB);
242  launch_data_t renderer_info = launch_msg(get_renderer_info);
243  launch_data_free(get_renderer_info);
244  if (renderer_info == nullptr) {
245    LOG(ERROR) << "Unexpected error: launchd doesn't return the data "
246               << "for the service.";
247    return false;
248  }
249
250  launch_data_t pid_data = launch_data_dict_lookup(
251      renderer_info, LAUNCH_JOBKEY_PID);
252  if (pid_data == nullptr) {
253    LOG(ERROR) <<
254        "Unexpected error: launchd response doesn't have PID";
255    launch_data_free(renderer_info);
256    return false;
257  }
258  *pid = launch_data_get_integer(pid_data);
259  launch_data_free(renderer_info);
260  return true;
261}
262
263bool MacUtil::CheckPrelauncherLoginItemStatus() {
264  scoped_cftyperef<LSSharedFileListItemRef> prelauncher_item(
265      GetPrelauncherLoginItem());
266  return (prelauncher_item.get() != nullptr);
267}
268
269void MacUtil::RemovePrelauncherLoginItem() {
270  scoped_cftyperef<LSSharedFileListItemRef> prelauncher_item(
271      GetPrelauncherLoginItem());
272
273  if (!prelauncher_item.get()) {
274    DLOG(INFO) << "prelauncher_item not found.  Probably not registered yet.";
275    return;
276  }
277  scoped_cftyperef<LSSharedFileListRef> login_items(
278      LSSharedFileListCreate(
279          kCFAllocatorDefault, kLSSharedFileListSessionLoginItems, nullptr));
280  if (!login_items.get()) {
281    LOG(ERROR) << "LSSharedFileListCreate error: Cannot get the login items.";
282    return;
283  }
284  LSSharedFileListItemRemove(login_items.get(), prelauncher_item.get());
285}
286
287void MacUtil::AddPrelauncherLoginItem() {
288  if (CheckPrelauncherLoginItemStatus()) {
289    return;
290  }
291  scoped_cftyperef<LSSharedFileListRef> login_items(
292      LSSharedFileListCreate(
293          kCFAllocatorDefault, kLSSharedFileListSessionLoginItems, nullptr));
294  if (!login_items.get()) {
295    LOG(ERROR) << "LSSharedFileListCreate error: Cannot get the login items.";
296    return;
297  }
298  scoped_cftyperef<CFURLRef> url(
299    CFURLCreateFromFileSystemRepresentation(
300        kCFAllocatorDefault, kPrelauncherPath,
301        strlen((const char *)kPrelauncherPath), true));
302
303  if (!url.get()) {
304    LOG(ERROR) << "CFURLCreateFromFileSystemRepresentation error:"
305               << " Cannot create CFURL object.";
306    return;
307  }
308  scoped_cftyperef<LSSharedFileListItemRef> new_item(
309      LSSharedFileListInsertItemURL(
310          login_items.get(), kLSSharedFileListItemLast, nullptr, nullptr, url.get(),
311          nullptr, nullptr));
312  if (!new_item.get()) {
313    LOG(ERROR) << "LSSharedFileListInsertItemURL error:"
314               << " Cannot insert the prelauncher to the login items.";
315    return;
316  }
317}
318
319bool MacUtil::GetFrontmostWindowNameAndOwner(string *name, string *owner) {
320  DCHECK(name);
321  DCHECK(owner);
322  scoped_cftyperef<CFArrayRef> window_list(
323      ::CGWindowListCopyWindowInfo(
324          kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements,
325          kCGNullWindowID));
326  const CFIndex window_count = CFArrayGetCount(window_list.get());
327  for (CFIndex i = 0; i < window_count; ++i) {
328    const NSDictionary *window_data = static_cast<const NSDictionary *>(
329        CFArrayGetValueAtIndex(window_list.get(), i));
330    if ([[window_data objectForKey:(id)kCGWindowSharingState] intValue] ==
331        kCGWindowSharingNone) {
332      // Skips not shared window.
333      continue;
334    }
335    NSString *window_name = [window_data objectForKey:(id)kCGWindowName];
336    NSString *owner_name =
337      [window_data objectForKey:(id)kCGWindowOwnerName];
338    NSNumber *window_layer = [window_data objectForKey:(id)kCGWindowLayer];
339
340    if ((window_name == nil) || (owner_name == nil) || (window_layer == nil)) {
341      continue;
342    }
343    // Ignores the windows which aren't normal window level.
344    if ([window_layer intValue] != kCGNormalWindowLevel) {
345      continue;
346    }
347
348    // Hack to ignore the window (name == "" and owner == "Google Chrome")
349    // Chrome browser seems to create a window which has no name in front of the
350    // actual frontmost Chrome window.
351    if ([window_name isEqualToString:@""] &&
352        [owner_name isEqualToString:@"Google Chrome"]) {
353      continue;
354    }
355    name->assign([window_name UTF8String]);
356    owner->assign([owner_name UTF8String]);
357    return true;
358  }
359  return false;
360}
361
362bool MacUtil::IsSuppressSuggestionWindow(const string &name,
363                                         const string &owner) {
364  // TODO(horo): Make a function to check the name, then share it with the
365  //             Windows client.
366  // Currently we don't support "Firefox", because in Firefox "activateServer:"
367  // of IMKStateSetting Protocol is not called when the user changes the
368  // browsing tab.
369  return (("Google Chrome" == owner) ||
370          ("Safari" == owner)) &&
371         (("Google" == name) ||
372          Util::EndsWith(
373              name,
374              " - Google \xE6\xA4\x9C\xE7\xB4\xA2") ||  // " - Google 検索"
375          Util::EndsWith(name, " - Google Search"));
376}
377
378}  // namespace mozc
379