diff --git a/Makefile.am b/Makefile.am index 708613ca..bfd57196 100644 --- a/Makefile.am +++ b/Makefile.am @@ -263,7 +263,7 @@ endif man_MANS = $(man_sources) -EXTRA_DIST = $(man_sources) $(icons_sources) $(themes_sources) $(script_sources) profrc.example LICENSE.txt +EXTRA_DIST = $(man_sources) $(icons_sources) $(themes_sources) $(script_sources) profrc.example LICENSE.txt README.md CHANGELOG if INCLUDE_GIT_VERSION EXTRA_DIST += .git/HEAD .git/index diff --git a/src/command/cmd_ac.c b/src/command/cmd_ac.c index 56baafc8..544719d4 100644 --- a/src/command/cmd_ac.c +++ b/src/command/cmd_ac.c @@ -184,6 +184,7 @@ static Autocomplete console_ac; static Autocomplete console_msg_ac; static Autocomplete autoping_ac; static Autocomplete plugins_ac; +static Autocomplete plugins_sourcepath_ac; static Autocomplete plugins_load_ac; static Autocomplete plugins_unload_ac; static Autocomplete plugins_reload_ac; @@ -714,6 +715,11 @@ cmd_ac_init(void) autocomplete_add(plugins_ac, "unload"); autocomplete_add(plugins_ac, "reload"); autocomplete_add(plugins_ac, "python_version"); + autocomplete_add(plugins_ac, "sourcepath"); + + plugins_sourcepath_ac = autocomplete_new(); + autocomplete_add(plugins_sourcepath_ac, "set"); + autocomplete_add(plugins_sourcepath_ac, "clear"); filepath_ac = autocomplete_new(); @@ -1011,6 +1017,7 @@ cmd_ac_reset(ProfWin *window) autocomplete_reset(console_msg_ac); autocomplete_reset(autoping_ac); autocomplete_reset(plugins_ac); + autocomplete_reset(plugins_sourcepath_ac); autocomplete_reset(blocked_ac); autocomplete_reset(tray_ac); autocomplete_reset(presence_ac); @@ -2025,10 +2032,21 @@ _plugins_autocomplete(ProfWin *window, const char *const input) { char *result = NULL; + if (strncmp(input, "/plugins sourcepath set ", 24) == 0) { + return cmd_ac_complete_filepath(input, "/plugins sourcepath set"); + } + if (strncmp(input, "/plugins install ", 17) == 0) { return cmd_ac_complete_filepath(input, "/plugins install"); } + if (strncmp(input, "/plugins sourcepath ", 20) == 0) { + result = autocomplete_param_with_ac(input, "/plugins sourcepath", plugins_sourcepath_ac, TRUE); + if (result) { + return result; + } + } + if (strncmp(input, "/plugins load ", 14) == 0) { if (plugins_load_ac == NULL) { plugins_load_ac = autocomplete_new(); diff --git a/src/command/cmd_defs.c b/src/command/cmd_defs.c index fb33dac9..8f8ced09 100644 --- a/src/command/cmd_defs.c +++ b/src/command/cmd_defs.c @@ -2046,26 +2046,38 @@ static struct cmd_t command_defs[] = }, { "/plugins", - parse_args, 0, 2, NULL, - CMD_NOSUBFUNCS + parse_args, 0, 3, NULL, + CMD_SUBFUNCS( + { "sourcepath", cmd_plugins_sourcepath }, + { "install", cmd_plugins_install }, + { "load", cmd_plugins_load }, + { "unload", cmd_plugins_unload }, + { "reload", cmd_plugins_reload }, + { "python_version", cmd_plugins_python_version }) CMD_MAINFUNC(cmd_plugins) CMD_NOTAGS CMD_SYN( "/plugins", - "/plugins install ", - "/plugins unload ", - "/plugins load ", + "/plugins sourcepath set ", + "/plugins sourcepath clear", + "/plugins install []", + "/plugins unload []", + "/plugins load []", "/plugins reload []", "/plugins python_version") CMD_DESC( "Manage plugins. Passing no arguments lists currently loaded plugins.") CMD_ARGS( - { "install ", "Install file to plugins directory, and load or reload the plugin." }, - { "load ", "Load a plugin that already exists in the plugin directory." }, - { "unload ", "Unload a loaded plugin." }, - { "reload []", "Reload a plugin, passing no argument will reload all plugins." }, - { "python_version", "Show the Python interpreter version." }) + { "sourcepath set ", "Set the default path to install plugins from, will be used if no arg is passed to /plugins install." }, + { "sourcepath clear", "Clear the default plugins source path." }, + { "install []", "Install a plugin, or all plugins found in a directory (recursive). Passing no argument will use the sourcepath if one is set." }, + { "load []", "Load a plugin that already exists in the plugin directory, passing no argument loads all found plugins." }, + { "unload []", "Unload a loaded plugin, passing no argument will unload all plugins." }, + { "reload []", "Reload a plugin, passing no argument will reload all plugins." }, + { "python_version", "Show the Python interpreter version." }) CMD_EXAMPLES( + "/plugins sourcepath set /home/meee/projects/profanity-plugins", + "/plugins install", "/plugins install /home/steveharris/Downloads/metal.py", "/plugins load browser.py", "/plugins unload say.py", diff --git a/src/command/cmd_funcs.c b/src/command/cmd_funcs.c index b8657e59..3df17079 100644 --- a/src/command/cmd_funcs.c +++ b/src/command/cmd_funcs.c @@ -6212,44 +6212,91 @@ cmd_xa(ProfWin *window, const char *const command, gchar **args) } gboolean -cmd_plugins(ProfWin *window, const char *const command, gchar **args) +cmd_plugins_sourcepath(ProfWin *window, const char *const command, gchar **args) { - if (g_strcmp0(args[0], "install") == 0) { - char *filename = args[1]; - if (filename == NULL) { + if (args[1] == NULL) { + char *sourcepath = prefs_get_string(PREF_PLUGINS_SOURCEPATH); + if (sourcepath) { + cons_show("Current plugins sourcepath: %s", sourcepath); + prefs_free_string(sourcepath); + } else { + cons_show("Plugins sourcepath not currently set."); + } + return TRUE; + } + + if (g_strcmp0(args[1], "clear") == 0) { + prefs_set_string(PREF_PLUGINS_SOURCEPATH, NULL); + cons_show("Plugins sourcepath cleared."); + return TRUE; + } + + if (g_strcmp0(args[1], "set") == 0) { + char *path = args[2]; + if (path == NULL) { cons_bad_cmd_usage(command); return TRUE; } // expand ~ to $HOME - if (filename[0] == '~' && filename[1] == '/') { - if (asprintf(&filename, "%s/%s", getenv("HOME"), filename+2) == -1) { + if (path[0] == '~' && path[1] == '/') { + if (asprintf(&path, "%s/%s", getenv("HOME"), path+2) == -1) { return TRUE; } } else { - filename = strdup(filename); + path = strdup(path); } - if (access(filename, R_OK) != 0) { - cons_show("File not found: %s", filename); - free(filename); + if (!is_dir(path)) { + cons_show("Plugins sourcepath must be a directory."); return TRUE; } - if (!is_regular_file(filename)) { - cons_show("Not a file: %s", filename); - free(filename); + cons_show("Setting plugins sourcepath: %s", path); + prefs_set_string(PREF_PLUGINS_SOURCEPATH, path); + return TRUE; + } + + cons_bad_cmd_usage(command); + return TRUE; +} + +gboolean +cmd_plugins_install(ProfWin *window, const char *const command, gchar **args) +{ + char *path = args[1]; + if (path == NULL) { + char* sourcepath = prefs_get_string(PREF_PLUGINS_SOURCEPATH); + if (sourcepath) { + path = strdup(sourcepath); + prefs_free_string(sourcepath); + } else { + cons_show("Either a path must be provided or the sourcepath property must be set, see /help plugins"); return TRUE; } + } else if (path[0] == '~' && path[1] == '/') { + if (asprintf(&path, "%s/%s", getenv("HOME"), path+2) == -1) { + return TRUE; + } + } else { + path = strdup(path); + } - if (!g_str_has_suffix(filename, ".py") && !g_str_has_suffix(filename, ".so")) { + if (access(path, R_OK) != 0) { + cons_show("File not found: %s", path); + free(path); + return TRUE; + } + + if (is_regular_file(path)) { + if (!g_str_has_suffix(path, ".py") && !g_str_has_suffix(path, ".so")) { cons_show("Plugins must have one of the following extensions: '.py' '.so'"); - free(filename); + free(path); return TRUE; } - gchar *plugin_name = g_path_get_basename(filename); - gboolean result = plugins_install(plugin_name, filename); + gchar *plugin_name = g_path_get_basename(path); + gboolean result = plugins_install(plugin_name, path); if (result) { cons_show("Plugin installed: %s", plugin_name); } else { @@ -6257,75 +6304,145 @@ cmd_plugins(ProfWin *window, const char *const command, gchar **args) } g_free(plugin_name); - free(filename); - return TRUE; - } else if (g_strcmp0(args[0], "load") == 0) { - if (args[1] == NULL) { - cons_bad_cmd_usage(command); - return TRUE; - } - gboolean res = plugins_load(args[1]); - if (res) { - cons_show("Loaded plugin: %s", args[1]); - } else { - cons_show("Failed to load plugin: %s", args[1]); - } - - return TRUE; - } else if (g_strcmp0(args[0], "unload") == 0) { - if (args[1] == NULL) { - cons_bad_cmd_usage(command); - return TRUE; - } - gboolean res = plugins_unload(args[1]); - if (res) { - cons_show("Unloaded plugin: %s", args[1]); - } else { - cons_show("Failed to unload plugin: %s", args[1]); - } - - return TRUE; - } else if (g_strcmp0(args[0], "reload") == 0) { - if (args[1] == NULL) { - plugins_reload_all(); - cons_show("Reloaded all plugins"); - } else { - gboolean res = plugins_reload(args[1]); - if (res) { - cons_show("Reloaded plugin: %s", args[1]); - } else { - cons_show("Failed to reload plugin: %s", args[1]); - } - } - - return TRUE; - } else if (g_strcmp0(args[0], "python_version") == 0) { -#ifdef HAVE_PYTHON - const char *version = python_get_version(); - cons_show("Python version:"); - cons_show("%s", version); -#else - cons_show("This build does not support pytyon plugins."); -#endif - return TRUE; - - } else { - GList *plugins = plugins_loaded_list(); - if (plugins == NULL) { - cons_show("No plugins installed."); - return TRUE; - } - - GList *curr = plugins; - cons_show("Installed plugins:"); - while (curr) { - cons_show(" %s", curr->data); - curr = g_list_next(curr); - } - g_list_free(plugins); - + free(path); return TRUE; } + + if (is_dir(path)) { + PluginsInstallResult* result = plugins_install_all(path); + if (result->installed || result->failed) { + if (result->installed) { + cons_show(""); + cons_show("Installed plugins:"); + GSList *curr = result->installed; + while (curr) { + cons_show(" %s", curr->data); + curr = g_slist_next(curr); + } + } + if (result->failed) { + cons_show(""); + cons_show("Failed installs:"); + GSList *curr = result->failed; + while (curr) { + cons_show(" %s", curr->data); + curr = g_slist_next(curr); + } + } + } else { + cons_show("No plugins found in: %s", path); + } + free(path); + plugins_free_install_result(result); + return TRUE; + } + + cons_show("Argument must be a file or directory."); + return TRUE; +} + +gboolean +cmd_plugins_load(ProfWin *window, const char *const command, gchar **args) +{ + if (args[1] == NULL) { + GSList *loaded = plugins_load_all(); + if (loaded) { + cons_show("Loaded plugins:"); + GSList *curr = loaded; + while (curr) { + cons_show(" %s", curr->data); + curr = g_slist_next(curr); + } + g_slist_free_full(loaded, g_free); + } else { + cons_show("No plugins loaded."); + } + return TRUE; + } + + gboolean res = plugins_load(args[1]); + if (res) { + cons_show("Loaded plugin: %s", args[1]); + } else { + cons_show("Failed to load plugin: %s", args[1]); + } + + return TRUE; +} + +gboolean +cmd_plugins_unload(ProfWin *window, const char *const command, gchar **args) +{ + if (args[1] == NULL) { + gboolean res = plugins_unload_all(); + if (res) { + cons_show("Unloaded all plugins."); + } else { + cons_show("No plugins unloaded."); + } + return TRUE; + } + + gboolean res = plugins_unload(args[1]); + if (res) { + cons_show("Unloaded plugin: %s", args[1]); + } else { + cons_show("Failed to unload plugin: %s", args[1]); + } + + return TRUE; +} + +gboolean +cmd_plugins_reload(ProfWin *window, const char *const command, gchar **args) +{ + if (args[1] == NULL) { + plugins_reload_all(); + cons_show("Reloaded all plugins"); + return TRUE; + } + + gboolean res = plugins_reload(args[1]); + if (res) { + cons_show("Reloaded plugin: %s", args[1]); + } else { + cons_show("Failed to reload plugin: %s", args[1]); + } + + return TRUE; +} + +gboolean +cmd_plugins_python_version(ProfWin *window, const char *const command, gchar **args) +{ +#ifdef HAVE_PYTHON + const char *version = python_get_version(); + cons_show("Python version:"); + cons_show("%s", version); +#else + cons_show("This build does not support pytyon plugins."); +#endif + return TRUE; +} + +gboolean +cmd_plugins(ProfWin *window, const char *const command, gchar **args) +{ + GList *plugins = plugins_loaded_list(); + if (plugins == NULL) { + cons_show("No plugins installed."); + return TRUE; + } + + GList *curr = plugins; + cons_show("Installed plugins:"); + while (curr) { + cons_show(" %s", curr->data); + curr = g_list_next(curr); + } + g_list_free(plugins); + + return TRUE; } gboolean diff --git a/src/command/cmd_funcs.h b/src/command/cmd_funcs.h index b16c187a..2f0e0bac 100644 --- a/src/command/cmd_funcs.h +++ b/src/command/cmd_funcs.h @@ -158,7 +158,15 @@ gboolean cmd_script(ProfWin *window, const char *const command, gchar **args); gboolean cmd_export(ProfWin *window, const char *const command, gchar **args); gboolean cmd_charset(ProfWin *window, const char *const command, gchar **args); gboolean cmd_console(ProfWin *window, const char *const command, gchar **args); + gboolean cmd_plugins(ProfWin *window, const char *const command, gchar **args); +gboolean cmd_plugins_sourcepath(ProfWin *window, const char *const command, gchar **args); +gboolean cmd_plugins_install(ProfWin *window, const char *const command, gchar **args); +gboolean cmd_plugins_load(ProfWin *window, const char *const command, gchar **args); +gboolean cmd_plugins_unload(ProfWin *window, const char *const command, gchar **args); +gboolean cmd_plugins_reload(ProfWin *window, const char *const command, gchar **args); +gboolean cmd_plugins_python_version(ProfWin *window, const char *const command, gchar **args); + gboolean cmd_blocked(ProfWin *window, const char *const command, gchar **args); gboolean cmd_account(ProfWin *window, const char *const command, gchar **args); diff --git a/src/common.c b/src/common.c index ef7129cd..898f62fa 100644 --- a/src/common.c +++ b/src/common.c @@ -536,3 +536,45 @@ prof_occurrences(const char *const needle, const char *const haystack, int offse return *result; } +int +is_regular_file(const char *path) +{ + struct stat st; + stat(path, &st); + return S_ISREG(st.st_mode); +} + +int +is_dir(const char *path) +{ + struct stat st; + stat(path, &st); + return S_ISDIR(st.st_mode); +} + +void +get_file_paths_recursive(const char *path, GSList **contents) +{ + if (!is_dir(path)) { + return; + } + + GDir* directory = g_dir_open(path, 0, NULL); + const gchar *entry = g_dir_read_name(directory); + while (entry) { + GString *full = g_string_new(path); + if (!g_str_has_suffix(full->str, "/")) { + g_string_append(full, "/"); + } + g_string_append(full, entry); + + if (is_dir(full->str)) { + get_file_paths_recursive(full->str, contents); + } else if (is_regular_file(full->str)) { + *contents = g_slist_append(*contents, full->str); + } + + g_string_free(full, FALSE); + entry = g_dir_read_name(directory); + } +} diff --git a/src/common.h b/src/common.h index c2317390..8c42ae52 100644 --- a/src/common.h +++ b/src/common.h @@ -123,4 +123,8 @@ gboolean is_notify_enabled(void); GSList* prof_occurrences(const char *const needle, const char *const haystack, int offset, gboolean whole_word, GSList **result); +int is_regular_file(const char *path); +int is_dir(const char *path); +void get_file_paths_recursive(const char *directory, GSList **contents); + #endif diff --git a/src/config/preferences.c b/src/config/preferences.c index f759da35..6dc8ef32 100644 --- a/src/config/preferences.c +++ b/src/config/preferences.c @@ -59,6 +59,7 @@ #define PREF_GROUP_OTR "otr" #define PREF_GROUP_PGP "pgp" #define PREF_GROUP_MUC "muc" +#define PREF_GROUP_PLUGINS "plugins" #define INPBLOCK_DEFAULT 1000 @@ -665,27 +666,27 @@ prefs_get_tray_timer(void) gchar** prefs_get_plugins(void) { - if (!g_key_file_has_group(prefs, "plugins")) { + if (!g_key_file_has_group(prefs, PREF_GROUP_PLUGINS)) { return NULL; } - if (!g_key_file_has_key(prefs, "plugins", "load", NULL)) { + if (!g_key_file_has_key(prefs, PREF_GROUP_PLUGINS, "load", NULL)) { return NULL; } - return g_key_file_get_string_list(prefs, "plugins", "load", NULL, NULL); + return g_key_file_get_string_list(prefs, PREF_GROUP_PLUGINS, "load", NULL, NULL); } void prefs_add_plugin(const char *const name) { - conf_string_list_add(prefs, "plugins", "load", name); + conf_string_list_add(prefs, PREF_GROUP_PLUGINS, "load", name); _save_prefs(); } void prefs_remove_plugin(const char *const name) { - conf_string_list_remove(prefs, "plugins", "load", name); + conf_string_list_remove(prefs, PREF_GROUP_PLUGINS, "load", name); _save_prefs(); } @@ -1614,6 +1615,8 @@ _get_group(preference_t pref) return PREF_GROUP_PGP; case PREF_BOOKMARK_INVITE: return PREF_GROUP_MUC; + case PREF_PLUGINS_SOURCEPATH: + return PREF_GROUP_PLUGINS; default: return NULL; } @@ -1814,6 +1817,8 @@ _get_key(preference_t pref) return "console.chat"; case PREF_BOOKMARK_INVITE: return "bookmark.invite"; + case PREF_PLUGINS_SOURCEPATH: + return "sourcepath"; default: return NULL; } diff --git a/src/config/preferences.h b/src/config/preferences.h index dbc74148..cc605c79 100644 --- a/src/config/preferences.h +++ b/src/config/preferences.h @@ -141,6 +141,7 @@ typedef enum { PREF_CONSOLE_PRIVATE, PREF_CONSOLE_CHAT, PREF_BOOKMARK_INVITE, + PREF_PLUGINS_SOURCEPATH, } preference_t; typedef struct prof_alias_t { diff --git a/src/plugins/plugins.c b/src/plugins/plugins.c index eb14f796..46297d5d 100644 --- a/src/plugins/plugins.c +++ b/src/plugins/plugins.c @@ -163,6 +163,43 @@ plugins_init(void) return; } +void +plugins_free_install_result(PluginsInstallResult *result) +{ + if (!result) { + return; + } + g_slist_free_full(result->installed, free); + g_slist_free_full(result->failed, free); +} + +PluginsInstallResult* +plugins_install_all(const char *const path) +{ + PluginsInstallResult *result = malloc(sizeof(PluginsInstallResult)); + result->installed = NULL; + result->failed = NULL; + GSList *contents = NULL; + get_file_paths_recursive(path, &contents); + + GSList *curr = contents; + while (curr) { + if (g_str_has_suffix(curr->data, ".py") || g_str_has_suffix(curr->data, ".so")) { + gchar *plugin_name = g_path_get_basename(curr->data); + if (plugins_install(plugin_name, curr->data)) { + result->installed = g_slist_append(result->installed, strdup(curr->data)); + } else { + result->failed = g_slist_append(result->failed, strdup(curr->data)); + } + } + curr = g_slist_next(curr); + } + + g_slist_free_full(contents, g_free); + + return result; +} + gboolean plugins_install(const char *const plugin_name, const char *const filename) { @@ -187,6 +224,23 @@ plugins_install(const char *const plugin_name, const char *const filename) return result; } +GSList* +plugins_load_all(void) +{ + GSList *plugins = plugins_unloaded_list(); + GSList *loaded = NULL; + GSList *curr = plugins; + while (curr) { + if (plugins_load(curr->data)) { + loaded = g_slist_append(loaded, strdup(curr->data)); + } + curr = g_slist_next(curr); + } + g_slist_free_full(plugins, g_free); + + return loaded; +} + gboolean plugins_load(const char *const name) { @@ -224,6 +278,32 @@ plugins_load(const char *const name) } } +gboolean +plugins_unload_all(void) +{ + gboolean result = FALSE; + GList *plugin_names = g_hash_table_get_keys(plugins); + GList *plugin_names_dup = NULL; + GList *curr = plugin_names; + while (curr) { + plugin_names_dup = g_list_append(plugin_names_dup, strdup(curr->data)); + curr = g_list_next(curr); + } + g_list_free(plugin_names); + + curr = plugin_names_dup; + while (curr) { + if (plugins_unload(curr->data)) { + result = TRUE; + } + curr = g_list_next(curr); + } + + g_list_free_full(plugin_names_dup, free); + + return result; +} + gboolean plugins_unload(const char *const name) { diff --git a/src/plugins/plugins.h b/src/plugins/plugins.h index 2ba99bba..f75a1e3b 100644 --- a/src/plugins/plugins.h +++ b/src/plugins/plugins.h @@ -44,6 +44,11 @@ typedef enum { LANG_C } lang_t; +typedef struct prof_plugins_install_t { + GSList *installed; + GSList *failed; +} PluginsInstallResult; + typedef struct prof_plugin_t { char *name; lang_t lang; @@ -109,9 +114,14 @@ char* plugins_autocomplete(const char *const input); void plugins_reset_autocomplete(void); void plugins_shutdown(void); +void plugins_free_install_result(PluginsInstallResult *result); + gboolean plugins_install(const char *const plugin_name, const char *const filename); +PluginsInstallResult* plugins_install_all(const char *const path); gboolean plugins_load(const char *const name); +GSList* plugins_load_all(void); gboolean plugins_unload(const char *const name); +gboolean plugins_unload_all(void); gboolean plugins_reload(const char *const name); void plugins_reload_all(void); diff --git a/src/tools/http_upload.c b/src/tools/http_upload.c index c1ac9fdf..6e5dd27c 100644 --- a/src/tools/http_upload.c +++ b/src/tools/http_upload.c @@ -52,6 +52,7 @@ #include "config/preferences.h" #include "ui/ui.h" #include "ui/window.h" +#include "common.h" #define FALLBACK_MIMETYPE "application/octet-stream" #define FALLBACK_CONTENTTYPE_HEADER "Content-Type: application/octet-stream" @@ -330,10 +331,3 @@ off_t file_size(const char* const filename) stat(filename, &st); return st.st_size; } - -int is_regular_file(const char *filename) -{ - struct stat st; - stat(filename, &st); - return S_ISREG(st.st_mode); -} diff --git a/src/tools/http_upload.h b/src/tools/http_upload.h index 15281fa5..ae8f8223 100644 --- a/src/tools/http_upload.h +++ b/src/tools/http_upload.h @@ -62,6 +62,5 @@ void* http_file_put(void *userdata); char* file_mime_type(const char* const file_name); off_t file_size(const char* const file_name); -int is_regular_file(const char *filename); #endif diff --git a/tests/unittests/tools/stub_http_upload.c b/tests/unittests/tools/stub_http_upload.c index c0b4e979..508d3f8b 100644 --- a/tests/unittests/tools/stub_http_upload.c +++ b/tests/unittests/tools/stub_http_upload.c @@ -24,6 +24,5 @@ void* http_file_put(void *userdata) {} char* file_mime_type(const char* const file_name) {} off_t file_size(const char* const file_name) {} -int is_regular_file(const char *filename) {} #endif