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("&", "&"); 745 str.ReplaceAll("<", "<"); 746 str.ReplaceAll(">", ">"); 747 return str; 748 } 749