1 /*
2  *  geniuspaste - paste your code on your favorite pastebin.
3  *
4  *  Copyright 2012 Enrico "Enrix835" Trotta <enrico.trt@gmail.com>
5  *
6  *  This program is free software; you can redistribute it and/or modify
7  *  it under the terms of the GNU General Public License as published by
8  *  the Free Software Foundation; either version 2 of the License, or
9  *  (at your option) any later version.
10  *
11  *  This program is distributed in the hope that it will be useful,
12  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  *  GNU General Public License for more details.
15  *
16  *  You should have received a copy of the GNU General Public License
17  *  along with this program; if not, write to the Free Software
18  *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19  *  MA 02110-1301, USA.
20  */
21 
22 #include <libsoup/soup.h>
23 #include <stdlib.h>
24 #include <string.h>
25 
26 #ifdef HAVE_LOCALE_H
27 #include <locale.h>
28 #endif
29 
30 #ifdef HAVE_CONFIG_H
31 #include "config.h" /* for the gettext domain */
32 #endif
33 
34 #include <geanyplugin.h>
35 
36 
37 #define PLUGIN_NAME "GeniusPaste"
38 #define PLUGIN_VERSION "0.3"
39 
40 #ifdef G_OS_WIN32
41 #define USERNAME        g_getenv("USERNAME")
42 #else
43 #define USERNAME        g_getenv("USER")
44 #endif
45 
46 /* stay compatible with GTK < 2.24 */
47 #if !GTK_CHECK_VERSION(2,24,0)
48 #define gtk_combo_box_text_new         gtk_combo_box_new_text
49 #define gtk_combo_box_text_append_text gtk_combo_box_append_text
50 #define GTK_COMBO_BOX_TEXT             GTK_COMBO_BOX
51 #endif
52 
53 #define PASTEBIN_GROUP_DEFAULTS                     "defaults"
54 #define PASTEBIN_GROUP_FORMAT                       "format"
55 #define PASTEBIN_GROUP_LANGUAGES                    "languages"
56 #define PASTEBIN_GROUP_PARSE                        "parse"
57 #define PASTEBIN_GROUP_PARSE_KEY_SEARCH             "search"
58 #define PASTEBIN_GROUP_PARSE_KEY_REPLACE            "replace"
59 #define PASTEBIN_GROUP_PASTEBIN                     "pastebin"
60 #define PASTEBIN_GROUP_PASTEBIN_KEY_NAME            "name"
61 #define PASTEBIN_GROUP_PASTEBIN_KEY_URL             "url"
62 #define PASTEBIN_GROUP_PASTEBIN_KEY_METHOD          "method"
63 #define PASTEBIN_GROUP_PASTEBIN_KEY_CONTENT_TYPE    "content-type"
64 
65 GeanyPlugin *geany_plugin;
66 GeanyData *geany_data;
67 
68 static GtkWidget *main_menu_item = NULL;
69 
70 typedef struct
71 {
72     gchar *name;
73     GKeyFile *config;
74 }
75 Pastebin;
76 
77 typedef enum
78 {
79     FORMAT_HTML_FORM_URLENCODED,
80     FORMAT_JSON
81 }
82 Format;
83 
84 GSList *pastebins = NULL;
85 
86 static struct
87 {
88     GtkWidget *combo;
89     GtkWidget *check_button;
90     GtkWidget *author_entry;
91 } widgets;
92 
93 static gchar *config_file = NULL;
94 static gchar *author_name = NULL;
95 
96 static gchar *pastebin_selected = NULL;
97 static gboolean check_button_is_checked = FALSE;
98 
99 PLUGIN_VERSION_CHECK(224)
100 PLUGIN_SET_TRANSLATABLE_INFO(LOCALEDIR, GETTEXT_PACKAGE, PLUGIN_NAME,
101                              _("Paste your code on your favorite pastebin"),
102                              PLUGIN_VERSION, "Enrico Trotta <enrico.trt@gmail.com>")
103 
104 
pastebin_free(Pastebin * pastebin)105 static void pastebin_free(Pastebin *pastebin)
106 {
107     g_key_file_free(pastebin->config);
108     g_free(pastebin->name);
109     g_free(pastebin);
110 }
111 
112 /* like g_key_file_has_group() but sets error if the group is missing */
ensure_keyfile_has_group(GKeyFile * kf,const gchar * group,GError ** error)113 static gboolean ensure_keyfile_has_group(GKeyFile *kf,
114                                          const gchar *group,
115                                          GError **error)
116 {
117     if (g_key_file_has_group(kf, group))
118         return TRUE;
119     else
120     {
121         g_set_error(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_GROUP_NOT_FOUND,
122                     _("Group \"%s\" not found."), group);
123         return FALSE;
124     }
125 }
126 
127 /* like g_key_file_has_key() but sets error if either the group or the key is
128  * missing */
ensure_keyfile_has_key(GKeyFile * kf,const gchar * group,const gchar * key,GError ** error)129 static gboolean ensure_keyfile_has_key(GKeyFile *kf,
130                                        const gchar *group,
131                                        const gchar *key,
132                                        GError **error)
133 {
134     if (! ensure_keyfile_has_group(kf, group, error))
135         return FALSE;
136     else if (g_key_file_has_key(kf, group, key, NULL))
137         return TRUE;
138     else
139     {
140         g_set_error(error, G_KEY_FILE_ERROR, G_KEY_FILE_ERROR_GROUP_NOT_FOUND,
141                     _("Group \"%s\" has no key \"%s\"."), group, key);
142         return FALSE;
143     }
144 }
145 
pastebin_new(const gchar * path,GError ** error)146 static Pastebin *pastebin_new(const gchar  *path,
147                               GError      **error)
148 {
149     Pastebin *pastebin = NULL;
150     GKeyFile *kf = g_key_file_new();
151 
152     if (g_key_file_load_from_file(kf, path, 0, error) &&
153         ensure_keyfile_has_key(kf, PASTEBIN_GROUP_PASTEBIN,
154                                PASTEBIN_GROUP_PASTEBIN_KEY_NAME, error) &&
155         ensure_keyfile_has_key(kf, PASTEBIN_GROUP_PASTEBIN,
156                                PASTEBIN_GROUP_PASTEBIN_KEY_URL, error) &&
157         ensure_keyfile_has_group(kf, PASTEBIN_GROUP_FORMAT, error))
158     {
159         pastebin = g_malloc(sizeof *pastebin);
160 
161         pastebin->name = g_key_file_get_string(kf, PASTEBIN_GROUP_PASTEBIN,
162                                                PASTEBIN_GROUP_PASTEBIN_KEY_NAME, NULL);
163         pastebin->config = kf;
164     }
165     else
166         g_key_file_free(kf);
167 
168     return pastebin;
169 }
170 
find_pastebin_by_name(const gchar * name)171 static const Pastebin *find_pastebin_by_name(const gchar *name)
172 {
173     GSList *node;
174 
175     g_return_val_if_fail(name != NULL, NULL);
176 
177     for (node = pastebins; node; node = node->next)
178     {
179         Pastebin *pastebin = node->data;
180 
181         if (strcmp(pastebin->name, name) == 0)
182             return pastebin;
183     }
184 
185     return NULL;
186 }
187 
sort_pastebins(gconstpointer a,gconstpointer b)188 static gint sort_pastebins(gconstpointer a, gconstpointer b)
189 {
190     return utils_str_casecmp(((Pastebin *) a)->name, ((Pastebin *) b)->name);
191 }
192 
load_pastebins_in_dir(const gchar * path)193 static void load_pastebins_in_dir(const gchar *path)
194 {
195     GError *err = NULL;
196     GDir *dir = g_dir_open(path, 0, &err);
197 
198     if (! dir && err->code != G_FILE_ERROR_NOENT)
199         g_critical("Failed to read directory %s: %s", path, err->message);
200     if (err)
201         g_clear_error(&err);
202     if (dir)
203     {
204         const gchar *filename;
205 
206         while ((filename = g_dir_read_name(dir)))
207         {
208             if (*filename == '.') /* silently skip dotfiles */
209                 continue;
210             else if (! g_str_has_suffix(filename, ".conf"))
211             {
212                 g_debug("Skipping %s%s%s because it has no .conf extension",
213                         path, G_DIR_SEPARATOR_S, filename);
214                 continue;
215             }
216             else
217             {
218                 gchar *fpath = g_build_filename(path, filename, NULL);
219                 Pastebin *pastebin = pastebin_new(fpath, &err);
220 
221                 if (! pastebin)
222                 {
223                     g_critical("Invalid pastebin configuration file %s: %s",
224                                fpath, err->message);
225                     g_clear_error(&err);
226                 }
227                 else
228                 {
229                     if (! find_pastebin_by_name(pastebin->name))
230                         pastebins = g_slist_prepend(pastebins, pastebin);
231                     else
232                     {
233                         g_debug("Skipping duplicate configuration \"%s\" for "
234                                 "pastebin \"%s\"", fpath, pastebin->name);
235                         pastebin_free(pastebin);
236                     }
237                 }
238 
239                 g_free(fpath);
240             }
241         }
242 
243         g_dir_close(dir);
244     }
245 }
246 
get_data_dir_path(const gchar * filename)247 static gchar *get_data_dir_path(const gchar *filename)
248 {
249     gchar *prefix = NULL;
250     gchar *path;
251 
252 #ifdef G_OS_WIN32
253     prefix = g_win32_get_package_installation_directory_of_module(NULL);
254 #elif defined(__APPLE__)
255     if (g_getenv("GEANY_PLUGINS_SHARE_PATH"))
256         return g_build_filename(g_getenv("GEANY_PLUGINS_SHARE_PATH"),
257                                 PLUGIN, filename, NULL);
258 #endif
259     path = g_build_filename(prefix ? prefix : "", PLUGINDATADIR, filename, NULL);
260     g_free(prefix);
261     return path;
262 }
263 
load_all_pastebins(void)264 static void load_all_pastebins(void)
265 {
266     gchar *paths[] = {
267         g_build_filename(geany->app->configdir, "plugins", "geniuspaste",
268                          "pastebins", NULL),
269         get_data_dir_path("pastebins")
270     };
271     guint i;
272 
273     for (i = 0; i < G_N_ELEMENTS(paths); i++)
274     {
275         load_pastebins_in_dir(paths[i]);
276         g_free(paths[i]);
277     }
278     pastebins = g_slist_sort(pastebins, sort_pastebins);
279 }
280 
free_all_pastebins(void)281 static void free_all_pastebins(void)
282 {
283     g_slist_free_full(pastebins, (GDestroyNotify) pastebin_free);
284     pastebins = NULL;
285 }
286 
load_settings(void)287 static void load_settings(void)
288 {
289     GKeyFile *config = g_key_file_new();
290 
291     if (config_file)
292         g_free(config_file);
293     config_file = g_strconcat(geany->app->configdir, G_DIR_SEPARATOR_S, "plugins", G_DIR_SEPARATOR_S,
294                               "geniuspaste", G_DIR_SEPARATOR_S, "geniuspaste.conf", NULL);
295     g_key_file_load_from_file(config, config_file, G_KEY_FILE_NONE, NULL);
296 
297     if (g_key_file_has_key(config, "geniuspaste", "pastebin", NULL) ||
298         ! g_key_file_has_key(config, "geniuspaste", "website", NULL))
299     {
300         pastebin_selected = utils_get_setting_string(config, "geniuspaste", "pastebin", "pastebin.geany.org");
301     }
302     else
303     {
304         /* compatibility for old setting "website" */
305         switch (utils_get_setting_integer(config, "geniuspaste", "website", 2))
306         {
307             case 0: pastebin_selected = g_strdup("codepad.org"); break;
308             case 1: pastebin_selected = g_strdup("tinypaste.com"); break;
309             default:
310             case 2: pastebin_selected = g_strdup("pastebin.geany.org"); break;
311             case 3: pastebin_selected = g_strdup("dpaste.de"); break;
312             case 4: pastebin_selected = g_strdup("sprunge.us"); break;
313         }
314     }
315     check_button_is_checked = utils_get_setting_boolean(config, "geniuspaste", "open_browser", FALSE);
316     author_name = utils_get_setting_string(config, "geniuspaste", "author_name", USERNAME);
317 
318     g_key_file_free(config);
319 }
320 
save_settings(void)321 static void save_settings(void)
322 {
323     GKeyFile *config = g_key_file_new();
324     gchar *data;
325     gchar *config_dir = g_path_get_dirname(config_file);
326 
327     g_key_file_load_from_file(config, config_file, G_KEY_FILE_NONE, NULL);
328 
329     g_key_file_set_string(config, "geniuspaste", "pastebin", pastebin_selected);
330     g_key_file_set_boolean(config, "geniuspaste", "open_browser", check_button_is_checked);
331     g_key_file_set_string(config, "geniuspaste", "author_name", author_name);
332 
333     if (! g_file_test(config_dir, G_FILE_TEST_IS_DIR) && utils_mkdir(config_dir, TRUE) != 0)
334     {
335         dialogs_show_msgbox(GTK_MESSAGE_ERROR,
336                             _("Plugin configuration directory could not be created."));
337     }
338     else
339     {
340         data = g_key_file_to_data(config, NULL, NULL);
341         utils_write_file(config_file, data);
342         g_free(data);
343     }
344 
345     g_free(config_dir);
346     g_key_file_free(config);
347 }
348 
get_paste_text(GeanyDocument * doc)349 static gchar *get_paste_text(GeanyDocument *doc)
350 {
351     gchar *paste_text;
352 
353     if (sci_has_selection(doc->editor->sci))
354     {
355         paste_text = sci_get_selection_contents(doc->editor->sci);
356     }
357     else
358     {
359         gint len = sci_get_length(doc->editor->sci) + 1;
360         paste_text = sci_get_contents(doc->editor->sci, len);
361     }
362 
363     return paste_text;
364 }
365 
pastebin_get_default(const Pastebin * pastebin,const gchar * key,const gchar * def)366 static gchar *pastebin_get_default(const Pastebin *pastebin,
367                                    const gchar *key,
368                                    const gchar *def)
369 {
370     return utils_get_setting_string(pastebin->config, PASTEBIN_GROUP_DEFAULTS,
371                                     key, def);
372 }
373 
pastebin_get_language(const Pastebin * pastebin,const gchar * geany_ft_name)374 static gchar *pastebin_get_language(const Pastebin *pastebin,
375                                     const gchar *geany_ft_name)
376 {
377     gchar *lang = g_key_file_get_string(pastebin->config, PASTEBIN_GROUP_LANGUAGES,
378                                         geany_ft_name, NULL);
379 
380     return lang ? lang : pastebin_get_default(pastebin, "language", "");
381 }
382 
383 /* append replacement for placeholder @placeholder to @str
384  * returns %TRUE on success, %FALSE if the placeholder didn't exist
385  *
386  * don't prepare replacements because they are unlikely to happen more than
387  * once for each paste */
append_placeholder(GString * str,const gchar * placeholder,const Pastebin * pastebin,GeanyDocument * doc,const gchar * contents)388 static gboolean append_placeholder(GString         *str,
389                                    const gchar     *placeholder,
390                                    /* some replacement sources */
391                                    const Pastebin  *pastebin,
392                                    GeanyDocument   *doc,
393                                    const gchar     *contents)
394 {
395     /* special builtin placeholders */
396     if (strcmp("contents", placeholder) == 0)
397         g_string_append(str, contents);
398     else if (strcmp("language", placeholder) == 0)
399     {
400         gchar *language = pastebin_get_language(pastebin, doc->file_type->name);
401 
402         g_string_append(str, language);
403         g_free(language);
404     }
405     else if (strcmp("title", placeholder) == 0)
406     {
407         gchar *title = g_path_get_basename(DOC_FILENAME(doc));
408 
409         g_string_append(str, title);
410         g_free(title);
411     }
412     else if (strcmp("user", placeholder) == 0)
413         g_string_append(str, author_name);
414     /* non-builtins (ones from [defaults] alone) */
415     else
416     {
417         gchar *val = pastebin_get_default(pastebin, placeholder, NULL);
418 
419         if (val)
420         {
421             g_string_append(str, val);
422             g_free(val);
423         }
424         else
425         {
426             g_warning("non-existing placeholder \"%%%s%%\"", placeholder);
427             return FALSE;
428         }
429     }
430 
431     return TRUE;
432 }
433 
434 /* expands "%name%" placeholders in @string
435  *
436  * placeholders are of the form:
437  *      "%" [a-zA-Z0-9_]+ "%"
438  * Only valid and supported placeholders are replaced; everything else is left
439  * as-is. */
expand_placeholders(const gchar * string,const Pastebin * pastebin,GeanyDocument * doc,const gchar * contents)440 static gchar *expand_placeholders(const gchar    *string,
441                                   const Pastebin *pastebin,
442                                   GeanyDocument  *doc,
443                                   const gchar    *contents)
444 {
445     GString *str = g_string_new(NULL);
446     const gchar *p;
447 
448     for (; (p = strchr(string, '%')); string = p + 1)
449     {
450         const gchar *k = p + 1;
451         gchar *key = NULL;
452         gsize key_len = 0;
453 
454         g_string_append_len(str, string, p - string);
455 
456         while (g_ascii_isalnum(k[key_len]) || k[key_len] == '_')
457             key_len++;
458 
459         if (key_len > 0 && k[key_len] == '%')
460             key = g_strndup(k, key_len);
461 
462         if (! key)
463             g_string_append_len(str, p, k + key_len - p);
464         else if (! append_placeholder(str, key, pastebin, doc, contents))
465             g_string_append_len(str, p, k + key_len + 1 - p);
466 
467         if (key) /* skip % suffix too */
468             key_len++;
469 
470         g_free(key);
471 
472         p = k + key_len - 1;
473     }
474     g_string_append(str, string);
475 
476     return g_string_free(str, FALSE);
477 }
478 
479 /* Matches @pattern on @haystack and perform match substitutions in @replace */
regex_replace(const gchar * pattern,const gchar * haystack,const gchar * replace,GError ** error)480 static gchar *regex_replace(const gchar  *pattern,
481                             const gchar  *haystack,
482                             const gchar  *replace,
483                             GError      **error)
484 {
485     GRegex *re = g_regex_new(pattern, (G_REGEX_DOTALL |
486                                        G_REGEX_DOLLAR_ENDONLY |
487                                        G_REGEX_RAW), 0, error);
488     GMatchInfo *info = NULL;
489     gchar *result = NULL;
490 
491     if (re && g_regex_match(re, haystack, 0, &info))
492     {
493         GString *str = g_string_new(NULL);
494         const gchar *p;
495 
496         for (; (p = strchr(replace, '\\')); replace = p + 1)
497         {
498             gint digit = ((gint) p[1]) - '0';
499 
500             g_string_append_len(str, replace, p - replace);
501             if (digit >= 0 && digit <= 9 &&
502                 digit < g_match_info_get_match_count(info))
503             {
504                 gchar *match = g_match_info_fetch(info, digit);
505 
506                 g_string_append(str, match);
507                 g_free(match);
508                 p++;
509             }
510             else
511                 g_string_append_c(str, *p);
512         }
513         g_string_append(str, replace);
514 
515         result = g_string_free(str, FALSE);
516     }
517 
518     if (info)
519         g_match_info_free(info);
520 
521     return result;
522 }
523 
pastebin_get_format(const Pastebin * pastebin)524 static Format pastebin_get_format(const Pastebin *pastebin)
525 {
526     static const struct
527     {
528         const gchar *name;
529         Format       format;
530     } formats[] = {
531         { "application/x-www-form-urlencoded",  FORMAT_HTML_FORM_URLENCODED },
532         { "application/json",                   FORMAT_JSON }
533     };
534     Format result = FORMAT_HTML_FORM_URLENCODED;
535     gchar *format = utils_get_setting_string(pastebin->config, PASTEBIN_GROUP_PASTEBIN,
536                                              PASTEBIN_GROUP_PASTEBIN_KEY_CONTENT_TYPE, NULL);
537 
538     if (format)
539     {
540         for (guint i = 0; i < G_N_ELEMENTS(formats); i++)
541         {
542             if (strcmp(formats[i].name, format) == 0)
543             {
544                 result = formats[i].format;
545                 break;
546             }
547         }
548 
549         g_free(format);
550     }
551 
552     return result;
553 }
554 
555 /* Appends a JSON string.  See:
556  * http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf */
append_json_string(GString * str,const gchar * value)557 static void append_json_string(GString *str, const gchar *value)
558 {
559     g_string_append_c(str, '"');
560     for (; *value; value++)
561     {
562         if (*value == '"' || *value == '\\')
563         {
564             g_string_append_c(str, '\\');
565             g_string_append_c(str, *value);
566         }
567         else if (*value == '\b')
568             g_string_append(str, "\\b");
569         else if (*value == '\f')
570             g_string_append(str, "\\f");
571         else if (*value == '\n')
572             g_string_append(str, "\\n");
573         else if (*value == '\r')
574             g_string_append(str, "\\r");
575         else if (*value == '\t')
576             g_string_append(str, "\\t");
577         else if (*value >= 0x00 && *value <= 0x1F)
578             g_string_append_printf(str, "\\u%04d", *value);
579         else
580             g_string_append_c(str, *value);
581     }
582     g_string_append_c(str, '"');
583 }
584 
append_json_data_item(GQuark id,gpointer data,gpointer user_data)585 static void append_json_data_item(GQuark id, gpointer data, gpointer user_data)
586 {
587     GString *str = user_data;
588 
589     if (str->len > 1) /* if there's more than the first "{" */
590         g_string_append_c(str, ',');
591     append_json_string(str, g_quark_to_string(id));
592     g_string_append_c(str, ':');
593     append_json_string(str, data);
594 }
595 
json_request_new(const gchar * method,const gchar * url,GData ** fields)596 static SoupMessage *json_request_new(const gchar *method,
597                                      const gchar *url,
598                                      GData **fields)
599 {
600     SoupMessage  *msg = soup_message_new(method, url);
601     GString      *str = g_string_new(NULL);
602 
603     g_string_append_c(str, '{');
604     g_datalist_foreach(fields, append_json_data_item, str);
605     g_string_append_c(str, '}');
606     soup_message_set_request(msg, "application/json", SOUP_MEMORY_TAKE,
607                              str->str, str->len);
608     g_string_free(str, FALSE);
609 
610     return msg;
611 }
612 
613 /* sends data to @pastebin and returns the raw response */
pastebin_soup_message_new(const Pastebin * pastebin,GeanyDocument * doc,const gchar * contents)614 static SoupMessage *pastebin_soup_message_new(const Pastebin  *pastebin,
615                                               GeanyDocument   *doc,
616                                               const gchar     *contents)
617 {
618     SoupMessage *msg;
619     gchar *url;
620     gchar *method;
621     Format format;
622     gsize n_fields;
623     gchar **fields;
624     GData *data;
625 
626     g_return_val_if_fail(pastebin != NULL, NULL);
627     g_return_val_if_fail(contents != NULL, NULL);
628 
629     url = utils_get_setting_string(pastebin->config, PASTEBIN_GROUP_PASTEBIN,
630                                    PASTEBIN_GROUP_PASTEBIN_KEY_URL, NULL);
631     method = utils_get_setting_string(pastebin->config, PASTEBIN_GROUP_PASTEBIN,
632                                       PASTEBIN_GROUP_PASTEBIN_KEY_METHOD, "POST");
633     format = pastebin_get_format(pastebin);
634     /* prepare the form data */
635     fields = g_key_file_get_keys(pastebin->config, PASTEBIN_GROUP_FORMAT, &n_fields, NULL);
636     g_datalist_init(&data);
637     for (gsize i = 0; fields && i < n_fields; i++)
638     {
639         gchar *value = g_key_file_get_string(pastebin->config, PASTEBIN_GROUP_FORMAT,
640                                              fields[i], NULL);
641 
642         SETPTR(value, expand_placeholders(value, pastebin, doc, contents));
643         g_datalist_set_data_full(&data, fields[i], value, g_free);
644     }
645     g_strfreev(fields);
646     switch (format)
647     {
648         case FORMAT_JSON:
649             msg = json_request_new(method, url, &data);
650             break;
651 
652         default:
653         case FORMAT_HTML_FORM_URLENCODED:
654             msg = soup_form_request_new_from_datalist(method, url, &data);
655             break;
656     }
657     g_datalist_clear(&data);
658 
659     return msg;
660 }
661 
662 /* parses @response and returns the URL of the paste, or %NULL on parse error
663  * or if the URL couldn't be found.
664  * @warning: it may return NULL even if @error is not set */
pastebin_parse_response(const Pastebin * pastebin,SoupMessage * msg,GeanyDocument * doc,const gchar * contents,GError ** error)665 static gchar *pastebin_parse_response(const Pastebin  *pastebin,
666                                       SoupMessage     *msg,
667                                       GeanyDocument   *doc,
668                                       const gchar     *contents,
669                                       GError         **error)
670 {
671     gchar *url = NULL;
672     gchar *search;
673     gchar *replace;
674 
675     g_return_val_if_fail(pastebin != NULL, NULL);
676     g_return_val_if_fail(msg != NULL, NULL);
677 
678     if (! g_key_file_has_group(pastebin->config, PASTEBIN_GROUP_PARSE))
679     {
680         /* by default, use the response URI (redirect) */
681         url = soup_uri_to_string(soup_message_get_uri(msg), FALSE);
682     }
683     else
684     {
685         search = utils_get_setting_string(pastebin->config, PASTEBIN_GROUP_PARSE,
686                                           PASTEBIN_GROUP_PARSE_KEY_SEARCH,
687                                           "^[[:space:]]*(.+?)[[:space:]]*$");
688         replace = utils_get_setting_string(pastebin->config, PASTEBIN_GROUP_PARSE,
689                                            PASTEBIN_GROUP_PARSE_KEY_REPLACE, "\\1");
690         SETPTR(replace, expand_placeholders(replace, pastebin, doc, contents));
691 
692         url = regex_replace(search, msg->response_body->data, replace, error);
693 
694         g_free(search);
695         g_free(replace);
696     }
697 
698     return url;
699 }
700 
message_dialog_label_link_activated(GtkLabel * label,gchar * uri,gpointer user_data)701 static gboolean message_dialog_label_link_activated(GtkLabel *label, gchar *uri, gpointer user_data)
702 {
703     utils_open_browser(uri);
704     return TRUE;
705 }
706 
message_dialog_label_set_url_hook(GtkWidget * widget,gpointer data)707 static void message_dialog_label_set_url_hook(GtkWidget *widget, gpointer data)
708 {
709     if (GTK_IS_LABEL(widget))
710     {
711         g_signal_connect(widget,
712                          "activate-link",
713                          G_CALLBACK(message_dialog_label_link_activated),
714                          NULL);
715     }
716 }
717 
718 G_GNUC_PRINTF (4, 5)
show_msgbox(GtkMessageType type,GtkButtonsType buttons,const gchar * main_text,const gchar * secondary_markup,...)719 static void show_msgbox(GtkMessageType type, GtkButtonsType buttons,
720                         const gchar *main_text,
721                         const gchar *secondary_markup, ...)
722 {
723     GtkWidget *dlg;
724     GtkWidget *dlg_vbox;
725     va_list ap;
726     gchar *markup;
727 
728     va_start(ap, secondary_markup);
729     markup = g_markup_vprintf_escaped(secondary_markup, ap);
730     va_end(ap);
731 
732     dlg = g_object_new(GTK_TYPE_MESSAGE_DIALOG,
733                        "message-type", type,
734                        "buttons", buttons,
735                        "transient-for", geany->main_widgets->window,
736                        "modal", TRUE,
737                        "destroy-with-parent", TRUE,
738                        "text", main_text,
739                        "secondary-text", markup,
740                        "secondary-use-markup", TRUE,
741                        NULL);
742     /* fetch the message area of the dialog and attach a custom URL hook to the labels */
743     dlg_vbox = gtk_message_dialog_get_message_area(GTK_MESSAGE_DIALOG(dlg));
744     gtk_container_foreach(GTK_CONTAINER(dlg_vbox),
745                           message_dialog_label_set_url_hook,
746                           NULL);
747     /* run the dialog */
748     gtk_dialog_run(GTK_DIALOG(dlg));
749     gtk_widget_destroy(dlg);
750 }
751 
debug_log_message_body(SoupMessage * msg,SoupMessageBody * body,const gchar * label)752 static void debug_log_message_body(SoupMessage *msg,
753                                    SoupMessageBody *body,
754                                    const gchar *label)
755 {
756     if (geany->app->debug_mode)
757     {
758         gchar *real_uri = soup_uri_to_string(soup_message_get_uri(msg), FALSE);
759 
760         soup_message_body_flatten(body);
761         msgwin_msg_add(COLOR_BLUE, -1, NULL,
762                        "[geniuspaste] %s:\n"
763                        "URI: %s\n"
764                        "Body: %s\n"
765                        "Code: %d (%s)",
766                        label,
767                        real_uri,
768                        body->data,
769                        msg->status_code,
770                        msg->reason_phrase);
771         g_free(real_uri);
772     }
773 }
774 
paste(GeanyDocument * doc,const gchar * website)775 static void paste(GeanyDocument * doc, const gchar * website)
776 {
777     const Pastebin *pastebin;
778     gchar *f_content;
779     SoupSession *session;
780     SoupMessage *msg;
781     gchar *user_agent = NULL;
782     guint status;
783 
784     g_return_if_fail(doc && doc->is_valid);
785 
786     /* find the pastebin */
787     pastebin = find_pastebin_by_name(website);
788     if (! pastebin)
789     {
790         show_msgbox(GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE,
791                     _("Invalid pastebin service."),
792                     _("Unknown pastebin service \"%s\". "
793                       "Select an existing pastebin service in the preferences "
794                       "or create an appropriate pastebin configuration and "
795                       "retry."),
796                     website);
797         return;
798     }
799 
800     /* get the contents */
801     f_content = get_paste_text(doc);
802     if (f_content == NULL || f_content[0] == '\0')
803     {
804         dialogs_show_msgbox(GTK_MESSAGE_ERROR, _("Refusing to create blank paste"));
805         return;
806     }
807 
808     msg = pastebin_soup_message_new(pastebin, doc, f_content);
809 
810     user_agent = g_strconcat(PLUGIN_NAME, " ", PLUGIN_VERSION, " / Geany ", GEANY_VERSION, NULL);
811     session = soup_session_async_new_with_options(SOUP_SESSION_USER_AGENT, user_agent, NULL);
812     g_free(user_agent);
813 
814     debug_log_message_body(msg, msg->request_body, "Request");
815 
816     status = soup_session_send_message(session, msg);
817     g_object_unref(session);
818 
819     debug_log_message_body(msg, msg->response_body, "Response");
820 
821     if (! SOUP_STATUS_IS_SUCCESSFUL(status))
822     {
823         show_msgbox(GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE,
824                     _("Failed to paste the code"),
825                     _("Error pasting the code to the pastebin service %s.\n"
826                       "Error code: %u (%s).\n\n%s"),
827                     pastebin->name, status, msg->reason_phrase,
828                     (SOUP_STATUS_IS_TRANSPORT_ERROR(status)
829                      ? _("Check your connection or the pastebin configuration and retry.")
830                      : SOUP_STATUS_IS_SERVER_ERROR(status)
831                      ? _("Wait for the service to come back and retry, or retry "
832                          "with another pastebin service.")
833                      : _("Check the pastebin configuration and retry.")));
834     }
835     else
836     {
837         GError *err = NULL;
838         gchar *p_url = pastebin_parse_response(pastebin, msg, doc, f_content,
839                                                &err);
840 
841         if (err || ! p_url)
842         {
843             show_msgbox(GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE,
844                         _("Failed to obtain paste URL."),
845                         _("The code was successfully pasted on %s, but an "
846                           "error occurred trying to obtain its URL: %s\n\n%s"),
847                         pastebin->name,
848                         (err ? err->message
849                          : _("Unexpected response from the pastebin service.")),
850                         _("Check the pastebin configuration and retry."));
851 
852             if (err)
853                 g_error_free(err);
854         }
855         else if (check_button_is_checked)
856         {
857             utils_open_browser(p_url);
858         }
859         else
860         {
861             show_msgbox(GTK_MESSAGE_INFO, GTK_BUTTONS_OK,
862                         _("Paste Successful"),
863                         _("Your paste can be found here:\n<a href=\"%s\" "
864                           "title=\"Click to open the paste in your browser\">%s</a>"),
865                         p_url, p_url);
866         }
867 
868         g_free(p_url);
869     }
870 
871     g_object_unref(msg);
872     g_free(f_content);
873 }
874 
item_activate(GtkMenuItem * menuitem,gpointer gdata)875 static void item_activate(GtkMenuItem * menuitem, gpointer gdata)
876 {
877     GeanyDocument *doc = document_get_current();
878 
879     if(!DOC_VALID(doc))
880     {
881         dialogs_show_msgbox(GTK_MESSAGE_ERROR, _("There are no opened documents. Open one and retry.\n"));
882         return;
883     }
884 
885     paste(doc, pastebin_selected);
886 }
887 
on_configure_response(GtkDialog * dialog,gint response,gpointer * user_data)888 static void on_configure_response(GtkDialog * dialog, gint response, gpointer * user_data)
889 {
890     if (response == GTK_RESPONSE_OK || response == GTK_RESPONSE_APPLY)
891     {
892         if(g_strcmp0(gtk_entry_get_text(GTK_ENTRY(widgets.author_entry)), "") == 0)
893         {
894             dialogs_show_msgbox(GTK_MESSAGE_ERROR, _("The author name field is empty!"));
895         }
896         else
897         {
898             SETPTR(pastebin_selected, gtk_combo_box_text_get_active_text(GTK_COMBO_BOX_TEXT(widgets.combo)));
899             check_button_is_checked = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widgets.check_button));
900             SETPTR(author_name, g_strdup(gtk_entry_get_text(GTK_ENTRY(widgets.author_entry))));
901             save_settings();
902         }
903     }
904 }
905 
plugin_configure(GtkDialog * dialog)906 GtkWidget *plugin_configure(GtkDialog * dialog)
907 {
908     GSList *node;
909     gint i;
910     GtkWidget *label, *vbox, *author_label;
911 
912     vbox = gtk_vbox_new(FALSE, 6);
913 
914     label = gtk_label_new(_("Select a pastebin:"));
915     gtk_misc_set_alignment(GTK_MISC(label), 0, 0.5);
916 
917     author_label = gtk_label_new(_("Enter the author name:"));
918     gtk_misc_set_alignment(GTK_MISC(author_label), 0, 0.5);
919 
920     widgets.author_entry = gtk_entry_new();
921 
922     if(author_name == NULL)
923         author_name = g_strdup(USERNAME);
924 
925     gtk_entry_set_text(GTK_ENTRY(widgets.author_entry), author_name);
926 
927     widgets.combo = gtk_combo_box_text_new();
928 
929     for (i = 0, node = pastebins; node; node = node->next, i++)
930     {
931         Pastebin *pastebin = node->data;
932 
933         gtk_combo_box_text_append_text(GTK_COMBO_BOX_TEXT(widgets.combo), pastebin->name);
934         if (pastebin_selected && strcmp(pastebin_selected, pastebin->name) == 0)
935             gtk_combo_box_set_active(GTK_COMBO_BOX(widgets.combo), i);
936     }
937 
938     widgets.check_button = gtk_check_button_new_with_label(_("Show your paste in a new browser tab"));
939 
940     gtk_box_pack_start(GTK_BOX(vbox), label, FALSE, FALSE, 0);
941     gtk_box_pack_start(GTK_BOX(vbox), widgets.combo, FALSE, FALSE, 0);
942     gtk_box_pack_start(GTK_BOX(vbox), author_label, FALSE, FALSE, 0);
943     gtk_box_pack_start(GTK_BOX(vbox), widgets.author_entry, FALSE, FALSE, 0);
944     gtk_box_pack_start(GTK_BOX(vbox), widgets.check_button, FALSE, FALSE, 0);
945 
946     gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widgets.check_button), check_button_is_checked);
947 
948     gtk_widget_show_all(vbox);
949 
950     g_signal_connect(dialog, "response", G_CALLBACK(on_configure_response), NULL);
951 
952     return vbox;
953 }
954 
add_menu_item(void)955 static void add_menu_item(void)
956 {
957     GtkWidget *paste_item;
958 
959     paste_item = gtk_menu_item_new_with_mnemonic(_("_Paste it!"));
960     gtk_widget_show(paste_item);
961     gtk_container_add(GTK_CONTAINER(geany->main_widgets->tools_menu),
962                       paste_item);
963     g_signal_connect(paste_item, "activate", G_CALLBACK(item_activate),
964                      NULL);
965 
966     main_menu_item = paste_item;
967 }
968 
plugin_init(GeanyData * data)969 void plugin_init(GeanyData * data)
970 {
971     load_all_pastebins();
972     load_settings();
973     add_menu_item();
974 }
975 
976 
plugin_cleanup(void)977 void plugin_cleanup(void)
978 {
979     g_free(author_name);
980     gtk_widget_destroy(main_menu_item);
981     free_all_pastebins();
982 }
983