Precis: Gopher browsing is now mostly functional.
Due to having to wait for the full content before converting to html, large pages like search results might take some seconds to load, but otherwise it works not too bad. Supported item-types: - 0 (plain text) - 1 (gopher dir) converted to html for display - 3 (error, when first item should behave as an HTTP 404, not much tested) - 7 (search) converted to html for display - 8 generates a link to a telnet: URI - g (GIF) adds a link (or optionally inlines the image as <img>) - h (html) selectors begining with URL: are automatically converted to direct links to the specified URL, which then is not limited to http: either. - i displays a text line - I (image) links to the item but tries to display as text. - 9 (binary) links to the item but tries to display as text. - d (PDF, unofficial) - p (seems to be PNG, unofficial) - all other unknown types are just linked as is, and will probably work when the mime type is correctly sniffed. Unsupported item-types: - 2 (CSO search) I failed to find a sample link, it requires a complex and separate protocol. - T (TN3270) Failed to find a sample link, it's an antique competitor to telnet. Missing stuff, which will be considered in a second iteration: - document the feature. - better error handling on non-existing files (will require an heuristic since gopher doesn't have out-of-band signaling). - make the parser more robust. - fix url escaping in generated html code, gopher selectors can include any character except tab, though no tested servers ever tried using reserved html chars yet. - add a search icons. - links to ambiguous items (like type 'I' which can be JPEG or PNG or another image type, and binary types like '9') will attempt to load the target as a text file. Handling them correctly probably requires implementing LLCACHE_RETRIEVE_SNIFF_TYPE. Archive types could probably get a fake Content-disposition header forcing a download though. - make gopher: URI scheme handling explicit to OS front-ends (done for BeOS, OSX, and AmigaOS (untested)). - fix search field sending an extra = in the url (add a "GOPHER" form method maybe ?), though all tested search engines just skip it. - maybe Gopher+ support but it seems most clients just ignore it. Added files Index: render/gopher.c =================================================================== --- /dev/null 2011-05-08 22:46:04.000000000 +0200 +++ render/gopher.c 2011-05-08 22:41:11.000000000 +0200 @@ -0,0 +1,753 @@ +/* + * Copyright 2006 James Bursa <bu...@users.sourceforge.net> + * Copyright 2006 Adrian Lees <adri...@users.sourceforge.net> + * Copyright 2011 François Revol <mmu_...@users.sourceforge.net> + * + * This file is part of NetSurf, http://www.netsurf-browser.org/ + * + * NetSurf 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; version 2 of the License. + * + * NetSurf 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 <http://www.gnu.org/licenses/>. + */ + +/** \file + * Content for text/x-gopher-directory (implementation). + */ + +#include <errno.h> +#include <stddef.h> +#include <string.h> +#include <strings.h> +#include <math.h> + +#include "content/content_protected.h" +#include "desktop/gui.h" +#include "desktop/options.h" +#include "render/gopher.h" +#include "utils/http.h" +#include "utils/log.h" +#include "utils/messages.h" +#include "utils/utils.h" + +/* HACK HACK */ +extern const content_handler html_content_handler; + +nserror gopher_create(const content_handler *handler, + lwc_string *imime_type, const http_parameter *params, + llcache_handle *llcache, const char *fallback_charset, + bool quirks, struct content **c); +bool gopher_convert(struct content *c); +content_type gopher_content_type(lwc_string *mime_type); + +static char *gen_nice_title(const char *path); +static bool gopher_generate_top(char *buffer, int buffer_length); +static bool gopher_generate_title(const char *title, char *buffer, int buffer_length); +static bool gopher_generate_row(const char **data, size_t *size, + char *buffer, int buffer_length); +static bool gopher_generate_bottom(char *buffer, int buffer_length); + +static const char gopher_faked_type[] = "text/x-gopher-directory"; +static lwc_string *gopher_faked_mime_type; + +static struct { + char type; + const char *mime; +} gopher_type_map[] = { + /* these come from http://tools.ietf.org/html/rfc1436 */ + { '0', "text/plain" }, + { '1', "text/x-gopher-directory;charset=UTF-8" }, /* gopher directory */ + /* 2 CSO search */ + /* 3 error message */ + /* 4 binhex encoded text */ + /* 5 binary archive file */ + /* 6 uuencoded text */ + { '7', "text/x-gopher-directory;charset=UTF-8" }, /* search query */ + /* 8 telnet: */ + /* 9 binary */ + { 'g', "image/gif" }, + { 'h', "text/html" }, + /* i information text */ + /* I image (depends, usually jpeg) */ + /* s audio (wav?) */ + /* T tn3270 session */ + + /* those are not standardized */ + { 'd', "application/pdf" }, /* display?? seems to be only for PDF files so far */ + { 'p', "image/png"}, /* at least on gopher://namcub.accelera-labs.com/1/pics */ + { 0, NULL } +}; + +static const content_handler gopher_content_handler = { + gopher_create, + NULL, + gopher_convert, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + NULL, + gopher_content_type, + true +}; + + +nserror gopher_init(void) +{ + lwc_error lerror; + nserror error; + + lerror = lwc_intern_string(gopher_faked_type, + strlen(gopher_faked_type), + &gopher_faked_mime_type); + if (lerror != lwc_error_ok) { + error = NSERROR_NOMEM; + goto error; + } + + error = content_factory_register_handler(gopher_faked_mime_type, + &gopher_content_handler); + if (error != NSERROR_OK) + goto error; + + return NSERROR_OK; + +error: + gopher_fini(); + + return error; +} + + +void gopher_fini(void) +{ + if (gopher_faked_mime_type) + lwc_string_unref(gopher_faked_mime_type); +} + + +/** + * Create a CONTENT_GOPHER. + */ + +nserror gopher_create(const content_handler *handler, + lwc_string *imime_type, const http_parameter *params, + llcache_handle *llcache, const char *fallback_charset, + bool quirks, struct content **c) +{ + nserror error; + + error = html_content_handler.create(handler, imime_type, params, + llcache, fallback_charset, quirks, c); + return error; +} + + +/** + * Convert a CONTENT_GOPHER for display. + */ + +bool gopher_convert(struct content *c) +{ + char *title; + char buffer[1024]; + const char *data; + unsigned long size; + const char *p; + unsigned long left; + bool ok; + + data = content__get_source_data(c, &size); + + p = data; + left = size; + if (data == NULL || size == 0) + return false; + + if (gopher_generate_bottom(buffer, sizeof(buffer))) { + ok = html_content_handler.process_data(c, buffer, strlen(buffer)); + if (!ok) + return false; + } + if (gopher_generate_top(buffer, sizeof(buffer))) { + ok = html_content_handler.process_data(c, buffer, strlen(buffer)); + if (!ok) + return false; + } + title = gen_nice_title(content__get_url(c)); + if (gopher_generate_title(title, buffer, sizeof(buffer))) { + ok = html_content_handler.process_data(c, buffer, strlen(buffer)); + if (!ok) + return false; + } + free(title); + + while (gopher_generate_row(&p, &left, buffer, sizeof(buffer))) { + ok = html_content_handler.process_data(c, buffer, strlen(buffer)); + if (!ok) + return false; + gui_multitask(); + } + + /* finally make it HTML so we don't have to bother for other calls */ + c->handler = &html_content_handler; + + /* forward to the HTML handler */ + return html_content_handler.convert(c); +} + +/** + * Compute the type of a content + * + * \param c Content to consider + * \return CONTENT_HTML + */ +content_type gopher_content_type(lwc_string *mime_type) +{ + /* we will end up with HTML content anyway */ + return CONTENT_HTML; +} + + +static char *html_escape_string(char *str) +{ + char *nice_str, *cnv, *tmp; + + if (str == NULL) { + return NULL; + } + + /* Convert str for display */ + nice_str = malloc(strlen(str) * SLEN("&") + 1); + if (nice_str == NULL) { + return NULL; + } + + /* Escape special HTML characters */ + for (cnv = nice_str, tmp = str; *tmp != '\0'; tmp++) { + if (*tmp == '<') { + *cnv++ = '&'; + *cnv++ = 'l'; + *cnv++ = 't'; + *cnv++ = ';'; + } else if (*tmp == '>') { + *cnv++ = '&'; + *cnv++ = 'g'; + *cnv++ = 't'; + *cnv++ = ';'; + } else if (*tmp == '&') { + *cnv++ = '&'; + *cnv++ = 'a'; + *cnv++ = 'm'; + *cnv++ = 'p'; + *cnv++ = ';'; + } else { + *cnv++ = *tmp; + } + } + *cnv = '\0'; + + return nice_str; +} + + +static char *gen_nice_title(const char *path) +{ + const char *tmp; + char *nice_path, *cnv; + char *title; + int title_length; + + /* Convert path for display */ + nice_path = malloc(strlen(path) * SLEN("&") + 1); + if (nice_path == NULL) { + return NULL; + } + + /* Escape special HTML characters */ + for (cnv = nice_path, tmp = path; *tmp != '\0'; tmp++) { + if (*tmp == '<') { + *cnv++ = '&'; + *cnv++ = 'l'; + *cnv++ = 't'; + *cnv++ = ';'; + } else if (*tmp == '>') { + *cnv++ = '&'; + *cnv++ = 'g'; + *cnv++ = 't'; + *cnv++ = ';'; + } else if (*tmp == '&') { + *cnv++ = '&'; + *cnv++ = 'a'; + *cnv++ = 'm'; + *cnv++ = 'p'; + *cnv++ = ';'; + } else { + *cnv++ = *tmp; + } + } + *cnv = '\0'; + + /* Construct a localised title string */ + title_length = (cnv - nice_path) + strlen(messages_get("FileIndex")); + title = malloc(title_length + 1); + + if (title == NULL) { + free(nice_path); + return NULL; + } + + /* Set title to localised "Index of <nice_path>" */ + snprintf(title, title_length, messages_get("FileIndex"), nice_path); + + free(nice_path); + + return title; +} + + +/** + * Convert the gopher item type to mime type + * + * \return MIME type string + * + */ + +const char *gopher_type_to_mime(char type) +{ + int i; + + for (i = 0; gopher_type_map[i].type; i++) + if (gopher_type_map[i].type == type) + return gopher_type_map[i].mime; + return NULL; +} + + +/** + * Tells if the gopher item type needs to be converted to html + * + * \return true iff the item must be converted + * + */ + +bool gopher_need_generate(char type) +{ + switch (type) { + case '1': + case '7': + return true; + default: + return false; + } +} + + +/** + * Generates the top part of an HTML directory listing page + * + * \return true iff buffer filled without error + * + * This is part of a series of functions. To generate a complete page, + * call the following functions in order: + * + * gopher_generate_top() + * gopher_generate_title() + * gopher_generate_row() -- call 'n' times for 'n' rows + * gopher_generate_bottom() + */ + +static bool gopher_generate_top(char *buffer, int buffer_length) +{ + int error = snprintf(buffer, buffer_length, + "<html>\n" + "<head>\n" + /*"<!-- base href=\"%s\" -->\n"*//* XXX: needs the content url */ + /* Don't do that: + * seems to trigger a reparsing of the gopher data itself as html... + * "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />\n" + */ + /* TODO: move this to clean CSS in internal.css */ + "<link rel=\"stylesheet\" title=\"Standard\" " + "type=\"text/css\" href=\"resource:internal.css\">\n"); + + if (error < 0 || error >= buffer_length) + /* Error or buffer too small */ + return false; + else + /* OK */ + return true; +} + + +/** + * Generates the part of an HTML directory listing page that contains the title + * + * \param title title to use + * \param buffer buffer to fill with generated HTML + * \param buffer_length maximum size of buffer + * \return true iff buffer filled without error + * + * This is part of a series of functions. To generate a complete page, + * call the following functions in order: + * + * gopher_generate_top() + * gopher_generate_title() + * gopher_generate_row() -- call 'n' times for 'n' rows + * gopher_generate_bottom() + */ + +static bool gopher_generate_title(const char *title, char *buffer, int buffer_length) +{ + int error; + + if (title == NULL) + title = ""; + + error = snprintf(buffer, buffer_length, + "<title>%s</title>\n" + "</head>\n" + "<body id=\"gopher\">\n" + "<h1>%s</h1>\n", + title, title); + if (error < 0 || error >= buffer_length) + /* Error or buffer too small */ + return false; + else + /* OK */ + return true; +} + +/** + * Internal worker called by gopher_generate_row(). + */ + +static bool gopher_generate_row_internal(char type, char *fields[5], + char *buffer, int buffer_length) +{ + char *nice_text; + char *redirect_url = NULL; + int error; + bool alt_port = false; + char *username = NULL; + + if (fields[3] && strcmp(fields[3], "70")) + alt_port = true; + + /* escape html special characters */ + nice_text = html_escape_string(fields[0]); + + /* XXX: outputting \n generates better looking html code, + * but currently screws up indentation due to a bug. + */ +#define HTML_LF +/*#define HTML_LF "\n"*/ + + switch (type) { + case '.': + /* end of the page */ + *buffer = '\0'; + break; + case '0': /* text/plain link */ + error = snprintf(buffer, buffer_length, + "<a href=\"gopher://%s%s%s/%c%s\">"HTML_LF + "<span class=\"text\">%s</span></a>"HTML_LF + "<br/>"HTML_LF, + fields[2], + alt_port ? ":" : "", + alt_port ? fields[3] : "", + type, fields[1], nice_text); + break; + case '9': /* binary */ + error = snprintf(buffer, buffer_length, + "<a href=\"gopher://%s%s%s/%c%s\">"HTML_LF + "<span class=\"binary\">%s</span></a>"HTML_LF + "<br/>"HTML_LF, + fields[2], + alt_port ? ":" : "", + alt_port ? fields[3] : "", + type, fields[1], nice_text); + break; + case '1': + /* + * directory link + */ + error = snprintf(buffer, buffer_length, + "<a href=\"gopher://%s%s%s/%c%s\">"HTML_LF + "<span class=\"dir\">%s</span></a>"HTML_LF + "<br/>"HTML_LF, + fields[2], + alt_port ? ":" : "", + alt_port ? fields[3] : "", + type, fields[1], nice_text); + break; + case '3': + /* Error + */ + error = snprintf(buffer, buffer_length, + "<span class=\"error\">%s</span><br/>"HTML_LF, + nice_text); + break; + case '7': + /* TODO: handle search better. + * For now we use an unnamed input field and accept sending ?=foo + * as it seems at least Veronica-2 ignores the = but it's unclean. + */ + error = snprintf(buffer, buffer_length, + "<form method=\"get\" action=\"gopher://%s%s%s/%c%s\">"HTML_LF + "<span class=\"query\">" + "<label>%s " + "<input name=\"\" type=\"text\" align=\"right\" />" + "</label>" + "</span></form>"HTML_LF + "<br/>"HTML_LF, + fields[2], + alt_port ? ":" : "", + alt_port ? fields[3] : "", + type, fields[1], nice_text); + break; + case '8': + /* telnet: links + * cf. gopher://78.80.30.202/1/ps3 + * -> gopher://78.80.30.202:23/8/ps3/new -> new@78.80.30.202 + */ + alt_port = false; + if (fields[3] && strcmp(fields[3], "23")) + alt_port = true; + username = strrchr(fields[1], '/'); + if (username) + username++; + error = snprintf(buffer, buffer_length, + "<a href=\"telnet://%s%s%s%s%s\">"HTML_LF + "<span class=\"dir\">%s</span></a>"HTML_LF + "<br/>"HTML_LF, + username ? username : "", + username ? "@" : "", + fields[2], + alt_port ? ":" : "", + alt_port ? fields[3] : "", + nice_text); + break; + case 'g': + case 'I': + case 'p': + /* quite dangerous, cf. gopher://namcub.accela-labs.com/1/pics */ + if (option_gopher_inline_images) { + error = snprintf(buffer, buffer_length, + "<a href=\"gopher://%s%s%s/%c%s\">"HTML_LF + "<span class=\"img\">%s "HTML_LF /* </span><br/> */ + //"<span class=\"img\" >"HTML_LF + "<img src=\"gopher://%s%s%s/%c%s\" alt=\"%s\"/>"HTML_LF + "</span>" + "</a>" + "<br/>"HTML_LF, + fields[2], + alt_port ? ":" : "", + alt_port ? fields[3] : "", + type, fields[1], + nice_text, + fields[2], + alt_port ? ":" : "", + alt_port ? fields[3] : "", + type, fields[1], + nice_text); + break; + } + /* fallback to default, link them */ + error = snprintf(buffer, buffer_length, + "<a href=\"gopher://%s%s%s/%c%s\">"HTML_LF + "<span class=\"dir\">%s</span></a>"HTML_LF + "<br/>"HTML_LF, + fields[2], + alt_port ? ":" : "", + alt_port ? fields[3] : "", + type, fields[1], nice_text); + break; + case 'h': + if (fields[1] && strncmp(fields[1], "URL:", 4) == 0) + redirect_url = fields[1] + 4; + /* cf. gopher://pineapple.vg/1 */ + if (fields[1] && strncmp(fields[1], "/URL:", 5) == 0) + redirect_url = fields[1] + 5; + if (redirect_url) { + error = snprintf(buffer, buffer_length, + "<a href=\"%s\">"HTML_LF + "<span class=\"link\">%s</span></a>"HTML_LF + "<br/>"HTML_LF, + redirect_url, + nice_text); + } else { + /* cf. gopher://sdf.org/1/sdf/classes/ */ + error = snprintf(buffer, buffer_length, + "<a href=\"gopher://%s%s%s/%c%s\">"HTML_LF + "<span class=\"dir\">%s</span></a>"HTML_LF + "<br/>"HTML_LF, + fields[2], + alt_port ? ":" : "", + alt_port ? fields[3] : "", + type, fields[1], nice_text); + } + break; + case 'i': + error = snprintf(buffer, buffer_length, + "<span class=\"info\">%s</span><br/>"HTML_LF, + nice_text); + break; + default: + LOG(("warning: unknown gopher item type 0x%02x '%c'\n", type, type)); + error = snprintf(buffer, buffer_length, + "<a href=\"gopher://%s%s%s/%c%s\">"HTML_LF + "<span class=\"dir\">%s</span></a>"HTML_LF + "<br/>"HTML_LF, + fields[2], + alt_port ? ":" : "", + alt_port ? fields[3] : "", + type, fields[1], nice_text); + break; + } + + free(nice_text); + + if (error < 0 || error >= buffer_length) + /* Error or buffer too small */ + return false; + else + /* OK */ + return true; +} + + +/** + * Generates the part of an HTML directory listing page that displays a row + * of the gopher data + * + * \param size pointer to the data buffer pointer + * \param size pointer to the remaining data size + * \param buffer buffer to fill with generated HTML + * \param buffer_length maximum size of buffer + * \return true iff buffer filled without error + * + * This is part of a series of functions. To generate a complete page, + * call the following functions in order: + * + * gopher_generate_top() + * gopher_generate_title() + * gopher_generate_row() -- call 'n' times for 'n' rows + * gopher_generate_bottom() + */ + +static bool gopher_generate_row(const char **data, size_t *size, + char *buffer, int buffer_length) +{ + bool ok = false; + char type = 0; + int field = 0; + /* name, selector, host, port, gopher+ flag */ + char *fields[5] = { NULL, NULL, NULL, NULL, NULL }; + const char *s = *data; + const char *p = *data; + int i; + + for (; *size && *p; p++, (*size)--) { + if (!type) { + type = *p; + if (!type || type == '\n' || type == '\r') { + LOG(("warning: invalid gopher item type 0x%02x\n", type)); + } + s++; + continue; + } + switch (*p) { + case '\n': + if (field > 0) { + LOG(("warning: unterminated gopher item '%c'\n", type)); + } + //FALLTHROUGH + case '\r': + if (*size < 1 || p[1] != '\n') { + LOG(("warning: CR without LF in gopher item '%c'\n", type)); + } + if (field < 3 && type != '.') { + LOG(("warning: unterminated gopher item '%c'\n", type)); + } + fields[field] = malloc(p - s + 1); + memcpy(fields[field], s, p - s); + fields[field][p - s] = '\0'; + if (type == '.' && field == 0 && p == s) { + ;/* XXX: signal end of page? For now we just ignore it. */ + } + ok = gopher_generate_row_internal(type, fields, buffer, buffer_length); + for (i = 0; i < 5; i++) { + free(fields[i]); + fields[i] = NULL; + } + (*size)--; + p++; + if (*size && *p == '\n') { + p++; + (*size)--; + } + *data = p; + field = 0; + return ok; + case '\x09': + if (field >= 4) { + LOG(("warning: extra tab in gopher item '%c'\n", type)); + break; + } + fields[field] = malloc(p - s + 1); + memcpy(fields[field], s, p - s); + fields[field][p - s] = '\0'; + field++; + s = p + 1; + break; + default: + break; + } + } + + return false; +} + + +/** + * Generates the bottom part of an HTML directory listing page + * + * \return Bottom of directory listing HTML + * + * This is part of a series of functions. To generate a complete page, + * call the following functions in order: + * + * gopher_generate_top() + * gopher_generate_title() + * gopher_generate_row() -- call 'n' times for 'n' rows + * gopher_generate_bottom() + */ + +static bool gopher_generate_bottom(char *buffer, int buffer_length) +{ + int error = snprintf(buffer, buffer_length, + "</div>\n" + "</body>\n" + "</html>\n"); + if (error < 0 || error >= buffer_length) + /* Error or buffer too small */ + return false; + else + /* OK */ + return true; +} + + Index: render/gopher.h =================================================================== --- /dev/null 2011-05-08 22:46:04.000000000 +0200 +++ render/gopher.h 2011-05-08 22:41:11.000000000 +0200 @@ -0,0 +1,39 @@ +/* + * Copyright 2006 James Bursa <bu...@users.sourceforge.net> + * Copyright 2006 Adrian Lees <adri...@users.sourceforge.net> + * Copyright 2011 François Revol <mmu_...@users.sourceforge.net> + * + * This file is part of NetSurf, http://www.netsurf-browser.org/ + * + * NetSurf 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; version 2 of the License. + * + * NetSurf 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 <http://www.gnu.org/licenses/>. + */ + +/** \file + * Content for text/x-gopher-directory (interface). + */ + +#ifndef _NETSURF_RENDER_GOPHER_H_ +#define _NETSURF_RENDER_GOPHER_H_ + +#include <stddef.h> + +struct content; +struct http_parameter; + +nserror gopher_init(void); +void gopher_fini(void); + +const char *gopher_type_to_mime(char type); +bool gopher_need_generate(char type); + +#endif Changed files !NetSurf/Resources/internal.css,f79 | 30 ++++++++++++++ Docs/BUILDING-Cocoa | 10 ++++ Makefile.sources | 3 - amiga/dist/Install | 1 beos/beos_res.rdef | 2 cocoa/res/NetSurf-Info.plist | 1 content/fetchers/curl.c | 74 ++++++++++++++++++++++++++++++++++-- desktop/netsurf.c | 6 ++ desktop/options.c | 3 + desktop/options.h | 1 render/html.c | 2 utils/url.c | 32 +++++++++++++++ utils/url.h | 1 13 files changed, 161 insertions(+), 5 deletions(-) Index: render/html.c =================================================================== --- render/html.c (revision 12321) +++ render/html.c (working copy) @@ -101,7 +101,7 @@ unsigned int depth); #endif -static const content_handler html_content_handler = { +const content_handler html_content_handler = { html_create, html_process_data, html_convert, Index: cocoa/res/NetSurf-Info.plist =================================================================== --- cocoa/res/NetSurf-Info.plist (revision 12321) +++ cocoa/res/NetSurf-Info.plist (working copy) @@ -35,6 +35,7 @@ <string>org.netsurf-browser.NetSurf.URI</string> <key>CFBundleURLSchemes</key> <array> + <string>gopher</string> <string>http</string> <string>https</string> </array> Index: !NetSurf/Resources/internal.css,f79 =================================================================== --- !NetSurf/Resources/internal.css,f79 (revision 12321) +++ !NetSurf/Resources/internal.css,f79 (working copy) @@ -174,3 +174,33 @@ body#configlist .null-content { font-style: italic; } + +/* + * gopher listing style + */ + +body#gopher { + /* margin: 10px;*/ + font-size: 100%; + padding-bottom: 2em; } + +body#gopher h1 { + padding: 5mm; + margin: 0; + border-bottom: 2px solid #777; } + +/* XXX: white-space: pre-wrap would be better but is currently buggy */ +body#gopher span { + margin-left: 1em; + padding-left: 2em; + font-family: Courier, monospace; + white-space: pre; } + +body#gopher span.dir { + background-image: url('resource:Icons/directory.png'); + background-repeat: no-repeat; + background-position: bottom left; } + +body#gopher span.text { + background-image: url('resource:Icons/content.png'); + background-repeat: no-repeat; background-position: bottom left; } Index: Docs/BUILDING-Cocoa =================================================================== --- Docs/BUILDING-Cocoa (revision 12321) +++ Docs/BUILDING-Cocoa (working copy) @@ -20,6 +20,16 @@ In both cases the actual build process is controlled by the Makefile. + Gopher support requires a recent libcurl from MacPorts since the one Apple + ships lacks the gopher handler. Building with MacPorts breaks the self- + contained nature of the .app bundle though. Install libcurl with MacPorts: + + $ sudo port install curl + + Then build with: + + $ make TARGET=cocoa WITH_MACPORTS=1 + Obtaining NetSurf's build dependencies ======================================== Index: beos/beos_res.rdef =================================================================== --- beos/beos_res.rdef (revision 12321) +++ beos/beos_res.rdef (working copy) @@ -98,9 +98,11 @@ "types" = "image/jpeg", "types" = "application/x-vnd.Be-bookmark", "types" = "text", + "types" = "text/x-gopher-directory", "types" = "application/x-vnd.Be-doc_bookmark", "types" = "application/x-vnd.Be.URL.file", "types" = "application/x-vnd.Be.URL.ftp", + "types" = "application/x-vnd.Be.URL.gopher", "types" = "application/x-vnd.Be.URL.http", "types" = "application/x-vnd.Be.URL.https" }; Index: Makefile.sources =================================================================== --- Makefile.sources (revision 12321) +++ Makefile.sources (working copy) @@ -13,7 +13,8 @@ S_RENDER := box.c box_construct.c box_normalise.c \ font.c form.c html.c html_interaction.c html_redraw.c \ - hubbub_binding.c imagemap.c layout.c list.c table.c textplain.c + hubbub_binding.c imagemap.c layout.c list.c table.c textplain.c \ + gopher.c S_UTILS := base64.c filename.c hashtable.c http.c locale.c messages.c \ talloc.c url.c utf8.c utils.c useragent.c filepath.c log.c Index: utils/url.c =================================================================== --- utils/url.c (revision 12321) +++ utils/url.c (working copy) @@ -856,6 +856,38 @@ } /** + * Extract the gopher document type from an URL + * + * \param url an absolute URL + * \param result pointer to buffer to hold result + * \return URL_FUNC_OK on success + */ + +url_func_result url_gopher_type(const char *url, char *result) +{ + url_func_result status; + struct url_components components; + + assert(url); + + status = url_get_components(url, &components); + if (status == URL_FUNC_OK) { + if (!components.path) { + status = URL_FUNC_FAILED; + } else { + if (strlen(components.path) < 2) + *result = '1'; + else if (components.path[0] == '/') + *result = components.path[1]; + else + status = URL_FUNC_FAILED; + } + } + url_destroy_components(&components); + return status; +} + +/** * Attempt to find a nice filename for a URL. * * \param url an absolute URL Index: utils/url.h =================================================================== --- utils/url.h (revision 12321) +++ utils/url.h (working copy) @@ -60,6 +60,7 @@ url_func_result url_path(const char *url, char **result); url_func_result url_leafname(const char *url, char **result); url_func_result url_fragment(const char *url, char **result); +url_func_result url_gopher_type(const char *url, char *result); url_func_result url_compare(const char *url1, const char *url2, bool nofrag, bool *result); Index: desktop/options.h =================================================================== --- desktop/options.h (revision 12321) +++ desktop/options.h (working copy) @@ -49,6 +49,7 @@ extern int option_http_proxy_auth; extern char *option_http_proxy_auth_user; extern char *option_http_proxy_auth_pass; +extern bool option_gopher_inline_images; extern int option_font_size; extern int option_font_min_size; extern char *option_accept_language; Index: desktop/netsurf.c =================================================================== --- desktop/netsurf.c (revision 12321) +++ desktop/netsurf.c (working copy) @@ -43,6 +43,7 @@ #include "desktop/gui.h" #include "desktop/options.h" #include "desktop/searchweb.h" +#include "render/gopher.h" #include "render/html.h" #include "render/textplain.h" #include "utils/log.h" @@ -142,6 +143,10 @@ if (error != NSERROR_OK) return error; + error = gopher_init(); + if (error != NSERROR_OK) + return error; + error = html_init(); if (error != NSERROR_OK) return error; @@ -212,6 +217,7 @@ textplain_fini(); image_fini(); html_fini(); + gopher_fini(); css_fini(); LOG(("Closing utf8")); Index: desktop/options.c =================================================================== --- desktop/options.c (revision 12321) +++ desktop/options.c (working copy) @@ -69,6 +69,8 @@ char *option_http_proxy_auth_user = 0; /** Proxy authentication password */ char *option_http_proxy_auth_pass = 0; +/** Inline image items in Gopher pages. Dangerous. */ +bool option_gopher_inline_images = false; /** Default font size / 0.1pt. */ int option_font_size = 128; /** Minimum font size. */ @@ -248,6 +250,7 @@ { "http_proxy_auth", OPTION_INTEGER, &option_http_proxy_auth }, { "http_proxy_auth_user", OPTION_STRING, &option_http_proxy_auth_user }, { "http_proxy_auth_pass", OPTION_STRING, &option_http_proxy_auth_pass }, + { "gopher_inline_images", OPTION_BOOL, &option_gopher_inline_images }, { "font_size", OPTION_INTEGER, &option_font_size }, { "font_min_size", OPTION_INTEGER, &option_font_min_size }, { "font_sans", OPTION_STRING, &option_font_sans }, Index: content/fetchers/curl.c =================================================================== --- content/fetchers/curl.c (revision 12321) +++ content/fetchers/curl.c (working copy) @@ -44,6 +44,7 @@ #include "content/urldb.h" #include "desktop/netsurf.h" #include "desktop/options.h" +#include "render/gopher.h" #include "utils/log.h" #include "utils/messages.h" #include "utils/schedule.h" @@ -72,6 +73,7 @@ bool abort; /**< Abort requested. */ bool stopped; /**< Download stopped on purpose. */ bool only_2xx; /**< Only HTTP 2xx responses acceptable. */ + char gopher_type; /**< Indicates the type of document for gopher: url */ char *url; /**< URL of this fetch. */ char *host; /**< The hostname of this fetch. */ struct curl_slist *headers; /**< List of request headers. */ @@ -111,6 +113,10 @@ bool only_2xx, const char *post_urlenc, const struct fetch_multipart_data *post_multipart, const char **headers); +static void * fetch_curl_setup_gopher(struct fetch *parent_fetch, const char *url, + bool only_2xx, const char *post_urlenc, + const struct fetch_multipart_data *post_multipart, + const char **headers); static bool fetch_curl_start(void *vfetch); static bool fetch_curl_initiate_fetch(struct curl_fetch_info *fetch, CURL *handle); @@ -220,14 +226,19 @@ data = curl_version_info(CURLVERSION_NOW); for (i = 0; data->protocols[i]; i++) { + fetcher_setup_fetch setup_hook; /* Ignore non-http(s) protocols */ - if (strcmp(data->protocols[i], "http") != 0 && - strcmp(data->protocols[i], "https") != 0) + if (strcmp(data->protocols[i], "http") == 0 || + strcmp(data->protocols[i], "https") == 0) + setup_hook = fetch_curl_setup; + else if (strcmp(data->protocols[i], "gopher") == 0) + setup_hook = fetch_curl_setup_gopher; + else continue; if (!fetch_add_fetcher(data->protocols[i], fetch_curl_initialise, - fetch_curl_setup, + setup_hook, fetch_curl_start, fetch_curl_abort, fetch_curl_free, @@ -339,6 +350,7 @@ fetch->abort = false; fetch->stopped = false; fetch->only_2xx = only_2xx; + fetch->gopher_type = 0; fetch->url = strdup(url); fetch->headers = 0; fetch->host = host; @@ -410,6 +422,43 @@ } +void * fetch_curl_setup_gopher(struct fetch *parent_fetch, const char *url, + bool only_2xx, const char *post_urlenc, + const struct fetch_multipart_data *post_multipart, + const char **headers) +{ + struct curl_fetch_info *f; + const char *mime; + char type; + f = fetch_curl_setup(parent_fetch, url, only_2xx, post_urlenc, + post_multipart, headers); + if (url_gopher_type(url, &type) == URL_FUNC_OK && type) { + f->gopher_type = type; + } else { + f->http_code = 404; + fetch_set_http_code(f->fetch_handle, f->http_code); + } + + mime = gopher_type_to_mime(type); + /* TODO: add a fetch_mimetype_by_ext() or fetch_mimetype_sniff_data() */ + /* + if (mime == NULL) + mime = "application/octet-stream"; + */ + + if (mime) { + char s[80]; + /* fprintf(stderr, "gopher mime is '%s'\n", mime); */ + snprintf(s, sizeof s, "Content-type: %s\r\n", mime); + s[sizeof s - 1] = 0; + fetch_send_callback(FETCH_HEADER, f->fetch_handle, s, strlen(s), + FETCH_ERROR_NO_ERROR); + } + + return f; +} + + /** * Dispatch a single job */ @@ -771,6 +820,8 @@ LOG(("done %s", f->url)); if (abort_fetch == false && result == CURLE_OK) { + //if (f->gopher_type) + //fetch_curl_gopher_data(NULL, 0, 0, f); /* fetch completed normally */ if (f->stopped || (!f->had_headers && @@ -978,6 +1029,23 @@ struct curl_fetch_info *f = _f; CURLcode code; + /* gopher data receives special treatment */ + if (f->gopher_type && gopher_need_generate(f->gopher_type)) { + /* type 3 items report an error */ + if (!f->http_code) { + if (data[0] == '3') { + /* TODO: try to guess better from the string ? + * like "3 '/bcd' doesn't exist!" + * TODO: what about other file types ? + */ + f->http_code = 404; + } else { + f->http_code = 200; + } + fetch_set_http_code(f->fetch_handle, f->http_code); + } + } + /* ensure we only have to get this information once */ if (!f->http_code) { Index: amiga/dist/Install =================================================================== --- amiga/dist/Install (revision 12321) +++ amiga/dist/Install (working copy) @@ -559,6 +559,7 @@ ; necessary to ask whether the user wants to add NetSurf to launch-handler. (working "Adding NetSurf to launch-handler config") (p_fitr "ENVARC:launch-handler/URL/FILE.LH" "ClientName=\"NETSURF\" ClientPath=\"APPDIR:NETSURF\" CMDFORMAT=\"URL=*\"file:///%s*\"\"") + (p_fitr "ENVARC:launch-handler/URL/GOPHER.LH" "ClientName=\"NETSURF\" ClientPath=\"APPDIR:NETSURF\" CMDFORMAT=\"URL=*\"gopher://%s*\"\"") (p_fitr "ENVARC:launch-handler/URL/HTTP.LH" "ClientName=\"NETSURF\" ClientPath=\"APPDIR:NETSURF\" CMDFORMAT=\"URL=*\"http://%s*\"\"") (p_fitr "ENVARC:launch-handler/URL/HTTPS.LH" "ClientName=\"NETSURF\" ClientPath=\"APPDIR:NETSURF\" CMDFORMAT=\"URL=*\"https://%s*\"\"") (p_fitr "ENVARC:launch-handler/URL/WWW.LH" "ClientName=\"NETSURF\" ClientPath=\"APPDIR:NETSURF\" CMDFORMAT=\"URL=*\"http://www.%s*\"\"") Conflicted files Removed files