1 /*
2  * common.c
3  * vim: expandtab:ts=4:sts=4:sw=4
4  *
5  * Copyright (C) 2012 - 2019 James Booth <boothj5@gmail.com>
6  * Copyright (C) 2019 - 2021 Michael Vetter <jubalh@iodoru.org>
7  *
8  * This file is part of Profanity.
9  *
10  * Profanity is free software: you can redistribute it and/or modify
11  * it under the terms of the GNU General Public License as published by
12  * the Free Software Foundation, either version 3 of the License, or
13  * (at your option) any later version.
14  *
15  * Profanity is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18  * GNU General Public License for more details.
19  *
20  * You should have received a copy of the GNU General Public License
21  * along with Profanity.  If not, see <https://www.gnu.org/licenses/>.
22  *
23  * In addition, as a special exception, the copyright holders give permission to
24  * link the code of portions of this program with the OpenSSL library under
25  * certain conditions as described in each individual source file, and
26  * distribute linked combinations including the two.
27  *
28  * You must obey the GNU General Public License in all respects for all of the
29  * code used other than OpenSSL. If you modify file(s) with this exception, you
30  * may extend this exception to your version of the file(s), but you are not
31  * obligated to do so. If you do not wish to do so, delete this exception
32  * statement from your version. If you delete this exception statement from all
33  * source files in the program, then also delete it here.
34  *
35  */
36 
37 #include "config.h"
38 
39 #include <errno.h>
40 #include <sys/select.h>
41 #include <assert.h>
42 #include <stdlib.h>
43 #include <stdint.h>
44 #include <stdio.h>
45 #include <string.h>
46 #include <sys/types.h>
47 #include <sys/stat.h>
48 
49 #include <curl/curl.h>
50 #include <curl/easy.h>
51 #include <glib.h>
52 #include <gio/gio.h>
53 
54 #ifdef HAVE_NCURSESW_NCURSES_H
55 #include <ncursesw/ncurses.h>
56 #elif HAVE_NCURSES_H
57 #include <ncurses.h>
58 #elif HAVE_CURSES_H
59 #include <curses.h>
60 #endif
61 
62 #include "log.h"
63 #include "common.h"
64 
65 struct curl_data_t
66 {
67     char* buffer;
68     size_t size;
69 };
70 
71 static size_t _data_callback(void* ptr, size_t size, size_t nmemb, void* data);
72 
73 gboolean
create_dir(char * name)74 create_dir(char* name)
75 {
76     struct stat sb;
77 
78     if (stat(name, &sb) != 0) {
79         if (errno != ENOENT || mkdir(name, S_IRWXU) != 0) {
80             return FALSE;
81         }
82     } else {
83         if ((sb.st_mode & S_IFDIR) != S_IFDIR) {
84             log_debug("create_dir: %s exists and is not a directory!", name);
85             return FALSE;
86         }
87     }
88 
89     return TRUE;
90 }
91 
92 gboolean
mkdir_recursive(const char * dir)93 mkdir_recursive(const char* dir)
94 {
95     gboolean result = TRUE;
96 
97     for (int i = 1; i <= strlen(dir); i++) {
98         if (dir[i] == '/' || dir[i] == '\0') {
99             gchar* next_dir = g_strndup(dir, i);
100             result = create_dir(next_dir);
101             g_free(next_dir);
102             if (!result) {
103                 break;
104             }
105         }
106     }
107 
108     return result;
109 }
110 
111 gboolean
copy_file(const char * const sourcepath,const char * const targetpath,const gboolean overwrite_existing)112 copy_file(const char* const sourcepath, const char* const targetpath, const gboolean overwrite_existing)
113 {
114     GFile* source = g_file_new_for_path(sourcepath);
115     GFile* dest = g_file_new_for_path(targetpath);
116     GError* error = NULL;
117     GFileCopyFlags flags = overwrite_existing ? G_FILE_COPY_OVERWRITE : G_FILE_COPY_NONE;
118     gboolean success = g_file_copy(source, dest, flags, NULL, NULL, NULL, &error);
119     if (error != NULL)
120         g_error_free(error);
121     g_object_unref(source);
122     g_object_unref(dest);
123     return success;
124 }
125 
126 char*
str_replace(const char * string,const char * substr,const char * replacement)127 str_replace(const char* string, const char* substr,
128             const char* replacement)
129 {
130     char* tok = NULL;
131     char* newstr = NULL;
132     char* head = NULL;
133 
134     if (string == NULL)
135         return NULL;
136 
137     if (substr == NULL || replacement == NULL || (strcmp(substr, "") == 0))
138         return strdup(string);
139 
140     newstr = strdup(string);
141     head = newstr;
142 
143     while ((tok = strstr(head, substr))) {
144         char* oldstr = newstr;
145         newstr = malloc(strlen(oldstr) - strlen(substr) + strlen(replacement) + 1);
146 
147         if (newstr == NULL) {
148             free(oldstr);
149             return NULL;
150         }
151 
152         memcpy(newstr, oldstr, tok - oldstr);
153         memcpy(newstr + (tok - oldstr), replacement, strlen(replacement));
154         memcpy(newstr + (tok - oldstr) + strlen(replacement),
155                tok + strlen(substr),
156                strlen(oldstr) - strlen(substr) - (tok - oldstr));
157         memset(newstr + strlen(oldstr) - strlen(substr) + strlen(replacement), 0, 1);
158 
159         head = newstr + (tok - oldstr) + strlen(replacement);
160         free(oldstr);
161     }
162 
163     return newstr;
164 }
165 
166 gboolean
strtoi_range(char * str,int * saveptr,int min,int max,char ** err_msg)167 strtoi_range(char* str, int* saveptr, int min, int max, char** err_msg)
168 {
169     char* ptr;
170     int val;
171 
172     errno = 0;
173     val = (int)strtol(str, &ptr, 0);
174     if (errno != 0 || *str == '\0' || *ptr != '\0') {
175         GString* err_str = g_string_new("");
176         g_string_printf(err_str, "Could not convert \"%s\" to a number.", str);
177         *err_msg = err_str->str;
178         g_string_free(err_str, FALSE);
179         return FALSE;
180     } else if (val < min || val > max) {
181         GString* err_str = g_string_new("");
182         g_string_printf(err_str, "Value %s out of range. Must be in %d..%d.", str, min, max);
183         *err_msg = err_str->str;
184         g_string_free(err_str, FALSE);
185         return FALSE;
186     }
187 
188     *saveptr = val;
189 
190     return TRUE;
191 }
192 
193 int
utf8_display_len(const char * const str)194 utf8_display_len(const char* const str)
195 {
196     if (!str) {
197         return 0;
198     }
199 
200     int len = 0;
201     gchar* curr = g_utf8_offset_to_pointer(str, 0);
202     while (*curr != '\0') {
203         gunichar curru = g_utf8_get_char(curr);
204         if (g_unichar_iswide(curru)) {
205             len += 2;
206         } else {
207             len++;
208         }
209         curr = g_utf8_next_char(curr);
210     }
211 
212     return len;
213 }
214 
215 char*
release_get_latest(void)216 release_get_latest(void)
217 {
218     char* url = "https://profanity-im.github.io/profanity_version.txt";
219 
220     CURL* handle = curl_easy_init();
221     struct curl_data_t output;
222     output.buffer = NULL;
223     output.size = 0;
224 
225     curl_easy_setopt(handle, CURLOPT_URL, url);
226     curl_easy_setopt(handle, CURLOPT_WRITEFUNCTION, _data_callback);
227     curl_easy_setopt(handle, CURLOPT_TIMEOUT, 2);
228     curl_easy_setopt(handle, CURLOPT_WRITEDATA, (void*)&output);
229 
230     curl_easy_perform(handle);
231     curl_easy_cleanup(handle);
232 
233     if (output.buffer) {
234         output.buffer[output.size++] = '\0';
235         return output.buffer;
236     } else {
237         return NULL;
238     }
239 }
240 
241 gboolean
release_is_new(char * found_version)242 release_is_new(char* found_version)
243 {
244     int curr_maj, curr_min, curr_patch, found_maj, found_min, found_patch;
245 
246     int parse_curr = sscanf(PACKAGE_VERSION, "%d.%d.%d", &curr_maj, &curr_min,
247                             &curr_patch);
248     int parse_found = sscanf(found_version, "%d.%d.%d", &found_maj, &found_min,
249                              &found_patch);
250 
251     if (parse_found == 3 && parse_curr == 3) {
252         if (found_maj > curr_maj) {
253             return TRUE;
254         } else if (found_maj == curr_maj && found_min > curr_min) {
255             return TRUE;
256         } else if (found_maj == curr_maj && found_min == curr_min
257                    && found_patch > curr_patch) {
258             return TRUE;
259         } else {
260             return FALSE;
261         }
262     } else {
263         return FALSE;
264     }
265 }
266 
267 static size_t
_data_callback(void * ptr,size_t size,size_t nmemb,void * data)268 _data_callback(void* ptr, size_t size, size_t nmemb, void* data)
269 {
270     size_t realsize = size * nmemb;
271     struct curl_data_t* mem = (struct curl_data_t*)data;
272     mem->buffer = realloc(mem->buffer, mem->size + realsize + 1);
273 
274     if (mem->buffer) {
275         memcpy(&(mem->buffer[mem->size]), ptr, realsize);
276         mem->size += realsize;
277         mem->buffer[mem->size] = 0;
278     }
279 
280     return realsize;
281 }
282 
283 char*
get_file_or_linked(char * loc,char * basedir)284 get_file_or_linked(char* loc, char* basedir)
285 {
286     char* true_loc = NULL;
287 
288     // check for symlink
289     if (g_file_test(loc, G_FILE_TEST_IS_SYMLINK)) {
290         true_loc = g_file_read_link(loc, NULL);
291 
292         // if relative, add basedir
293         if (!g_str_has_prefix(true_loc, "/") && !g_str_has_prefix(true_loc, "~")) {
294             GString* base_str = g_string_new(basedir);
295             g_string_append(base_str, "/");
296             g_string_append(base_str, true_loc);
297             free(true_loc);
298             true_loc = base_str->str;
299             g_string_free(base_str, FALSE);
300         }
301         // use given location
302     } else {
303         true_loc = strdup(loc);
304     }
305 
306     return true_loc;
307 }
308 
309 char*
strip_arg_quotes(const char * const input)310 strip_arg_quotes(const char* const input)
311 {
312     char* unquoted = strdup(input);
313 
314     // Remove starting quote if it exists
315     if (strchr(unquoted, '"')) {
316         if (strchr(unquoted, ' ') + 1 == strchr(unquoted, '"')) {
317             memmove(strchr(unquoted, '"'), strchr(unquoted, '"') + 1, strchr(unquoted, '\0') - strchr(unquoted, '"'));
318         }
319     }
320 
321     // Remove ending quote if it exists
322     if (strchr(unquoted, '"')) {
323         if (strchr(unquoted, '\0') - 1 == strchr(unquoted, '"')) {
324             memmove(strchr(unquoted, '"'), strchr(unquoted, '"') + 1, strchr(unquoted, '\0') - strchr(unquoted, '"'));
325         }
326     }
327 
328     return unquoted;
329 }
330 
331 gboolean
is_notify_enabled(void)332 is_notify_enabled(void)
333 {
334     gboolean notify_enabled = FALSE;
335 
336 #ifdef HAVE_OSXNOTIFY
337     notify_enabled = TRUE;
338 #endif
339 #ifdef HAVE_LIBNOTIFY
340     notify_enabled = TRUE;
341 #endif
342 #ifdef PLATFORM_CYGWIN
343     notify_enabled = TRUE;
344 #endif
345 
346     return notify_enabled;
347 }
348 
349 GSList*
prof_occurrences(const char * const needle,const char * const haystack,int offset,gboolean whole_word,GSList ** result)350 prof_occurrences(const char* const needle, const char* const haystack, int offset, gboolean whole_word, GSList** result)
351 {
352     if (needle == NULL || haystack == NULL) {
353         return *result;
354     }
355 
356     gchar* haystack_curr = g_utf8_offset_to_pointer(haystack, offset);
357     if (g_str_has_prefix(haystack_curr, needle)) {
358         if (whole_word) {
359             gunichar before = 0;
360             gchar* haystack_before_ch = g_utf8_find_prev_char(haystack, haystack_curr);
361             if (haystack_before_ch) {
362                 before = g_utf8_get_char(haystack_before_ch);
363             }
364 
365             gunichar after = 0;
366             gchar* haystack_after_ch = haystack_curr + strlen(needle);
367             if (haystack_after_ch[0] != '\0') {
368                 after = g_utf8_get_char(haystack_after_ch);
369             }
370 
371             if (!g_unichar_isalnum(before) && !g_unichar_isalnum(after)) {
372                 *result = g_slist_append(*result, GINT_TO_POINTER(offset));
373             }
374         } else {
375             *result = g_slist_append(*result, GINT_TO_POINTER(offset));
376         }
377     }
378 
379     offset++;
380     if (g_strcmp0(g_utf8_offset_to_pointer(haystack, offset), "\0") != 0) {
381         *result = prof_occurrences(needle, haystack, offset, whole_word, result);
382     }
383 
384     return *result;
385 }
386 
387 int
is_regular_file(const char * path)388 is_regular_file(const char* path)
389 {
390     struct stat st;
391     int ret = stat(path, &st);
392     if (ret != 0) {
393         perror(NULL);
394         return 0;
395     }
396     return S_ISREG(st.st_mode);
397 }
398 
399 int
is_dir(const char * path)400 is_dir(const char* path)
401 {
402     struct stat st;
403     int ret = stat(path, &st);
404     if (ret != 0) {
405         perror(NULL);
406         return 0;
407     }
408     return S_ISDIR(st.st_mode);
409 }
410 
411 void
get_file_paths_recursive(const char * path,GSList ** contents)412 get_file_paths_recursive(const char* path, GSList** contents)
413 {
414     if (!is_dir(path)) {
415         return;
416     }
417 
418     GDir* directory = g_dir_open(path, 0, NULL);
419     const gchar* entry = g_dir_read_name(directory);
420     while (entry) {
421         GString* full = g_string_new(path);
422         if (!g_str_has_suffix(full->str, "/")) {
423             g_string_append(full, "/");
424         }
425         g_string_append(full, entry);
426 
427         if (is_dir(full->str)) {
428             get_file_paths_recursive(full->str, contents);
429         } else if (is_regular_file(full->str)) {
430             *contents = g_slist_append(*contents, full->str);
431         }
432 
433         g_string_free(full, FALSE);
434         entry = g_dir_read_name(directory);
435     }
436 }
437 
438 char*
get_random_string(int length)439 get_random_string(int length)
440 {
441     GRand* prng;
442     char* rand;
443     char alphabet[] = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
444     int endrange = sizeof(alphabet) - 1;
445 
446     rand = calloc(length + 1, sizeof(char));
447 
448     prng = g_rand_new();
449 
450     for (int i = 0; i < length; i++) {
451         rand[i] = alphabet[g_rand_int_range(prng, 0, endrange)];
452     }
453     g_rand_free(prng);
454 
455     return rand;
456 }
457 
458 GSList*
get_mentions(gboolean whole_word,gboolean case_sensitive,const char * const message,const char * const nick)459 get_mentions(gboolean whole_word, gboolean case_sensitive, const char* const message, const char* const nick)
460 {
461     GSList* mentions = NULL;
462     gchar* message_search = case_sensitive ? g_strdup(message) : g_utf8_strdown(message, -1);
463     gchar* mynick_search = case_sensitive ? g_strdup(nick) : g_utf8_strdown(nick, -1);
464 
465     mentions = prof_occurrences(mynick_search, message_search, 0, whole_word, &mentions);
466 
467     g_free(message_search);
468     g_free(mynick_search);
469 
470     return mentions;
471 }
472 
473 gboolean
call_external(gchar ** argv,gchar ** std_out,gchar ** std_err)474 call_external(gchar** argv, gchar** std_out, gchar** std_err)
475 {
476     GSpawnFlags flags = G_SPAWN_SEARCH_PATH;
477     if (std_out == NULL)
478         flags |= G_SPAWN_STDOUT_TO_DEV_NULL;
479     if (std_err == NULL)
480         flags |= G_SPAWN_STDERR_TO_DEV_NULL;
481 
482     gint exit_status;
483     gboolean spawn_result;
484     GError* spawn_error;
485     spawn_result = g_spawn_sync(NULL, // Inherit the parent PWD.
486                                 argv,
487                                 NULL, // Inherit the parent environment.
488                                 flags,
489                                 NULL, NULL, // No func. before exec() in child.
490                                 std_out, std_err,
491                                 &exit_status, &spawn_error);
492 
493     if (!spawn_result
494         || !g_spawn_check_exit_status(exit_status, &spawn_error)) {
495         gchar* cmd = g_strjoinv(" ", argv);
496         log_error("Spawning '%s' failed with '%s'.", cmd, spawn_error->message);
497         g_free(cmd);
498         g_error_free(spawn_error);
499     }
500 
501     return spawn_result;
502 }
503 
504 gchar**
format_call_external_argv(const char * template,const char * url,const char * filename)505 format_call_external_argv(const char* template, const char* url, const char* filename)
506 {
507     gchar** argv = g_strsplit(template, " ", 0);
508 
509     guint num_args = 0;
510     while (argv[num_args]) {
511         if (0 == g_strcmp0(argv[num_args], "%u") && url != NULL) {
512             g_free(argv[num_args]);
513             argv[num_args] = g_strdup(url);
514         } else if (0 == g_strcmp0(argv[num_args], "%p") && filename != NULL) {
515             g_free(argv[num_args]);
516             argv[num_args] = strdup(filename);
517         }
518         num_args++;
519     }
520 
521     return argv;
522 }
523 
524 gchar*
_unique_filename(const char * filename)525 _unique_filename(const char* filename)
526 {
527     gchar* unique = g_strdup(filename);
528 
529     unsigned int i = 0;
530     while (g_file_test(unique, G_FILE_TEST_EXISTS)) {
531         g_free(unique);
532 
533         if (i > 1000) { // Give up after 1000 attempts.
534             return NULL;
535         }
536 
537         unique = g_strdup_printf("%s.%u", filename, i);
538         if (!unique) {
539             return NULL;
540         }
541 
542         i++;
543     }
544 
545     return unique;
546 }
547 
548 bool
_has_directory_suffix(const char * path)549 _has_directory_suffix(const char* path)
550 {
551     return (g_str_has_suffix(path, ".")
552             || g_str_has_suffix(path, "..")
553             || g_str_has_suffix(path, G_DIR_SEPARATOR_S));
554 }
555 
556 char*
_basename_from_url(const char * url)557 _basename_from_url(const char* url)
558 {
559     const char* default_name = "index";
560 
561     GFile* file = g_file_new_for_commandline_arg(url);
562     char* basename = g_file_get_basename(file);
563 
564     if (_has_directory_suffix(basename)) {
565         g_free(basename);
566         basename = strdup(default_name);
567     }
568 
569     g_object_unref(file);
570 
571     return basename;
572 }
573 
574 gchar*
get_expanded_path(const char * path)575 get_expanded_path(const char* path)
576 {
577     GString* exp_path = g_string_new("");
578     gchar* result;
579 
580     if (g_str_has_prefix(path, "file://")) {
581         path += strlen("file://");
582     }
583     if (strlen(path) >= 2 && path[0] == '~' && path[1] == '/') {
584         g_string_printf(exp_path, "%s/%s", getenv("HOME"), path + 2);
585     } else {
586         g_string_printf(exp_path, "%s", path);
587     }
588 
589     result = exp_path->str;
590     g_string_free(exp_path, FALSE);
591 
592     return result;
593 }
594 
595 gchar*
unique_filename_from_url(const char * url,const char * path)596 unique_filename_from_url(const char* url, const char* path)
597 {
598     gchar* realpath;
599 
600     // Default to './' as path when none has been provided.
601     if (path == NULL) {
602         realpath = g_strdup("./");
603     } else {
604         realpath = get_expanded_path(path);
605     }
606 
607     // Resolves paths such as './../.' for path.
608     GFile* target = g_file_new_for_commandline_arg(realpath);
609     gchar* filename = NULL;
610 
611     if (_has_directory_suffix(realpath) || g_file_test(realpath, G_FILE_TEST_IS_DIR)) {
612         // The target should be used as a directory. Assume that the basename
613         // should be derived from the URL.
614         char* basename = _basename_from_url(url);
615         filename = g_build_filename(g_file_peek_path(target), basename, NULL);
616         g_free(basename);
617     } else {
618         // Just use the target as filename.
619         filename = g_build_filename(g_file_peek_path(target), NULL);
620     }
621 
622     gchar* unique_filename = _unique_filename(filename);
623     if (unique_filename == NULL) {
624         g_free(filename);
625         g_free(realpath);
626         return NULL;
627     }
628 
629     g_object_unref(target);
630     g_free(filename);
631     g_free(realpath);
632 
633     return unique_filename;
634 }
635