diff --git a/tools/ftp-indexer.c b/tools/ftp-indexer.c index 5457f3d..32c6de9 100644 --- a/tools/ftp-indexer.c +++ b/tools/ftp-indexer.c @@ -1,1169 +1,1293 @@ /* ftp-indexer.c - Create an HTML index file for an FTP directory * Copyright (C) 2017 g10 Code GmbH * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, see . */ -/* How to use: +/* The following script is triggred by a cronjob at ftp.gnupg.org to + * build the index pages. + * +--8<---------------cut here---------------start------------->8--- +#!/bin/sh + +set -e +top=/home/ftp +scratch=/home/ftp/.scratch +cd "$top" + +opt_force=no +if [ "$1" == "--force" ]; then + shift + opt_force=yes +fi + +INDEXER=/usr/local/bin/ftp-indexer +if [ ! -x $INDEXER ]; then + echo "mk-ftp-index.html.sh: Index tool $INDEXER not found - aborting" >&2 + exit 1 +fi +INDEXER_OPTS="--reverse-ver --gpgweb --readme --index $scratch/ftp-index.new" +INDEXER_OPTS="$INDEXER_OPTS --exclude README --exclude index.html" + + +(find . -type d ! -name '\.*' ! -name dev ; echo .) |\ + while read dir rest; do + dir=${dir##./} + if cd "$dir"; then + if [ "$dir" = "." ]; then + desc="/" + extraopt="--exclude dev" + else + desc="$dir/" + extraopt="" + fi + + [ -f $scratch/index.html ] && rm $scratch/index.html + [ -f index.html ] && cat index.html >$scratch/index.html + $INDEXER $INDEXER_OPTS $extraopt . "$desc" >$scratch/index.html.new + if [ $opt_force = no -a -f $scratch/index.html ]; then + grep -v '^$scratch/index.html.x + grep -v '^$scratch/index.html.new.x + if ! cmp -s $scratch/index.html.x $scratch/index.html.new.x ; then + mv $scratch/index.html.new index.html + mv $scratch/ftp-index.new .ftp-index + fi + rm $scratch/index.html + [ -f $scratch/index.html.new ] && rm $scratch/index.html.new + [ -f $scratch/ftp-index.new ] && rm $scratch/ftp-index.new + else + mv $scratch/index.html.new index.html + mv $scratch/ftp-index.new .ftp-index + fi + fi + cd "$top" +done +--8<---------------cut here---------------end--------------->8--- * **/ #include #include #include #include #include #include #include #include #include #include #include #define PGMNAME "ftp-indexer" - +#define VERSION "0.1" #define DIM(v) (sizeof(v)/sizeof((v)[0])) #define DIMof(type,member) DIM(((type *)0)->member) #if __GNUC__ > 2 || (__GNUC__ == 2 && __GNUC_MINOR__ >= 5) #define ATTR_PRINTF(a,b) __attribute__ ((format (printf,a,b))) #define ATTR_NR_PRINTF(a,b) __attribute__ ((noreturn,format (printf,a,b))) #else #define ATTR_PRINTF(a,b) #define ATTR_NR_PRINTF(a,b) #endif #define digitp(a) ((a) >= '0' && (a) <= '9') #define VALID_URI_CHARS "abcdefghijklmnopqrstuvwxyz" \ "ABCDEFGHIJKLMNOPQRSTUVWXYZ" \ "01234567890@" \ "!\"#$%&'()*+,-./:;<=>?[\\]^_{|}~" /* A simple object to keep strings in a list. */ struct strlist_s { struct strlist_s *next; char d[1]; }; typedef struct strlist_s *strlist_t; /* An object to collect information about files. */ struct finfo_s { struct finfo_s *next; unsigned int is_dir:1; unsigned int is_reg:1; time_t mtime; unsigned long long size; char name[1]; }; typedef struct finfo_s *finfo_t; static int opt_verbose; static int opt_debug; static int opt_reverse; static int opt_reverse_ver; static int opt_files_first; static int opt_html; -static int opt_index; +static const char *opt_index; static int opt_gpgweb; static int opt_readme; static strlist_t opt_exclude; static void die (const char *format, ...) ATTR_NR_PRINTF(1,2); static void err (const char *format, ...) ATTR_PRINTF(1,2); static void inf (const char *format, ...) ATTR_PRINTF(1,2); static void die (const char *fmt, ...) { va_list arg_ptr; va_start (arg_ptr, fmt); fputs (PGMNAME": ", stderr); vfprintf (stderr, fmt, arg_ptr); va_end (arg_ptr); exit (1); } static void err (const char *fmt, ...) { va_list arg_ptr; va_start (arg_ptr, fmt); fputs (PGMNAME": ", stderr); vfprintf (stderr, fmt, arg_ptr); va_end (arg_ptr); } static void inf (const char *fmt, ...) { va_list arg_ptr; if (!opt_verbose) return; va_start (arg_ptr, fmt); fputs (PGMNAME": ", stderr); vfprintf (stderr, fmt, arg_ptr); va_end (arg_ptr); } static void * xmalloc (size_t n) { void *p = malloc (n); if (!p) die ("out of core\n"); return p; } static void * xcalloc (size_t n, size_t k) { void *p = calloc (n, k); if (!p) die ("out of core\n"); return p; } static char * xstrdup (const char *string) { char *buf = xmalloc (strlen (string)); strcpy (buf, string); return buf; } static inline char * my_stpcpy (char *a, const char *b) { while (*b) *a++ = *b++; *a = 0; return (char*)a; } /* If SPECIAL is NULL this function escapes in forms mode. */ static size_t escape_data (char *buffer, const void *data, size_t datalen, const char *special) { int forms = !special; const unsigned char *s; size_t n = 0; if (forms) special = "%;?&="; for (s = data; datalen; s++, datalen--) { if (forms && *s == ' ') { if (buffer) *buffer++ = '+'; n++; } else if (forms && *s == '\n') { if (buffer) memcpy (buffer, "%0D%0A", 6); n += 6; } else if (forms && *s == '\r' && datalen > 1 && s[1] == '\n') { if (buffer) memcpy (buffer, "%0D%0A", 6); n += 6; s++; datalen--; } else if (strchr (VALID_URI_CHARS, *s) && !strchr (special, *s)) { if (buffer) *(unsigned char*)buffer++ = *s; n++; } else { if (buffer) { snprintf (buffer, 4, "%%%02X", *s); buffer += 3; } n += 3; } } return n; } static int insert_escapes (char *buffer, const char *string, const char *special) { return escape_data (buffer, string, strlen (string), special); } /* Allocate a new string from STRING using standard HTTP escaping as * well as escaping of characters given in SPECIALS. A common pattern * for SPECIALS is "%;?&=". However it depends on the needs, for * example "+" and "/: often needs to be escaped too. Returns NULL on * failure and sets ERRNO. If SPECIAL is NULL a dedicated forms * encoding mode is used. */ static char * http_escape_string (const char *string, const char *specials) { int n; char *buf; n = insert_escapes (NULL, string, specials); buf = xmalloc (n+1); insert_escapes (buf, string, specials); buf[n] = 0; return buf; } /* Same as http_escape_string but with an explict length. */ static char * http_escape_buffer (const char *string, size_t length, const char *specials) { int n; char *buf; n = escape_data (NULL, string, length, specials); buf = xmalloc (n+1); escape_data (buf, string, length, specials); buf[n] = 0; return buf; } /* Percent-escape the string STR by replacing colons with '%3a'. */ static char * do_percent_escape (const char *str) { int i, j; char *ptr; for (i=j=0; str[i]; i++) if (str[i] == ':' || str[i] == '%' || str[i] == '\n') j++; ptr = xmalloc (i + 2 * j + 1); i = 0; while (*str) { if (*str == ':') { ptr[i++] = '%'; ptr[i++] = '3'; ptr[i++] = 'a'; } else if (*str == '%') { ptr[i++] = '%'; ptr[i++] = '2'; ptr[i++] = '5'; } else if (*str == '\n') { /* The newline is problematic in a line-based format. */ ptr[i++] = '%'; ptr[i++] = '0'; ptr[i++] = 'a'; } else ptr[i++] = *str; str++; } ptr[i] = '\0'; return ptr; } /* Simple percent escape for colon based listings. Returns a * statically allocated buffer. */ static char * percent_escape (const char *str) { static char *buffer; free (buffer); buffer = do_percent_escape (str); return buffer; } /* Escape STRING at a max length of N for use in HTML. Returns a * statically allocated buffer. */ static const char * html_escape_n (const char *string, size_t length) { static char *buffer; char *p; /* The escaped string may be up to 6 times of STRING due to the expansion of '\"' to """. */ free (buffer); p = buffer = xmalloc (6 * length + 1); for (; *string && length; string++, length--) { switch (*string) { case '\"': p = my_stpcpy (p, """); break; case '&': p = my_stpcpy (p, "&"); break; case '<': p = my_stpcpy (p, "<"); break; case '>': p = my_stpcpy (p, ">"); break; default: *p++ = *string; break; } } *p = 0; return buffer; } /* Escape STRING for use in HTML. Returns a statically allocated * buffer. noet that this buffer is shared with the buffer returned * by html_escape_n. */ static const char * html_escape (const char *string) { return html_escape_n (string, strlen (string)); } /* Escape STRING but insert for one https link. */ static const char * html_escape_detect_link (const char *string) { const char *start, *s; char *part1, *url, *part2, *part3; size_t urllen; char *buffer, *p; start = strstr (string, "https://"); if (!start || !start[8] || start[8] == ' ' || start[8] == '\t') return html_escape (string); if (!(start == string || start[-1] == ' ' || start[-1] == '\t' || start[-1] == '<')) return html_escape (string); urllen = 0; for (s = start; *s && *s != ' ' && *s != '\t' && *s != '>'; s++) urllen++; part1 = xstrdup (html_escape_n (string, start-string)); url = http_escape_buffer (start, urllen, "\""); part2 = xstrdup (html_escape_n (start, urllen)); part3 = xstrdup (html_escape (start + urllen)); buffer = xmalloc (strlen (part1) + strlen (url) + strlen (part2) + strlen (part3) + 100); p = my_stpcpy (buffer, part1); p = my_stpcpy (p, ""); p = my_stpcpy (p, part2); p = my_stpcpy (p, ""); my_stpcpy (p, part3); free (part1); free (url); free (part2); free (part3); return buffer; } /* Escape STRING for use as a HREF attribute. Returns a statically * allocated buffer. */ static const char * html_escape_href (const char *string) { static char *buffer; free (buffer); buffer = http_escape_string (string, "\""); return buffer; } +/* Format T and return a statically allocated buffer. */ +static const char * +format_time_now (int human) +{ + static char buffer[40]; + struct tm *tp; + time_t now; + + time (&now); + tp = gmtime (&now); + if (!tp) + *buffer = 0; + + if (human) + snprintf (buffer, sizeof buffer, "%04d-%02d-%02d", + 1900 + tp->tm_year, tp->tm_mon+1, tp->tm_mday); + else + snprintf (buffer, sizeof buffer, "%04d%02d%02dT%02d%02d%02dZ", + 1900 + tp->tm_year, tp->tm_mon+1, tp->tm_mday, + tp->tm_hour, tp->tm_min, tp->tm_sec); + + return buffer; +} + + /* Format T and return a statically allocated buffer. */ static const char * format_time (time_t t) { static char buffer[80]; struct tm *tp; tp = gmtime (&t); if (!tp) *buffer = 0; else if (opt_html) snprintf (buffer, sizeof buffer, "" "%04d-%02d-%02d", 1900 + tp->tm_year, tp->tm_mon+1, tp->tm_mday, tp->tm_hour, tp->tm_min, tp->tm_sec, 1900 + tp->tm_year, tp->tm_mon+1, tp->tm_mday); else snprintf (buffer, sizeof buffer, "%04d%02d%02dT%02d%02d%02dZ", 1900 + tp->tm_year, tp->tm_mon+1, tp->tm_mday, tp->tm_hour, tp->tm_min, tp->tm_sec); return buffer; } /* Format SIZE and return a statically allocated buffer. */ static const char * format_size (unsigned long long size) { static char buffer[80]; const char *suffix; unsigned long long val = size; if (size < 1024) { val = size; suffix = ""; } else if (size < 1024 * 1024) { val = size / 1024; suffix = "k"; } else if (size < 1024 * 1024 * 1024) { val = size / (1024 * 1024); suffix = "M"; } else { val = size / (1024 * 1024 * 1024); suffix = "G"; } if (opt_html) snprintf (buffer, sizeof buffer, "%llu%s", size, size == 1? "":"s", val, suffix); else snprintf (buffer, sizeof buffer, "%llu%s", val, suffix); return buffer; } /* This function parses the first portion of the version number S and * stores it at NUMBER. On success, this function returns a pointer * into S starting with the first character, which is not part of the * initial number portion; on failure, NULL is returned. */ static const char* parse_version_number (const char *s, int *number) { int val = 0; if (*s == '0' && digitp (s[1])) return NULL; /* Leading zeros are not allowed. */ for (; digitp (*s); s++) { val *= 10; val += *s - '0'; } *number = val; return val < 0 ? NULL : s; } /* This function breaks up the complete string-representation of the * version number S, which is of the following struture: ... The major, * minor and micro number components will be stored in *MAJOR, *MINOR * and *MICRO. * * On success, the last component, the patch level, will be returned; * in failure, NULL will be returned. */ static const char * parse_version_string (const char *s, int *major, int *minor, int *micro) { s = parse_version_number (s, major); if (!s || *s != '.') return NULL; s++; s = parse_version_number (s, minor); if (!s || *s != '.') return NULL; s++; s = parse_version_number (s, micro); if (!s) return NULL; return s; /* patchlevel */ } /* Compare function for version strings. */ static int compare_version_strings (const char *a, const char *b) { int a_major, a_minor, a_micro; int b_major, b_minor, b_micro; const char *a_plvl, *b_plvl; a_plvl = parse_version_string (a, &a_major, &a_minor, &a_micro); if (!a_plvl) a_major = a_minor = a_micro = 0; b_plvl = parse_version_string (b, &b_major, &b_minor, &b_micro); if (!b_plvl) b_major = b_minor = b_micro = 0; if (!a_plvl && !b_plvl) return -1; /* Put invalid strings at the end. */ if (a_plvl && !b_plvl) return 1; if (!a_plvl && b_plvl) return -1; if (a_major > b_major) return 1; if (a_major < b_major) return -1; if (a_minor > b_minor) return 1; if (a_minor < b_minor) return -1; if (a_micro > b_micro) return 1; if (a_micro < b_micro) return -1; if (opt_reverse_ver && !opt_reverse) { /* We may only compare up to the next dot and the swicth back to * regular order. */ for (; *a_plvl && *b_plvl; a_plvl++, b_plvl++) { if (*a_plvl == '.' && *b_plvl == '.') return 0 - strcmp (a_plvl, b_plvl); else if (*a_plvl == '.') return 1; /* B is larger but we need to reverse. */ else if (*b_plvl == '.') return -1; /* A is larger but we need to reverse. */ else if (*a_plvl != *b_plvl) break; } if (*a_plvl == *b_plvl) return 0; else return (*(signed char *)b_plvl - *(signed char *)a_plvl); } else return strcmp (a_plvl, b_plvl); } /* If string looks like a file name with a version nuymber, return a * pointer to the version number part; else return NULL. */ static const char * find_version_string (const char *s) { do { s = strchr (s, '-'); if (!s) return NULL; /* Version string must be prefixed with a dash. */ s++; } while (!digitp (*s)); return s; } /* Sort function for the directory listing. */ static int sort_finfo (const void *arg_a, const void *arg_b) { const finfo_t *a = arg_a; const finfo_t *b = arg_b; const char *astr, *bstr; const char *aver, *bver; if (opt_reverse) { astr = (*b)->name; bstr = (*a)->name; } else { astr = (*a)->name; bstr = (*b)->name; } aver = find_version_string (astr); bver = aver? find_version_string (bstr) : NULL; if (aver && bver && (aver - astr) == (bver - bstr) && !memcmp (astr, bstr, (aver - astr))) { if (opt_reverse_ver) return 0 - compare_version_strings (aver, bver); else return compare_version_strings (aver, bver); } return strcmp(astr, bstr); } /* Note: This function assumes that the CWD is the listed directory. */ static void print_header (const char *title) { const char *esc_title; if (!opt_html) return; esc_title = html_escape (title); if (opt_gpgweb) { FILE *readme; char line[256]; char *p; int c; fputs ("\n" "\n" "\n", stdout); printf("\n" - "ftp.gnupg.org - %s\n", + "ftp.gnupg.org:%s\n", esc_title); fputs ("\n" + " content=\"text/html; charset=UTF-8\"/>\n", stdout); + printf("\n", format_time_now (0)); + fputs ("\n" "\n" + " content=\"width=device-width, initial-scale=1\"/>\n" "\n" + " type=\"text/css\"/>\n" "\n", stdout); fputs ("\n" "
\n" "
" " " "
\n", stdout); printf("
\n" "
\n" - "

%s

\n" + "

ftp.gnupg.org:%s

\n" "
\n", esc_title); readme = fopen ("README", "r"); if (opt_readme && (readme = fopen ("README", "r"))) { fputs ("
\n", stdout);
           while (fgets (line, sizeof line, readme))
             {
               int no_lf = 0;
               /* Eat up the rest of an incomplete line.  */
               if (!*line)
                 no_lf = 1;
               else if (line[strlen (line)-1] != '\n')
                 {
                   no_lf = 1;
                   while ((c = getc (readme)) != EOF && c != '\n')
                     ;
                 }
 
               /* Replace empty lines with a leading doc by an empty
                * line.  These lines are used on FTP servers to avoid
                * problems with broken FTP cleints.  */
               if (*line == '.')
                 {
                   for (p=line+1; (*p == ' ' || *p == '\t' || *p == '\n'); p++)
                     ;
                   if (!*p)
                     {
                       putchar ('\n');
                       *line = 0;
                     }
                 }
 
               if (*line)
                 fputs (html_escape_detect_link (line), stdout);
               if (no_lf)
                 putchar ('\n');
             }
           fputs ("
\n", stdout); fclose (readme); } fputs ("
\n", stdout); } else { printf ("\n" "\n" "Index of %s\n" "\n" "\n" "

Index of %s

\n" - "\n", + "
\n", esc_title, esc_title); } } static void print_footer (void) { if (!opt_html) return; if (opt_gpgweb) { fputs ("\n" "\n" "
\n", stdout); fputs ("
\n" "\n" "
\n", stdout); fputs ("
\n" "\"Traueranzeige:\n" "

\n" "
\n", stdout); fputs ("
\n" "" "\"CC " "This web page is Copyright 2017 GnuPG e.V. and" " licensed under a " "Creative Commons Attribution-ShareAlike 4.0 International" " License. See " "copying for details.\n", stdout); - printf("Page last updated on 2017-xx-xx.\n"); + printf("Page last updated on %s.\n", format_time_now (1)); fputs ("
\n" "
\n" "\n" "\n" "\n", stdout); } else { printf ("
\n" "\n" "\n"); } } /* Print COUNT directories from the array SORTED. */ static void -print_dirs (finfo_t *sorted, int count, const char *directory) +print_dirs (finfo_t *sorted, int count, int at_root) { int idx; finfo_t fi; int any = 0; for (idx=0; idx < count; idx++) { fi = sorted[idx]; if (!fi->is_dir) continue; if (!any && opt_html) { any = 1; if (opt_gpgweb) { fputs ("

Directories

\n" "
\n" - "\n", stdout); + "
\n", stdout); - if (strcmp (directory, "/")) + if (!at_root) fputs ("" "\n", stdout); } else { fputs ("" "\n", stdout); - if (strcmp (directory, "/")) + if (!at_root) fputs ("\n", stdout); } } if (opt_gpgweb) printf ("" "\n", html_escape_href (fi->name), html_escape (fi->name)); else if (opt_html) printf ("\n", html_escape_href (fi->name), html_escape (fi->name)); else printf ("D %s\n", fi->name); } if (any && opt_gpgweb) { fputs ("
" "Parent Directory
 

Directories

" "Parent Directory
%s
%s
\n" "
\n\n", stdout); + } + else if (opt_gpgweb && !at_root) + { + /* !any - need to print an UP link */ + fputs ("
\n" + "\n" + "" + "\n" + "
" + "Parent Directory
\n" + "
\n", stdout); + + } } /* Print COUNT files from the array SORTED. */ static void print_files (finfo_t *sorted, int count) { int idx; finfo_t fi; int any = 0; for (idx=0; idx < count; idx++) { fi = sorted[idx]; if (!fi->is_reg) continue; if (!any && opt_html) { any = 1; if (opt_gpgweb) { fputs ("

Files

\n" "
\n" - "\n", stdout); + "
\n", stdout); } else fputs ("\n", stdout); } if (opt_gpgweb) printf ("" "" "\n", strstr (fi->name, ".sig")? "document": strstr (fi->name, ".tar")? "tar" : "document", html_escape_href (fi->name), html_escape (fi->name), format_time (fi->mtime), format_size (fi->size)); else if (opt_html) printf ("" "\n", html_escape_href (fi->name), html_escape (fi->name), format_time (fi->mtime), format_size (fi->size)); else printf ("F %s\n", fi->name); } if (any && opt_gpgweb) { fputs ("

Files

%s%s%s
%s%s%s
\n" "
\n\n", stdout); } } /* Scan DIRECTORY and print an index. * FIXME: This does a chdir and does not preserve the old PWD. * The fix is to build the full filename beofre stat'ing. */ static void scan_directory (const char *directory, const char *title) { DIR *dir; struct dirent *dentry; finfo_t fi; finfo_t finfo = NULL; finfo_t *sorted; int count = 0; int idx; size_t len; strlist_t sl; + int at_root = 0; + + if (opt_gpgweb) + { + if (!strcmp (title, "/")) + at_root = 1; + } + else if (!strcmp (directory, "/")) + at_root = 1; dir = opendir (directory); if (!dir) { err ("can't open directory '%s': %s\n", directory, strerror (errno)); return; } while (errno=0,(dentry = readdir (dir))) { if (*dentry->d_name == '.') continue; /* Skip self, parent, and hidden directories. */ len = strlen (dentry->d_name); if (!len) continue; /* Empty filenames should actually not exist. */ if (dentry->d_name[len-1] == '~') continue; /* Skip backup files. */ for (sl = opt_exclude; sl; sl = sl->next) if (!strcmp (sl->d, dentry->d_name)) break; if (sl) continue; /* Skip excluded names. */ fi = xcalloc (1, sizeof *fi + strlen (dentry->d_name)); strcpy (fi->name, dentry->d_name); fi->next = finfo; finfo = fi; count++; } if (errno) die ("error reading directory '%s': %s\n", directory, strerror (errno)); closedir (dir); sorted = xcalloc (count, sizeof *sorted); for (fi=finfo, idx=0; fi; fi = fi->next) sorted[idx++] = fi; inf ("directory '%s' has %d files\n", directory, count); qsort (sorted, count, sizeof *sorted, sort_finfo); if (chdir (directory)) die ("cannot chdir to '%s': %s\n", directory, strerror (errno)); for (idx=0; idx < count; idx++) { struct stat sb; fi = sorted[idx]; if (stat (fi->name, &sb)) { err ("cannot stat '%s': %s\n", fi->name, strerror (errno)); continue; } fi->is_dir = !!S_ISDIR(sb.st_mode); fi->is_reg = !!S_ISREG(sb.st_mode); fi->size = fi->is_reg? sb.st_size : 0; fi->mtime = sb.st_mtime; } print_header (title); if (opt_files_first) { print_files (sorted, count); - print_dirs (sorted, count, directory); + print_dirs (sorted, count, at_root); } else { - print_dirs (sorted, count, directory); + print_dirs (sorted, count, at_root); print_files (sorted, count); } print_footer (); /* We create the index file in the current directory. */ if (opt_index) { - FILE *indexfp = fopen (".ftp-index", "w"); + FILE *indexfp = fopen (opt_index, "w"); if (!indexfp) - die ("error creating .ftp-index in '%s': %s\n", - directory, strerror (errno)); + die ("error creating '%s' for '%s': %s\n", + opt_index, directory, strerror (errno)); for (idx=0; idx < count; idx++) { fi = sorted[idx]; fprintf (indexfp, "%s:%c:%llu:%lu:\n", percent_escape (fi->name), fi->is_dir? 'd': fi->is_reg? 'r': '?', fi->size, (unsigned long)fi->mtime); } if (ferror (indexfp)) - die ("error writing .ftp-index in '%s': %s\n", - directory, strerror (errno)); + die ("error writing '%s' for '%s': %s\n", + opt_index, directory, strerror (errno)); /* Fixme: Check for close errors. */ fclose (indexfp); } free (sorted); while ((fi = finfo)) { fi = finfo->next; free (finfo); finfo = fi; } } int main (int argc, char **argv) { int last_argc = -1; strlist_t sl; if (argc < 1) die ("Hey, read up on how to use exec(2)\n"); argv++; argc--; while (argc && last_argc != argc ) { last_argc = argc; if (!strcmp (*argv, "--")) { argc--; argv++; break; } + else if (!strcmp (*argv, "--version")) + { + fputs (PGMNAME " " VERSION "\n" + "Copyright (C) 2017 g10 Code GmbH\n" + "License GPLv3+: GNU GPL version 3 or later" + " \n" + "This is free software: you are free to change" + " and redistribute it.\n" + "There is NO WARRANTY, to the extent permitted by law.\n", + stdout); + exit (0); + } else if (!strcmp (*argv, "--help")) { fputs ("usage: " PGMNAME " [options] directory [title]\n" "Print an index for an FTP directory.\n\n" "Options:\n" + " --version print program version\n" " --verbose verbose diagnostics\n" " --debug flyswatter\n" " --reverse reverse sort order\n" " --reverse-ver reverse only the version number order\n" " --files-first print files before directories\n" " --html output HTML\n" " --gpgweb output HTML as used at gnupg.org\n" " --readme include README file\n" - " --index create an .ftp-index file\n" + " --index FILE create index FILE\n" " --exclude NAME ignore file NAME\n" , stdout); exit (0); } else if (!strcmp (*argv, "--verbose")) { opt_verbose++; argc--; argv++; } else if (!strcmp (*argv, "--debug")) { opt_debug++; argc--; argv++; } else if (!strcmp (*argv, "--reverse")) { opt_reverse = 1; argc--; argv++; } else if (!strcmp (*argv, "--reverse-ver")) { opt_reverse_ver = 1; argc--; argv++; } else if (!strcmp (*argv, "--files-first")) { opt_files_first = 1; argc--; argv++; } else if (!strcmp (*argv, "--readme")) { opt_readme = 1; argc--; argv++; } else if (!strcmp (*argv, "--html")) { opt_html = 1; argc--; argv++; } else if (!strcmp (*argv, "--index")) { - opt_index = 1; + argc--; argv++; + if (!argc || !**argv) + die ("argument missing for option '%s'\n", argv[-1]); + opt_index = *argv; argc--; argv++; } else if (!strcmp (*argv, "--gpgweb")) { opt_gpgweb = opt_html = 1; argc--; argv++; } else if (!strcmp (*argv, "--exclude")) { argc--; argv++; if (!argc || !**argv) die ("argument missing for option '%s'\n", argv[-1]); sl = xmalloc (sizeof *sl + strlen (*argv)); strcpy (sl->d, *argv); sl->next = opt_exclude; opt_exclude = sl; argc--; argv++; } else if (!strncmp (*argv, "--", 2)) die ("unknown option '%s' (use --help)\n", *argv); } if (argc < 1 || argc > 2) die ("usage: " PGMNAME " [options] directory [title]\n"); scan_directory (argv[0], argv[1]? argv[1]:argv[0]); return 0; } /* Local Variables: compile-command: "cc -Wall -g -o ftp-indexer ftp-indexer.c" End: */ diff --git a/web/share/gpgweb.el b/web/share/gpgweb.el index 9a719f2..6539206 100644 --- a/web/share/gpgweb.el +++ b/web/share/gpgweb.el @@ -1,524 +1,527 @@ ;;; gpgweb.el --- elisp helper code for the GnuPG web pages (if (< (string-to-number emacs-version) 24) (require 'org-exp)) ;; makeindex disabled because the generated file is created in the ;; source directory. (defun gpgweb-setup-project () "Set up an org-publish project for the gnupg.org website." (progn (require 'ox-gpgweb (concat gpgweb-root-dir "share/ox-gpgweb.el")) (aput 'org-publish-project-alist "gpgweb-org" `(:base-directory ,gpgweb-root-dir :base-extension "org" :language "en" :html-extension "html" :recursive t :publishing-directory ,gpgweb-stage-dir :publishing-function gpgweb-org-to-html :body-only t :section-numbers nil :tags nil :with-toc nil :makeindex nil :auto-sitemap nil :sitemap-title "GnuPG - Sitemap" :sitemap-sort-folders "last" :sitemap-file-entry-format "%t @@html:@@(%d)@@html:@@" :style-include-default nil :timestamp-file nil :html-head "" :html-head-include-scripts nil)) (aput 'org-publish-project-alist "gpgweb-other" `(:base-directory ,gpgweb-root-dir :base-extension "jpg\\|png\\|css\\|txt\\|rss\\|lst\\|sig\\|js\\|map\\|eot\\|ttf\\|woff\\|woff2\\|svg" :recursive t :publishing-directory ,gpgweb-stage-dir :publishing-function org-publish-attachment :completion-function gpgweb-upload)) (aput 'org-publish-project-alist "gpgweb" '(:components ("gpgweb-org" "gpgweb-other"))) (add-hook 'org-export-before-processing-hook 'gpgweb-preprocess))) (defun gpgweb-preprocess (backend) "Insert certain stuff before processing." (let () (goto-char (point-min)) (when (re-search-forward "^#\\+GPGWEB-NEED-SWDB\\b" 2048 t) (beginning-of-line) (kill-line 1) (insert (org-file-contents (concat gpgweb-root-dir "swdb.mac") 'noerror))))) (defun gpgweb-insert-header (title committed-at custom) "Insert the header. COMMITTED-AT is the commit date string of the source file or nil if not available. If CUSTOM is true only a minimal header is set." (goto-char (point-min)) (insert " " title " ") (when (and committed-at (>= (length committed-at) 10)) (insert "\n")) (insert " ") (unless custom (insert " "))) (defconst gpgweb-gnupg-menu-alist '(("/index.html" "Home" (("/index.html" "Home") ("/news.html" "News") ("/people/index.html" "People") ("/verein/index.html" "Verein") ("/documentation/sites.html" "Sites"))) ("/donate/index.html" "Donate" (("/donate/index.html" "Donate") ("/donate/kudos.html" "List of Donors"))) ("/software/index.html" "Software" (("/software/index.html" "GnuPG") ("/software/frontends.html" "Frontends") ("/software/tools.html" "Tools") ("/software/libraries.html" "Libraries") ("/software/swlist.html" "All"))) ("/download/index.html" "Download" (("/download/index.html" "Download") ("/download/integrity_check.html" "Integrity Check") ("/download/supported_systems.html" "Supported Systems") ("/download/release_notes.html" "Release Notes") ("/download/mirrors.html" "Mirrors") ("/download/git.html" "GIT"))) ("/documentation/index.html" "Documentation" (("/documentation/howtos.html" "HOWTOs") ("/documentation/manuals.html" "Manuals") ("/documentation/guides.html" "Guides") ("/documentation/faqs.html" "FAQs") ("/documentation/mailing-lists.html" "Mailing Lists") ("/service.html" "3rd Party Support") ("/documentation/bts.html" "Bug Tracker") ("/documentation/security.html" "Security"))) ("/blog/index.html" "Blog")) "The definition of the gnupg.org menu structure.") (defconst gpgweb-gnupg-bottom-menu-alist '(("/privacy-policy.html" "Privacy Policy" ()) ("/imprint.html" "Imprint" ()) ("/misc/index.html" "Archive" ()) ("/sitemap.html" "Sitemap" ()) ("/blog/index.html" "Blog" + ()) + ("/ftp/index.html" + "Files" ())) "The definition of the gnupg.org bottom menu structure.") (defun gpgweb--any-selected-menu-p (menu selected-file) "Return t if any item in MENU has been selected." (let ((item (car menu)) res) (when menu (when item (when (string= (car item) selected-file) (setq res t)) (when (caddr item) (when (gpgweb--any-selected-menu-p (caddr item) selected-file) (setq res t)))) (when (gpgweb--any-selected-menu-p (cdr menu) selected-file) (setq res t))) res)) (defun gpgweb--selected-top-menu (menu selected-file) "Return the selected top menu or nil." (when menu (let ((item (car menu))) (if (and item (or (string= (car item) selected-file) (gpgweb--any-selected-menu-p (caddr item) selected-file))) menu (gpgweb--selected-top-menu (cdr menu) selected-file))))) (defun gpgweb--insert-menu (menu lvl selected-file) "Helper function to insert the menu." (when menu (let ((item (car menu))) (when item (dotimes (i (1+ lvl)) (insert " ")) (insert "
  • " (cadr item) "\n") (when (caddr item) (dotimes (i (1+ lvl)) (insert " ")) (insert "
      \n") (gpgweb--insert-menu (caddr item) (1+ lvl) selected-file) (dotimes (i (1+ lvl)) (insert " ")) (insert "
    \n")) (dotimes (i (1+ lvl)) (insert " ")) (insert "
  • \n"))) (gpgweb--insert-menu (cdr menu) lvl selected-file))) (defun gpgweb--insert-submenu (menu selected-file) "Helper function to insert the sub-menu." (when menu (let ((item (car menu))) (when item (insert "
  • " (cadr item) "
  • \n"))) (gpgweb--insert-submenu (cdr menu) selected-file))) (defun gpgweb-insert-menu (selected-file) "Insert the menu structure into the HTML file." (goto-char (point-min)) (when (re-search-forward "^\n" nil t) (insert "
     
    ") (let ((m (caddr (car (gpgweb--selected-top-menu gpgweb-gnupg-menu-alist selected-file))))) (when m (insert "\n"))) (insert "
    "))) (defun gpgweb-blog-index (orgfile filelist) "Return the index of ORGFILE in FILELIST or nil if not found." (let (found (i 0)) (while (and filelist (not found)) (if (string= orgfile (car filelist)) (setq found i)) (setq i (1+ i)) (setq filelist (cdr filelist))) found)) (defun gpgweb-blog-prev (fileidx filelist) "Return the chronological previous file at FILEIDX from FILELIST with the suffixed replaced by \"html\"." (if (> fileidx 1) (concat (file-name-sans-extension (nth (1- fileidx) filelist)) ".html"))) (defun gpgweb-blog-next (orgfile filelist) "Return the chronological next file at FILEIDX from FILELIST with the suffixed replaced by \"html\"." (if (< fileidx (1- (length filelist))) (concat (file-name-sans-extension (nth (1+ fileidx) filelist)) ".html"))) (defun gpgweb-fixup-blog (info orgfile filelist) "Insert the blog specific content. INFO is the usual plist. ORGFILE is the name of the current source file without the directory part. If FILELIST is a list it has an ordered list of org filenames." (let ((authorstr (car (plist-get info :author))) (datestr (car (plist-get info :date)))) (goto-char (point-min)) (if (re-search-forward "^
    " nil t) (let* ((indexp (string= orgfile "index.org")) (fileidx (if (listp filelist) (if indexp (1- (length filelist)) (gpgweb-blog-index orgfile filelist)))) (prevfile (if fileidx (gpgweb-blog-prev fileidx filelist))) (nextfile (if (and fileidx (not indexp)) (gpgweb-blog-next fileidx filelist)))) (move-beginning-of-line nil) (insert "\n"))) (if (and datestr authorstr) (if (re-search-forward "^

    Posted " datestr " by " authorstr "

    \n"))))) (defun gpgweb-insert-footer (htmlfile committed-at blogmode) "Insert the footer. HTMLFILE is HTML file name and COMMITTED-AT is the commit date string of the source file or nil if not available." (let ((srcfile (concat "https://git.gnupg.org/cgi-bin/gitweb.cgi?" "p=gnupg-doc.git;a=blob;f=" (if blogmode "misc/blog.gnupg.org" "web/") ;; The replace below is a hack to cope with ;; blogmode where HTMLFILE is like "./foo.html". (replace-regexp-in-string "^\\./" "/" (file-name-sans-extension htmlfile) t) ".org")) (changed (if (and committed-at (>= (length committed-at) 10)) (substring committed-at 0 10) "[unknown]"))) (goto-char (point-max)) (insert "

      ") (gpgweb--insert-menu gpgweb-gnupg-bottom-menu-alist 0 nil) (insert "
    ") (insert "
    \"Traueranzeige:

    ") (goto-char (point-min)) (unless (search-forward "" nil t) (goto-char (point-max)) (if (string-prefix-p "verein/" htmlfile) (insert "
    \"CC  This web page is Copyright 2017 GnuPG e.V. and licensed under a Creative Commons Attribution-ShareAlike 4.0 International License. See copying for details. Page source last changed on " changed ".
    \n") (insert "
    \"CC  These web pages are Copyright 1998--2017 The GnuPG Project and licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License. See copying for details. Page source last changed on " changed ".
    \n"))) (goto-char (point-max)) (insert "
    "))) (defun gpgweb-publish-find-title (file &optional reset) "Find the title of FILE in project. This is a copy of org-publish-find-title which switches the buffer into read-write mode so that it works with read-only files." (or (and (not reset) (org-publish-cache-get-file-property file :title nil t)) (let* ((org-inhibit-startup t) (visiting (find-buffer-visiting file)) (buffer (or visiting (find-file-noselect file)))) (with-current-buffer buffer (toggle-read-only 0) (let ((title (let ((property (plist-get ;; protect local variables in open buffers (if visiting (org-export-with-buffer-copy (org-export-get-environment)) (org-export-get-environment)) :title))) (if property (org-no-properties (org-element-interpret-data property)) (file-name-nondirectory (file-name-sans-extension file)))))) (unless visiting (kill-buffer buffer)) (org-publish-cache-set-file-property file :title title) title))))) (defun gpgweb-want-custom-page-p () "Return true if the current buffer indicated that it wants to be a custom page." (let ((savepoint (point)) (result)) (goto-char (point-min)) (setq result (not (not (search-forward "" nil t)))) (goto-char savepoint) result)) (defun gpgweb-postprocess-html (plist orgfile htmlfile blogmode) "Post-process the generated HTML file - Insert header and footer - Insert \"class=selected\" into the active menu entry - Fixup sitemap. If blogmode is not nil the output is rendered as a blog. BLOGMODE may then contain an ordered list of org file names which are used to create the previous and Next links for an entry." (let* ((visitingp (find-buffer-visiting htmlfile)) (work-buffer (or visitingp (find-file-noselect htmlfile))) (committed-at (shell-command-to-string (concat "git" (if blogmode (concat " -C " gpgweb-blog-dir)) " log -1 --format='%ci' -- " orgfile)))) (prog1 (with-current-buffer work-buffer (let ((fname (file-name-nondirectory htmlfile)) (fname-2 (replace-regexp-in-string ".*/gnupg-doc-stage/web/\\(.*\\)$" "\\1" htmlfile t)) (title (gpgweb-publish-find-title orgfile)) (custom (gpgweb-want-custom-page-p))) ;; Insert header, menu, and footer. (gpgweb-insert-header title committed-at custom) (unless custom (goto-char (point-min)) (unless (search-forward "" nil t) (gpgweb-insert-menu fname-2)) (if blogmode (gpgweb-fixup-blog plist (file-name-nondirectory orgfile) blogmode)) (gpgweb-insert-footer fname-2 committed-at blogmode)) ; Fixup the sitemap (when (string-equal fname "sitemap.html") (goto-char (point-min)) (while (re-search-forward "^.*
  • .*>\\(GnuPG - \\).* and ; attributes. (goto-char (point-min)) (when (search-forward "" nil t) (goto-char (point-min)) (while (re-search-forward "^