Replace deprecated split() calls.
[awl.git] / inc / classEditor.php
blob2b718157a04093aa5c7fecf6c69e94c6a960ac7e
1 <?php
2 /**
3 * Class for editing a record using a templated form.
5 * @package awl
6 * @subpackage classEditor
7 * @author Andrew McMillan <andrew@mcmillan.net.nz>
8 * @copyright Catalyst IT Ltd, Morphoss Ltd <http://www.morphoss.com/>
9 * @license http://gnu.org/copyleft/gpl.html GNU GPL v2
12 require_once("DataUpdate.php");
13 require_once("DataEntry.php");
15 /**
16 * A class for the fields in the editor
17 * @package awl
19 class EditorField
21 var $Field;
22 var $Sql;
23 var $Value;
24 var $Attributes;
25 var $LookupSql;
26 var $OptionList;
28 function __construct( $field, $sql="", $lookup_sql="" ) {
29 global $session;
30 $this->Field = $field;
31 $this->Sql = $sql;
32 $this->LookupSql = $lookup_sql;
33 $this->Attributes = array();
36 function Set($value) {
37 $this->Value = $value;
40 function SetSql( $sql ) {
41 $this->Sql = $sql;
44 function SetLookup( $lookup_sql ) {
45 $this->LookupSql = $lookup_sql;
48 function SetOptionList( $options, $current = null, $parameters = null) {
49 if ( gettype($options) == 'array' ) {
50 $this->OptionList = '';
52 if ( is_array($parameters) ) {
53 if ( isset($parameters['maxwidth']) ) $maxwidth = max(4,intval($parameters['maxwidth']));
54 if ( isset($parameters['translate']) ) $translate = true;
57 foreach( $options AS $k => $v ) {
58 if (is_array($current)) {
59 $selected = ( ( in_array($k,$current,true) || in_array($v,$current,true)) ? ' selected="selected"' : '' );
61 else {
62 $selected = ( ( "$k" == "$current" || "$v" == "$current" ) ? ' selected="selected"' : '' );
64 if ( isset($translate) ) $v = translate( $v );
65 if ( isset($maxwidth) ) $v = substr( $v, 0, $maxwidth);
66 $this->OptionList .= "<option value=\"".htmlspecialchars($k)."\"$selected>".htmlspecialchars($v)."</option>";
69 else {
70 $this->OptionList = $options;
74 function GetTarget() {
75 if ( $this->Sql == "" ) return $this->Field;
76 return "$this->Sql AS $this->Field";
79 function AddAttribute( $k, $v ) {
80 $this->Attributes[$k] = $v;
83 function RenderAttributes() {
84 $attributes = "";
85 if ( count($this->Attributes) == 0 ) return $attributes;
86 foreach( $this->Attributes AS $k => $v ) {
87 $attributes .= " $k=\"" . str_replace('"', '\\"', $v) . '"';
89 return $attributes;
95 /**
96 * The class for the Editor form in full
97 * @package awl
99 class Editor
101 var $Title;
102 var $Action;
103 var $Fields;
104 var $OrderedFields;
105 var $BaseTable;
106 var $Joins;
107 var $Where;
108 var $NewWhere;
109 var $Order;
110 var $Limit;
111 var $Query;
112 var $Template;
113 var $RecordAvailable;
114 var $Record;
115 var $SubmitName;
116 var $Id;
118 function __construct( $title = "", $fields = null ) {
119 global $c, $session, $form_id_increment;
120 $this->Title = $title;
121 $this->Order = "";
122 $this->Limit = "";
123 $this->Template = "";
124 $this->RecordAvailable = false;
125 $this->SubmitName = 'submit';
126 $form_id_increment = (isset($form_id_increment)? ++$form_id_increment : 1);
127 $this->Id = 'editor_'.$form_id_increment;
129 if ( isset($fields) ) {
130 if ( is_array($fields) ) {
131 foreach( $fields AS $k => $v ) {
132 $this->AddField($v);
135 else if ( is_string($fields) ) {
136 // We've been given a table name, so get all fields for it.
137 $this->BaseTable = $fields;
138 $field_list = get_fields($fields);
139 foreach( $field_list AS $k => $v ) {
140 $this->AddField($k);
144 @dbg_error_log( 'editor', 'DBG: New editor called %s', $title);
147 function &AddField( $field, $sql="", $lookup_sql="" ) {
148 $this->Fields[$field] = new EditorField( $field, $sql, $lookup_sql );
149 $this->OrderedFields[] = $field;
150 return $this->Fields[$field];
153 function SetSql( $field, $sql ) {
154 $this->Fields[$field]->SetSql( $sql );
157 function SetLookup( $field, $lookup_sql ) {
158 if (is_object($this->Fields[$field])) {
159 $this->Fields[$field]->SetLookup( $lookup_sql );
163 function Value( $value_field_name ) {
164 if ( !isset($this->Record->{$value_field_name}) ) return null;
165 return $this->Record->{$value_field_name};
168 function Assign( $value_field_name, $new_value ) {
169 $this->Record->{$value_field_name} = $new_value;
172 function Id( $id = null ) {
173 if ( isset($id) ) $this->Id = preg_replace( '#[^a-z0-9_+-]#', '', $id);
174 return $this->Id;
177 function SetOptionList( $field, $options, $current = null, $parameters = null) {
178 $this->Fields[$field]->SetOptionList( $options, $current, $parameters );
181 function AddAttribute( $field, $k, $v ) {
182 $this->Fields[$field]->AddAttribute($k,$v);
186 function SetBaseTable( $base_table ) {
187 $this->BaseTable = $base_table;
190 function SetJoins( $join_list ) {
191 $this->Joins = $join_list;
196 * Accessor for the Title for the browse, which could set the title also.
198 * @param string $new_title The new title for the browser
199 * @return string The current title for the browser
201 function Title( $new_title = null ) {
202 if ( isset($new_title) ) $this->Title = $new_title;
203 return $this->Title;
207 function SetSubmitName( $new_submit ) {
208 $this->SubmitName = $new_submit;
211 function IsSubmit() {
212 return isset($_POST[$this->SubmitName]);
215 function IsUpdate() {
216 $is_update = $this->Available();
217 if ( isset( $_POST['_editor_action']) && isset( $_POST['_editor_action'][$this->Id]) ) {
218 $is_update = ( $_POST['_editor_action'][$this->Id] == 'update' );
219 @dbg_error_log( 'editor', 'Checking update: %s => %d', $_POST['_editor_action'][$this->Id], $is_update );
221 return $is_update;
224 function IsCreate() {
225 return ! $this->IsUpdate();
228 function SetWhere( $where_clause ) {
229 $this->Where = $where_clause;
232 function WhereNewRecord( $where_clause ) {
233 $this->NewWhere = $where_clause;
236 function MoreWhere( $operator, $more_where ) {
237 if ( $this->Where == "" ) {
238 $this->Where = $more_where;
239 return;
241 $this->Where = "$this->Where $operator $more_where";
244 function AndWhere( $more_where ) {
245 $this->MoreWhere("AND",$more_where);
248 function OrWhere( $more_where ) {
249 $this->MoreWhere("OR",$more_where);
252 function SetTemplate( $template ) {
253 $this->Template = $template;
256 function Layout( $template ) {
257 if ( strstr( $template, '##form##' ) === false && stristr( $template, '<form' ) === false ) $template = '##form##' . $template;
258 if ( stristr( $template, '</form' ) === false ) $template .= '</form>';
259 $this->Template = $template;
262 function Available( ) {
263 return $this->RecordAvailable;
266 function SetRecord( $row ) {
267 $this->Record = $row;
268 $this->RecordAvailable = is_object($this->Record);
269 return $this->Record;
273 * Set some particular values to the ones from the array.
275 * @param array $values An array of fieldname / value pairs
277 function Initialise( $values ) {
278 $this->RecordAvailable = false;
279 foreach( $values AS $fname => $value ) {
280 $this->Record->{$fname} = $value;
286 * This will assign $_POST values to the internal Values object for each
287 * field that exists in the Fields array.
289 function PostToValues( $prefix = '' ) {
290 foreach ( $this->Fields AS $fname => $fld ) {
291 @dbg_error_log( 'editor', ":PostToValues: %s => %s", $fname, $_POST["$prefix$fname"] );
292 if ( isset($_POST[$prefix.$fname]) ) {
293 $this->Record->{$fname} = $_POST[$prefix.$fname];
294 @dbg_error_log( 'editor', ":PostToValues: %s => %s", $fname, $_POST["$prefix$fname"] );
299 function GetRecord( $where = "" ) {
300 global $session;
301 $target_fields = "";
302 foreach( $this->Fields AS $k => $column ) {
303 if ( $target_fields != "" ) $target_fields .= ", ";
304 $target_fields .= $column->GetTarget();
306 if ( $where == "" ) $where = $this->Where;
307 $sql = sprintf( "SELECT %s FROM %s %s WHERE %s %s %s",
308 $target_fields, $this->BaseTable, $this->Joins, $where, $this->Order, $this->Limit);
309 $this->Query = new PgQuery( $sql );
310 @dbg_error_log( 'editor', "DBG: EditorGetQry: %s", $sql );
311 if ( $this->Query->Exec("Browse:$this->Title:DoQuery") ) {
312 $this->Record = $this->Query->Fetch();
313 $this->RecordAvailable = is_object($this->Record);
315 if ( !$this->RecordAvailable ) {
316 $this->Record = (object) array();
318 return $this->Record;
323 * Replace parts into the form template.
324 * @param array $matches The matches found which preg_replace_callback is calling us for.
325 * @return string What we want to replace this match with.
327 function ReplaceEditorPart($matches)
329 global $session;
331 // $matches[0] is the complete match
332 switch( $matches[0] ) {
333 case "##form##": /** @todo It might be nice to construct a form ID */
334 return sprintf('<form method="POST" enctype="multipart/form-data" class="editor" id="%s">', $this->Id);
335 case "##submit##":
336 $action = ( $this->RecordAvailable ? 'update' : 'insert' );
337 $submittype = ($this->RecordAvailable ? translate('Apply Changes') : translate('Create'));
338 return sprintf('<input type="hidden" name="_editor_action[%s]" value="%s"><input type="submit" class="submit" name="%s" value="%s">',
339 $this->Id, $action, $this->SubmitName, $submittype );
342 // $matches[1] the match for the first subpattern
343 // enclosed in '(...)' and so on
344 $field_name = $matches[1];
345 $what_part = $matches[3];
346 $part3 = (isset($matches[5]) ? $matches[5] : null);
348 $value_field_name = $field_name;
349 if ( substr($field_name,0,4) == 'xxxx' ) {
350 // Sometimes we will prepend 'xxxx' to the field name so that the field
351 // name differs from the column name in the database. We also remove it
352 // when it's submitted.
353 $value_field_name = substr($field_name,4);
356 $attributes = "";
357 if ( isset($this->Fields[$field_name]) && is_object($this->Fields[$field_name]) ) {
358 $field = $this->Fields[$field_name];
359 $attributes = $field->RenderAttributes();
361 $field_value = (isset($this->Record->{$value_field_name}) ? $this->Record->{$value_field_name} : null);
363 switch( $what_part ) {
364 case "options":
365 $currval = $part3;
366 if ( ! isset($currval) && isset($field_value) )
367 $currval = $field_value;
368 if ( isset($field->OptionList) && $field->OptionList != "" ) {
369 $option_list = $field->OptionList;
371 else {
372 @dbg_error_log( 'editor', "DBG: Current=%s, OptionQuery: %s", $currval, $field->LookupSql );
373 $opt_qry = new PgQuery( $field->LookupSql );
374 $option_list = $opt_qry->BuildOptionList($currval, "FieldOptions: $field_name" );
375 $field->OptionList = $option_list;
377 return $option_list;
378 case "select":
379 $currval = $part3;
380 if ( ! isset($currval) && isset($field_value) )
381 $currval = $field_value;
382 if ( isset($field->OptionList) && $field->OptionList != "" ) {
383 $option_list = $field->OptionList;
385 else {
386 @dbg_error_log( 'editor', "DBG: Current=%s, OptionQuery: %s", $currval, $field->LookupSql );
387 $opt_qry = new PgQuery( $field->LookupSql );
388 $option_list = $opt_qry->BuildOptionList($currval, "FieldOptions: $field_name" );
389 $field->OptionList = $option_list;
391 return "<select class=\"entry\" name=\"$field_name\"$attributes>$option_list</select>";
392 case "checkbox":
393 switch ( $field_value ) {
394 case 'f':
395 case 'off':
396 case 'false':
397 case '':
398 case '0':
399 $checked = "";
400 break;
402 default:
403 $checked = " CHECKED";
405 return "<input type=\"hidden\" value=\"off\" name=\"$field_name\"><input class=\"entry\" type=\"checkbox\" value=\"on\" name=\"$field_name\"$checked$attributes>";
406 case "input":
407 $size = (isset($part3) ? $part3 : 6);
408 return "<input class=\"entry\" value=\"".htmlspecialchars($field_value)."\" name=\"$field_name\" size=\"$size\"$attributes>";
409 case "file":
410 $size = (isset($part3) ? $part3 : 30);
411 return "<input type=\"file\" class=\"entry\" value=\"".htmlspecialchars($field_value)."\" name=\"$field_name\" size=\"$size\"$attributes>";
412 case "money":
413 $size = (isset($part3) ? $part3 : 8);
414 return "<input class=\"money\" value=\"".htmlspecialchars(sprintf("%0.2lf",$field_value))."\" name=\"$field_name\" size=\"$size\"$attributes>";
415 case "date":
416 $size = (isset($part3) ? $part3 : 10);
417 return "<input class=\"date\" value=\"".htmlspecialchars($field_value)."\" name=\"$field_name\" size=\"$size\"$attributes>";
418 case "textarea":
419 list( $cols, $rows ) = explode( 'x', $part3);
420 return "<textarea class=\"entry\" name=\"$field_name\" rows=\"$rows\" cols=\"$cols\"$attributes>".htmlspecialchars($field_value)."</textarea>";
421 case "hidden":
422 return sprintf( "<input type=\"hidden\" value=\"%s\" name=\"$field_name\">", htmlspecialchars($field_value) );
423 case "password":
424 return sprintf( "<input type=\"password\" value=\"%s\" name=\"$field_name\" size=\"10\">", htmlspecialchars($part3) );
425 case "encval":
426 case "enc":
427 return htmlspecialchars($field_value);
428 case "submit":
429 $action = ( $this->RecordAvailable ? 'update' : 'insert' );
430 return sprintf('<input type="hidden" name="_editor_action[%s]" value="%s"><input type="submit" class="submit" name="%s" value="%s">',
431 $this->Id, $action, $this->SubmitName, $value_field_name );
432 default:
433 return str_replace( "\n", "<br />", $field_value );
438 * Render the templated component. The heavy lifting is done by the callback...
440 function Render( $title_tag = null ) {
441 @dbg_error_log( 'editor', "classEditor", "Rendering editor $this->Title" );
442 if ( $this->Template == "" ) $this->DefaultTemplate();
444 $html = sprintf('<div class="editor" id="%s">', $this->Id);
445 if ( isset($this->Title) && $this->Title != "" ) {
446 if ( !isset($title_tag) ) $title_tag = 'h1';
447 $html = "<$title_tag>$this->Title</$title_tag>\n";
450 // Stuff like "##fieldname.part## gets converted to the appropriate value
451 $replaced = preg_replace_callback("/##([^#.]+)(\.([^#.]+))?(\.([^#.]+))?##/", array(&$this, "ReplaceEditorPart"), $this->Template );
452 $html .= $replaced;
454 $html .= '</div>';
455 return $html;
459 * Write the record
460 * @param boolean $is_update Tell the write whether it's an update or insert. Hopefully it
461 * should be able to figure it out though.
463 function Write( $is_update = null ) {
464 global $c, $component;
466 @dbg_error_log( 'editor', 'DBG: Writing editor %s', $this->Title);
468 if ( !isset($is_update) ) {
469 if ( isset( $_POST['_editor_action']) && isset( $_POST['_editor_action'][$this->Id]) ) {
470 $is_update = ( $_POST['_editor_action'][$this->Id] == 'update' );
472 else {
473 /** @todo Our old approach will not work for translation. We need to have a hidden field
474 * containing the submittype. Probably we should add placeholders like ##form##, ##script## etc.
475 * which the editor can use for internal purposes.
477 // Then we dvine the action by looking at the submit button value...
478 $is_update = preg_match( '/(save|update|apply)/i', $_POST[$this->SubmitName] );
479 dbg_error_log('WARN', $_SERVER['REQUEST_URI']. " is using a deprecated method for controlling insert/update" );
482 $this->Action = ( $is_update ? "update" : "create" );
483 $qry = new PgQuery( sql_from_post( $this->Action, $this->BaseTable, "WHERE ".$this->Where ) );
484 if ( !$qry->Exec("Editor::Write") ) {
485 $c->messages[] = "ERROR: $qry->errorstring";
486 return 0;
488 if ( $this->Action == "create" && isset($this->NewWhere) ) {
489 $this->GetRecord($this->NewWhere);
491 else {
492 $this->GetRecord($this->Where);
494 return $this->Record;