Overview

Packages

  • awl
    • AuthPlugin
    • AwlDatabase
    • Browser
    • classEditor
    • DataEntry
    • DataUpdate
    • EMail
    • iCalendar
    • MenuSet
    • PgQuery
    • Session
    • Translation
    • User
    • Utilities
    • Validation
    • vCalendar
    • vComponent
    • XMLDocument
    • XMLElement
  • None

Classes

  • AuthPlugin
  • AwlCache
  • AwlDatabase
  • AwlDBDialect
  • AwlQuery
  • AwlUpgrader
  • Browser
  • BrowserColumn
  • DBRecord
  • Editor
  • EditorField
  • EMail
  • EntryField
  • EntryForm
  • iCalComponent
  • iCalendar
  • iCalProp
  • MenuOption
  • MenuSet
  • Multipart
  • PgQuery
  • Session
  • SinglePart
  • User
  • Validation
  • vCalendar
  • vComponent
  • vObject
  • vProperty
  • XMLDocument
  • XMLElement

Functions

  • _awl_connect_configured_database
  • _CompareMenuSequence
  • auth_external
  • auth_other_awl
  • awl_replace_sql_args
  • awl_set_locale
  • awl_version
  • BuildXMLTree
  • check_by_regex
  • check_temporary_passwords
  • clean_string
  • connect_configured_database
  • dbg_error_log
  • dbg_log_array
  • define_byte_mappings
  • deprecated
  • duration
  • fatal
  • force_utf8
  • get_fields
  • getCacheInstance
  • gzdecode
  • i18n
  • init_gettext
  • olson_from_tzstring
  • param_to_global
  • qpg
  • quoted_printable_encode
  • replace_uri_params
  • session_salted_md5
  • session_salted_sha1
  • session_simple_md5
  • session_validate_password
  • sql_from_object
  • sql_from_post
  • trace_bug
  • translate
  • uuid
  • Overview
  • Package
  • Class
  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: 836: 
<?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;
  }

}
API documentation generated by ApiGen