source: lib/misc.c @ 7e90c03

Last change on this file since 7e90c03 was 7e90c03, checked in by GitHub <noreply@…>, at 2022-11-28T17:47:55Z

Add HTML5 escape sequence for ', < and > (#164)

  • Property mode set to 100644
File size: 17.7 KB
Line 
1  /********************************************************************\
2  * BitlBee -- An IRC to other IM-networks gateway                     *
3  *                                                                    *
4  * Copyright 2002-2012 Wilmer van der Gaast and others                *
5  \********************************************************************/
6
7/*
8 * Various utility functions. Some are copied from Gaim to support the
9 * IM-modules, most are from BitlBee.
10 *
11 * Copyright (C) 1998-1999, Mark Spencer <markster@marko.net>
12 *                          (and possibly other members of the Gaim team)
13 * Copyright 2002-2012 Wilmer van der Gaast <wilmer@gaast.net>
14 */
15
16/*
17  This program is free software; you can redistribute it and/or modify
18  it under the terms of the GNU General Public License as published by
19  the Free Software Foundation; either version 2 of the License, or
20  (at your option) any later version.
21
22  This program is distributed in the hope that it will be useful,
23  but WITHOUT ANY WARRANTY; without even the implied warranty of
24  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
25  GNU General Public License for more details.
26
27  You should have received a copy of the GNU General Public License with
28  the Debian GNU/Linux distribution in /usr/share/common-licenses/GPL;
29  if not, write to the Free Software Foundation, Inc., 51 Franklin St.,
30  Fifth Floor, Boston, MA  02110-1301  USA
31*/
32
33#define BITLBEE_CORE
34#include "nogaim.h"
35#include "base64.h"
36#include "md5.h"
37#include <stdio.h>
38#include <stdlib.h>
39#include <string.h>
40#include <ctype.h>
41#include <glib.h>
42#include <time.h>
43
44#ifdef HAVE_RESOLV_A
45#include <arpa/nameser.h>
46#include <resolv.h>
47#endif
48
49#include "md5.h"
50#include "ssl_client.h"
51
52void strip_linefeed(gchar *text)
53{
54        int i, j;
55        gchar *text2 = g_malloc(strlen(text) + 1);
56
57        for (i = 0, j = 0; text[i]; i++) {
58                if (text[i] != '\r') {
59                        text2[j++] = text[i];
60                }
61        }
62        text2[j] = '\0';
63
64        strcpy(text, text2);
65        g_free(text2);
66}
67
68time_t get_time(int year, int month, int day, int hour, int min, int sec)
69{
70        struct tm tm;
71
72        memset(&tm, 0, sizeof(struct tm));
73        tm.tm_year = year - 1900;
74        tm.tm_mon = month - 1;
75        tm.tm_mday = day;
76        tm.tm_hour = hour;
77        tm.tm_min = min;
78        tm.tm_sec = sec >= 0 ? sec : time(NULL) % 60;
79
80        return mktime(&tm);
81}
82
83time_t mktime_utc(struct tm *tp)
84{
85        struct tm utc;
86        time_t res, tres;
87
88        tp->tm_isdst = -1;
89        res = mktime(tp);
90        /* Problem is, mktime() just gave us the GMT timestamp for the
91           given local time... While the given time WAS NOT local. So
92           we should fix this now.
93
94           Now I could choose between messing with environment variables
95           (kludgy) or using timegm() (not portable)... Or doing the
96           following, which I actually prefer...
97
98           tzset() may also work but in other places I actually want to
99           use local time.
100
101           FFFFFFFFFFFFFFFFFFFFFUUUUUUUUUUUUUUUUUUUU!! */
102        gmtime_r(&res, &utc);
103        utc.tm_isdst = -1;
104        if (utc.tm_hour == tp->tm_hour && utc.tm_min == tp->tm_min) {
105                /* Sweet! We're in UTC right now... */
106                return res;
107        }
108
109        tres = mktime(&utc);
110        res += res - tres;
111
112        /* Yes, this is a hack. And it will go wrong around DST changes.
113           BUT this is more likely to be threadsafe than messing with
114           environment variables, and possibly more portable... */
115
116        return res;
117}
118
119typedef struct htmlentity {
120        char code[7];
121        char is[3];
122} htmlentity_t;
123
124static const htmlentity_t ent[] =
125{
126        { "lt",     "<" },
127        { "gt",     ">" },
128        { "amp",    "&" },
129        { "apos",   "'" },
130        { "quot",   "\"" },
131        { "aacute", "á" },
132        { "eacute", "é" },
133        { "iacute", "é" },
134        { "oacute", "ó" },
135        { "uacute", "ú" },
136        { "agrave", "à" },
137        { "egrave", "è" },
138        { "igrave", "ì" },
139        { "ograve", "ò" },
140        { "ugrave", "ù" },
141        { "acirc",  "â" },
142        { "ecirc",  "ê" },
143        { "icirc",  "î" },
144        { "ocirc",  "ô" },
145        { "ucirc",  "û" },
146        { "auml",   "ä" },
147        { "euml",   "ë" },
148        { "iuml",   "ï" },
149        { "ouml",   "ö" },
150        { "uuml",   "ü" },
151        { "nbsp",   " " },
152        { "#38",    "&" },
153        { "#39",    "'" },
154        { "#60",    "<" },
155        { "#62",    ">" },
156        { "",        ""  }
157};
158
159void strip_html(char *in)
160{
161        char *start = in;
162        char out[strlen(in) + 1];
163        char *s = out, *cs;
164        int i, matched;
165        int taglen;
166
167        memset(out, 0, sizeof(out));
168
169        while (*in) {
170                if (*in == '<' && (g_ascii_isalpha(*(in + 1)) || *(in + 1) == '/')) {
171                        /* If in points at a < and in+1 points at a letter or a slash, this is probably
172                           a HTML-tag. Try to find a closing > and continue there. If the > can't be
173                           found, assume that it wasn't a HTML-tag after all. */
174
175                        cs = in;
176
177                        while (*in && *in != '>') {
178                                in++;
179                        }
180
181                        taglen = in - cs - 1;   /* not <0 because the above loop runs at least once */
182                        if (*in) {
183                                if (g_strncasecmp(cs + 1, "b", taglen) == 0) {
184                                        *(s++) = '\x02';
185                                } else if (g_strncasecmp(cs + 1, "/b", taglen) == 0) {
186                                        *(s++) = '\x02';
187                                } else if (g_strncasecmp(cs + 1, "i", taglen) == 0) {
188                                        *(s++) = '\x1f';
189                                } else if (g_strncasecmp(cs + 1, "/i", taglen) == 0) {
190                                        *(s++) = '\x1f';
191                                } else if (g_strncasecmp(cs + 1, "br", taglen) == 0) {
192                                        *(s++) = '\n';
193                                } else if (g_strncasecmp(cs + 1, "br/", taglen) == 0) {
194                                        *(s++) = '\n';
195                                } else if (g_strncasecmp(cs + 1, "br /", taglen) == 0) {
196                                        *(s++) = '\n';
197                                }
198                                in++;
199                        } else {
200                                in = cs;
201                                *(s++) = *(in++);
202                        }
203                } else if (*in == '&') {
204                        cs = ++in;
205
206                        if (*in == '#') {
207                                in++;
208                        }
209
210                        while (*in && g_ascii_isalnum(*in)) {
211                                in++;
212                        }
213
214                        if (*in == ';') {
215                                in++;
216                        }
217                        matched = 0;
218
219                        for (i = 0; *ent[i].code; i++) {
220                                if (g_strncasecmp(ent[i].code, cs, strlen(ent[i].code)) == 0) {
221                                        int j;
222
223                                        for (j = 0; ent[i].is[j]; j++) {
224                                                *(s++) = ent[i].is[j];
225                                        }
226
227                                        matched = 1;
228                                        break;
229                                }
230                        }
231
232                        /* None of the entities were matched, so return the string */
233                        if (!matched) {
234                                in = cs - 1;
235                                *(s++) = *(in++);
236                        }
237                } else {
238                        *(s++) = *(in++);
239                }
240        }
241
242        strcpy(start, out);
243}
244
245char *escape_html(const char *html)
246{
247        const char *c = html;
248        GString *ret;
249        char *str;
250
251        if (html == NULL) {
252                return(NULL);
253        }
254
255        ret = g_string_new("");
256
257        while (*c) {
258                switch (*c) {
259                case '&':
260                        ret = g_string_append(ret, "&amp;");
261                        break;
262                case '<':
263                        ret = g_string_append(ret, "&lt;");
264                        break;
265                case '>':
266                        ret = g_string_append(ret, "&gt;");
267                        break;
268                case '"':
269                        ret = g_string_append(ret, "&quot;");
270                        break;
271                default:
272                        ret = g_string_append_c(ret, *c);
273                }
274                c++;
275        }
276
277        str = ret->str;
278        g_string_free(ret, FALSE);
279        return(str);
280}
281
282/* Decode%20a%20file%20name                                             */
283void http_decode(char *s)
284{
285        char *t;
286        int i, j, k;
287
288        t = g_new(char, strlen(s) + 1);
289
290        for (i = j = 0; s[i]; i++, j++) {
291                if (s[i] == '%') {
292                        if (sscanf(s + i + 1, "%2x", &k)) {
293                                t[j] = k;
294                                i += 2;
295                        } else {
296                                *t = 0;
297                                break;
298                        }
299                } else {
300                        t[j] = s[i];
301                }
302        }
303        t[j] = 0;
304
305        strcpy(s, t);
306        g_free(t);
307}
308
309/* Warning: This one explodes the string. Worst-cases can make the string 3x its original size! */
310/* This function is safe, but make sure you call it safely as well! */
311void http_encode(char *s)
312{
313        char t[strlen(s) + 1];
314        int i, j;
315
316        strcpy(t, s);
317        for (i = j = 0; t[i]; i++, j++) {
318                /* Warning: g_ascii_isalnum() is locale-aware, so don't use it here! */
319                if ((t[i] >= 'A' && t[i] <= 'Z') ||
320                    (t[i] >= 'a' && t[i] <= 'z') ||
321                    (t[i] >= '0' && t[i] <= '9') ||
322                    strchr("._-~", t[i])) {
323                        s[j] = t[i];
324                } else {
325                        sprintf(s + j, "%%%02X", ((unsigned char *) t)[i]);
326                        j += 2;
327                }
328        }
329        s[j] = 0;
330}
331
332/* Strip newlines from a string. Modifies the string passed to it. */
333char *strip_newlines(char *source)
334{
335        int i;
336
337        for (i = 0; source[i] != '\0'; i++) {
338                if (source[i] == '\n' || source[i] == '\r') {
339                        source[i] = ' ';
340                }
341        }
342
343        return source;
344}
345
346/* Convert from one charset to another.
347
348   from_cs, to_cs: Source and destination charsets
349   src, dst: Source and destination strings
350   size: Size if src. 0 == use strlen(). strlen() is not reliable for UNICODE/UTF16 strings though.
351   maxbuf: Maximum number of bytes to write to dst
352
353   Returns the number of bytes written to maxbuf or -1 on an error.
354*/
355signed int do_iconv(char *from_cs, char *to_cs, char *src, char *dst, size_t size, size_t maxbuf)
356{
357        GIConv cd;
358        size_t res;
359        size_t inbytesleft, outbytesleft;
360        char *inbuf = src;
361        char *outbuf = dst;
362
363        cd = g_iconv_open(to_cs, from_cs);
364        if (cd == (GIConv) - 1) {
365                return -1;
366        }
367
368        inbytesleft = size ? size : strlen(src);
369        outbytesleft = maxbuf - 1;
370        res = g_iconv(cd, &inbuf, &inbytesleft, &outbuf, &outbytesleft);
371        *outbuf = '\0';
372        g_iconv_close(cd);
373
374        if (res != 0) {
375                return -1;
376        } else {
377                return outbuf - dst;
378        }
379}
380
381/* A wrapper for /dev/urandom.
382 * If /dev/urandom is not present or not usable, it calls abort()
383 * to prevent bitlbee from working without a decent entropy source */
384void random_bytes(unsigned char *buf, int count)
385{
386        int fd;
387
388        if (((fd = open("/dev/urandom", O_RDONLY)) == -1) ||
389            (read(fd, buf, count) == -1)) {
390                log_message(LOGLVL_ERROR, "/dev/urandom not present - aborting");
391                abort();
392        }
393
394        close(fd);
395}
396
397int is_bool(char *value)
398{
399        if (*value == 0) {
400                return 0;
401        }
402
403        if ((g_strcasecmp(value,
404                          "true") == 0) || (g_strcasecmp(value, "yes") == 0) || (g_strcasecmp(value, "on") == 0)) {
405                return 1;
406        }
407        if ((g_strcasecmp(value,
408                          "false") == 0) || (g_strcasecmp(value, "no") == 0) || (g_strcasecmp(value, "off") == 0)) {
409                return 1;
410        }
411
412        while (*value) {
413                if (!g_ascii_isdigit(*value)) {
414                        return 0;
415                } else {
416                        value++;
417                }
418        }
419
420        return 1;
421}
422
423int bool2int(char *value)
424{
425        int i;
426
427        if ((g_strcasecmp(value,
428                          "true") == 0) || (g_strcasecmp(value, "yes") == 0) || (g_strcasecmp(value, "on") == 0)) {
429                return 1;
430        }
431        if ((g_strcasecmp(value,
432                          "false") == 0) || (g_strcasecmp(value, "no") == 0) || (g_strcasecmp(value, "off") == 0)) {
433                return 0;
434        }
435
436        if (sscanf(value, "%d", &i) == 1) {
437                return i;
438        }
439
440        return 0;
441}
442
443struct ns_srv_reply **srv_lookup(char *service, char *protocol, char *domain)
444{
445        struct ns_srv_reply **replies = NULL;
446
447#ifdef HAVE_RESOLV_A
448        struct ns_srv_reply *reply = NULL;
449        char name[1024];
450        unsigned char querybuf[1024];
451        const unsigned char *buf;
452        ns_msg nsh;
453        ns_rr rr;
454        int n, len, size;
455
456        g_snprintf(name, sizeof(name), "_%s._%s.%s", service, protocol, domain);
457
458        if ((size = res_query(name, ns_c_in, ns_t_srv, querybuf, sizeof(querybuf))) <= 0) {
459                return NULL;
460        }
461
462        if (ns_initparse(querybuf, size, &nsh) != 0) {
463                return NULL;
464        }
465
466        n = 0;
467        while (ns_parserr(&nsh, ns_s_an, n, &rr) == 0) {
468                char name[NS_MAXDNAME];
469
470                if (ns_rr_rdlen(rr) < 7) {
471                        break;
472                }
473
474                buf = ns_rr_rdata(rr);
475
476                if (dn_expand(querybuf, querybuf + size, &buf[6], name, NS_MAXDNAME) == -1) {
477                        break;
478                }
479
480                len = strlen(name) + 1;
481
482                reply = g_malloc(sizeof(struct ns_srv_reply) + len);
483                memcpy(reply->name, name, len);
484
485                reply->prio = (buf[0] << 8) | buf[1];
486                reply->weight = (buf[2] << 8) | buf[3];
487                reply->port = (buf[4] << 8) | buf[5];
488
489                n++;
490                replies = g_renew(struct ns_srv_reply *, replies, n + 1);
491                replies[n - 1] = reply;
492        }
493        if (replies) {
494                replies[n] = NULL;
495        }
496#endif
497
498        return replies;
499}
500
501void srv_free(struct ns_srv_reply **srv)
502{
503        int i;
504
505        if (srv == NULL) {
506                return;
507        }
508
509        for (i = 0; srv[i]; i++) {
510                g_free(srv[i]);
511        }
512        g_free(srv);
513}
514
515char *word_wrap(const char *msg, int line_len)
516{
517        GString *ret = g_string_sized_new(strlen(msg) + 16);
518
519        while (strlen(msg) > line_len) {
520                int i;
521
522                /* First try to find out if there's a newline already. Don't
523                   want to add more splits than necessary. */
524                for (i = line_len; i > 0 && msg[i] != '\n'; i--) {
525                        ;
526                }
527                if (msg[i] == '\n') {
528                        g_string_append_len(ret, msg, i + 1);
529                        msg += i + 1;
530                        continue;
531                }
532
533                for (i = line_len; i > 0; i--) {
534                        if (msg[i] == '-') {
535                                g_string_append_len(ret, msg, i + 1);
536                                g_string_append_c(ret, '\n');
537                                msg += i + 1;
538                                break;
539                        } else if (msg[i] == ' ') {
540                                g_string_append_len(ret, msg, i);
541                                g_string_append_c(ret, '\n');
542                                msg += i + 1;
543                                break;
544                        }
545                }
546                if (i == 0) {
547                        const char *end;
548                        size_t len;
549
550                        g_utf8_validate(msg, line_len, &end);
551
552                        len = (end != msg) ? end - msg : line_len;
553
554                        g_string_append_len(ret, msg, len);
555                        g_string_append_c(ret, '\n');
556                        msg += len;
557                }
558        }
559        g_string_append(ret, msg);
560
561        return g_string_free(ret, FALSE);
562}
563
564gboolean ssl_sockerr_again(void *ssl)
565{
566        if (ssl) {
567                return ssl_errno == SSL_AGAIN;
568        } else {
569                return sockerr_again();
570        }
571}
572
573/* Returns values: -1 == Failure (base64-decoded to something unexpected)
574                    0 == Okay
575                    1 == Password doesn't match the hash. */
576int md5_verify_password(char *password, char *hash)
577{
578        md5_byte_t *pass_dec = NULL;
579        md5_byte_t pass_md5[16];
580        md5_state_t md5_state;
581        int ret = -1, i;
582
583        if (base64_decode(hash, &pass_dec) == 21) {
584                md5_init(&md5_state);
585                md5_append(&md5_state, (md5_byte_t *) password, strlen(password));
586                md5_append(&md5_state, (md5_byte_t *) pass_dec + 16, 5);  /* Hmmm, salt! */
587                md5_finish(&md5_state, pass_md5);
588
589                for (i = 0; i < 16; i++) {
590                        if (pass_dec[i] != pass_md5[i]) {
591                                ret = 1;
592                                break;
593                        }
594                }
595
596                /* If we reached the end of the loop, it was a match! */
597                if (i == 16) {
598                        ret = 0;
599                }
600        }
601
602        g_free(pass_dec);
603
604        return ret;
605}
606
607/* Split commands (root-style, *not* IRC-style). Handles "quoting of"
608   white\ space in 'various ways'. Returns a NULL-terminated static
609   char** so watch out with nested use! Definitely not thread-safe. */
610char **split_command_parts(char *command, int limit)
611{
612        static char *cmd[IRC_MAX_ARGS + 1];
613        char *s, q = 0;
614        int k;
615
616        memset(cmd, 0, sizeof(cmd));
617        cmd[0] = command;
618        k = 1;
619        for (s = command; *s && k < IRC_MAX_ARGS; s++) {
620                if (*s == ' ' && !q) {
621                        *s = 0;
622                        while (*++s == ' ') {
623                                ;
624                        }
625                        if (k != limit && (*s == '"' || *s == '\'')) {
626                                q = *s;
627                                s++;
628                        }
629                        if (*s) {
630                                cmd[k++] = s;
631                                if (limit && k > limit) {
632                                        break;
633                                }
634                                s--;
635                        } else {
636                                break;
637                        }
638                } else if (*s == '\\' && ((!q && s[1]) || (q && q == s[1]))) {
639                        char *cpy;
640
641                        for (cpy = s; *cpy; cpy++) {
642                                cpy[0] = cpy[1];
643                        }
644                } else if (*s == q) {
645                        q = *s = 0;
646                }
647        }
648
649        /* Full zero-padding for easier argc checking. */
650        while (k <= IRC_MAX_ARGS) {
651                cmd[k++] = NULL;
652        }
653
654        return cmd;
655}
656
657char *get_rfc822_header(const char *text, const char *header, int len)
658{
659        int hlen = strlen(header), i;
660        const char *ret;
661
662        if (text == NULL) {
663                return NULL;
664        }
665
666        if (len == 0) {
667                len = strlen(text);
668        }
669
670        i = 0;
671        while ((i + hlen) < len) {
672                /* Maybe this is a bit over-commented, but I just hate this part... */
673                if (g_strncasecmp(text + i, header, hlen) == 0) {
674                        /* Skip to the (probable) end of the header */
675                        i += hlen;
676
677                        /* Find the first non-[: \t] character */
678                        while (i < len && (text[i] == ':' || text[i] == ' ' || text[i] == '\t')) {
679                                i++;
680                        }
681
682                        /* Make sure we're still inside the string */
683                        if (i >= len) {
684                                return(NULL);
685                        }
686
687                        /* Save the position */
688                        ret = text + i;
689
690                        /* Search for the end of this line */
691                        while (i < len && text[i] != '\r' && text[i] != '\n') {
692                                i++;
693                        }
694
695                        /* Copy the found data */
696                        return(g_strndup(ret, text + i - ret));
697                }
698
699                /* This wasn't the header we were looking for, skip to the next line. */
700                while (i < len && (text[i] != '\r' && text[i] != '\n')) {
701                        i++;
702                }
703                while (i < len && (text[i] == '\r' || text[i] == '\n')) {
704                        i++;
705                }
706
707                /* End of headers? */
708                if ((i >= 4 && strncmp(text + i - 4, "\r\n\r\n", 4) == 0) ||
709                    (i >= 2 && (strncmp(text + i - 2, "\n\n", 2) == 0 ||
710                                strncmp(text + i - 2, "\r\r", 2) == 0))) {
711                        break;
712                }
713        }
714
715        return NULL;
716}
717
718/* Takes a string, truncates it where it's safe, returns the new length */
719int truncate_utf8(char *string, int maxlen)
720{
721        char *end;
722
723        g_utf8_validate((const gchar *) string, maxlen, (const gchar **) &end);
724        *end = '\0';
725        return end - string;
726}
727
728/* Parses a guint64 from string, returns TRUE on success */
729gboolean parse_int64(char *string, int base, guint64 *number)
730{
731        guint64 parsed;
732        char *endptr;
733
734        errno = 0;
735        parsed = g_ascii_strtoull(string, &endptr, base);
736        if (errno || endptr == string || *endptr != '\0') {
737                return FALSE;
738        }
739        *number = parsed;
740        return TRUE;
741}
742
743/* Filters all the characters in 'blacklist' replacing them with 'replacement'.
744 * Modifies the string in-place and returns the string itself.
745 * For the opposite, use g_strcanon() */
746char *str_reject_chars(char *string, const char *reject, char replacement)
747{
748        char *c = string;
749
750        while (*c) {
751                c += strcspn(c, reject);
752                if (*c) {
753                        *c = replacement;
754                }
755        }
756
757        return string;
758}
759
760/* Returns a string that is exactly 'char_len' utf8 characters long (not bytes),
761 * padded to the right with spaces or truncated with the 'ellipsis' parameter
762 * if specified (can be NULL).
763 * Returns a newly allocated string, or NULL on invalid parameters. */
764char *str_pad_and_truncate(const char *string, long char_len, const char *ellipsis)
765{
766        size_t string_len = strlen(string);
767        size_t ellipsis_len = (ellipsis) ? strlen(ellipsis) : 0;
768        long orig_len = g_utf8_strlen(string, -1);
769
770        g_return_val_if_fail(char_len > ellipsis_len, NULL);
771
772        if (orig_len > char_len) {
773                char *ret = g_malloc(string_len + 1);
774                g_utf8_strncpy(ret, string, char_len - ellipsis_len);
775                if (ellipsis) {
776                        g_strlcat(ret, ellipsis, string_len);
777                }
778                return ret;
779        } else if (orig_len < char_len) {
780                return g_strdup_printf("%s%*s", string, (int) (char_len - orig_len), "");
781        } else {
782                return g_strdup(string);
783        }
784}
785
786/* copied from irssi's misc.c, by timo sirainen */
787int b_istr_equal(gconstpointer v, gconstpointer v2)
788{
789        return g_ascii_strcasecmp((const char *) v, (const char *) v2) == 0;
790}
791
792/* copied from irssi's misc.c, by lemonboy */
793guint b_istr_hash(gconstpointer v)
794{
795        const signed char *p;
796        guint32 h = 5381;
797
798        for (p = v; *p != '\0'; p++) {
799                h = (h << 5) + h + g_ascii_toupper(*p);
800        }
801
802        return h;
803}
804
805#ifdef NO_STRCASESTR
806char* strcasestr(const char* haystack, const char* needle)
807{
808        size_t haystackn = strlen(haystack);
809        size_t needlen = strlen(needle);
810
811        const char *p = haystack;
812        while (haystackn >= needlen) {
813                if (g_strncasecmp(p, needle, needlen) == 0) {
814                    return (char*) p;
815                }
816                p++;
817                haystackn--;
818        }
819        return NULL;
820}
821#endif
Note: See TracBrowser for help on using the repository browser.