This file is indexed.

/usr/share/awl/inc/classBrowser.php is in libawl-php 0.56-1ubuntu1.

This file is owned by root:root, with mode 0o644.

The actual contents of the file can be viewed below.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
<?php
/**
* Table browser / lister class
*
* Browsers are constructed from BrowserColumns and can support sorting
* and other interactive behaviour.  Cells may contain data which is
* formatted as a link, or the entire row may be linked through an onclick
* action.
*
* @package   awl
* @subpackage   Browser
* @author    Andrew McMillan <andrew@mcmillan.net.nz>
* @copyright Catalyst IT Ltd, Morphoss Ltd <http://www.morphoss.com/>
* @license   http://gnu.org/copyleft/gpl.html GNU GPL v2 or later
*/

require_once("AWLUtilities.php");

/**
* Ensure that this is not set elsewhere.
*/
$BrowserCurrentRow = (object) array();



/**
* BrowserColumns are the basic building blocks.  You can specify just the
* field name, and the column header or you can get fancy and specify an
* alignment, format string, SQL formula and cell CSS class.
* @package   awl
*/
class BrowserColumn
{
  var $Field;
  var $Header;
  var $Format;
  var $Sql;
  var $Align;
  var $Class;
  var $Type;
  var $Translatable;
  var $Hook;
  var $current_row;

  /**
  * BrowserColumn constructor.  Only the first parameter is mandatory.
  *
  * @param string field The name of the column in the SQL result.
  * @param string header The text to appear in the column header on output
  *                      (@see BrowserColumn::RenderHeader()).  If this is not supplied then
  *                      a default of the field name will be used.
  * @param string align left|center|right - text alignment.  Defaults to 'left'.
  * @param string format A format (a-la-printf) to render data values within.
  *                      (@see BrowserColumn::RenderValue()).  If this is not supplied
  *                      then the default will ensure the column value is displayed as-is.
  * @param string sql Some SQL which will return the desired value to be presented as column 'field' of
  *                   the result. If this is blank then the column is assumed to be a real data column.
  * @param string class Additional classes to apply to the column header and column value cells.
  * @param string datatype This will allow 'date' or 'timestamp' to preformat the field correctly before
  *                        using it in replacements or display.  Other types may be added in future.
  * @param string $hook The name of a global function which will preprocess the column value
  *
  * The hook function should be defined as follows:
  *   function hookfunction( $column_value, $column_name, $database_row ) {
  *     ...
  *     return $value;
  *   }
  */
  function BrowserColumn( $field, $header="", $align="", $format="", $sql="", $class="", $datatype="", $hook=null ) {
    $this->Field  = $field;
    $this->Sql    = $sql;
    $this->Header = $header;
    $this->Format = $format;
    $this->Class  = $class;
    $this->Align  = $align;
    $this->Type   = $datatype;
    $this->Translatable = false;
    $this->Hook   = $hook;
  }

  /**
  * GetTarget
  *
  * Retrieves a 'field' or '...SQL... AS field' definition for the target list of the SQL.
  */
  function GetTarget() {
    if ( $this->Sql == "" ) return $this->Field;
    return "$this->Sql AS $this->Field";
  }

  /**
  * RenderHeader
  * Renders the column header cell for this column.  This will be rendered as a <th>...</th>
  * with class and alignment applied to it.  Browser column headers are clickable, and the
  * ordering will also display an 'up' or 'down' triangle with the column header that the SQL
  * is sorted on at the moment.
  *
  * @param string order_field The name of the field currently being sorted on.
  * @param string order_direction Whether the sort is Ascending or Descending.
  * @param int browser_array_key Used this to help handle separate ordering of
  *                              multiple browsers on the same page.
  * @param string forced_order If true, then we don't allow order to be changed.
  */
  function RenderHeader( $order_field, $order_direction, $browser_array_key=0, $forced_order=false ) {
    global $c;
    if ( $this->Align == "" ) $this->Align = "left";
    $html = '<th class="'.$this->Align.'" '. ($this->Class == "" ? "" : "class=\"$this->Class\"") . '>';

    $direction = 'A';
    $image = "";
    if ( !$forced_order && $order_field == $this->Field ) {
      if ( strtoupper( substr( $order_direction, 0, 1) ) == 'A' ) {
        $image = 'down';
        $direction = 'D';
      }
      else {
        $image = 'up';
      }
      $image = "<img class=\"order\" src=\"$c->images/$image.gif\" alt=\"$image\" />";
    }
    if ( !isset($browser_array_key) || $browser_array_key == '' ) $browser_array_key = 0;
    if ( !$forced_order ) $html .= '<a href="'.replace_uri_params( $_SERVER['REQUEST_URI'], array( "o[$browser_array_key]" => $this->Field, "d[$browser_array_key]" => $direction ) ).'" class="order">';
    $html .= ($this->Header == "" ? $this->Field : $this->Header);
    if ( !$forced_order ) $html .= "$image</a>";
    $html .= "</th>\n";
    return $html;
  }

  function SetTranslatable() {
    $this->Translatable = true;
  }

  function RenderValue( $value, $extraclass = "" ) {
    global $session;

    if ( $this->Type == 'date' || $this->Type == 'timestamp') {
      $value = $session->FormattedDate( $value, $this->Type );
    }

    if ( $this->Hook && function_exists($this->Hook) ) {
      dbg_error_log( "Browser", ":Browser: Hook for $this->Hook on column $this->Field");
      $value = call_user_func( $this->Hook, $value, $this->Field, $this->current_row );
    }

    if ( $this->Translatable ) {
      $value = translate($value);
    }

    $value = str_replace( "\n", "<br />", $value );
    if ( substr(strtolower($this->Format),0,3) == "<td" ) {
      $html = sprintf($this->Format,$value);
    }
    else {
      // These quite probably don't work.  The CSS standard for multiple classes is 'class="a b c"' but is lightly
      // implemented according to some web references.  Perhaps modern browsers are better?
      $class = $this->Align . ($this->Class == "" ? "" : " $this->Class") . ($extraclass == "" ? "" : " $extraclass");
      if ( $class != "" ) $class = ' class="'.$class.'"';
      $html = sprintf('<td%s>',$class);
      $html .= ($this->Format == "" ? $value : sprintf($this->Format,$value,$value));
      $html .= "</td>\n";
    }
    return $html;
  }
}


/**
* Start a new Browser, add columns, set a join and Render it to create a basic
* list of records in a table.
* You can, of course, get a lot fancier with setting ordering, where clauses
* totalled columns and so forth.
* @package   awl
*/
class Browser
{
  var $Title;
  var $SubTitle;
  var $FieldNames;
  var $Columns;
  var $HiddenColumns;
  var $Joins;
  var $Where;
  var $Distinct;
  var $Union;
  var $Order;
  var $OrderField;
  var $OrderDirection;
  var $OrderBrowserKey;
  var $ForcedOrder;
  var $Grouping;
  var $Limit;
  var $Offset;
  var $Query;
  var $BeginRow;
  var $CloseRow;
  var $BeginRowArgs;
  var $Totals;
  var $TotalFuncs;
  var $ExtraRows;
  var $match_column;
  var $match_value;
  var $match_function;
  var $DivOpen;
  var $DivClose;

  /**
  * The Browser class constructor
  *
  * @param string $title A title for the browser (optional).
  */
  function Browser( $title = "" ) {
    global $c;
    $this->Title = $title;
    $this->SubTitle = "";
    $this->Distinct = "";
    $this->Order = "";
    $this->Limit = "";
    $this->Offset = "";
    $this->BeginRow = "<tr class=\"row%d\">\n";
    $this->CloseRow = "</tr>\n";
    $this->BeginRowArgs = array('#even');
    $this->Totals = array();
    $this->Columns = array();
    $this->HiddenColumns = array();
    $this->FieldNames = array();
    $this->DivOpen = '<div id="browser">';
    $this->DivClose = '</div>';
    $this->ForcedOrder = false;
    dbg_error_log( "Browser", ":Browser: New browser called $title");
  }

  /**
  * Add a column to the Browser.
  *
  * This constructs a new BrowserColumn, appending it to the array of columns
  * in this Browser.
  *
  * Note that if the $format parameter starts with '<td>' the format will replace
  * the column format, otherwise it will be used within '<td>...</td>' tags.
  * @see BrowserColumn
  *
  * @param string $field The name of the field.
  * @param string $header A column header for the field.
  * @param string $align An alignment for column values.
  * @param string $format A sprintf format for displaying column values.
  * @param string $sql An SQL fragment for calculating the value.
  * @param string $class A CSS class to apply to the cells of this column.
  * @param string $hook The name of a global function which will preprocess the column value
  *
  * The hook function should be defined as follows:
  *   function hookfunction( $column_value, $column_name, $database_row ) {
  *     ...
  *     return $value;
  *   }
  *
  */
  function AddColumn( $field, $header="", $align="", $format="", $sql="", $class="", $datatype="", $hook=null ) {
    $this->Columns[] = new BrowserColumn( $field, $header, $align, $format, $sql, $class, $datatype, $hook );
    $this->FieldNames[$field] = count($this->Columns) - 1;
  }

  /**
  * Add a hidden column - one that is present in the SQL result, but for
  * which there is no column displayed.
  *
  * This can be useful for including a value in (e.g.) clickable links or title
  * attributes which is not actually displayed as a visible column.
  *
  * @param string $field The name of the field.
  * @param string $sql An SQL fragment to calculate the field, if it is calculated.
  */
  function AddHidden( $field, $sql="" ) {
    $this->HiddenColumns[] = new BrowserColumn( $field, "", "", "", $sql );
    $this->FieldNames[$field] = count($this->Columns) - 1;
  }

  /**
  * Set the Title for the browse.
  *
  * This can also be set in the constructor but if you create a template Browser
  * and then clone it in a loop you may want to assign a different Title for each
  * instance.
  *
  * @param string $new_title The new title for the browser
  */
  function SetTitle( $new_title ) {
    $this->Title = $new_title;
  }


  /**
  * Accessor for the Title for the browse, which could set the title also.
  *
  * @param string $new_title The new title for the browser
  * @return string The current title for the browser
  */
  function Title( $new_title = null ) {
    if ( isset($new_title) ) $this->Title = $new_title;
    return $this->Title;
  }


  /**
  * Set the named columns to be translatable
  *
  * @param array $column_list The list of columns which are translatable
  */
  function SetTranslatable( $column_list ) {
    $top = count($this->Columns);
    for( $i=0; $i < $top; $i++ ) {
      dbg_error_log( "Browser", "Comparing %s with column name list", $this->Columns[$i]->Field);
      if ( in_array($this->Columns[$i]->Field,$column_list) ) $this->Columns[$i]->SetTranslatable();
    }
    $top = count($this->HiddenColumns);
    for( $i=0; $i < $top; $i++ ) {
      dbg_error_log( "Browser", "Comparing %s with column name list", $this->HiddenColumns[$i]->Field);
      if ( in_array($this->HiddenColumns[$i]->Field,$column_list) ) $this->HiddenColumns[$i]->SetTranslatable();
    }
  }

  /**
  * Set a Sub Title for the browse.
  *
  * @param string $sub_title The sub title string
  */
  function SetSubTitle( $sub_title ) {
    $this->SubTitle = $sub_title;
  }

  /**
  * Set a div for wrapping the browse.
  *
  * @param string $open_div The HTML to open the div
  * @param string $close_div The HTML to open the div
  */
  function SetDiv( $open_div, $close_div ) {
    $this->DivOpen = $open_div;
    $this->DivClose = $close_div;
  }

  /**
  * Set the tables and joins for the SQL.
  *
  * For a single table this should just contain the name of that table, but for
  * multiple tables it should be the full content of the SQL 'FROM ...' clause
  * (excluding the actual 'FROM' keyword).
  *
  * @param string $join_list
  */
  function SetJoins( $join_list ) {
    $this->Joins = $join_list;
  }

  /**
  * Set a Union SQL statement.
  *
  * In rare cases this might be useful.  It's currently a fairly simple hack
  * which requires you to put an entire valid (& matching) UNION subclause
  * (although without the UNION keyword).
  *
  * @param string $union_select
  */
  function SetUnion( $union_select ) {
    $this->Union = $union_select;
  }

  /**
  * Set the SQL Where clause to a specific value.
  *
  * The WHERE keyword should not be included.
  *
  * @param string $where_clause A valide SQL WHERE ... clause.
  */
  function SetWhere( $where_clause ) {
    $this->Where = $where_clause;
  }

  /**
  * Set the SQL DISTINCT clause to a specific value.
  *
  * The whole clause (except the keyword) needs to be supplied
  *
  * @param string $distinct The whole clause, after 'DISTINCT'
  */
  function SetDistinct( $distinct ) {
    $this->Distinct = "DISTINCT ".$distinct;
  }

  /**
  * Set the SQL LIMIT clause to a specific value.
  *
  * Only the limit number should be supplied.
  *
  * @param int $limit_n A number of rows to limit the SQL selection to
  */
  function SetLimit( $limit_n ) {
    $this->Limit = "LIMIT ".intval($limit_n);
  }

  /**
  * Set the SQL OFFSET clause to a specific value.
  *
  * Only the offset number
  *
  * @param int $offset_n A number of rows to offset the SQL selection to, based from the start of the results.
  */
  function SetOffset( $offset_n ) {
    $this->Offset = "OFFSET ".intval($offset_n);
  }

  /**
  * Add an [operator] ... to the SQL Where clause
  *
  * You will generally want to call OrWhere or AndWhere rather than
  * this function, but hey: who am I to tell you how to code!
  *
  * @param string $operator The operator to combine with previous where clause parts.
  * @param string $more_where The extra part of the where clause
  */
  function MoreWhere( $operator, $more_where ) {
    if ( $this->Where == "" ) {
      $this->Where = $more_where;
      return;
    }
    $this->Where = "$this->Where $operator $more_where";
  }

  /**
  * Add an OR ...  to the SQL Where clause
  *
  * @param string $more_where The extra part of the where clause
  */
  function AndWhere( $more_where ) {
    $this->MoreWhere("AND",$more_where);
  }

  /**
  * Add an OR ... to the SQL Where clause
  *
  * @param string $more_where The extra part of the where clause
  */
  function OrWhere( $more_where ) {
    $this->MoreWhere("OR",$more_where);
  }

  function AddGrouping( $field, $browser_array_key=0 ) {
    if ( $this->Grouping == "" )
      $this->Grouping = "GROUP BY ";
    else
      $this->Grouping .= ", ";

    $this->Grouping .= clean_string($field);
  }


  /**
  * Add an ordering to the browser widget.
  *
  * The ordering can be overridden by GET parameters which will be
  * rendered into the column headers so that a user can click on
  * the column headers to control the actual order.
  *
  * @param string $field The name of the field to be ordered by.
  * @param string $direction A for Ascending, otherwise it will be descending order.
  * @param string $browser_array_key Use this to distinguish between multiple
  *               browser widgets on the same page.  Leave it empty if you only
  *               have a single browser instance.
  * @param string $secondary Use this to indicate a default secondary order
  *               which shouldn't interfere with the default primary order.
  */
  function AddOrder( $field, $direction, $browser_array_key=0, $secondary=0 ) {
    $field = check_by_regex($field,'/^[^\'"!\\\\()\[\]|*\/{}&%@~;:?<>]+$/');
    if ( ! isset($this->FieldNames[$field]) ) return;

    if ( !isset($this->Order) || $this->Order == "" )
      $this->Order = "ORDER BY ";
    else
      $this->Order .= ", ";

    if ( $secondary == 0 ) {
      $this->OrderField = $field;
      $this->OrderBrowserKey = $browser_array_key;
    }
    $this->Order .= $field;

    if ( preg_match( '/^A/i', $direction) ) {
      $this->Order .= " ASC";
      if ( $secondary == 0)
        $this->OrderDirection = 'A';
    }
    else {
      $this->Order .= " DESC";
      if ( $secondary == 0)
        $this->OrderDirection = 'D';
    }
  }


  /**
  * Force a particular ordering onto the browser widget.
  *
  * @param string $field The name of the field to be ordered by.
  * @param string $direction A for Ascending, otherwise it will be descending order.
  */
  function ForceOrder( $field, $direction ) {
    $field = clean_string($field);
    if ( ! isset($this->FieldNames[$field]) ) return;

    if ( $this->Order == "" )
      $this->Order = "ORDER BY ";
    else
      $this->Order .= ", ";

    $this->Order .= $field;

    if ( preg_match( '/^A/i', $direction) ) {
      $this->Order .= " ASC";
    }
    else {
      $this->Order .= " DESC";
    }

    $this->ForcedOrder = true;
  }


  /**
  * Set up the ordering for the browser.  Generally you should call this with
  * the first parameter set as a field to order by default.  Call with the second
  * parameter set to 'D' or 'DESCEND' if you want to reverse the default order.
  */
  function SetOrdering( $default_fld=null, $default_dir='A' , $browser_array_key=0 ) {
    if ( isset( $_GET['o'][$browser_array_key] ) && isset($_GET['d'][$browser_array_key] ) ) {
      $this->AddOrder( $_GET['o'][$browser_array_key], $_GET['d'][$browser_array_key], $browser_array_key );
    }
    else {
      if ( ! isset($default_fld) ) $default_fld = $this->Columns[0];
      $this->AddOrder( $default_fld, $default_dir, $browser_array_key );
    }
  }


  /**
  * Mark a column as something to be totalled.  You can also specify the name of
  * a function which may modify the value before the actual totalling.
  *
  * The callback function will be called with each row, with the first argument
  * being the entire record object and the second argument being only the column
  * being totalled.  The callback should return a number, to be added to the total.
  *
  * @param string $column_name The name of the column to be totalled.
  * @param string $total_function The name of the callback function.
  */
  function AddTotal( $column_name, $total_function = false ) {
    $this->Totals[$column_name] = 0;
    if ( $total_function != false ) {
      $this->TotalFuncs[$column_name] = $total_function;
    }
  }


  /**
  * Retrieve the total from a totalled column
  *
  * @param string $column_name The name of the column to be totalled.
  */
  function GetTotal( $column_name ) {
    return $this->Totals[$column_name];
  }


  /**
  * Set the format for an output row.
  *
  * The row format is set as an sprintf format string for the start of the row,
  * and a plain text string for the close of the row.  Subsequent arguments
  * are interpreted as names of fields, the values of which will be sprintf'd
  * into the beginrow string for each row.
  *
  * Some special field names exist beginning with the '#' character which have
  * 'magic' functionality, including '#even' which will insert '0' for even
  * rows and '1' for odd rows, allowing a nice colour alternation if the
  * beginrow format refers to it like: 'class="r%d"' so that even rows will
  * become 'class="r0"' and odd rows will be 'class="r1"'.
  *
  * At present only '#even' exists, although other magic values may be defined
  * in future.
  *
  * @param string $beginrow The new printf format for the start of the row.
  * @param string $closerow The new string for the close of the row.
  * @param string $rowargs ... The row arguments which will be sprintf'd into
  * the $beginrow format for each row
  */
  function RowFormat( $beginrow, $closerow, $rowargs )
  {
    $argc = func_num_args();
    $this->BeginRow = func_get_arg(0);
    $this->CloseRow = func_get_arg(1);

    $this->BeginRowArgs = array();
    for( $i=2; $i < $argc; $i++ ) {
      $this->BeginRowArgs[] = func_get_arg($i);
    }
  }


  /**
  * This method is used to build and execute the database query.
  *
  * You need not call this method, since Browser::Render() will call it for
  * you if you have not done so at that point.
  *
  * @return boolean The success / fail status of the AwlQuery::Exec()
  */
  function DoQuery() {
    $target_fields = "";
    foreach( $this->Columns AS $k => $column ) {
      if ( $target_fields != "" ) $target_fields .= ", ";
      $target_fields .= $column->GetTarget();
    }
    if ( isset($this->HiddenColumns) ) {
      foreach( $this->HiddenColumns AS $k => $column ) {
        if ( $target_fields != "" ) $target_fields .= ", ";
        $target_fields .= $column->GetTarget();
      }
    }
    $where_clause = ((isset($this->Where) && $this->Where != "") ? "WHERE $this->Where" : "" );
    $sql = sprintf( "SELECT %s %s FROM %s %s %s ", $this->Distinct, $target_fields,
                 $this->Joins, $where_clause, $this->Grouping );
    if ( "$this->Union" != "" ) {
      $sql .= "UNION $this->Union ";
    }
    $sql .= $this->Order . ' ' . $this->Limit . ' ' . $this->Offset;
    $this->Query = new AwlQuery( $sql );
    return $this->Query->Exec("Browse:$this->Title:DoQuery");
  }


  /**
  * Add an extra arbitrary row onto the end of the browser.
  *
  * @var array $column_values Contains an array of named fields, hopefully matching the column names.
  */
  function AddRow( $column_values ) {
    if ( !isset($this->ExtraRows) || typeof($this->ExtraRows) != 'array' ) $this->ExtraRows = array();
    $this->ExtraRows[] = &$column_values;
  }


  /**
  * Replace a row where $column = $value with an extra arbitrary row, returned from calling $function
  *
  * @param string $column The name of a column to match
  * @param string $value  The value to match in the column
  * @param string $function The name of the function to call for the matched row
  */
  function MatchedRow( $column, $value, $function ) {
    $this->match_column = $column;
    $this->match_value  = $value;
    $this->match_function = $function;
  }


  /**
  * Return values from the current row for replacing into a template.
  *
  * This is used to return values from the current row, so they can
  * be inserted into a row template.  It is used as a callback
  * function for preg_replace_callback.
  *
  * @param array of string $matches An array containing a field name as offset 1
  */
  function ValueReplacement($matches)
  {
    // as usual: $matches[0] is the complete match
    // $matches[1] the match for the first subpattern
    // enclosed in '##...##' and so on

    $field_name = $matches[1];
    if ( !isset($this->current_row->{$field_name}) && substr($field_name,0,4) == "URL:" ) {
      $field_name = substr($field_name,4);
      $replacement = urlencode($this->current_row->{$field_name});
    }
    else {
      $replacement = (isset($this->current_row->{$field_name}) ? $this->current_row->{$field_name} : '');
    }
    dbg_error_log( "Browser", ":ValueReplacement: Replacing %s with %s", $field_name, $replacement);
    return $replacement;
  }


  /**
  * This method is used to render the browser as HTML.  If the query has
  * not yet been executed then this will call DoQuery to do so.
  *
  * The browser (including the title) will be displayed in a div with id="browser" so
  * that you can style '#browser tr.header', '#browser tr.totals' and so forth.
  *
  * @param string $title_tag The tag to use around the browser title (default 'h1')
  * @return string The rendered HTML fragment to display to the user.
  */
  function Render( $title_tag = null, $subtitle_tag = null ) {
    global $c, $BrowserCurrentRow;

    if ( !isset($this->Query) ) $this->DoQuery();  // Ensure the query gets run before we render!

    dbg_error_log( "Browser", ":Render: browser $this->Title");
    $html = $this->DivOpen;
    if ( $this->Title != "" ) {
      if ( !isset($title_tag) ) $title_tag = 'h1';
      $html .= "<$title_tag>$this->Title</$title_tag>\n";
    }
    if ( $this->SubTitle != "" ) {
      if ( !isset($subtitle_tag) ) $subtitle_tag = 'h2';
      $html .= "<$subtitle_tag>$this->SubTitle</$subtitle_tag>\n";
    }

    $html .= "<table id=\"browse_table\">\n";
    $html .= "<thead><tr class=\"header\">\n";
    foreach( $this->Columns AS $k => $column ) {
      $html .= $column->RenderHeader( $this->OrderField, $this->OrderDirection, $this->OrderBrowserKey, $this->ForcedOrder );
    }
    $html .= "</tr></thead>\n<tbody>";

    $rowanswers = array();
    while( $BrowserCurrentRow = $this->Query->Fetch() ) {

      // Work out the answers to any stuff that may be being substituted into the row start
      /** @TODO: We should deprecate this approach in favour of simply doing the ValueReplacement on field names */
      foreach( $this->BeginRowArgs AS $k => $fld ) {
        if ( isset($BrowserCurrentRow->{$fld}) ) {
          $rowanswers[$k] = $BrowserCurrentRow->{$fld};
        }
        else {
          switch( $fld ) {
            case '#even':
              $rowanswers[$k] = ($this->Query->rownum() % 2);
              break;
            default:
              $rowanswers[$k] = $fld;
          }
        }
      }
      // Start the row
      $row_html = vsprintf( preg_replace("/#@even@#/", ($this->Query->rownum() % 2), $this->BeginRow), $rowanswers);

      if ( isset($this->match_column) && isset($this->match_value) && $BrowserCurrentRow->{$this->match_column} == $this->match_value ) {
        $row_html .= call_user_func( $this->match_function, $BrowserCurrentRow );
      }
      else {
        // Each column
        foreach( $this->Columns AS $k => $column ) {
          $row_html .= $column->RenderValue( (isset($BrowserCurrentRow->{$column->Field})?$BrowserCurrentRow->{$column->Field}:'') );
          if ( isset($this->Totals[$column->Field]) ) {
            if ( isset($this->TotalFuncs[$column->Field]) && function_exists($this->TotalFuncs[$column->Field]) ) {
              // Run the amount through the callback function  $floatval = my_function( $row, $fieldval );
              $this->Totals[$column->Field] += $this->TotalFuncs[$column->Field]( $BrowserCurrentRow, $BrowserCurrentRow->{$column->Field} );
            }
            else {
              // Just add the amount
              $this->Totals[$column->Field] += doubleval( preg_replace( '/[^0-9.-]/', '', $BrowserCurrentRow->{$column->Field} ));
            }
          }
        }
      }

      // Finish the row
      $row_html .= preg_replace("/#@even@#/", ($this->Query->rownum() % 2), $this->CloseRow);
      $this->current_row = $BrowserCurrentRow;
      $html .= preg_replace_callback("/##([^#]+)##/", array( &$this, "ValueReplacement"), $row_html );
    }

    if ( count($this->Totals) > 0 ) {
      $BrowserCurrentRow = (object) "";
      $row_html = "<tr class=\"totals\">\n";
      foreach( $this->Columns AS $k => $column ) {
        if ( isset($this->Totals[$column->Field]) ) {
          $row_html .= $column->RenderValue( $this->Totals[$column->Field], "totals" );
        }
        else {
          $row_html .= $column->RenderValue( "" );
        }
      }
      $row_html .= "</tr>\n";
      $this->current_row = $BrowserCurrentRow;
      $html .= preg_replace_callback("/##([^#]+)##/", array( &$this, "ValueReplacement"), $row_html );
    }


    if ( count($this->ExtraRows) > 0 ) {
      foreach( $this->ExtraRows AS $k => $v ) {
        $BrowserCurrentRow = (object) $v;
        // Work out the answers to any stuff that may be being substituted into the row start
        foreach( $this->BeginRowArgs AS $k => $fld ) {
          if ( isset( $BrowserCurrentRow->{$fld} ) ) {
            $rowanswers[$k] = $BrowserCurrentRow->{$fld};
          }
          else {
            switch( $fld ) {
              case '#even':
                $rowanswers[$k] = ($this->Query->rownum() % 2);
                break;
              default:
                $rowanswers[$k] = $fld;
            }
          }
        }

        // Start the row
        $row_html = vsprintf( preg_replace("/#@even@#/", ($this->Query->rownum() % 2), $this->BeginRow), $rowanswers);

        if ( isset($this->match_column) && isset($this->match_value) && $BrowserCurrentRow->{$this->match_column} == $this->match_value ) {
          $row_html .= call_user_func( $this->match_function, $BrowserCurrentRow );
        }
        else {
          // Each column
          foreach( $this->Columns AS $k => $column ) {
            $row_html .= $column->RenderValue( (isset($BrowserCurrentRow->{$column->Field}) ? $BrowserCurrentRow->{$column->Field} : '') );
          }
        }

        // Finish the row
        $row_html .= preg_replace("/#@even@#/", ($this->Query->rownum() % 2), $this->CloseRow);
        $this->current_row = $BrowserCurrentRow;
        $html .= preg_replace_callback("/##([^#]+)##/", array( &$this, "ValueReplacement"), $row_html );
      }
    }

    $html .= "</tbody>\n</table>\n";
    $html .= $this->DivClose;

    return $html;
  }

}