Location: PHPKode > projects > Calia > calia/local.inc
<?php

/**
 * Calia - Support functions and startup code.
 *
 * @package    Calia
 * @author     Andrew Gray <hide@address.com>
 * @version    CVS: $Id: local.inc,v 1.14 2010/04/27 20:30:53 blargh2015 Exp $
 *
 * Copyright 2001-2008 Andrew Gray
 *
 * This file is part of Calia.
 *
 * Calia is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Calia is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Calia; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 **/

require 'install.inc';

$GLOBALS['core-version'] = '1.5.5';
$GLOBALS['database-time'] = 0.0;
$GLOBALS['render-start'] = microtime(TRUE);

// Exception handler
function CaliaExceptionHandler($obj)
{
  if($GLOBALS['debug-mode'] === TRUE)
    {
      $backtrace = $obj->getTrace();
      $btText = '';
      foreach($backtrace as $bt)
	{
	  $btText .= 'File '.$bt['file'].', line '.$bt['line'].', calling '.$bt['function'].'(';
	  $btArgText = array();
	  foreach($bt['args'] as $arg) $btArgText[] = var_export($arg, TRUE);
	  $btText .= implode(', ', $btArgText).').<hr>';
	}

      LibHtmlMessageBoxError("<b><u>Internal Error:</u> ".$obj->getMessage()."</b>".
			     (get_class($obj) == 'dbException'?"<br><b><u>Database Error Message:</u> ".
			      $obj->InternalError()."</b>":'').
			     "<hr><b><u>From:</u></b> ".$obj->getFile().", line ".$obj->getLine().".".
			     "<br><b><u>Backtrace:</u></b><br>".$btText); //.str_replace("\n",'<br>',$obj->getTraceAsString()));
    }
  exit;
}
set_exception_handler('CaliaExceptionHandler');

// Error handler
function CaliaErrorHandler($errno, $errstr, $errfile, $errline)
{
  if($GLOBALS['debug-mode'] === TRUE)
    {
      $backtrace = debug_backtrace();  
      $btText = '';
      foreach($backtrace as $bt)
	{
	  $btText .= 'File '.$bt['file'].', line '.$bt['line'].', calling '.$bt['function'].'(';
	  $btArgText = array();
	  //foreach($bt['args'] as $arg) $btArgText[] = var_export($arg, TRUE);
	  $btText .= implode(', ', $btArgText).').<hr>';
	}

      switch($errno)
	{
	case E_ERROR:
	case E_USER_ERROR:
	case E_RECOVERABLE_ERROR:
	  LibHtmlMessageBoxError("<b><u>PHP Error:</u> $errstr</b><hr><b><u>From:</u></b> ".$errfile.", line $errline.");
	  exit;
	default:
	  //DebugPrint($errfile, $errline, $btText);
	  //print("<b><u>PHP Warning $errno:</u> $errstr</b><hr><b><u>From:</u></b> ".$errfile.", line $errline.<br><b><u>Backtrace:</b></u><br>$btText");
	  LibHtmlMessageBoxWarning("<b><u>PHP Warning $errno:</u> $errstr</b><hr><b><u>From:</u></b> ".$errfile.", line $errline.<br><b><u>Backtrace:</b></u><br>$btText");
	  break;
	}
    }
  return NULL;
}
set_error_handler('CaliaErrorHandler');

/**
 * Load and activate an object (if needed), return the object.
 */
function ModuleObject($moduleGuid)
{
  global $db;

  if(!isset($GLOBALS['modules'][$moduleGuid]['object']))
    {
      if(!isset($GLOBALS['modules'][$moduleGuid]))
	{
	  throw new Exception("Unknown module $moduleGuid was requested.", 0);
	} else {
	  require_once $GLOBALS['include-path'].'modules/'.$GLOBALS['modules'][$moduleGuid]['classFilename'];
	  $GLOBALS['modules'][$moduleGuid]['object'] = new $GLOBALS['modules'][$moduleGuid]['className']($moduleGuid, unserialize($GLOBALS['modules'][$moduleGuid]['cfgData']));
        }
    }

  return $GLOBALS['modules'][$moduleGuid]['object'];
}

// Localization.
bindtextdomain('Calia', getcwd().'/locale');
textdomain('Calia');

// Load our interface definitions.
require_once 'core_interfaces.inc';

// If we are running code from the modules directory, make sure our paths are correct.
if( (strstr($_SERVER['SCRIPT_FILENAME'], 'modules')) || (isset($_SERVER['PWD']) && (substr($_SERVER['PWD'], -7) == 'modules')))
  {
    $GLOBALS['include-path'] = '../';
  }
else
  {
    $GLOBALS['include-path'] = '';
  }

// Load CaliaLib
require_once $GLOBALS['include-path'].'lib/includes.inc';

// Load database configuration
require_once $GLOBALS['include-path'].'site.inc';

// Init DB module only at this point, since it needs special work.
// Load.
require_once $GLOBALS['include-path'].'modules/'.$dbInformation[0];

// Quotes check.
if(get_magic_quotes_gpc())
  {
    LibHtmlMessageBoxError(_('Magic Quotes is active and must be disabled for Calia to function correctly.'));
    exit;
  }

// Connect to our database.
try {
  $db = new $dbInformation[1](NULL, $dbInformation[2]);
} catch(Exception $e) {
  LibHtmlStart('System Unavailable');
  LibHtmlMessageBoxError('Unable to connect to the system database.  Please try again later.');
  LibHtmlEnd();
  exit;
}      

// Clear database configuration since we don't need it any more and to help prevent security leaks.
unset($dbInformation);

// Init some globals from our config table.
try {
  foreach($db->FetchTable('core_config') as $cfgRow)
    {
      switch($cfgRow['name'])
	{
	case 'menu-data':
	  $cData = unserialize($cfgRow['data']);
	  $GLOBALS['menu-data'][] = $cData;
	  break;
	case 'callbacks':
	  $cData = unserialize($cfgRow['data']);
	  $GLOBALS['callbacks'][$cData[0]][] = $cData[1];
	  break;
	case 'perm-group':
	  $cData = unserialize($cfgRow['data']);
	  $GLOBALS['perm-group'][$cData[1]] = $cData;
	  break;
	case 'edit-handler':
	  $cData = unserialize($cfgRow['data']);
	  $GLOBALS['edit-handler'][$cData[0]][] = $cData;
	  break;
	case 'edit-chain':
	  $cData = unserialize($cfgRow['data']);
	  $GLOBALS['edit-chain'][$cData[0]][] = $cData;
	  break;
	case 'commit-table':
	  $cData = unserialize($cfgRow['data']);    
	  $GLOBALS['commit-table'][] = $cData[0];
	  break;
	case 'commit-func':
	  $cData = unserialize($cfgRow['data']);
	  $GLOBALS['commit-func'][] = $cData;
	  break;
	case 'commit-time':
	  $cData = unserialize($cfgRow['data']);
	  $GLOBALS['commit-time'] = $cData;
	  break;
	}
    }
} catch (dbException $e) {
  LibHtmlMessageBoxError(_('The database server is not reachable.  Please contact the administrator.'));
  exit;
}

$GLOBALS['modules'] = $db->Fetch2DArray('core_modules','guid');
foreach($GLOBALS['modules'] as $modGuid=>$modData) 
{
  $tArray = array();
  foreach(explode(',', $GLOBALS['modules'][$modGuid]['classImplements']) as $c) $tArray[$c] = $c;
  $GLOBALS['modules'][$modGuid]['classImplements'] = $tArray;
  if(isset($tArray['authzInterface'])) $GLOBALS['authz-object'] = ModuleObject($modGuid);
}

// Autoloader to support other modules as needed
function __autoload($class)
{
  global $db;

  // Is it a class?
  foreach($GLOBALS['modules'] as $modRow)
    {
      if($modRow['className'] == $class)
	{
	  require_once $GLOBALS['include-path'].'modules/'.$modRow['classFilename'];
	  return;
	}
    }
  // No.  Data object?
  foreach($GLOBALS['edit-handler'] as $table=>$handlers)
    {
      foreach($handlers as $handlerRow)
	{
	  if($handlerRow[2] == $class)
	    {
	      require_once $GLOBALS['include-path'].'modules/'.$handlerRow[1];
	      return;
	    }
	}
    }

  throw new Exception("Unknown class $class was requested.", 0);
}

/**
 * Load and activate an object (if needed) by name, assuming it is a non-multiple, return the object.
 */
function ModuleObjectByName($moduleName)
{
  global $db;

  foreach($GLOBALS['modules'] as $moduleGuid=>$modData)
    if($modData['className'] == $moduleName) return ModuleObject($moduleGuid);

  throw new Exception(sprintf(_("A module named %s was requested, but is not presently installed."), $moduleName));
}

/**
 * Get an installed module GUID.  Only really works sanely on non-multiples.
 */
function ModuleGuidByName($moduleName)
{
  global $db;

  foreach($GLOBALS['modules'] as $moduleGuid=>$modData)
    if($modData['className'] == $moduleName) return $moduleGuid;

  throw new Exception(sprintf(_("A module named %s was requested, but is not presently installed."), $moduleName));
}

/**
 * Return the configuration data for module.
 */
function ModuleConfig($moduleGuid)
{
  return unserialize($GLOBALS['modules'][$moduleGuid]['cfgData']);
}  

/**
 * Return available servers.
 */
function AvailableServers()
{
  $ret = array();
  foreach($GLOBALS['modules'] as $modGuid=>$modData)
    {
      if(isset($modData['classImplements']['serverInterface']))
	{
	  $cfgData = unserialize($modData['cfgData']);
	  $ret[$modGuid] = $cfgData['_inst_title'];
	}	    
    }
  return $ret;
}

/**
 * Attempt to authenticate based on username and password. 
 *
 * @param  string        $username      Username to try
 * @param  string        $password      Unencrypted password to try
 *
 * @return boolean       TRUE if authentication was valid, FALSE if not. 
 */
function Authenticate($username, $password)
{
  global $db;

  // Try all AuthN sources
  foreach($GLOBALS['modules'] as $sourceId=>$moduleData)
    {
      if(isset($moduleData['classImplements']['authnInterface']))
	{
	  $authnObject = ModuleObject($sourceId);

	  $authnRes = $authnObject->Authenticate($username, $password);
	  if($authnRes !== FALSE)
	    {
	      // Map UIDs and GIDs
	      $authUser = GetUID($authnRes[0], $sourceId);
	      $authGroups = array();
	      foreach($authnRes[1] as $groupName)
		{
		  $gid = GetGID($groupName, $sourceId);
		  $authGroups[$gid] = $gid;
		}
	      $GLOBALS['user-guid'] = $authUser;
	      $GLOBALS['user-gids'] = $authGroups;

	      $userInfo = $authnObject->GetUserInformation($username);

	      $GLOBALS['user-name'] = $userInfo['_printed-name'];
	      $db->SetAuditSessionData($GLOBALS['user-name']);

	      return TRUE;
	    }
	}
    }
  return FALSE;
}

function DebugPrint($obj, $tableMode = FALSE)
{
  /*
  if($tableMode)
    {
      LibHtmlRenderTable(array_keys(reset($obj)), $obj);
    } 
  else 
  {*/
      //print('<pre>'); print(htmlentities(print_r($obj, TRUE))); print('</pre>'); print("<br><hr>");
      print('<pre>'); var_dump($obj); print('</pre>'); print("<br><hr>");
      /*
    }
      */
}

function GetUID($username, $authnSourceId)
{
  global $db;

  try {
    $row = $db->FetchRecordA('core_user_ids', array('authn_id'=>$authnSourceId, 'authn_data'=>$username));
    return $row['guid'];
  } catch(dbException $e) {
    $db->Insert('core_user_ids', array('authn_id'=>$authnSourceId, 'authn_data'=>$username));
    return $db->GetInsertGuid('core_user_ids', 'guid');
  }    
}

function GetGID($groupname, $authnSourceId)
{
  global $db;

  try {
    $row = $db->FetchRecordA('core_group_ids', array('authn_id'=>$authnSourceId, 'authn_data'=>$groupname));
    return $row['guid'];
  } catch(dbException $e) {
    $db->Insert('core_group_ids', array('authn_id'=>$authnSourceId, 'authn_data'=>$groupname));
    return $db->GetInsertGuid('core_group_ids', 'guid');
  }    
}

function PermsCheck($table, $itemGuid, $permLevel)
{
  //DebugPrint(array($table, $itemGuid, $permLevel));
  return $GLOBALS['authz-object']->Check($table, $itemGuid, $permLevel);
}

/**
 * Calia's start of rendering.
 *
 * @param string  $title              Page title.
 * @param array   $breadcrumbing      Array of MenuText=>Pointer URL.
 * @param string  $headerText         Header text
 *
 * @return void
 */
function DisplayStart($title, $breadcrumbing, $headerText = NULL)
{
  global $db;

  // Start our buffering, just in case we choose to abort and do something else.
  ob_start();

  register_shutdown_function('DisplayEnd');
  LibHtmlStart($title, FALSE, array($GLOBALS['BASE_URL'].'project.css'));

  foreach($breadcrumbing as $key=>$data) $breadcrumbing[$key] = $data;

  // Check commit tables.
  if(!isset($GLOBALS['commit-time'])) $GLOBALS['commit-time'] = 0;
  // FIXME: DB agnostic needed here.
  $tableMods = $db->Fetch1DArray('db_postgres.table_mod', 'tab', 'lastmod');
  $needCommit = FALSE;
  if(isset($GLOBALS['commit-table']))
    {
      foreach($GLOBALS['commit-table'] as $comTab)
	{
	  if(isset($tableMods[$comTab]))
	    {
	      $tabModTime = strtotime($tableMods[$comTab]);
	      if($tabModTime > $GLOBALS['commit-time']) $needCommit = TRUE;
	    }
	}
    }

  // Header
  print('<div id="header"><table width="100%"><tr valign="middle"><td>'.
	'<font size="+2">Calia </font><font size="-2"><i>v'.$GLOBALS['core-version'].'</i></font><br>'.
	'<font size="-2">'.implode(' >> ', array_map('LibHtmlMakeLink', $breadcrumbing, array_keys($breadcrumbing))).'</font>'.
	'</td><td align="right" class="header">'.
	sprintf(_('You are logged in as %s.'),$GLOBALS['user-name']).'<br>'.
	($needCommit?(sprintf(_('Changes are pending. %s.<br>'), LibHtmlMakeLink($GLOBALS['BASE_URL'].'commit.php', _('Commit them')))):'').
	'</td></tr></table></div>');

  // Build our menu.
  $horizMenu = array();
  $horizMenuTargets = array();
  
  foreach($GLOBALS['menu-data'] as $menuItem)
    {
      list($permsArray, $menuHierarchy, $targetUrl) = $menuItem;
      $horizMenuTargets[implode('|',$menuHierarchy)] = $menuItem;
    }
  ksort($horizMenuTargets);
  foreach($horizMenuTargets as $menuItem)
    {
      list($permsArray, $menuHierarchy, $targetUrl) = $menuItem;

      if( ($permsArray === NULL) || (PermsCheck($permsArray[0], $permsArray[1], $permsArray[2])) )
	{
	  if(!strpos($targetUrl, ':')) $targetUrl = $GLOBALS['BASE_URL'].$targetUrl;
	  $arrPtr = &$horizMenu;
	  foreach($menuHierarchy as $lev=>$itemName)
	    if($lev == (count($menuHierarchy) - 1))
	      $arrPtr[$itemName] = array($targetUrl, array());
	    else
	      {
		if(!isset($arrPtr[$itemName]))
		  $arrPtr[$itemName] = array('', array());
		$arrPtr = &$arrPtr[$itemName][1];
	      }
	}
    }

  // If there are no menu options, reject the user authentication.
  if(count($horizMenu) == 0)
    {
      ob_end_clean();
      printf('<h1>%s</h1>%s',_('No access'),_('Your account credentials are valid, but you have been assigned no rights to this system.  Please contact the administrator if you feel this is in error.'));
      exit;
    }

  // Render it.
  print('<div id="sidebar">');
  LibHtmlHorizontalMenu($horizMenu);
  print('</div><div id="content">');
  if($headerText !== NULL) print('<p class="header">'.$headerText.'</p>');
  RenderMessages();
  if(!isset($GLOBALS['pre-displayend'])) $GLOBALS['pre-displayend'] = array();
}

function DisplayEnd()
{
  global $db;
  $db->Commit();
  // Make sure we're not in CSV output mode.
  $hdrs = headers_list();
  foreach($hdrs as $hdr) if(!strncmp($hdr, "Content-Type: text/csv", 22)) exit;
  print("</div>\n");
  print('<hr><p align="center"><font size="-3"><i>'.sprintf(_('Page took %0.2fs to render, %0.2fs of which was spent waiting for the database.'), microtime(TRUE) - $GLOBALS['render-start'], $GLOBALS['database-time'])."</i></font></p>\n");
  // Output anything (like scripts and such) that was queued.
  if(isset($GLOBALS['pre-displayend'])) print(implode("\n", $GLOBALS['pre-displayend']));
  LibHtmlEnd();
}

function StoreMessage($type, $message)
{
  global $db;

  $db->Insert('core_session_data', array('user'=>$GLOBALS['user-guid'], 'ip'=>$_SERVER['REMOTE_ADDR'], 'data'=>"STORED-MESSAGE:".($type[0]).":$message"));
}

function StoreFunction($type, $file, $function, $data = NULL)
{
  global $db;

  $db->Insert('core_session_data', array('user'=>$GLOBALS['user-guid'], 'ip'=>$_SERVER['REMOTE_ADDR'], 'data'=>sprintf('STORED-FUNCTION:%s:%s:%s:%s', $type, $file, $function, $data)));
}

function RenderMessages()
{
  global $db;

  $messages = $db->Fetch1DArray('core_session_data', 'id', 'data', array('&', '&', '=', 'user', '|'.$GLOBALS['user-guid'], '=', 'ip', '|'.$_SERVER['REMOTE_ADDR'], '=L', 'data', '|STORED-MESSAGE:%'));
  if(count($messages))
    {
      $db->Delete('core_session_data', array('&', '&', '=', 'user', '|'.$GLOBALS['user-guid'], '=', 'ip', '|'.$_SERVER['REMOTE_ADDR'], '=L', 'data', '|STORED-MESSAGE:%'));
      foreach($messages as $msgString)
	{
	  switch($msgString[15])
	    {
	    case 'c':
	      LibHtmlMessageBoxConfirm(substr($msgString, 17));
	      break;
	    case 'w':
	      LibHtmlMessageBoxWarning(substr($msgString, 17));
	      break;
	    case 'e':
	      LibHtmlMessageBoxError(substr($msgString, 17));
	      break;
	    }
	}
    }

  $messages = $db->Fetch1DArray('core_session_data', 'id', 'data', array('&', '&', '=', 'user', '|'.$GLOBALS['user-guid'], '=', 'ip', '|'.$_SERVER['REMOTE_ADDR'], '=L', 'data', '|STORED-FUNCTION:%'));
  if(count($messages))
    {
      $db->Delete('core_session_data', array('&', '&', '=', 'user', '|'.$GLOBALS['user-guid'], '=', 'ip', '|'.$_SERVER['REMOTE_ADDR'], '=L', 'data', '|STORED-FUNCTION:%'));
      foreach($messages as $msgString)
	{
	  $msgParts = explode(':', $msgString);
	  require_once $GLOBALS['include-path'].$msgParts[2];	  
	  $text = call_user_func($msgParts[3], $msgParts[4]);
	  if(strlen($text))
	    {
	      switch($msgParts[1])
		{
		case 'c':
		  LibHtmlMessageBoxConfirm($text);
		  break;
		case 'w':
		  LibHtmlMessageBoxWarning($text);
		  break;
		case 'e':
		  LibHtmlMessageBoxError($text);
		  break;
		}
	    }
	}
    }
}

/**
 * Master function for handling listing, editing, and deleting records.  Handles, literally, everything.
 *
 * @param mixed   $mainGuid           mainGuid for PermsCheck (usually module GUID)
 * @param string  $tableName          Table to process.
 * @param array   $baseBC             Base breadcrumbing - edit, create, and delete will be based off this.
 * @param string  $listTitle          Title string to use in listing
 * @param boolean $defaultListAll     Default to listing all records?
 * @param string  $titleField         Field in the records to use in breadcrumbing (will be substituted for a %s in the BC)
 * @param string  $editBC             Title of breadcrumbing to use for editing an existing record - %s replaced by titleField
 * @param string  $createBC           Title of breadcrumbing to use for creating a new record
 * @param string  $delConfBC          Title of breadcrumbing to use for delete confirmations - %s replaced by titleField
 * @param string  $baseURL            Base URL that will return back to this function
 * @param array   $addRowFuncs        Additional functions each row - Edit and Delete are generated automatically.
 * @param array   $addBotFuncs        Additional functions at the bottom - Add New is generated automatically.
 */

function DataHandler($mainGuid, $tableName, $baseBC, $listTitle, $defaultListAll, $titleField, $editBC, $createBC, $delConfBC, $baseURL, $addRowFuncs = array(), $addBotFuncs = array())
{
  global $db;

  if(isset($_REQUEST['dh-mode']))
    {
      switch($_REQUEST['dh-mode'])
	{
	case 'copy':
	case 'edit':
	case 'save':
	case 'view':
	  $thisBC = $baseBC;
	  if(isset($_REQUEST['guid']) && (strlen($_REQUEST['guid'])))
	    {
	      // We do it this way because some modules have SQL operators in titleField (as opposed to just FetchRecord'ing it)
	      $thisRec = $db->Fetch1DArray($tableName, 'guid', $titleField, array('guid'=>$_REQUEST['guid']));
	      //DebugPrint(array('thisRec'=>$thisRec, 'guid'=>$_REQUEST['guid']));
	      $thisRecKeys = array_keys($thisRec);
	      $guid = reset($thisRecKeys);
	      $title = reset($thisRec);

	      if($_REQUEST['dh-mode'] == 'copy')
		{
		  $thisBC[$createBC] = $baseURL.'&dh-mode=copy&guid='.$guid;	      
		  $thisTitle = sprintf(_('%s (Copied from %s)'), $createBC, $title);
		  $saveUrl = $baseURL.'&dh-mode=save';
		} 
	      else 
		{
		  $thisBC[sprintf($editBC, $title)] = $baseURL.'&dh-mode=edit&guid='.$guid;	      
		  $thisTitle = sprintf($editBC, $title);
		  $saveUrl = $baseURL.'&dh-mode=save&guid='.$guid;
		}
	    } 
	  else 
	    {
	      $thisBC[$createBC] = $baseURL.'&dh-mode=edit';
	      $thisTitle = $createBC;
	      $guid = NULL;
	      $saveUrl = $baseURL.'&dh-mode=save';
	    }
	  
	  if($_REQUEST['dh-mode'] == 'edit' || $_REQUEST['dh-mode'] == 'copy' || $_REQUEST['dh-mode'] == 'view')
	    {
	      DisplayStart('Calia', $thisBC, $thisTitle);
	      DataHandlerEdit($mainGuid, $tableName, $guid, $saveUrl);
	      exit;
	    } 
	  else
	    {
	      DataHandlerSave($mainGuid, $tableName, $guid, array('DisplayStart', 'Calia', $thisBC, $thisTitle),
			      $saveUrl, $baseURL);
	      exit;
	    }
	  break;
	case 'delete':
	  $thisRec = $db->Fetch1DArray($tableName, 'guid', $titleField, array('guid'=>$_REQUEST['guid']));
	  $thisRecKeys = array_keys($thisRec);
	  $guid = reset($thisRecKeys);
	  $title = reset($thisRec);
	  
	  $thisBC = $baseBC;
	  $thisBC[sprintf($delConfBC, $title)] = $baseURL.'&dh-mode=delete&guid='.$guid;
	  $thisTitle = sprintf($delConfBC, $title);
	  
	  DataHandlerDelete($mainGuid, $tableName, $guid, array('DisplayStart', 'Calia', $thisBC, $thisTitle), 
			    $baseURL.'&dh-mode=delete&guid='.$guid, $baseURL);
	  exit;
	}
    }

  DisplayStart('Calia', $baseBC, $listTitle);
  $rowFunctions = $addRowFuncs;
  $botFunctions = $addBotFuncs;
  $botFunctions[_('Add New')] = array($baseURL.'&dh-mode=edit', 'c');
  $botFunctions[_('Add New Bulk')] = array($baseURL.'&dh-mode=edit&bulk=1', 'c');
  DataHandlerList($mainGuid, $tableName, $baseURL, $rowFunctions, $botFunctions, $defaultListAll);
  exit;
}

/**
 * Master function to generate a listing of data, with possible editing and such.
 *
 * @param mixed   $mainGuid           mainGuid for PermsCheck (usually module GUID)
 * @param string  $tableName          Table to process.
 * @param string  $returnUrl          URL to come back to this same spot (for filtering and CSV export)
 * @param array   $functions          Functions, in Button Title=>array(URL, PermCheckNeeded) format, where PermCheckNeeded is the permission 
 *                                    character required for this function.  Leave unset to have it always available.
 * @param string  $bottomFuncs        Functions to add at the bottom (i.e. empty row), (e.x. Add New) in the same format as $functions.
 * @param boolean $defaultAll         Start with all records listed.  For small tables this should be TRUE, for ones that can grow huge, 
 *                                    set to FALSE.
 * @param boolean $recursive          Set to TRUE if we're calling ourselves for a chain (and thus just return data, not do anything).
 */
function DataHandlerList($mainGuid, $tableName, $returnUrl, $functions = array(), $bottomFuncs = array(), $defaultAll = TRUE, $recursive = FALSE)
{
  global $db;

  //DebugPrint(array($mainGuid, $tableName, $returnUrl, $functions, $bottomFuncs, $defaultAll, $recursive));

  $dataHandlerObjs = array();
  $tableData = array();
  $searchFields = array();
  $filteredUrl = $returnUrl.'&f=1';
  $showResults = ((isset($_REQUEST['f']) && ($_REQUEST['f'] == 1)) || $defaultAll === TRUE)?TRUE:FALSE;

  $allHeaders = $allTables = $allFields = $allWhereDB = $allWhereHuman = $allCallbacks = array();

  // Go through all registered handlers, loading mods and getting search criteria
  foreach($GLOBALS['edit-handler'][$tableName] as $editHandler)
    {
      require_once $GLOBALS['include-path'].'modules/'.$editHandler[1];
      if($editHandler[3] !== NULL)
	$obj = new $editHandler[2](NULL, $editHandler[3]);
      else
	$obj = new $editHandler[2](NULL);

      $searchCrit = array(); $searchData = array();
      foreach($obj->SearchFields() as $sk=>$sf)
	{
	  $fn = 'search-'.$editHandler[2].$sk;
	  if(isset($_REQUEST[$fn])) 
	    { 
	      $searchCrit[$sk] = $_REQUEST[$fn]; 
	      $filteredUrl .= '&'.$fn.'='.htmlspecialchars($_REQUEST[$fn]);
	    }	  
	  $searchFields[$fn] = $sf;
	}

      if($showResults)
	{
	  $res = $obj->SearchExecute($searchCrit);
	  $hHeaders = $res[0];
	  $hTables = $res[1];
	  $hFields = $res[2];
	  $hWhereDB = $res[3];
	  $hWhereHuman = $res[4];
	  if(isset($res[5])) $hCallbacks = $res[5]; else $hCallbacks = array();
	}
      else
	list($hHeaders, $hTables, $hFields, $hWhereDB, $hWhereHuman, $hCallbacks) = array(array(), array(), array(), array(), array(), array());

      // Merge in results to the $allXXX variables from the $hXXX variables (all <= handler)
      $allHeaders = array_merge($allHeaders, $hHeaders);
      $allTables = array_merge($allTables, $hTables);
      $allFields = array_merge($allFields, $hFields);
      $allWhereHuman = array_merge($allWhereHuman, $hWhereHuman);
      if(count($hWhereDB))
	{
	  if(count($allWhereDB)) array_unshift($allWhereDB, '&');
	  $allWhereDB = array_merge($allWhereDB, $hWhereDB);
	}
      $allCallbacks = array_merge($allCallbacks, $hCallbacks);
      $dataHandlerObjs[] = $obj;
    }

  // Handle edit chains.
  if(isset($GLOBALS['edit-chain'][$tableName]))
    {
      foreach($GLOBALS['edit-chain'][$tableName] as $editChain)
	{
	  // Call the handler NULL.
	  list($hHeaders, $hTables, $hFields, $hWhereDB, $hWhereHuman, $hSearchFields, $hCallbacks) = DataHandlerList($mainGuid, $editChain[2], $returnUrl, array(), array(), $defaultAll, TRUE);
	  $allHeaders = array_merge($allHeaders, $hHeaders);
	  // If the callback isn't a complicated tables list (ie with other joins), drop it, as we join it below.
	  $allFields = array_merge($allFields, $hFields);
	  $allWhereHuman = array_merge($allWhereHuman, $hWhereHuman);
	  $searchFields = array_merge($searchFields, $hSearchFields);
	  if(count($hWhereDB))
	    {
	      if(count($allWhereDB)) array_unshift($allWhereDB, '&');
	      $allWhereDB = array_merge($allWhereDB, $hWhereDB);
	    }	  
	  $allCallbacks = array_merge($allCallbacks, $hCallbacks);
	  // Add in the chain table
	  //print('Merging:<table border="1"><tr><th>AllTables<th>hTables<th>Edit Chain<th>Result<tr><td>'); DebugPrint($allTables); print("<td>"); DebugPrint($hTables); print("<td>"); DebugPrint($editChain);
	  $allTables = array_merge($allTables, $hTables);
	  array_unshift($allTables, ':LO');
	  $allTables = array_merge($allTables, array('=', $editChain[0].'.'.$editChain[1], $editChain[2].'.'.$editChain[3]));
	  //print("<td>"); DebugPrint($allTables); print("<tr><td colspan=4>"); $aT = $allTables; print($db->SafeTable($aT)); print("</table>");
	}
    }

  // If we are recursing, return.
  if($recursive) return array($allHeaders, $allTables, $allFields, $allWhereDB, $allWhereHuman, $searchFields, $allCallbacks);
  
  //DebugPrint(array($allHeaders, $allTables, $allFields, $allWhereDB, $allWhereHuman, $searchFields));
  
  // This is our outer level.  Some higher level chains may have requested a field reordering or redesign.
  // Search through listed allFields, looking for table/column names.  If they appear later, remove those and their associated header.
  $fieldsToClear = array();
  foreach($allFields as $i1=>$f1)
    {
      foreach($allFields as $i2=>$f2)
	{
	  if(strstr($f1, $f2) && $i1 != $i2 && $i2 > $i1) $fieldsToClear[$i2] = $i2;
	}
    }
  foreach($fieldsToClear as $ftc)
    {
      unset($allHeaders[$ftc]);
      unset($allFields[$ftc]);
    }

  // DebugPrint(array($allHeaders, $allTables, $allFields, $allWhereDB, $allWhereHuman, $searchFields));   

  // If we had CSV requested, dump what we have so far on the floor and output.
  if( isset($_REQUEST['savecsv']) && ($_REQUEST['savecsv'] == 1))
    {
      ob_end_clean();
      if(count($allTables) == 0) $allTables[] = $tableName;
      $allData = $db->Fetch2DArraySpecific($allTables, NULL, $allFields, $allWhereDB);
      OutputCSVFile('export.csv', $allHeaders, $allData);
      exit;
    }

  // Output filter form.
  print('<p class="data-filter"><a class="data-filter" href="#" onclick="var e=document.getElementById(\'searchFilter\'); if(e.style.display == \'block\') e.style.display = \'none\'; else e.style.display = \'block\';">Filters</a>');
  // If filtered, offer to clear..
  if(count($allWhereHuman)) print('<a class="data-filter" href="'.$returnUrl.'"> (Clear)</a>');
  // Offer to save these results as CSV if we have data
  if($showResults) print('<a class="data-filter" href="'.$filteredUrl.'&savecsv=1">&nbsp;&nbsp;&nbsp;&nbsp;Save</a>');
  
  print('</p><div id="searchFilter" style="display:'.($showResults?'none':'block').'">');
  $searchData = array(); foreach($searchFields as $sk=>$sd) if(isset($_REQUEST[$sk])) $searchData[$sk] = $_REQUEST[$sk];
  FormRender($searchFields, $searchData, array('SubmitURL'=>$filteredUrl, 'SubmitLabel'=>_('Apply'), 'Caption'=>_('Search Filters')));
  print('</div>');

  $showFunctions = FALSE;
  if($showResults)
    {
      // If our table layout was REALLY simple, we never did any joins, etc., then allTables will be empty.  Add in the table we've been called with.
      if(count($allTables) == 0) $allTables[] = $tableName;
      $allFields[] = $tableName.'.guid';

      $allData = $db->Fetch2DArraySpecific($allTables, NULL, $allFields, $allWhereDB);
      foreach($allData as $rowId=>$row)
	{
	  foreach($allCallbacks as $cbField=>$cbFunc)
	    if(substr($cbField,0,1) != '*')
	      $allData[$rowId][$cbField] = call_user_func($cbFunc, $row[$cbField], FALSE);

	  $rowGuid = array_pop($allData[$rowId]);
	  if(!PermsCheck($tableName, $rowGuid, 'r'))
	    {
	      unset($allData[$rowId]);
	    } 
	  else 
	    {
	      $allData[$rowId]['Functions'] = '';

	      foreach($functions as $funcName=>$funcArray)
		{
		  if( (!isset($funcArray[1])) || (PermsCheck($tableName, $rowGuid, $funcArray[1])) )
		    {
		      $showFunctions = TRUE;
		      $allData[$rowId]['Functions'] .= LibHtmlMakeLink($funcArray[0].$rowGuid, $funcName).'&nbsp;';
		    }
		}

	      // Do our normal fields
	      if(PermsCheck($tableName, $rowGuid, 'm')) 
		{ $showFunctions = TRUE; $allData[$rowId]['Functions'] .= LibHtmlMakeLink($returnUrl.'&dh-mode=edit&guid='.$rowGuid, _('Edit')).'&nbsp;'; }
	      else
		if(PermsCheck($tableName, $rowGuid, 'r'))
		  { $showFunctions = TRUE; $allData[$rowId]['Functions'] .= LibHtmlMakeLink($returnUrl.'&dh-mode=view&guid='.$rowGuid, _('View')).'&nbsp;'; }
	      if(PermsCheck($tableName, $rowGuid, 'c')) 
		{ $showFunctions = TRUE; $allData[$rowId]['Functions'] .= LibHtmlMakeLink($returnUrl.'&dh-mode=copy&guid='.$rowGuid, _('Copy')).'&nbsp;'; }
	      if(PermsCheck($tableName, $rowGuid, 'm')) 
		{ $showFunctions = TRUE; $allData[$rowId]['Functions'] .= LibHtmlMakeLink($returnUrl.'&dh-mode=delete&guid='.$rowGuid, _('Delete')).'&nbsp;'; }	      
		
	      if(!strlen($allData[$rowId]['Functions'])) unset($allData[$rowId]['Functions']);
	    }

	  foreach($allCallbacks as $cbField=>$cbFunc)
	    if(substr($cbField,0,1) == '*')
	      {
		$res = call_user_func($cbFunc, $allData[$rowId], FALSE);
		if($res === FALSE)
		  unset($allData[$rowId]);
		else
		  $allData[$rowId] = $res;
	      }
	}
    }
  if($showFunctions) $allHeaders[] = _('Functions');
      
  // Print human-readable filter text.
  if(count($allWhereHuman))
    {
      $headerText = 'Filtered on '.implode(', ', $allWhereHuman).'.';
      print('<p class="header">'.$headerText.'</p>');
    }

  // Quick nasty kludge.
  if($tableName == 'network_devices')
    {
      unset($allHeaders[11]);
      unset($allHeaders[12]);
      unset($allHeaders[13]);
      unset($allHeaders[14]);
      foreach(array('Contact_Person','Contact_Phone','Location','Notes') as $field)
	{
	  foreach($allData as $idx=>$datum) unset($allData[$idx][$field]);
	}
    }

  if($showResults) 
    {
      LibHtmlRenderTable($allHeaders, $allData, NULL, array('Functions'));     
      printf('<p><font size="-1"><i>'._('%d records matched filters.').'</i></font></p>', count($allData));
    }

  foreach($bottomFuncs as $funcName=>$funcArray)
    {
      if( (!isset($funcArray[1])) || (PermsCheck($mainGuid, NULL, $funcArray[1])) )
	{
	  print(LibHtmlQuickButton($funcArray[0], $funcName));
	}
    }

}

/**
 * Master function for editing a record
 *
 * @param mixed   $mainGuid           mainGuid for PermsCheck (usually module GUID)
 * @param string  $tableName          Table to process.
 * @param mixed   $editGuid           Guid for editing.
 * @param string  $saveUrl            URL for saving.
 * @param string  $fieldPrefix        Field prefix, used for edit chains.
 * @param boolean $reedit             Set to TRUE if we are re-editting.
 */
function DataHandlerEdit($mainGuid, $tableName, $editGuid, $saveUrl, $fieldPrefix = '', $reedit = FALSE, $recurse = FALSE)
{
  global $db;

  $editFields = array(); $editData = array();
  
  // Go through all registered handlers, loading mods and getting edit fields.
  foreach($GLOBALS['edit-handler'][$tableName] as $editHandler)
    {
      require_once $GLOBALS['include-path'].'modules/'.$editHandler[1];
      if($editHandler[3] !== NULL)
	$obj = new $editHandler[2]($editGuid, $editHandler[3]);
      else
	$obj = new $editHandler[2]($editGuid);
      $searchCrit = array();

      list($thisData, $thisFields) = $obj->EditFields();

      // Check for GUIDPTRs.
      foreach($thisFields as $fieldKey=>$fieldData)
	{
	  if($fieldData[0] == 'guidptr')
	    {
	      $childPrefix = $fieldPrefix.$fieldData[3].':';

	      list($childData, $childFields) = DataHandlerEdit($mainGuid, $fieldData[3], $thisData[$fieldKey], $saveUrl, $childPrefix, $reedit, TRUE);
	      
	      $rD = array(); $rF = array();
	      foreach($childFields as $k=>$d)
		{
		  $rF[$childPrefix.$k] = $d;
		  $rD[$childPrefix.$k] = $childData[$k];
		}

	      $thisFields[$fieldKey] = array('section', $fieldData[1], $fieldData[2], $rF, array());
	      $thisData[$fieldKey] = $rD;
	    }
	}

      $editData = array_merge($editData, $thisData);
      $editFields = array_merge($editFields, $thisFields);
    }

  if($_REQUEST['dh-mode'] != 'copy')
    {
      $editFields['guid'] = array('hidden', 'guid', 'guid');
      $editData['guid'] = $editGuid;
    }

  // Handle edit chains.
  if($editGuid !== NULL) $rec = $db->FetchRecord($tableName, 'guid', $editGuid);

  if(isset($GLOBALS['edit-chain'][$tableName]))
    {
      foreach($GLOBALS['edit-chain'][$tableName] as $editChainId=>$editChain)
	{
	  // Call ourselves once for each field that matches, plus NULL.  We assume the edit field definitions are the same regardless of data provided.
	  if($editGuid !== NULL) $children = $db->Fetch1DArray($editChain[2], 'guid', 'guid', array($editChain[3]=>$rec[$editChain[1]])); else $children = array();

	  if(count($children) == 0) { $children[] = NULL; $ignoreData = TRUE; } else { $ignoreData = FALSE; }
	  foreach($children as $childGuid)
	    {
	      $childPrefix = $fieldPrefix.$editChain[2].':'.$childGuid.':';
	      list($childData, $childFields) = DataHandlerEdit($mainGuid, $editChain[2], $childGuid, $saveUrl, $childPrefix, $reedit, TRUE);
	      
	      $rD = array(); $rF = array();
	      foreach($childFields as $k=>$d)
		{
		  $rF[$k] = $d;
		  $rD[$k] = $childData[$k];
		}

	      $editFields[$editChain[2]] = array('dynlist', NULL, NULL, $rF, _('Add'), _('Remove'), TRUE);
	      if($ignoreData) $editData[$editChain[2]] = array(); else $editData[$editChain[2]][] = $rD;
	    }
	}
    }

  if($recurse) {
    return array($editData, $editFields);
  }

  //DebugPrint(array($editData, $editFields));

  // If re-called from edit mode, reset values.
  if($reedit) $editData = DataHandlerEditSetValues($editFields);

  $sessId = SaveSessionData(array('ef'=>$editFields));

  // If we're in bulk mode, supply the template, otherwise, render the form.
  if(isset($_REQUEST['bulk']))
    {
      if(!isset($_REQUEST['bulkdata']))
	{
	  $txt = _("# Bulk Editing Instructions\n# Copy and paste your additions, following the template provided.\n# Lines starting with a '#' are ignored in their entirety.\n# The first line is the list of fields, and must be maintained.\n# Any fields surrounded by []s are optional, and can be left blank, but all lines must have the correct number of fields.\n");	 
	  $fn = DataHandlerEditExtractFieldNames($editFields);
	  $txt .= implode(',',$fn)."\n";
	} 
      else 
	{
	  $txt = $_REQUEST['bulkdata'];
	}
      $newFields = array('bulkdata'=>array('textarea', 'Bulk Import', '', 120, 20));
      $newData = array('bulkdata'=>$txt);
      // Save fields.
      $newFields['sess_id'] = array('hidden','', '');
      $newData['sess_id'] = $sessId;
      $newFields['bulk'] = array('hidden', '', '');
      $newData['bulk'] = 1;
      FormRender($newFields, $newData, array('SubmitURL'=>$saveUrl, 'SubmitLabel'=>_('Import')));
    } 
  else 
    {
      // Save fields.
      $editFields['sess_id'] = array('hidden', '', '');
      $editData['sess_id'] = $sessId;

      // If view mode only, change.
      if($_REQUEST['dh-mode'] == 'view') 
	{
	  list($editFields, $editData) = MakeFieldsNonedit($editFields, $editData);
	} 
      else 
	{
	  // Check permissions to edit, just to be sure.
	  if(!PermsCheck($tableName, $mainGuid, 'm'))
	    {
	      if(!PermsCheck($tableName, $mainGuid, 'r'))
		{
		  HtmlRedirect($GLOBALS['BASE_URL']);
		  exit;
		}
	      // They can't edit, but can view.
	      list($editFields, $editData) = MakeFieldsNonedit($editFields, $editData);
	      $_REQUEST['dh-mode'] = 'view';
	    }
	}

      if($_REQUEST['dh-mode'] != 'view')
	FormRender($editFields, $editData, array('SubmitURL'=>$saveUrl, 'SubmitLabel'=>_('Save')));
      else
	FormRender($editFields, $editData, array());

      // Audit Log
      if(strlen($editGuid))
	print('<br><p align="right">'.LibHtmlMakeLink($GLOBALS['BASE_URL'].'audit.php?guid='.$editGuid, _('Audit Log')).'</p>');
    }
}

function DataHandlerEditSetValues($editFields)
{
  $ret = array();
  foreach($editFields as $fk=>$fd)
    {
      $ret[$fk] = FormValidateGeneric($fk, $fd);
    }
  return $ret;
}

function DataHandlerEditExtractFieldNames($editFields)
{
  //DebugPrint($editFields);
  $fn = array();
  foreach($editFields as $ek=>$ef)
    {
      if(!isset($ef['req'])) $ef[1] = '['.$ef[1].']';
      switch($ef[0])
	{
	case 'dynlist':
	  $r = DataHandlerEditExtractFieldNames($ef[3]);
	  foreach($r as $k=>$d)
	    $fn[$ek.'|'.$k] = $d;
	  //$fn = array_merge($fn, $r);
	  break;
	case 'hidden':
	  break;
	case 'select':
	  // Hack for the moment.
	  if($ek != 'sub') 
	    {
	      // Strip off _guid if it is there.
	      $fn[$ek] = $ef[1];
	    }
	  break;
	default:
	  if($ek != '[guid]' && $ek != 'guid')
	    $fn[$ek] = $ef[1];
	  break;
	}
    }
  return $fn;
}

/**
 * Master function for saving a record
 *
 * @param mixed   $mainGuid           mainGuid for PermsCheck (usually module GUID)
 * @param string  $tableName          Table to process.
 * @param mixed   $editGuid           Guid for editing.
 * @param array   $resaveCallback     Callback function if re-editing needed (generally DisplayStart with args)
 * @param string  $resaveUrl          URL for saving if we need to come back for more editing
 * @param string  $finishUrl          URL to redirect to on success.
 */
function DataHandlerSave($mainGuid, $tableName, $editGuid, $resaveCallback, $resaveUrl, $finishUrl)
{
  global $db;

  // Start a transaction in case something goes horribly, horribly wrong.
  $db->Begin();

  $reedit = FALSE;
  $editFields = array();
  $dataObjects = array();

  // Retrieve editFields
  $sessData = GetSessionData($_REQUEST['sess_id']);
  $editFields = $sessData['ef'];
  $validFields = FormValidate($editFields, array('SubmitURL'=>$resaveUrl, 'SubmitLabel'=>'Save'), TRUE, $resaveCallback);

  // If in bulk mode, extract the lines into fields and call the save helper one by one.  Start a transaction,
  // so we rollback EVERYTHING if ANYTHING is wrong.
  if(isset($_REQUEST['bulk']))
    {
      $lines = explode("\n", $_REQUEST['bulkdata']);
      // Trim lines, eliminate any comments, trailing spaces, newlines, etc.
      $ef = DataHandlerEditExtractFieldNames($editFields);
      $dataLines = array();
      foreach($lines as $lineNum=>$lineData)
	if(strlen(trim($lineData)) && ($lineData != '') && (substr($lineData,0,1) != '#')) 
	  {
	    $lineFields = explode(',', trim($lineData));
	    $lineData = array();
	    $fNum = 0;
	    foreach($ef as $ek=>$ed)
	      $lineData[$ek] = $lineFields[$fNum ++];
	    $dataLines[$lineNum] = DataHandlerSaveBulkHelper($editFields, $lineData);
	  }

      // Strip off the first line, which are our fields.
      array_shift($dataLines);

      $errorMessages = array();
      $rollback = FALSE;
      $db->Begin();
      // Go through each result.
      foreach($dataLines as $lineNum=>$dataLine)
	{
	  $dataGood = DataHandlerSaveHelper($mainGuid, $tableName, NULL, $dataLine, '');

	  // Grab saved messages, tack on line number, mark us bad, and continue (to try and catch as many errors as possible).
	  $errorMessages[$lineNum] = $db->Fetch1DArray('core_session_data', 'id', 'data', array('&', '&', '=', 'user', '|'.$GLOBALS['user-guid'], '=', 'ip', '|'.$_SERVER['REMOTE_ADDR'], '=L', 'data', '|STORED-%'));
	  $db->Delete('core_session_data', array('&', '&', '=', 'user', '|'.$GLOBALS['user-guid'], '=', 'ip', '|'.$_SERVER['REMOTE_ADDR'], '=L', 'data', '|STORED-%'));

	  if(!$dataGood) { $rollback = TRUE; $errorMessages[$lineNum][] = 'STORED-MESSAGE:e,'.sprintf(_('Data did not verify.')); }
	}

      foreach($errorMessages as $lineNum=>$msgs)
	{
	  foreach($msgs as $msg)
	    {
	      switch($msg[7])
		{
		case 'F': // STORED-FUNCTION:
		  $dParts = explode(':', $msg);
		  StoreFunction($dParts[1], $dParts[2], $dParts[3], $dParts[4]);
		  break;
		default:
		  switch($msg[15])
		    {
		    case 'c':
		      // Confirmed, no error.
		      StoreMessage('c', sprintf(_('Line %d: %s'), $lineNum, substr($msg, 17)));
		      break;
		    case 'w':
		      // Warning, continue.
		      StoreMessage('w', sprintf(_('Line %d: %s'), $lineNum, substr($msg, 17)));
		      break;
		    case 'e':
		      // Error, abort.
		  StoreMessage('e', sprintf(_('Line %d: %s'), $lineNum, substr($msg, 17)));
		  $rollback = TRUE;
		  break;
		    }
		}
	    }
	}

      if($rollback)
	{
	  $func = array_shift($resaveCallback);
	  call_user_func_array($func, $resaveCallback);
	  $db->Rollback();
	  DataHandlerEdit($mainGuid, $tableName, $editGuid, $resaveUrl, '', TRUE);
	  exit;
	}
    } 
  else 
    {
      // Non-bulk mode.
            
      // Passed basic forms validation, now do module validation.  Begin a transaction in case dataGood is FALSE.
      $db->Begin();
      $dataGood = DataHandlerSaveHelper($mainGuid, $tableName, $editGuid, $validFields, $editGuid);
      
      // If we need to reedit, do so.
      if($dataGood === FALSE)
	{      
	  // Preserve messages but rollback everything else.
	  $messages = $db->Fetch1DArray('core_session_data', 'id', 'data', array('&', '&', '=', 'user', '|'.$GLOBALS['user-guid'], '=', 'ip', '|'.$_SERVER['REMOTE_ADDR'], '=L', 'data', '|STORED-%'));
	  $db->Rollback();
	  foreach($messages as $id=>$data) StoreMessage($data[15], substr($data, 17));
	  
	  $func = array_shift($resaveCallback);
	  call_user_func_array($func, $resaveCallback);
	  
	  DataHandlerEdit($mainGuid, $tableName, $editGuid, $resaveUrl, '', TRUE);
	  exit;
	}
    }

  $db->Commit();
  LibHtmlRedirect($finishUrl);
}

/**
 * Helper function for converting bulk data lines into arrays.
 */
function DataHandlerSaveBulkHelper($editFields, $dataLine)
{
  $ret = array();
  foreach($editFields as $ek=>$ef)
    {
      switch($ef[0])
	{
	case 'dynlist':
	  $subLines = array();
	  $thisMatch = $ek.'|';
	  foreach($dataLine as $dk=>$dd)
	    if(substr($dk, 0, strlen($thisMatch)) == $thisMatch)
	      $subLines[substr($dk, strlen($thisMatch))] = $dd;
	  
	  $ret[$ek] = array(DataHandlerSaveBulkHelper($ef[3], $subLines));  // This is expected to be an array of multiple rows - we only have one, thus the array()
	  break;
	default:
	  $ret[$ek] = $dataLine[$ek];
	}
    }
  return $ret;
}

/**
 * Helper function for saving a record.  Called from DataHandlerSave to handle the recursion.
 *
 * @param mixed   $mainGuid           mainGuid for PermsCheck (usually module GUID)
 * @param string  $tableName          Table to process.
 * @param mixed   $editGuid           Guid for editing.
 * @param array   $validFields        The fields from Forms' validation function.
 * @param string  $fieldPrefix        Current field prefix to use
 *
 * @return Returns FALSE if data was bad, otherwise the new GUID.
 */
function DataHandlerSaveHelper($mainGuid, $tableName, $editGuid, $validFields, $fieldPrefix, $linkedField = '', $linkedData = '')
{
  global $db;
  
  //DebugPrint(array($mainGuid, $tableName, $editGuid, $validFields, $fieldPrefix, $linkedField, $linkedData));

  $dataGood = TRUE;
  $dataFields = array(); 
  if($editGuid == '') $editGuid = NULL;

  // Do this one.
  foreach($GLOBALS['edit-handler'][$tableName] as $editHandler)
    {
      require_once $GLOBALS['include-path'].'modules/'.$editHandler[1];
      if($editHandler[3] !== NULL)
	$obj = new $editHandler[2]($editGuid, $editHandler[3]);
      else
	$obj = new $editHandler[2]($editGuid);

      list($thisData, $thisFields) = $obj->EditFields();

      foreach($thisFields as $fieldKey=>$fieldData)
	{
	  if($fieldData[0] == 'guidptr')
	    {
	      // Find the first colon.
	      $colon = strlen($fieldData[3]) + 1;
	      $ve = array_key_string_strip($validFields[$fieldKey], $colon);
	      //$ve = $validFields[$fieldKey];
	      $ret = DataHandlerSaveHelper($mainGuid, $fieldData[3], $thisData[$fieldKey], $ve, '', $validateMode, '', '');
	      
	      if($ret === FALSE) 
		$dataGood = FALSE;
	      else
		$dataFields[$fieldKey] = $ret;
	    }
	  else
	    {
	      $dataFields[$fieldKey] = $validFields[$fieldKey];
	    }
	}

      // Set linked data
      if($linkedField != '') $dataFields[$linkedField] = $linkedData;
      $editGuid = $obj->SaveFields($dataFields);

      if($editGuid === FALSE) return FALSE;	
      if(is_array($editGuid))
	{
	  $dataGood = FALSE;
	  foreach($editGuid as $errMsg)
	    {
	      StoreMessage($errMsg[0], $errMsg[1]);
	    }
	  return FALSE;
	}
    }

  //DebugPrint(array('validFields'=>$validFields));
  // Handle edit chains
  if(isset($GLOBALS['edit-chain'][$tableName]))
    foreach($GLOBALS['edit-chain'][$tableName] as $editChainId=>$editChain)
      {
	// ValidateFields returns us an array, indexed by the editChain table (element 2).
	if(isset($validFields[$editChain[2]]) && is_array($validFields[$editChain[2]]))
	  {
	    // Go through the returned elements.
	    //if($editChain[0] == 'network_devices') $editChain[4] = TRUE;

	    $retGuids = array();
	    $curGuids = $db->Fetch1DArray($editChain[2], 'guid', 'guid', array($editChain[3]=>$editGuid));

	    foreach($validFields[$editChain[2]] as $validElement)
	      {
		$firstKey = reset(array_keys($validElement));
		// Find the second colon.
		//$colon2 = strpos($firstKey, ':', strpos($firstKey, ':', 0) + 1) + 1;		
		//$ve = array_key_string_strip($validElement, $colon2);
		$ve = $validElement;
		if(strlen($ve['guid'])) $retGuids[$ve['guid']] = $ve['guid'];
		$ret = DataHandlerSaveHelper($mainGuid, $editChain[2], $ve['guid'], $ve, '', $editChain[3], $editGuid);
		if($ret === FALSE) $dataGood = FALSE;
	      }

	    // Now see what guids WEREN'T returned that we started with, and delete.
	    foreach($curGuids as $savedGuid)
	      if(!isset($retGuids[$savedGuid]))
		$db->Delete($editChain[2], array('guid'=>$savedGuid));
	  }
      }

  if($dataGood === FALSE)
    return FALSE;
  else
    return $editGuid;
}

function array_key_string_strip($arr, $l)
{
  $ret = array();
  foreach($arr as $k=>$d)
    {
      $newKey = substr($k, $l);
      if(is_array($d))
	$ret[$newKey] = $d;
      //$ret[$newKey] = array_key_string_strip($d, $l);
      else
	$ret[$newKey] = $d;
    }
  return $ret;
}

/**
 * Master function for deleting a record
 *
 * @param mixed   $mainGuid           mainGuid for PermsCheck (usually module GUID)
 * @param string  $tableName          Table to process.
 * @param mixed   $delGuid            Guid for deleting
 * @param array   $confCallback       Callback before printing confirm box if it is needed.
 * @param string  $returnUrl          URL to return to if verification is needed (should just re-call this function)
 * @param string  $doneUrl            URL to return to on completion.
 */
function DataHandlerDelete($mainGuid, $tableName, $delGuid, $confCallback, $returnUrl, $doneUrl)
{
  global $db;

  if(isset($_REQUEST['delete-verified'])) $delVer = TRUE; else $delVer = FALSE;

  $confirmMessages = array();

  // Transaction for rolling back.
  $db->Begin();

  // Go through all registered handlers, loading mods and requesting the delete.
  foreach($GLOBALS['edit-handler'][$tableName] as $editHandler)
    {
      require_once $GLOBALS['include-path'].'modules/'.$editHandler[1];
      $obj = new $editHandler[2]($delGuid);
      $res = $obj->Delete($delVer);
      if($res !== TRUE)
	$confirmMessages[] = $res;
    }
  
  // If confirm is needed, rollback and print.
  if(count($confirmMessages))
    {
      $db->Rollback();
      $func = array_shift($confCallback);
      call_user_func_array($func, $confCallback);
      LibHtmlMessageBoxProceed(implode('<br><br>',$confirmMessages), $returnUrl.'&delete-verified=1', $doneUrl);
      exit;
    }

  $db->Commit();
  LibHtmlRedirect($doneUrl);
}

/**
 * Commonly used function to present data from the database for editting in table form.  The various permission checks go by:
 * PermsCheck($mainGuid, $dbArrayKeyGuid, $permChar).  URLs will have the row GUID tacked on the end.
 *
 * @param mixed   $mainGuid           mainGuid for PermsCheck (usually module GUID)
 * @param array   $colHeaders         Headers for the columns /without/ the end 'functions'.
 * @param array   $dbArray            Data from the database, in guid=>data format.
 * @param array   $functions          Functions, in Button Title=>array(URL, PermCheckNeeded) format, where PermCheckNeeded is the permission 
 *                                    character required for this function.  Leave unset to have it always available.
 * @param string  $bottomFuncs        Functions to add at the bottom (i.e. empty row), (e.x. Add New) in the same format as $functions.
 *
 * @return void
 */
function EasyListing($mainGuid, $colHeaders, $dbData, $functions, $bottomFuncs)
{
  $showFunctions = FALSE;

  foreach($dbData as $rowGuid=>$row)
    {
      $dbData[$rowGuid]['Functions'] = '';
      foreach($functions as $funcName=>$funcArray)
	{
	  if( (!isset($funcArray[1])) || (PermsCheck($mainGuid, $rowGuid, $funcArray[1])) )
	    {
	      $showFunctions = TRUE;
	      $dbData[$rowGuid]['Functions'] .= LibHtmlQuickButton($funcArray[0].$rowGuid, $funcName);
	    }
	}
    }

  
  if($showFunctions) $colHeaders[] = _('Functions');
  LibHtmlRenderTable($colHeaders, $dbData, NULL, TRUE);

  foreach($bottomFuncs as $funcName=>$funcArray)
    {
      if( (!isset($funcArray[1])) || (PermsCheck($mainGuid, NULL, $funcArray[1])) )
	{
	  print(LibHtmlQuickButton($funcArray[0], $funcName));
	}
    }
}

/**
 * Output a file as CSV.
 */
function OutputCSVFile($filename, $headers, $dataRows)
{
  header('Content-Type: text/csv');
  header('Content-Disposition: inline; filename="'.$filename.'"');
  $outFp = fopen('php://output', 'w');
  fputcsv($outFp, $headers); fprintf($outFp, "\r");
  foreach($dataRows as $dataRow)
    { fputcsv($outFp, $dataRow); fprintf($outFp, "\r"); }
  fclose($outFp);
}

/**
 * Our normal search execute functions all do the exact same thing, just different fields.  But this code keeps getting tweaked
 * just slightly, so this function was made to replace all of those.
 *
 * @param array   $condArray          Conditions array (the argument to SearchExecute)
 * @param string  $condFieldName      The name in condArray to work with.
 * @param string  $dbFieldName        The database field name to reference should it be needed.
 * @param string  $humanFieldName     The human field name.
 * @param array   $w                  The where array being built.
 * @param array   $h                  The human-readable filter array being built.
 * @param boolean $caseInsensitive    Whether to do matches case-insensitive.
 * @param boolean $checkStars         Handle stars (*) as wildcards.
 * @param boolean $checkCIDR          Handle CIDR notation.
 */
function SearchExecuteField($condArray, $condFieldName, $dbFieldName, $humanFieldName, &$w, &$h, $caseInsensitive = TRUE, $checkStars = TRUE, $checkCIDR = FALSE)
{
  //DebugPrint(array($condArray, $condFieldName, $dbFieldName, $humanFieldName, &$w, &$h, $caseInsensitive, $checkStars, $checkCIDR));
  if(!isset($condArray[$condFieldName]) || (strlen($condArray[$condFieldName]) == 0)) return;

  $val = $condArray[$condFieldName];

  if(count($w)) array_unshift($w, '&');

  if($checkCIDR && (strpos($val, '/') !== FALSE))
    {
      $w[] = '<<'; $w[] = $dbFieldName; $w[] = '|'.$val;
      $h[] = sprintf(_('%s inside %s'), $humanFieldName, $val);
      return;
    }
  
  if($caseInsensitive) $matchOp = '=I'; else $matchOp = '=';
  
  if($checkStars && (strpos($val, '*') !== FALSE))
    {
      $w[] = $matchOp.'L'; $w[] = $dbFieldName; $w[] = '|'.str_replace('*', '%', $val);
      $h[] = sprintf(_('%s matches %s'), $humanFieldName, $val);
      return;
    }
  
  $w[] = $matchOp; $w[] = $dbFieldName; $w[] = '|'.$val;
  $h[] = sprintf(_('%s is %s'), $humanFieldName, $val);      
}

/**
 * Schedule something to be called in the future.
 */
function ScheduleLaunch($moduleGuid, $scriptName, $time)
{
  global $db;

  $db->Insert('core_cron', array('module_guid'=>$moduleGuid, 'filename'=>$scriptName, 'next_run'=>strftime('%Y-%m-%d %H:%M:%S', $time)));
}

/**
 * Handle callbacks.
 */
function HandleCallbacks($name, $data)
{
  $c = 0;
  if(isset($GLOBALS['callbacks'][$name]))
    {
      foreach($GLOBALS['callbacks'][$name] as $cbData)
	{
	  require_once $cbData[0];
	  call_user_func($cbData[1], $data, $cbData[2]);
	  $c ++;
	}
    }
  return $c;
}

function DebugArrayTable($arr)
{
  if(!is_array($arr)) return;
  $keyArray = array();
  foreach(reset($arr) as $key=>$val) $keyArray[$key] = $key;

  LibHtmlRenderTable($keyArray, $arr, 'Key');
}

function SaveSessionData($arr)
{
  global $db;

  // Delete really old data
  $db->Delete('core_session_data', array('<', 'created', '|'.strftime('%Y-%m-%d %H:%M:%S', time() - 86400)));

  $db->Insert('core_session_data', array('user'=>$GLOBALS['user-guid'], 'data'=>serialize($arr), 'ip'=>$_SERVER['REMOTE_ADDR']));
  return $db->GetInsertId('core_session_data', 'id');
}

function GetSessionData($id)
{
  global $db;

  $rec = $db->FetchRecord('core_session_data', 'id', $id);
  if($rec['user'] != $GLOBALS['user-guid']) exit;

  //$db->Delete('core_session_data', array('id'=>$id));
  return unserialize($rec['data']);
}

function ReallyShortDuration($curValue)
{
  if( ($curValue / 60) > 1)
    if( ($curValue / 3600) > 1)
      if( ($curValue / 86400) > 1)      
        $ret = sprintf('%ud %02uh',($curValue / 86400),($curValue % 86400) / 3600);
      else
        $ret = sprintf('%uh %02um',($curValue % 86400) / 3600,($curValue % 3600) / 60);
    else
      $ret = sprintf('%um %02us',($curValue % 3600) / 60,($curValue % 60));
  else
    $ret = sprintf('%us',($curValue % 60));

  return $ret;
}

function TimestampToSecs($value)
{
  $vParts = explode(' ', $value);
  $dParts = explode('-', $vParts[0]);
  $tParts = explode(':', $vParts[1]);
  return @mktime($tParts[0], $tParts[1], $tParts[2], $dParts[1], $dParts[2], $dParts[0]);
}

function array_merge_norenumber($a1, $a2)
{
  $ret = array();
  if(is_array($a1)) { foreach($a1 as $key=>$values) $ret[$key] = $values; }
  if(is_array($a2)) { foreach($a2 as $key=>$values) if(isset($ret[$key])) $ret[$key] = array_merge($ret[$key], $values); else $ret[$key] = $values; }

  return $ret;
}

function TorFImage($val, $csv)
{
  switch($val)
    {
    case 't':
      return 'Yes';
    case 'f':
      return 'No';
    default:
      return $val;
    }
}

function GetWorkDirectory()
{
  $name = tempnam(sys_get_temp_dir(), 'CAL');
  unlink($name);
  mkdir($name, 0700);
  return $name;
}

function MakeFieldsNonedit($fields, $data)
{
  $newFields = array(); $newData = array(); $fieldInc = 0;

  foreach($fields as $fK=>$fD)
    {
      $fN = 'f'.$fieldInc;
      $newFields[$fN] = array('nonedit', $fD[1], $fD[2]);
      $newData[$fN] = $data[$fK];

      switch($fD[0])
	{
	case 'section':
	  $newFields[$fN][0] = 'section';
	  $newFields[$fN][4] = array();
	  list($newFields[$fN][3], $newData[$fN]) = MakeFieldsNonedit($fD[3], $data[$fK]);
	  break;
	case 'dynlist':
	  unset($newFields[$fN]); unset($newData[$fN]);
	  
	  foreach($data[$fK] as $i=>$thisData)
	    {
	      $fN = 'f'.$fieldInc.'-'.$i;
	      $newFields[$fN] = array('section', $fD[1], $fD[2], array(), array());
	      list($newFields[$fN][3], $newData[$fN]) = MakeFieldsNonedit($fD[3], $thisData);
	    }
	  break;
	case 'duration':
	  $newData[$fN] = ReallyShortDuration($data[$fK]);
	  break;
	case 'select':
	  $newData[$fN] = $fields[$fK][3][$data[$fK]];
	  break;
	case 'hidden':
	  unset($newFields[$fN]); unset($newData[$fN]);
	  break;
	case 'checkbox':
	  $curValue = $data[$fK];
	  if( ($curValue === 'N') || ($curValue === 0) || ($curValue === 'FALSE') || ($curValue === 'f') || ($curValue === 'false') ||
	      ($curValue === FALSE) || ($curValue === '') || ($curValue === NULL)) $newData[$fN] = _('No'); else $newData[$fN] = _('Yes');
	  break;
	default:
	  break;
	}
      $fieldInc ++;
    }
  return array($newFields, $newData);
}

function GetBacktrace()
{
  $backtrace = debug_backtrace();
  $btText = '';
  foreach($backtrace as $bt)
    {
      $btText .= 'File '.$bt['file'].', line '.$bt['line'].', calling '.$bt['function'].'(';
      $btArgText = array();
      foreach($bt['args'] as $arg) $btArgText[] = var_export($arg, TRUE);
      $btText .= implode(', ', $btArgText).').<hr>';
    }

  return $btText;
}

if ( !function_exists('sys_get_temp_dir') )
{
    // Based on http://www.phpit.net/
    // article/creating-zip-tar-archives-dynamically-php/2/
    function sys_get_temp_dir()
    {
      return('/tmp/');
    }
}

// If we have authentication data from Apache, try it.
$validUser = FALSE;
if(isset($_SERVER['PHP_AUTH_USER']))
  {
    $validUser = Authenticate($_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW']);
  }
if(!$validUser)
  {
    // MUST figure out a better way to do this (for command line utils)
    if(isset($GLOBALS['bypass-auth'])) return;

    header('WWW-Authenticate: Basic realm="'.gettext('Calia Management System').'"');
    header('HTTP/1.0 401 Unauthorized');
    print("<HTML><HEAD>\n<TITLE>401 Authorization Required</TITLE>\n</HEAD><BODY>\n<H1>Authorization Required</H1>This server could not verify that you\nare authorized to access the document\nrequested.  Either you supplied the wrong\ncredentials (e.g., bad password), or your\nbrowser doesn't understand how to supply\nthe credentials required.<P>\n<HR>\n".$_SERVER['SERVER_SIGNATURE']."\n</BODY></HTML>");
    exit;
  }
Return current item: Calia