xref: /haiku/src/kits/network/libnetservices/GopherRequest.cpp (revision 13581b3d2a71545960b98fefebc5225b5bf29072)
1 /*
2  * Copyright 2013-2014 Haiku Inc. All rights reserved.
3  * Distributed under the terms of the MIT License.
4  *
5  * Authors:
6  * 		François Revol, revol@free.fr
7  */
8 
9 
10 #include <assert.h>
11 #include <ctype.h>
12 #include <stdlib.h>
13 #include <stdio.h>
14 
15 #include <Directory.h>
16 #include <DynamicBuffer.h>
17 #include <File.h>
18 #include <GopherRequest.h>
19 #include <NodeInfo.h>
20 #include <Path.h>
21 #include <Socket.h>
22 #include <StackOrHeapArray.h>
23 #include <String.h>
24 #include <StringList.h>
25 
26 using namespace BPrivate::Network;
27 
28 
29 /*
30  * TODO: fix '+' in selectors, cf. gopher://gophernicus.org/1/doc/gopher/
31  * TODO: add proper favicon
32  * TODO: add proper dir and document icons
33  * TODO: correctly eat the extraneous .\r\n at end of text files
34  * TODO: move parsing stuff to a translator?
35  *
36  * docs:
37  * gopher://gopher.floodgap.com/1/gopher/tech
38  * gopher://gopher.floodgap.com/0/overbite/dbrowse?pluginm%201
39  *
40  * tests:
41  * gopher://sdf.org/1/sdf/historical	images
42  * gopher://gopher.r-36.net/1/	large photos
43  * gopher://sdf.org/1/sdf/classes	binaries
44  * gopher://sdf.org/1/users/	long page
45  * gopher://jgw.mdns.org/1/	search items
46  * gopher://jgw.mdns.org/1/MISC/	's' item (sound)
47  * gopher://gopher.floodgap.com/1/gopher	broken link
48  * gopher://sdf.org/1/maps/m	missing lines
49  * gopher://sdf.org/1/foo	gophernicus reports errors incorrectly
50  * gopher://gopher.floodgap.com/1/foo	correct error report
51  */
52 
53 /** Type of Gopher items */
54 typedef enum {
55 	GOPHER_TYPE_NONE	= 0,	/**< none set */
56 	GOPHER_TYPE_ENDOFPAGE	= '.',	/**< a dot alone on a line */
57 	/* these come from http://tools.ietf.org/html/rfc1436 */
58 	GOPHER_TYPE_TEXTPLAIN	= '0',	/**< text/plain */
59 	GOPHER_TYPE_DIRECTORY	= '1',	/**< gopher directory */
60 	GOPHER_TYPE_CSO_SEARCH	= '2',	/**< CSO search */
61 	GOPHER_TYPE_ERROR	= '3',	/**< error message */
62 	GOPHER_TYPE_BINHEX	= '4',	/**< binhex encoded text */
63 	GOPHER_TYPE_BINARCHIVE	= '5',	/**< binary archive file */
64 	GOPHER_TYPE_UUENCODED	= '6',	/**< uuencoded text */
65 	GOPHER_TYPE_QUERY	= '7',	/**< gopher search query */
66 	GOPHER_TYPE_TELNET	= '8',	/**< telnet link */
67 	GOPHER_TYPE_BINARY	= '9',	/**< generic binary */
68 	GOPHER_TYPE_DUPSERV	= '+',	/**< duplicated server */
69 	GOPHER_TYPE_GIF		= 'g',	/**< GIF image */
70 	GOPHER_TYPE_IMAGE	= 'I',	/**< image (depends, usually jpeg) */
71 	GOPHER_TYPE_TN3270	= 'T',	/**< tn3270 session */
72 	/* not standardized but widely used,
73 	 * cf. http://en.wikipedia.org/wiki/Gopher_%28protocol%29#Gopher_item_types
74 	 */
75 	GOPHER_TYPE_HTML	= 'h',	/**< HTML file or URL */
76 	GOPHER_TYPE_INFO	= 'i',	/**< information text */
77 	GOPHER_TYPE_AUDIO	= 's',	/**< audio (wav?) */
78 	/* not standardized, some servers use them */
79 	GOPHER_TYPE_DOC		= 'd',	/**< gophernicus uses it for PS and PDF */
80 	GOPHER_TYPE_PNG		= 'p',	/**< PNG image */
81 		/* cf. gopher://namcub.accelera-labs.com/1/pics */
82 	GOPHER_TYPE_MIME	= 'M',	/**< multipart/mixed MIME data */
83 		/* cf. http://www.pms.ifi.lmu.de/mitarbeiter/ohlbach/multimedia/IT/IBMtutorial/3376c61.html */
84 	/* cf. http://nofixedpoint.motd.org/2011/02/22/an-introduction-to-the-gopher-protocol/ */
85 	GOPHER_TYPE_PDF		= 'P',	/**< PDF file */
86 	GOPHER_TYPE_BITMAP	= ':',	/**< Bitmap image (Gopher+) */
87 	GOPHER_TYPE_MOVIE	= ';',	/**< Movie (Gopher+) */
88 	GOPHER_TYPE_SOUND	= '<',	/**< Sound (Gopher+) */
89 	GOPHER_TYPE_CALENDAR	= 'c',	/**< Calendar */
90 	GOPHER_TYPE_EVENT	= 'e',	/**< Event */
91 	GOPHER_TYPE_MBOX	= 'm',	/**< mbox file */
92 } gopher_item_type;
93 
94 /** Types of fields in a line */
95 typedef enum {
96 	FIELD_NAME,
97 	FIELD_SELECTOR,
98 	FIELD_HOST,
99 	FIELD_PORT,
100 	FIELD_GPFLAG,
101 	FIELD_EOL,
102 	FIELD_COUNT = FIELD_EOL
103 } gopher_field;
104 
105 /** Map of gopher types to MIME types */
106 static struct {
107 	gopher_item_type type;
108 	const char *mime;
109 } gopher_type_map[] = {
110 	/* these come from http://tools.ietf.org/html/rfc1436 */
111 	{ GOPHER_TYPE_TEXTPLAIN, "text/plain" },
112 	{ GOPHER_TYPE_DIRECTORY, "text/html;charset=UTF-8" },
113 	{ GOPHER_TYPE_QUERY, "text/html;charset=UTF-8" },
114 	{ GOPHER_TYPE_GIF, "image/gif" },
115 	{ GOPHER_TYPE_HTML, "text/html" },
116 	/* those are not standardized */
117 	{ GOPHER_TYPE_PDF, "application/pdf" },
118 	{ GOPHER_TYPE_PNG, "image/png"},
119 	{ GOPHER_TYPE_NONE, NULL }
120 };
121 
122 static const char *kStyleSheet = "\n"
123 "/*\n"
124 " * gopher listing style\n"
125 " */\n"
126 "\n"
127 "body#gopher {\n"
128 "	/* margin: 10px;*/\n"
129 "	background-color: Window;\n"
130 "	color: WindowText;\n"
131 "	font-size: 100%;\n"
132 "	padding-bottom: 2em; }\n"
133 "\n"
134 "body#gopher div.uplink {\n"
135 "	padding: 0;\n"
136 "	margin: 0;\n"
137 "	position: fixed;\n"
138 "	top: 5px;\n"
139 "	right: 5px; }\n"
140 "\n"
141 "body#gopher h1 {\n"
142 "	padding: 5mm;\n"
143 "	margin: 0;\n"
144 "	border-bottom: 2px solid #777; }\n"
145 "\n"
146 "body#gopher span {\n"
147 "	margin-left: 1em;\n"
148 "	padding-left: 2em;\n"
149 "	font-family: 'Noto Sans Mono', Courier, monospace;\n"
150 "	word-wrap: break-word;\n"
151 "	white-space: pre-wrap; }\n"
152 "\n"
153 "body#gopher span.error {\n"
154 "	color: #f00; }\n"
155 "\n"
156 "body#gopher span.unknown {\n"
157 "	color: #800; }\n"
158 "\n"
159 "body#gopher span.dir {\n"
160 "	background-image: url('resource:icons/directory.png');\n"
161 "	background-repeat: no-repeat;\n"
162 "	background-position: bottom left; }\n"
163 "\n"
164 "body#gopher span.text {\n"
165 "	background-image: url('resource:icons/content.png');\n"
166 "	background-repeat: no-repeat;\n"
167 "	background-position: bottom left; }\n"
168 "\n"
169 "body#gopher span.query {\n"
170 "	background-image: url('resource:icons/search.png');\n"
171 "	background-repeat: no-repeat;\n"
172 "	background-position: bottom left; }\n"
173 "\n"
174 "body#gopher span.img img {\n"
175 "	display: block;\n"
176 "	margin-left:auto;\n"
177 "	margin-right:auto; }\n";
178 
179 static const int32 kGopherBufferSize = 4096;
180 
181 static const bool kInlineImages = true;
182 
183 
184 BGopherRequest::BGopherRequest(const BUrl& url, BDataIO* output,
185 	BUrlProtocolListener* listener, BUrlContext* context)
186 	:
187 	BNetworkRequest(url, output, listener, context, "BUrlProtocol.Gopher",
188 		"gopher"),
189 	fItemType(GOPHER_TYPE_NONE),
190 	fPosition(0),
191 	fResult()
192 {
193 	fSocket = new(std::nothrow) BSocket();
194 
195 	fUrl.UrlDecode();
196 	// the first part of the path is actually the document type
197 
198 	fPath = Url().Path();
199 	if (!Url().HasPath() || fPath.Length() == 0 || fPath == "/") {
200 		// default entry
201 		fItemType = GOPHER_TYPE_DIRECTORY;
202 		fPath = "";
203 	} else if (fPath.Length() > 1 && fPath[0] == '/') {
204 		fItemType = fPath[1];
205 		fPath.Remove(0, 2);
206 	}
207 }
208 
209 
210 BGopherRequest::~BGopherRequest()
211 {
212 	Stop();
213 
214 	delete fSocket;
215 }
216 
217 
218 status_t
219 BGopherRequest::Stop()
220 {
221 	if (fSocket != NULL) {
222 		fSocket->Disconnect();
223 			// Unlock any pending connect, read or write operation.
224 	}
225 	return BNetworkRequest::Stop();
226 }
227 
228 
229 const BUrlResult&
230 BGopherRequest::Result() const
231 {
232 	return fResult;
233 }
234 
235 
236 status_t
237 BGopherRequest::_ProtocolLoop()
238 {
239 	if (fSocket == NULL)
240 		return B_NO_MEMORY;
241 
242 	if (!_ResolveHostName(fUrl.Host(), fUrl.HasPort() ? fUrl.Port() : 70)) {
243 		_EmitDebug(B_URL_PROTOCOL_DEBUG_ERROR,
244 			"Unable to resolve hostname (%s), aborting.",
245 				fUrl.Host().String());
246 		return B_SERVER_NOT_FOUND;
247 	}
248 
249 	_EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, "Connection to %s on port %d.",
250 		fUrl.Authority().String(), fRemoteAddr.Port());
251 	status_t connectError = fSocket->Connect(fRemoteAddr);
252 
253 	if (connectError != B_OK) {
254 		_EmitDebug(B_URL_PROTOCOL_DEBUG_ERROR, "Socket connection error %s",
255 			strerror(connectError));
256 		return connectError;
257 	}
258 
259 	//! ProtocolHook:ConnectionOpened
260 	if (fListener != NULL)
261 		fListener->ConnectionOpened(this);
262 
263 	_EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT,
264 		"Connection opened, sending request.");
265 
266 	_SendRequest();
267 	_EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT, "Request sent.");
268 
269 	// Receive loop
270 	bool receiveEnd = false;
271 	status_t readError = B_OK;
272 	ssize_t bytesRead = 0;
273 	//ssize_t bytesReceived = 0;
274 	//ssize_t bytesTotal = 0;
275 	bool dataValidated = false;
276 	BStackOrHeapArray<char, 4096> chunk(kGopherBufferSize);
277 
278 	while (!fQuit && !receiveEnd) {
279 		bytesRead = fSocket->Read(chunk, kGopherBufferSize);
280 
281 		if (bytesRead < 0) {
282 			readError = bytesRead;
283 			break;
284 		} else if (bytesRead == 0)
285 			receiveEnd = true;
286 
287 		fInputBuffer.AppendData(chunk, bytesRead);
288 
289 		if (!dataValidated) {
290 			size_t i;
291 			// on error (file doesn't exist, ...) the server sends
292 			// a faked directory entry with an error message
293 			if (fInputBuffer.Size() && fInputBuffer.Data()[0] == '3') {
294 				int tabs = 0;
295 				bool crlf = false;
296 
297 				// make sure the buffer only contains printable characters
298 				// and has at least 3 tabs before a CRLF
299 				for (i = 0; i < fInputBuffer.Size(); i++) {
300 					char c = fInputBuffer.Data()[i];
301 					if (c == '\t') {
302 						if (!crlf)
303 							tabs++;
304 					} else if (c == '\r' || c == '\n') {
305 						if (tabs < 3)
306 							break;
307 						crlf = true;
308 					} else if (!isprint(fInputBuffer.Data()[i])) {
309 						crlf = false;
310 						break;
311 					}
312 				}
313 				if (crlf && tabs > 2 && tabs < 5) {
314 					// TODO:
315 					//if enough data
316 					// else continue
317 					fItemType = GOPHER_TYPE_DIRECTORY;
318 					readError = B_RESOURCE_NOT_FOUND;
319 					// continue parsing the error text anyway
320 				}
321 			}
322 			// special case for buggy(?) Gophernicus/1.5
323 			static const char *buggy = "Error: File or directory not found!";
324 			if (fInputBuffer.Size() > strlen(buggy)
325 				&& !memcmp(fInputBuffer.Data(), buggy, strlen(buggy))) {
326 				fItemType = GOPHER_TYPE_DIRECTORY;
327 				readError = B_RESOURCE_NOT_FOUND;
328 				// continue parsing the error text anyway
329 				// but it won't look good
330 			}
331 
332 			// now we probably have correct data
333 			dataValidated = true;
334 
335 			//! ProtocolHook:ResponseStarted
336 			if (fListener != NULL)
337 				fListener->ResponseStarted(this);
338 
339 			// now we can assign MIME type if we know it
340 			const char *mime = "application/octet-stream";
341 			for (i = 0; gopher_type_map[i].type != GOPHER_TYPE_NONE; i++) {
342 				if (gopher_type_map[i].type == fItemType) {
343 					mime = gopher_type_map[i].mime;
344 					break;
345 				}
346 			}
347 			fResult.SetContentType(mime);
348 
349 			// we don't really have headers but well...
350 			//! ProtocolHook:HeadersReceived
351 			if (fListener != NULL)
352 				fListener->HeadersReceived(this);
353 		}
354 
355 		if (_NeedsParsing())
356 			readError = _ParseInput(receiveEnd);
357 		else if (fInputBuffer.Size()) {
358 			// send input directly
359 			if (fOutput != NULL) {
360 				size_t written = 0;
361 				readError = fOutput->WriteExactly(
362 					(const char*)fInputBuffer.Data(), fInputBuffer.Size(),
363 					&written);
364 				if (fListener != NULL && written > 0)
365 					fListener->BytesWritten(this, written);
366 				if (readError != B_OK)
367 					break;
368 			}
369 
370 			fPosition += fInputBuffer.Size();
371 
372 			if (fListener != NULL)
373 				fListener->DownloadProgress(this, fPosition, 0);
374 
375 			// XXX: this is plain stupid, we already copied the data
376 			// and just want to drop it...
377 			char *inputTempBuffer = new(std::nothrow) char[bytesRead];
378 			if (inputTempBuffer == NULL) {
379 				readError = B_NO_MEMORY;
380 				break;
381 			}
382 			fInputBuffer.RemoveData(inputTempBuffer, fInputBuffer.Size());
383 			delete[] inputTempBuffer;
384 		}
385 	}
386 
387 	if (fPosition > 0)
388 		fResult.SetLength(fPosition);
389 
390 	fSocket->Disconnect();
391 
392 	if (readError != B_OK)
393 		return readError;
394 
395 	return fQuit ? B_INTERRUPTED : B_OK;
396 }
397 
398 
399 void
400 BGopherRequest::_SendRequest()
401 {
402 	BString request;
403 
404 	request << fPath;
405 
406 	if (Url().HasRequest())
407 		request << '\t' << Url().Request();
408 
409 	request << "\r\n";
410 
411 	fSocket->Write(request.String(), request.Length());
412 }
413 
414 
415 bool
416 BGopherRequest::_NeedsParsing()
417 {
418 	if (fItemType == GOPHER_TYPE_DIRECTORY
419 		|| fItemType == GOPHER_TYPE_QUERY)
420 		return true;
421 	return false;
422 }
423 
424 
425 bool
426 BGopherRequest::_NeedsLastDotStrip()
427 {
428 	if (fItemType == GOPHER_TYPE_DIRECTORY
429 		|| fItemType == GOPHER_TYPE_QUERY
430 		|| fItemType == GOPHER_TYPE_TEXTPLAIN)
431 		return true;
432 	return false;
433 }
434 
435 
436 status_t
437 BGopherRequest::_ParseInput(bool last)
438 {
439 	BString line;
440 
441 	while (_GetLine(line) == B_OK) {
442 		char type = GOPHER_TYPE_NONE;
443 		BStringList fields;
444 
445 		line.MoveInto(&type, 0, 1);
446 
447 		line.Split("\t", false, fields);
448 
449 		if (type != GOPHER_TYPE_ENDOFPAGE
450 			&& fields.CountStrings() < FIELD_GPFLAG)
451 			_EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT,
452 				"Unterminated gopher item (type '%c')", type);
453 
454 		BString pageTitle;
455 		BString item;
456 		BString title = fields.StringAt(FIELD_NAME);
457 		BString link("gopher://");
458 		BString user;
459 		if (fields.CountStrings() > 3) {
460 			link << fields.StringAt(FIELD_HOST);
461 			if (fields.StringAt(FIELD_PORT).Length())
462 				link << ":" << fields.StringAt(FIELD_PORT);
463 			link << "/" << type;
464 			//if (fields.StringAt(FIELD_SELECTOR).ByteAt(0) != '/')
465 			//	link << "/";
466 			link << fields.StringAt(FIELD_SELECTOR);
467 		}
468 		_HTMLEscapeString(title);
469 		_HTMLEscapeString(link);
470 
471 		switch (type) {
472 			case GOPHER_TYPE_ENDOFPAGE:
473 				/* end of the page */
474 				break;
475 			case GOPHER_TYPE_TEXTPLAIN:
476 				item << "<a href=\"" << link << "\">"
477 						"<span class=\"text\">" << title << "</span></a>"
478 						"<br/>\n";
479 				break;
480 			case GOPHER_TYPE_BINARY:
481 			case GOPHER_TYPE_BINHEX:
482 			case GOPHER_TYPE_BINARCHIVE:
483 			case GOPHER_TYPE_UUENCODED:
484 				item << "<a href=\"" << link << "\">"
485 						"<span class=\"binary\">" << title << "</span></a>"
486 						"<br/>\n";
487 				break;
488 			case GOPHER_TYPE_DIRECTORY:
489 				/*
490 				 * directory link
491 				 */
492 				item << "<a href=\"" << link << "\">"
493 						"<span class=\"dir\">" << title << "</span></a>"
494 						"<br/>\n";
495 				break;
496 			case GOPHER_TYPE_ERROR:
497 				item << "<span class=\"error\">" << title << "</span>"
498 						"<br/>\n";
499 				if (fPosition == 0 && pageTitle.Length() == 0)
500 					pageTitle << "Error: " << title;
501 				break;
502 			case GOPHER_TYPE_QUERY:
503 				/* TODO: handle search better.
504 				 * For now we use an unnamed input field and accept sending ?=foo
505 				 * as it seems at least Veronica-2 ignores the = but it's unclean.
506 				 */
507 				item << "<form method=\"get\" action=\"" << link << "\" "
508 							"onsubmit=\"window.location = this.action + '?' + "
509 								"this.elements['q'].value; return false;\">"
510 						"<span class=\"query\">"
511 						"<label>" << title << " "
512 						"<input id=\"q\" name=\"\" type=\"text\" align=\"right\" />"
513 						"</label>"
514 						"</span></form>"
515 						"<br/>\n";
516 				break;
517 			case GOPHER_TYPE_TELNET:
518 				/* telnet: links
519 				 * cf. gopher://78.80.30.202/1/ps3
520 				 * -> gopher://78.80.30.202:23/8/ps3/new -> new@78.80.30.202
521 				 */
522 				link = "telnet://";
523 				user = fields.StringAt(FIELD_SELECTOR);
524 				if (user.FindLast('/') > -1) {
525 					user.Remove(0, user.FindLast('/'));
526 					link << user << "@";
527 				}
528 				link << fields.StringAt(FIELD_HOST);
529 				if (fields.StringAt(FIELD_PORT) != "23")
530 					link << ":" << fields.StringAt(FIELD_PORT);
531 
532 				item << "<a href=\"" << link << "\">"
533 						"<span class=\"telnet\">" << title << "</span></a>"
534 						"<br/>\n";
535 				break;
536 			case GOPHER_TYPE_TN3270:
537 				/* tn3270: URI scheme, cf. http://tools.ietf.org/html/rfc6270 */
538 				link = "tn3270://";
539 				user = fields.StringAt(FIELD_SELECTOR);
540 				if (user.FindLast('/') > -1) {
541 					user.Remove(0, user.FindLast('/'));
542 					link << user << "@";
543 				}
544 				link << fields.StringAt(FIELD_HOST);
545 				if (fields.StringAt(FIELD_PORT) != "23")
546 					link << ":" << fields.StringAt(FIELD_PORT);
547 
548 				item << "<a href=\"" << link << "\">"
549 						"<span class=\"telnet\">" << title << "</span></a>"
550 						"<br/>\n";
551 				break;
552 			case GOPHER_TYPE_CSO_SEARCH:
553 				/* CSO search.
554 				 * At least Lynx supports a cso:// URI scheme:
555 				 * http://lynx.isc.org/lynx2.8.5/lynx2-8-5/lynx_help/lynx_url_support.html
556 				 */
557 				link = "cso://";
558 				user = fields.StringAt(FIELD_SELECTOR);
559 				if (user.FindLast('/') > -1) {
560 					user.Remove(0, user.FindLast('/'));
561 					link << user << "@";
562 				}
563 				link << fields.StringAt(FIELD_HOST);
564 				if (fields.StringAt(FIELD_PORT) != "105")
565 					link << ":" << fields.StringAt(FIELD_PORT);
566 
567 				item << "<a href=\"" << link << "\">"
568 						"<span class=\"cso\">" << title << "</span></a>"
569 						"<br/>\n";
570 				break;
571 			case GOPHER_TYPE_GIF:
572 			case GOPHER_TYPE_IMAGE:
573 			case GOPHER_TYPE_PNG:
574 			case GOPHER_TYPE_BITMAP:
575 				/* quite dangerous, cf. gopher://namcub.accela-labs.com/1/pics */
576 				if (kInlineImages) {
577 					item << "<a href=\"" << link << "\">"
578 							"<span class=\"img\">" << title << " "
579 							"<img src=\"" << link << "\" "
580 								"alt=\"" << title << "\"/>"
581 							"</span></a>"
582 							"<br/>\n";
583 					break;
584 				}
585 				/* fallback to default, link them */
586 				item << "<a href=\"" << link << "\">"
587 						"<span class=\"img\">" << title << "</span></a>"
588 						"<br/>\n";
589 				break;
590 			case GOPHER_TYPE_HTML:
591 				/* cf. gopher://pineapple.vg/1 */
592 				if (fields.StringAt(FIELD_SELECTOR).StartsWith("URL:")) {
593 					link = fields.StringAt(FIELD_SELECTOR);
594 					link.Remove(0, 4);
595 				}
596 				/* cf. gopher://sdf.org/1/sdf/classes/ */
597 
598 				item << "<a href=\"" << link << "\">"
599 						"<span class=\"html\">" << title << "</span></a>"
600 						"<br/>\n";
601 				break;
602 			case GOPHER_TYPE_INFO:
603 				// TITLE resource, cf.
604 				// gopher://gophernicus.org/0/doc/gopher/gopher-title-resource.txt
605 				if (fPosition == 0 && pageTitle.Length() == 0
606 					&& fields.StringAt(FIELD_SELECTOR) == "TITLE") {
607 						pageTitle = title;
608 						break;
609 				}
610 				item << "<span class=\"info\">" << title << "</span>"
611 						"<br/>\n";
612 				break;
613 			case GOPHER_TYPE_AUDIO:
614 			case GOPHER_TYPE_SOUND:
615 				item << "<a href=\"" << link << "\">"
616 						"<span class=\"audio\">" << title << "</span></a>"
617 						"<audio src=\"" << link << "\" "
618 							//TODO:Fix crash in WebPositive with these
619 							//"controls=\"controls\" "
620 							//"width=\"300\" height=\"50\" "
621 							"alt=\"" << title << "\"/>"
622 						"<span>[player]</span></audio>"
623 						"<br/>\n";
624 				break;
625 			case GOPHER_TYPE_PDF:
626 			case GOPHER_TYPE_DOC:
627 				/* generic case for known-to-work items */
628 				item << "<a href=\"" << link << "\">"
629 						"<span class=\"document\">" << title << "</span></a>"
630 						"<br/>\n";
631 				break;
632 			case GOPHER_TYPE_MOVIE:
633 				item << "<a href=\"" << link << "\">"
634 						"<span class=\"video\">" << title << "</span></a>"
635 						"<video src=\"" << link << "\" "
636 							//TODO:Fix crash in WebPositive with these
637 							//"controls=\"controls\" "
638 							//"width=\"300\" height=\"300\" "
639 							"alt=\"" << title << "\"/>"
640 						"<span>[player]</span></audio>"
641 						"<br/>\n";
642 				break;
643 			default:
644 				_EmitDebug(B_URL_PROTOCOL_DEBUG_TEXT,
645 					"Unknown gopher item (type 0x%02x '%c')", type, type);
646 				item << "<a href=\"" << link << "\">"
647 						"<span class=\"unknown\">" << title << "</span></a>"
648 						"<br/>\n";
649 				break;
650 		}
651 
652 		if (fPosition == 0) {
653 			if (pageTitle.Length() == 0)
654 				pageTitle << "Index of " << Url();
655 
656 			const char *uplink = ".";
657 			if (fPath.EndsWith("/"))
658 				uplink = "..";
659 
660 			// emit header
661 			BString header;
662 			header <<
663 				"<html>\n"
664 				"<head>\n"
665 				"<meta http-equiv=\"Content-Type\""
666 					" content=\"text/html; charset=UTF-8\" />\n"
667 				//FIXME: fix links
668 				//"<link rel=\"icon\" type=\"image/png\""
669 				//	" href=\"resource:icons/directory.png\">\n"
670 				"<style type=\"text/css\">\n" << kStyleSheet << "</style>\n"
671 				"<title>" << pageTitle << "</title>\n"
672 				"</head>\n"
673 				"<body id=\"gopher\">\n"
674 				"<div class=\"uplink dontprint\">\n"
675 				"<a href=" << uplink << ">[up]</a>\n"
676 				"<a href=\"/\">[top]</a>\n"
677 				"</div>\n"
678 				"<h1>" << pageTitle << "</h1>\n";
679 
680 			if (fOutput != NULL) {
681 				size_t written = 0;
682 				status_t error = fOutput->WriteExactly(header.String(),
683 					header.Length(), &written);
684 				if (fListener != NULL && written > 0)
685 					fListener->BytesWritten(this, written);
686 				if (error != B_OK)
687 					return error;
688 			}
689 
690 			fPosition += header.Length();
691 
692 			if (fListener != NULL)
693 				fListener->DownloadProgress(this, fPosition, 0);
694 		}
695 
696 		if (item.Length()) {
697 			if (fOutput != NULL) {
698 				size_t written = 0;
699 				status_t error = fOutput->WriteExactly(item.String(),
700 					item.Length(), &written);
701 				if (fListener != NULL && written > 0)
702 					fListener->BytesWritten(this, written);
703 				if (error != B_OK)
704 					return error;
705 			}
706 
707 			fPosition += item.Length();
708 
709 			if (fListener != NULL)
710 				fListener->DownloadProgress(this, fPosition, 0);
711 		}
712 	}
713 
714 	if (last) {
715 		// emit footer
716 		BString footer =
717 			"</div>\n"
718 			"</body>\n"
719 			"</html>\n";
720 
721 		if (fListener != NULL) {
722 			size_t written = 0;
723 			status_t error = fOutput->WriteExactly(footer.String(),
724 				footer.Length(), &written);
725 			if (fListener != NULL && written > 0)
726 				fListener->BytesWritten(this, written);
727 			if (error != B_OK)
728 				return error;
729 		}
730 
731 		fPosition += footer.Length();
732 
733 		if (fListener != NULL)
734 			fListener->DownloadProgress(this, fPosition, 0);
735 	}
736 
737 	return B_OK;
738 }
739 
740 
741 BString&
742 BGopherRequest::_HTMLEscapeString(BString &str)
743 {
744 	str.ReplaceAll("&", "&amp;");
745 	str.ReplaceAll("<", "&lt;");
746 	str.ReplaceAll(">", "&gt;");
747 	return str;
748 }
749