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
BGopherRequest(const BUrl & url,BDataIO * output,BUrlProtocolListener * listener,BUrlContext * context)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
~BGopherRequest()210 BGopherRequest::~BGopherRequest()
211 {
212 Stop();
213
214 delete fSocket;
215 }
216
217
218 status_t
Stop()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&
Result() const230 BGopherRequest::Result() const
231 {
232 return fResult;
233 }
234
235
236 status_t
_ProtocolLoop()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
_SendRequest()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
_NeedsParsing()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
_NeedsLastDotStrip()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
_ParseInput(bool last)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&
_HTMLEscapeString(BString & str)742 BGopherRequest::_HTMLEscapeString(BString &str)
743 {
744 str.ReplaceAll("&", "&");
745 str.ReplaceAll("<", "<");
746 str.ReplaceAll(">", ">");
747 return str;
748 }
749