Xataface  2.0alpha2
Xataface Application Framework
 All Data Structures Namespaces Files Functions Variables Groups Pages
HistoryTool.php
Go to the documentation of this file.
1 <?php
2 import('Dataface/AuthenticationTool.php');
65 
69  var $meta_fields = array(
70  'history__id'=>array('Type'=>'int(11)', 'Extra'=>'auto_increment'),
71  'history__language'=> array('Type'=>'varchar(2)'),
72  'history__comments'=> array('Type'=>'text'),
73  'history__user'=>array('Type'=>'varchar(32)'),
74  'history__state'=>array('Type'=>'int(5)'),
75  'history__modified'=>array('Type'=>'datetime')
76  );
77 
78 
79 
94  function logRecord(&$record, $comments='', $lang=null, $state=null){
96 
97  if ( !isset($lang) ){
98  $lang = $app->_conf['lang'];
99  }
100 
101  if ( !isset($state) ){
102  $state = 0;
103  }
104 
105 
106  $fieldnames = array_keys($record->_table->fields());
107  $sql = 'select `'.implode('`,`', $fieldnames).'` from `'.$record->_table->tablename.'` where';
108  $keynames = array_keys($record->_table->keys());
109  $where_clauses = array();
110  foreach ( $keynames as $keyname){
111  $where_clauses[] = '`'.$keyname.'`=\''.addslashes($record->strval($keyname)).'\'';
112  }
113  $sql .= ' '.implode(' and ', $where_clauses);
114 
115  if ( @$app->_conf['multilingual_content'] ){
116  $db =& Dataface_DB::getInstance();
117  $sql = $db->translate_query($sql, $lang);
118  $sql = $sql[0];
119  }
120 
122  $userRecord =& $auth->getLoggedInUser();
123  if ( !isset($userRecord) ){
124  $user = null;
125  } else {
126  $user = $auth->getLoggedInUsername();
127  }
128 
129 
130  $insertsql = "insert into `".$this->logTableName($record->_table->tablename)."`
131  (`".implode('`,`', $fieldnames)."`, `history__language`,`history__comments`,`history__user`,`history__state`,`history__modified`)
132  select *, '".addslashes($lang)."','".addslashes($comments)."','".addslashes($user)."','".addslashes($state)."', NOW()
133  from (".$sql.") as t";
134 
135  $res = mysql_query($insertsql, $app->db());
136  if ( !$res ){
137  $this->updateHistoryTable($record->_table->tablename);
138  $res = mysql_query($insertsql, $app->db());
139  }
140  if ( !$res ){
141  echo $insertsql;
142  trigger_error(mysql_error($app->db()), E_USER_ERROR);
143  }
144 
145  // Now for the individual fields
146  $hid = mysql_insert_id($app->db());
147  foreach ($fieldnames as $fieldname){
148  $this->logField($record, $fieldname, $hid);
149  }
150 
151  return $hid;
152 
153 
154  }
155 
166  function logField(&$record, $fieldname, $history_id){
167  $field =& $record->_table->getField($fieldname);
168  $s = DIRECTORY_SEPARATOR;
169  switch(strtolower($field['Type'])){
170  case 'container':
171  $savepath = $field['savepath'];
172  if ( $savepath{strlen($savepath)-1} != $s ) $savepath.=$s;
173  if ( !$record->val($fieldname) ) break; // there is no file currently stored in this field.
174  if ( !is_readable($savepath.$record->val($fieldname)) ) break; // the file does not exist
175  if ( !file_exists($savepath) || !is_dir($savepath) )
176  trigger_error(
177  df_translate(
178  'scripts.Dataface.HistoryTool.logField.ERROR_CONTAINER_FIELD_SAVEPATH_MISSING',
179  "Field {$fieldname} is a Container field but its corresponding savepath {$savepath} does not exist. Please create the directory {$savepath} and ensure that it is writable by the web server",
180  array('fieldname'=>$fieldname,'savepath'=>$savepath)
181  ), E_USER_ERROR);
182  $histpath = $savepath.'.dataface_history'.$s;
183  if ( !file_exists($histpath) ){
184  $res = mkdir($histpath, 0777);
185  if ( !$res ) trigger_error(
186  df_translate(
187  'scripts.Dataface.HistoryTool.logField.ERROR_FAILED_TO_MAKE_HISTORY_FOLDER',
188  "Failed to make history folder {$histpath} to store the history for container field {$fieldname} in table {$record->_table->tablename}. It could be a permissions problem. Please ensure that the {$savepath} directory is writable by the web server.",
189  array('histpath'=>$histpath,'fieldname'=>$fieldname,'tablename'=>$record->_table->tablename,'savepath'=>$savepath)
190  ), E_USER_ERROR);
191 
192  }
193  if ( !is_dir($histpath) ){
194  trigger_error(
195  df_translate(
196  'scripts.Dataface.HistoryTool.logField.ERROR_NOT_A_DIRECTORY',
197  "The history path for the field {$fieldname} in table {$record->_table->tablename} is not a directory. Perhaps a file has been uploaded with the reserved name '.history'. Please delete this file to allow Dataface's history feature to work properly.",
198  array('fieldname'=>$fieldname, 'tablename'=>$record->_table->tablename)
199  ), E_USER_ERROR);
200  }
201  if ( !is_writable($histpath) ){
202  trigger_error("The history folder for field {$fieldname} of table {$record->_table->tablename} is not writable by the web server. Please make it writable by the web server for Dataface's history feature to work properly.", E_USER_ERROR);
203  }
204 
205 
206  $destpath = $histpath.$history_id;
207  if ( file_exists($destpath) ) { return;} // the file already exists... just skip it.
208 
209  $srcpath = $savepath.$record->val($fieldname);
210  $res = copy($srcpath,$destpath);
211 
212  break;
213 
214 
215 
216  }
217  }
218 
219 
227  return $tablename.'__history';
228  }
229 
236  $name = $this->logTableName($tablename);
237 
238  //first check to see if the table exists
239  if ( mysql_num_rows(mysql_query("show tables like '".$name."'", $app->db())) == 0 ){
241  }
242 
243  $res = mysql_query("show columns from `".$this->logTableName($tablename)."`", $app->db());
244  if ( !$res ) trigger_error(mysql_error($app->db()), E_USER_ERROR);
245  $history_fields = array();
246  while ( $row = mysql_fetch_assoc($res) ){
247  $history_fields[$row['Field']] = $row;
248  }
249  @mysql_free_result($res);
250 
252  $fieldnames = array_keys($table->fields());
253 
254  foreach ($fieldnames as $fieldname){
255  if ( !isset($history_fields[$fieldname]) ){
256  $field =& $table->getField($fieldname);
257  $type = (( strcasecmp($field['Type'],'container') === 0 ) ? 'varchar(64)' : $field['Type'] );
258  $sql = "alter table `".$name."` add column `".$fieldname."` {$type} DEFAULT NULL";
259  $res = mysql_query($sql, $app->db());
260  if ( !$res ){
261  trigger_error(mysql_error($app->db()), E_USER_ERROR);
262  }
263  unset($field);
264  }
265  }
266 
268 
269  foreach ( array_keys($meta_fields) as $fieldname){
270  if ( !isset($history_fields[$fieldname]) ){
271  $sql = "alter table `".$name."` add column `".$fieldname."` ".$history_fields[$fieldname]['Type'].' '.@$history_fields[$fieldname]['Extra'];
272  $res = mysql_query($sql, $app->db());
273  if ( !$res ){
274  trigger_error(mysql_error($app->db()), E_USER_ERROR);
275  }
276 
277  }
278  }
279 
280  }
281 
282 
290  $sql = "create table `".$this->logTableName($tablename)."` (
291  `history__id` int(11) auto_increment NOT NULL,
292  `history__language` varchar(2) DEFAULT NULL,
293  `history__comments` text default null,
294  `history__user` varchar(32) default null,
295  `history__state` int(5) default 0,
296  `history__modified` datetime,";
297 
299  $res = df_q("SHOW TABLE STATUS LIKE '".addslashes($tablename)."'");
300  $status = mysql_fetch_assoc($res);
301  $charset = substr($status['Collation'],0, strpos($status['Collation'],'_'));
302  $collation = $status['Collation'];
303  $fieldnames = array_keys($table->fields());
304  $fielddefs = array();
305  foreach ( $fieldnames as $fieldname){
306  $field =& $table->getField($fieldname);
307  $type = (( strcasecmp($field['Type'],'container') === 0 ) ? 'varchar(64)' : $field['Type'] );
308  $fielddefs[] = "`".$fieldname."` ".$type;
309  unset($field);
310  }
311 
312  $sql .= implode(",\n",$fielddefs);
313  $sql .= ",
314  PRIMARY KEY (`history__id`),
315  KEY prikeys using hash (`".implode('`,`', array_keys($table->keys()))."`),
316  KEY datekeys using btree (`history__modified`)) ".(@$status['Engine'] ? "ENGINE=".$status['Engine']:'')." ".($charset ? "DEFAULT CHARSET=".$charset.($collation ? " COLLATE $collation":''):'');
317 
318  $res = mysql_query($sql, $app->db());
319  if ( !$res ){
320  trigger_error(mysql_error($app->db()), E_USER_ERROR);
321  }
322  }
323 
324 
336  function getDiffs($tablename, $id1, $id2=null, $fieldname=null ){
337  import('Text/Diff.php');
338  import('Text/Diff/Renderer/inline.php');
339  $htablename = $tablename.'__history';
340  if ( !Dataface_Table::tableExists($htablename) )
341  return PEAR::raiseError(
342  df_translate('scripts.Dataface.HistoryTool.getDiffs.ERROR_HISTORY_TABLE_DOES_NOT_EXIST',
343  "History table for '{$tablename}' does not exist, so we cannot obtain changes for records of that table.",
344  array('tablename'=>$tablename)
345  ), DATAFACE_E_ERROR);
346 
347  $rec1 = df_get_record($htablename, array('history__id'=>$id1));
348 
349  if ( !isset($id2) ){
350  // The 2nd id wasn't provided so we assume we want to know the diffs
351  // against the current state of the record.
353  $query = $rec1->strvals(array_keys($table->keys()));
354  $io = new Dataface_IO($tablename);
355  $io->lang = $rec1->val('history__language');
356  $rec2 = new Dataface_Record($tablename, array());
357  $io->read($query, $rec2);
358  } else {
359  $rec2 = df_get_record($htablename, array('history__id'=>$id2));
360  }
361 
362  $vals1 = $rec1->strvals();
363  $vals2 = $rec2->strvals();
364 
365  $vals_diff = array();
366  $renderer = new Text_Diff_Renderer_inline();
367  foreach ($vals2 as $key=>$val ){
368  $diff = new Text_Diff(explode("\n", @$vals1[$key]), explode("\n", $val));
369 
370  $vals_diff[$key] = $renderer->render($diff);
371  }
372 
373  $diff_rec = new Dataface_Record($htablename, $vals_diff);
374  if ( isset($fieldname) ) return $diff_rec->val($fieldname);
375  return $diff_rec;
376 
377 
378  }
379 
380 
393  function getDiffsByDate(&$record, $date1, $date2=null, $lang=null, $fieldname=null){
394  if ( !isset($date2) ) $date2 = date('Y-m-d H:i:s');
395  $time1 = strtotime($date1);
396  $time2 = strtotime($date2);
397  if ( $time1 > $time2 ){
398  $temp = $date2;
399  $date2 = $date1;
400  $date1 = $temp;
401  }
403  if ( !isset($lang) ) $lang = $app->_conf['lang'];
404  $htablename = $record->_table->tablename.'__history';
405  if ( !Dataface_Table::tableExists($htablename) )
406  return PEAR::raiseError(
407  df_translate('scripts.Dataface.HistoryTool.getDiffs.ERROR_HISTORY_TABLE_DOES_NOT_EXIST',
408  "History table for '{$tablename}' does not exist, so we cannot obtain changes for records of that table.",
409  array('tablename'=>$tablename)
410  ), DATAFACE_E_ERROR);
411  $clauses = array();
412  $keyvals = $record->strvals(array_keys($record->_table->keys()));
413  foreach ($keyvals as $key=>$val){
414  $clauses[] = "`{$key}`='".addslashes($val)."'";
415  }
416  $clauses[] = "`history__language`='".addslashes($lang)."'";
417 
418  $sql = "select `history__id` from `{$htablename}` where ".implode(' and ',$clauses);
419  $sql1 = $sql . " and `history__modified` <= '".addslashes($date1)."' order by `history__modified` desc limit 1";
420  $sql2 = $sql . " and `history__modified` <= '".addslashes($date2)."' order by `history__modified` desc limit 1";
421 
422  $res2 = mysql_query($sql2, $app->db());
423  if ( !$res2 ){
424  //echo $sql2;
425  trigger_error(mysql_error($app->db()), E_USER_ERROR);
426  }
427  if ( mysql_num_rows($res2) == 0 ){
428  if (isset($fieldname) ) return '';
429  else return new Dataface_Record($htablename, array());
430  }
431  list($id2) = mysql_fetch_row($res2);
432  @mysql_free_result($res2);
433 
434  $res1 = mysql_query($sql1, $app->db());
435  if ( !$res1 ){
436  //echo $sql1;
437  trigger_error(mysql_error($app->db()), E_USER_ERROR);
438  }
439  if ( mysql_num_rows($res1) == 0 ){
440  $rec = df_get_record($htablename, array('history__id'=>$id2));
441  if ( !isset($rec) )
442  trigger_error(
443  df_translate(
444  'scripts.Dataface.HistoryTool.getDiffsByDate.ERROR_FAILED_TO_LOAD_HISTORY_RECORD',
445  "Failed to load history record with id {$id2}",
446  array('id'=>$id2)
447  ), DATAFACE_E_ERROR);
448  if ( isset($fieldname) ) return $rec->val($fieldname);
449  return $rec;
450  }
451  list($id1) = mysql_fetch_row($res1);
452  @mysql_free_result($res1);
453  $out = $this->getDiffs($record->_table->tablename, $id1, $id2, $fieldname);
454  return $out;
455 
456  }
457 
458 
462  function restore(&$record, $id, $fieldname=null, $secure=false){
464  if ( isset($fieldname) ) $fieldnames = array($fieldname);
465  else $fieldnames = array_keys($record->_table->fields());
466  if ( $secure ){
467  $tmp = array();
468  foreach ($fieldnames as $k=>$f){
469  $fld = $record->table()->getField($f);
470  if ( @$fld['encryption'] ) continue;
471  if ( $record->checkPermission('edit', array('field'=>$f)) ){
472  $tmp[] = $f;
473  }
474  }
475  $fieldnames = $tmp;
476  }
477  $htablename = $record->_table->tablename.'__history';
478  $res = mysql_query("select `".implode('`,`', $fieldnames)."`,`history__language` from `{$htablename}` where `history__id`='".addslashes($id)."'", $app->db());
479  if ( !$res ) trigger_error(mysql_error($app->db()), E_USER_ERROR);
480  if ( mysql_num_rows($res) == 0 )
481  return PEAR::raiseError(
482  df_translate(
483  'scripts.Dataface.HistoryTool.restore.ERROR_NO_SUCH_RECORD',
484  "Could not restore record with id {$id} in table {$htablename} because no such record exists. Perhaps the history was cleaned out.",
485  array('id'=>$id, 'tablename'=>$htablename)
486  ), DATAFACE_E_ERROR);
487  $vals = mysql_fetch_assoc($res);
488  @mysql_free_result($res);
489  $old_record = new Dataface_Record($record->_table->tablename, $record->getValues());
490  $lang = $vals['history__language'];
491  unset($vals['history__language']);
492  if ( isset($fieldname) ){
493  $record->setValue($fieldname, $vals[$fieldname]);
494  } else {
495  $record->setValues($vals);
496  }
497  $record->save($lang, $secure);
498  foreach ($fieldnames as $fld){
499  $this->restoreField($record,$old_record, $id, $fld);
500  }
501  return true;
502 
503  }
504 
515  function restoreField(&$record, &$old_record, $id, $fieldname){
517  $htablename = $record->_table->tablename.'__history';
518  $field =& $record->_table->getField($fieldname);
519  switch (strtolower($field['Type'])){
520  case 'container':
521  $savepath = $field['savepath'];
522  if ( $savepath{strlen($savepath)-1} != '/' ) $savepath .= '/';
523 
524  if ( $old_record->val($fieldname) ){
525  // we need to delete the existing file
526  $filepath = $savepath.basename($old_record->val($fieldname));
527  if ( file_exists($filepath) ) unlink($filepath);
528  }
529 
530  $hsavepath = $savepath.'.dataface_history/'.$id;
531  if ( !file_exists($hsavepath) || !is_readable($hsavepath) ) return false;
532  $filename = basename($record->val($fieldname));
533  $filepath = $savepath.'/'.$filename;
534  while ( file_exists($filepath) ){
535  $filepath = $savepath.'/'.strval(rand(0,10000)).'_'.$filename;
536  }
537  return copy($hsavepath, $filepath);
538 
539  }
540 
541  }
542 
550  function restoreToDate(&$record, $date, $lang=null, $fieldname=null){
552  $id = $this->getPreviousVersion($record, $date, $lang, $fieldname, true);
553  return $this->restore($record, $id, $fieldname);
554  /*
555  $sql = "select * from `{$record->_table->_tablename}__history}` where `history__id` = '{$id}' limit 1";
556 
557  $res = mysql_query($sql, $app->db());
558  if ( !$res ) trigger_error(mysql_error($app->db()), E_USER_ERROR);
559  if ( mysql_num_rows($res) == 0 ){
560  return PEAR::raiseError("Attempt to restore record \"{$record->getTitle()}\" to nonexistent history record with id '{$id}'", DATAFACE_E_ERROR);
561  }
562  $row = mysql_fetch_assoc($res);
563  @mysql_free_result($res);
564  $old_record = new Dataface_Record($record->_table->tablename, $record->getValues());
565  $record->setValues($row);
566  $res = $record->save($lang);
567  foreach ($fieldnames as $fld){
568  $this->restoreField($record, $old_record, $id, $fld);
569  }
570 
571  return $res;
572  */
573 
574 
575  }
576 
587  function getPreviousVersion(&$record, $date, $lang=null, $fieldname=null, $idonly=false){
589  if ( !isset($lang) ) $lang = $app->_conf['lang'];
590  $htablename = $record->_table->tablename.'__history';
591  if ( !Dataface_Table::tableExists($htablename) )
592  return PEAR::raiseError(
593  df_translate(
594  'scripts.Dataface.HistoryTool.getDiffs.ERROR_HISTORY_TABLE_DOES_NOT_EXIST',
595  "History table for '{$record->_table->tablename}' does not exist, so we cannot obtain changes for records of that table.",
596  array('tablename'=>$record->_table->tablename)
597  ), DATAFACE_E_ERROR);
598  $clauses = array();
599  $keyvals = $record->strvals(array_keys($record->_table->keys()));
600  foreach ($keyvals as $key=>$val){
601  $clauses[] = "`{$key}`='".addslashes($val)."'";
602  }
603  $clauses[] = "`history__language`='".addslashes($lang)."'";
604 
605  $sql = "select `history__id` from `{$htablename}` where ".implode(' and ',$clauses)."
606  and `history__modified` <= '".addslashes($date)."' order by `history__modified` desc limit 1";
607 
608  $res = mysql_query($sql, $app->db());
609  if ( !$res ) trigger_error(mysql_error($app->db()), E_USER_ERROR);
610  if ( mysql_num_rows($res) == 0 ){
611  return null;
612  }
613  list($id) = mysql_fetch_row($res);
614  @mysql_free_result($res);
615  if ( $idonly ) return $id;
616  $out = $this->getRecordById($record->_table->tablename, $id);
617 
618  if ( isset($fieldname) ) return $out->val($fieldname);
619  return $out;
620 
621 
622 
623  }
624 
631  function getRecordById($tablename, $id){
632  $htablename = $tablename.'__history';
633  if ( !Dataface_Table::tableExists($htablename) )
634  return PEAR::raiseError(
635  df_translate(
636  'scripts.Dataface.HistoryTool.getDiffs.ERROR_HISTORY_TABLE_DOES_NOT_EXIST',
637  "History table for '{$tablename}' does not exist, so we cannot obtain changes for records of that table.",
638  array('tablename'=>$tablename)
639  ), DATAFACE_E_ERROR);
640 
641  $rec = df_get_record($htablename, array('history__id'=>$id));
642  return $rec;
643  }
644 
645 
654  function getHistoryLog(&$record, $lang=null, $limit=100){
656  $history_tablename = $record->_table->tablename.'__history';
657  if ( !Dataface_Table::tableExists($history_tablename) ) return array();
658  $keys = $record->strvals(array_keys($record->_table->keys()));
659  $clauses = array();
660  foreach ( $keys as $key=>$val){
661  $clauses[] = "`{$key}`='".addslashes($val)."'";
662  }
663  if ( isset($lang) ) $clauses[] = "`history__language` = '".addslashes($lang)."'";
664  $where = implode(' and ', $clauses);
665  if ( isset($limit) ) $limit = "LIMIT $limit";
666  else $limit = '';
667 
668  $sql = "select `".implode('`,`', array_keys($this->meta_fields))."` from `{$history_tablename}` where {$where} order by `history__modified` desc {$limit}";
669  //echo $sql;
670  $res = mysql_query($sql, $app->db());
671  if ( !$res ) trigger_error(mysql_error($app->db()), E_USER_ERROR);
672  $out = array();
673  while ( $row = mysql_fetch_assoc($res) ) $out[] = $row;
674  @mysql_free_result($res);
675  return $out;
676 
677 
678 
679 
680  }
681 
682 
687  function findMatchingSnapshots($record, $query, $idsOnly=true){
689  $htablename = $record->_table->tablename.'__history';
690  if ( !Dataface_Table::tableExists($htablename) ) return array();
691  $keys = $record->strvals(array_keys($record->_table->keys()));
692  foreach ($keys as $key=>$val){
693  $query[$key] = '='.$val;
694  }
695  if ( $idsOnly ){
696  $qbuilder = new Dataface_QueryBuilder($htablename, $query);
697  $sql = $qbuilder->select(array('history__id'), $query);
698  $res = mysql_query($sql, df_db());
699  $ids = array();
700  while ( $row = mysql_fetch_row($res) ) $ids[] = $row[0];
701  @mysql_free_result($res);
702  return $ids;
703  } else {
704  return df_get_records_array($htablename, $query);
705  }
706 
707  }
708 
709 
710 
711 
712 
713 }