Branch: refs/heads/master Author: Frank Lanitz frank@frank.uvena.de Committer: Frank Lanitz frank@frank.uvena.de Date: Sun, 21 Feb 2016 08:49:36 UTC Commit: 79fc1fafee53a18633e4c2e612345fb2a9aa6d61 https://github.com/geany/geany-plugins/commit/79fc1fafee53a18633e4c2e612345f...
Log Message: ----------- Merge pull request #338 from b4n/geniuspaste/customizable
[GeniusPaste] Rewrite to make pastebins customizable
Modified Paths: -------------- build/geniuspaste.m4 geniuspaste/Makefile.am geniuspaste/README geniuspaste/data/Makefile.am geniuspaste/data/codepad.org.conf geniuspaste/data/dpaste.de.conf geniuspaste/data/fpaste.org.conf geniuspaste/data/paste.debian.net.conf geniuspaste/data/pastebin.geany.org.conf geniuspaste/data/sprunge.us.conf geniuspaste/data/tinypaste.com.conf geniuspaste/src/Makefile.am geniuspaste/src/geniuspaste.c
Modified: build/geniuspaste.m4 1 lines changed, 1 insertions(+), 0 deletions(-) =================================================================== @@ -9,6 +9,7 @@ AC_DEFUN([GP_CHECK_GENIUSPASTE],
AC_CONFIG_FILES([ geniuspaste/Makefile + geniuspaste/data/Makefile geniuspaste/src/Makefile ]) ])
Modified: geniuspaste/Makefile.am 2 lines changed, 1 insertions(+), 1 deletions(-) =================================================================== @@ -1,4 +1,4 @@ include $(top_srcdir)/build/vars.auxfiles.mk
-SUBDIRS = src +SUBDIRS = data src plugin = geniuspaste
Modified: geniuspaste/README 116 lines changed, 109 insertions(+), 7 deletions(-) =================================================================== @@ -5,8 +5,9 @@ GeniusPaste Plugin
About ----- -This plugin allows the user to paste the code from Geany into five different -pastebins. At the moment it supports this services: +This plugin allows the user to paste code from Geany into a configured +pastebin service. At the moment it ships with builtin support these pastebin +services, but more can be added:
* codepad.org * tinypaste.com @@ -14,19 +15,20 @@ pastebins. At the moment it supports this services: * dpaste.de * sprunge.us
-GeniusPaste detects automatically the syntax of the code and paste it +GeniusPaste detects automatically the syntax of the code and pastes it with syntax highlighting enabled. It can also display the pasted code opening a new browser tab.
Issues ------ -The API of the pastebin services can be updated in every moment. It +The API of the pastebin services can be updated at every moment. It may happen that GeniusPaste plugin could use an outdated API that meanwhile has been deprecated.
If you get a unexpected API response during the paste process (practically -if the plugin doesn't return a link to the pasted code) write a email to me -and warn me about the changes: +if the plugin doesn't return a link to the pasted code), you will need to fix +that pastebin service configuration to use its new API. If it is a +configuration shipped with the plugin, please report the issue to:
<enrico(dot)trt(at)gmail(dot)com>
@@ -34,12 +36,112 @@ Requirements ------------ * GTK+ >= 2.12 * libsoup 2.4 >= 2.4.0 - + Installation ------------ This plugin is part of the geany-plugins project. See the README file of that package.
+Pastebin configuration +---------------------- + +Configuration for the pastebin services is looked up in one of data directories +as follows, in order: ``$GEANY_CONFIG_DIR/plugins/geniuspaste/pastebins/`` and +``$PREFIX/share/geany-plugins/geniuspaste/pastebins/`` (where +``$GEANY_CONFIG_DIR`` is ``$HOME/.config/geany`` on *NIX; and ``$PREFIX`` is +generally either ``/usr/`` or ``/usr/local/`` under *NIX). + +If more than one configuration file declare a pastebin service of the same +name, only the first one found is used. This way, one can easily override the +configuration in a system directory with a custom one in a user directory. + +Format +^^^^^^ + +The pastebin configuration format uses an INI-style syntax. + +Placeholders +++++++++++++ + +Values from the `[format] section`_ and the *replace* key in the `[parse] +section`_ can contain references to placeholders with the syntax ``%name%`` +(i.e. ``%contents%``). +Custom placeholders can be defined in the `[defaults] section`_. + +The builtin placeholders are: + +*contents* + The data to actually paste. +*language* + The language of the current document, as mapped through the `[languages] + section`_. +*title* + The base name of the current document. +*user* + The configured author name. + +*[pastebin]* section +++++++++++++++++++++ + +The *pastebin* section is required, and must contain at least the *name* and +*url* keys. + +*name* + The name of the pastebin service, as displayed to the user. This key is + required. +*url* + The URL to which submit the data. This key is required. +*method* + The HTTP method to use to submit the data. Defaults to ``POST``. + +*[format]* section +++++++++++++++++++ + +The *format* section describes the fields of the form to send to the pastebin. +Each key in this section is a field, and each value that field's value. + +*[parse]* section ++++++++++++++++++ + +If the *parse* section is declared, it defines the regular expression used to +parse the raw response body from the pastebin service and build the final +paste URL. + +If this section doesn't exist, the URI of the response itself is used, after +any possible redirection. This is usually good for non-API pastebin services, +where the server redirects the user to the paste page. + +**Warning:** If the *parse* section exists, it will be used, no matter whether +any key is actually defined. This means that a ``[parse]`` line is enough to +enable response body parsing, and it will use the default *search* and +*replace* settings. + +*search* + A regular expression (PCRE) pattern to match against the pastebin + service's raw response data. + Defaults to ``^[[:space:]]*(.+?)[[:space:]]*$``, e.g. capture everything + but the leading and trailing whitespaces. +*replace* + The final URL, with regular expression capture groups from *search* + expanded. Group references use the numeric, one-digit syntax: ``\0`` + refers to the whole matched text, ``\1`` to the first captured group, and + so on. Defaults to ``\1``, which works nicely with the default *search* + pattern. + +*[defaults]* section +++++++++++++++++++++ + +The *defaults* section defines default values for some builtin `placeholders`_ +or define new placeholders. + +*[languages]* section ++++++++++++++++++++++ + +The *languages* section maps Geany's filetype names (the keys) to the pastebin +service's own name for this language. This allows to translate the filetypes +Geany knows about to ones the pastebin service understands. + + License ------- GeniusPaste is distributed under the terms of the GNU General Public
Modified: geniuspaste/data/Makefile.am 12 lines changed, 12 insertions(+), 0 deletions(-) =================================================================== @@ -0,0 +1,12 @@ +include $(top_srcdir)/build/vars.docs.mk +plugin = geniuspaste + +pastebinsdir = $(plugindatadir)/pastebins +dist_pastebins_DATA = \ + codepad.org.conf \ + dpaste.de.conf \ + fpaste.org.conf \ + pastebin.geany.org.conf \ + paste.debian.net.conf \ + sprunge.us.conf \ + tinypaste.com.conf
Modified: geniuspaste/data/codepad.org.conf 30 lines changed, 30 insertions(+), 0 deletions(-) =================================================================== @@ -0,0 +1,30 @@ +[pastebin] +name=codepad.org +url=http://codepad.org/ + +[defaults] +language=Plain Text + +[format] +lang=%language% +code=%contents% +submit=Submit +#private=False +#run=True + +# map GeanyFileType=PastebinFileType +[languages] +# as of 2016-01-15 +None=Plain Text +C=C +C++=C++ +D=D +Haskell=Haskell +Lua=Lua +CAML=OCaml +PHP=PHP +Perl=Perl +Python=Python +Ruby=Ruby +Scheme=Scheme +Tcl=Tcl
Modified: geniuspaste/data/dpaste.de.conf 64 lines changed, 64 insertions(+), 0 deletions(-) =================================================================== @@ -0,0 +1,64 @@ +[pastebin] +name=dpaste.de +url=http://dpaste.de/api/ + +[format] +title=%title% +content=%contents% +lexer=%language% +#expires=%expire% + +[parse] +search=^"(.+)"$ +replace=\1 + +[defaults] +# default language to plain which means "Code" +language=plain +expire=604800 + +# map GeanyFileType=PastebinFileType +[languages] +# list as of 2016-01-14 +None=text +ActionScript=as +C=c +# better than nothing +C++=c +#CAML=ocaml +Clojure=clojure +COBOL=cobol +Conf=ini +CSS=css +CUDA=cuda +Diff=diff +Docbook=xml +Erlang=erlang +F77=fortran +Fortran=fortran +GLSL=C +Go=go +Haskell=haskell +HTML=html +Java=java +Javascript=js +JSON=json +LaTeX=tex +Lua=lua +Make=make +Matlab/Octave=matlab +Objective-C=objc +Perl=perl +PHP=php +PowerShell=powershell +Python=python +reStructuredText=rst +Ruby=rb +Rust=rust +Scala=scala +Sh=bash +SQL=sql +Tcl=tcl +XML=xml +YAML=yaml +Zephir=php
Modified: geniuspaste/data/fpaste.org.conf 67 lines changed, 67 insertions(+), 0 deletions(-) =================================================================== @@ -0,0 +1,67 @@ +[pastebin] +name=fpaste.org +url=http://fpaste.org/ + +[format] +paste_data=%contents% +paste_lang=%language% +api_submit=true +mode=xml + +# Optional stuff + +paste_user=%user% +# expiration in seconds +#paste_expire=0 + +[parse] +search=<id>(.+?)</id> +replace=http://fpaste.org/%5C1 + +# map GeanyFileType=PastebinFileType +[languages] +# map for GeSHi 2015-01-14 +ActionScript=Actionscript +Ada=ADA +ASM=ASM +C=C +C#=C# +C++=C++ +COBOL=COBOL +Conf=INI +CSS=CSS +CUDA=C +Cython=Python +D=D +Diff=Diff +Docbook=XML +F77=Fortran +Fortran=Fortran +FreeBasic=FreeBasic +GLSL=C +Haskell=Haskell +HTML=HTML +Java=Java +Javascript=Javascript +JSON=Javascript +LaTeX=LaTeX +Lisp=Lisp +Lua=Lua +Make=Make +NSIS=NSIS +Objective-C=Objective-C +Pascal=Pascal +Perl=Perl +PHP=PHP +Po=GetText +PowerShell=PowerShell +Prolog=Prolog +Python=Python +Ruby=Ruby +Scala=Scala +Sh=Bash +SQL=SQL +Tcl=TCL +Verilog=Verilog +VHDL=VHDL +XML=XML
Modified: geniuspaste/data/paste.debian.net.conf 67 lines changed, 67 insertions(+), 0 deletions(-) =================================================================== @@ -0,0 +1,67 @@ +[pastebin] +name=paste.debian.net +url=http://paste.debian.net/ + +[format] +code=%contents% +poster=%user% +lang=%language% +# expire is required +expire=%expire% +#private=0 +#wrap=0 + +[defaults] +language=-1 +expire=604800 + +# map GeanyFileType=PastebinFileType +[languages] +# list as of 2016-01-16 +Ada=ada +ActionScript=as +Batch=bat +C=c +C++=cpp +C#=csharp +CAML=ocaml +Clojure=clojure +CMake=cmake +CoffeeScript=coffee-script +Conf=ini +CSS=css +Cython=cython +D=d +Diff=diff +Docbook=XML +Erlang=erlang +F77=fortran +Fortran=fortran +GLSL=glsl +Go=go +Haskell=haskell +HTML=html+php +Java=java +Javascript=js +JSON=json +LaTeX=tex +Lua=lua +Lisp=common-lisp +Make=make +Matlab/Octave=octave +Objective-C=objective-c +Perl=perl +PHP=html+php +Po=pot +PowerShell=powershell +Python=python +reStructuredText=rst +Ruby=rb +Scala=scala +Sh=bash +SQL=sql +Tcl=tcl +Vala=vala +VHDL=vhdl +XML=xml +YAML=yaml
Modified: geniuspaste/data/pastebin.geany.org.conf 35 lines changed, 35 insertions(+), 0 deletions(-) =================================================================== @@ -0,0 +1,35 @@ +[pastebin] +name=pastebin.geany.org +url=http://pastebin.geany.org/api/ + +[format] +content=%contents% +author=%user% +title=%title% +lexer=%language% +#expire_options=%expire% + +[parse] +search=^.+$ +replace=\0 + +[defaults] +language=text +expire=2592000 + +# map GeanyFileType=PastebinFileType +[languages] +# list as of 2016-01-14 +C=c +# better than nothing +C++=c +CSS=css +Diff=diff +HTML=html +Javascript=js +Perl=perl +PHP=php +Python=python +reStructuredText=rst +SQL=sql +None=text
Modified: geniuspaste/data/sprunge.us.conf 88 lines changed, 88 insertions(+), 0 deletions(-) =================================================================== @@ -0,0 +1,88 @@ +[pastebin] +name=sprunge.us +url=http://sprunge.us/ + +[format] +sprunge=%contents% + +[parse] +search=^[[:space:]]*(.+?)[[:space:]]*$ +replace=\1?%language% + +# map GeanyFileType=PastebinFileType +[languages] +# Pygments supports quite a bunch of stuff and is pretty forgiving, so +# basically just map 1:1 (but for a few things that either can't be used in +# and HTML query string (like C#), or for which Geany doesn't use the most +# common name (i.e. Conf)). +# Additionally, Sprunge seems to require lowercase names, but is happy with +# any lexer name, and just falls back to plain text if it can't find it, so we +# can safely just list everything and hope it works or will in the future. +None=text +Abaqus=abaqus +Abc=abc +ActionScript=actionscript +Ada=ada +Asciidoc=asciidoc +ASM=asm +Batch=batch +C=c +C#=csharp +C++=c++ +CAML=caml +Clojure=clojure +CMake=cmake +COBOL=cobol +CoffeeScript=coffeescript +Conf=conf +CSS=css +CUDA=cuda +Cython=cython +D=d +Diff=diff +Docbook=docbook +Erlang=erlang +F77=fortran +Ferite=ferite +Forth=forth +Fortran=fortran +FreeBasic=freebasic +Genie=genie +GLSL=glsl +Go=go +Graphviz=graphviz +Haskell=haskell +Haxe=haxe +HTML=html +Java=java +Javascript=javascript +JSON=json +LaTeX=latex +Lisp=lisp +Lua=lua +Make=make +Markdown=markdown +Matlab/Octave=octave +NSIS=nsis +Objective-C=objective-c +Pascal=pascal +Perl=perl +PHP=php +Po=po +PowerShell=powershell +Python=python +R=r +reStructuredText=restructuredtext +Ruby=ruby +Rust=rust +Scala=scala +Sh=sh +SQL=sql +Tcl=tcl +Txt2tags=txt2tags +Vala=vala +Verilog=verilog +VHDL=vhdl +XML=xml +YAML=yaml +Zephir=zephir
Modified: geniuspaste/data/tinypaste.com.conf 20 lines changed, 20 insertions(+), 0 deletions(-) =================================================================== @@ -0,0 +1,20 @@ +# FIXME: outdated, this leads to a 404 on http://pasted.co/ +[pastebin] +name=tinypaste.com +url=http://tinypaste.com/api/create.xml + +[format] +paste=%contents% +title=%title% +is_code=%language% + +[parse] +search=<response>(.*)</response> +replace=\1 + +[defaults] +language=1 + +# map GeanyFileType=PastebinFileType +[languages] +None=0
Modified: geniuspaste/src/Makefile.am 2 lines changed, 2 insertions(+), 0 deletions(-) =================================================================== @@ -1,4 +1,5 @@ include $(top_srcdir)/build/vars.build.mk +include $(top_srcdir)/build/vars.docs.mk plugin = geniuspaste
geanyplugins_LTLIBRARIES = geniuspaste.la @@ -6,6 +7,7 @@ geanyplugins_LTLIBRARIES = geniuspaste.la geniuspaste_la_SOURCES = geniuspaste.c geniuspaste_la_CPPFLAGS = \ $(AM_CPPFLAGS) \ + -DPLUGINDATADIR="$(plugindatadir)" \ -DGEANY_VERSION="$(GEANY_VERSION)" \ -DG_LOG_DOMAIN="GeniusPaste" geniuspaste_la_CFLAGS = \
Modified: geniuspaste/src/geniuspaste.c 768 lines changed, 549 insertions(+), 219 deletions(-) =================================================================== @@ -35,7 +35,7 @@
#define PLUGIN_NAME "GeniusPaste" -#define PLUGIN_VERSION "0.2" +#define PLUGIN_VERSION "0.3"
#ifdef G_OS_WIN32 #define USERNAME g_getenv("USERNAME") @@ -50,14 +50,16 @@ #define GTK_COMBO_BOX_TEXT GTK_COMBO_BOX #endif
-#define CODEPAD_ORG 0 -#define TINYPASTE_COM 1 -#define PASTEBIN_GEANY_ORG 2 -#define DPASTE_DE 3 -#define SPRUNGE_US 4 - -#define DEFAULT_TYPE_CODEPAD langs_supported_codepad[8]; -#define DEFAULT_TYPE_DPASTE langs_supported_dpaste[15]; +#define PASTEBIN_GROUP_DEFAULTS "defaults" +#define PASTEBIN_GROUP_FORMAT "format" +#define PASTEBIN_GROUP_LANGUAGES "languages" +#define PASTEBIN_GROUP_PARSE "parse" +#define PASTEBIN_GROUP_PARSE_KEY_SEARCH "search" +#define PASTEBIN_GROUP_PARSE_KEY_REPLACE "replace" +#define PASTEBIN_GROUP_PASTEBIN "pastebin" +#define PASTEBIN_GROUP_PASTEBIN_KEY_NAME "name" +#define PASTEBIN_GROUP_PASTEBIN_KEY_URL "url" +#define PASTEBIN_GROUP_PASTEBIN_KEY_METHOD "method"
GeanyPlugin *geany_plugin; GeanyData *geany_data; @@ -65,23 +67,14 @@ GeanyFunctions *geany_functions;
static GtkWidget *main_menu_item = NULL;
-static const gchar *websites[] = -{ - "codepad.org", - "tinypaste.com", - "pastebin.geany.org", - "dpaste.de", - "sprunge.us", -}; - -static const gchar *websites_api[] = +typedef struct { - "http://codepad.org/", - "http://tinypaste.com/api/create.xml", - "http://pastebin.geany.org/api/", - "http://dpaste.de/api/", - "http://sprunge.us/", -}; + gchar *name; + GKeyFile *config; +} +Pastebin; + +GSList *pastebins = NULL;
static struct { @@ -93,7 +86,7 @@ static struct static gchar *config_file = NULL; static gchar *author_name = NULL;
-static gint website_selected; +static gchar *pastebin_selected = NULL; static gboolean check_button_is_checked = FALSE;
PLUGIN_VERSION_CHECK(147) @@ -101,24 +94,203 @@ PLUGIN_SET_TRANSLATABLE_INFO(LOCALEDIR, GETTEXT_PACKAGE, PLUGIN_NAME, _("Paste your code on your favorite pastebin"), PLUGIN_VERSION, "Enrico Trotta enrico.trt@gmail.com")
-static gint indexof(const gchar * string, gchar c) + +static void pastebin_free(Pastebin *pastebin) +{ + g_key_file_unref(pastebin->config); + g_free(pastebin->name); + g_free(pastebin); +} + +/* like g_key_file_has_group() but sets error if the group is missing */ +static gboolean ensure_keyfile_has_group(GKeyFile *kf, + const gchar *group, + GError **error) +{ + if (g_key_file_has_group(kf, group)) + return TRUE; + else + { + g_set_error(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_GROUP_NOT_FOUND, + _("Group "%s" not found."), group); + return FALSE; + } +} + +/* like g_key_file_has_key() but sets error if either the group or the key is + * missing */ +static gboolean ensure_keyfile_has_key(GKeyFile *kf, + const gchar *group, + const gchar *key, + GError **error) +{ + if (! ensure_keyfile_has_group(kf, group, error)) + return FALSE; + else if (g_key_file_has_key(kf, group, key, NULL)) + return TRUE; + else + { + g_set_error(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_GROUP_NOT_FOUND, + _("Group "%s" has no key "%s"."), group, key); + return FALSE; + } +} + +static Pastebin *pastebin_new(const gchar *path, + GError **error) +{ + Pastebin *pastebin = NULL; + GKeyFile *kf = g_key_file_new(); + + if (g_key_file_load_from_file(kf, path, 0, error) && + ensure_keyfile_has_key(kf, PASTEBIN_GROUP_PASTEBIN, + PASTEBIN_GROUP_PASTEBIN_KEY_NAME, error) && + ensure_keyfile_has_key(kf, PASTEBIN_GROUP_PASTEBIN, + PASTEBIN_GROUP_PASTEBIN_KEY_URL, error) && + ensure_keyfile_has_group(kf, PASTEBIN_GROUP_FORMAT, error)) + { + pastebin = g_malloc(sizeof *pastebin); + + pastebin->name = g_key_file_get_string(kf, PASTEBIN_GROUP_PASTEBIN, + PASTEBIN_GROUP_PASTEBIN_KEY_NAME, NULL); + pastebin->config = g_key_file_ref(kf); + } + + g_key_file_unref(kf); + + return pastebin; +} + +static const Pastebin *find_pastebin_by_name(const gchar *name) +{ + GSList *node; + + g_return_val_if_fail(name != NULL, NULL); + + for (node = pastebins; node; node = node->next) + { + Pastebin *pastebin = node->data; + + if (strcmp(pastebin->name, name) == 0) + return pastebin; + } + + return NULL; +} + +static gint sort_pastebins(gconstpointer a, gconstpointer b) +{ + return utils_str_casecmp(((Pastebin *) a)->name, ((Pastebin *) b)->name); +} + +static void load_pastebins_in_dir(const gchar *path) +{ + GError *err = NULL; + GDir *dir = g_dir_open(path, 0, &err); + + if (! dir && err->code != G_FILE_ERROR_NOENT) + g_critical("Failed to read directory %s: %s", path, err->message); + if (err) + g_clear_error(&err); + if (dir) + { + const gchar *filename; + + while ((filename = g_dir_read_name(dir))) + { + if (*filename == '.') /* silently skip dotfiles */ + continue; + else if (! g_str_has_suffix(filename, ".conf")) + { + g_debug("Skipping %s%s%s because it has no .conf extension", + path, G_DIR_SEPARATOR_S, filename); + continue; + } + else + { + gchar *fpath = g_build_filename(path, filename, NULL); + Pastebin *pastebin = pastebin_new(fpath, &err); + + if (! pastebin) + { + g_critical("Invalid pastebin configuration file %s: %s", + fpath, err->message); + g_clear_error(&err); + } + else + { + if (! find_pastebin_by_name(pastebin->name)) + pastebins = g_slist_prepend(pastebins, pastebin); + else + { + g_debug("Skipping duplicate configuration "%s" for " + "pastebin "%s"", fpath, pastebin->name); + pastebin_free(pastebin); + } + } + + g_free(fpath); + } + } + + g_dir_close(dir); + } +} + +static void load_all_pastebins(void) +{ + gchar *paths[] = { + g_build_filename(geany->app->configdir, "plugins", "geniuspaste", + "pastebins", NULL), + g_build_filename(PLUGINDATADIR, "pastebins", NULL) + }; + guint i; + + for (i = 0; i < G_N_ELEMENTS(paths); i++) + { + load_pastebins_in_dir(paths[i]); + g_free(paths[i]); + } + pastebins = g_slist_sort(pastebins, sort_pastebins); +} + +static void free_all_pastebins(void) { - gchar * occ = strchr(string, c); - return occ ? occ - string : -1; + g_slist_free_full(pastebins, (GDestroyNotify) pastebin_free); + pastebins = NULL; }
static void load_settings(void) { GKeyFile *config = g_key_file_new();
+ if (config_file) + g_free(config_file); config_file = g_strconcat(geany->app->configdir, G_DIR_SEPARATOR_S, "plugins", G_DIR_SEPARATOR_S, "geniuspaste", G_DIR_SEPARATOR_S, "geniuspaste.conf", NULL); g_key_file_load_from_file(config, config_file, G_KEY_FILE_NONE, NULL); - - website_selected = utils_get_setting_integer(config, "geniuspaste", "website", PASTEBIN_GEANY_ORG); + + if (g_key_file_has_key(config, "geniuspaste", "pastebin", NULL) || + ! g_key_file_has_key(config, "geniuspaste", "website", NULL)) + { + pastebin_selected = utils_get_setting_string(config, "geniuspaste", "pastebin", "pastebin.geany.org"); + } + else + { + /* compatibility for old setting "website" */ + switch (utils_get_setting_integer(config, "geniuspaste", "website", 2)) + { + case 0: pastebin_selected = g_strdup("codepad.org"); break; + case 1: pastebin_selected = g_strdup("tinypaste.com"); break; + default: + case 2: pastebin_selected = g_strdup("pastebin.geany.org"); break; + case 3: pastebin_selected = g_strdup("dpaste.de"); break; + case 4: pastebin_selected = g_strdup("sprunge.us"); break; + } + } check_button_is_checked = utils_get_setting_boolean(config, "geniuspaste", "open_browser", FALSE); author_name = utils_get_setting_string(config, "geniuspaste", "author_name", USERNAME); - + g_key_file_free(config); }
@@ -130,7 +302,7 @@ static void save_settings(void)
g_key_file_load_from_file(config, config_file, G_KEY_FILE_NONE, NULL);
- g_key_file_set_integer(config, "geniuspaste", "website", website_selected); + g_key_file_set_string(config, "geniuspaste", "pastebin", pastebin_selected); g_key_file_set_boolean(config, "geniuspaste", "open_browser", check_button_is_checked); g_key_file_set_string(config, "geniuspaste", "author_name", author_name);
@@ -145,266 +317,416 @@ static void save_settings(void) utils_write_file(config_file, data); g_free(data); } - + g_free(config_dir); g_key_file_free(config); }
-static gchar *get_paste_text(GeanyDocument *doc, gsize *text_len) +static gchar *get_paste_text(GeanyDocument *doc) { - gsize len; gchar *paste_text;
if (sci_has_selection(doc->editor->sci)) { - len = sci_get_selected_text_length(doc->editor->sci) + 1; paste_text = sci_get_selection_contents(doc->editor->sci); } else { - len = sci_get_length(doc->editor->sci) + 1; + gint len = sci_get_length(doc->editor->sci) + 1; paste_text = sci_get_contents(doc->editor->sci, len); }
- if (text_len) - *text_len = len; - return paste_text; }
-static void paste(GeanyDocument * doc, const gchar * website) +static gchar *pastebin_get_default(const Pastebin *pastebin, + const gchar *key, + const gchar *def) { - SoupSession *session; - SoupMessage *msg = NULL; + return utils_get_setting_string(pastebin->config, PASTEBIN_GROUP_DEFAULTS, + key, def); +}
- gchar *f_content; - gchar const *f_type; - gchar *f_title; - gchar *p_url; - gchar *formdata = NULL; - gchar *user_agent = NULL; - gchar **tokens_array; +static gchar *pastebin_get_language(const Pastebin *pastebin, + const gchar *geany_ft_name) +{ + gchar *lang = g_key_file_get_string(pastebin->config, PASTEBIN_GROUP_LANGUAGES, + geany_ft_name, NULL); + + return lang ? lang : pastebin_get_default(pastebin, "language", ""); +}
- const gchar *langs_supported_codepad[] = +/* append replacement for placeholder @placeholder to @str + * returns %TRUE on success, %FALSE if the placeholder didn't exist + * + * don't prepare replacements because they are unlikely to happen more than + * once for each paste */ +static gboolean append_placeholder(GString *str, + const gchar *placeholder, + /* some replacement sources */ + const Pastebin *pastebin, + GeanyDocument *doc, + const gchar *contents) +{ + /* special builtin placeholders */ + if (strcmp("contents", placeholder) == 0) + g_string_append(str, contents); + else if (strcmp("language", placeholder) == 0) { - "C", "C++", "D", "Haskell", - "Lua", "OCaml", "PHP", "Perl", "Plain Text", - "Python", "Ruby", "Scheme", "Tcl" - }; + gchar *language = pastebin_get_language(pastebin, doc->file_type->name);
- const gchar *langs_supported_dpaste[] = + g_string_append(str, language); + g_free(language); + } + else if (strcmp("title", placeholder) == 0) { - "Bash", "C", "CSS", "Diff", - "Django/Jinja", "HTML", "IRC logs", "JavaScript", "PHP", - "Python console session", "Python Traceback", "Python", - "Python3", "Restructured Text", "SQL", "Text only" - }; + gchar *title = g_path_get_basename(DOC_FILENAME(doc));
- gint occ_position; - guint i; - guint status; - gsize f_length; + g_string_append(str, title); + g_free(title); + } + else if (strcmp("user", placeholder) == 0) + g_string_append(str, author_name); + /* non-builtins (ones from [defaults] alone) */ + else + { + gchar *val = pastebin_get_default(pastebin, placeholder, NULL);
- g_return_if_fail(doc && doc->is_valid); + if (val) + { + g_string_append(str, val); + g_free(val); + } + else + { + g_warning("non-existing placeholder "%%%s%%"", placeholder); + return FALSE; + } + }
- f_type = doc->file_type->name; + return TRUE; +}
- if (doc->file_name == NULL) - f_title = document_get_basename_for_display(doc, -1); - else - f_title = g_path_get_basename(doc->file_name); +/* expands "%name%" placeholders in @string + * + * placeholders are of the form: + * "%" [a-zA-Z0-9_]+ "%" + * Only valid and supported placeholders are replaced; everything else is left + * as-is. */ +static gchar *expand_placeholders(const gchar *string, + const Pastebin *pastebin, + GeanyDocument *doc, + const gchar *contents) +{ + GString *str = g_string_new(NULL); + const gchar *p;
- load_settings(); - - f_content = get_paste_text(doc, &f_length); - if (f_content == NULL || f_content[0] == '\0') + for (; (p = strchr(string, '%')); string = p + 1) { - dialogs_show_msgbox(GTK_MESSAGE_ERROR, _("Refusing to create blank paste")); - return; - } + const gchar *k = p + 1; + gchar *key = NULL; + gsize key_len = 0;
- switch (website_selected) - { + g_string_append_len(str, string, p - string);
- case CODEPAD_ORG: + while (g_ascii_isalnum(k[key_len]) || k[key_len] == '_') + key_len++;
- for (i = 0; i < G_N_ELEMENTS(langs_supported_codepad); i++) - { - if (g_strcmp0(f_type, langs_supported_codepad[i]) == 0) - break; - else - f_type = DEFAULT_TYPE_CODEPAD; - } + if (key_len > 0 && k[key_len] == '%') + key = g_strndup(k, key_len);
- msg = soup_message_new("POST", website); - formdata = soup_form_encode("lang", f_type, - "code", f_content, - "submit", "Submit", - NULL); + if (! key) + g_string_append_len(str, p, k + key_len - p); + else if (! append_placeholder(str, key, pastebin, doc, contents)) + g_string_append_len(str, p, k + key_len + 1 - p);
- break; + if (key) /* skip % suffix too */ + key_len++;
- case TINYPASTE_COM: + g_free(key);
- msg = soup_message_new("POST", website); - formdata = soup_form_encode("paste", f_content, - "title", f_title, - "is_code", g_strcmp0(f_type, "None") == 0 ? "0" : "1", - NULL); + p = k + key_len - 1; + } + g_string_append(str, string);
- break; + return g_string_free(str, FALSE); +}
+/* Matches @pattern on @haystack and perform match substitutions in @replace */ +static gchar *regex_replace(const gchar *pattern, + const gchar *haystack, + const gchar *replace, + GError **error) +{ + GRegex *re = g_regex_new(pattern, (G_REGEX_DOTALL | + G_REGEX_DOLLAR_ENDONLY | + G_REGEX_RAW), 0, error); + GMatchInfo *info = NULL; + gchar *result = NULL;
- case DPASTE_DE: + if (re && g_regex_match(re, haystack, 0, &info)) + { + GString *str = g_string_new(NULL); + const gchar *p;
- for (i = 0; i < G_N_ELEMENTS(langs_supported_dpaste); i++) + for (; (p = strchr(replace, '\')); replace = p + 1) { - if (g_strcmp0(f_type, langs_supported_dpaste[i]) == 0) - break; + gint digit = ((gint) p[1]) - '0'; + + g_string_append_len(str, replace, p - replace); + if (digit >= 0 && digit <= 9 && + digit < g_match_info_get_match_count(info)) + { + gchar *match = g_match_info_fetch(info, digit); + + g_string_append(str, match); + g_free(match); + p++; + } else - f_type = DEFAULT_TYPE_DPASTE; + g_string_append_c(str, *p); } + g_string_append(str, replace);
- msg = soup_message_new("POST", website); - /* apparently dpaste.de detects automatically the syntax of the - * pasted code so 'lexer' should be unneeded - */ - formdata = soup_form_encode("content", f_content, - "title", f_title, - "lexer", f_type, - NULL); + result = g_string_free(str, FALSE); + }
- break; + if (info) + g_match_info_free(info);
- case SPRUNGE_US: + return result; +}
- msg = soup_message_new("POST", website); - formdata = soup_form_encode("sprunge", f_content, NULL); +static void free_data_item(GQuark id, gpointer data, gpointer user_data) +{ + g_free(data); +}
- break; +/* sends data to @pastebin and returns the raw response */ +static SoupMessage *pastebin_soup_message_new(const Pastebin *pastebin, + GeanyDocument *doc, + const gchar *contents) +{ + SoupMessage *msg; + gchar *url; + gchar *method; + gsize n_fields; + gchar **fields; + GData *data; + + g_return_val_if_fail(pastebin != NULL, NULL); + g_return_val_if_fail(contents != NULL, NULL); + + url = utils_get_setting_string(pastebin->config, PASTEBIN_GROUP_PASTEBIN, + PASTEBIN_GROUP_PASTEBIN_KEY_URL, NULL); + method = utils_get_setting_string(pastebin->config, PASTEBIN_GROUP_PASTEBIN, + PASTEBIN_GROUP_PASTEBIN_KEY_METHOD, "POST"); + /* prepare the form data */ + fields = g_key_file_get_keys(pastebin->config, PASTEBIN_GROUP_FORMAT, &n_fields, NULL); + g_datalist_init(&data); + for (gsize i = 0; fields && i < n_fields; i++) + { + gchar *value = g_key_file_get_string(pastebin->config, PASTEBIN_GROUP_FORMAT, + fields[i], NULL); + + SETPTR(value, expand_placeholders(value, pastebin, doc, contents)); + g_datalist_set_data(&data, fields[i], value); + } + g_strfreev(fields); + msg = soup_form_request_new_from_datalist(method, url, &data); + g_datalist_foreach(&data, free_data_item, NULL); + g_datalist_clear(&data);
- case PASTEBIN_GEANY_ORG: + return msg; +}
- msg = soup_message_new("POST", website); - formdata = soup_form_encode("content", f_content, - "author", author_name, - "title", f_title, - "lexer", f_type, - NULL); +/* parses @response and returns the URL of the paste, or %NULL on parse error + * or if the URL couldn't be found. + * @warning: it may return NULL even if @error is not set */ +static gchar *pastebin_parse_response(const Pastebin *pastebin, + SoupMessage *msg, + GeanyDocument *doc, + const gchar *contents, + GError **error) +{ + gchar *url = NULL; + gchar *search; + gchar *replace;
- break; + g_return_val_if_fail(pastebin != NULL, NULL); + g_return_val_if_fail(msg != NULL, NULL);
+ if (! g_key_file_has_group(pastebin->config, PASTEBIN_GROUP_PARSE)) + { + /* by default, use the response URI (redirect) */ + url = soup_uri_to_string(soup_message_get_uri(msg), FALSE); } + else + { + search = utils_get_setting_string(pastebin->config, PASTEBIN_GROUP_PARSE, + PASTEBIN_GROUP_PARSE_KEY_SEARCH, + "^[[:space:]]*(.+?)[[:space:]]*$"); + replace = utils_get_setting_string(pastebin->config, PASTEBIN_GROUP_PARSE, + PASTEBIN_GROUP_PARSE_KEY_REPLACE, "\1"); + SETPTR(replace, expand_placeholders(replace, pastebin, doc, contents));
- g_free(f_content); + url = regex_replace(search, msg->response_body->data, replace, error);
- user_agent = g_strconcat(PLUGIN_NAME, " ", PLUGIN_VERSION, " / Geany ", GEANY_VERSION, NULL); - session = soup_session_async_new_with_options(SOUP_SESSION_USER_AGENT, user_agent, NULL); - g_free(user_agent); + g_free(search); + g_free(replace); + }
- soup_message_set_request(msg, "application/x-www-form-urlencoded", - SOUP_MEMORY_TAKE, formdata, strlen(formdata)); + return url; +}
- status = soup_session_send_message(session, msg); - p_url = g_strdup(msg->response_body->data); +G_GNUC_PRINTF (4, 5) +static void show_msgbox(GtkMessageType type, GtkButtonsType buttons, + const gchar *main_text, + const gchar *secondary_markup, ...) +{ + GtkWidget *dlg; + va_list ap; + gchar *markup; + + va_start(ap, secondary_markup); + markup = g_markup_vprintf_escaped(secondary_markup, ap); + va_end(ap); + + dlg = g_object_new(GTK_TYPE_MESSAGE_DIALOG, + "message-type", type, + "buttons", buttons, + "transient-for", geany->main_widgets->window, + "modal", TRUE, + "destroy-with-parent", TRUE, + "text", main_text, + "secondary-text", markup, + "secondary-use-markup", TRUE, + NULL); + gtk_dialog_run(GTK_DIALOG(dlg)); + gtk_widget_destroy(dlg); +}
- g_object_unref(session); - g_object_unref(msg); +static void debug_log_message_body(SoupMessage *msg, + SoupMessageBody *body, + const gchar *label) +{ + if (geany->app->debug_mode) + { + gchar *real_uri = soup_uri_to_string(soup_message_get_uri(msg), FALSE); + + soup_message_body_flatten(body); + msgwin_msg_add(COLOR_BLUE, -1, NULL, + "[geniuspaste] %s:\n" + "URI: %s\n" + "Body: %s\n" + "Code: %d (%s)", + label, + real_uri, + body->data, + msg->status_code, + msg->reason_phrase); + g_free(real_uri); + } +}
- if(status == SOUP_STATUS_OK) +static void paste(GeanyDocument * doc, const gchar * website) +{ + const Pastebin *pastebin; + gchar *f_content; + SoupSession *session; + SoupMessage *msg; + gchar *user_agent = NULL; + guint status; + + g_return_if_fail(doc && doc->is_valid); + + /* find the pastebin */ + pastebin = find_pastebin_by_name(website); + if (! pastebin) { + show_msgbox(GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, + _("Invalid pastebin service."), + _("Unknown pastebin service "%s". " + "Select an existing pastebin service in the preferences " + "or create an appropriate pastebin configuration and " + "retry."), + website); + return; + }
- /* - * codepad.org doesn't return only the url of the new snippet pasted - * but an html page. This minimal parser will get the bare url. - */ + /* get the contents */ + f_content = get_paste_text(doc); + if (f_content == NULL || f_content[0] == '\0') + { + dialogs_show_msgbox(GTK_MESSAGE_ERROR, _("Refusing to create blank paste")); + return; + }
- if (website_selected == CODEPAD_ORG) - { - tokens_array = g_strsplit(p_url, "<a href="", 0); + msg = pastebin_soup_message_new(pastebin, doc, f_content);
- /* cuts the string when it finds the first occurrence of '/' - * It shoud work even if codepad would change its url. - */ + user_agent = g_strconcat(PLUGIN_NAME, " ", PLUGIN_VERSION, " / Geany ", GEANY_VERSION, NULL); + session = soup_session_async_new_with_options(SOUP_SESSION_USER_AGENT, user_agent, NULL); + g_free(user_agent);
- SETPTR(p_url, g_strdup(tokens_array[5])); - occ_position = indexof(tokens_array[5], '"'); + debug_log_message_body(msg, msg->request_body, "Request");
- g_strfreev(tokens_array); + status = soup_session_send_message(session, msg); + g_object_unref(session);
- if(occ_position != -1) - { - p_url[occ_position] = '\0'; - } - else - { - dialogs_show_msgbox(GTK_MESSAGE_ERROR, _("Unable to paste the code on codepad.org\n" - "Retry or select another pastebin.")); - g_free(p_url); - return; - } + debug_log_message_body(msg, msg->response_body, "Response");
- } - else if(website_selected == TINYPASTE_COM) - { - /* tinypaste.com returns a XML response which looks - * like this: - * - * <?xml version="1.0" encoding="utf-8"?> - * <result> - * <response>xxxxx</response> - * </result> - */ - tokens_array = g_strsplit_set(p_url, "<>", 0); - - SETPTR(p_url, g_strdup_printf("http://%s/%s", websites[TINYPASTE_COM], tokens_array[6])); - - g_strfreev(tokens_array); - } - - else if(website_selected == DPASTE_DE) - { - SETPTR(p_url, g_strndup(p_url + 1, strlen(p_url) - 2)); + if (! SOUP_STATUS_IS_SUCCESSFUL(status)) + { + show_msgbox(GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, + _("Failed to paste the code"), + _("Error pasting the code to the pastebin service %s.\n" + "Error code: %u (%s).\n\n%s"), + pastebin->name, status, msg->reason_phrase, + (SOUP_STATUS_IS_TRANSPORT_ERROR(status) + ? _("Check your connection or the pastebin configuration and retry.") + : SOUP_STATUS_IS_SERVER_ERROR(status) + ? _("Wait for the service to come back and retry, or retry " + "with another pastebin service.") + : _("Check the pastebin configuration and retry."))); + } + else + { + GError *err = NULL; + gchar *p_url = pastebin_parse_response(pastebin, msg, doc, f_content, + &err);
- } - else if(website_selected == SPRUNGE_US) + if (err || ! p_url) { - - /* in order to enable the syntax highlightning on sprunge.us - * it is necessary to append at the returned url a question - * mark '?' followed by the file type. - * - * e.g. sprunge.us/xxxx?c - */ - gchar *ft_tmp = g_ascii_strdown(f_type, -1); - g_strstrip(p_url); - SETPTR(p_url, g_strdup_printf("%s?%s", p_url, ft_tmp)); - g_free(ft_tmp); + show_msgbox(GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, + _("Failed to obtain paste URL."), + _("The code was successfully pasted on %s, but an " + "error occurred trying to obtain its URL: %s\n\n%s"), + pastebin->name, + (err ? err->message + : _("unexpected response from the pastebin service.")), + _("Check the pastebin configuration and retry.")); + + if (err) + g_error_free(err); } - - if (check_button_is_checked) + else if (check_button_is_checked) { utils_open_browser(p_url); } else { - GtkWidget *dlg = gtk_message_dialog_new(GTK_WINDOW(geany->main_widgets->window), - GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT, GTK_MESSAGE_INFO, GTK_BUTTONS_OK, - _("Paste Successful")); - gtk_message_dialog_format_secondary_markup(GTK_MESSAGE_DIALOG(dlg), - _("Your paste can be found here:\n<a href="%s" " - "title="Click to open the paste in your browser">%s</a>"), p_url, p_url); - gtk_dialog_run(GTK_DIALOG(dlg)); - gtk_widget_destroy(dlg); + show_msgbox(GTK_MESSAGE_INFO, GTK_BUTTONS_OK, + _("Paste Successful"), + _("Your paste can be found here:\n<a href="%s" " + "title="Click to open the paste in your browser">%s</a>"), + p_url, p_url); } - } - else - { - dialogs_show_msgbox(GTK_MESSAGE_ERROR, _("Unable to paste the code. Check your connection and retry.\n" - "Error code: %d\n"), status); + + g_free(p_url); }
- g_free(p_url); + g_object_unref(msg); + g_free(f_content); }
static void item_activate(GtkMenuItem * menuitem, gpointer gdata) @@ -417,7 +739,7 @@ static void item_activate(GtkMenuItem * menuitem, gpointer gdata) return; }
- paste(doc, websites_api[website_selected]); + paste(doc, pastebin_selected); }
static void on_configure_response(GtkDialog * dialog, gint response, gpointer * user_data) @@ -430,7 +752,7 @@ static void on_configure_response(GtkDialog * dialog, gint response, gpointer * } else { - website_selected = gtk_combo_box_get_active(GTK_COMBO_BOX(widgets.combo)); + SETPTR(pastebin_selected, gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(widgets.combo))); check_button_is_checked = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widgets.check_button)); SETPTR(author_name, g_strdup(gtk_entry_get_text(GTK_ENTRY(widgets.author_entry)))); save_settings(); @@ -440,7 +762,8 @@ static void on_configure_response(GtkDialog * dialog, gint response, gpointer *
GtkWidget *plugin_configure(GtkDialog * dialog) { - guint i; + GSList *node; + gint i; GtkWidget *label, *vbox, *author_label;
vbox = gtk_vbox_new(FALSE, 6); @@ -460,8 +783,14 @@ GtkWidget *plugin_configure(GtkDialog * dialog)
widgets.combo = gtk_combo_box_text_new();
- for (i = 0; i < G_N_ELEMENTS(websites); i++) - gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(widgets.combo), websites[i]); + for (i = 0, node = pastebins; node; node = node->next, i++) + { + Pastebin *pastebin = node->data; + + gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(widgets.combo), pastebin->name); + if (pastebin_selected && strcmp(pastebin_selected, pastebin->name) == 0) + gtk_combo_box_set_active(GTK_COMBO_BOX(widgets.combo), i); + }
widgets.check_button = gtk_check_button_new_with_label(_("Show your paste in a new browser tab"));
@@ -471,7 +800,6 @@ GtkWidget *plugin_configure(GtkDialog * dialog) gtk_box_pack_start(GTK_BOX(vbox), widgets.author_entry, FALSE, FALSE, 0); gtk_box_pack_start(GTK_BOX(vbox), widgets.check_button, FALSE, FALSE, 0);
- gtk_combo_box_set_active(GTK_COMBO_BOX(widgets.combo), website_selected); gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widgets.check_button), check_button_is_checked);
gtk_widget_show_all(vbox); @@ -497,6 +825,7 @@ static void add_menu_item(void)
void plugin_init(GeanyData * data) { + load_all_pastebins(); load_settings(); main_locale_init(LOCALEDIR, GETTEXT_PACKAGE); add_menu_item(); @@ -507,4 +836,5 @@ void plugin_cleanup(void) { g_free(author_name); gtk_widget_destroy(main_menu_item); + free_all_pastebins(); }
-------------- This E-Mail was brought to you by github_commit_mail.py (Source: https://github.com/geany/infrastructure).