Andrew's Web Libraries (AWL)
classBrowser.php
1 <?php
17 require_once("AWLUtilities.php");
18 
22 $BrowserCurrentRow = (object) array();
23 
24 
25 
33 {
34  var $Field;
35  var $Header;
36  var $Format;
37  var $Sql;
38  var $Align;
39  var $Class;
40  var $Type;
41  var $Translatable;
42  var $Hook;
43  var $current_row;
44 
69  function __construct( $field, $header="", $align="", $format="", $sql="", $class="", $datatype="", $hook=null ) {
70  $this->Field = $field;
71  $this->Sql = $sql;
72  $this->Header = $header;
73  $this->Format = $format;
74  $this->Class = $class;
75  $this->Align = $align;
76  $this->Type = $datatype;
77  $this->Translatable = false;
78  $this->Hook = $hook;
79  }
80 
86  function GetTarget() {
87  if ( $this->Sql == "" ) return $this->Field;
88  return "$this->Sql AS $this->Field";
89  }
90 
104  function RenderHeader( $order_field, $order_direction, $browser_array_key=0, $forced_order=false ) {
105  global $c;
106  if ( $this->Align == "" ) $this->Align = "left";
107  $html = '<th class="'.$this->Align.'" '. ($this->Class == "" ? "" : "class=\"$this->Class\"") . '>';
108 
109  $direction = 'A';
110  $image = "";
111  if ( !$forced_order && $order_field == $this->Field ) {
112  if ( strtoupper( substr( $order_direction, 0, 1) ) == 'A' ) {
113  $image = 'down';
114  $direction = 'D';
115  }
116  else {
117  $image = 'up';
118  }
119  $image = "<img class=\"order\" src=\"$c->images/$image.gif\" alt=\"$image\" />";
120  }
121  if ( !isset($browser_array_key) || $browser_array_key == '' ) $browser_array_key = 0;
122  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">';
123  $html .= ($this->Header == "" ? $this->Field : $this->Header);
124  if ( !$forced_order ) $html .= "$image</a>";
125  $html .= "</th>\n";
126  return $html;
127  }
128 
129  function SetTranslatable() {
130  $this->Translatable = true;
131  }
132 
133  function RenderValue( $value, $extraclass = "" ) {
134  global $session;
135 
136  if ( $this->Type == 'date' || $this->Type == 'timestamp') {
137  $value = $session->FormattedDate( $value, $this->Type );
138  }
139 
140  if ( $this->Hook ) {
141  if ( function_exists($this->Hook) ) {
142  dbg_error_log( "Browser", ":Browser: Hook for $this->Hook on column $this->Field");
143  $value = call_user_func( $this->Hook, $value, $this->Field, $this->current_row );
144  } else {
145  dbg_error_log( "Browser", ":Browser: Hook for $this->Hook on column $this->Field doesn't exist");
146  }
147  }
148 
149  if ( $this->Translatable ) {
150  $value = translate($value);
151  }
152 
153  $value = str_replace( "\n", "<br />", $value );
154  if ( substr(strtolower($this->Format),0,3) == "<td" ) {
155  $html = sprintf($this->Format,$value);
156  }
157  else {
158  // These quite probably don't work. The CSS standard for multiple classes is 'class="a b c"' but is lightly
159  // implemented according to some web references. Perhaps modern browsers are better?
160  $class = $this->Align . ($this->Class == "" ? "" : " $this->Class") . ($extraclass == "" ? "" : " $extraclass");
161  if ( $class != "" ) $class = ' class="'.$class.'"';
162  $html = sprintf('<td%s>',$class);
163  $html .= ($this->Format == "" ? $value : sprintf($this->Format,$value,$value));
164  $html .= "</td>\n";
165  }
166  return $html;
167  }
168 }
169 
170 
178 class Browser
179 {
180  var $Title;
181  var $SubTitle;
182  var $FieldNames;
183  var $Columns;
184  var $HiddenColumns;
185  var $Joins;
186  var $Where;
187  var $Distinct;
188  var $Union;
189  var $Order;
190  var $OrderField;
191  var $OrderDirection;
192  var $OrderBrowserKey;
193  var $ForcedOrder;
194  var $Grouping;
195  var $Limit;
196  var $Offset;
197  var $Query;
198  var $BeginRow;
199  var $CloseRow;
200  var $BeginRowArgs;
201  var $BeginExtraRow;
202  var $CloseExtraRow;
203  var $BeginExtraRowArgs;
204  var $Totals;
205  var $TotalFuncs;
206  var $ExtraRows;
207  var $match_column;
208  var $match_value;
209  var $match_function;
210  var $DivOpen;
211  var $DivClose;
212 
218  function __construct( $title = "" ) {
219  global $c;
220  $this->Title = $title;
221  $this->SubTitle = "";
222  $this->Distinct = "";
223  $this->Order = "";
224  $this->Limit = "";
225  $this->Offset = "";
226  $this->BeginRow = "<tr class=\"row%d\">\n";
227  $this->CloseRow = "</tr>\n";
228  $this->BeginRowArgs = array('#even');
229  $this->Totals = array();
230  $this->Columns = array();
231  $this->HiddenColumns = array();
232  $this->FieldNames = array();
233  $this->DivOpen = '<div id="browser">';
234  $this->DivClose = '</div>';
235  $this->ForcedOrder = false;
236  dbg_error_log( "Browser", ":Browser: New browser called $title");
237  }
238 
264  function AddColumn( $field, $header="", $align="", $format="", $sql="", $class="", $datatype="", $hook=null ) {
265  $this->Columns[] = new BrowserColumn( $field, $header, $align, $format, $sql, $class, $datatype, $hook );
266  $this->FieldNames[$field] = count($this->Columns) - 1;
267  }
268 
279  function AddHidden( $field, $sql="" ) {
280  $this->HiddenColumns[] = new BrowserColumn( $field, "", "", "", $sql );
281  $this->FieldNames[$field] = count($this->Columns) - 1;
282  }
283 
293  function SetTitle( $new_title ) {
294  $this->Title = $new_title;
295  }
296 
297 
304  function Title( $new_title = null ) {
305  if ( isset($new_title) ) $this->Title = $new_title;
306  return $this->Title;
307  }
308 
309 
315  function SetTranslatable( $column_list ) {
316  $top = count($this->Columns);
317  for( $i=0; $i < $top; $i++ ) {
318  dbg_error_log( "Browser", "Comparing %s with column name list", $this->Columns[$i]->Field);
319  if ( in_array($this->Columns[$i]->Field,$column_list) ) $this->Columns[$i]->SetTranslatable();
320  }
321  $top = count($this->HiddenColumns);
322  for( $i=0; $i < $top; $i++ ) {
323  dbg_error_log( "Browser", "Comparing %s with column name list", $this->HiddenColumns[$i]->Field);
324  if ( in_array($this->HiddenColumns[$i]->Field,$column_list) ) $this->HiddenColumns[$i]->SetTranslatable();
325  }
326  }
327 
333  function SetSubTitle( $sub_title ) {
334  $this->SubTitle = $sub_title;
335  }
336 
343  function SetDiv( $open_div, $close_div ) {
344  $this->DivOpen = $open_div;
345  $this->DivClose = $close_div;
346  }
347 
357  function SetJoins( $join_list ) {
358  $this->Joins = $join_list;
359  }
360 
370  function SetUnion( $union_select ) {
371  $this->Union = $union_select;
372  }
373 
381  function SetWhere( $where_clause ) {
382  $this->Where = $where_clause;
383  }
384 
392  function SetDistinct( $distinct ) {
393  $this->Distinct = "DISTINCT ".$distinct;
394  }
395 
403  function SetLimit( $limit_n ) {
404  $this->Limit = "LIMIT ".intval($limit_n);
405  }
406 
414  function SetOffset( $offset_n ) {
415  $this->Offset = "OFFSET ".intval($offset_n);
416  }
417 
427  function MoreWhere( $operator, $more_where ) {
428  if ( $this->Where == "" ) {
429  $this->Where = $more_where;
430  return;
431  }
432  $this->Where = "$this->Where $operator $more_where";
433  }
434 
440  function AndWhere( $more_where ) {
441  $this->MoreWhere("AND",$more_where);
442  }
443 
449  function OrWhere( $more_where ) {
450  $this->MoreWhere("OR",$more_where);
451  }
452 
453  function AddGrouping( $field, $browser_array_key=0 ) {
454  if ( $this->Grouping == "" )
455  $this->Grouping = "GROUP BY ";
456  else
457  $this->Grouping .= ", ";
458 
459  $this->Grouping .= clean_string($field);
460  }
461 
462 
478  function AddOrder( $field, $direction, $browser_array_key=0, $secondary=0 ) {
479  $field = check_by_regex($field,'/^[^\'"!\\\\()\[\]|*\/{}&%@~;:?<>]+$/');
480  if ( ! isset($this->FieldNames[$field]) ) return;
481 
482  if ( !isset($this->Order) || $this->Order == "" )
483  $this->Order = "ORDER BY ";
484  else
485  $this->Order .= ", ";
486 
487  if ( $secondary == 0 ) {
488  $this->OrderField = $field;
489  $this->OrderBrowserKey = $browser_array_key;
490  }
491  $this->Order .= $field;
492 
493  if ( preg_match( '/^A/i', $direction) ) {
494  $this->Order .= " ASC";
495  if ( $secondary == 0)
496  $this->OrderDirection = 'A';
497  }
498  else {
499  $this->Order .= " DESC";
500  if ( $secondary == 0)
501  $this->OrderDirection = 'D';
502  }
503  }
504 
505 
512  function ForceOrder( $field, $direction ) {
513  $field = clean_string($field);
514  if ( ! isset($this->FieldNames[$field]) ) return;
515 
516  if ( $this->Order == "" )
517  $this->Order = "ORDER BY ";
518  else
519  $this->Order .= ", ";
520 
521  $this->Order .= $field;
522 
523  if ( preg_match( '/^A/i', $direction) ) {
524  $this->Order .= " ASC";
525  }
526  else {
527  $this->Order .= " DESC";
528  }
529 
530  $this->ForcedOrder = true;
531  }
532 
533 
539  function SetOrdering( $default_fld=null, $default_dir='A' , $browser_array_key=0 ) {
540  if ( isset( $_GET['o'][$browser_array_key] ) && isset($_GET['d'][$browser_array_key] ) ) {
541  $this->AddOrder( $_GET['o'][$browser_array_key], $_GET['d'][$browser_array_key], $browser_array_key );
542  }
543  else {
544  if ( ! isset($default_fld) ) $default_fld = $this->Columns[0];
545  $this->AddOrder( $default_fld, $default_dir, $browser_array_key );
546  }
547  }
548 
549 
561  function AddTotal( $column_name, $total_function = false ) {
562  $this->Totals[$column_name] = 0;
563  if ( $total_function != false ) {
564  $this->TotalFuncs[$column_name] = $total_function;
565  }
566  }
567 
568 
574  function GetTotal( $column_name ) {
575  return $this->Totals[$column_name];
576  }
577 
578 
601  function RowFormat( $beginrow, $closerow, $rowargs )
602  {
603  $argc = func_num_args();
604  $this->BeginRow = func_get_arg(0);
605  $this->CloseRow = func_get_arg(1);
606 
607  $this->BeginRowArgs = array();
608  for( $i=2; $i < $argc; $i++ ) {
609  $this->BeginRowArgs[] = func_get_arg($i);
610  }
611  }
612 
613 
626  function ExtraRowFormat( $beginrow, $closerow, $rowargs )
627  {
628  $argc = func_num_args();
629  $this->BeginExtraRow = func_get_arg(0);
630  $this->CloseExtraRow = func_get_arg(1);
631 
632  $this->BeginExtraRowArgs = array();
633  for( $i=2; $i < $argc; $i++ ) {
634  $this->BeginExtraRowArgs[] = func_get_arg($i);
635  }
636  }
637 
638 
647  function DoQuery() {
648  $target_fields = "";
649  foreach( $this->Columns AS $k => $column ) {
650  if ( $target_fields != "" ) $target_fields .= ", ";
651  $target_fields .= $column->GetTarget();
652  }
653  if ( isset($this->HiddenColumns) ) {
654  foreach( $this->HiddenColumns AS $k => $column ) {
655  if ( $target_fields != "" ) $target_fields .= ", ";
656  $target_fields .= $column->GetTarget();
657  }
658  }
659  $where_clause = ((isset($this->Where) && $this->Where != "") ? "WHERE $this->Where" : "" );
660  $sql = sprintf( "SELECT %s %s FROM %s %s %s ", $this->Distinct, $target_fields,
661  $this->Joins, $where_clause, $this->Grouping );
662  if ( "$this->Union" != "" ) {
663  $sql .= "UNION $this->Union ";
664  }
665  $sql .= $this->Order . ' ' . $this->Limit . ' ' . $this->Offset;
666  $this->Query = new AwlQuery( $sql );
667  return $this->Query->Exec("Browse:$this->Title:DoQuery");
668  }
669 
670 
676  function AddRow( $column_values ) {
677  if ( !isset($this->ExtraRows) || typeof($this->ExtraRows) != 'array' ) $this->ExtraRows = array();
678  $this->ExtraRows[] = &$column_values;
679  }
680 
681 
689  function MatchedRow( $column, $value, $function ) {
690  $this->match_column = $column;
691  $this->match_value = $value;
692  $this->match_function = $function;
693  }
694 
695 
705  function ValueReplacement($matches)
706  {
707  // as usual: $matches[0] is the complete match
708  // $matches[1] the match for the first subpattern
709  // enclosed in '##...##' and so on
710 
711  $field_name = $matches[1];
712  if ( !isset($this->current_row->{$field_name}) && substr($field_name,0,4) == "URL:" ) {
713  $field_name = substr($field_name,4);
714  $replacement = urlencode($this->current_row->{$field_name});
715  }
716  else {
717  $replacement = (isset($this->current_row->{$field_name}) ? $this->current_row->{$field_name} : '');
718  }
719  dbg_error_log( "Browser", ":ValueReplacement: Replacing %s with %s", $field_name, $replacement);
720  return $replacement;
721  }
722 
723 
734  function Render( $title_tag = null, $subtitle_tag = null ) {
735  global $c, $BrowserCurrentRow;
736 
737  if ( !isset($this->Query) ) $this->DoQuery(); // Ensure the query gets run before we render!
738 
739  dbg_error_log( "Browser", ":Render: browser $this->Title");
740  $html = $this->DivOpen;
741  if ( $this->Title != "" ) {
742  if ( !isset($title_tag) ) $title_tag = 'h1';
743  $html .= "<$title_tag>$this->Title</$title_tag>\n";
744  }
745  if ( $this->SubTitle != "" ) {
746  if ( !isset($subtitle_tag) ) $subtitle_tag = 'h2';
747  $html .= "<$subtitle_tag>$this->SubTitle</$subtitle_tag>\n";
748  }
749 
750  $html .= "<table id=\"browse_table\">\n";
751  $html .= "<thead><tr class=\"header\">\n";
752  foreach( $this->Columns AS $k => $column ) {
753  $html .= $column->RenderHeader( $this->OrderField, $this->OrderDirection, $this->OrderBrowserKey, $this->ForcedOrder );
754  }
755  $html .= "</tr></thead>\n<tbody>";
756 
757  $rowanswers = array();
758  while( $BrowserCurrentRow = $this->Query->Fetch() ) {
759 
760  // Work out the answers to any stuff that may be being substituted into the row start
762  foreach( $this->BeginRowArgs AS $k => $fld ) {
763  if ( isset($BrowserCurrentRow->{$fld}) ) {
764  $rowanswers[$k] = $BrowserCurrentRow->{$fld};
765  }
766  else {
767  switch( $fld ) {
768  case '#even':
769  $rowanswers[$k] = ($this->Query->rownum() % 2);
770  break;
771  default:
772  $rowanswers[$k] = $fld;
773  }
774  }
775  }
776  // Start the row
777  $row_html = vsprintf( preg_replace("/#@even@#/", ($this->Query->rownum() % 2), $this->BeginRow), $rowanswers);
778 
779  if ( isset($this->match_column) && isset($this->match_value) && $BrowserCurrentRow->{$this->match_column} == $this->match_value ) {
780  $row_html .= call_user_func( $this->match_function, $BrowserCurrentRow );
781  }
782  else {
783  // Each column
784  foreach( $this->Columns AS $k => $column ) {
785  $row_html .= $column->RenderValue( (isset($BrowserCurrentRow->{$column->Field})?$BrowserCurrentRow->{$column->Field}:'') );
786  if ( isset($this->Totals[$column->Field]) ) {
787  if ( isset($this->TotalFuncs[$column->Field]) && function_exists($this->TotalFuncs[$column->Field]) ) {
788  // Run the amount through the callback function $floatval = my_function( $row, $fieldval );
789  $this->Totals[$column->Field] += $this->TotalFuncs[$column->Field]( $BrowserCurrentRow, $BrowserCurrentRow->{$column->Field} );
790  }
791  else {
792  // Just add the amount
793  $this->Totals[$column->Field] += doubleval( preg_replace( '/[^0-9.-]/', '', $BrowserCurrentRow->{$column->Field} ));
794  }
795  }
796  }
797  }
798 
799  // Finish the row
800  $row_html .= preg_replace("/#@even@#/", ($this->Query->rownum() % 2), $this->CloseRow);
801  $this->current_row = $BrowserCurrentRow;
802  $html .= preg_replace_callback("/##([^#]+)##/", array( &$this, "ValueReplacement"), $row_html );
803  }
804 
805  if ( count($this->Totals) > 0 ) {
806  $BrowserCurrentRow = (object) "";
807  $row_html = "<tr class=\"totals\">\n";
808  foreach( $this->Columns AS $k => $column ) {
809  if ( isset($this->Totals[$column->Field]) ) {
810  $row_html .= $column->RenderValue( $this->Totals[$column->Field], "totals" );
811  }
812  else {
813  $row_html .= $column->RenderValue( "" );
814  }
815  }
816  $row_html .= "</tr>\n";
817  $this->current_row = $BrowserCurrentRow;
818  $html .= preg_replace_callback("/##([^#]+)##/", array( &$this, "ValueReplacement"), $row_html );
819  }
820 
821 
822  if ( is_array($this->ExtraRows) && count($this->ExtraRows) > 0 ) {
823  if ( !isset($this->BeginExtraRow) )
824  $this->BeginExtraRow = $this->BeginRow;
825  if ( !isset($this->CloseExtraRow) )
826  $this->CloseExtraRow = $this->CloseRow;
827  if ( !isset($this->BeginExtraRowArgs) )
828  $this->BeginExtraRowArgs = $this->BeginRowArgs;
829 
830  foreach( $this->ExtraRows AS $k => $v ) {
831  $BrowserCurrentRow = (object) $v;
832  // Work out the answers to any stuff that may be being substituted into the row start
833  foreach( $this->BeginExtraRowArgs AS $k => $fld ) {
834  if ( isset( $BrowserCurrentRow->{$fld} ) ) {
835  $rowanswers[$k] = $BrowserCurrentRow->{$fld};
836  }
837  else {
838  switch( $fld ) {
839  case '#even':
840  $rowanswers[$k] = ($this->Query->rownum() % 2);
841  break;
842  default:
843  $rowanswers[$k] = $fld;
844  }
845  }
846  }
847 
848  // Start the row
849  $row_html = vsprintf( preg_replace("/#@even@#/", ($this->Query->rownum() % 2), $this->BeginExtraRow), $rowanswers);
850 
851  if ( isset($this->match_column) && isset($this->match_value) && $BrowserCurrentRow->{$this->match_column} == $this->match_value ) {
852  $row_html .= call_user_func( $this->match_function, $BrowserCurrentRow );
853  }
854  else {
855  // Each column
856  foreach( $this->Columns AS $k => $column ) {
857  $row_html .= $column->RenderValue( (isset($BrowserCurrentRow->{$column->Field}) ? $BrowserCurrentRow->{$column->Field} : '') );
858  }
859  }
860 
861  // Finish the row
862  $row_html .= preg_replace("/#@even@#/", ($this->Query->rownum() % 2), $this->CloseExtraRow);
863  $this->current_row = $BrowserCurrentRow;
864  $html .= preg_replace_callback("/##([^#]+)##/", array( &$this, "ValueReplacement"), $row_html );
865  }
866  }
867 
868  $html .= "</tbody>\n</table>\n";
869  $html .= $this->DivClose;
870 
871  return $html;
872  }
873 
874 }
RenderHeader( $order_field, $order_direction, $browser_array_key=0, $forced_order=false)
__construct( $field, $header="", $align="", $format="", $sql="", $class="", $datatype="", $hook=null)
AddHidden( $field, $sql="")
SetDistinct( $distinct)
OrWhere( $more_where)
AddOrder( $field, $direction, $browser_array_key=0, $secondary=0)
SetUnion( $union_select)
SetJoins( $join_list)
SetSubTitle( $sub_title)
SetTranslatable( $column_list)
ForceOrder( $field, $direction)
SetTitle( $new_title)
SetDiv( $open_div, $close_div)
SetLimit( $limit_n)
ValueReplacement($matches)
ExtraRowFormat( $beginrow, $closerow, $rowargs)
GetTotal( $column_name)
AndWhere( $more_where)
RowFormat( $beginrow, $closerow, $rowargs)
AddColumn( $field, $header="", $align="", $format="", $sql="", $class="", $datatype="", $hook=null)
SetWhere( $where_clause)
AddTotal( $column_name, $total_function=false)
__construct( $title="")
MatchedRow( $column, $value, $function)
MoreWhere( $operator, $more_where)
Render( $title_tag=null, $subtitle_tag=null)
SetOffset( $offset_n)
Title( $new_title=null)
SetOrdering( $default_fld=null, $default_dir='A', $browser_array_key=0)