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