/* $Id$ */ /* Copyright (c) 2017-2018 Pierre Pronchery */ /* This file is part of DeforaOS libDesktop */ /* All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include #include #ifdef DEBUG # include #endif #include #include #include #include "Desktop.h" #include "mimehandler.h" /* constants */ #ifndef PROGNAME_BROWSER # define PROGNAME_BROWSER "browser" #endif #ifndef PROGNAME_HTMLAPP # define PROGNAME_HTMLAPP "htmlapp" #endif #ifndef PREFIX # define PREFIX "/usr/local" #endif #ifndef BINDIR # define BINDIR PREFIX "/bin" #endif #ifndef DATADIR # define DATADIR PREFIX "/share" #endif /* private */ /* types */ struct _MimeHandler { Config * config; String * filename; String ** categories; String ** types; String * environment; }; /* constants */ #define EXTENSION ".desktop" #define SECTION "Desktop Entry" /* prototypes */ /* accessors */ static String const * _mimehandler_get_translation(MimeHandler * handler, String const * key); /* useful */ static void _mimehandler_cache_invalidate(MimeHandler * handler); /* protected */ /* functions */ int mimehandler_set(MimeHandler * handler, String const * variable, String const * value) { return config_set(handler->config, SECTION, variable, value); } /* public */ /* functions */ /* mimehandler_new */ MimeHandler * mimehandler_new(void) { MimeHandler * handler; if((handler = object_new(sizeof(*handler))) == NULL) return NULL; handler->config = config_new(); handler->filename = NULL; handler->categories = NULL; handler->types = NULL; handler->environment = NULL; if(handler->config == NULL) { mimehandler_delete(handler); return NULL; } return handler; } /* mimehandler_new_load */ MimeHandler * mimehandler_new_load(String const * filename) { MimeHandler * handler; if((handler = mimehandler_new()) == NULL) return NULL; if(mimehandler_load(handler, filename) != 0) { mimehandler_delete(handler); return NULL; } return handler; } /* mimehandler_new_load_by_name */ MimeHandler * mimehandler_new_load_by_name(String const * name) { MimeHandler * handler; if((handler = mimehandler_new()) == NULL) return NULL; if(mimehandler_load_by_name(handler, name) != 0) { mimehandler_delete(handler); return NULL; } return handler; } /* mimehandler_delete */ void mimehandler_delete(MimeHandler * handler) { _mimehandler_cache_invalidate(handler); config_delete(handler->config); string_delete(handler->filename); string_delete(handler->environment); object_delete(handler); } /* accessors */ /* mimehandler_can_display */ int mimehandler_can_display(MimeHandler * handler) { String const * p; if(mimehandler_is_deleted(handler)) return 0; if((p = config_get(handler->config, SECTION, "OnlyShowIn")) != NULL && (handler->environment == NULL || string_compare(p, handler->environment))) return 0; if((p = config_get(handler->config, SECTION, "NoDisplay")) != NULL && string_compare(p, "true") == 0) return 0; return 1; } /* mimehandler_can_execute */ static int _can_execute_access(String const * path, int mode); static int _can_execute_access_path(String const * path, String const * filename, int mode); int mimehandler_can_execute(MimeHandler * handler) { String const * p; if(mimehandler_get_type(handler) != MIMEHANDLER_TYPE_APPLICATION) return 0; if((p = config_get(handler->config, SECTION, "TryExec")) != NULL && _can_execute_access(p, X_OK) <= 0) return 0; return (mimehandler_get_program(handler) != NULL) ? 1 : 0; } static int _can_execute_access(String const * path, int mode) { int ret = -1; String const * p; String * q; String * last; if(path[0] == '/') return (access(path, mode) == 0) ? 1 : 0; if((p = getenv("PATH")) == NULL) return 0; if((q = string_new(p)) == NULL) return -1; for(p = strtok_r(q, ":", &last); p != NULL; p = strtok_r(NULL, ":", &last)) if((ret = _can_execute_access_path(p, path, mode)) > 0) break; string_delete(q); return ret; } static int _can_execute_access_path(String const * path, String const * filename, int mode) { int ret; String * p; if((p = string_new_append(path, "/", filename, NULL)) == NULL) return -1; ret = (access(p, mode) == 0) ? 1 : 0; string_delete(p); return ret; } /* mimehandler_can_open */ static int _can_open_application(MimeHandler * handler); int mimehandler_can_open(MimeHandler * handler) { switch(mimehandler_get_type(handler)) { case MIMEHANDLER_TYPE_APPLICATION: return _can_open_application(handler); case MIMEHANDLER_TYPE_DIRECTORY: /* let errors be handled by the API user */ return 1; case MIMEHANDLER_TYPE_LINK: return 1; case MIMEHANDLER_TYPE_UNKNOWN: return 0; } return 0; } static int _can_open_application(MimeHandler * handler) { String const * program; String const * p; if(mimehandler_can_execute(handler) == 0) return 0; if((program = mimehandler_get_program(handler)) == NULL) /* XXX should not fail */ return 0; for(p = string_find(program, "%"); p != NULL; p = string_find(p, "%")) switch(*(++p)) { case 'f': case 'F': case 'u': case 'U': return 1; } return 0; } /* mimehandler_get_categories */ String const ** mimehandler_get_categories(MimeHandler * handler) { String ** ret = NULL; size_t cnt = 0; size_t i; String const * p; String * q; String * last; String ** r; if(handler->categories != NULL) return (String const **)handler->categories; if((p = config_get(handler->config, SECTION, "Categories")) == NULL) { if((ret = malloc(sizeof(String *))) == NULL) return NULL; ret[0] = NULL; handler->categories = ret; return (String const **)ret; } if((q = string_new(p)) == NULL) return NULL; for(p = strtok_r(q, ";", &last); p != NULL; p = strtok_r(NULL, ";", &last)) { if(strlen(p) == 0) continue; if((r = realloc(ret, sizeof(*ret) * (cnt + 2))) != NULL) { ret = r; ret[cnt] = string_new(p); } if(r == NULL || ret[cnt] == NULL) { for(i = 0; i < cnt; i++) string_delete(ret[i]); free(ret); return NULL; } cnt++; } string_delete(q); if(ret != NULL) ret[cnt] = NULL; handler->categories = ret; return (String const **)ret; } /* mimehandler_get_comment */ String const * mimehandler_get_comment(MimeHandler * handler, int translate) { String const key[] = "Comment"; if(translate) return _mimehandler_get_translation(handler, key); return config_get(handler->config, SECTION, key); } /* mimehandler_get_environment */ String const * mimehandler_get_environment(MimeHandler * handler) { return handler->environment; } /* mimehandler_get_filename */ String const * mimehandler_get_filename(MimeHandler * handler) { return handler->filename; } /* mimehandler_get_generic_name */ String const * mimehandler_get_generic_name(MimeHandler * handler, int translate) { String const * ret; String const key[] = "GenericName"; if(translate) if((ret = _mimehandler_get_translation(handler, key)) != NULL && string_get_length(ret) != 0) return ret; if((ret = config_get(handler->config, SECTION, key)) != NULL && string_get_length(ret) == 0) ret = NULL; return ret; } /* mimehandler_get_icon */ String const * mimehandler_get_icon(MimeHandler * handler, int translate) { String const key[] = "Icon"; if(translate) return _mimehandler_get_translation(handler, key); return config_get(handler->config, SECTION, key); } /* mimehandler_get_name */ String const * mimehandler_get_name(MimeHandler * handler, int translate) { String const key[] = "Name"; if(translate) return _mimehandler_get_translation(handler, key); return config_get(handler->config, SECTION, key); } /* mimehandler_get_path */ String const * mimehandler_get_path(MimeHandler * handler) { switch(mimehandler_get_type(handler)) { case MIMEHANDLER_TYPE_APPLICATION: case MIMEHANDLER_TYPE_DIRECTORY: return config_get(handler->config, SECTION, "Path"); default: return NULL; } } /* mimehandler_get_program */ String const * mimehandler_get_program(MimeHandler * handler) { switch(mimehandler_get_type(handler)) { case MIMEHANDLER_TYPE_APPLICATION: /* XXX may be a format string */ return config_get(handler->config, SECTION, "Exec"); case MIMEHANDLER_TYPE_DIRECTORY: case MIMEHANDLER_TYPE_UNKNOWN: case MIMEHANDLER_TYPE_LINK: return NULL; } return NULL; } /* mimehandler_get_type */ MimeHandlerType mimehandler_get_type(MimeHandler * handler) { String const * type; struct { String const * name; MimeHandlerType type; } types[] = { { "Application",MIMEHANDLER_TYPE_APPLICATION }, { "Directory", MIMEHANDLER_TYPE_DIRECTORY }, { "Link", MIMEHANDLER_TYPE_LINK } }; size_t i; if((type = config_get(handler->config, SECTION, "Type")) == NULL) return MIMEHANDLER_TYPE_UNKNOWN; for(i = 0; i < sizeof(types) / sizeof(*types); i++) if(string_compare(types[i].name, type) == 0) return types[i].type; return MIMEHANDLER_TYPE_UNKNOWN; } /* mimehandler_get_types */ String const ** mimehandler_get_types(MimeHandler * handler) { String ** ret = NULL; size_t cnt = 0; size_t i; String const * p; String * q; String * last; String ** r; if(handler->types != NULL) return (String const **)handler->types; if(mimehandler_get_type(handler) != MIMEHANDLER_TYPE_APPLICATION) return NULL; if((p = config_get(handler->config, SECTION, "MimeType")) == NULL) { if((ret = malloc(sizeof(String *))) == NULL) return NULL; ret[0] = NULL; handler->types = ret; return (String const **)ret; } if((q = string_new(p)) == NULL) return NULL; for(p = strtok_r(q, ":", &last); p != NULL; p = strtok_r(NULL, ":", &last)) { if(strlen(p) == 0) continue; if((r = realloc(ret, sizeof(*ret) * (cnt + 2))) != NULL) { ret = r; ret[cnt] = string_new(p); } if(r == NULL || ret[cnt] == NULL) { for(i = 0; i < cnt; i++) string_delete(ret[i]); free(ret); return NULL; } cnt++; } string_delete(q); if(ret != NULL) ret[cnt] = NULL; handler->types = ret; return (String const **)ret; } /* mimehandler_get_url */ String const * mimehandler_get_url(MimeHandler * handler) { if(mimehandler_get_type(handler) == MIMEHANDLER_TYPE_LINK) return config_get(handler->config, SECTION, "URL"); return NULL; } /* mimehandler_is_deleted */ int mimehandler_is_deleted(MimeHandler * handler) { String const * p; if((p = config_get(handler->config, SECTION, "Hidden")) != NULL && string_compare(p, "true") == 0) return 1; switch(mimehandler_get_type(handler)) { case MIMEHANDLER_TYPE_APPLICATION: if(mimehandler_can_execute(handler) == 0) return 1; break; default: break; } return 0; } /* mimehandler_set_environment */ int mimehandler_set_environment(MimeHandler * handler, String const * environment) { String * p; if((p = string_new(environment)) == NULL) return -1; string_delete(handler->environment); handler->environment = p; return 0; } /* useful */ /* mimehandler_load */ int mimehandler_load(MimeHandler * handler, String const * filename) { Config * config; String * p; if((config = config_new()) == NULL) return -1; if(config_load(config, filename) != 0 || (p = string_new(filename)) == NULL) { config_delete(config); return -1; } config_delete(handler->config); handler->config = config; string_delete(handler->filename); handler->filename = p; _mimehandler_cache_invalidate(handler); return 0; } /* mimehandler_load_by_name */ static int _load_by_name_path(MimeHandler * handler, String const * name, String const * path); int mimehandler_load_by_name(MimeHandler * handler, String const * name) { int ret; String const fallback[] = ".local/share"; String const * path; String const * homedir; String * p; String const * q; String * last; #ifdef DEBUG fprintf(stderr, "DEBUG: %s(\"%s\")\n", __func__, name); #endif /* use $XDG_DATA_HOME if set and not empty */ if((path = getenv("XDG_DATA_HOME")) != NULL && strlen(path) > 0 && _load_by_name_path(handler, name, path) == 0) return 0; /* fallback to "$HOME/.local/share" */ if((homedir = getenv("HOME")) == NULL) homedir = g_get_home_dir(); if((p = string_new_append(homedir, "/", fallback, NULL)) == NULL) return -1; ret = _load_by_name_path(handler, name, p); string_delete(p); if(ret == 0) return ret; /* read through every XDG application folder */ if((path = getenv("XDG_DATA_DIRS")) == NULL || strlen(path) == 0) { /* XXX avoid duplicates if PREFIX is "/usr/local" */ if(string_compare("/usr/local/share", DATADIR) != 0) path = "/usr/local/share:" DATADIR ":/usr/share"; else path = "/usr/local/share:/usr/share"; } if((p = string_new(path)) == NULL) return -1; for(q = strtok_r(p, ":", &last); q != NULL; q = strtok_r(NULL, ":", &last)) if((ret = _load_by_name_path(handler, name, q)) == 0) break; string_delete(p); #ifdef DEBUG fprintf(stderr, "DEBUG: %s() => %d\n", __func__, ret); #endif return ret; } static int _load_by_name_path(MimeHandler * handler, String const * name, String const * path) { int ret; String const applications[] = "/applications/"; String * filename; if((filename = string_new_append(path, applications, name, EXTENSION, NULL)) == NULL) return -1; ret = mimehandler_load(handler, filename); string_delete(filename); return ret; } /* mimehandler_open */ static int _open_application(MimeHandler * handler, String const * filename); static int _open_application_getcwd(String const * filename, char * buf, size_t size); static int _open_directory(MimeHandler * handler, String const * filename); static String * _open_escape(String const * filename); static int _open_url(MimeHandler * handler, String const * filename); int mimehandler_open(MimeHandler * handler, String const * filename) { #ifdef DEBUG fprintf(stderr, "DEBUG: %s(\"%s\")\n", __func__, filename); #endif switch(mimehandler_get_type(handler)) { case MIMEHANDLER_TYPE_APPLICATION: return _open_application(handler, filename); case MIMEHANDLER_TYPE_DIRECTORY: return _open_directory(handler, filename); case MIMEHANDLER_TYPE_LINK: return _open_url(handler, filename); case MIMEHANDLER_TYPE_UNKNOWN: /* XXX report error */ return -1; } return error_set_code(-ENOSYS, "%s", strerror(ENOSYS)); } static int _open_application(MimeHandler * handler, String const * filename) { int ret = 0; String * f; String * program; String * p; String const * q; String const * name; String const * icon; size_t len; char buf[MAXPATHLEN]; pid_t pid; GError * error = NULL; if((q = mimehandler_get_program(handler)) == NULL) return -1; if((program = string_new(q)) == NULL) return -1; for(p = string_find(program, "%"); p != NULL; p = string_find(p, "%")) { switch(p[1]) { case 'c': /* XXX should not fail */ if((name = mimehandler_get_name(handler, 1)) == NULL) { /* ignore */ memmove(p, &p[2], string_get_length( &p[1])); break; } *p = '\0'; q = p; if((p = string_new_append(program, name, &q[2], NULL)) == NULL) { string_delete(program); return -1; } len = string_get_length(program) - string_get_length(&q[2]); string_delete(program); program = p; p += len; break; case 'f': case 'F': if(filename == NULL) { /* ignore */ memmove(p, &p[2], string_get_length( &p[1])); break; } *p = '\0'; q = p; if(_open_application_getcwd(filename, buf, sizeof(buf)) != 0) { string_delete(program); return -1; } if((f = _open_escape(filename)) == NULL || (p = string_new_append( program, buf, (f[0] != '/') ? "/" : "", f, &q[2], NULL)) == NULL) { string_delete(f); string_delete(program); return -1; } string_delete(f); len = string_get_length(program) - string_get_length(&q[2]); string_delete(program); program = p; p += len; /* XXX avoid multiple inclusion */ filename = NULL; break; case 'u': case 'U': if(filename == NULL) { /* ignore */ memmove(p, &p[2], string_get_length( &p[1])); break; } *p = '\0'; q = p; if(_open_application_getcwd(filename, buf, sizeof(buf)) != 0) { string_delete(program); return -1; } if((f = _open_escape(filename)) == NULL || (p = string_new_append( program, "file://", buf, (f[0] != '/') ? "/" : "", f, &q[2], NULL)) == NULL) { string_delete(f); string_delete(program); return -1; } string_delete(f); len = string_get_length(program) - string_get_length(&q[2]); string_delete(program); program = p; p += len; /* XXX avoid multiple inclusion */ filename = NULL; break; case 'i': if((icon = mimehandler_get_icon(handler, 1)) == NULL) { /* ignore */ memmove(p, &p[2], string_get_length( &p[1])); break; } *p = '\0'; q = p; if((p = string_new_append(program, "--icon ", icon, &q[2], NULL)) == NULL) { string_delete(program); return -1; } len = string_get_length(program) - string_get_length(&q[2]); string_delete(program); program = p; p += len; break; case 'k': if((name = handler->filename) == NULL) { /* ignore */ *(p++) = '"'; *(p++) = '"'; break; } *p = '\0'; q = p; if((p = string_new_append(program, name, &q[2], NULL)) == NULL) { string_delete(program); return -1; } len = string_get_length(program) - string_get_length(&q[2]); string_delete(program); program = p; p += len; break; case '%': /* ignore */ memmove(&p[1], &p[2], string_get_length(&p[1])); break; default: /* XXX skip */ p++; break; } } if((q = mimehandler_get_path(handler)) == NULL) { /* execute the program directly */ if(g_spawn_command_line_async(program, &error) != TRUE) { error_set_code(1, "%s: %s", program, error->message); g_error_free(error); } } else if((pid = fork()) == 0) { /* change the current working directory */ if(chdir(q) != 0) error_set_code(-errno, "%s: %s: %s", program, q, strerror(errno)); else if(g_spawn_command_line_async(program, &error) != TRUE) { error_set_code(1, "%s: %s", program, error->message); g_error_free(error); } exit(125); } else if(pid < 0) { error_set_code(-errno, "%s: %s", program, strerror(errno)); ret = -1; } string_delete(program); return ret; } static int _open_application_getcwd(String const * filename, char * buf, size_t size) { if(size == 0) return -error_set_code(-ENOMEM, "%s", strerror(ENOMEM)); if(filename[0] == '/') { buf[0] = '\0'; return 0; } if(getcwd(buf, size) == NULL) return -error_set_code(errno, "%s", strerror(errno)); return 0; } static int _open_directory(MimeHandler * handler, String const * filename) { /* behave like an application */ return _open_application(handler, filename); } static String * _open_escape(String const * filename) { String * ret; struct { String const * from; String const * to; } escapes[] = { { "\\", "\\\\" }, { "&", "\\&" }, { "|", "\\|" }, { ";", "\\;" }, { "<", "\\<" }, { ">", "\\>" }, { "{", "\\{" }, { "}", "\\}" }, { "!", "\\!" }, { "$", "\\$" }, { "'", "\\'" }, { "\"", "\\\"" }, { " ", "\\ " }, { "\t", "\\\t" }, { "\n", "\\\n" } }; size_t i; if((ret = string_new(filename)) == NULL) return NULL; for(i = 0; i < sizeof(escapes) / sizeof(*escapes); i++) if(string_replace(&ret, escapes[i].from, escapes[i].to) != 0) { string_delete(ret); return NULL; } return ret; } static int _open_url(MimeHandler * handler, String const * filename) { int ret = 0; String const * url; /* XXX open with the default web browser instead */ char * argv[] = { BINDIR "/" PROGNAME_HTMLAPP, "--", NULL, NULL }; const unsigned int flags = 0; GError * error = NULL; if(filename != NULL) return error_set_code(-EINVAL, "%s", strerror(EINVAL)); if((url = mimehandler_get_url(handler)) == NULL) /* XXX report an error? */ return 0; if((argv[2] = string_new(url)) == NULL) return -1; else if(g_spawn_async(NULL, argv, NULL, flags, NULL, NULL, NULL, &error) != TRUE) { ret = -error_set_code(1, "%s: %s", url, error->message); g_error_free(error); } string_delete(argv[2]); return ret; } /* private */ /* accessors */ /* mimehandler_get_translation */ static String const * _translation_strip_country(MimeHandler * handler, String * locale, String const * key); static String const * _translation_strip_encoding(MimeHandler * handler, String * locale, String const * key); static String const * _translation_strip_modifier(MimeHandler * handler, String * locale, String const * key); static String const * _mimehandler_get_translation(MimeHandler * handler, String const * key) { String const * ret; String * locale; if((ret = getenv("LC_MESSAGES")) != NULL || (ret = getenv("LANG")) != NULL) { if((locale = string_new(ret)) == NULL) return NULL; if((ret = _translation_strip_encoding(handler, locale, key)) == NULL && (ret = _translation_strip_modifier(handler, locale, key)) == NULL) ret = _translation_strip_country(handler, locale, key); string_delete(locale); if(ret != NULL && string_get_length(ret) > 0) return ret; } return config_get(handler->config, SECTION, key); } static String const * _translation_strip_country(MimeHandler * handler, String * locale, String const * key) { String const * ret; String * p; if((p = string_find(locale, "_")) == NULL) return NULL; *p = '\0'; if((p = string_new_append(key, "[", locale, "]", NULL)) == NULL) return NULL; ret = config_get(handler->config, SECTION, p); string_delete(p); return ret; } static String const * _translation_strip_encoding(MimeHandler * handler, String * locale, String const * key) { String const * ret; String * p; /* TODO really strip the encoding */ if((p = string_new_append(key, "[", locale, "]", NULL)) == NULL) return NULL; ret = config_get(handler->config, SECTION, p); string_delete(p); return ret; } static String const * _translation_strip_modifier(MimeHandler * handler, String * locale, String const * key) { String const * ret; String * p; if((p = string_find(locale, "@")) == NULL) return NULL; *p = '\0'; if((p = string_new_append(key, "[", locale, "]", NULL)) == NULL) return NULL; ret = config_get(handler->config, SECTION, p); string_delete(p); return ret; } /* useful */ /* mimehandler_cache_invalidate */ static void _mimehandler_cache_invalidate(MimeHandler * handler) { size_t i; if(handler->categories != NULL) for(i = 0; handler->categories[i] != NULL; i++) string_delete(handler->categories[i]); free(handler->categories); handler->categories = NULL; if(handler->types != NULL) for(i = 0; handler->types[i] != NULL; i++) string_delete(handler->types[i]); free(handler->types); handler->types = NULL; }