Activated strict SQL mode, minor SQL injection fix, fixed _vl() debug helper.
[fa-stable.git] / includes / ui / class.crud_view.inc
1 <?php
2 /**********************************************************************
3     Copyright (C) FrontAccounting, LLC.
4         Released under the terms of the GNU General Public License, GPL, 
5         as published by the Free Software Foundation, either version 3 
6         of the License, or (at your option) any later version.
7     This program is distributed in the hope that it will be useful,
8     but WITHOUT ANY WARRANTY; without even the implied warranty of
9     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  
10     See the License here <http://www.gnu.org/licenses/gpl-3.0.html>.
11 ***********************************************************************/
12
13 include_once $path_to_root.'/includes/db/class.data_set.inc';
14
15 class UI {
16         var $ui_mode;
17
18         // formatters
19         function text($value, $name='', $opts=array())
20         {
21                 $text = array('label', 'size'=>"", 'max'=>"", 'title'=>false,   'labparams'=>"", 'post_label'=>"", 'inparams'=>"");
22                 $opts = array_merge($text, $opts);
23
24                 if (!$name)
25                         return $value;
26
27                 call_user_func_array('text_cells', $opts);
28         }
29 }
30 /*
31         TODO: for php5:
32         . use __construct in base class to avoid need for implicit call to parent constructor.
33         . use private instead _* function name convention
34 */
35 //
36 //      User input-output conversions.
37 //
38 class user_view {
39
40         var     $data;          // data in php format
41         var $fields;    // input fields format descriptions
42         var $errors = array();
43         var $dec;
44         var $name;
45
46         function __construct($name)
47         {
48                 $this->name = $name;
49         }
50
51         function error($msg, $context=null)
52         {
53                 // save error message
54                 if (!isset($context))
55                         $context = count($this->errors);
56                 $this->errors[$context] = $msg;
57
58                 return false;
59         }
60         /*
61                 Input/output formatters - convert values between php/user domains.
62         */
63         function _format_input($value, $fmt)
64         {
65                 switch($fmt) {
66                         case 'stock':
67                                 $this->dec = get_qty_dec($value);
68                                 return $value;
69                         case 'price':
70                         case 'qty':
71                         case 'number':
72                                     return user_numeric($value);
73                         case 'percent':
74                                         return user_numeric($value)/100;
75                         case 'check':
76                                         return isset($value) ? 1 : 0;
77                         case 'text':
78                         case 'date':
79                         default:
80                                 return $value;
81                 }
82         }
83         //
84         // Returns formatted value
85         //
86         function _format_output($value, $fmt)
87         {
88                 switch($fmt) {
89                         case 'price':
90                                 return price_format($value);
91                         case 'qty':
92                                 return number_format2($value, $this->dec);
93                         case 'number':
94                                 return number_format2($value);
95                         case 'percent':
96                                 return percent_format($value*100);
97                         case 'check':
98                                 return !empty($value);
99                         case 'stock':
100                                 $this->dec = get_qty_dec($value); // retrieve dec for use in following qty fields
101                         case 'text':
102                         case 'date':
103                         default:
104                                 return $value;
105                 }
106         }
107         /**
108         *       Returns html element for given field
109         *       @mode - true for edit, false for read-only
110         **/
111         function _format_cells($value, $fmt, $mode)
112         {
113                 $value = $this->_format_output($fmt);
114
115                 // available formatters with parameters
116                 $formatters = array(
117                         'email_cell' => array('label', 'params'=>'', 'id'=>null),
118                         'amount_cell' => array('label'=>null, 'bold'=>false, 'params'=>'', 'id'=>null),
119                         'text_cells' => array('label', 'name', 'value'=>null, 'size'=>"", 'max'=>"", 'title'=>false,
120                                 'labparams'=>"", 'post_label'=>"", 'inparams'=>"")
121                 );
122                 // format functions used in various modes
123                 $formats = array(
124                         // field format => (view [,edit])
125                         '' => array('label_cell', 'text_cells'), // default
126                         'price' => array('label_cell', 'amount_cell'),
127                         'qty',
128                         'number',
129                         'percent',
130                         'stock',
131                         'text',
132                         'date',
133                 );
134         }
135
136         /**
137         *
138         *       PHP->user format values convertion.
139         *
140         **/
141         function set_output($data=null, &$output=null)
142         {
143                 if (isset($data))
144                         $this->data = $data;
145
146                 if (!isset($output)) {
147                         $output = &$_POST;
148                         $prefix = $this->name;
149                 } else
150                         $prefix = '';
151
152                 foreach($this->fields as $name => $fmt) {
153
154                         if (is_int($name)) {
155                                 $name = $fmt;
156                                 $fmt = array();
157                         } elseif (!is_array($fmt))
158                                 $fmt = array('fmt' => $fmt);
159
160                         $post = $prefix.(isset($fmt['post']) ? $fmt['post'] : $name);
161
162                         $fld = isset($fmt['fld']) ? $fmt['fld'] : $name;
163
164                         if (is_object($this->data))
165                                 $value = isset($this->data->$fld) ?     $this->data->$fld : @$fmt['dflt'];
166                         else
167                                 $value = isset($this->data[$fld]) ?     $this->data[$fld] : @$fmt['dflt'];
168
169                         if (isset($value))
170                                 $output[$post] = $this->_format_output($value, @$fmt['fmt']);
171                 }
172         }
173
174         /**
175         *
176         *       User->php format values convertion.
177         *       $input - data in user format
178         *       $all - return also null values for non-existing input field.
179         **/
180         function get_input($input=null, $all=false)
181         {
182                 if (!isset($input))
183                         $input = $_POST;
184
185                 if ($this->name)        // strip view name prefix
186                     foreach($input as $postkey=>$postval )
187                 {
188                                 if (strpos($postkey, $this->name) === 0)
189                                 {
190                                         $id = substr($postkey, strlen($this->name));
191                                         $input[$id] = $postval;
192                                 }
193                                 unset($input[$postkey]);
194                 }
195
196         $data = array();
197                 foreach ($this->fields as $name => $fmt) {
198                         if (is_int($name)) {    // direct string passed: this is name of field
199                                 $name = $fmt;
200                                 $fmt = array(); // default format
201                         } elseif (!is_array($fmt))
202                                 $fmt = array('fmt' => $fmt);
203                         $post = isset($fmt['post']) ? $fmt['post'] : $name; // input name (default to field name)
204                         $fld = isset($fmt['fld']) ? $fmt['fld'] : $name;        // input value (default to field name)
205
206
207 //                      if ($all || array_key_exists($post, $input))
208 //                      {
209                                 if (@$fmt['fmt'] == 'check')
210                                         $value = @$input[$post] ? 1 : 0;
211                                 else
212                                         $value = $this->_format_input(@$input[$post], @$fmt['fmt']);
213
214 //                              if (is_array($data))
215                                 if ($all || isset($value))
216                                         $data[$fld] = $value;
217 //                              else
218 //                                      $data->$fld = $value;
219 //                      }
220                 }
221
222                 return $data;
223         }
224
225         //--------------------------------------------------------
226         //
227         //      Return data formatted according to field descriptions.
228         //
229         function get_fields_views($input=null, $mode=null)
230         {
231
232                 if (!isset($input))
233                         $input = $_POST;
234
235                 $view = array();
236                 foreach ($this->fields as $name => $fmt) {
237                         if (is_int($name)) {
238                                 $name = $fmt;
239                                 $fmt = array();
240                         }
241                         $post = isset($fmt['post']) ? $fmt['post'] : $name;
242                         $fld = isset($fmt['fld']) ? $fmt['fld'] : $name;
243
244                         $value = $this->_format_cells(@$input[$post], @$fmt['fmt'], $mode);
245
246                         $view[$fld] = $value;
247                 }
248                 return $view;
249         }
250
251 }
252
253 //
254 //      Template for simple table editors
255 //
256
257 class simple_crud_view extends user_view {
258         var $name;
259                 // object status:
260         var $Mode = 'RESET';
261         var $selected_id;
262         var $prev_id;
263
264         var $_none = ''; // selector value when no item is selected
265         var $pre_handlers; // control buttons and related methods called before view display
266         var $views;
267         var $data = array();
268         var $fields;
269         var $tool_buttons;
270         var $options = array(
271                 'delete' => true,               // true or message for successfull action.
272                 'update' => true,
273                 'insert' => true,
274                 'clone' => true,
275         );
276         var $dec;
277         var $data_set;
278         var $display_both = false; //when set to true both list and editor are displayed all the time (eventually set in sub classes)
279         //
280         //
281         function __construct($name, $data_set = null, $options=array())
282         {
283                 parent::__construct($name);
284
285                 $this->options = array_merge($this->options, $options);
286
287                 $this->views[''] = 'list_view';                 // default view
288
289                 if ($this->options['update'])
290                         $this->_add_action('Edit', '_edit', _('Edit'), _('Edit document line'), ICON_EDIT, '',
291                                 'editor_view');
292
293                 if ($this->options['delete'])
294                         $this->_add_action('Delete', '_delete', _('Delete'), _('Remove line from document'), ICON_DELETE, '',
295                                 'list_view');
296
297                 if ($this->options['update'])
298                         $this->_add_action('UPDATE', '_update', _('Update'), _('Submit changes'), ICON_UPDATE, 'default',
299                                 'editor_view');
300
301                 $this->_add_action('RESET', '_cancel', _('Cancel'), _('Cancel changes'), ICON_ESCAPE, 'cancel',
302                         'list_view');
303
304                 if ($this->options['insert'])
305                         $this->_add_action('ADD', '_add', _('Add'), _('Add new'), ICON_ADD, 'default',
306                                 'editor_view');
307
308                 if ($this->options['insert'])
309                         $this->_add_action('NEW', '_add', _('New'), _('Add new'), ICON_ADD, 'default',
310                                 'editor_view');
311
312                 if ($this->options['clone'])
313                         $this->_add_action('CLONE', '_cloning', _('Clone'), _('Clone'), ICON_ADD, '',
314                                 'editor_view');
315
316                 $this->data_set = $data_set;
317                 $this->fields = $data_set->fields;
318
319 //              $this->_prev_status();
320         }
321
322         function _add_action($name, $handler, $but_value=null, $but_title=false, $but_icon=false, $aspect='', $view=null)
323         {
324                 $this->pre_handlers[$name] = $handler;
325
326                 if ($but_value)
327                         $this->tool_buttons[$name] = array($but_value, $but_title, $but_icon, $aspect);
328
329                 if ($view)
330                         $this->_add_mode($name, $view);
331         }
332
333         function _add_mode($name, $view)
334         {
335                 $this->views[$name] = $view;
336         }
337
338         function _prev_status()
339         {
340                 // Restore previous mode/key (obsolete for views stored in session)
341
342                 $mod = get_post($this->name.'Mode', $this->Mode);
343                 if ($mod) {
344                         if (is_array($mod)) {
345                                 $val = key($mod);
346                                 $this->selected_id = $val!==null ? @quoted_printable_decode($val) : $this->_none;
347                                 $mod = $mod[$val];
348                         } else {
349                                 $val = $mod;
350                                 $this->selected_id = $this->_none;
351                         }
352                 }
353                 $this->Mode = $mod;
354         }
355
356         function _check_mode()
357         {
358                 global $Ajax;
359
360                 $mod = '';//$this->Mode;
361                 // Detect action (mode change)
362                 foreach (array_keys($this->pre_handlers) as $m) { // check button controls
363
364                         if (isset($_POST[$this->name.$m])) {
365                                 unset($_POST['_focus']); // focus on first form entry
366                                 $Ajax->activate($this->name.'_div');
367                                 $Ajax->activate($this->name.'_controls');
368                                 $val = is_array($_POST[$this->name.$m]) ? key($_POST[$this->name.$m]) : null;
369                                 $this->prev_id = $this->selected_id;
370                                 $this->selected_id = $val!==null ? @quoted_printable_decode($val) : $this->_none;
371                                 $mod = $m; break;
372                         }
373                 }
374                 if (!$mod && $_SERVER['REQUEST_METHOD'] == 'GET') // initialize on every GET
375                         $mod = 'RESET';
376
377                 return $mod;
378         }
379
380         function display_error()
381         {
382                 $this->errors = array_merge($this->data_set->errors, $this->errors);
383                 $firsterr = reset($this->errors);
384                 $field = key($this->errors);
385
386                 if (!is_numeric($field))
387                         set_focus($this->name.$field);
388                 display_error($firsterr);
389
390                 $this->errors = array(); // clean up to prevent false errors on object reuse
391         }
392         //
393         //      Set record for edition
394         //
395         function _edit($mode)
396         {
397                 if ($this->selected_id != $this->prev_id || $mode != $this->Mode) { // get record for edition
398                         $this->data = $this->data_set->get($this->selected_id !== $this->_none ? $this->selected_id : null);
399                         $this->set_output();
400                 }
401 //              if ($this->Mode != $mode) {
402 //              }
403
404 //              else
405 //                      $this->display_error();
406
407                 $this->Mode = $mode;
408         }
409         //
410         //      Update record after edition
411         //
412         function _update($mode)
413         {
414                 if (!$this->options['update'])
415                         return;
416
417                 $this->data = $this->get_input();
418                 if ($this->data_set->update_check($this->selected_id, $this->data) && $this->data_set->update($this->selected_id, $this->data)) {
419                                 $this->selected_id = $this->_none;
420                                 $this->Mode = 'RESET';
421                                 $this->_cancel();
422                                 if (is_string($this->options['update']))
423                                         display_notification($this->options['update']);
424                                 return;
425                 }
426                 else
427                         $this->display_error();
428
429                 $this->Mode = $mode;
430         }
431         //
432         //      Add new record
433         //
434         function _add($mode)
435         {
436                 if (!$this->options['insert'])
437                         return;
438
439                 if ($mode == 'ADD') {
440                         $this->data = $this->get_input();
441                         if ($this->data_set->insert_check($this->data) && ($this->data_set->insert($this->data) !== false)) {
442                                 $this->_cancel();
443                                 if (is_string($this->options['insert']))
444                                         display_notification($this->options['insert']);
445                                 $this->Mode = 'RESET';
446                                 return;
447                         }
448                         else
449                                 $this->display_error();
450                 } else {// mode==NEW
451                                 $this->data = $this->data_set->get();
452                 }
453                 $this->Mode = 'ADD';
454         }
455         //
456         //      Delete selected  record
457         //
458         function _delete()
459         {
460                 if (!$this->options['delete'])
461                         return;
462
463                 if ($this->data_set->delete_check($this->selected_id) && $this->data_set->delete($this->selected_id))
464                 {
465                         if (is_string($this->options['delete']))
466                                 display_notification($this->options['delete']);
467                 } else
468                         $this->display_error();
469                 $this->_cancel();
470         }
471         //
472         //      Return to listing view
473         //
474         function _cancel()
475         {
476                 global $Ajax;
477                 $this->selected_id = $this->_none;
478                 if ($this->display_both)
479                 {
480                         $this->data = $this->data_set->get();
481                         $this->set_output();
482                 }
483                 $this->cancel();
484                 if (!$this->display_both)
485                         $Ajax->activate($this->name.'_div');
486                 $this->Mode = 'RESET';
487         }
488         //
489         // Clone record for new edition
490         //
491         function _cloning()
492         {
493                 if (!$this->options['clone'])
494                         return;
495                 $this->Mode = 'ADD';
496                 $this->_edit('Edit');
497                 $this->selected_id = $this->_none;
498         }
499         /*
500                 Generate form controls
501         */
502
503         function _record_controls($list_view = false)
504         {
505                 $clone = $this->options['clone'] && $this->selected_id != $this->_none;
506
507                 div_start($this->name.'_controls');
508                 echo "<center>";
509
510                 if ($list_view)
511                         $this->action_button('NEW');
512                 else {
513                         if ($this->Mode == 'NEW' || $this->selected_id==$this->_none)
514                         {
515                                 $this->action_button('ADD');
516                                 $this->action_button('RESET');
517                         } else {
518                                 $this->action_button('UPDATE', $this->selected_id);
519                                 if ($clone && $this->display_both) 
520                                         $this->action_button('CLONE', $this->selected_id);
521                                 $this->action_button('RESET');
522                         }
523                 }
524
525                 echo "</center>";
526                 div_end();
527         }
528
529         //===========================================================================
530         // Public functions
531         //
532
533         //
534         //      Submit buttons for form actions
535         //
536         function action_button($action, $selected_id=null)
537         {
538                 list($value, $title, $icon, $aspect) = $this->tool_buttons[$action];
539                 submit($this->name.$action.(isset($selected_id) ? "[$selected_id]" : ''), $value, true, $title, $aspect, $icon);
540         }
541
542         //
543         //      Tool button for grid line actions.
544         //
545         function tool_button($name, $selected_id=null, $params='')
546         {
547                 $b = $this->tool_buttons[$name];
548
549                 return "<td align='center' $params>"
550                         .button( "{$this->name}$name"
551                                 .($selected_id === null || $selected_id === $this->_none ? '': "[$selected_id]"),
552                                 $b[0], $b[1], $b[2], $b[3])."</td>";
553         }
554         //
555         //      Main function - display current CRUD editor content
556         //
557         function show($Mode=null)
558         {
559                 if (!isset($Mode))
560                         $Mode = $this->_check_mode(true);
561
562                 div_start($this->name.'_div');
563
564                 if (array_key_exists($Mode, $this->pre_handlers)) {
565                         $fun = $this->pre_handlers[$Mode];
566                         if (is_array($fun))
567                                 call_user_func($fun, $Mode);
568                         else
569                                 $this->$fun($Mode);
570                 }
571
572                 if (isset($this->views[$this->Mode]))
573                         $this->{$this->views[$this->Mode]}($Mode);
574                 else
575                         $this->{$this->views['']}($Mode); // default view
576
577                 // this is needed only when we use temporary crud object together with ajax screen updates
578                 hidden($this->name.'Mode'.'['.$this->selected_id.']', $this->Mode);
579                 div_end();
580         }
581         
582         //
583         //      Optional things like focus set performed on edition cancel.
584         //
585         function cancel()
586         {
587         }
588
589         //
590         //      Show database content in pager/table
591         //      parameter $Mode contains new mode on change, or '' otherwise
592         //
593         function list_view($Mode) {
594                 display_notification(__FUNCTION__. ' is not defined...');
595         }
596
597         //
598         //      Show record editor screen content
599         //      parameter $Mode contains new mode on change, or '' otherwise
600         //
601         function editor_view($Mode) {
602                 display_notification(__FUNCTION__. ' is not defined...');
603         }
604 };
605
606 class selector_crud_view extends simple_crud_view {
607
608         function __construct($name, $data_set = null, $options=array())
609         {
610                 $this->display_both = true;
611                 parent::__construct($name, $data_set, $options);
612         }
613
614         function _check_mode()
615         {
616                 global $Ajax;
617
618                 // list controls lookup
619                 $prev_mode = $this->Mode;
620                 $mod = '';
621
622                 $list = $this->name.'_id';
623                 $this->prev_id = $this->selected_id;
624                 $this->selected_id = get_post($list);
625                 if (list_updated($list)) {
626                         $Ajax->activate($this->name);
627                         $Ajax->activate($this->name.'_controls');
628                         $mod = $this->selected_id == $this->_none ? 'RESET' : 'Edit';
629                 } else {
630                         $mod = simple_crud_view::_check_mode();
631                 }
632
633                 if ($mod != $prev_mode) {
634                         $Ajax->activate($this->name.'_controls');
635                 }
636                 if (get_post('_show_inactive_update'))
637                 {
638                         $Ajax->activate($this->name.'_id');
639                 }
640
641                 $_POST[$list] = $this->selected_id;
642                 return $mod;
643         }
644
645         function cancel()
646         {
647                 global $Ajax;
648
649                 $_POST[$this->name.'_id'] = $this->selected_id = $this->_none;
650                 $Ajax->activate($this->name.'_id');
651         }
652 }
653
654 //
655 //      Template for line table editors
656 //
657
658 class table_crud_view extends simple_crud_view {
659
660         var $title;
661
662         function __construct($name, &$data_set = null, $options=array())
663         {
664                 $this->display_both = true;
665                 parent::__construct($name, $data_set, $options);
666         }
667         /**
668         *
669         *       Returns selector string from data or current selected_id
670         *
671         **/
672         function curr_id($data=null)
673         {
674                 if ($data)
675                         return $data ? implode('_', array_intersect_key($data, array_fill_keys($this->key, true))) : null; // php 5.2.0
676                 else
677                         return $this->selected_id;
678         }
679         /**
680         *
681         *       Show single line from record table
682         *
683         **/
684         function show_record_row($line_no, $record=array())
685         {
686 /* TBD
687                 $id = $this->curr_id($record); // extract key
688
689                 $view = array();
690                 $this->set_output($line, $view);
691
692                 $editables = array();
693                 if ($this->options['edit'] && $this->Mode=='Edit' && $this->selected_id==$id)
694                 {
695                         $editables = $this->data_set->edit_status($key);
696                 }
697                 $ctrls = $this->get_field_views($record, $editables);
698                 alt_table_row_color($k);
699                 foreach($ctrls as $key => $fld)
700                 {
701                         echo '<td>';
702                         echo $ctrls[$key];
703                         echo '</td>';
704                 }
705
706                 if ($this->options['edit']) {
707                         echo '<td>';
708                         $this->tool_button($this->selected_id==$id ? 'UPDATE' : 'Edit', $id);
709                         echo '</td>';
710                 }
711                 if ($this->options['delete']$this->selected_id==$id) {
712                         echo '<td>';
713                         $this->tool_button( ? 'Cancel' : 'Delete', $id);
714                         echo '</td>';
715                 }
716                 end_row();
717
718                 if ($this->options['insert'])
719                 {
720                         alt_table_row_color($k);
721                         $this->new_line($line_no, $line);
722                         end_row();
723                 }
724 */
725         }
726         function show_list_headers()
727         {
728         }
729         //
730         //      Main function - display current CRUD table content
731         //
732         function show($Mode=null)
733         {
734                 if (!isset($Mode))
735                         $Mode = $this->_check_mode(true);
736                 div_start($this->name.'_div');
737
738                 if (array_key_exists($Mode, $this->pre_handlers))
739                 {
740                         $fun = $this->pre_handlers[$Mode];
741                         if (is_array($fun))
742                         {
743                                 if (is_object($fun[0]))
744                                         $fun[0]->$fun[1]($Mode);
745                         } else
746                         $this->$fun($Mode);
747                 }
748
749                 if ($this->title)
750                         display_heading($this->title);
751
752                 start_table(TABLESTYLE, "width=95%");
753                 $this->show_list_headers();
754
755                 foreach($this->data_set->get_all() as $line_no => $line) {
756                         $this->show_record_row($line_no, $line);
757                 }
758                 $this->show_record_row();
759
760                 end_table();
761                 hidden($this->name.'Mode'.'['.$this->selected_id.']', $this->Mode);
762                 div_end();
763         }
764
765 };