fix possible crash on user deletion
[srvx.git] / src / helpfile.c
index b04c7838b28f42355c064ca6aa590cac7e39ff6f..aaf0d80ab961eed5048225f3d241f536ff4a106b 100644 (file)
 #include "conf.h"
 #include "helpfile.h"
 #include "log.h"
+#include "modcmd.h"
 #include "nickserv.h"
+#include "spamserv.h"
 
+#if defined(HAVE_DIRENT_H)
 #include <dirent.h>
+#endif
+
+#if defined(HAVE_SYS_STAT_H)
+#include <sys/stat.h>
+#endif
 
 static const struct message_entry msgtab[] = {
     { "HFMSG_MISSING_HELPFILE", "The help file could not be found.  Sorry!" },
@@ -31,10 +39,10 @@ static const struct message_entry msgtab[] = {
     { NULL, NULL }
 };
 
-#define DEFAULT_LINE_SIZE      MAX_LINE_SIZE
+#define DEFAULT_LINE_SIZE       MAX_LINE_SIZE
 #define DEFAULT_TABLE_SIZE      80
 
-extern struct userNode *global, *chanserv, *opserv, *nickserv;
+extern struct userNode *global, *chanserv, *opserv, *nickserv, *spamserv;
 struct userNode *message_dest;
 struct userNode *message_source;
 struct language *lang_C;
@@ -64,7 +72,7 @@ static struct language *language_alloc(const char *name)
 {
     struct language *lang = calloc(1, sizeof(*lang));
     lang->name = strdup(name);
-    lang->parent = language_find("C");
+    lang->parent = lang_C;
     if (!languages) {
         languages = dict_new();
         dict_set_free_data(languages, language_free);
@@ -77,7 +85,7 @@ static struct language *language_alloc(const char *name)
  * lang is a two-letter code according to ISO-639-1 (or three-letter
  * code according to ISO-639-2 for languages not in ISO-639-1), and
  * COUNTRY is the ISO 3166 country code in all upper case.
- * 
+ *
  * See also:
  * http://www.loc.gov/standards/iso639-2/
  * http://www.loc.gov/standards/iso639-2/langhome.html
@@ -136,7 +144,7 @@ static void language_set_messages(struct language *lang, dict_t dict)
             log_module(MAIN_LOG, LOG_WARNING, "Unsupported record type for message %s in language %s", msgid, lang->name);
             continue;
         }
-        dict_insert(lang->messages, msgid, msg);
+        dict_insert(lang->messages, strdup(msgid), msg);
         it = iter_next(it);
         it2 = iter_next(it2);
     }
@@ -144,7 +152,8 @@ static void language_set_messages(struct language *lang, dict_t dict)
         missing++;
         it2 = iter_next(it2);
     }
-    log_module(MAIN_LOG, LOG_WARNING, "In language %s, %d extra and %d missing messages", lang->name, extra, missing);
+    if (extra || missing)
+        log_module(MAIN_LOG, LOG_WARNING, "In language %s, %d extra and %d missing messages.", lang->name, extra, missing);
 }
 
 static struct language *language_read(const char *name)
@@ -163,8 +172,10 @@ static struct language *language_read(const char *name)
 
     /* Open the directory stream; if we can't, fail. */
     snprintf(filename, sizeof(filename), "languages/%s", name);
-    if (!(dir = opendir(filename)))
+    if (!(dir = opendir(filename))) {
+        log_module(MAIN_LOG, LOG_ERROR, "Unable to open language directory languages/%s: %s", name, strerror(errno));
         return NULL;
+    }
     if (!(lang = dict_find(languages, name, NULL)))
         lang = language_alloc(name);
 
@@ -187,6 +198,7 @@ static struct language *language_read(const char *name)
     /* (Re-)initialize the language's dicts. */
     dict_delete(lang->messages);
     lang->messages = dict_new();
+    dict_set_free_keys(lang->messages, free);
     dict_set_free_data(lang->messages, free);
     lang->helpfiles = dict_new();
     dict_set_free_data(lang->helpfiles, language_free_helpfile);
@@ -194,7 +206,7 @@ static struct language *language_read(const char *name)
     /* Read all the translations from the directory. */
     while ((dirent = readdir(dir))) {
         snprintf(filename, sizeof(filename), "languages/%s/%s", name, dirent->d_name);
-        if (!strcmp(dirent->d_name,"parent")) {
+        if (!strcmp(dirent->d_name, "parent")) {
             continue;
         } else if (!strcmp(dirent->d_name, "strings.db")) {
             dict = parse_database(filename);
@@ -213,39 +225,32 @@ static struct language *language_read(const char *name)
 
 static void language_read_list(void)
 {
+    struct stat sbuf;
     struct dirent *dirent;
     DIR *dir;
+    char namebuf[MAXLEN];
 
     if (!(dir = opendir("languages")))
         return;
-    while ((dirent = readdir(dir)))
-        language_alloc(dirent->d_name);
-    closedir(dir);
-}
-
-static void language_read_all(void)
-{
-    struct string_list *slist;
-    struct dirent *dirent;
-    DIR *dir;
-    unsigned int ii;
-
-    /* Read into an in-memory list and sort so we are likely to load
-     * parent languages before their children (de_DE sorts after de).
-     */
-    if (!(dir = opendir("languages")))
-        return;
-    slist = alloc_string_list(4);
-    while ((dirent = readdir(dir)))
-        string_list_append(slist, strdup(dirent->d_name));
-    closedir(dir);
-    string_list_sort(slist);
-    for (ii = 0; ii < slist->used; ++ii) {
-        if (!strcmp(slist->list[ii], ".") || !strcmp(slist->list[ii], ".."))
+    while ((dirent = readdir(dir))) {
+        if (dirent->d_name[0] == '.')
+            continue;
+        snprintf(namebuf, sizeof(namebuf), "languages/%s", dirent->d_name);
+        if (!strcmp(dirent->d_name, "strings.db")) {
+            continue;
+        }
+        if (stat(namebuf, &sbuf) < 0) {
+            log_module(MAIN_LOG, LOG_INFO, "Skipping language entry '%s' (unable to stat).", dirent->d_name);
             continue;
-        language_read(slist->list[ii]);
+        }
+        if (!S_ISDIR(sbuf.st_mode)) {
+            log_module(MAIN_LOG, LOG_INFO, "Skipping language entry '%s' (not directory).", dirent->d_name);
+            continue;
+        }
+        if (!dict_find(languages, dirent->d_name, NULL))
+            language_alloc(dirent->d_name);
     }
-    free_string_list(slist);
+    closedir(dir);
 }
 
 const char *language_find_message(struct language *lang, const char *msgid) {
@@ -389,7 +394,7 @@ vsend_message(const char *dest, struct userNode *src, struct handle_info *handle
     void (*irc_send)(struct userNode *from, const char *to, const char *msg);
     static struct string_buffer input;
     unsigned int size, ipos, pos, length, chars_sent, use_color;
-    unsigned int expand_pos, expand_ipos, newline_ipos;
+    unsigned int expand_ipos, newline_ipos;
     char line[MAX_LINE_SIZE];
 
     if (IsChannelName(dest) || *dest == '$') {
@@ -406,7 +411,8 @@ vsend_message(const char *dest, struct userNode *src, struct handle_info *handle
 #endif
     }
     message_source = src;
-    if (!(msg_type & 4) && !(format = handle_find_message(handle, format)))
+    if (!(msg_type & MSG_TYPE_NOXLATE)
+        && !(format = handle_find_message(handle, format)))
         return 0;
     /* fill in a buffer with the string */
     input.used = 0;
@@ -414,8 +420,8 @@ vsend_message(const char *dest, struct userNode *src, struct handle_info *handle
 
     /* figure out how to send the messages */
     if (handle) {
-       msg_type |= (HANDLE_FLAGGED(handle, USE_PRIVMSG) ? 1 : 0);
-       use_color = HANDLE_FLAGGED(handle, MIRC_COLOR);
+        msg_type |= (HANDLE_FLAGGED(handle, USE_PRIVMSG) ? 1 : 0);
+        use_color = HANDLE_FLAGGED(handle, MIRC_COLOR);
         size = handle->screen_width;
         if (size > sizeof(line))
             size = sizeof(line);
@@ -423,7 +429,7 @@ vsend_message(const char *dest, struct userNode *src, struct handle_info *handle
         size = sizeof(line);
         use_color = 1;
     }
-    if (!size)
+    if (!size || !(msg_type & MSG_TYPE_MULTILINE))
         size = DEFAULT_LINE_SIZE;
     switch (msg_type & 3) {
         case 0:
@@ -442,23 +448,23 @@ vsend_message(const char *dest, struct userNode *src, struct handle_info *handle
      * that requires a very big intermediate buffer.
      */
     expand_ipos = newline_ipos = ipos = 0;
-    expand_pos = pos = 0;
+    pos = 0;
     chars_sent = 0;
     while (input.list[ipos]) {
-       char ch, *value, *free_value;
+        char ch, *value, *free_value;
 
         while ((ch = input.list[ipos]) && (ch != '$') && (ch != '\n') && (pos < size)) {
-           line[pos++] = ch;
+            line[pos++] = ch;
             ipos++;
-       }
+        }
 
-       if (!input.list[ipos])
+        if (!input.list[ipos])
             goto send_line;
         if (input.list[ipos] == '\n') {
             ipos++;
             goto send_line;
         }
-       if (pos == size) {
+        if (pos == size) {
             unsigned int new_ipos;
             /* Scan backwards for a space in the input, until we hit
              * either the last newline or the last variable expansion.
@@ -483,96 +489,133 @@ vsend_message(const char *dest, struct userNode *src, struct handle_info *handle
             ipos = new_ipos;
             while (input.list[ipos] == ' ')
                 ipos++;
-           goto send_line;
-       }
+            goto send_line;
+        }
 
         free_value = 0;
-       switch (input.list[++ipos]) {
+        switch (input.list[++ipos]) {
         /* Literal '$' or end of string. */
-       case 0:
-           ipos--;
-       case '$':
-           value = "$";
-           break;
-       /* The following two expand to mIRC color codes if enabled
-          by the user. */
-       case 'b':
-           value = use_color ? "\002" : "";
-           break;
-       case 'o':
-           value = use_color ? "\017" : "";
-           break;
+        case 0:
+            ipos--;
+        case '$':
+            value = "$";
+            break;
+        /* The following two expand to mIRC color codes if enabled
+           by the user. */
+        case 'b':
+            value = use_color ? "\002" : "";
+            break;
+        case 'o':
+            value = use_color ? "\017" : "";
+            break;
         case 'r':
             value = use_color ? "\026" : "";
             break;
-       case 'u':
-           value = use_color ? "\037" : "";
-           break;
-       /* Service nicks. */
+        case 'u':
+            value = use_color ? "\037" : "";
+            break;
+        /* Service nicks. */
         case 'S':
             value = src->nick;
             break;
-       case 'G':
-           value = global ? global->nick : "Global";
-           break;
-       case 'C':
-           value = chanserv ? chanserv->nick : "ChanServ";
-           break;
-       case 'O':
-           value = opserv ? opserv->nick : "OpServ";
-           break;
-       case 'N':
+        case 'G':
+            value = global ? global->nick : "Global";
+            break;
+        case 'C':
+            value = chanserv ? chanserv->nick : "ChanServ";
+            break;
+        case 'O':
+            value = opserv ? opserv->nick : "OpServ";
+            break;
+        case 'N':
             value = nickserv ? nickserv->nick : "NickServ";
             break;
+        case 'X':
+            value = spamserv ? spamserv->nick : "SpamServ";
+            break;
         case 's':
             value = self->name;
             break;
-       case 'H':
-           value = handle ? handle->handle : "Account";
-           break;
-#define SEND_LINE() do { line[pos] = 0; if (pos > 0) irc_send(src, dest, line); chars_sent += pos; pos = 0; newline_ipos = ipos; } while (0)
-       /* Custom expansion handled by helpfile-specific function. */
-       case '{':
-       case '(':
-           if (expand_f) {
-               char *name_end = input.list + ipos + 1;
-
-               while (*name_end != '}' && *name_end != ')' && *name_end) name_end++;
-               if (*name_end) {
-                    struct helpfile_expansion exp;
-                   *name_end = 0;
-                   exp = expand_f(input.list + ipos + 1);
-                    switch (exp.type) {
-                    case HF_STRING:
-                        free_value = value = exp.value.str;
-                        if (!value)
-                            value = "";
-                        break;
-                    case HF_TABLE:
-                        /* Must send current line, then emit table. */
-                        SEND_LINE();
-                        table_send(src, (message_dest ? message_dest->nick : dest), 0, irc_send, exp.value.table);
-                        value = "";
-                        break;
-                    default:
-                        value = "";
-                        log_module(MAIN_LOG, LOG_ERROR, "Invalid exp.type %d from expansion function %p.", exp.type, expand_f);
-                        break;
-                    }
-                   ipos = name_end - input.list;
-                   break;
-               }
-           }
-
-       /* Let it fall through when there's no expansion function or
-       terminating ')'. */
-       default:
-               value = alloca(3);
-               value[0] = '$';
-               value[1] = input.list[ipos];
-               value[2] = 0;
-       }
-       ipos++;
+        case 'A':
+            value = handle ? handle->handle : "Account";
+            break;
+        case 'U':
+            value = message_dest ? message_dest->nick : "Nick";
+            break;
+        case 'I':
+            value = message_dest ? (IsFakeIdent(message_dest) ? message_dest->fakeident : message_dest->ident) : "Ident";
+            break;
+        case 'H':
+            value = message_dest ? (IsFakeHost(message_dest) ? message_dest->fakehost : message_dest->hostname) : "Hostname";
+            break;
+#define SEND_LINE(TRUNCED) do { \
+    line[pos] = 0; \
+    if (pos > 0) { \
+        if (!(msg_type & MSG_TYPE_MULTILINE) && (pos > 1) && TRUNCED) \
+            line[pos-2] = line[pos-1] = '.'; \
+        irc_send(src, dest, line); \
+    } \
+    chars_sent += pos; \
+    pos = 0; \
+    newline_ipos = ipos; \
+    if (!(msg_type & MSG_TYPE_MULTILINE)) return chars_sent; \
+} while (0)
+        /* Custom expansion handled by helpfile-specific function. */
+        case '{':
+        case '(': {
+            struct helpfile_expansion exp;
+            char *name_end = input.list + ipos + 1, *colon = NULL;
+
+            while (*name_end != '}' && *name_end != ')' && *name_end) {
+                if (*name_end == ':') {
+                    colon = name_end;
+                    *colon = '\0';
+                }
+                name_end++;
+            }
+            if (!*name_end)
+                goto fallthrough;
+            *name_end = '\0';
+            if (colon) {
+                struct module *module = module_find(input.list + ipos + 1);
+                if (module && module->expand_help)
+                    exp = module->expand_help(colon + 1);
+                else {
+                    *colon = ':';
+                    goto fallthrough;
+                }
+            } else if (expand_f)
+                exp = expand_f(input.list + ipos + 1);
+            else
+                goto fallthrough;
+            switch (exp.type) {
+            case HF_STRING:
+                free_value = value = exp.value.str;
+                if (!value)
+                    value = "";
+                break;
+            case HF_TABLE:
+                /* Must send current line, then emit table. */
+                SEND_LINE(0);
+                table_send(src, (message_dest ? message_dest->nick : dest), 0, irc_send, exp.value.table);
+                value = "";
+                break;
+            default:
+                value = "";
+                log_module(MAIN_LOG, LOG_ERROR, "Invalid exp.type %d from expansion function %p.", exp.type, (void*)expand_f);
+                break;
+            }
+            ipos = name_end - input.list;
+            break;
+        }
+        default:
+        fallthrough:
+            value = alloca(3);
+            value[0] = '$';
+            value[1] = input.list[ipos];
+            value[2] = 0;
+        }
+        ipos++;
         while ((pos + strlen(value) > size) || strchr(value, '\n')) {
             unsigned int avail;
             avail = size - pos - 1;
@@ -591,7 +634,7 @@ vsend_message(const char *dest, struct userNode *src, struct handle_info *handle
                 /* word to send is too big to send now.. what to do? */
                 if (pos > 0) {
                     /* try to put it on a separate line */
-                    SEND_LINE();
+                    SEND_LINE(1);
                 } else {
                     /* already at start of line; only send part of it */
                     strncpy(line, value, avail);
@@ -604,24 +647,22 @@ vsend_message(const char *dest, struct userNode *src, struct handle_info *handle
             }
             /* if we're looking at a newline, send the accumulated text */
             if (*value == '\n') {
-                SEND_LINE();
+                SEND_LINE(0);
                 value++;
             }
         }
         length = strlen(value);
-       memcpy(line + pos, value, length);
+        memcpy(line + pos, value, length);
         if (free_value)
             free(free_value);
-       pos += length;
+        pos += length;
         if ((pos < size-1) && input.list[ipos]) {
-            expand_pos = pos;
             expand_ipos = ipos;
             continue;
         }
       send_line:
-        expand_pos = pos;
         expand_ipos = ipos;
-        SEND_LINE();
+        SEND_LINE(0);
 #undef SEND_LINE
     }
     return chars_sent;
@@ -633,7 +674,7 @@ send_message(struct userNode *dest, struct userNode *src, const char *format, ..
     int res;
     va_list ap;
 
-    if (IsLocal(dest)) return 0;
+    if (IsLocal(dest) && !IsDummy(dest)) return 0;
     va_start(ap, format);
     res = vsend_message(dest->nick, src, dest->handle_info, 0, NULL, format, ap);
     va_end(ap);
@@ -645,7 +686,7 @@ send_message_type(int msg_type, struct userNode *dest, struct userNode *src, con
     int res;
     va_list ap;
 
-    if (IsLocal(dest)) return 0;
+    if (IsLocal(dest) && !IsDummy(dest)) return 0;
     va_start(ap, format);
     res = vsend_message(dest->nick, src, dest->handle_info, msg_type, NULL, format, ap);
     va_end(ap);
@@ -671,7 +712,7 @@ _send_help(struct userNode *dest, struct userNode *src, expand_func_t expand, co
 
     va_list ap;
     va_start(ap, format);
-    res = vsend_message(dest->nick, src, dest->handle_info, 4, expand, format, ap);
+    res = vsend_message(dest->nick, src, dest->handle_info, 12, expand, format, ap);
     va_end(ap);
     return res;
 }
@@ -703,7 +744,7 @@ send_help(struct userNode *dest, struct userNode *src, struct helpfile *hf, cons
     if (!rec)
         return send_message(dest, src, "MSG_TOPIC_UNKNOWN");
     if (rec->type != RECDB_QSTRING)
-       return send_message(dest, src, "HFMSG_HELP_NOT_STRING");
+        return send_message(dest, src, "HFMSG_HELP_NOT_STRING");
     return _send_help(dest, src, hf->expand, rec->d.qstring);
 }
 
@@ -779,7 +820,7 @@ helpfile_eval_identifier(const char *start, const char **end)
         *end = start + 5;
         return 0;
     } else {
-        log_module(MAIN_LOG, LOG_FATAL, "Unexpected helpfile identifier '%.*s'.", *end-start, start);
+        log_module(MAIN_LOG, LOG_FATAL, "Unexpected helpfile identifier '%.*s'.", (int)(*end-start), start);
         return -1;
     }
 }
@@ -811,7 +852,7 @@ helpfile_eval_atomicexpr(const char *start, const char **end)
     while (isspace(*sep) && (sep < *end))
         sep++;
     if ((sep == *end) || (sep[0] != ')')) {
-        log_module(MAIN_LOG, LOG_FATAL, "Expected close parenthesis at '%.*s'.", *end-sep, sep);
+        log_module(MAIN_LOG, LOG_FATAL, "Expected close parenthesis at '%.*s'.", (int)(*end-sep), sep);
         return -1;
     }
 
@@ -848,7 +889,7 @@ helpfile_eval_expr(const char *start, const char **end)
         sep += len;
     }
     if (op == OP_INVALID) {
-        log_module(MAIN_LOG, LOG_FATAL, "Unrecognized helpfile operator at '%.*s'.", *end-sep, sep);
+        log_module(MAIN_LOG, LOG_FATAL, "Unrecognized helpfile operator at '%.*s'.", (int)(*end-sep), sep);
         return -1;
     }
 
@@ -862,7 +903,7 @@ helpfile_eval_expr(const char *start, const char **end)
     while (isspace(*sep2) && (sep2 < *end))
         sep2++;
     if (sep2 != *end) {
-        log_module(MAIN_LOG, LOG_FATAL, "Trailing garbage in helpfile expression: '%.*s'.", *end-sep2, sep2);
+        log_module(MAIN_LOG, LOG_FATAL, "Trailing garbage in helpfile expression: '%.*s'.", (int)(*end-sep2), sep2);
         return -1;
     }
 
@@ -889,7 +930,7 @@ helpfile_eval_condition(const char *start, const char **end)
     for (term = start; isalnum(*term) && (term < *end); ++term) ;
     if (term != start) {
         if ((term + 2 >= *end) || (term[0] != ':') || (term[1] != ' ')) {
-            log_module(MAIN_LOG, LOG_FATAL, "In helpfile condition '%.*s' expected prefix to end with ': '.", *end-start, start);
+            log_module(MAIN_LOG, LOG_FATAL, "In helpfile condition '%.*s' expected prefix to end with ': '.", (int)(*end-start), start);
             return -1;
         }
         start = term + 2;
@@ -907,25 +948,25 @@ unlistify_help(const char *key, void *data, void *extra)
 
     switch (rd->type) {
     case RECDB_QSTRING:
-       dict_insert(newdb, strdup(key), alloc_record_data_qstring(GET_RECORD_QSTRING(rd)));
-       return 0;
+        dict_insert(newdb, strdup(key), alloc_record_data_qstring(GET_RECORD_QSTRING(rd)));
+        return 0;
     case RECDB_STRING_LIST: {
-       struct string_list *slist = GET_RECORD_STRING_LIST(rd);
-       char *dest;
-       unsigned int totlen, len, i;
-
-       for (i=totlen=0; i<slist->used; i++)
-           totlen = totlen + strlen(slist->list[i]) + 1;
-       dest = alloca(totlen+1);
-       for (i=totlen=0; i<slist->used; i++) {
-           len = strlen(slist->list[i]);
-           memcpy(dest+totlen, slist->list[i], len);
-           dest[totlen+len] = '\n';
-           totlen = totlen + len + 1;
-       }
-       dest[totlen] = 0;
-       dict_insert(newdb, strdup(key), alloc_record_data_qstring(dest));
-       return 0;
+        struct string_list *slist = GET_RECORD_STRING_LIST(rd);
+        char *dest;
+        unsigned int totlen, len, i;
+
+        for (i=totlen=0; i<slist->used; i++)
+            totlen = totlen + strlen(slist->list[i]) + 1;
+        dest = alloca(totlen+1);
+        for (i=totlen=0; i<slist->used; i++) {
+            len = strlen(slist->list[i]);
+            memcpy(dest+totlen, slist->list[i], len);
+            dest[totlen+len] = '\n';
+            totlen = totlen + len + 1;
+        }
+        dest[totlen] = 0;
+        dict_insert(newdb, strdup(key), alloc_record_data_qstring(dest));
+        return 0;
     }
     case RECDB_OBJECT: {
         dict_iterator_t it;
@@ -955,7 +996,7 @@ unlistify_help(const char *key, void *data, void *extra)
         return 0;
     }
     default:
-       return 1;
+        return 1;
     }
 }
 
@@ -976,8 +1017,8 @@ open_helpfile(const char *fname, expand_func_t expand)
         dict_insert(language_find("C")->helpfiles, hf->name, hf);
     }
     if (db) {
-       dict_foreach(db, unlistify_help, hf->db);
-       free_database(db);
+        dict_foreach(db, unlistify_help, hf->db);
+        free_database(db);
     }
     return hf;
 }
@@ -1007,8 +1048,27 @@ void helpfile_init(void)
     language_read_list();
 }
 
+static void helpfile_read_languages(void)
+{
+    dict_iterator_t it;
+    dict_t dict;
+
+    language_read_list();
+    for (it = dict_first(languages); it; it = iter_next(it))
+        language_read(iter_key(it));
+
+    /* If the user has a strings.db in their languages directory,
+     * allow that to override C language strings.
+     */
+    dict = parse_database("languages/strings.db");
+    if (dict) {
+        language_set_messages(lang_C, dict);
+        free_database(dict);
+    }
+}
+
 void helpfile_finalize(void)
 {
-    language_read_all();
+    conf_register_reload(helpfile_read_languages);
     reg_exit_func(language_cleanup);
 }