]> git.ipfire.org Git - thirdparty/squid.git/commitdiff
Multi-Lingual FTP directory listing
authorAmos Jeffries <squid3@treenet.co.nz>
Sat, 31 Oct 2009 11:53:09 +0000 (00:53 +1300)
committerAmos Jeffries <squid3@treenet.co.nz>
Sat, 31 Oct 2009 11:53:09 +0000 (00:53 +1300)
This replaces hard-coded FTP directory page with dynamically loaded
'error page' same as all other FTP result HTML outputs.

This has the added benefit of making the page HTML standards compliant,
CSS branded, and presentable in any of the auto-negotiated languages.

doc/release-notes/release-3.2.sgml
errors/errorpage.css
errors/templates/ERR_DIR_LISTING
src/MemBuf.h
src/cache_cf.cc
src/cf.data.pre
src/err_type.h
src/errorpage.cc
src/errorpage.h
src/ftp.cc

index e2ca6a7b69a1aaf0947763c35f4d6c45b347b1ed..36c6affe96a21035879b18fad9cf55c2c7526ec7 100644 (file)
@@ -335,12 +335,8 @@ This section gives a thorough account of those changes in three categories:
 <sect1>Removed tags<label id="removedtags">
 <p>
 <descrip>
-
-       <tag>???alphabetical list???</tag>
-       <p> ???
-       <verb>
-       relevant quote directly from cf.data.pre
-       </verb>
+       <tag>ftp_list_width</tag>
+       <p>Obsolete.
 
 </descrip>
 
index 8cfa802c8c649cb1f4b6d3e8db3f0f7b7e79a47a..8ecc4c8279bc25fd81ed7e05d8ecb24ea6e95483 100644 (file)
@@ -67,6 +67,15 @@ pre {
 }
 
 /* special event: FTP / Gopher directory listing */
+#dirmsg {
+    font-family: courier;
+    color: black;
+    font-size: 10pt;
+}
+#dirlisting {
+    margin-left: 2%;
+    margin-right: 2%;
+}
 #dirlisting tr.entry td.icon,td.filename,td.size,td.date {
     border-bottom: groove;
 }
index f20313d52f846e9f9b79ec61e833a74c158ba542..c1fa488f9d20b873fefc847472f6a2e729ef98c7 100644 (file)
@@ -2,7 +2,7 @@
 <html><head>
 <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
 <title>Directory: %U</title>
-<style type="text/css"><!-- 
+<style type="text/css"><!--
  %l
 
 body
@@ -16,10 +16,10 @@ body
 <hr>
 
 <div id="content">
-<p>Directory Content:</p>
+<h4>Directory Content:</h4>
 
 <blockquote id="data">
-<pre>%z</pre>
+<pre id="dirmsg">%z</pre>
 </blockquote>
 
 <table id="dirlisting" summary="Directory Listing">
index a4805202570fb6871b6cde294eece8460550fbd3..bd8c41ac68eeb3dd5abf53add75469aae0d39a8e 100644 (file)
@@ -72,8 +72,8 @@ public:
 
     /**
      * Whether the buffer contains any data space available.
-     \retval true      if data can be added to teh buffer
-     \retval false     if teh buffer is full
+     \retval true      if data can be added to the buffer
+     \retval false     if the buffer is full
      */
     bool hasSpace() const { return size+1 < capacity; }
 
index ad9b8040c646a2f24fef064c5b005d270b18f819..daec10505c52f70e9222934c61dea377b599a33b 100644 (file)
@@ -2532,11 +2532,13 @@ free_time_t(time_t * var)
     *var = 0;
 }
 
+#if UNUSED_CODE
 static void
 dump_size_t(StoreEntry * entry, const char *name, size_t var)
 {
     storeAppendPrintf(entry, "%s %d\n", name, (int) var);
 }
+#endif
 
 static void
 dump_b_size_t(StoreEntry * entry, const char *name, size_t var)
@@ -2564,6 +2566,7 @@ dump_kb_int64_t(StoreEntry * entry, const char *name, int64_t var)
     storeAppendPrintf(entry, "%s %"PRId64" %s\n", name, var, B_KBYTES_STR);
 }
 
+#if UNUSED_CODE
 static void
 parse_size_t(size_t * var)
 {
@@ -2571,6 +2574,7 @@ parse_size_t(size_t * var)
     i = GetInteger();
     *var = (size_t) i;
 }
+#endif
 
 static void
 parse_b_size_t(size_t * var)
index 8e1a548019b0a74258bfe0a407ebb5c6d77374d4..4efce5b758e43528beed476048360623b05547b3 100644 (file)
@@ -2964,16 +2964,6 @@ DOC_START
        (for example perl.com).
 DOC_END
 
-NAME: ftp_list_width
-TYPE: size_t
-DEFAULT: 32
-LOC: Config.Ftp.list_width
-DOC_START
-       Sets the width of ftp listings. This should be set to fit in
-       the width of a standard browser. Setting this too small
-       can cut off long filenames when browsing ftp sites.
-DOC_END
-
 NAME: ftp_passive
 TYPE: onoff
 DEFAULT: on
index 5e6f922eed0eca0a8364dd21e2cc24566b2ad75c..88e2ab0e107ce0703441cedc8cd51a4e4d787074 100644 (file)
@@ -52,6 +52,7 @@ typedef enum {
     ERR_ICAP_FAILURE,
 
     /* Special Cases */
+    ERR_DIR_LISTING,            /* Display of remote directory (FTP, Gopher) */
     ERR_SQUID_SIGNATURE,        /* not really an error */
     ERR_SHUTTING_DOWN,
     TCP_RESET,
index 18bc3cba09ee73bc1f6e4703e40cc4c53f21b3b6..3a525643352398b86abc4edb5ba24f354f4f7ef6 100644 (file)
@@ -657,7 +657,12 @@ ErrorState::Convert(char token, bool url_presentable)
     case 'g':
         if (url_presentable) break;
         /* FTP SERVER MESSAGE */
-        wordlistCat(ftp.server_msg, &mb);
+        if(ftp.server_msg)
+            wordlistCat(ftp.server_msg, &mb);
+        else if(ftp.listing) {
+            mb.append(ftp.listing->content(), ftp.listing->contentSize());
+            do_quote = 0;
+        }
         break;
 
     case 'h':
@@ -816,6 +821,8 @@ ErrorState::Convert(char token, bool url_presentable)
         if (url_presentable) break;
         if (dnsError.size() > 0)
             p = dnsError.termedBuf();
+        else if (ftp.cwd_msg)
+            p = ftp.cwd_msg;
         else
             p = "[unknown]";
         break;
index 04b20e588951fd3a7f802d78eac9e21d1c32e867..9bd3f85ce67e8531a211746672e5e24bd57d315e 100644 (file)
@@ -148,6 +148,8 @@ public:
         wordlist *server_msg;
         char *request;
         char *reply;
+        char *cwd_msg;
+        MemBuf *listing;
     } ftp;
 
     char *request_hdrs;
index 8541d7ab4cb51b14d73fec09d59d75ec290c7df4..8ed280d56ef893b23d57bd3b8730de8919d7ec7a 100644 (file)
@@ -53,6 +53,7 @@
 #include "wordlist.h"
 #include "SquidTime.h"
 #include "URLScheme.h"
+#include "SquidString.h"
 
 /**
  \defgroup ServerProtocolFTPInternal Server-Side FTP Internals
@@ -114,13 +115,12 @@ struct _ftp_flags {
     bool dir_slash;
     bool root_dir;
     bool no_dotdot;
-    bool html_header_sent;
     bool binary;
     bool try_slash_hack;
     bool put;
     bool put_mkdir;
     bool listformat_unknown;
-    bool listing_started;
+    bool listing;
     bool completed_forwarding;
 };
 
@@ -178,11 +178,12 @@ public:
     int64_t restart_offset;
     char *proxy_host;
     size_t list_width;
-    wordlist *cwd_message;
+    String cwd_message;
     char *old_request;
     char *old_reply;
     char *old_filepath;
     char typecode;
+    MemBuf listing;            ///< FTP directory listing in HTML format.
 
     // \todo: optimize ctrl and data structs member order, to minimize size
     /// FTP control channel info; the channel is opened once per transaction
@@ -219,13 +220,12 @@ public:
     void failed(err_type, int xerrno);
     void failedErrorMessage(err_type, int xerrno);
     void unhack();
-    void listingStart();
-    void listingFinish();
     void scheduleReadControlReply(int);
     void handleControlReply();
     void readStor();
-    char *htmlifyListEntry(const char *line);
     void parseListing();
+    MemBuf *htmlifyListEntry(const char *line);
+    void completedListing(void);
     void dataComplete();
     void dataRead(const CommIoCbParams &io);
     int checkAuth(const HttpHeader * req_hdr);
@@ -501,8 +501,7 @@ FtpStateData::~FtpStateData()
     if (ctrl.message)
         wordlistDestroy(&ctrl.message);
 
-    if (cwd_message)
-        wordlistDestroy(&cwd_message);
+    cwd_message.clean();
 
     safe_free(ctrl.last_reply);
 
@@ -600,115 +599,21 @@ FtpStateData::ftpTimeout(const CommTimeoutCbParams &io)
     /* failed() closes ctrl.fd and frees ftpState */
 }
 
-void
-FtpStateData::listingStart()
-{
-    debugs(9,3, HERE);
-    wordlist *w;
-    char *dirup;
-    int i, j, k;
-    const char *title = title_url.termedBuf();
-    flags.listing_started = true;
-    printfReplyBody("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\" \"http://www.w3.org/TR/html4/loose.dtd\">\n");
-    printfReplyBody("<!-- HTML listing generated by Squid %s -->\n",
-                    version_string);
-    printfReplyBody("<!-- %s -->\n", mkrfc1123(squid_curtime));
-    printfReplyBody("<HTML><HEAD><TITLE>\n");
-    {
-        char *t = xstrdup(title);
-        rfc1738_unescape(t);
-        printfReplyBody("FTP Directory: %s\n", html_quote(t));
-        xfree(t);
-    }
-
-    printfReplyBody("</TITLE>\n");
-    printfReplyBody("<STYLE type=\"text/css\"><!--BODY{background-color:#ffffff;font-family:verdana,sans-serif}--></STYLE>\n");
-
-    if (flags.need_base_href)
-        printfReplyBody("<BASE HREF=\"%s\">\n",
-                        html_quote(base_href.termedBuf()));
-
-    printfReplyBody("</HEAD><BODY>\n");
-
-    if (cwd_message) {
-        printfReplyBody("<PRE>\n");
-
-        for (w = cwd_message; w; w = w->next)
-            printfReplyBody("%s\n", html_quote(w->key));
-
-        printfReplyBody("</PRE>\n");
-
-        printfReplyBody("<HR noshade size=\"1px\">\n");
-
-        wordlistDestroy(&cwd_message);
-    }
-
-    printfReplyBody("<H2>\n");
-    printfReplyBody("FTP Directory: ");
-    /* "ftp://" == 6 characters */
-    assert(title_url.size() >= 6);
-    k = 6 + strcspn(&title[6], "/");
-
-    for (i = 6, j = 0; title[i]; j = i) {
-        printfReplyBody("<A HREF=\"");
-        i += strcspn(&title[i], "/");
-
-        if (i > j) {
-            char *url = xstrdup(title);
-            url[i] = '\0';
-            printfReplyBody("%s", html_quote(url + k));
-            printfReplyBody("/");
-            printfReplyBody("\">");
-            rfc1738_unescape(url + j);
-            printfReplyBody("%s", html_quote(url + j));
-            safe_free(url);
-            printfReplyBody("</A>");
-        }
-
-        printfReplyBody("/");
-
-        if (title[i] == '/')
-            i++;
-
-        if (i == j) {
-            /* Error guard, or "assert" */
-            printfReplyBody("ERROR: Failed to parse URL: %s\n",
-                            html_quote(title));
-            debugs(9, DBG_CRITICAL, "Failed to parse URL: " << title);
-            break;
-        }
-    }
-
-    printfReplyBody("</H2>\n");
-    printfReplyBody("<PRE>\n");
-    dirup = htmlifyListEntry("<internal-dirup>");
-    writeReplyBody(dirup, strlen(dirup));
-    flags.html_header_sent = 1;
-}
-
+#if DEAD_CODE // obsoleted by ERR_DIR_LISTING
 void
 FtpStateData::listingFinish()
 {
-    debugs(9,3,HERE);
-    entry->buffer();
-    printfReplyBody("</PRE>\n");
+    // TODO: figure out what this means and how to show it ...
 
     if (flags.listformat_unknown && !flags.tried_nlst) {
-        printfReplyBody("<A HREF=\"%s/;type=d\">[As plain directory]</A>\n",
+        printfReplyBody("<a href=\"%s/;type=d\">[As plain directory]</a>\n",
                         flags.dir_slash ? rfc1738_escape_part(old_filepath) : ".");
     } else if (typecode == 'D') {
         const char *path = flags.dir_slash ? filepath : ".";
-        printfReplyBody("<A HREF=\"%s/\">[As extended directory]</A>\n", rfc1738_escape_part(path));
+        printfReplyBody("<a href=\"%s/\">[As extended directory]</a>\n", rfc1738_escape_part(path));
     }
-
-    printfReplyBody("<HR noshade size=\"1px\">\n");
-    printfReplyBody("<ADDRESS>\n");
-    printfReplyBody("Generated %s by %s (%s)\n",
-                    mkrfc1123(squid_curtime),
-                    getMyHostname(),
-                    visible_appname_string);
-    printfReplyBody("</ADDRESS></BODY></HTML>\n");
 }
+#endif /* DEAD_CODE */
 
 /// \ingroup ServerProtocolFTPInternal
 static const char *Month[] = {
@@ -972,47 +877,28 @@ found:
     return p;
 }
 
-/// \ingroup ServerProtocolFTPInternal
-static const char *
-dots_fill(size_t len)
-{
-    static char buf[256];
-    size_t i = 0;
-
-    if (len > Config.Ftp.list_width) {
-        memset(buf, ' ', 256);
-        buf[0] = '\n';
-        buf[Config.Ftp.list_width + 4] = '\0';
-        return buf;
-    }
-
-    for (i = len; i < Config.Ftp.list_width; i++)
-        buf[i - len] = (i % 2) ? '.' : ' ';
-
-    buf[i - len] = '\0';
-
-    return buf;
-}
-
-char *
+MemBuf *
 FtpStateData::htmlifyListEntry(const char *line)
 {
-    LOCAL_ARRAY(char, icon, 2048);
-    LOCAL_ARRAY(char, href, 2048 + 40);
-    LOCAL_ARRAY(char, text, 2048);
-    LOCAL_ARRAY(char, size, 2048);
-    LOCAL_ARRAY(char, chdir, 2048 + 40);
-    LOCAL_ARRAY(char, view, 2048 + 40);
-    LOCAL_ARRAY(char, download, 2048 + 40);
-    LOCAL_ARRAY(char, link, 2048 + 40);
-    LOCAL_ARRAY(char, html, 8192);
-    LOCAL_ARRAY(char, prefix, 2048);
-    size_t width = Config.Ftp.list_width;
+    char icon[2048];
+    char href[2048 + 40];
+    char text[ 2048];
+    char size[ 2048];
+    char chdir[ 2048 + 40];
+    char view[ 2048 + 40];
+    char download[ 2048 + 40];
+    char link[ 2048 + 40];
+    MemBuf *html;
+    char prefix[2048];
     ftpListParts *parts;
-    *icon = *href = *text = *size = *chdir = *view = *download = *link = *html = '\0';
+    *icon = *href = *text = *size = *chdir = *view = *download = *link = '\0';
 
-    if ((int) strlen(line) > 1024) {
-        snprintf(html, 8192, "%s\n", line);
+    debugs(9, 7, HERE << " line ={" << line << "}");
+
+    if (strlen(line) > 1024) {
+        html = new MemBuf();
+        html->init();
+        html->Printf("<tr><td colspan=\"5\">%s</td></tr>\n", line);
         return html;
     }
 
@@ -1021,64 +907,12 @@ FtpStateData::htmlifyListEntry(const char *line)
     else
         prefix[0] = '\0';
 
-    /* Handle builtin <dirup> */
-    if (strcmp(line, "<internal-dirup>") == 0) {
-        /* <A HREF="{href}">{icon}</A> <A HREF="{href}">{text}</A> {link} */
-        snprintf(icon, 2048, "<IMG border=\"0\" SRC=\"%s\" ALT=\"%-6s\">",
-                 mimeGetIconURL("internal-dirup"),
-                 "[DIRUP]");
-
-        if (!flags.no_dotdot && !flags.root_dir) {
-            /* Normal directory */
-
-            if (!flags.dir_slash)
-                strcpy(href, "../");
-            else
-                strcpy(href, "./");
-
-            strcpy(text, "Parent Directory");
-        } else if (!flags.no_dotdot && flags.root_dir) {
-            /* "Top level" directory */
-            strcpy(href, "%2e%2e/");
-            strcpy(text, "Parent Directory");
-            snprintf(link, 2048, "(<A HREF=\"%s\">%s</A>)",
-                     "%2f/",
-                     "Root Directory");
-        } else if (flags.no_dotdot && !flags.root_dir) {
-            char *url;
-            /* Normal directory where last component is / or ..  */
-            strcpy(href, "%2e%2e/");
-            strcpy(text, "Parent Directory");
-
-            if (flags.dir_slash) {
-                url = xstrdup("./");
-            } else {
-                const char *title = title_url.termedBuf();
-                int k = 6 + strcspn(&title[6], "/");
-                char *t;
-                url = xstrdup(title + k);
-                t = url + strlen(url) - 2;
-
-                while (t > url && *t != '/')
-                    *t-- = '\0';
-            }
-
-            snprintf(link, 2048, "(<A HREF=\"%s\">%s</A>)", url, "Back");
-            safe_free(url);
-        } else {               /* NO_DOTDOT && ROOT_DIR */
-            /* "UNIX Root" directory */
-            strcpy(href, "/");
-            strcpy(text, "Home Directory");
-        }
-
-        snprintf(html, 8192, "<A HREF=\"%s\">%s</A> <A HREF=\"%s\">%s</A> %s\n",
-                 href, icon, href, text, link);
-        return html;
-    }
-
     if ((parts = ftpListParseParts(line, flags)) == NULL) {
         const char *p;
-        snprintf(html, 8192, "%s\n", line);
+
+        html = new MemBuf();
+        html->init();
+        html->Printf("<tr class=\"entry\"><td colspan=\"5\">%s</td></tr>\n", line);
 
         for (p = line; *p && xisspace(*p); p++);
         if (*p && !xisspace(*p))
@@ -1088,22 +922,14 @@ FtpStateData::htmlifyListEntry(const char *line)
     }
 
     if (!strcmp(parts->name, ".") || !strcmp(parts->name, "..")) {
-        *html = '\0';
         ftpListPartsFree(&parts);
-        return html;
+        return NULL;
     }
 
     parts->size += 1023;
     parts->size >>= 10;
     parts->showname = xstrdup(parts->name);
 
-    if (!Config.Ftp.list_wrap) {
-        if (strlen(parts->showname) > width - 1) {
-            *(parts->showname + width - 1) = '>';
-            *(parts->showname + width - 0) = '\0';
-        }
-    }
-
     /* {icon} {text} . . . {date}{size}{chdir}{view}{download}{link}\n  */
     xstrncpy(href, rfc1738_escape_part(parts->name), 2048);
 
@@ -1112,21 +938,21 @@ FtpStateData::htmlifyListEntry(const char *line)
     switch (parts->type) {
 
     case 'd':
-        snprintf(icon, 2048, "<IMG border=\"0\" SRC=\"%s\" ALT=\"%-6s\">",
+        snprintf(icon, 2048, "<img border=\"0\" src=\"%s\" alt=\"%-6s\">",
                  mimeGetIconURL("internal-dir"),
                  "[DIR]");
         strcat(href, "/");     /* margin is allocated above */
         break;
 
     case 'l':
-        snprintf(icon, 2048, "<IMG border=\"0\" SRC=\"%s\" ALT=\"%-6s\">",
+        snprintf(icon, 2048, "<img border=\"0\" src=\"%s\" alt=\"%-6s\">",
                  mimeGetIconURL("internal-link"),
                  "[LINK]");
         /* sometimes there is an 'l' flag, but no "->" link */
 
         if (parts->link) {
             char *link2 = xstrdup(html_quote(rfc1738_escape(parts->link)));
-            snprintf(link, 2048, " -> <A HREF=\"%s%s\">%s</A>",
+            snprintf(link, 2048, " -&gt; <a href=\"%s%s\">%s</a>",
                      *link2 != '/' ? prefix : "", link2,
                      html_quote(parts->link));
             safe_free(link2);
@@ -1135,11 +961,11 @@ FtpStateData::htmlifyListEntry(const char *line)
         break;
 
     case '\0':
-        snprintf(icon, 2048, "<IMG border=\"0\" SRC=\"%s\" ALT=\"%-6s\">",
+        snprintf(icon, 2048, "<img border=\"0\" src=\"%s\" alt=\"%-6s\">",
                  mimeGetIconURL(parts->name),
                  "[UNKNOWN]");
-        snprintf(chdir, 2048, " <A HREF=\"%s/;type=d\"><IMG border=\"0\" SRC=\"%s\" "
-                 "ALT=\"[DIR]\"></A>",
+        snprintf(chdir, 2048, "<a href=\"%s/;type=d\"><img border=\"0\" src=\"%s\" "
+                 "alt=\"[DIR]\"></a>",
                  rfc1738_escape_part(parts->name),
                  mimeGetIconURL("internal-dir"));
         break;
@@ -1147,7 +973,7 @@ FtpStateData::htmlifyListEntry(const char *line)
     case '-':
 
     default:
-        snprintf(icon, 2048, "<IMG border=\"0\" SRC=\"%s\" ALT=\"%-6s\">",
+        snprintf(icon, 2048, "<img border=\"0\" src=\"%s\" alt=\"%-6s\">",
                  mimeGetIconURL(parts->name),
                  "[FILE]");
         snprintf(size, 2048, " %6"PRId64"k", parts->size);
@@ -1156,31 +982,33 @@ FtpStateData::htmlifyListEntry(const char *line)
 
     if (parts->type != 'd') {
         if (mimeGetViewOption(parts->name)) {
-            snprintf(view, 2048, " <A HREF=\"%s%s;type=a\"><IMG border=\"0\" SRC=\"%s\" "
-                     "ALT=\"[VIEW]\"></A>",
+            snprintf(view, 2048, "<a href=\"%s%s;type=a\"><img border=\"0\" src=\"%s\" "
+                     "alt=\"[VIEW]\"></a>",
                      prefix, href, mimeGetIconURL("internal-view"));
         }
 
         if (mimeGetDownloadOption(parts->name)) {
-            snprintf(download, 2048, " <A HREF=\"%s%s;type=i\"><IMG border=\"0\" SRC=\"%s\" "
-                     "ALT=\"[DOWNLOAD]\"></A>",
+            snprintf(download, 2048, "<a href=\"%s%s;type=i\"><img border=\"0\" src=\"%s\" "
+                     "alt=\"[DOWNLOAD]\"></a>",
                      prefix, href, mimeGetIconURL("internal-download"));
         }
     }
 
-    /* <A HREF="{href}">{icon}</A> <A HREF="{href}">{text}</A> . . . {date}{size}{chdir}{view}{download}{link}\n  */
-    if (parts->type != '\0') {
-        snprintf(html, 8192, "<A HREF=\"%s%s\">%s</A> <A HREF=\"%s%s\">%s</A>%s "
-                 "%s%8s%s%s%s%s\n",
-                 prefix, href, icon, prefix, href, html_quote(text), dots_fill(strlen(text)),
-                 parts->date, size, chdir, view, download, link);
-    } else {
-        /* Plain listing. {icon} {text} ... {chdir}{view}{download} */
-        snprintf(html, 8192, "<A HREF=\"%s%s\">%s</A> <A HREF=\"%s%s\">%s</A>%s "
-                 "%s%s%s%s\n",
-                 prefix, href, icon, prefix, href, html_quote(text), dots_fill(strlen(text)),
-                 chdir, view, download, link);
-    }
+    /* construct the table row from parts. */
+    html = new MemBuf();
+    html->init();
+    html->Printf("<tr class=\"entry\">"
+             "<td class=\"icon\"><a href=\"%s%s\">%s</a></td>"
+             "<td class=\"filename\"><a href=\"%s%s\">%s</a></td>"
+             "<td class=\"date\">%s</td>"
+             "<td class=\"size\">%s</td>"
+             "<td class=\"actions\">%s%s%s%s</td>"
+             "</tr>\n",
+             prefix, href, icon,
+             prefix, href, html_quote(text),
+             parts->date,
+             size,
+             chdir, view, download, link);
 
     ftpListPartsFree(&parts);
     return html;
@@ -1194,14 +1022,13 @@ FtpStateData::parseListing()
     char *end;
     char *line;
     char *s;
-    char *t;
+    MemBuf *t;
     size_t linelen;
     size_t usable;
-    StoreEntry *e = entry;
     size_t len = data.readBuf->contentSize();
 
     if (!len) {
-        debugs(9, 3, HERE << "no content to parse for " << e->url()  );
+        debugs(9, 3, HERE << "no content to parse for " << entry->url()  );
         return;
     }
 
@@ -1220,7 +1047,7 @@ FtpStateData::parseListing()
     debugs(9, 3, HERE << "usable = " << usable);
 
     if (usable == 0) {
-        debugs(9, 3, HERE << "didn't find end for " << e->url()  );
+        debugs(9, 3, HERE << "didn't find end for " << entry->url()  );
         xfree(sbuf);
         return;
     }
@@ -1229,7 +1056,6 @@ FtpStateData::parseListing()
 
     line = (char *)memAllocate(MEM_4K_BUF);
     end++;
-    e->buffer();       /* released when done processing current data payload */
     s = sbuf;
     s += strspn(s, crlf);
 
@@ -1252,11 +1078,14 @@ FtpStateData::parseListing()
 
         t = htmlifyListEntry(line);
 
-        assert(t != NULL);
-
-        writeReplyBody(t, strlen(t));
+        if( t != NULL) {
+            debugs(9, 7, HERE << "listing append: t = {" << t->contentSize() << ", '" << t->content() << "'}");
+            listing.append(t->content(), t->contentSize());
+//leak?            delete t;
+        }
     }
 
+    debugs(9, 7, HERE << "Done.");
     data.readBuf->consume(usable);
     memFree(line, MEM_4K_BUF);
     xfree(sbuf);
@@ -1373,7 +1202,7 @@ FtpStateData::dataRead(const CommIoCbParams &io)
 
             maybeReadVirginBody();
         } else {
-            if (!flags.http_header_sent && !fwd->ftpPasvFailed() && flags.pasv_supported) {
+            if (!flags.http_header_sent && !fwd->ftpPasvFailed() && flags.pasv_supported && !flags.listing) {
                 fwd->dontRetry(false); /* this is a retryable error */
                 fwd->ftpPasvFailed(true);
             }
@@ -1410,7 +1239,8 @@ FtpStateData::processReplyBody()
         return;
     }
 
-    if (!flags.http_header_sent && data.readBuf->contentSize() >= 0)
+    /* Directory listings are special. They write ther own headers via the error objects */
+    if (!flags.http_header_sent && data.readBuf->contentSize() >= 0 && !flags.isdir)
         appendSuccessHeader();
 
     if (EBIT_TEST(entry->flags, ENTRY_ABORTED)) {
@@ -1431,11 +1261,14 @@ FtpStateData::processReplyBody()
 
 #endif
 
-    if (flags.isdir && !flags.listing_started)
-        listingStart();
-
     if (flags.isdir) {
+        if (!flags.listing) {
+            flags.listing = 1;
+            listing.reset();
+        }
         parseListing();
+        maybeReadVirginBody();
+        return;
     } else if (const int csize = data.readBuf->contentSize()) {
         writeReplyBody(data.readBuf->content(), csize);
         debugs(9, 5, HERE << "consuming " << csize << " bytes of readBuf");
@@ -1925,7 +1758,12 @@ FtpStateData::handleControlReply()
     /* Copy the rest of the message to cwd_message to be printed in
      * error messages
      */
-    wordlistAddWl(&cwd_message, ctrl.message);
+    if (ctrl.message) {
+        for (wordlist *w = ctrl.message; w; w = w->next) {
+            cwd_message.append('\n');
+            cwd_message.append(w->key);
+        }
+    }
 
     debugs(9, 3, HERE << "state=" << state << ", code=" << ctrl.replycode);
 
@@ -2269,13 +2107,13 @@ ftpReadCwd(FtpStateData * ftpState)
     if (code >= 200 && code < 300) {
         /* CWD OK */
         ftpState->unhack();
-        /* Reset cwd_message to only include the last message */
-
-        if (ftpState->cwd_message)
-            wordlistDestroy(&ftpState->cwd_message);
-
-        ftpState->cwd_message = ftpState->ctrl.message;
 
+        /* Reset cwd_message to only include the last message */
+        ftpState->cwd_message.reset("");
+        for (wordlist *w = ftpState->ctrl.message; w; w = w->next) {
+            ftpState->cwd_message.append(' ');
+            ftpState->cwd_message.append(w->key);
+        }
         ftpState->ctrl.message = NULL;
 
         /* Continue to traverse the path */
@@ -3441,6 +3279,28 @@ ftpReadRetr(FtpStateData * ftpState)
     }
 }
 
+/**
+ * Generate the HTTP headers and template fluff around an FTP
+ * directory listing display.
+ */
+void
+FtpStateData::completedListing()
+{
+    assert(entry);
+    entry->lock();
+    ErrorState *ferr = errorCon(ERR_DIR_LISTING, HTTP_OK, request);
+    ferr->ftp.listing = &listing;
+    ferr->ftp.cwd_msg = xstrdup(cwd_message.termedBuf());
+    ferr->ftp.server_msg = ctrl.message;
+    ctrl.message = NULL;
+    entry->replaceHttpReply( ferr->BuildHttpReply() );
+    errorStateFree(ferr);
+    EBIT_CLR(entry->flags, ENTRY_FWD_HDR_WAIT);
+    entry->flush();
+    entry->unlock();
+}
+
+
 /// \ingroup ServerProtocolFTPInternal
 static void
 ftpReadTransferDone(FtpStateData * ftpState)
@@ -3450,10 +3310,10 @@ ftpReadTransferDone(FtpStateData * ftpState)
 
     if (code == 226 || code == 250) {
         /* Connection closed; retrieval done. */
-
-        if (ftpState->flags.html_header_sent)
-            ftpState->listingFinish();
-
+        if (ftpState->flags.listing) {
+            ftpState->completedListing();
+            /* QUIT operation handles sending the reply to client */
+        }
         ftpSendQuit(ftpState);
     } else {                   /* != 226 */
         debugs(9, DBG_IMPORTANT, HERE << "Got code " << code << " after reading data");
@@ -3513,11 +3373,15 @@ ftpSendQuit(FtpStateData * ftpState)
     ftpState->state = SENT_QUIT;
 }
 
-/// \ingroup ServerProtocolFTPInternal
+/**
+ * \ingroup ServerProtocolFTPInternal
+ *
+ *  This completes a client FTP operation with success or other page
+ *  generated and stored in the entry field by the code issuing QUIT.
+ */
 static void
 ftpReadQuit(FtpStateData * ftpState)
 {
-    /** \todo XXX should this just be a case of abortTransaction? */
     ftpState->serverComplete();
 }
 
@@ -3687,7 +3551,6 @@ FtpStateData::failedErrorMessage(err_type error, int xerrno)
     err->xerrno = xerrno;
 
     err->ftp.server_msg = ctrl.message;
-
     ctrl.message = NULL;
 
     if (old_request)
@@ -3764,7 +3627,6 @@ FtpStateData::appendSuccessHeader()
     String urlpath = request->urlpath;
     const char *filename = NULL;
     const char *t = NULL;
-    StoreEntry *e = entry;
 
     debugs(9, 3, HERE);
 
@@ -3775,11 +3637,11 @@ FtpStateData::appendSuccessHeader()
 
     flags.http_header_sent = 1;
 
-    assert(e->isEmpty());
+    assert(entry->isEmpty());
 
-    EBIT_CLR(e->flags, ENTRY_FWD_HDR_WAIT);
+    EBIT_CLR(entry->flags, ENTRY_FWD_HDR_WAIT);
 
-    e->buffer();       /* released when done processing current data payload */
+    entry->buffer();   /* released when done processing current data payload */
 
     filename = (t = urlpath.rpos('/')) ? t + 1 : urlpath.termedBuf();