source: trunk/gsdl/src/recpt/cgiwrapper.cpp@ 2344

Last change on this file since 2344 was 2344, checked in by sjboddie, 21 years ago

Made the web library print some more reasonable debug info if gsdlhome
isn't set to a valid value

  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Revision
File size: 21.6 KB
Line 
1/**********************************************************************
2 *
3 * cgiwrapper.cpp -- output pages using the cgi protocol
4 * Copyright (C) 1999 The New Zealand Digital Library Project
5 *
6 * A component of the Greenstone digital library software
7 * from the New Zealand Digital Library Project at the
8 * University of Waikato, New Zealand.
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by
12 * the Free Software Foundation; either version 2 of the License, or
13 * (at your option) any later version.
14 *
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 * GNU General Public License for more details.
19 *
20 * You should have received a copy of the GNU General Public License
21 * along with this program; if not, write to the Free Software
22 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
23 *
24 *********************************************************************/
25
26#include "gsdlconf.h"
27#include "cgiwrapper.h"
28#include "recptconfig.h"
29#include "fileutil.h"
30#include <stdlib.h>
31#include <assert.h>
32
33#if defined(GSDL_USE_OBJECTSPACE)
34# include <ospace/std/iostream>
35# include <ospace/std/fstream>
36#elif defined(GSDL_USE_IOS_H)
37# include <iostream.h>
38# include <fstream.h>
39#else
40# include <iostream>
41# include <fstream>
42#endif
43
44#ifdef USE_FASTCGI
45#include "fcgiapp.h"
46#endif
47
48
49#ifdef USE_FASTCGI
50// used to output the text from receptionist
51class fcgistreambuf : public streambuf {
52public:
53 fcgistreambuf ();
54 int sync ();
55 int overflow (int ch);
56 int underflow () {return EOF;}
57
58 void fcgisbreset() {fcgx_stream = NULL; other_ostream = NULL;};
59 void set_fcgx_stream(FCGX_Stream *newone) {fcgx_stream=newone;};
60 void set_other_ostream(ostream *newone) {other_ostream=newone;};
61
62private:
63 FCGX_Stream *fcgx_stream;
64 ostream *other_ostream;
65};
66
67fcgistreambuf::fcgistreambuf() {
68 fcgisbreset();
69 if (base() == ebuf()) allocate();
70 setp (base(), ebuf());
71};
72
73int fcgistreambuf::sync () {
74 if ((fcgx_stream != NULL) &&
75 (FCGX_PutStr (pbase(), out_waiting(), fcgx_stream) < 0)) {
76 fcgx_stream = NULL;
77 }
78
79 if (other_ostream != NULL) {
80 char *thepbase=pbase();
81 for (int i=0;i<out_waiting();i++) (*other_ostream).put(thepbase[i]);
82 }
83
84 setp (pbase(), epptr());
85
86 return 0;
87}
88
89int fcgistreambuf::overflow (int ch) {
90 if (sync () == EOF) return EOF;
91 if (ch != EOF) sputc (ch);
92 return 0;
93}
94
95#endif
96
97static void format_error_string (text_t &errorpage, const text_t &errortext, bool debug) {
98
99 errorpage.clear();
100
101 if (debug) {
102 errorpage += "\n";
103 errorpage += "ERROR: " + errortext;
104 errorpage += "\n";
105
106 } else {
107
108 errorpage += "Content-type: text/html\n\n";
109
110 errorpage += "<html>\n";
111 errorpage += "<head>\n";
112 errorpage += "<title>Error</title>\n";
113 errorpage += "</head>\n";
114 errorpage += "<body>\n";
115 errorpage += "<h2>Oops!</h2>\n";
116 errorpage += errortext;
117 errorpage += "</body>\n";
118 errorpage += "</html>\n";
119 }
120}
121
122static void page_errorcollect (const text_t &gsdlhome, text_t &errorpage, bool debug) {
123
124 text_t collectdir = filename_cat (gsdlhome, "collect");
125
126 text_t errortext = "No valid collections were found: Check that your collect directory\n";
127 errortext += "(" + collectdir + ") is readable and contains at least one valid collection.\n";
128 errortext += "Note that modelcol is NOT a valid collection.\n";
129 errortext += "If the path to your collect directory is wrong edit the 'gsdlhome' field\n";
130 errortext += "in your gsdlsite.cfg configuration file.\n";
131
132 format_error_string (errorpage, errortext, debug);
133}
134
135static void page_errorsitecfg (text_t &errorpage, bool debug, int mode) {
136
137 text_t errortext;
138
139 if (mode == 0) {
140 errortext += "The gsdlsite.cfg configuration file could not be found. This\n";
141 errortext += "file should contain configuration information relating to this\n";
142 errortext += "site's setup.\n";
143
144 } else if (mode == 1) {
145 errortext += "The gsdlsite.cfg configuration file does not contain a valid\n";
146 errortext += "gsdlhome entry.\n";
147 }
148
149 if (debug) {
150 errortext += "gsdlsite.cfg should reside in the directory from which the\n";
151 errortext += "library executable was run.\n";
152 } else {
153 errortext += "gsdlsite.cfg should reside in the same directory as the library\n";
154 errortext += "executable file.\n";
155 }
156
157 format_error_string (errorpage, errortext, debug);
158}
159
160
161static void page_errormaincfg (const text_t &gsdlhome, const text_t &collection,
162 bool debug, text_t &errorpage) {
163
164 text_t errortext;
165
166 if (collection.empty()) {
167 text_t main_cfg_file = filename_cat (gsdlhome, "etc", "main.cfg");
168 errortext += "The main.cfg configuration file could not be found. This file\n";
169 errortext += "should contain configuration information relating to the\n";
170 errortext += "setup of the interface. As this receptionist is not being run\n";
171 errortext += "in collection specific mode the file should reside at\n";
172 errortext += main_cfg_file + ".\n";
173 } else {
174 text_t collect_cfg_file = filename_cat (gsdlhome, "collect", collection, "etc", "collect.cfg");
175 text_t main_collect_cfg_file = filename_cat (gsdlhome, "etc", "collect.cfg");
176 text_t main_cfg_file = filename_cat (gsdlhome, "etc", "main.cfg");
177 errortext += "Either the collect.cfg or main.cfg configuration file could\n";
178 errortext += "not be found. This file should contain configuration information\n";
179 errortext += "relating to the setup of the interface. As this receptionist is\n";
180 errortext += "being run in collection specific mode the file should reside\n";
181 errortext += "at either " + collect_cfg_file + ",\n";
182 errortext += main_collect_cfg_file + " or " + main_cfg_file + ".\n";
183 }
184
185 format_error_string (errorpage, errortext, debug);
186}
187
188
189static void page_errorinit (const text_t &gsdlhome, bool debug, text_t &errorpage) {
190
191 text_t errortext = "An error occurred during the initialisation of the Greenstone Digital\n";
192 errortext += "Library software. It is likely that the software has not been setup\n";
193 errortext += "correctly.\n";
194
195 text_t init_file = filename_cat (gsdlhome, "etc", "initout.txt");
196 char *ifile = init_file.getcstr();
197 ifstream initin (ifile);
198 delete ifile;
199 if (initin) {
200 errortext += "The initialisation error log, " + init_file + ", contains the\n";
201 errortext += "following information:\n\n";
202 if (!debug) errortext += "<pre>\n";
203
204 char c;
205 initin.get(c);
206 while (!initin.eof ()) {
207 errortext.push_back(c);
208 initin.get(c);
209 }
210
211 if (!debug) errortext += "</pre>\n";
212
213 initin.close();
214
215 } else {
216 errortext += "Please consult " + init_file + " for more information.\n";
217 }
218
219 format_error_string (errorpage, errortext, debug);
220}
221
222static void page_errorparseargs (const text_t &gsdlhome, bool debug, text_t &errorpage) {
223
224 text_t errortext = "An error occurred during the parsing of the cgi arguments.\n";
225
226 text_t error_file = filename_cat (gsdlhome, "etc", "errout.txt");
227 char *efile = error_file.getcstr();
228 ifstream errin (efile);
229 delete efile;
230 if (errin) {
231 errortext += "The error log, " + error_file + ", contains the\n";
232 errortext += "following information:\n\n";
233 if (!debug) errortext += "<pre>\n";
234
235 char c;
236 errin.get(c);
237 while (!errin.eof ()) {
238 errortext.push_back(c);
239 errin.get(c);
240 }
241 if (!debug) errortext += "</pre>\n";
242 errin.close();
243
244 } else {
245 errortext += "Please consult " + error_file + " for more information.\n";
246 }
247
248 format_error_string (errorpage, errortext, debug);
249}
250
251static void page_errorcgipage (const text_t &gsdlhome, bool debug, text_t &errorpage) {
252
253 text_t errortext = "An error occurred during the construction of the cgi page.\n";
254
255 text_t error_file = filename_cat (gsdlhome, "etc", "errout.txt");
256 char *efile = error_file.getcstr();
257 ifstream errin (efile);
258 delete efile;
259 if (errin) {
260 errortext += "The error log, " + error_file + ", contains the\n";
261 errortext += "following information:\n\n";
262 if (!debug) errortext += "<pre>\n";
263
264 char c;
265 errin.get(c);
266 while (!errin.eof ()) {
267 errortext.push_back(c);
268 errin.get(c);
269 }
270 if (!debug) errortext += "</pre>\n";
271 errin.close();
272
273 } else {
274 errortext += "Please consult " + error_file + " for more information.\n";
275 }
276
277 format_error_string (errorpage, errortext, debug);
278}
279
280static void print_debug_info (receptionist &recpt) {
281
282 outconvertclass text_t2ascii;
283 const recptconf &configinfo = recpt.get_configinfo ();
284 text_t etc_dir = filename_cat (configinfo.gsdlhome, "etc");
285
286 cout << "\n";
287 cout << text_t2ascii
288 << "------------------------------------------------------------\n"
289 << "Configuration and initialization completed successfully.\n"
290 << " Note that more debug information may be available in the\n"
291 << " initialization and error logs initout.txt and errout.txt\n"
292 << " in " << etc_dir << ".\n"
293 << "------------------------------------------------------------\n\n";
294
295 bool colspec = false;
296 if (configinfo.collection.empty()) {
297 cout << "Receptionist is running in \"general\" (i.e. not \"collection\n"
298 << "specific\") mode.\n";
299 } else {
300 cout << text_t2ascii
301 << "Receptionist is running in \"collection specific\" mode.\n"
302 << " collection=" << configinfo.collection << "\n"
303 << " collection directory=" << configinfo.collectdir << "\n";
304 colspec = true;
305 }
306
307 cout << text_t2ascii << "gsdlhome=" << configinfo.gsdlhome << "\n";
308 if (!configinfo.gdbmhome.empty())
309 cout << text_t2ascii << "gdbmhome=" << configinfo.gdbmhome << "\n";
310 cout << text_t2ascii << "httpprefix=" << configinfo.httpprefix << "\n";
311 cout << text_t2ascii << "httpimg=" << configinfo.httpimg << "\n";
312 cout << text_t2ascii << "gwcgi=" << configinfo.gwcgi << "\n"
313 << " Note that unless gwcgi has been set from a configuration\n"
314 << " file it is dependent on environment variables set by your\n"
315 << " webserver. Therefore it may not have the same value when run\n"
316 << " from the command line as it would be when run from your\n"
317 << " web server.\n";
318 if (configinfo.usecookies)
319 cout << "cookies are enabled\n";
320 else
321 cout << "cookies are disabled\n";
322 if (configinfo.logcgiargs)
323 cout << "logging is enabled\n";
324 else
325 cout << "logging is disabled\n";
326 cout << "------------------------------------------------------------\n\n";
327
328 text_tset::const_iterator this_mfile = configinfo.macrofiles.begin();
329 text_tset::const_iterator end_mfile = configinfo.macrofiles.end();
330 cout << "Macro Files:\n"
331 << "------------\n";
332 text_t mfile;
333 bool found;
334 while (this_mfile != end_mfile) {
335 cout << text_t2ascii << *this_mfile;
336 int spaces = (22 - (*this_mfile).size());
337 if (spaces < 2) spaces = 2;
338 text_t outspaces;
339 for (int i = 0; i < spaces; i++) outspaces.push_back (' ');
340 cout << text_t2ascii << outspaces;
341
342 found = false;
343 if (colspec) {
344 // collection specific - try collectdir/macros first
345 mfile = filename_cat (configinfo.collectdir, "macros", *this_mfile);
346 if (file_exists (mfile)) {
347 cout << text_t2ascii << "found (" << mfile << ")\n";
348 found = true;
349 }
350 }
351
352 if (!found) {
353 // try main macro directory
354 mfile = filename_cat (configinfo.gsdlhome, "macros", *this_mfile);
355 if (file_exists (mfile)) {
356 cout << text_t2ascii << "found (" << mfile << ")\n";
357 found = true;
358 }
359 }
360
361 if (!found)
362 cout << text_t2ascii << "NOT FOUND\n";
363
364 this_mfile ++;
365 }
366
367 cout << "------------------------------------------------------------\n\n"
368 << "Collections:\n"
369 << "------------\n"
370 << " Note that collections will only appear as \"running\" if\n"
371 << " their build.cfg files exist, are readable, contain a valid\n"
372 << " builddate field (i.e. > 0), and are in the collection's\n"
373 << " index directory (i.e. NOT the building directory)\n\n";
374
375 recptprotolistclass *protos = recpt.get_recptprotolist_ptr();
376 recptprotolistclass::iterator rprotolist_here = protos->begin();
377 recptprotolistclass::iterator rprotolist_end = protos->end();
378
379 bool is_z3950 = false;
380 bool found_valid_col = false;
381
382
383 while (rprotolist_here != rprotolist_end) {
384 comerror_t err;
385 if ((*rprotolist_here).p == NULL) continue;
386 else if (is_z3950==false &&
387 (*rprotolist_here).p->get_protocol_name(err) == "z3950proto") {
388 cout << "\nZ39.50 Servers: (always public)\n"
389 << "---------------\n";
390 is_z3950=true;
391 }
392
393 text_tarray collist;
394 (*rprotolist_here).p->get_collection_list (collist, err, cerr);
395 if (err == noError) {
396 text_tarray::iterator collist_here = collist.begin();
397 text_tarray::iterator collist_end = collist.end();
398
399 while (collist_here != collist_end) {
400
401 cout << text_t2ascii << *collist_here;
402
403 int spaces = (22 - (*collist_here).size());
404 if (spaces < 2) spaces = 2;
405 text_t outspaces;
406 for (int i = 0; i < spaces; i++) outspaces.push_back (' ');
407 cout << text_t2ascii << outspaces;
408
409 ColInfoResponse_t *cinfo = recpt.get_collectinfo_ptr ((*rprotolist_here).p, *collist_here, cerr);
410 if (cinfo != NULL) {
411 if (cinfo->isPublic) cout << "public ";
412 else cout << "private";
413
414 if (cinfo->buildDate > 0) {
415 cout << " running ";
416 found_valid_col = true;
417 } else {
418 cout << " not running";
419 }
420 }
421
422 cout << "\n";
423
424 collist_here ++;
425 }
426 }
427 is_z3950=false;
428 rprotolist_here ++;
429 } // end of while loop
430
431 if (!found_valid_col) {
432 cout << "WARNING: No \"running\" collections were found. You need to\n";
433 cout << " build one of the above collections\n";
434 }
435
436 cout << "\n------------------------------------------------------------\n";
437 cout << "------------------------------------------------------------\n\n";
438 cout << "receptionist running in command line debug mode\n";
439 cout << "enter cgi arguments as name=value pairs (e.g. 'a=p&p=home'):\n";
440
441}
442
443// cgiwrapper does everything necessary to output a page
444// using the cgi protocol. If this is being run for a particular
445// collection then "collection" should be set, otherwise it
446// should equal "".
447void cgiwrapper (receptionist &recpt, text_t collection) {
448
449 int numrequests = 0;
450 bool debug = false;
451 const recptconf &configinfo = recpt.get_configinfo ();
452
453 // find out whether this is being run as a cgi-script
454 // or a fastcgi script
455#ifdef USE_FASTCGI
456 fcgistreambuf outbuf;
457 int isfastcgi = !FCGX_IsCGI();
458 FCGX_Stream *fcgiin, *fcgiout, *fcgierr;
459 FCGX_ParamArray fcgienvp;
460#else
461 int isfastcgi = 0;
462#endif
463
464 // get the query string if it is not being run as a fastcgi
465 // script
466 text_t argstr = "";
467 cgiargsclass args;
468 char *aURIStr;
469 if (!isfastcgi) {
470 char *request_method_str = getenv("REQUEST_METHOD");
471 char *content_length_str = getenv("CONTENT_LENGTH");
472 if (request_method_str != NULL && strcmp(request_method_str, "POST") == 0 &&
473 content_length_str != NULL) {
474 // POST form data
475 int content_length = text_t(content_length_str).getint();
476 if (content_length > 0) {
477 char c;
478 do {
479 cin.get(c);
480 if (cin.eof()) break;
481 argstr.push_back (c);
482 content_length--;
483 } while (content_length > 0);
484 }
485
486 } else {
487 aURIStr = getenv("QUERY_STRING");
488 if ((request_method_str != NULL && strcmp(request_method_str, "GET") == 0)
489 || aURIStr != NULL) {
490 // GET form data
491 if (aURIStr != NULL) argstr = aURIStr;
492 } else {
493 // debugging from command line
494 debug = true;
495 }
496 }
497 }
498
499 if (debug) {
500 cout << "Configuring Greenstone...\n";
501 cout << flush;
502 }
503
504 // init stuff - we can't output error pages directly with
505 // fastcgi so the pages are stored until we can output them
506 text_t errorpage;
507 outconvertclass text_t2ascii;
508
509 // set defaults
510 int maxrequests = 10000;
511 recpt.configure ("collection", collection);
512 recpt.configure ("httpimg", "/gsdl/images");
513 char *script_name = getenv("SCRIPT_NAME");
514 if (script_name != NULL) recpt.configure("gwcgi", script_name);
515 else recpt.configure("gwcgi", "/gsdl");
516
517 // read in the configuration files.
518 text_t gsdlhome;
519 if (!site_cfg_read (recpt, gsdlhome, maxrequests)) {
520 // couldn't find the site configuration file
521 page_errorsitecfg (errorpage, debug, 0);
522 } else if (gsdlhome.empty()) {
523 // no gsdlhome in gsdlsite.cfg
524 page_errorsitecfg (errorpage, debug, 1);
525 } else if (!directory_exists(gsdlhome)) {
526 // gsdlhome not a valid directory
527 page_errorsitecfg (errorpage, debug, 1);
528 } else if (!main_cfg_read (recpt, gsdlhome, collection)) {
529 // couldn't find the main configuration file
530 page_errormaincfg (gsdlhome, collection, debug, errorpage);
531 } else if (configinfo.collectinfo.empty() && false) { // commented out for corba
532 // don't have any collections
533 page_errorcollect (gsdlhome, errorpage, debug);
534 }
535
536 if (errorpage.empty()) {
537
538 // initialise the library software
539 if (debug) {
540 cout << "Initializing...\n";
541 cout << flush;
542 }
543
544 text_t init_file = filename_cat (gsdlhome, "etc", "initout.txt");
545 char *iout = init_file.getcstr();
546 ofstream initout (iout);
547 delete iout;
548 if (!recpt.init(initout)) {
549 // an error occurred during the initialisation
550 initout.close();
551 page_errorinit(gsdlhome, debug, errorpage);
552 }
553 initout.close();
554 }
555
556 if (debug && errorpage.empty()) {
557 // get query string from command line
558 print_debug_info (recpt);
559 char cinURIStr[1024];
560 cin.get(cinURIStr, 1024);
561 argstr = cinURIStr;
562 }
563
564 // cgi scripts only deal with one request
565 if (!isfastcgi) maxrequests = 1;
566
567 // Page-request loop. If this is not being run as a fastcgi
568 // process then only one request will be processed and then
569 // the process will exit.
570 while (numrequests < maxrequests) {
571#ifdef USE_FASTCGI
572 if (isfastcgi) {
573 if (FCGX_Accept(&fcgiin, &fcgiout, &fcgierr, &fcgienvp) < 0) break;
574
575 char *request_method_str = FCGX_GetParam ("REQUEST_METHOD", fcgienvp);
576 char *content_length_str = FCGX_GetParam ("CONTENT_LENGTH", fcgienvp);
577
578 if (request_method_str != NULL && strcmp(request_method_str, "POST") == 0 &&
579 content_length_str != NULL) {
580 // POST form data
581 int content_length = text_t(content_length_str).getint();
582 if (content_length > 0) {
583 argstr.clear();
584 int c;
585 do {
586 c = FCGX_GetChar (fcgiin);
587 if (c < 0) break;
588 argstr.push_back (c);
589 content_length--;
590 } while (content_length > 0);
591 }
592
593 } else {
594 // GET form data
595 aURIStr = FCGX_GetParam("QUERY_STRING", fcgienvp);
596 if (aURIStr != NULL) argstr = aURIStr;
597 else argstr = "";
598 }
599 }
600#endif
601
602 // get output streams ready
603#ifdef USE_FASTCGI
604 outbuf.fcgisbreset ();
605 if (isfastcgi) outbuf.set_fcgx_stream (fcgiout);
606 else outbuf.set_other_ostream (&cout);
607 ostream pageout (&outbuf);
608#else
609#define pageout cout
610#endif
611
612 // if using fastcgi we'll load environment into a map,
613 // otherwise simply pass empty map (can't get environment
614 // variables using getenv() while using FCGX versions
615 // of fastcgi - at least I can't ;-) - Stefan)
616 text_tmap fastcgienv;
617#ifdef USE_FASTCGI
618 if (isfastcgi) {
619 for(; *fcgienvp != NULL; fcgienvp++) {
620 text_t fvalue = *fcgienvp;
621 text_t::const_iterator begin = fvalue.begin();
622 text_t::const_iterator end = fvalue.end();
623 text_t::const_iterator equals_sign = findchar (begin, end, '=');
624 if (equals_sign != end)
625 fastcgienv[substr(begin, equals_sign)] = substr(equals_sign+1, end);
626 }
627 }
628#endif
629
630 // temporarily need to configure gwcgi here when using fastcgi as I can't
631 // get it to pass the SCRIPT_NAME environment variable to the initial
632 // environment (if anyone can work out how to do this using the apache
633 // server, let me know). Note that this overrides the gwcgi field in
634 // site.cfg (which it shouldn't do) but I can't at present set gwcgi
635 // from site.cfg as I have old receptionists laying around that wouldn't
636 // appreciate it. The following 5 lines of code should be deleted once
637 // I either a: get the server to pass SCRIPT_NAME at initialization
638 // time or b: convert all the collections using old receptionists over
639 // to this version and uncomment gwcgi in the site.cfg file -- Stefan.
640#ifdef USE_FASTCGI
641 if (isfastcgi) {
642 recpt.configure("gwcgi", fastcgienv["SCRIPT_NAME"]);
643 }
644#endif
645
646
647 // if there has been no error so far, perform the production of the
648 // output page
649 if (errorpage.empty()) {
650 text_t error_file = filename_cat (gsdlhome, "etc", "errout.txt");
651 char *eout = error_file.getcstr();
652 ofstream errout (eout, ios::app);
653 delete eout;
654 // note that the following line appears to cause a runtime
655 // error using debug versions of VC++ 6.0 (on windows)
656 cerr = errout;
657
658 // parse the cgi arguments and produce the resulting page if there
659 // has been no errors so far
660 if (!recpt.parse_cgi_args (argstr, args, errout, fastcgienv)) {
661 errout.close ();
662 page_errorparseargs(gsdlhome, debug, errorpage);
663 } else {
664 // produce the output page
665
666 if (!recpt.produce_cgi_page (args, pageout, errout, fastcgienv)) {
667 errout.close ();
668 page_errorcgipage(gsdlhome, debug, errorpage);
669 }
670 recpt.log_cgi_args (args, errout, fastcgienv);
671 errout.close ();
672 }
673 }
674 // there was an error, output the error page
675 if (!errorpage.empty()) {
676 pageout << text_t2ascii << errorpage;
677 errorpage.clear();
678 numrequests = maxrequests; // make this the last page
679 }
680 pageout << flush;
681
682 // finish with the output streams
683#ifdef USE_FASTCGI
684 if (isfastcgi) FCGX_Finish();
685#endif
686
687 numrequests++;
688 }
689
690 return;
691}
Note: See TracBrowser for help on using the repository browser.