10c1a4ebfSFrançois Revol/*
20c1a4ebfSFrançois Revol * Copyright 2013-2014 Haiku Inc. All rights reserved.
30c1a4ebfSFrançois Revol * Distributed under the terms of the MIT License.
40c1a4ebfSFrançois Revol *
50c1a4ebfSFrançois Revol * Authors:
60c1a4ebfSFrançois Revol * 		Fran��ois Revol, revol@free.fr
70c1a4ebfSFrançois Revol */
80c1a4ebfSFrançois Revol
90c1a4ebfSFrançois Revol
100c1a4ebfSFrançois Revol#include <assert.h>
110c1a4ebfSFrançois Revol#include <ctype.h>
120c1a4ebfSFrançois Revol#include <stdlib.h>
130c1a4ebfSFrançois Revol#include <stdio.h>
140c1a4ebfSFrançois Revol
150c1a4ebfSFrançois Revol#include <Directory.h>
160c1a4ebfSFrançois Revol#include <DynamicBuffer.h>
170c1a4ebfSFrançois Revol#include <File.h>
180c1a4ebfSFrançois Revol#include <GopherRequest.h>
190c1a4ebfSFrançois Revol#include <NodeInfo.h>
200c1a4ebfSFrançois Revol#include <Path.h>
210c1a4ebfSFrançois Revol#include <Socket.h>
22111d695aSFrançois Revol#include <StackOrHeapArray.h>
230c1a4ebfSFrançois Revol#include <String.h>
240c1a4ebfSFrançois Revol#include <StringList.h>
250c1a4ebfSFrançois Revol
260c1a4ebfSFrançois Revol/*
2744a4999bSFrançois Revol * TODO: fix '+' in selectors, cf. gopher://gophernicus.org/1/doc/gopher/
28cf2bf306SFrançois Revol * TODO: add proper favicon
29cf2bf306SFrançois Revol * TODO: add proper dir and document icons
30cf2bf306SFrançois Revol * TODO: correctly eat the extraneous .\r\n at end of text files
310c1a4ebfSFrançois Revol * TODO: move parsing stuff to a translator?
320c1a4ebfSFrançois Revol *
330c1a4ebfSFrançois Revol * docs:
340c1a4ebfSFrançois Revol * gopher://gopher.floodgap.com/1/gopher/tech
350c1a4ebfSFrançois Revol * gopher://gopher.floodgap.com/0/overbite/dbrowse?pluginm%201
360c1a4ebfSFrançois Revol *
370c1a4ebfSFrançois Revol * tests:
380c1a4ebfSFrançois Revol * gopher://sdf.org/1/sdf/historical	images
390c1a4ebfSFrançois Revol * gopher://gopher.r-36.net/1/	large photos
400c1a4ebfSFrançois Revol * gopher://sdf.org/1/sdf/classes	binaries
410c1a4ebfSFrançois Revol * gopher://sdf.org/1/users/	long page
420c1a4ebfSFrançois Revol * gopher://jgw.mdns.org/1/	search items
430c1a4ebfSFrançois Revol * gopher://jgw.mdns.org/1/MISC/	's' item (sound)
440c1a4ebfSFrançois Revol * gopher://gopher.floodgap.com/1/gopher	broken link
450c1a4ebfSFrançois Revol * gopher://sdf.org/1/maps/m	missing lines
46e243a034SFrançois Revol * gopher://sdf.org/1/foo	gophernicus reports errors incorrectly
47e243a034SFrançois Revol * gopher://gopher.floodgap.com/1/foo	correct error report
480c1a4ebfSFrançois Revol */
490c1a4ebfSFrançois Revol
500c1a4ebfSFrançois Revol/** Type of Gopher items */
510c1a4ebfSFrançois Revoltypedef enum {
520c1a4ebfSFrançois Revol	GOPHER_TYPE_NONE	= 0,	/**< none set */
530c1a4ebfSFrançois Revol	GOPHER_TYPE_ENDOFPAGE	= '.',	/**< a dot alone on a line */
540c1a4ebfSFrançois Revol	/* these come from http://tools.ietf.org/html/rfc1436 */
550c1a4ebfSFrançois Revol	GOPHER_TYPE_TEXTPLAIN	= '0',	/**< text/plain */
560c1a4ebfSFrançois Revol	GOPHER_TYPE_DIRECTORY	= '1',	/**< gopher directory */
570c1a4ebfSFrançois Revol	GOPHER_TYPE_CSO_SEARCH	= '2',	/**< CSO search */
580c1a4ebfSFrançois Revol	GOPHER_TYPE_ERROR	= '3',	/**< error message */
590c1a4ebfSFrançois Revol	GOPHER_TYPE_BINHEX	= '4',	/**< binhex encoded text */
600c1a4ebfSFrançois Revol	GOPHER_TYPE_BINARCHIVE	= '5',	/**< binary archive file */
610c1a4ebfSFrançois Revol	GOPHER_TYPE_UUENCODED	= '6',	/**< uuencoded text */
620c1a4ebfSFrançois Revol	GOPHER_TYPE_QUERY	= '7',	/**< gopher search query */
630c1a4ebfSFrançois Revol	GOPHER_TYPE_TELNET	= '8',	/**< telnet link */
640c1a4ebfSFrançois Revol	GOPHER_TYPE_BINARY	= '9',	/**< generic binary */
650c1a4ebfSFrançois Revol	GOPHER_TYPE_DUPSERV	= '+',	/**< duplicated server */
660c1a4ebfSFrançois Revol	GOPHER_TYPE_GIF		= 'g',	/**< GIF image */
670c1a4ebfSFrançois Revol	GOPHER_TYPE_IMAGE	= 'I',	/**< image (depends, usually jpeg) */
680c1a4ebfSFrançois Revol	GOPHER_TYPE_TN3270	= 'T',	/**< tn3270 session */
690c1a4ebfSFrançois Revol	/* not standardized but widely used,
700c1a4ebfSFrançois Revol	 * cf. http://en.wikipedia.org/wiki/Gopher_%28protocol%29#Gopher_item_types
710c1a4ebfSFrançois Revol	 */
720c1a4ebfSFrançois Revol	GOPHER_TYPE_HTML	= 'h',	/**< HTML file or URL */
730c1a4ebfSFrançois Revol	GOPHER_TYPE_INFO	= 'i',	/**< information text */
740c1a4ebfSFrançois Revol	GOPHER_TYPE_AUDIO	= 's',	/**< audio (wav?) */
750c1a4ebfSFrançois Revol	/* not standardized, some servers use them */
7680be7c9dSFrançois Revol	GOPHER_TYPE_DOC		= 'd',	/**< gophernicus uses it for PS and PDF */
770c1a4ebfSFrançois Revol	GOPHER_TYPE_PNG		= 'p',	/**< PNG image */
780c1a4ebfSFrançois Revol		/* cf. gopher://namcub.accelera-labs.com/1/pics */
790c1a4ebfSFrançois Revol	GOPHER_TYPE_MIME	= 'M',	/**< multipart/mixed MIME data */
800c1a4ebfSFrançois Revol		/* cf. http://www.pms.ifi.lmu.de/mitarbeiter/ohlbach/multimedia/IT/IBMtutorial/3376c61.html */
810c1a4ebfSFrançois Revol	/* cf. http://nofixedpoint.motd.org/2011/02/22/an-introduction-to-the-gopher-protocol/ */
820c1a4ebfSFrançois Revol	GOPHER_TYPE_PDF		= 'P',	/**< PDF file */
830c1a4ebfSFrançois Revol	GOPHER_TYPE_BITMAP	= ':',	/**< Bitmap image (Gopher+) */
840c1a4ebfSFrançois Revol	GOPHER_TYPE_MOVIE	= ';',	/**< Movie (Gopher+) */
850c1a4ebfSFrançois Revol	GOPHER_TYPE_SOUND	= '<',	/**< Sound (Gopher+) */
860c1a4ebfSFrançois Revol	GOPHER_TYPE_CALENDAR	= 'c',	/**< Calendar */
870c1a4ebfSFrançois Revol	GOPHER_TYPE_EVENT	= 'e',	/**< Event */
880c1a4ebfSFrançois Revol	GOPHER_TYPE_MBOX	= 'm',	/**< mbox file */
890c1a4ebfSFrançois Revol} gopher_item_type;
900c1a4ebfSFrançois Revol
910c1a4ebfSFrançois Revol/** Types of fields in a line */
920c1a4ebfSFrançois Revoltypedef enum {
930c1a4ebfSFrançois Revol	FIELD_NAME,
940c1a4ebfSFrançois Revol	FIELD_SELECTOR,
950c1a4ebfSFrançois Revol	FIELD_HOST,
960c1a4ebfSFrançois Revol	FIELD_PORT,
970c1a4ebfSFrançois Revol	FIELD_GPFLAG,
980c1a4ebfSFrançois Revol	FIELD_EOL,
990c1a4ebfSFrançois Revol	FIELD_COUNT = FIELD_EOL
1000c1a4ebfSFrançois Revol} gopher_field;
1010c1a4ebfSFrançois Revol
1020c1a4ebfSFrançois Revol/** Map of gopher types to MIME types */
1030c1a4ebfSFrançois Revolstatic struct {
1040c1a4ebfSFrançois Revol	gopher_item_type type;
1050c1a4ebfSFrançois Revol	const char *mime;
1060c1a4ebfSFrançois Revol} gopher_type_map[] = {
1070c1a4ebfSFrançois Revol	/* these come from http://tools.ietf.org/html/rfc1436 */
1080c1a4ebfSFrançois Revol	{ GOPHER_TYPE_TEXTPLAIN, "text/plain" },
1090c1a4ebfSFrançois Revol	{ GOPHER_TYPE_DIRECTORY, "text/html;charset=UTF-8" },
1100c1a4ebfSFrançois Revol	{ GOPHER_TYPE_QUERY, "text/html;charset=UTF-8" },
1110c1a4ebfSFrançois Revol	{ GOPHER_TYPE_GIF, "image/gif" },
1120c1a4ebfSFrançois Revol	{ GOPHER_TYPE_HTML, "text/html" },
1130c1a4ebfSFrançois Revol	/* those are not standardized */
1140c1a4ebfSFrançois Revol	{ GOPHER_TYPE_PDF, "application/pdf" },
1150c1a4ebfSFrançois Revol	{ GOPHER_TYPE_PNG, "image/png"},
1160c1a4ebfSFrançois Revol	{ GOPHER_TYPE_NONE, NULL }
1170c1a4ebfSFrançois Revol};
1180c1a4ebfSFrançois Revol
1196983b35dSFrançois Revolstatic const char *kStyleSheet = "\n"
1206983b35dSFrançois Revol"/*\n"
1216983b35dSFrançois Revol" * gopher listing style\n"
1226983b35dSFrançois Revol" */\n"
1236983b35dSFrançois Revol"\n"
1246983b35dSFrançois Revol"body#gopher {\n"
1256983b35dSFrançois Revol"	/* margin: 10px;*/\n"
1266983b35dSFrançois Revol"	background-color: Window;\n"
1276983b35dSFrançois Revol"	color: WindowText;\n"
1286983b35dSFrançois Revol"	font-size: 100%;\n"
1296983b35dSFrançois Revol"	padding-bottom: 2em; }\n"
1306983b35dSFrançois Revol"\n"
1316983b35dSFrançois Revol"body#gopher div.uplink {\n"
1326983b35dSFrançois Revol"	padding: 0;\n"
1336983b35dSFrançois Revol"	margin: 0;\n"
1346983b35dSFrançois Revol"	position: fixed;\n"
1356983b35dSFrançois Revol"	top: 5px;\n"
1366983b35dSFrançois Revol"	right: 5px; }\n"
1376983b35dSFrançois Revol"\n"
1386983b35dSFrançois Revol"body#gopher h1 {\n"
1396983b35dSFrançois Revol"	padding: 5mm;\n"
1406983b35dSFrançois Revol"	margin: 0;\n"
1416983b35dSFrançois Revol"	border-bottom: 2px solid #777; }\n"
1426983b35dSFrançois Revol"\n"
1436983b35dSFrançois Revol"body#gopher span {\n"
1446983b35dSFrançois Revol"	margin-left: 1em;\n"
1456983b35dSFrançois Revol"	padding-left: 2em;\n"
1469a90ee3aSJérôme Duval"	font-family: 'Noto Mono', Courier, monospace;\n"
1476983b35dSFrançois Revol"	word-wrap: break-word;\n"
1486983b35dSFrançois Revol"	white-space: pre-wrap; }\n"
1496983b35dSFrançois Revol"\n"
1506983b35dSFrançois Revol"body#gopher span.error {\n"
1516983b35dSFrançois Revol"	color: #f00; }\n"
1526983b35dSFrançois Revol"\n"
1536983b35dSFrançois Revol"body#gopher span.unknown {\n"
1546983b35dSFrançois Revol"	color: #800; }\n"
1556983b35dSFrançois Revol"\n"
1566983b35dSFrançois Revol"body#gopher span.dir {\n"
1576983b35dSFrançois Revol"	background-image: url('resource:icons/directory.png');\n"
1586983b35dSFrançois Revol"	background-repeat: no-repeat;\n"
1596983b35dSFrançois Revol"	background-position: bottom left; }\n"
1606983b35dSFrançois Revol"\n"
1616983b35dSFrançois Revol"body#gopher span.text {\n"
1626983b35dSFrançois Revol"	background-image: url('resource:icons/content.png');\n"
1636983b35dSFrançois Revol"	background-repeat: no-repeat;\n"
1646983b35dSFrançois Revol"	background-position: bottom left; }\n"
1656983b35dSFrançois Revol"\n"
1666983b35dSFrançois Revol"body#gopher span.query {\n"
1676983b35dSFrançois Revol"	background-image: url('resource:icons/search.png');\n"
1686983b35dSFrançois Revol"	background-repeat: no-repeat;\n"
1696983b35dSFrançois Revol"	background-position: bottom left; }\n"
1706983b35dSFrançois Revol"\n"
1716983b35dSFrançois Revol"body#gopher span.img img {\n"
1726983b35dSFrançois Revol"	display: block;\n"
1736983b35dSFrançois Revol"	margin-left:auto;\n"
1746983b35dSFrançois Revol"	margin-right:auto; }\n";
1756983b35dSFrançois Revol
1760c1a4ebfSFrançois Revolstatic const int32 kGopherBufferSize = 4096;
1770c1a4ebfSFrançois Revol
1780e48c9aeSFrançois Revolstatic const bool kInlineImages = true;
1790c1a4ebfSFrançois Revol
1800c1a4ebfSFrançois Revol
1810c1a4ebfSFrançois RevolBGopherRequest::BGopherRequest(const BUrl& url, BUrlProtocolListener* listener,
1820c1a4ebfSFrançois Revol	BUrlContext* context)
1830c1a4ebfSFrançois Revol	:
1842f9b1874SAdrien Destugues	BNetworkRequest(url, listener, context, "BUrlProtocol.Gopher", "gopher"),
1850c1a4ebfSFrançois Revol	fItemType(GOPHER_TYPE_NONE),
1860c1a4ebfSFrançois Revol	fPosition(0),
1870c1a4ebfSFrançois Revol	fResult()
1880c1a4ebfSFrançois Revol{
1890c1a4ebfSFrançois Revol	fSocket = new(std::nothrow) BSocket();
1900c1a4ebfSFrançois Revol
1910c1a4ebfSFrançois Revol	fUrl.UrlDecode();
1920c1a4ebfSFrançois Revol	// the first part of the path is actually the document type
1930c1a4ebfSFrançois Revol
1940c1a4ebfSFrançois Revol	fPath = Url().Path();
19535edaf28SFrançois Revol	if (!Url().HasPath() || fPath.Length() == 0 || fPath == "/") {
1960c1a4ebfSFrançois Revol		// default entry
1970c1a4ebfSFrançois Revol		fItemType = GOPHER_TYPE_DIRECTORY;
1980c1a4ebfSFrançois Revol		fPath = "";
1990c1a4ebfSFrançois Revol	} else if (fPath.Length() > 1 && fPath[0] == '/') {
2000c1a4ebfSFrançois Revol		fItemType = fPath[1];
2010c1a4ebfSFrançois Revol		fPath.Remove(0, 2);
2020c1a4ebfSFrançois Revol	}
2030c1a4ebfSFrançois Revol}
2040c1a4ebfSFrançois Revol
2050c1a4ebfSFrançois Revol
2060c1a4ebfSFrançois RevolBGopherRequest::~BGopherRequest()
2070c1a4ebfSFrançois Revol{
2080c1a4ebfSFrançois Revol	Stop();
2090c1a4ebfSFrançois Revol
2100c1a4ebfSFrançois Revol	delete fSocket;
2110c1a4ebfSFrançois Revol}
2120c1a4ebfSFrançois Revol
2130c1a4ebfSFrançois Revol
2140c1a4ebfSFrançois Revolstatus_t
2150c1a4ebfSFrançois RevolBGopherRequest::Stop()
2160c1a4ebfSFrançois Revol{
2170c1a4ebfSFrançois Revol	if (fSocket != NULL) {
2180c1a4ebfSFrançois Revol		fSocket->Disconnect();
2190c1a4ebfSFrançois Revol			// Unlock any pending connect, read or write operation.
2200c1a4ebfSFrançois Revol	}
22189b4e98aSAdrien Destugues	return BNetworkRequest::Stop();
2220c1a4ebfSFrançois Revol}
2230c1a4ebfSFrançois Revol
2240c1a4ebfSFrançois Revol
2250c1a4ebfSFrançois Revolconst BUrlResult&
2260c1a4ebfSFrançois RevolBGopherRequest::Result() const
2270c1a4ebfSFrançois Revol{
2280c1a4ebfSFrançois Revol	return fResult;
2290c1a4ebfSFrançois Revol}
2300c1a4ebfSFrançois Revol
2310c1a4ebfSFrançois Revol
2320c1a4ebfSFrançois Revolstatus_t
2330c1a4ebfSFrançois RevolBGopherRequest::_ProtocolLoop()
2340c1a4ebfSFrançois Revol{
2350c1a4ebfSFrançois Revol	if (fSocket == NULL)
2360c1a4ebfSFrançois Revol		return B_NO_MEMORY;
2370c1a4ebfSFrançois Revol
238c98378e5SAdrien Destugues	if (!_ResolveHostName(fUrl.Host(), fUrl.HasPort() ? fUrl.Port() : 70)) {
2390c1a4ebfSFrançois Revol		_EmitDebug(B_URL_PROTOCOL_DEBUG_ERROR,
2400c1a4ebfSFrançois Revol			"Unable to resolve hostname (%s), aborting.",
2410c1a4ebfSFrançois Revol				fUrl.Host().String());
2420c1a4ebfSFrançois Revol		return B_SERVER_NOT_FOUND;
2430c1a4ebfSFrançois Revol	}
2440c1a4ebfSFrançois Revol
2450c1a4ebfSFrançois Revol	_EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, "Connection to %s on port %d.",
2460c1a4ebfSFrançois Revol		fUrl.Authority().String(), fRemoteAddr.Port());
2470c1a4ebfSFrançois Revol	status_t connectError = fSocket->Connect(fRemoteAddr);
2480c1a4ebfSFrançois Revol
2490c1a4ebfSFrançois Revol	if (connectError != B_OK) {
2500c1a4ebfSFrançois Revol		_EmitDebug(B_URL_PROTOCOL_DEBUG_ERROR, "Socket connection error %s",
2510c1a4ebfSFrançois Revol			strerror(connectError));
2520c1a4ebfSFrançois Revol		return connectError;
2530c1a4ebfSFrançois Revol	}
2540c1a4ebfSFrançois Revol
2550c1a4ebfSFrançois Revol	//! ProtocolHook:ConnectionOpened
2560c1a4ebfSFrançois Revol	if (fListener != NULL)
2570c1a4ebfSFrançois Revol		fListener->ConnectionOpened(this);
2580c1a4ebfSFrançois Revol
2590c1a4ebfSFrançois Revol	_EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT,
2600c1a4ebfSFrançois Revol		"Connection opened, sending request.");
2610c1a4ebfSFrançois Revol
2620c1a4ebfSFrançois Revol	_SendRequest();
2630c1a4ebfSFrançois Revol	_EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, "Request sent.");
2640c1a4ebfSFrançois Revol
2650c1a4ebfSFrançois Revol	// Receive loop
2660c1a4ebfSFrançois Revol	bool receiveEnd = false;
2670c1a4ebfSFrançois Revol	status_t readError = B_OK;
2680c1a4ebfSFrançois Revol	ssize_t bytesRead = 0;
2690c1a4ebfSFrançois Revol	//ssize_t bytesReceived = 0;
2700c1a4ebfSFrançois Revol	//ssize_t bytesTotal = 0;
2710c1a4ebfSFrançois Revol	bool dataValidated = false;
272111d695aSFrançois Revol	BStackOrHeapArray<char, 4096> chunk(kGopherBufferSize);
2730c1a4ebfSFrançois Revol
2740c1a4ebfSFrançois Revol	while (!fQuit && !receiveEnd) {
2750c1a4ebfSFrançois Revol		fSocket->WaitForReadable();
276111d695aSFrançois Revol		bytesRead = fSocket->Read(chunk, kGopherBufferSize);
2770c1a4ebfSFrançois Revol
2780c1a4ebfSFrançois Revol		if (bytesRead < 0) {
2790c1a4ebfSFrançois Revol			readError = bytesRead;
2800c1a4ebfSFrançois Revol			break;
2810c1a4ebfSFrançois Revol		} else if (bytesRead == 0)
2820c1a4ebfSFrançois Revol			receiveEnd = true;
2830c1a4ebfSFrançois Revol
284111d695aSFrançois Revol		fInputBuffer.AppendData(chunk, bytesRead);
2850c1a4ebfSFrançois Revol
2860c1a4ebfSFrançois Revol		if (!dataValidated) {
2870c1a4ebfSFrançois Revol			size_t i;
2880c1a4ebfSFrançois Revol			// on error (file doesn't exist, ...) the server sends
2890c1a4ebfSFrançois Revol			// a faked directory entry with an error message
2900c1a4ebfSFrançois Revol			if (fInputBuffer.Size() && fInputBuffer.Data()[0] == '3') {
2910c1a4ebfSFrançois Revol				int tabs = 0;
2920c1a4ebfSFrançois Revol				bool crlf = false;
2930c1a4ebfSFrançois Revol
2940c1a4ebfSFrançois Revol				// make sure the buffer only contains printable characters
2950c1a4ebfSFrançois Revol				// and has at least 3 tabs before a CRLF
2960c1a4ebfSFrançois Revol				for (i = 0; i < fInputBuffer.Size(); i++) {
2970c1a4ebfSFrançois Revol					char c = fInputBuffer.Data()[i];
2980c1a4ebfSFrançois Revol					if (c == '\t') {
2990c1a4ebfSFrançois Revol						if (!crlf)
3000c1a4ebfSFrançois Revol							tabs++;
3010c1a4ebfSFrançois Revol					} else if (c == '\r' || c == '\n') {
3020c1a4ebfSFrançois Revol						if (tabs < 3)
3030c1a4ebfSFrançois Revol							break;
3040c1a4ebfSFrançois Revol						crlf = true;
3050c1a4ebfSFrançois Revol					} else if (!isprint(fInputBuffer.Data()[i])) {
3060c1a4ebfSFrançois Revol						crlf = false;
3070c1a4ebfSFrançois Revol						break;
3080c1a4ebfSFrançois Revol					}
3090c1a4ebfSFrançois Revol				}
3100c1a4ebfSFrançois Revol				if (crlf && tabs > 2 && tabs < 5) {
3110c1a4ebfSFrançois Revol					// TODO:
3120c1a4ebfSFrançois Revol					//if enough data
3130c1a4ebfSFrançois Revol					// else continue
3140c1a4ebfSFrançois Revol					fItemType = GOPHER_TYPE_DIRECTORY;
315e243a034SFrançois Revol					readError = B_RESOURCE_NOT_FOUND;
3160c1a4ebfSFrançois Revol					// continue parsing the error text anyway
3170c1a4ebfSFrançois Revol				}
3180c1a4ebfSFrançois Revol			}
319b8be1867SFrançois Revol			// special case for buggy(?) Gophernicus/1.5
320b8be1867SFrançois Revol			static const char *buggy = "Error: File or directory not found!";
321b8be1867SFrançois Revol			if (fInputBuffer.Size() > strlen(buggy)
322b8be1867SFrançois Revol				&& !memcmp(fInputBuffer.Data(), buggy, strlen(buggy))) {
323b8be1867SFrançois Revol				fItemType = GOPHER_TYPE_DIRECTORY;
324b8be1867SFrançois Revol				readError = B_RESOURCE_NOT_FOUND;
325b8be1867SFrançois Revol				// continue parsing the error text anyway
326b8be1867SFrançois Revol				// but it won't look good
327b8be1867SFrançois Revol			}
3280c1a4ebfSFrançois Revol
3290c1a4ebfSFrançois Revol			// now we probably have correct data
3300c1a4ebfSFrançois Revol			dataValidated = true;
3310c1a4ebfSFrançois Revol
3320c1a4ebfSFrançois Revol			//! ProtocolHook:ResponseStarted
3330c1a4ebfSFrançois Revol			if (fListener != NULL)
3340c1a4ebfSFrançois Revol				fListener->ResponseStarted(this);
3350c1a4ebfSFrançois Revol
3360c1a4ebfSFrançois Revol			// now we can assign MIME type if we know it
337e95d0f00SFrançois Revol			const char *mime = "application/octet-stream";
3380c1a4ebfSFrançois Revol			for (i = 0; gopher_type_map[i].type != GOPHER_TYPE_NONE; i++) {
3390c1a4ebfSFrançois Revol				if (gopher_type_map[i].type == fItemType) {
340e95d0f00SFrançois Revol					mime = gopher_type_map[i].mime;
3410c1a4ebfSFrançois Revol					break;
3420c1a4ebfSFrançois Revol				}
3430c1a4ebfSFrançois Revol			}
344e95d0f00SFrançois Revol			fResult.SetContentType(mime);
345f9e1854fSAdrien Destugues
346f9e1854fSAdrien Destugues			// we don't really have headers but well...
347f9e1854fSAdrien Destugues			//! ProtocolHook:HeadersReceived
348f9e1854fSAdrien Destugues			if (fListener != NULL)
349f9e1854fSAdrien Destugues				fListener->HeadersReceived(this, fResult);
3500c1a4ebfSFrançois Revol		}
3510c1a4ebfSFrançois Revol
3520c1a4ebfSFrançois Revol		if (_NeedsParsing())
3530c1a4ebfSFrançois Revol			_ParseInput(receiveEnd);
3540c1a4ebfSFrançois Revol		else if (fInputBuffer.Size()) {
3550c1a4ebfSFrançois Revol			// send input directly
356e04f294fSPhilippe Saint-Pierre			if (fListener != NULL) {
357e04f294fSPhilippe Saint-Pierre				fListener->DataReceived(this, (const char *)fInputBuffer.Data(),
358e04f294fSPhilippe Saint-Pierre					fPosition, fInputBuffer.Size());
359e04f294fSPhilippe Saint-Pierre			}
3600c1a4ebfSFrançois Revol
3610c1a4ebfSFrançois Revol			fPosition += fInputBuffer.Size();
3620c1a4ebfSFrançois Revol
3630c1a4ebfSFrançois Revol			// XXX: this is plain stupid, we already copied the data
3640c1a4ebfSFrançois Revol			// and just want to drop it...
3650c1a4ebfSFrançois Revol			char *inputTempBuffer = new(std::nothrow) char[bytesRead];
3660c1a4ebfSFrançois Revol			if (inputTempBuffer == NULL) {
3670c1a4ebfSFrançois Revol				readError = B_NO_MEMORY;
3680c1a4ebfSFrançois Revol				break;
3690c1a4ebfSFrançois Revol			}
3700c1a4ebfSFrançois Revol			fInputBuffer.RemoveData(inputTempBuffer, fInputBuffer.Size());
3710c1a4ebfSFrançois Revol			delete[] inputTempBuffer;
3720c1a4ebfSFrançois Revol		}
3730c1a4ebfSFrançois Revol	}
3740c1a4ebfSFrançois Revol
3750c1a4ebfSFrançois Revol	if (fPosition > 0) {
3760c1a4ebfSFrançois Revol		fResult.SetLength(fPosition);
377e04f294fSPhilippe Saint-Pierre		if (fListener != NULL)
378e04f294fSPhilippe Saint-Pierre			fListener->DownloadProgress(this, fPosition, fPosition);
3790c1a4ebfSFrançois Revol	}
3800c1a4ebfSFrançois Revol
3810c1a4ebfSFrançois Revol	fSocket->Disconnect();
3820c1a4ebfSFrançois Revol
3830c1a4ebfSFrançois Revol	if (readError != B_OK)
3840c1a4ebfSFrançois Revol		return readError;
3850c1a4ebfSFrançois Revol
3860c1a4ebfSFrançois Revol	return fQuit ? B_INTERRUPTED : B_OK;
3870c1a4ebfSFrançois Revol}
3880c1a4ebfSFrançois Revol
3890c1a4ebfSFrançois Revol
3900c1a4ebfSFrançois Revolvoid
3910c1a4ebfSFrançois RevolBGopherRequest::_SendRequest()
3920c1a4ebfSFrançois Revol{
3930c1a4ebfSFrançois Revol	BString request;
3940c1a4ebfSFrançois Revol
3950c1a4ebfSFrançois Revol	request << fPath;
3960c1a4ebfSFrançois Revol
3970c1a4ebfSFrançois Revol	if (Url().HasRequest())
3980c1a4ebfSFrançois Revol		request << '\t' << Url().Request();
3990c1a4ebfSFrançois Revol
4000c1a4ebfSFrançois Revol	request << "\r\n";
4010c1a4ebfSFrançois Revol
4020c1a4ebfSFrançois Revol	fSocket->Write(request.String(), request.Length());
4030c1a4ebfSFrançois Revol}
4040c1a4ebfSFrançois Revol
4050c1a4ebfSFrançois Revol
4060c1a4ebfSFrançois Revolbool
4070c1a4ebfSFrançois RevolBGopherRequest::_NeedsParsing()
4080c1a4ebfSFrançois Revol{
4090c1a4ebfSFrançois Revol	if (fItemType == GOPHER_TYPE_DIRECTORY
4100c1a4ebfSFrançois Revol		|| fItemType == GOPHER_TYPE_QUERY)
4110c1a4ebfSFrançois Revol		return true;
4120c1a4ebfSFrançois Revol	return false;
4130c1a4ebfSFrançois Revol}
4140c1a4ebfSFrançois Revol
4150c1a4ebfSFrançois Revol
4160c1a4ebfSFrançois Revolbool
4170c1a4ebfSFrançois RevolBGopherRequest::_NeedsLastDotStrip()
4180c1a4ebfSFrançois Revol{
4190c1a4ebfSFrançois Revol	if (fItemType == GOPHER_TYPE_DIRECTORY
4200c1a4ebfSFrançois Revol		|| fItemType == GOPHER_TYPE_QUERY
4210c1a4ebfSFrançois Revol		|| fItemType == GOPHER_TYPE_TEXTPLAIN)
4220c1a4ebfSFrançois Revol		return true;
4230c1a4ebfSFrançois Revol	return false;
4240c1a4ebfSFrançois Revol}
4250c1a4ebfSFrançois Revol
4260c1a4ebfSFrançois Revol
4270c1a4ebfSFrançois Revolvoid
4280c1a4ebfSFrançois RevolBGopherRequest::_ParseInput(bool last)
4290c1a4ebfSFrançois Revol{
4300c1a4ebfSFrançois Revol	BString line;
4310c1a4ebfSFrançois Revol
4320c1a4ebfSFrançois Revol	while (_GetLine(line) == B_OK) {
4330c1a4ebfSFrançois Revol		char type = GOPHER_TYPE_NONE;
4340c1a4ebfSFrançois Revol		BStringList fields;
4350c1a4ebfSFrançois Revol
4360c1a4ebfSFrançois Revol		line.MoveInto(&type, 0, 1);
4370c1a4ebfSFrançois Revol
4380c1a4ebfSFrançois Revol		line.Split("\t", false, fields);
4390c1a4ebfSFrançois Revol
4400c1a4ebfSFrançois Revol		if (type != GOPHER_TYPE_ENDOFPAGE
4410c1a4ebfSFrançois Revol			&& fields.CountStrings() < FIELD_GPFLAG)
4420c1a4ebfSFrançois Revol			_EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT,
4430c1a4ebfSFrançois Revol				"Unterminated gopher item (type '%c')", type);
4440c1a4ebfSFrançois Revol
445f74e08fcSFrançois Revol		BString pageTitle;
4460c1a4ebfSFrançois Revol		BString item;
4470c1a4ebfSFrançois Revol		BString title = fields.StringAt(FIELD_NAME);
4480c1a4ebfSFrançois Revol		BString link("gopher://");
4490e48c9aeSFrançois Revol		BString user;
4500c1a4ebfSFrançois Revol		if (fields.CountStrings() > 3) {
4510c1a4ebfSFrançois Revol			link << fields.StringAt(FIELD_HOST);
4520c1a4ebfSFrançois Revol			if (fields.StringAt(FIELD_PORT).Length())
4530c1a4ebfSFrançois Revol				link << ":" << fields.StringAt(FIELD_PORT);
4540c1a4ebfSFrançois Revol			link << "/" << type;
4550c1a4ebfSFrançois Revol			//if (fields.StringAt(FIELD_SELECTOR).ByteAt(0) != '/')
4560c1a4ebfSFrançois Revol			//	link << "/";
4570c1a4ebfSFrançois Revol			link << fields.StringAt(FIELD_SELECTOR);
4580c1a4ebfSFrançois Revol		}
4590c1a4ebfSFrançois Revol		_HTMLEscapeString(title);
4600c1a4ebfSFrançois Revol		_HTMLEscapeString(link);
4610c1a4ebfSFrançois Revol
4620c1a4ebfSFrançois Revol		switch (type) {
4630c1a4ebfSFrançois Revol			case GOPHER_TYPE_ENDOFPAGE:
4640c1a4ebfSFrançois Revol				/* end of the page */
4650c1a4ebfSFrançois Revol				break;
4660c1a4ebfSFrançois Revol			case GOPHER_TYPE_TEXTPLAIN:
4670c1a4ebfSFrançois Revol				item << "<a href=\"" << link << "\">"
4680c1a4ebfSFrançois Revol						"<span class=\"text\">" << title << "</span></a>"
4690c1a4ebfSFrançois Revol						"<br/>\n";
4700c1a4ebfSFrançois Revol				break;
4712e8b8fd0SFrançois Revol			case GOPHER_TYPE_BINARY:
4722e8b8fd0SFrançois Revol			case GOPHER_TYPE_BINHEX:
4732e8b8fd0SFrançois Revol			case GOPHER_TYPE_BINARCHIVE:
4742e8b8fd0SFrançois Revol			case GOPHER_TYPE_UUENCODED:
4752e8b8fd0SFrançois Revol				item << "<a href=\"" << link << "\">"
4762e8b8fd0SFrançois Revol						"<span class=\"binary\">" << title << "</span></a>"
4772e8b8fd0SFrançois Revol						"<br/>\n";
4782e8b8fd0SFrançois Revol				break;
4792e8b8fd0SFrançois Revol			case GOPHER_TYPE_DIRECTORY:
4802e8b8fd0SFrançois Revol				/*
4812e8b8fd0SFrançois Revol				 * directory link
4822e8b8fd0SFrançois Revol				 */
4832e8b8fd0SFrançois Revol				item << "<a href=\"" << link << "\">"
4842e8b8fd0SFrançois Revol						"<span class=\"dir\">" << title << "</span></a>"
4852e8b8fd0SFrançois Revol						"<br/>\n";
4862e8b8fd0SFrançois Revol				break;
4872e8b8fd0SFrançois Revol			case GOPHER_TYPE_ERROR:
4882e8b8fd0SFrançois Revol				item << "<span class=\"error\">" << title << "</span>"
4892e8b8fd0SFrançois Revol						"<br/>\n";
490f74e08fcSFrançois Revol				if (fPosition == 0 && pageTitle.Length() == 0)
491f74e08fcSFrançois Revol					pageTitle << "Error: " << title;
492f74e08fcSFrançois Revol				break;
4930e48c9aeSFrançois Revol			case GOPHER_TYPE_QUERY:
4940e48c9aeSFrançois Revol				/* TODO: handle search better.
4950e48c9aeSFrançois Revol				 * For now we use an unnamed input field and accept sending ?=foo
4960e48c9aeSFrançois Revol				 * as it seems at least Veronica-2 ignores the = but it's unclean.
4970e48c9aeSFrançois Revol				 */
498ad3d3335SFrançois Revol				item << "<form method=\"get\" action=\"" << link << "\" "
499ad3d3335SFrançois Revol							"onsubmit=\"window.location = this.action + '?' + "
500ad3d3335SFrançois Revol								"this.elements['q'].value; return false;\">"
5010e48c9aeSFrançois Revol						"<span class=\"query\">"
5020e48c9aeSFrançois Revol						"<label>" << title << " "
503ad3d3335SFrançois Revol						"<input id=\"q\" name=\"\" type=\"text\" align=\"right\" />"
5040e48c9aeSFrançois Revol						"</label>"
5050e48c9aeSFrançois Revol						"</span></form>"
5060e48c9aeSFrançois Revol						"<br/>\n";
5070e48c9aeSFrançois Revol				break;
5080e48c9aeSFrançois Revol			case GOPHER_TYPE_TELNET:
5090e48c9aeSFrançois Revol				/* telnet: links
5100e48c9aeSFrançois Revol				 * cf. gopher://78.80.30.202/1/ps3
5110e48c9aeSFrançois Revol				 * -> gopher://78.80.30.202:23/8/ps3/new -> new@78.80.30.202
5120e48c9aeSFrançois Revol				 */
5130e48c9aeSFrançois Revol				link = "telnet://";
5140e48c9aeSFrançois Revol				user = fields.StringAt(FIELD_SELECTOR);
5150e48c9aeSFrançois Revol				if (user.FindLast('/') > -1) {
5160e48c9aeSFrançois Revol					user.Remove(0, user.FindLast('/'));
5170e48c9aeSFrançois Revol					link << user << "@";
5180e48c9aeSFrançois Revol				}
5190e48c9aeSFrançois Revol				link << fields.StringAt(FIELD_HOST);
5200e48c9aeSFrançois Revol				if (fields.StringAt(FIELD_PORT) != "23")
5210e48c9aeSFrançois Revol					link << ":" << fields.StringAt(FIELD_PORT);
5220e48c9aeSFrançois Revol
5230e48c9aeSFrançois Revol				item << "<a href=\"" << link << "\">"
5240e48c9aeSFrançois Revol						"<span class=\"telnet\">" << title << "</span></a>"
5250e48c9aeSFrançois Revol						"<br/>\n";
5260e48c9aeSFrançois Revol				break;
5270e48c9aeSFrançois Revol			case GOPHER_TYPE_TN3270:
5280e48c9aeSFrançois Revol				/* tn3270: URI scheme, cf. http://tools.ietf.org/html/rfc6270 */
5290e48c9aeSFrançois Revol				link = "tn3270://";
5300e48c9aeSFrançois Revol				user = fields.StringAt(FIELD_SELECTOR);
5310e48c9aeSFrançois Revol				if (user.FindLast('/') > -1) {
5320e48c9aeSFrançois Revol					user.Remove(0, user.FindLast('/'));
5330e48c9aeSFrançois Revol					link << user << "@";
5340e48c9aeSFrançois Revol				}
5350e48c9aeSFrançois Revol				link << fields.StringAt(FIELD_HOST);
5360e48c9aeSFrançois Revol				if (fields.StringAt(FIELD_PORT) != "23")
5370e48c9aeSFrançois Revol					link << ":" << fields.StringAt(FIELD_PORT);
5380e48c9aeSFrançois Revol
5390e48c9aeSFrançois Revol				item << "<a href=\"" << link << "\">"
5400e48c9aeSFrançois Revol						"<span class=\"telnet\">" << title << "</span></a>"
5410e48c9aeSFrançois Revol						"<br/>\n";
5420e48c9aeSFrançois Revol				break;
5430e48c9aeSFrançois Revol			case GOPHER_TYPE_CSO_SEARCH:
5440e48c9aeSFrançois Revol				/* CSO search.
5450e48c9aeSFrançois Revol				 * At least Lynx supports a cso:// URI scheme:
5460e48c9aeSFrançois Revol				 * http://lynx.isc.org/lynx2.8.5/lynx2-8-5/lynx_help/lynx_url_support.html
5470e48c9aeSFrançois Revol				 */
5480e48c9aeSFrançois Revol				link = "cso://";
5490e48c9aeSFrançois Revol				user = fields.StringAt(FIELD_SELECTOR);
5500e48c9aeSFrançois Revol				if (user.FindLast('/') > -1) {
5510e48c9aeSFrançois Revol					user.Remove(0, user.FindLast('/'));
5520e48c9aeSFrançois Revol					link << user << "@";
5530e48c9aeSFrançois Revol				}
5540e48c9aeSFrançois Revol				link << fields.StringAt(FIELD_HOST);
5550e48c9aeSFrançois Revol				if (fields.StringAt(FIELD_PORT) != "105")
5560e48c9aeSFrançois Revol					link << ":" << fields.StringAt(FIELD_PORT);
5570e48c9aeSFrançois Revol
5580e48c9aeSFrançois Revol				item << "<a href=\"" << link << "\">"
5590e48c9aeSFrançois Revol						"<span class=\"cso\">" << title << "</span></a>"
5600e48c9aeSFrançois Revol						"<br/>\n";
5610e48c9aeSFrançois Revol				break;
5620e48c9aeSFrançois Revol			case GOPHER_TYPE_GIF:
5630e48c9aeSFrançois Revol			case GOPHER_TYPE_IMAGE:
5640e48c9aeSFrançois Revol			case GOPHER_TYPE_PNG:
5650e48c9aeSFrançois Revol			case GOPHER_TYPE_BITMAP:
5660e48c9aeSFrançois Revol				/* quite dangerous, cf. gopher://namcub.accela-labs.com/1/pics */
5670e48c9aeSFrançois Revol				if (kInlineImages) {
5680e48c9aeSFrançois Revol					item << "<a href=\"" << link << "\">"
5690e48c9aeSFrançois Revol							"<span class=\"img\">" << title << " "
5700e48c9aeSFrançois Revol							"<img src=\"" << link << "\" "
5710e48c9aeSFrançois Revol								"alt=\"" << title << "\"/>"
5720e48c9aeSFrançois Revol							"</span></a>"
5730e48c9aeSFrançois Revol							"<br/>\n";
5740e48c9aeSFrançois Revol					break;
5750e48c9aeSFrançois Revol				}
5760e48c9aeSFrançois Revol				/* fallback to default, link them */
5770e48c9aeSFrançois Revol				item << "<a href=\"" << link << "\">"
5780e48c9aeSFrançois Revol						"<span class=\"img\">" << title << "</span></a>"
5790e48c9aeSFrançois Revol						"<br/>\n";
5800e48c9aeSFrançois Revol				break;
5810e48c9aeSFrançois Revol			case GOPHER_TYPE_HTML:
5820e48c9aeSFrançois Revol				/* cf. gopher://pineapple.vg/1 */
5830e48c9aeSFrançois Revol				if (fields.StringAt(FIELD_SELECTOR).StartsWith("URL:")) {
5840e48c9aeSFrançois Revol					link = fields.StringAt(FIELD_SELECTOR);
5850e48c9aeSFrançois Revol					link.Remove(0, 4);
5860e48c9aeSFrançois Revol				}
5870e48c9aeSFrançois Revol				/* cf. gopher://sdf.org/1/sdf/classes/ */
5880e48c9aeSFrançois Revol
5890e48c9aeSFrançois Revol				item << "<a href=\"" << link << "\">"
5900e48c9aeSFrançois Revol						"<span class=\"html\">" << title << "</span></a>"
5910e48c9aeSFrançois Revol						"<br/>\n";
5920e48c9aeSFrançois Revol				break;
593f74e08fcSFrançois Revol			case GOPHER_TYPE_INFO:
594f74e08fcSFrançois Revol				// TITLE resource, cf.
595f74e08fcSFrançois Revol				// gopher://gophernicus.org/0/doc/gopher/gopher-title-resource.txt
596f74e08fcSFrançois Revol				if (fPosition == 0 && pageTitle.Length() == 0
597f74e08fcSFrançois Revol					&& fields.StringAt(FIELD_SELECTOR) == "TITLE") {
598f74e08fcSFrançois Revol						pageTitle = title;
599f74e08fcSFrançois Revol						break;
600f74e08fcSFrançois Revol				}
601f74e08fcSFrançois Revol				item << "<span class=\"info\">" << title << "</span>"
602f74e08fcSFrançois Revol						"<br/>\n";
6032e8b8fd0SFrançois Revol				break;
604ec0e8153SFrançois Revol			case GOPHER_TYPE_AUDIO:
605ec0e8153SFrançois Revol			case GOPHER_TYPE_SOUND:
606ec0e8153SFrançois Revol				item << "<a href=\"" << link << "\">"
607ec0e8153SFrançois Revol						"<span class=\"audio\">" << title << "</span></a>"
608ec0e8153SFrançois Revol						"<audio src=\"" << link << "\" "
609579f9564SFrançois Revol							//TODO:Fix crash in WebPositive with these
610579f9564SFrançois Revol							//"controls=\"controls\" "
611579f9564SFrançois Revol							//"width=\"300\" height=\"50\" "
612ec0e8153SFrançois Revol							"alt=\"" << title << "\"/>"
613ec0e8153SFrançois Revol						"<span>[player]</span></audio>"
614ec0e8153SFrançois Revol						"<br/>\n";
615ec0e8153SFrançois Revol				break;
616ec0e8153SFrançois Revol			case GOPHER_TYPE_PDF:
61780be7c9dSFrançois Revol			case GOPHER_TYPE_DOC:
618ec0e8153SFrançois Revol				/* generic case for known-to-work items */
619ec0e8153SFrançois Revol				item << "<a href=\"" << link << "\">"
62080be7c9dSFrançois Revol						"<span class=\"document\">" << title << "</span></a>"
621ec0e8153SFrançois Revol						"<br/>\n";
622ec0e8153SFrançois Revol				break;
623ec0e8153SFrançois Revol			case GOPHER_TYPE_MOVIE:
624ec0e8153SFrançois Revol				item << "<a href=\"" << link << "\">"
625ec0e8153SFrançois Revol						"<span class=\"video\">" << title << "</span></a>"
626ec0e8153SFrançois Revol						"<video src=\"" << link << "\" "
627579f9564SFrançois Revol							//TODO:Fix crash in WebPositive with these
628579f9564SFrançois Revol							//"controls=\"controls\" "
629579f9564SFrançois Revol							//"width=\"300\" height=\"300\" "
630ec0e8153SFrançois Revol							"alt=\"" << title << "\"/>"
631ec0e8153SFrançois Revol						"<span>[player]</span></audio>"
632ec0e8153SFrançois Revol						"<br/>\n";
633ec0e8153SFrançois Revol				break;
6340c1a4ebfSFrançois Revol			default:
635ec0e8153SFrançois Revol				_EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT,
636ec0e8153SFrançois Revol					"Unknown gopher item (type 0x%02x '%c')", type, type);
637ec0e8153SFrançois Revol				item << "<a href=\"" << link << "\">"
638ec0e8153SFrançois Revol						"<span class=\"unknown\">" << title << "</span></a>"
639ec0e8153SFrançois Revol						"<br/>\n";
6400c1a4ebfSFrançois Revol				break;
6410c1a4ebfSFrançois Revol		}
6420c1a4ebfSFrançois Revol
6430c1a4ebfSFrançois Revol		if (fPosition == 0) {
644f74e08fcSFrançois Revol			if (pageTitle.Length() == 0)
645f74e08fcSFrançois Revol				pageTitle << "Index of " << Url();
646f74e08fcSFrançois Revol
6470c1a4ebfSFrançois Revol			const char *uplink = ".";
6480c1a4ebfSFrançois Revol			if (fPath.EndsWith("/"))
6490c1a4ebfSFrançois Revol				uplink = "..";
6500c1a4ebfSFrançois Revol
6510c1a4ebfSFrançois Revol			// emit header
6520c1a4ebfSFrançois Revol			BString header;
6530c1a4ebfSFrançois Revol			header <<
6540c1a4ebfSFrançois Revol				"<html>\n"
6550c1a4ebfSFrançois Revol				"<head>\n"
6560c1a4ebfSFrançois Revol				"<meta http-equiv=\"Content-Type\""
6570c1a4ebfSFrançois Revol					" content=\"text/html; charset=UTF-8\" />\n"
6580c1a4ebfSFrançois Revol				//FIXME: fix links
6596983b35dSFrançois Revol				//"<link rel=\"icon\" type=\"image/png\""
6606983b35dSFrançois Revol				//	" href=\"resource:icons/directory.png\">\n"
6616983b35dSFrançois Revol				"<style type=\"text/css\">\n" << kStyleSheet << "</style>\n"
662f74e08fcSFrançois Revol				"<title>" << pageTitle << "</title>\n"
6630c1a4ebfSFrançois Revol				"</head>\n"
6640c1a4ebfSFrançois Revol				"<body id=\"gopher\">\n"
6650c1a4ebfSFrançois Revol				"<div class=\"uplink dontprint\">\n"
6660c1a4ebfSFrançois Revol				"<a href=" << uplink << ">[up]</a>\n"
6670c1a4ebfSFrançois Revol				"<a href=\"/\">[top]</a>\n"
6680c1a4ebfSFrançois Revol				"</div>\n"
669f74e08fcSFrançois Revol				"<h1>" << pageTitle << "</h1>\n";
6700c1a4ebfSFrançois Revol
6710c1a4ebfSFrançois Revol			fListener->DataReceived(this, header.String(), fPosition,
6720c1a4ebfSFrançois Revol				header.Length());
6730c1a4ebfSFrançois Revol
6740c1a4ebfSFrançois Revol			fPosition += header.Length();
6750c1a4ebfSFrançois Revol		}
6760c1a4ebfSFrançois Revol
6770c1a4ebfSFrançois Revol		if (item.Length()) {
6780c1a4ebfSFrançois Revol			fListener->DataReceived(this, item.String(), fPosition,
6790c1a4ebfSFrançois Revol				item.Length());
6800c1a4ebfSFrançois Revol
6810c1a4ebfSFrançois Revol			fPosition += item.Length();
6820c1a4ebfSFrançois Revol		}
6830c1a4ebfSFrançois Revol	}
6840c1a4ebfSFrançois Revol
6850c1a4ebfSFrançois Revol	if (last) {
6860c1a4ebfSFrançois Revol		// emit footer
6870c1a4ebfSFrançois Revol		BString footer =
6880c1a4ebfSFrançois Revol			"</div>\n"
6890c1a4ebfSFrançois Revol			"</body>\n"
6900c1a4ebfSFrançois Revol			"</html>\n";
6910c1a4ebfSFrançois Revol
6920c1a4ebfSFrançois Revol		fListener->DataReceived(this, footer.String(), fPosition,
6930c1a4ebfSFrançois Revol			footer.Length());
6940c1a4ebfSFrançois Revol
6950c1a4ebfSFrançois Revol		fPosition += footer.Length();
6960c1a4ebfSFrançois Revol	}
6970c1a4ebfSFrançois Revol}
6980c1a4ebfSFrançois Revol
6990c1a4ebfSFrançois Revol
7000c1a4ebfSFrançois RevolBString&
7010c1a4ebfSFrançois RevolBGopherRequest::_HTMLEscapeString(BString &str)
7020c1a4ebfSFrançois Revol{
7030c1a4ebfSFrançois Revol	str.ReplaceAll("&", "&amp;");
7040c1a4ebfSFrançois Revol	str.ReplaceAll("<", "&lt;");
7050c1a4ebfSFrançois Revol	str.ReplaceAll(">", "&gt;");
7060c1a4ebfSFrançois Revol	return str;
7070c1a4ebfSFrançois Revol}
708