source: trunk/gsdl/src/recpt/receptionist.cpp@ 370

Last change on this file since 370 was 366, checked in by rjmcnab, 25 years ago

Stored origin of cgiarg with argument.

  • Property svn:executable set to *
  • Property svn:keywords set to Author Date Id Revision
File size: 22.0 KB
Line 
1/**********************************************************************
2 *
3 * receptionist.cpp -- a web interface for the gsdl
4 * Copyright (C) 1999 The New Zealand Digital Library Project
5 *
6 * PUT COPYRIGHT NOTICE HERE
7 *
8 * $Id: receptionist.cpp 366 1999-07-11 01:05:20Z rjmcnab $
9 *
10 *********************************************************************/
11
12/*
13 $Log$
14 Revision 1.18 1999/07/11 01:05:20 rjmcnab
15 Stored origin of cgiarg with argument.
16
17 Revision 1.17 1999/07/10 22:18:26 rjmcnab
18 Added calls to define_external_cgiargs.
19
20 Revision 1.16 1999/06/27 21:49:03 sjboddie
21 fixed a couple of version conflicts - tidied up some small things
22
23 Revision 1.15 1999/06/26 01:14:32 rjmcnab
24 Made a couple of changes to handle different encodings.
25
26 Revision 1.14 1999/06/09 00:08:36 sjboddie
27 query string macro (_cgiargq_) is now made html safe before being set
28
29 Revision 1.13 1999/06/08 04:29:31 sjboddie
30 added argsinfo to the call to check_cgiargs to make it easy to set
31 args to their default if they're found to be screwed up
32
33 Revision 1.12 1999/04/30 01:59:42 sjboddie
34 lots of stuff - getting documentaction working (documentaction replaces
35 old browseaction)
36
37 Revision 1.11 1999/03/25 03:06:43 sjboddie
38
39 altered receptionist slightly so it now passes *collectproto to
40 define_internal_macros and define_external_macros - need it
41 for browseaction
42
43 Revision 1.10 1999/03/05 03:53:54 sjboddie
44
45 fixed some bugs
46
47 Revision 1.9 1999/02/28 20:00:16 rjmcnab
48
49
50 Fixed a few things.
51
52 Revision 1.8 1999/02/25 21:58:59 rjmcnab
53
54 Merged sources.
55
56 Revision 1.7 1999/02/21 22:33:55 rjmcnab
57
58 Lots of stuff :-)
59
60 Revision 1.6 1999/02/11 01:24:05 rjmcnab
61
62 Fixed a few compiler warnings.
63
64 Revision 1.5 1999/02/08 01:28:02 rjmcnab
65
66 Got the receptionist producing something using the statusaction.
67
68 Revision 1.4 1999/02/05 10:42:46 rjmcnab
69
70 Continued working on receptionist
71
72 Revision 1.3 1999/02/04 10:00:56 rjmcnab
73
74 Developed the idea of an "action" and having them define the cgi arguments
75 which they need and how those cgi arguments function.
76
77 Revision 1.2 1999/02/04 01:17:27 rjmcnab
78
79 Got it outputing something.
80
81
82 */
83
84
85#include "receptionist.h"
86#include "fileutil.h"
87#include "cgiutils.h"
88#include "htmlutils.h"
89#include "OIDtools.h"
90#include <assert.h>
91#include <time.h>
92
93
94
95// configure should be called for each line in the
96// configuration files to configure the receptionist and everything
97// it contains. The configuration should take place after everything
98// has been added but before the initialisation.
99void receptionist::configure (const text_t &key, const text_tarray &cfgline) {
100 // configure the receptionist
101 if (cfgline.size() >= 1) {
102 if (key == "gsdlhome") configinfo.gsdlhome = cfgline[0];
103 else if (key == "collection") configinfo.collection = cfgline[0];
104 else if (key == "collectdir") configinfo.collectdir = cfgline[0];
105 else if (key == "httpprefix") configinfo.httpprefix = cfgline[0];
106 else if (key == "httpimg") configinfo.httpimg = cfgline[0];
107 else if (key == "gwcgi") configinfo.gwcgi = cfgline[0];
108 else if (key == "macrofiles") configinfo.macrofiles = cfgline;
109 else if (key == "saveconf") configinfo.saveconf = cfgline[0];
110 }
111
112 // configure the actions
113 actionptrmap::iterator actionhere = actions.begin ();
114 actionptrmap::iterator actionend = actions.end ();
115
116 while (actionhere != actionend) {
117 assert ((*actionhere).second.a != NULL);
118 if ((*actionhere).second.a != NULL)
119 (*actionhere).second.a->configure(key, cfgline);
120
121 actionhere++;
122 }
123
124 // configure the protocols
125 recptprotolistclass::iterator protohere = protocols.begin ();
126 recptprotolistclass::iterator protoend = protocols.end ();
127
128 while (protohere != protoend) {
129 assert ((*protohere).p != NULL);
130 if ((*protohere).p != NULL)
131 (*protohere).p->configure(key, cfgline);
132
133 protohere++;
134 }
135}
136
137void receptionist::configure (const text_t &key, const text_t &value) {
138 text_tarray cfgline;
139 cfgline.push_back (value);
140 configure(key, cfgline);
141}
142
143
144// init should be called after all the actions, protocols, and
145// converters have been added to the receptionist and after everything
146// has been configured but before any pages are created.
147// It returns true on success and false on failure. If false is
148// returned getpage should not be called (without producing
149// meaningless output), instead an error page should be
150// produced by the calling code.
151bool receptionist::init (ostream &logout) {
152 // first configure collectdir
153 text_t thecollectdir = configinfo.gsdlhome;
154 if (!configinfo.collection.empty()) {
155 // collection specific mode
156 if (!configinfo.collectdir.empty()) {
157 // has already been configured
158 thecollectdir = configinfo.collectdir;
159 } else {
160 // decide where collectdir is by searching for collect.cfg
161 // look in $GSDLHOME/collect/collection-name/etc/collect.cfg and
162 // then $GSDLHOME/etc/collect.cfg
163 thecollectdir = filename_cat (configinfo.gsdlhome, "collect");
164 thecollectdir = filename_cat (thecollectdir, configinfo.collection);
165 text_t filename = filename_cat (thecollectdir, "etc");
166 filename = filename_cat (filename, "collect.cfg");
167 if (!file_exists(filename)) thecollectdir = configinfo.gsdlhome;
168 }
169 }
170 configure("collectdir", thecollectdir);
171
172 // read in the macro files
173 if (!read_macrofiles (logout)) return false;
174
175 // defined the main cgi arguments
176 if (!define_mainargs (logout)) return false;
177
178 // there must be at least one action defined
179 if (actions.empty()) {
180 logout << "Error: no actions have been added to the receptionist\n";
181 return false;
182 }
183
184 // add the cgi arguments from the actions
185 actionptrmap::iterator here = actions.begin ();
186 actionptrmap::iterator end = actions.end ();
187 while (here != end) {
188 assert ((*here).second.a != NULL);
189 if ((*here).second.a != NULL) {
190 if (!argsinfo.addarginfo (&logout, (*here).second.a->getargsinfo()))
191 return false;
192 }
193 here++;
194 }
195
196 // create a saveconf string if there isn't one already
197 if (configinfo.saveconf.empty())
198 configinfo.saveconf = create_save_conf_str (argsinfo, logout);
199
200 // check the saveconf string
201 if (!check_save_conf_str (configinfo.saveconf, argsinfo, logout))
202 return false;
203
204 // set a random seed
205 srand (time(NULL));
206
207 // make the output converters remove all the zero-width spaces
208 convertinfoclass::iterator converthere = converters.begin ();
209 convertinfoclass::iterator convertend = converters.end ();
210 text_t defaultconvertname;
211 while (converthere != convertend) {
212 assert ((*converthere).second.outconverter != NULL);
213 if ((*converthere).second.outconverter != NULL) {
214 (*converthere).second.outconverter->set_rzws(1);
215 if (defaultconvertname.empty())
216 defaultconvertname = (*converthere).second.name;
217 }
218 converthere++;
219 }
220
221 // set default converter if no good one has been defined
222 if (!defaultconvertname.empty()) {
223 cgiarginfo *ainfo = argsinfo.getarginfo ("w");
224 if ((ainfo != NULL) && (ainfo->defaultstatus < cgiarginfo::good)) {
225 ainfo->defaultstatus = cgiarginfo::good;
226 ainfo->argdefault = defaultconvertname;
227 }
228 }
229
230 // init the actions
231 actionptrmap::iterator actionhere = actions.begin ();
232 actionptrmap::iterator actionend = actions.end ();
233 while (actionhere != actionend) {
234 if (((*actionhere).second.a == NULL) ||
235 !(*actionhere).second.a->init(logout)) return false;
236 actionhere++;
237 }
238
239 // init the protocols
240 recptprotolistclass::iterator protohere = protocols.begin ();
241 recptprotolistclass::iterator protoend = protocols.end ();
242 while (protohere != protoend) {
243 if (((*protohere).p == NULL) ||
244 !(*protohere).p->init(logout)) return false;
245 protohere++;
246 }
247
248 return true;
249}
250
251
252// parse_cgi_args parses cgi arguments into an argument class.
253// This function should be called for each page request. It returns false
254// if there was a major problem with the cgi arguments.
255bool receptionist::parse_cgi_args (const text_t &argstr, cgiargsclass &args,
256 ostream &logout) {
257 outconvertclass text_t2ascii;
258
259 // get an initial list of cgi arguments
260 args.clear();
261 split_cgi_args (argstr, args);
262
263 // expand the compressed argument (if there was one)
264 if (!expand_save_args (argsinfo, configinfo.saveconf, args, logout)) return false;
265
266 // add the defaults
267 add_default_args (argsinfo, args, logout);
268
269
270 // get the input encoding
271 text_t &arg_w = args["w"];
272 inconvertclass defaultinconvert;
273 inconvertclass *inconvert = converters.get_inconverter (arg_w);
274 if (inconvert == NULL) inconvert = &defaultinconvert;
275
276 // see if the next page will have a different encoding
277 if (args.getarg("nw") != NULL) arg_w = args["nw"];
278
279 // convert arguments which aren't in unicode to unicode
280 args_tounicode (args, *inconvert);
281
282
283 // decide on the output conversion class (needed for checking the external
284 // cgi arguments)
285 rzwsoutconvertclass defaultoutconverter;
286 rzwsoutconvertclass *outconverter = converters.get_outconverter (arg_w);
287 if (outconverter == NULL) outconverter = &defaultoutconverter;
288 outconverter->reset();
289
290
291 // check the main cgi arguments
292 if (!check_mainargs (args, logout)) return false;
293
294 // check the arguments for the action
295 action *a = actions.getaction (args["a"]);
296 if (a != NULL) {
297 if (!a->check_cgiargs (argsinfo, args, logout)) return false;
298 } else {
299 // the action was not found!!
300 logout << text_t2ascii << "Error: the action \"" << args["a"]
301 << "\" could not be found.\n";
302 return false;
303 }
304
305 // check external cgi arguments for each action
306 actionptrmap::iterator actionhere = actions.begin ();
307 actionptrmap::iterator actionend = actions.end ();
308 while (actionhere != actionend) {
309 assert ((*actionhere).second.a != NULL);
310 if ((*actionhere).second.a != NULL) {
311 if (!(*actionhere).second.a->check_external_cgiargs (argsinfo, args, *outconverter,
312 configinfo.saveconf, logout))
313 return false;
314 }
315 actionhere++;
316 }
317
318 // the action might have changed but we will assume that
319 // the cgiargs were checked properly when the change was made
320
321 return true;
322}
323
324
325// produce_cgi_page will call get_cgihead_info and
326// produce_content in the appropriate way to output a cgi header and
327// the page content (if needed). If a page could not be created it
328// will return false
329bool receptionist::produce_cgi_page (cgiargsclass &args, ostream &contentout,
330 ostream &logout) {
331 outconvertclass text_t2ascii;
332
333 response_t response;
334 text_t response_data;
335
336 // produce cgi header
337 get_cgihead_info (args, response, response_data, logout);
338 if (response == location) {
339 // I've forgotten how to do this :-/
340 return true;
341 } else if (response == content) {
342 // content response
343 contentout << text_t2ascii << "Content-type: " << response_data << "\n\n";
344 } else {
345 // unknown response
346 logout << "Error: get_cgihead_info returned an unknown response type.\n";
347 return false;
348 }
349
350 // produce cgi page
351 if (!produce_content (args, contentout, logout)) return false;
352
353 // flush contentout
354 contentout << flush;
355 return true;
356}
357
358
359// get_cgihead_info determines the cgi header information for
360// a set of cgi arguments. If response contains location then
361// response_data contains the redirect address. If reponse
362// contains content then reponse_data contains the content-type.
363// Note that images can now be produced by the receptionist.
364void receptionist::get_cgihead_info (cgiargsclass &args, response_t &response,
365 text_t &response_data, ostream &logout) {
366 outconvertclass text_t2ascii;
367
368 // get the action
369 action *a = actions.getaction (args["a"]);
370 if (a != NULL) {
371 a->get_cgihead_info (args, response, response_data, logout);
372
373 } else {
374 // the action was not found!!
375 logout << text_t2ascii << "Error receptionist::get_cgihead_info: the action \""
376 << args["a"] << "\" could not be found.\n";
377 response = content;
378 response_data = "text/html";
379 }
380}
381
382
383// produce the page content
384bool receptionist::produce_content (cgiargsclass &args, ostream &contentout,
385 ostream &logout) {
386 // decide on the output conversion class
387 text_t &arg_w = args["w"];
388 rzwsoutconvertclass defaultoutconverter;
389 rzwsoutconvertclass *outconverter = converters.get_outconverter (arg_w);
390 if (outconverter == NULL) outconverter = &defaultoutconverter;
391 outconverter->reset();
392
393 // decide on the protocol used for communicating with
394 // the collection server
395 recptproto *collectproto = NULL;
396 if (!args["c"].empty()) {
397 collectproto = protocols.getrecptproto (args["c"], logout);
398 }
399
400 // produce the page using the desired action
401 action *a = actions.getaction (args["a"]);
402 if (a != NULL) {
403 if (a->uses_display(args)) prepare_page (a, args, collectproto, (*outconverter), logout);
404 if (!a->do_action (args, collectproto, disp, (*outconverter), contentout, logout))
405 return false;
406
407 } else {
408 // the action was not found!!
409 outconvertclass text_t2ascii;
410
411 logout << text_t2ascii << "Error receptionist::produce_content: the action \""
412 << args["a"] << "\" could not be found.\n";
413
414 contentout << (*outconverter)
415 << "<html>\n"
416 << "<head>\n"
417 << "<title>Error</title>\n"
418 << "</head>\n"
419 << "<body>\n"
420 << "<h2>Oops!</h2>\n"
421 << "Undefined Page. The action \""
422 << args["a"] << "\" could not be found.\n"
423 << "</body>\n"
424 << "</html>\n";
425 }
426
427 return true;
428}
429
430
431// returns the compressed argument ("e") corresponding to the argument
432// list. This can be used to save preferences between sessions.
433text_t receptionist::get_compressed_arg (cgiargsclass &args, ostream &logout) {
434 // decide on the output conversion class
435 text_t &arg_w = args["w"];
436 rzwsoutconvertclass defaultoutconverter;
437 rzwsoutconvertclass *outconverter = converters.get_outconverter (arg_w);
438 if (outconverter == NULL) outconverter = &defaultoutconverter;
439 outconverter->reset();
440
441 text_t compressed_args;
442 if (compress_save_args (argsinfo, configinfo.saveconf, args,
443 compressed_args, *outconverter, logout))
444 return compressed_args;
445
446 return "";
447}
448
449
450// will read in all the macro files. If one is not found an
451// error message will be written to logout and the method will
452// return false.
453bool receptionist::read_macrofiles (ostream &logout) {
454 outconvertclass text_t2ascii;
455
456 // redirect the error output to logout
457 disp.setlogout (&logout);
458
459 // load up the default macro files, the collection directory
460 // is searched first for the file (if this is being used in
461 // collection specific mode) and then the main directory
462 text_t colmacrodir = filename_cat (configinfo.collectdir, "macros");
463 text_t gsdlmacrodir = filename_cat (configinfo.gsdlhome, "macros");
464 text_tarray::iterator arrhere = configinfo.macrofiles.begin();
465 text_tarray::iterator arrend = configinfo.macrofiles.end();
466 text_t filename;
467 while (arrhere != arrend) {
468 // filename is used as a flag to indicate whether
469 // the macro file has been found
470 filename.clear();
471
472 // try in the collection directory if this is being
473 // run in collection specific mode
474 if (!configinfo.collection.empty()) {
475 filename = filename_cat (colmacrodir, *arrhere);
476 if (!file_exists (filename)) filename.clear ();
477 }
478
479 // if we haven't found the macro file yet try in
480 // the main macro directory
481 if (filename.empty()) {
482 filename = filename_cat (gsdlmacrodir, *arrhere);
483 if (!file_exists (filename)) filename.clear ();
484 }
485
486 // see if we found the file or not
487 if (filename.empty()) {
488 logout << text_t2ascii
489 << "Error: the macro file \"" << *arrhere << "\" could not be found.\n";
490 if (configinfo.collection.empty()) {
491 logout << text_t2ascii
492 << "It should be in " << gsdlmacrodir << ".\n\n";
493 } else {
494 logout << text_t2ascii
495 << "It should be in either " << colmacrodir << " or in "
496 << gsdlmacrodir << ".\n\n";
497 }
498 return false;
499
500 } else { // found the file
501 disp.loaddefaultmacros(filename);
502 }
503
504 arrhere++;
505 }
506
507 // success
508 return true;
509}
510
511
512// Will define the main general arguments used by the receptionist.
513// If an error occurs a message will be written to logout and the
514// method will return false.
515bool receptionist::define_mainargs (ostream &logout) {
516 // create a list of cgi arguments
517 cgiarginfo ainfo;
518
519 ainfo.shortname = "e";
520 ainfo.longname = "compressed arguments";
521 ainfo.multiplechar = true;
522 ainfo.defaultstatus = cgiarginfo::good;
523 ainfo.argdefault = "";
524 ainfo.savedarginfo = cgiarginfo::mustnot;
525 if (!argsinfo.addarginfo (&logout, ainfo)) return false;
526
527 ainfo.shortname = "a";
528 ainfo.longname = "action";
529 ainfo.multiplechar = true;
530 ainfo.defaultstatus = cgiarginfo::none;
531 ainfo.argdefault = "";
532 ainfo.savedarginfo = cgiarginfo::must;
533 if (!argsinfo.addarginfo (&logout, ainfo)) return false;
534
535 // w=western
536 ainfo.shortname = "w";
537 ainfo.longname = "encoding";
538 ainfo.multiplechar = true;
539 ainfo.defaultstatus = cgiarginfo::weak;
540 ainfo.argdefault = "w";
541 ainfo.savedarginfo = cgiarginfo::must;
542 if (!argsinfo.addarginfo (&logout, ainfo)) return false;
543
544 ainfo.shortname = "nw";
545 ainfo.longname = "new encoding";
546 ainfo.multiplechar = true;
547 ainfo.defaultstatus = cgiarginfo::none;
548 ainfo.argdefault = "";
549 ainfo.savedarginfo = cgiarginfo::mustnot;
550 if (!argsinfo.addarginfo (&logout, ainfo)) return false;
551
552 ainfo.shortname = "c";
553 ainfo.longname = "collection";
554 ainfo.multiplechar = true;
555 if (configinfo.collection.empty()) {
556 ainfo.defaultstatus = cgiarginfo::none;
557 ainfo.argdefault = "";
558 ainfo.savedarginfo = cgiarginfo::must;
559 } else {
560 ainfo.defaultstatus = cgiarginfo::good;
561 ainfo.argdefault = configinfo.collection;
562 ainfo.savedarginfo = cgiarginfo::can;
563 }
564 if (!argsinfo.addarginfo (&logout, ainfo)) return false;
565
566 // 0=text+graphics, 1=text
567 ainfo.shortname = "v";
568 ainfo.longname = "version";
569 ainfo.multiplechar = false;
570 ainfo.defaultstatus = cgiarginfo::weak;
571 ainfo.argdefault = "0";
572 ainfo.savedarginfo = cgiarginfo::can;
573 if (!argsinfo.addarginfo (&logout, ainfo)) return false;
574
575 // 0=normal, 1=big
576 ainfo.shortname = "f";
577 ainfo.longname = "query box size";
578 ainfo.multiplechar = false;
579 ainfo.defaultstatus = cgiarginfo::weak;
580 ainfo.argdefault = "0";
581 ainfo.savedarginfo = cgiarginfo::can;
582 if (!argsinfo.addarginfo (&logout, ainfo)) return false;
583
584 // the interface language name should use the ISO 639
585 // standard
586 ainfo.shortname = "l";
587 ainfo.longname = "interface language";
588 ainfo.multiplechar = true;
589 ainfo.defaultstatus = cgiarginfo::weak;
590 ainfo.argdefault = "en";
591 ainfo.savedarginfo = cgiarginfo::must;
592 if (!argsinfo.addarginfo (&logout, ainfo)) return false;
593
594 return true;
595}
596
597
598// check_mainargs will check all the main arguments. If a major
599// error is found it will return false and no cgi page should
600// be created using the arguments.
601bool receptionist::check_mainargs (cgiargsclass &args, ostream &/*logout*/) {
602 // if this receptionist is running in collection dependant mode
603 // then it should always set the collection argument to the
604 // collection
605 if (!configinfo.collection.empty()) args["c"] = configinfo.collection;
606
607 // argument "v" can only be 0 or 1. Use the default value
608 // if it is out of range
609 int arg_v = args.getintarg ("v");
610 if (arg_v != 0 && arg_v != 1) {
611 cgiarginfo *vinfo = argsinfo.getarginfo ("v");
612 if (vinfo != NULL) args["v"] = vinfo->argdefault;
613 }
614
615 // argument "f" can only be 0 or 1. Use the default value
616 // if it is out of range
617 int arg_f = args.getintarg ("f");
618 if (arg_f != 0 && arg_f != 1) {
619 cgiarginfo *finfo = argsinfo.getarginfo ("f");
620 if (finfo != NULL) args["f"] = finfo->argdefault;
621 }
622
623 return true;
624}
625
626// prepare_page sets up page parameters, sets display macros
627// and opens the page ready for output
628void receptionist::prepare_page (action *a, cgiargsclass &args, recptproto *collectproto,
629 outconvertclass &outconvert, ostream &logout) {
630 // set up page parameters
631 text_t pageparams;
632
633 bool first = true;
634 if (!args["c"].empty()) {
635 pageparams += "collection=" + args["c"]; first = false;}
636 if (args.getintarg("u") == 1)
637 if (first) {pageparams += "style=htmlonly"; first = false;}
638 else pageparams += ",style=htmlonly";
639 if (args.getintarg("v") == 1)
640 if (first) {pageparams += "version=text"; first = false;}
641 else pageparams += ",version=text";
642 if (args.getintarg("f") == 1)
643 if (first) {pageparams += ",queryversion=big"; first = false;}
644 else pageparams += ",queryversion=big";
645 if (args["l"] != "en")
646 if (first) pageparams += ",language=" + args["l"];
647 else pageparams += ",language=" + args["l"];
648
649 // open the page
650 disp.openpage(pageparams, MACROPRECEDENCE);
651
652
653 // define general macros
654 define_general_macros (args, outconvert, logout);
655
656
657 // define external macros for each action
658 actionptrmap::iterator actionhere = actions.begin ();
659 actionptrmap::iterator actionend = actions.end ();
660
661 while (actionhere != actionend) {
662 assert ((*actionhere).second.a != NULL);
663 if ((*actionhere).second.a != NULL)
664 (*actionhere).second.a->define_external_macros (disp, args, collectproto, logout);
665 actionhere++;
666 }
667
668
669 // define internal macros for the current action
670 a->define_internal_macros (disp, args, collectproto, logout);
671}
672
673void receptionist::define_general_macros (cgiargsclass &args, outconvertclass &/*outconvert*/,
674 ostream &logout) {
675 disp.setmacro ("gwcgi", "Global", configinfo.gwcgi);
676 disp.setmacro ("httpimg", "Global", configinfo.httpimg);
677 disp.setmacro ("httpprefix", "Global", configinfo.httpprefix);
678 disp.setmacro("compressedoptions", "Global", get_compressed_arg(args, logout));
679
680 // set _cgiargX_ macros for each cgi argument
681 cgiargsclass::const_iterator argshere = args.begin();
682 cgiargsclass::const_iterator argsend = args.end();
683 while (argshere != argsend) {
684 if ((*argshere).first == "q")
685 // need to escape special characters from query string
686 disp.setmacro ("cgiargq", "Global", html_safe((*argshere).second.value));
687 else
688 disp.setmacro ("cgiarg" + (*argshere).first, "Global", (*argshere).second.value);
689 argshere ++;
690 }
691}
Note: See TracBrowser for help on using the repository browser.