1/* GIO - GLib Input, Output and Streaming Library 2 * 3 * Copyright (C) 2014 Patrick Griffis 4 * 5 * This library is free software; you can redistribute it and/or 6 * modify it under the terms of the GNU Lesser General Public 7 * License as published by the Free Software Foundation; either 8 * version 2.1 of the License, or (at your option) any later version. 9 * 10 * This library is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13 * Lesser General Public License for more details. 14 * 15 * You should have received a copy of the GNU Lesser General 16 * Public License along with this library; if not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20#include "config.h" 21 22#include "gappinfo.h" 23#include "gosxappinfo.h" 24#include "gcontenttype.h" 25#include "gfile.h" 26#include "gfileicon.h" 27#include "gioerror.h" 28 29#import <CoreFoundation/CoreFoundation.h> 30#import <Foundation/Foundation.h> 31#import <ApplicationServices/ApplicationServices.h> 32 33/** 34 * SECTION:gosxappinfo 35 * @title: GOsxAppInfo 36 * @short_description: Application information from NSBundles 37 * @include: gio/gosxappinfo.h 38 * 39 * #GOsxAppInfo is an implementation of #GAppInfo based on NSBundle information. 40 * 41 * Note that `<gio/gosxappinfo.h>` is unique to OSX. 42 */ 43 44static void g_osx_app_info_iface_init (GAppInfoIface *iface); 45static const char *g_osx_app_info_get_id (GAppInfo *appinfo); 46 47/** 48 * GOsxAppInfo: 49 * 50 * Information about an installed application from a NSBundle. 51 */ 52struct _GOsxAppInfo 53{ 54 GObject parent_instance; 55 56 NSBundle *bundle; 57 58 /* Note that these are all NULL until first call 59 * to getter at which point they are cached here 60 */ 61 gchar *id; 62 gchar *name; 63 gchar *executable; 64 gchar *filename; 65 GIcon *icon; 66}; 67 68G_DEFINE_TYPE_WITH_CODE (GOsxAppInfo, g_osx_app_info, G_TYPE_OBJECT, 69 G_IMPLEMENT_INTERFACE (G_TYPE_APP_INFO, g_osx_app_info_iface_init)) 70 71static GOsxAppInfo * 72g_osx_app_info_new (NSBundle *bundle) 73{ 74 GOsxAppInfo *info = g_object_new (G_TYPE_OSX_APP_INFO, NULL); 75 76 info->bundle = [bundle retain]; 77 78 return info; 79} 80 81static void 82g_osx_app_info_init (GOsxAppInfo *info) 83{ 84} 85 86static void 87g_osx_app_info_finalize (GObject *object) 88{ 89 GOsxAppInfo *info = G_OSX_APP_INFO (object); 90 91 g_free (info->id); 92 g_free (info->name); 93 g_free (info->executable); 94 g_free (info->filename); 95 g_clear_object (&info->icon); 96 97 [info->bundle release]; 98 99 G_OBJECT_CLASS (g_osx_app_info_parent_class)->finalize (object); 100} 101 102static void 103g_osx_app_info_class_init (GOsxAppInfoClass *klass) 104{ 105 GObjectClass *gobject_class = G_OBJECT_CLASS (klass); 106 107 gobject_class->finalize = g_osx_app_info_finalize; 108} 109 110static GAppInfo * 111g_osx_app_info_dup (GAppInfo *appinfo) 112{ 113 GOsxAppInfo *info; 114 GOsxAppInfo *new_info; 115 116 g_return_val_if_fail (appinfo != NULL, NULL); 117 118 info = G_OSX_APP_INFO (appinfo); 119 new_info = g_osx_app_info_new ([info->bundle retain]); 120 121 return G_APP_INFO (new_info); 122} 123 124static gboolean 125g_osx_app_info_equal (GAppInfo *appinfo1, 126 GAppInfo *appinfo2) 127{ 128 const gchar *str1, *str2; 129 130 g_return_val_if_fail (appinfo1 != NULL, FALSE); 131 g_return_val_if_fail (appinfo2 != NULL, FALSE); 132 133 str1 = g_osx_app_info_get_id (appinfo1); 134 str2 = g_osx_app_info_get_id (appinfo2); 135 136 return (g_strcmp0 (str1, str2) == 0); 137} 138 139/*< internal > 140 * get_bundle_string_value: 141 * @bundle: a #NSBundle 142 * @key: an #NSString key 143 * 144 * Returns a value from a bundles info.plist file. 145 * It will be utf8 encoded and it must be g_free()'d. 146 * 147 */ 148static gchar * 149get_bundle_string_value (NSBundle *bundle, 150 NSString *key) 151{ 152 NSString *value; 153 const gchar *cvalue; 154 gchar *ret; 155 156 g_return_val_if_fail (bundle != NULL, NULL); 157 158 value = (NSString *)[bundle objectForInfoDictionaryKey: key]; 159 if (!value) 160 return NULL; 161 162 cvalue = [value cStringUsingEncoding: NSUTF8StringEncoding]; 163 ret = g_strdup (cvalue); 164 165 return ret; 166} 167 168static CFStringRef 169create_cfstring_from_cstr (const gchar *cstr) 170{ 171 return CFStringCreateWithCString (NULL, cstr, kCFStringEncodingUTF8); 172} 173 174#ifdef G_ENABLE_DEBUG 175static gchar * 176create_cstr_from_cfstring (CFStringRef str) 177{ 178 g_return_val_if_fail (str != NULL, NULL); 179 180 CFIndex length = CFStringGetLength (str); 181 CFIndex maxlen = CFStringGetMaximumSizeForEncoding (length, kCFStringEncodingUTF8); 182 gchar *buffer = g_malloc (maxlen + 1); 183 Boolean success = CFStringGetCString (str, (char *) buffer, maxlen, 184 kCFStringEncodingUTF8); 185 if (success) 186 return buffer; 187 else 188 { 189 g_free (buffer); 190 return NULL; 191 } 192} 193#endif 194 195static char * 196url_escape_hostname (const char *url) 197{ 198 char *host_start, *ret; 199 200 host_start = strstr (url, "://"); 201 if (host_start != NULL) 202 { 203 char *host_end, *scheme, *host, *hostname; 204 205 scheme = g_strndup (url, host_start - url); 206 host_start += 3; 207 host_end = strchr (host_start, '/'); 208 209 if (host_end != NULL) 210 host = g_strndup (host_start, host_end - host_start); 211 else 212 host = g_strdup (host_start); 213 214 hostname = g_hostname_to_ascii (host); 215 216 ret = g_strconcat (scheme, "://", hostname, host_end, NULL); 217 218 g_free (scheme); 219 g_free (host); 220 g_free (hostname); 221 222 return ret; 223 } 224 225 return g_strdup (url); 226} 227 228static CFURLRef 229create_url_from_cstr (gchar *cstr, 230 gboolean is_file) 231{ 232 gchar *puny_cstr; 233 CFStringRef str; 234 CFURLRef url; 235 236 puny_cstr = url_escape_hostname (cstr); 237 str = CFStringCreateWithCString (NULL, puny_cstr ? puny_cstr : cstr, kCFStringEncodingUTF8); 238 239 if (is_file) 240 url = CFURLCreateWithFileSystemPath (NULL, str, kCFURLPOSIXPathStyle, FALSE); 241 else 242 url = CFURLCreateWithString (NULL, str, NULL); 243 244 if (!url) 245 g_debug ("Creating CFURL from %s %s failed!", cstr, is_file ? "file" : "uri"); 246 247 g_free (puny_cstr); 248 CFRelease(str); 249 return url; 250} 251 252static CFArrayRef 253create_url_list_from_glist (GList *uris, 254 gboolean are_files) 255{ 256 GList *lst; 257 int len = g_list_length (uris); 258 CFMutableArrayRef array; 259 260 if (!len) 261 return NULL; 262 263 array = CFArrayCreateMutable (NULL, len, &kCFTypeArrayCallBacks); 264 if (!array) 265 return NULL; 266 267 for (lst = uris; lst != NULL && lst->data; lst = lst->next) 268 { 269 CFURLRef url = create_url_from_cstr ((char*)lst->data, are_files); 270 if (url) 271 CFArrayAppendValue (array, url); 272 } 273 274 return (CFArrayRef)array; 275} 276 277static LSLaunchURLSpec * 278create_urlspec_for_appinfo (GOsxAppInfo *info, 279 GList *uris, 280 gboolean are_files) 281{ 282 LSLaunchURLSpec *urlspec = g_new0 (LSLaunchURLSpec, 1); 283 gchar *app_cstr = g_osx_app_info_get_filename (info); 284 285 /* Strip file:// from app url but ensure filesystem url */ 286 urlspec->appURL = create_url_from_cstr (app_cstr + 7, TRUE); 287 urlspec->launchFlags = kLSLaunchDefaults; 288 urlspec->itemURLs = create_url_list_from_glist (uris, are_files); 289 290 return urlspec; 291} 292 293static void 294free_urlspec (LSLaunchURLSpec *urlspec) 295{ 296 if (urlspec->itemURLs) 297 { 298 CFArrayRemoveAllValues ((CFMutableArrayRef)urlspec->itemURLs); 299 CFRelease (urlspec->itemURLs); 300 } 301 CFRelease (urlspec->appURL); 302 g_free (urlspec); 303} 304 305static NSBundle * 306get_bundle_for_url (CFURLRef app_url) 307{ 308 NSBundle *bundle = [NSBundle bundleWithURL: (NSURL*)app_url]; 309 310 if (!bundle) 311 { 312 g_debug ("Bundle not found for url."); 313 return NULL; 314 } 315 316 return bundle; 317} 318 319static NSBundle * 320get_bundle_for_id (CFStringRef bundle_id) 321{ 322 CFURLRef app_url; 323 NSBundle *bundle; 324 325#ifdef AVAILABLE_MAC_OS_X_VERSION_10_10_AND_LATER 326 CFArrayRef urls = LSCopyApplicationURLsForBundleIdentifier (bundle_id, NULL); 327 if (urls) 328 { 329 /* TODO: if there's multiple, we should perhaps prefer one thats in $HOME, 330 * instead of just always picking the first. 331 */ 332 app_url = CFArrayGetValueAtIndex (urls, 0); 333 CFRetain (app_url); 334 CFRelease (urls); 335 } 336 else 337#else 338 if (LSFindApplicationForInfo (kLSUnknownCreator, bundle_id, NULL, NULL, &app_url) == kLSApplicationNotFoundErr) 339#endif 340 { 341#ifdef G_ENABLE_DEBUG /* This can fail often, no reason to alloc strings */ 342 gchar *id_str = create_cstr_from_cfstring (bundle_id); 343 if (id_str) 344 { 345 g_debug ("Application not found for id \"%s\".", id_str); 346 g_free (id_str); 347 } 348 else 349 g_debug ("Application not found for unconvertable bundle id."); 350#endif 351 return NULL; 352 } 353 354 bundle = get_bundle_for_url (app_url); 355 CFRelease (app_url); 356 return bundle; 357} 358 359static const char * 360g_osx_app_info_get_id (GAppInfo *appinfo) 361{ 362 GOsxAppInfo *info = G_OSX_APP_INFO (appinfo); 363 364 if (!info->id) 365 info->id = get_bundle_string_value (info->bundle, @"CFBundleIdentifier"); 366 367 return info->id; 368} 369 370static const char * 371g_osx_app_info_get_name (GAppInfo *appinfo) 372{ 373 GOsxAppInfo *info = G_OSX_APP_INFO (appinfo); 374 375 if (!info->name) 376 info->name = get_bundle_string_value (info->bundle, @"CFBundleName"); 377 378 return info->name; 379} 380 381static const char * 382g_osx_app_info_get_display_name (GAppInfo *appinfo) 383{ 384 return g_osx_app_info_get_name (appinfo); 385} 386 387static const char * 388g_osx_app_info_get_description (GAppInfo *appinfo) 389{ 390 /* Bundles do not contain descriptions */ 391 return NULL; 392} 393 394static const char * 395g_osx_app_info_get_executable (GAppInfo *appinfo) 396{ 397 GOsxAppInfo *info = G_OSX_APP_INFO (appinfo); 398 399 if (!info->executable) 400 info->executable = get_bundle_string_value (info->bundle, @"CFBundleExecutable"); 401 402 return info->executable; 403} 404 405char * 406g_osx_app_info_get_filename (GOsxAppInfo *info) 407{ 408 g_return_val_if_fail (info != NULL, NULL); 409 410 if (!info->filename) 411 { 412 info->filename = g_strconcat ("file://", [[info->bundle bundlePath] 413 cStringUsingEncoding: NSUTF8StringEncoding], 414 NULL); 415 } 416 417 return info->filename; 418} 419 420static const char * 421g_osx_app_info_get_commandline (GAppInfo *appinfo) 422{ 423 /* There isn't really a command line value */ 424 return NULL; 425} 426 427static GIcon * 428g_osx_app_info_get_icon (GAppInfo *appinfo) 429{ 430 GOsxAppInfo *info = G_OSX_APP_INFO (appinfo); 431 432 if (!info->icon) 433 { 434 gchar *icon_name, *app_uri, *icon_uri; 435 GFile *file; 436 437 icon_name = get_bundle_string_value (info->bundle, @"CFBundleIconFile"); 438 if (!icon_name) 439 return NULL; 440 441 app_uri = g_osx_app_info_get_filename (info); 442 icon_uri = g_strconcat (app_uri + 7, "/Contents/Resources/", icon_name, 443 g_str_has_suffix (icon_name, ".icns") ? NULL : ".icns", NULL); 444 g_free (icon_name); 445 446 file = g_file_new_for_path (icon_uri); 447 info->icon = g_file_icon_new (file); 448 g_object_unref (file); 449 g_free (icon_uri); 450 } 451 452 return info->icon; 453} 454 455static gboolean 456g_osx_app_info_launch_internal (GAppInfo *appinfo, 457 GList *uris, 458 gboolean are_files, 459 GError **error) 460{ 461 GOsxAppInfo *info = G_OSX_APP_INFO (appinfo); 462 LSLaunchURLSpec *urlspec = create_urlspec_for_appinfo (info, uris, are_files); 463 gint ret, success = TRUE; 464 465 if ((ret = LSOpenFromURLSpec (urlspec, NULL))) 466 { 467 /* TODO: Better error codes */ 468 g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, 469 "Opening application failed with code %d", ret); 470 success = FALSE; 471 } 472 473 free_urlspec (urlspec); 474 return success; 475} 476 477static gboolean 478g_osx_app_info_supports_uris (GAppInfo *appinfo) 479{ 480 return TRUE; 481} 482 483static gboolean 484g_osx_app_info_supports_files (GAppInfo *appinfo) 485{ 486 return TRUE; 487} 488 489static gboolean 490g_osx_app_info_launch (GAppInfo *appinfo, 491 GList *files, 492 GAppLaunchContext *launch_context, 493 GError **error) 494{ 495 return g_osx_app_info_launch_internal (appinfo, files, TRUE, error); 496} 497 498static gboolean 499g_osx_app_info_launch_uris (GAppInfo *appinfo, 500 GList *uris, 501 GAppLaunchContext *launch_context, 502 GError **error) 503{ 504 return g_osx_app_info_launch_internal (appinfo, uris, FALSE, error); 505} 506 507static gboolean 508g_osx_app_info_should_show (GAppInfo *appinfo) 509{ 510 /* Bundles don't have hidden attribute */ 511 return TRUE; 512} 513 514static gboolean 515g_osx_app_info_set_as_default_for_type (GAppInfo *appinfo, 516 const char *content_type, 517 GError **error) 518{ 519 return FALSE; 520} 521 522static const char ** 523g_osx_app_info_get_supported_types (GAppInfo *appinfo) 524{ 525 /* TODO: get CFBundleDocumentTypes */ 526 return NULL; 527} 528 529static gboolean 530g_osx_app_info_set_as_last_used_for_type (GAppInfo *appinfo, 531 const char *content_type, 532 GError **error) 533{ 534 /* Not supported. */ 535 return FALSE; 536} 537 538static gboolean 539g_osx_app_info_can_delete (GAppInfo *appinfo) 540{ 541 return FALSE; 542} 543 544static void 545g_osx_app_info_iface_init (GAppInfoIface *iface) 546{ 547 iface->dup = g_osx_app_info_dup; 548 iface->equal = g_osx_app_info_equal; 549 550 iface->get_id = g_osx_app_info_get_id; 551 iface->get_name = g_osx_app_info_get_name; 552 iface->get_display_name = g_osx_app_info_get_display_name; 553 iface->get_description = g_osx_app_info_get_description; 554 iface->get_executable = g_osx_app_info_get_executable; 555 iface->get_commandline = g_osx_app_info_get_commandline; 556 iface->get_icon = g_osx_app_info_get_icon; 557 iface->get_supported_types = g_osx_app_info_get_supported_types; 558 559 iface->set_as_last_used_for_type = g_osx_app_info_set_as_last_used_for_type; 560 iface->set_as_default_for_type = g_osx_app_info_set_as_default_for_type; 561 562 iface->launch = g_osx_app_info_launch; 563 iface->launch_uris = g_osx_app_info_launch_uris; 564 565 iface->supports_uris = g_osx_app_info_supports_uris; 566 iface->supports_files = g_osx_app_info_supports_files; 567 iface->should_show = g_osx_app_info_should_show; 568 iface->can_delete = g_osx_app_info_can_delete; 569} 570 571GAppInfo * 572g_app_info_create_from_commandline (const char *commandline, 573 const char *application_name, 574 GAppInfoCreateFlags flags, 575 GError **error) 576{ 577 return NULL; 578} 579 580GList * 581g_osx_app_info_get_all_for_scheme (const char *cscheme) 582{ 583 CFArrayRef bundle_list; 584 CFStringRef scheme; 585 NSBundle *bundle; 586 GList *info_list = NULL; 587 gint i; 588 589 scheme = create_cfstring_from_cstr (cscheme); 590 bundle_list = LSCopyAllHandlersForURLScheme (scheme); 591 CFRelease (scheme); 592 593 if (!bundle_list) 594 return NULL; 595 596 for (i = 0; i < CFArrayGetCount (bundle_list); i++) 597 { 598 CFStringRef bundle_id = CFArrayGetValueAtIndex (bundle_list, i); 599 GAppInfo *info; 600 601 bundle = get_bundle_for_id (bundle_id); 602 603 if (!bundle) 604 continue; 605 606 info = G_APP_INFO (g_osx_app_info_new (bundle)); 607 info_list = g_list_append (info_list, info); 608 } 609 CFRelease (bundle_list); 610 return info_list; 611} 612 613GList * 614g_app_info_get_all_for_type (const char *content_type) 615{ 616 gchar *mime_type; 617 CFArrayRef bundle_list; 618 CFStringRef type; 619 NSBundle *bundle; 620 GList *info_list = NULL; 621 gint i; 622 623 mime_type = g_content_type_get_mime_type (content_type); 624 if (g_str_has_prefix (mime_type, "x-scheme-handler/")) 625 { 626 gchar *scheme = strchr (mime_type, '/') + 1; 627 GList *ret = g_osx_app_info_get_all_for_scheme (scheme); 628 629 g_free (mime_type); 630 return ret; 631 } 632 g_free (mime_type); 633 634 type = create_cfstring_from_cstr (content_type); 635 bundle_list = LSCopyAllRoleHandlersForContentType (type, kLSRolesAll); 636 CFRelease (type); 637 638 if (!bundle_list) 639 return NULL; 640 641 for (i = 0; i < CFArrayGetCount (bundle_list); i++) 642 { 643 CFStringRef bundle_id = CFArrayGetValueAtIndex (bundle_list, i); 644 GAppInfo *info; 645 646 bundle = get_bundle_for_id (bundle_id); 647 648 if (!bundle) 649 continue; 650 651 info = G_APP_INFO (g_osx_app_info_new (bundle)); 652 info_list = g_list_append (info_list, info); 653 } 654 CFRelease (bundle_list); 655 return info_list; 656} 657 658GList * 659g_app_info_get_recommended_for_type (const char *content_type) 660{ 661 return g_app_info_get_all_for_type (content_type); 662} 663 664GList * 665g_app_info_get_fallback_for_type (const char *content_type) 666{ 667 return g_app_info_get_all_for_type (content_type); 668} 669 670GAppInfo * 671g_app_info_get_default_for_type (const char *content_type, 672 gboolean must_support_uris) 673{ 674 gchar *mime_type; 675 CFStringRef type; 676 NSBundle *bundle; 677#ifdef AVAILABLE_MAC_OS_X_VERSION_10_10_AND_LATER 678 CFURLRef bundle_id; 679#else 680 CFStringRef bundle_id; 681#endif 682 683 mime_type = g_content_type_get_mime_type (content_type); 684 if (g_str_has_prefix (mime_type, "x-scheme-handler/")) 685 { 686 gchar *scheme = strchr (mime_type, '/') + 1; 687 GAppInfo *ret = g_app_info_get_default_for_uri_scheme (scheme); 688 689 g_free (mime_type); 690 return ret; 691 } 692 g_free (mime_type); 693 694 type = create_cfstring_from_cstr (content_type); 695 696#ifdef AVAILABLE_MAC_OS_X_VERSION_10_10_AND_LATER 697 bundle_id = LSCopyDefaultApplicationURLForContentType (type, kLSRolesAll, NULL); 698#else 699 bundle_id = LSCopyDefaultRoleHandlerForContentType (type, kLSRolesAll); 700#endif 701 CFRelease (type); 702 703 if (!bundle_id) 704 { 705 g_warning ("No default handler found for content type '%s'.", content_type); 706 return NULL; 707 } 708 709#ifdef AVAILABLE_MAC_OS_X_VERSION_10_10_AND_LATER 710 bundle = get_bundle_for_url (bundle_id); 711#else 712 bundle = get_bundle_for_id (bundle_id); 713#endif 714 CFRelease (bundle_id); 715 716 if (!bundle) 717 return NULL; 718 719 return G_APP_INFO (g_osx_app_info_new (bundle)); 720} 721 722GAppInfo * 723g_app_info_get_default_for_uri_scheme (const char *uri_scheme) 724{ 725 CFStringRef scheme, bundle_id; 726 NSBundle *bundle; 727 728 scheme = create_cfstring_from_cstr (uri_scheme); 729 bundle_id = LSCopyDefaultHandlerForURLScheme (scheme); 730 CFRelease (scheme); 731 732 if (!bundle_id) 733 { 734 g_warning ("No default handler found for url scheme '%s'.", uri_scheme); 735 return NULL; 736 } 737 738 bundle = get_bundle_for_id (bundle_id); 739 CFRelease (bundle_id); 740 741 if (!bundle) 742 return NULL; 743 744 return G_APP_INFO (g_osx_app_info_new (bundle)); 745} 746 747GList * 748g_app_info_get_all (void) 749{ 750 /* There is no API for this afaict 751 * could manually do it... 752 */ 753 return NULL; 754} 755 756void 757g_app_info_reset_type_associations (const char *content_type) 758{ 759} 760