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 g_return_val_if_fail (cstr != NULL, NULL); 172 return CFStringCreateWithCString (NULL, cstr, kCFStringEncodingUTF8); 173} 174 175#ifdef G_ENABLE_DEBUG 176static gchar * 177create_cstr_from_cfstring (CFStringRef str) 178{ 179 g_return_val_if_fail (str != NULL, NULL); 180 181 CFIndex length = CFStringGetLength (str); 182 CFIndex maxlen = CFStringGetMaximumSizeForEncoding (length, kCFStringEncodingUTF8); 183 gchar *buffer = g_malloc (maxlen + 1); 184 Boolean success = CFStringGetCString (str, (char *) buffer, maxlen, 185 kCFStringEncodingUTF8); 186 if (success) 187 return buffer; 188 else 189 { 190 g_free (buffer); 191 return NULL; 192 } 193} 194#endif 195 196static char * 197url_escape_hostname (const char *url) 198{ 199 char *host_start, *ret; 200 201 host_start = strstr (url, "://"); 202 if (host_start != NULL) 203 { 204 char *host_end, *scheme, *host, *hostname; 205 206 scheme = g_strndup (url, host_start - url); 207 host_start += 3; 208 host_end = strchr (host_start, '/'); 209 210 if (host_end != NULL) 211 host = g_strndup (host_start, host_end - host_start); 212 else 213 host = g_strdup (host_start); 214 215 hostname = g_hostname_to_ascii (host); 216 217 ret = g_strconcat (scheme, "://", hostname, host_end, NULL); 218 219 g_free (scheme); 220 g_free (host); 221 g_free (hostname); 222 223 return ret; 224 } 225 226 return g_strdup (url); 227} 228 229static CFURLRef 230create_url_from_cstr (const gchar *cstr, 231 gboolean is_file) 232{ 233 gchar *puny_cstr; 234 CFStringRef str; 235 CFURLRef url; 236 237 puny_cstr = url_escape_hostname (cstr); 238 str = CFStringCreateWithCString (NULL, puny_cstr ? puny_cstr : cstr, kCFStringEncodingUTF8); 239 240 if (is_file) 241 url = CFURLCreateWithFileSystemPath (NULL, str, kCFURLPOSIXPathStyle, FALSE); 242 else 243 url = CFURLCreateWithString (NULL, str, NULL); 244 245 if (!url) 246 g_debug ("Creating CFURL from %s %s failed!", cstr, is_file ? "file" : "uri"); 247 248 g_free (puny_cstr); 249 CFRelease(str); 250 return url; 251} 252 253static CFArrayRef 254create_url_list_from_glist (GList *uris, 255 gboolean are_files) 256{ 257 GList *lst; 258 int len = g_list_length (uris); 259 CFMutableArrayRef array; 260 261 if (!len) 262 return NULL; 263 264 array = CFArrayCreateMutable (NULL, len, &kCFTypeArrayCallBacks); 265 if (!array) 266 return NULL; 267 268 for (lst = uris; lst != NULL && lst->data; lst = lst->next) 269 { 270 CFURLRef url = create_url_from_cstr ((char*)lst->data, are_files); 271 if (url) 272 CFArrayAppendValue (array, url); 273 } 274 275 return (CFArrayRef)array; 276} 277 278static LSLaunchURLSpec * 279create_urlspec_for_appinfo (GOsxAppInfo *info, 280 GList *uris, 281 gboolean are_files) 282{ 283 LSLaunchURLSpec *urlspec = NULL; 284 const gchar *app_cstr; 285 286 g_return_val_if_fail (G_IS_OSX_APP_INFO (info), NULL); 287 288 urlspec = g_new0 (LSLaunchURLSpec, 1); 289 app_cstr = g_osx_app_info_get_filename (info); 290 g_assert (app_cstr != NULL); 291 292 /* Strip file:// from app url but ensure filesystem url */ 293 urlspec->appURL = create_url_from_cstr (app_cstr + strlen ("file://"), TRUE); 294 urlspec->launchFlags = kLSLaunchDefaults; 295 urlspec->itemURLs = create_url_list_from_glist (uris, are_files); 296 297 return urlspec; 298} 299 300static void 301free_urlspec (LSLaunchURLSpec *urlspec) 302{ 303 if (urlspec->itemURLs) 304 { 305 CFArrayRemoveAllValues ((CFMutableArrayRef)urlspec->itemURLs); 306 CFRelease (urlspec->itemURLs); 307 } 308 CFRelease (urlspec->appURL); 309 g_free (urlspec); 310} 311 312static NSBundle * 313get_bundle_for_url (CFURLRef app_url) 314{ 315 NSBundle *bundle = [NSBundle bundleWithURL: (NSURL*)app_url]; 316 317 if (!bundle) 318 { 319 g_debug ("Bundle not found for url."); 320 return NULL; 321 } 322 323 return bundle; 324} 325 326static NSBundle * 327get_bundle_for_id (CFStringRef bundle_id) 328{ 329 CFURLRef app_url; 330 NSBundle *bundle; 331 332#ifdef AVAILABLE_MAC_OS_X_VERSION_10_10_AND_LATER 333 CFArrayRef urls = LSCopyApplicationURLsForBundleIdentifier (bundle_id, NULL); 334 if (urls) 335 { 336 /* TODO: if there's multiple, we should perhaps prefer one that's in $HOME, 337 * instead of just always picking the first. 338 */ 339 app_url = CFArrayGetValueAtIndex (urls, 0); 340 CFRetain (app_url); 341 CFRelease (urls); 342 } 343 else 344#else 345 if (LSFindApplicationForInfo (kLSUnknownCreator, bundle_id, NULL, NULL, &app_url) == kLSApplicationNotFoundErr) 346#endif 347 { 348#ifdef G_ENABLE_DEBUG /* This can fail often, no reason to alloc strings */ 349 gchar *id_str = create_cstr_from_cfstring (bundle_id); 350 if (id_str) 351 { 352 g_debug ("Application not found for id \"%s\".", id_str); 353 g_free (id_str); 354 } 355 else 356 g_debug ("Application not found for unconvertable bundle id."); 357#endif 358 return NULL; 359 } 360 361 bundle = get_bundle_for_url (app_url); 362 CFRelease (app_url); 363 return bundle; 364} 365 366static const char * 367g_osx_app_info_get_id (GAppInfo *appinfo) 368{ 369 GOsxAppInfo *info = G_OSX_APP_INFO (appinfo); 370 371 if (!info->id) 372 info->id = get_bundle_string_value (info->bundle, @"CFBundleIdentifier"); 373 374 return info->id; 375} 376 377static const char * 378g_osx_app_info_get_name (GAppInfo *appinfo) 379{ 380 GOsxAppInfo *info = G_OSX_APP_INFO (appinfo); 381 382 if (!info->name) 383 info->name = get_bundle_string_value (info->bundle, @"CFBundleName"); 384 385 return info->name; 386} 387 388static const char * 389g_osx_app_info_get_display_name (GAppInfo *appinfo) 390{ 391 return g_osx_app_info_get_name (appinfo); 392} 393 394static const char * 395g_osx_app_info_get_description (GAppInfo *appinfo) 396{ 397 /* Bundles do not contain descriptions */ 398 return NULL; 399} 400 401static const char * 402g_osx_app_info_get_executable (GAppInfo *appinfo) 403{ 404 GOsxAppInfo *info = G_OSX_APP_INFO (appinfo); 405 406 if (!info->executable) 407 info->executable = get_bundle_string_value (info->bundle, @"CFBundleExecutable"); 408 409 return info->executable; 410} 411 412const char * 413g_osx_app_info_get_filename (GOsxAppInfo *info) 414{ 415 g_return_val_if_fail (info != NULL, NULL); 416 417 if (!info->filename) 418 { 419 info->filename = g_strconcat ("file://", [[info->bundle bundlePath] 420 cStringUsingEncoding: NSUTF8StringEncoding], 421 NULL); 422 } 423 424 return info->filename; 425} 426 427static const char * 428g_osx_app_info_get_commandline (GAppInfo *appinfo) 429{ 430 /* There isn't really a command line value */ 431 return NULL; 432} 433 434static GIcon * 435g_osx_app_info_get_icon (GAppInfo *appinfo) 436{ 437 GOsxAppInfo *info = G_OSX_APP_INFO (appinfo); 438 439 if (!info->icon) 440 { 441 const gchar *app_uri; 442 gchar *icon_name, *icon_uri; 443 GFile *file; 444 445 icon_name = get_bundle_string_value (info->bundle, @"CFBundleIconFile"); 446 if (!icon_name) 447 return NULL; 448 449 app_uri = g_osx_app_info_get_filename (info); 450 icon_uri = g_strconcat (app_uri + strlen ("file://"), "/Contents/Resources/", icon_name, 451 g_str_has_suffix (icon_name, ".icns") ? NULL : ".icns", NULL); 452 g_free (icon_name); 453 454 file = g_file_new_for_path (icon_uri); 455 info->icon = g_file_icon_new (file); 456 g_object_unref (file); 457 g_free (icon_uri); 458 } 459 460 return info->icon; 461} 462 463static gboolean 464g_osx_app_info_launch_internal (GAppInfo *appinfo, 465 GList *uris, 466 gboolean are_files, 467 GError **error) 468{ 469 GOsxAppInfo *info = G_OSX_APP_INFO (appinfo); 470 LSLaunchURLSpec *urlspec; 471 gint ret, success = TRUE; 472 473 g_return_val_if_fail (G_IS_OSX_APP_INFO (appinfo), FALSE); 474 g_return_val_if_fail (error == NULL || *error == NULL, FALSE); 475 476 urlspec = create_urlspec_for_appinfo (info, uris, are_files); 477 478 if ((ret = LSOpenFromURLSpec (urlspec, NULL))) 479 { 480 /* TODO: Better error codes */ 481 g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, 482 "Opening application failed with code %d", ret); 483 success = FALSE; 484 } 485 486 free_urlspec (urlspec); 487 return success; 488} 489 490static gboolean 491g_osx_app_info_supports_uris (GAppInfo *appinfo) 492{ 493 return TRUE; 494} 495 496static gboolean 497g_osx_app_info_supports_files (GAppInfo *appinfo) 498{ 499 return TRUE; 500} 501 502static gboolean 503g_osx_app_info_launch (GAppInfo *appinfo, 504 GList *files, 505 GAppLaunchContext *launch_context, 506 GError **error) 507{ 508 return g_osx_app_info_launch_internal (appinfo, files, TRUE, error); 509} 510 511static gboolean 512g_osx_app_info_launch_uris (GAppInfo *appinfo, 513 GList *uris, 514 GAppLaunchContext *launch_context, 515 GError **error) 516{ 517 return g_osx_app_info_launch_internal (appinfo, uris, FALSE, error); 518} 519 520static gboolean 521g_osx_app_info_should_show (GAppInfo *appinfo) 522{ 523 /* Bundles don't have hidden attribute */ 524 return TRUE; 525} 526 527static gboolean 528g_osx_app_info_set_as_default_for_type (GAppInfo *appinfo, 529 const char *content_type, 530 GError **error) 531{ 532 return FALSE; 533} 534 535static const char ** 536g_osx_app_info_get_supported_types (GAppInfo *appinfo) 537{ 538 /* TODO: get CFBundleDocumentTypes */ 539 return NULL; 540} 541 542static gboolean 543g_osx_app_info_set_as_last_used_for_type (GAppInfo *appinfo, 544 const char *content_type, 545 GError **error) 546{ 547 /* Not supported. */ 548 return FALSE; 549} 550 551static gboolean 552g_osx_app_info_can_delete (GAppInfo *appinfo) 553{ 554 return FALSE; 555} 556 557static void 558g_osx_app_info_iface_init (GAppInfoIface *iface) 559{ 560 iface->dup = g_osx_app_info_dup; 561 iface->equal = g_osx_app_info_equal; 562 563 iface->get_id = g_osx_app_info_get_id; 564 iface->get_name = g_osx_app_info_get_name; 565 iface->get_display_name = g_osx_app_info_get_display_name; 566 iface->get_description = g_osx_app_info_get_description; 567 iface->get_executable = g_osx_app_info_get_executable; 568 iface->get_commandline = g_osx_app_info_get_commandline; 569 iface->get_icon = g_osx_app_info_get_icon; 570 iface->get_supported_types = g_osx_app_info_get_supported_types; 571 572 iface->set_as_last_used_for_type = g_osx_app_info_set_as_last_used_for_type; 573 iface->set_as_default_for_type = g_osx_app_info_set_as_default_for_type; 574 575 iface->launch = g_osx_app_info_launch; 576 iface->launch_uris = g_osx_app_info_launch_uris; 577 578 iface->supports_uris = g_osx_app_info_supports_uris; 579 iface->supports_files = g_osx_app_info_supports_files; 580 iface->should_show = g_osx_app_info_should_show; 581 iface->can_delete = g_osx_app_info_can_delete; 582} 583 584GAppInfo * 585g_app_info_create_from_commandline (const char *commandline, 586 const char *application_name, 587 GAppInfoCreateFlags flags, 588 GError **error) 589{ 590 return NULL; 591} 592 593GList * 594g_osx_app_info_get_all_for_scheme (const char *cscheme) 595{ 596 CFArrayRef bundle_list; 597 CFStringRef scheme; 598 NSBundle *bundle; 599 GList *info_list = NULL; 600 gint i; 601 602 scheme = create_cfstring_from_cstr (cscheme); 603 bundle_list = LSCopyAllHandlersForURLScheme (scheme); 604 CFRelease (scheme); 605 606 if (!bundle_list) 607 return NULL; 608 609 for (i = 0; i < CFArrayGetCount (bundle_list); i++) 610 { 611 CFStringRef bundle_id = CFArrayGetValueAtIndex (bundle_list, i); 612 GAppInfo *info; 613 614 bundle = get_bundle_for_id (bundle_id); 615 616 if (!bundle) 617 continue; 618 619 info = G_APP_INFO (g_osx_app_info_new (bundle)); 620 info_list = g_list_append (info_list, info); 621 } 622 CFRelease (bundle_list); 623 return info_list; 624} 625 626GList * 627g_app_info_get_all_for_type (const char *content_type) 628{ 629 gchar *mime_type; 630 CFArrayRef bundle_list; 631 CFStringRef type; 632 NSBundle *bundle; 633 GList *info_list = NULL; 634 gint i; 635 636 mime_type = g_content_type_get_mime_type (content_type); 637 if (g_str_has_prefix (mime_type, "x-scheme-handler/")) 638 { 639 gchar *scheme = strchr (mime_type, '/') + 1; 640 GList *ret = g_osx_app_info_get_all_for_scheme (scheme); 641 642 g_free (mime_type); 643 return ret; 644 } 645 g_free (mime_type); 646 647 type = create_cfstring_from_cstr (content_type); 648 bundle_list = LSCopyAllRoleHandlersForContentType (type, kLSRolesAll); 649 CFRelease (type); 650 651 if (!bundle_list) 652 return NULL; 653 654 for (i = 0; i < CFArrayGetCount (bundle_list); i++) 655 { 656 CFStringRef bundle_id = CFArrayGetValueAtIndex (bundle_list, i); 657 GAppInfo *info; 658 659 bundle = get_bundle_for_id (bundle_id); 660 661 if (!bundle) 662 continue; 663 664 info = G_APP_INFO (g_osx_app_info_new (bundle)); 665 info_list = g_list_append (info_list, info); 666 } 667 CFRelease (bundle_list); 668 return info_list; 669} 670 671GList * 672g_app_info_get_recommended_for_type (const char *content_type) 673{ 674 return g_app_info_get_all_for_type (content_type); 675} 676 677GList * 678g_app_info_get_fallback_for_type (const char *content_type) 679{ 680 return g_app_info_get_all_for_type (content_type); 681} 682 683GAppInfo * 684g_app_info_get_default_for_type (const char *content_type, 685 gboolean must_support_uris) 686{ 687 gchar *mime_type; 688 CFStringRef type; 689 NSBundle *bundle; 690#ifdef AVAILABLE_MAC_OS_X_VERSION_10_10_AND_LATER 691 CFURLRef bundle_id; 692#else 693 CFStringRef bundle_id; 694#endif 695 696 mime_type = g_content_type_get_mime_type (content_type); 697 if (g_str_has_prefix (mime_type, "x-scheme-handler/")) 698 { 699 gchar *scheme = strchr (mime_type, '/') + 1; 700 GAppInfo *ret = g_app_info_get_default_for_uri_scheme (scheme); 701 702 g_free (mime_type); 703 return ret; 704 } 705 g_free (mime_type); 706 707 type = create_cfstring_from_cstr (content_type); 708 709#ifdef AVAILABLE_MAC_OS_X_VERSION_10_10_AND_LATER 710 bundle_id = LSCopyDefaultApplicationURLForContentType (type, kLSRolesAll, NULL); 711#else 712 bundle_id = LSCopyDefaultRoleHandlerForContentType (type, kLSRolesAll); 713#endif 714 CFRelease (type); 715 716 if (!bundle_id) 717 { 718 g_warning ("No default handler found for content type '%s'.", content_type); 719 return NULL; 720 } 721 722#ifdef AVAILABLE_MAC_OS_X_VERSION_10_10_AND_LATER 723 bundle = get_bundle_for_url (bundle_id); 724#else 725 bundle = get_bundle_for_id (bundle_id); 726#endif 727 CFRelease (bundle_id); 728 729 if (!bundle) 730 return NULL; 731 732 return G_APP_INFO (g_osx_app_info_new (bundle)); 733} 734 735GAppInfo * 736g_app_info_get_default_for_uri_scheme (const char *uri_scheme) 737{ 738 CFStringRef scheme, bundle_id; 739 NSBundle *bundle; 740 741 scheme = create_cfstring_from_cstr (uri_scheme); 742 bundle_id = LSCopyDefaultHandlerForURLScheme (scheme); 743 CFRelease (scheme); 744 745 if (!bundle_id) 746 { 747 g_warning ("No default handler found for url scheme '%s'.", uri_scheme); 748 return NULL; 749 } 750 751 bundle = get_bundle_for_id (bundle_id); 752 CFRelease (bundle_id); 753 754 if (!bundle) 755 return NULL; 756 757 return G_APP_INFO (g_osx_app_info_new (bundle)); 758} 759 760GList * 761g_app_info_get_all (void) 762{ 763 /* There is no API for this afaict 764 * could manually do it... 765 */ 766 return NULL; 767} 768 769void 770g_app_info_reset_type_associations (const char *content_type) 771{ 772} 773